-
Notifications
You must be signed in to change notification settings - Fork 1
Create retry policies from the subscription page #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import React, {useState} from "react"; | ||
| import Modal from "src/components/common/Modal"; | ||
| import FormField from "src/components/common/forms/FormField"; | ||
| import TextEditor from "src/components/common/forms/TextEditor"; | ||
| import RetryGroupsEditor from "src/components/RetryPolicies/RetryGroupsEditor"; | ||
| import {useCreateRetryPolicyMutation} from "src/client/apis/retryPoliciesApi"; | ||
| import {RetryGroup} from "src/types/retryPolicies"; | ||
| import {apiErrorMessage} from "src/client/apis/apiError"; | ||
|
|
||
| interface Props { | ||
| /** Suggested name, so a policy made for one subscription is recognisable in the list later. */ | ||
| suggestedName?: string | ||
| onCreated: (id: number) => void | ||
| onClose: () => void | ||
| } | ||
|
|
||
| // Creates a real, named policy from wherever one is being picked, rather than sending the reader to | ||
| // the policies page and back — a subscription being edited would lose what has been typed so far. | ||
| // | ||
| // It is a named policy on purpose. The alternative used to be an inline policy stored on the | ||
| // subscription, which no page listed and no reset could reach: once its budget ran out that | ||
| // subscription stopped retrying for good. | ||
| const NewRetryPolicyModal: React.FC<Props> = ({suggestedName, onCreated, onClose}) => { | ||
|
|
||
| const [name, setName] = useState(suggestedName ?? "") | ||
| const [groups, setGroups] = useState<RetryGroup[]>([]) | ||
| const [error, setError] = useState<string>() | ||
| const [create, {isLoading}] = useCreateRetryPolicyMutation() | ||
|
|
||
| const onSubmit = async () => { | ||
| try { | ||
| const result = await create({name, groups}).unwrap() | ||
| onCreated(result.id) | ||
| } catch (e) { | ||
| // Kept open with the groups intact: the server rejects a group that could never fire, and | ||
| // that is worth fixing here rather than starting again. | ||
| setError(apiErrorMessage(e, "Could not create this policy.")) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <Modal onClose={onClose} submitLabel={"Create"} onSubmit={onSubmit} | ||
| submitDisabled={!name.trim() || isLoading}> | ||
| {error && | ||
| <p className={"text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2 mb-3"}> | ||
| {error} | ||
| </p>} | ||
|
|
||
| <FormField title="Name" | ||
| tooltip="Shown wherever this policy can be selected, so name it after what it is for rather than after one subscription."> | ||
| <TextEditor value={name} onChange={setName}/> | ||
| </FormField> | ||
|
|
||
| <p className={"text-xs text-gray-500 mt-2"}> | ||
| Creating it here selects it for this subscription. It can be reused by others, and | ||
| edited later on the retry policies page. | ||
| </p> | ||
|
|
||
| <div className={"mt-3"}> | ||
| <RetryGroupsEditor title={"Groups"} groups={groups} onChange={setGroups}/> | ||
| </div> | ||
| </Modal> | ||
| ); | ||
| } | ||
|
|
||
| export default NewRetryPolicyModal; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ import { NATIVE_JSON_MAPPER_ID } from "src/types/mapping"; | |
| import SubscriptionSelector from "./Subscriptions/SubscriptionSelector"; | ||
| import ScheduleEditor from "./Subscriptions/ScheduleEditor"; | ||
| import RetryPolicySelector from "src/components/RetryPolicies/RetryPolicySelector"; | ||
| import RetryGroupsEditor from "src/components/RetryPolicies/RetryGroupsEditor"; | ||
| import NewRetryPolicyModal from "src/components/RetryPolicies/NewRetryPolicyModal"; | ||
| import SubscriptionFilter from "src/components/Subscriptions/SubscriptionFilter"; | ||
| import {TrailBaseModel} from "src/types/trail"; | ||
| import TrialsViewModal from "src/components/common/trails/trialsViewModal"; | ||
|
|
@@ -56,7 +56,6 @@ const Component = () => { | |
| const [openModal, setOpenModal] = useState<"NONE" | "TRAIL" | "CREATE_DRAFT">("NONE"); | ||
| const [subscriptionTrail, setSubscriptionTrail] = useState<TrailBaseModel[]>([]); | ||
| const [updateSubscriptionData, setUpdateSubscriptionData] = useState<ISubscription>({}) | ||
| const [retryPolicyMode, setRetryPolicyMode] = useState<"NONE" | "NAMED" | "CUSTOM">("NONE"); | ||
| const savedDataRef = useRef<string>('{}'); | ||
| const { workGroupsAvailable } = useTypedSelector(state => state.features); | ||
| const subscriptionCategories = useSubscriptionCategoriesQuery({limit: 1000, offset: 0}) | ||
|
|
@@ -70,6 +69,7 @@ const Component = () => { | |
| const [publishDraft] = usePublishDraftSubscriptionMutation() | ||
| const [receiveNow] = useReceiveSubscriptionMutation() | ||
| const [mode, setMode] = useState<EditMode>("PUBLISHED") | ||
| const [creatingPolicy, setCreatingPolicy] = useState(false) | ||
| const mapperMetadata = useAdapterMetadataQuery(updateSubscriptionData.mapperId, {skip: !updateSubscriptionData.mapperId}) | ||
| const handlerMetadata = useAdapterMetadataQuery(updateSubscriptionData.handlerId, {skip: !updateSubscriptionData.handlerId}) | ||
| const receiverMetadata = useAdapterMetadataQuery(updateSubscriptionData.receiverId, {skip: !updateSubscriptionData.receiverId}) | ||
|
|
@@ -89,11 +89,9 @@ const Component = () => { | |
| }); | ||
| setUpdateSubscriptionData(normalized); | ||
| savedDataRef.current = JSON.stringify(normalized); | ||
| setRetryPolicyMode(normalized.customRetryPolicy ? "CUSTOM" : normalized.retryPolicyId ? "NAMED" : "NONE"); | ||
| } else { | ||
| setUpdateSubscriptionData({}); | ||
| savedDataRef.current = '{}'; | ||
| setRetryPolicyMode("NONE"); | ||
| } | ||
| }, [subscriptionData, id]); | ||
|
|
||
|
|
@@ -153,21 +151,21 @@ const Component = () => { | |
|
|
||
| if (!updateSubscriptionData) return <></> | ||
| const subscriptionType = normalizeSubscriptionType(updateSubscriptionData?.type); | ||
| const onChangeRetryPolicyMode = (mode: string) => { | ||
| setRetryPolicyMode(mode as typeof retryPolicyMode) | ||
| if (mode === "NAMED") { | ||
| onChangeSubscriptionData("customRetryPolicy", null) | ||
| } else if (mode === "CUSTOM") { | ||
| onChangeSubscriptionData("retryPolicyId", null) | ||
| onChangeSubscriptionData("customRetryPolicy", updateSubscriptionData.customRetryPolicy ?? {groups: []}) | ||
| } else { | ||
| onChangeSubscriptionData("retryPolicyId", null) | ||
| onChangeSubscriptionData("customRetryPolicy", null) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex flex-col w-full "> | ||
| {creatingPolicy && | ||
| <NewRetryPolicyModal | ||
| suggestedName={updateSubscriptionData.name} | ||
| onClose={() => setCreatingPolicy(false)} | ||
| onCreated={(policyId) => { | ||
| setCreatingPolicy(false) | ||
| // Selected straight away, so the reader is left where they were with the thing | ||
| // they just made already chosen. Inline rules, if any, give way to it. | ||
| onChangeSubscriptionData("retryPolicyId", policyId) | ||
| onChangeSubscriptionData("customRetryPolicy", null) | ||
| }}/>} | ||
|
Comment on lines
+157
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add automated coverage for the new retry-policy flow. No test file is included in this cohort. Test successful creation, API-error retention of the entered groups, direct selector changes, and clearing As per path instructions, 🤖 Prompt for AI AgentsSource: Path instructions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair, and not done — flagging why rather than silently skipping. This repo has no component-testing setup: vitest is present, but all four existing test files cover pure logic (the Scriban helpers), and there is no jsdom or happy-dom environment and no @testing-library/react. Rendering The behaviour was verified in a real browser instead, which is the established practice here: creating a policy from the subscription page, Create staying disabled until a name is entered, and — for the sibling finding on this PR — the save payload carrying Worth doing properly as its own change: add the component-test stack, then cover this flow along with the other subscription editors that have no tests either. |
||
|
|
||
| { | ||
| openModal === "CREATE_DRAFT" && | ||
| <Dialog title={"Are you sure that you want to create a draft version for this subscription"} | ||
|
|
@@ -432,34 +430,43 @@ const Component = () => { | |
| </div> | ||
|
|
||
| <div className="bg-white border shadow-lg rounded-lg px-2 py-2"> | ||
| <FormField title="Retry Policy" className="grow w-64"> | ||
| <ChoiceEditor | ||
| value={retryPolicyMode} | ||
| onChange={onChangeRetryPolicyMode} | ||
| optionTitle={(item: OptionType) => item.title} | ||
| optionValue={(item: OptionType) => item.id} | ||
| isClearable={false} | ||
| options={[ | ||
| {id: "NONE", title: "None"}, | ||
| {id: "NAMED", title: "Named Policy"}, | ||
| {id: "CUSTOM", title: "Custom"}, | ||
| ]}/> | ||
| </FormField> | ||
|
|
||
| {retryPolicyMode === "NAMED" && | ||
| <div className={"mt-3 w-64"}> | ||
| <RetryPolicySelector | ||
| value={updateSubscriptionData.retryPolicyId?.toString()} | ||
| onChange={(v) => onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null)}/> | ||
| {/* Clearing the selector is what "no retries" means, so there is no separate mode | ||
| to choose first. Policies are always named ones: the inline alternative was | ||
| listed nowhere and its spent budget could not be reset, so a subscription that | ||
| ran out stopped retrying for good. */} | ||
| <FormField title="Retry Policy" | ||
| tooltip="The rules deciding which failures of this subscription are retried, and how many times. Leave empty for no automatic retries."> | ||
| <div className={"flex flex-row items-center gap-2"}> | ||
| <div className={"w-64"}> | ||
| <RetryPolicySelector | ||
| value={updateSubscriptionData.retryPolicyId?.toString()} | ||
| onChange={(v) => { | ||
| onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null) | ||
| // A named policy replaces inline rules rather than sitting | ||
| // beside them: the evaluator prefers a stored | ||
| // customRetryPolicy over the named one, so leaving it behind | ||
| // would keep invisible rules in charge of a subscription | ||
| // whose page says they were replaced. Cleared when the | ||
| // selector is emptied too, which is what "no retries" means. | ||
| onChangeSubscriptionData("customRetryPolicy", null) | ||
| }}/> | ||
| </div> | ||
| <Authorize roles={["Admin", "Member"]}> | ||
| <Button variant={"secondary"} onClick={() => setCreatingPolicy(true)}> | ||
| New policy… | ||
| </Button> | ||
| </Authorize> | ||
| </div> | ||
| } | ||
| </FormField> | ||
|
|
||
| {retryPolicyMode === "CUSTOM" && | ||
| <RetryGroupsEditor | ||
| title={"Groups"} | ||
| groups={updateSubscriptionData.customRetryPolicy?.groups ?? []} | ||
| onChange={(g) => onChangeSubscriptionData("customRetryPolicy", {groups: g})}/> | ||
| } | ||
| {/* Only reachable for a subscription whose inline policy was set through the API. | ||
| Saying so beats a page that shows an empty selector while retries are in fact | ||
| governed by rules it does not display. */} | ||
| {updateSubscriptionData.customRetryPolicy && | ||
| <p className={"text-xs text-amber-700 mt-2"}> | ||
| This subscription carries its own retry rules, set outside this page. They | ||
| still apply. Selecting a policy above replaces them. | ||
| </p>} | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.