Skip to content

Commit 1c3f5d9

Browse files
feat(schedules): add overlap policy cluster to advanced create form (PR09b)
Add overlap policy fields to the advanced create-schedule accordion: extend form schema, transform helper, and gRPC mapping; update horizontal field layout and styled components for the policy cluster. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dcc5972 commit 1c3f5d9

11 files changed

Lines changed: 360 additions & 114 deletions

src/views/domain-schedules/domain-schedules-create-advanced-form/__tests__/domain-schedules-create-advanced-form.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ describe(DomainSchedulesCreateAdvancedForm.name, () => {
3030
expect(screen.getByLabelText('Schedule Id')).toBeInTheDocument();
3131
expect(screen.getByLabelText('Jitter duration')).toBeInTheDocument();
3232
expect(screen.getByLabelText('Workflow Id Prefix')).toBeInTheDocument();
33+
expect(
34+
screen.getByRole('combobox', { name: /overlap policy/i })
35+
).toBeInTheDocument();
3336
});
3437

3538
it('collapses advanced fields when the toggle is clicked again', async () => {
@@ -47,6 +50,42 @@ describe(DomainSchedulesCreateAdvancedForm.name, () => {
4750
screen.getByRole('button', { name: /show advanced configurations/i })
4851
).toBeInTheDocument();
4952
});
53+
54+
it('shows buffer limit only when overlap policy is Buffer', async () => {
55+
const { user } = setup();
56+
57+
await user.click(
58+
screen.getByRole('button', { name: /show advanced configurations/i })
59+
);
60+
61+
const overlapPolicy = screen.getByRole('combobox', {
62+
name: /overlap policy/i,
63+
});
64+
await user.click(overlapPolicy);
65+
await user.click(screen.getByText('Buffer'));
66+
67+
expect(screen.getByLabelText('Buffer limit')).toBeInTheDocument();
68+
expect(
69+
screen.queryByLabelText('Concurrency limit')
70+
).not.toBeInTheDocument();
71+
});
72+
73+
it('shows concurrency limit only when overlap policy is Concurrent', async () => {
74+
const { user } = setup();
75+
76+
await user.click(
77+
screen.getByRole('button', { name: /show advanced configurations/i })
78+
);
79+
80+
const overlapPolicy = screen.getByRole('combobox', {
81+
name: /overlap policy/i,
82+
});
83+
await user.click(overlapPolicy);
84+
await user.click(screen.getByText('Concurrent'));
85+
86+
expect(screen.getByLabelText('Concurrency limit')).toBeInTheDocument();
87+
expect(screen.queryByLabelText('Buffer limit')).not.toBeInTheDocument();
88+
});
5089
});
5190

5291
function setup() {
Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
/** Stable ids for advanced create-schedule horizontal fields. */
2-
export const CREATE_SCHEDULE_ADVANCED_FIELD_IDS = {
3-
scheduleId: 'domain-schedules-create-form-schedule-id',
4-
jitterSeconds: 'domain-schedules-create-form-jitter-seconds',
5-
workflowIdPrefix: 'domain-schedules-create-form-workflow-id-prefix',
6-
} as const;
1+
import { ScheduleOverlapPolicy } from '@/__generated__/proto-ts/uber/cadence/api/v1/ScheduleOverlapPolicy';
2+
import { SCHEDULE_OVERLAP_POLICIES } from '@/route-handlers/create-schedule/create-schedule.constants';
73

8-
export const CREATE_SCHEDULE_ADVANCED_FIELD_DESCRIPTIONS = {
9-
scheduleId: 'Unique name provided by users to name the schedule.',
10-
jitterSeconds:
11-
'Time range to distribute starting workflows across. This helps avoiding burst of workflow creations in a single point of time.',
12-
workflowIdPrefix:
13-
'Prefix text to add into started workflows. Ids are formed as `${Prefix}+{auto generated postfix}`.',
14-
} as const;
4+
const overlapPolicyLabels: Record<
5+
(typeof SCHEDULE_OVERLAP_POLICIES)[number],
6+
string
7+
> = {
8+
[ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_SKIP_NEW]: 'Skip',
9+
[ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER]: 'Buffer',
10+
[ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CONCURRENT]: 'Concurrent',
11+
[ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CANCEL_PREVIOUS]:
12+
'Cancel previous',
13+
[ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_TERMINATE_PREVIOUS]:
14+
'Terminate previous',
15+
};
16+
17+
export const OVERLAP_POLICY_OPTIONS = SCHEDULE_OVERLAP_POLICIES.map(
18+
(policy) => ({
19+
id: policy,
20+
label: overlapPolicyLabels[policy],
21+
})
22+
);

src/views/domain-schedules/domain-schedules-create-advanced-form/domain-schedules-create-advanced-form.tsx

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ import { StatefulPanel } from 'baseui/accordion';
66
import { Button } from 'baseui/button';
77
import { mergeOverrides } from 'baseui/helpers/overrides';
88
import { Input } from 'baseui/input';
9+
import { Select } from 'baseui/select';
910
import { LabelXSmall } from 'baseui/typography';
10-
import { Controller } from 'react-hook-form';
11+
import { Controller, useWatch } from 'react-hook-form';
1112
import { MdExpandLess, MdExpandMore } from 'react-icons/md';
1213

14+
import { ScheduleOverlapPolicy } from '@/__generated__/proto-ts/uber/cadence/api/v1/ScheduleOverlapPolicy';
1315
import DomainSchedulesHorizontalField from '@/views/domain-schedules/domain-schedules-horizontal-field/domain-schedules-horizontal-field';
1416
import getFieldErrorMessage from '@/views/workflow-actions/workflow-action-start-form/helpers/get-field-error-message';
1517

16-
import {
17-
CREATE_SCHEDULE_ADVANCED_FIELD_DESCRIPTIONS,
18-
CREATE_SCHEDULE_ADVANCED_FIELD_IDS,
19-
} from './domain-schedules-create-advanced-form.constants';
18+
import { OVERLAP_POLICY_OPTIONS } from './domain-schedules-create-advanced-form.constants';
2019
import {
2120
overrides,
2221
styled,
@@ -27,6 +26,12 @@ export default function DomainSchedulesCreateAdvancedForm({
2726
control,
2827
fieldErrors,
2928
}: Props) {
29+
const overlapPolicy = useWatch({
30+
control,
31+
name: 'overlapPolicy',
32+
defaultValue: ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CONCURRENT,
33+
});
34+
3035
return (
3136
<StatefulPanel
3237
overrides={mergeOverrides(overrides.panel, {
@@ -59,18 +64,18 @@ export default function DomainSchedulesCreateAdvancedForm({
5964
<>
6065
<DomainSchedulesHorizontalField
6166
label="Schedule Id"
62-
description={CREATE_SCHEDULE_ADVANCED_FIELD_DESCRIPTIONS.scheduleId}
63-
htmlFor={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.scheduleId}
67+
description="Unique name provided by users to name the schedule."
68+
htmlFor="create-schedule-form-schedule-id"
6469
error={getFieldErrorMessage(fieldErrors, 'scheduleId')}
6570
>
6671
<Controller
6772
name="scheduleId"
6873
control={control}
69-
defaultValue=""
7074
render={({ field: { ref, ...field } }) => (
7175
<Input
7276
{...field}
73-
id={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.scheduleId}
77+
value={field.value ?? ''}
78+
id="create-schedule-form-schedule-id"
7479
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
7580
inputRef={ref}
7681
aria-label="Schedule Id"
@@ -84,22 +89,115 @@ export default function DomainSchedulesCreateAdvancedForm({
8489
/>
8590
</DomainSchedulesHorizontalField>
8691

92+
<DomainSchedulesHorizontalField
93+
label="Overlap Policy"
94+
description="Policy that controls what the scheduler should do if the previous action is still running."
95+
error={getFieldErrorMessage(fieldErrors, 'overlapPolicy')}
96+
>
97+
<Controller
98+
name="overlapPolicy"
99+
control={control}
100+
render={({ field: { value, onChange, ref, ...field } }) => (
101+
<Select
102+
{...field}
103+
inputRef={ref}
104+
aria-label="Overlap Policy"
105+
options={OVERLAP_POLICY_OPTIONS}
106+
value={value ? [{ id: value }] : []}
107+
onChange={(params) => {
108+
onChange(params.value[0]?.id);
109+
}}
110+
error={Boolean(
111+
getFieldErrorMessage(fieldErrors, 'overlapPolicy')
112+
)}
113+
size="compact"
114+
clearable={false}
115+
/>
116+
)}
117+
/>
118+
</DomainSchedulesHorizontalField>
119+
120+
{overlapPolicy ===
121+
ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_BUFFER && (
122+
<DomainSchedulesHorizontalField
123+
subfield={true}
124+
label="Buffer limit"
125+
description="Max number of pending workflows allowed when using Buffer overlap policy."
126+
htmlFor="create-schedule-form-buffer-limit"
127+
caption="Defaults to 0 (unlimited)"
128+
error={getFieldErrorMessage(fieldErrors, 'bufferLimit')}
129+
>
130+
<Controller
131+
name="bufferLimit"
132+
control={control}
133+
render={({ field: { ref, ...field } }) => (
134+
<Input
135+
{...field}
136+
id="create-schedule-form-buffer-limit"
137+
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
138+
inputRef={ref}
139+
aria-label="Buffer limit"
140+
type="number"
141+
min={0}
142+
onBlur={field.onBlur}
143+
error={Boolean(
144+
getFieldErrorMessage(fieldErrors, 'bufferLimit')
145+
)}
146+
size="compact"
147+
placeholder="Set buffer limit"
148+
/>
149+
)}
150+
/>
151+
</DomainSchedulesHorizontalField>
152+
)}
153+
154+
{overlapPolicy ===
155+
ScheduleOverlapPolicy.SCHEDULE_OVERLAP_POLICY_CONCURRENT && (
156+
<DomainSchedulesHorizontalField
157+
subfield={true}
158+
label="Concurrency limit"
159+
description="Max number of concurrently running workflows allowed for Concurrent overlap policy."
160+
htmlFor="create-schedule-form-concurrency-limit"
161+
caption="Defaults to 0 (unlimited)"
162+
error={getFieldErrorMessage(fieldErrors, 'concurrencyLimit')}
163+
>
164+
<Controller
165+
name="concurrencyLimit"
166+
control={control}
167+
render={({ field: { ref, ...field } }) => (
168+
<Input
169+
{...field}
170+
id="create-schedule-form-concurrency-limit"
171+
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
172+
inputRef={ref}
173+
aria-label="Concurrency limit"
174+
type="number"
175+
min={0}
176+
onBlur={field.onBlur}
177+
error={Boolean(
178+
getFieldErrorMessage(fieldErrors, 'concurrencyLimit')
179+
)}
180+
size="compact"
181+
placeholder="Set concurrency limit"
182+
/>
183+
)}
184+
/>
185+
</DomainSchedulesHorizontalField>
186+
)}
187+
87188
<DomainSchedulesHorizontalField
88189
label="Jitter duration"
89-
description={
90-
CREATE_SCHEDULE_ADVANCED_FIELD_DESCRIPTIONS.jitterSeconds
91-
}
92-
htmlFor={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.jitterSeconds}
190+
description="Time range to distribute starting workflows across. This helps avoiding burst of workflow creations in a single point of time."
191+
htmlFor="create-schedule-form-jitter"
93192
error={getFieldErrorMessage(fieldErrors, 'jitterSeconds')}
94193
>
95194
<Controller
96195
name="jitterSeconds"
97196
control={control}
98-
defaultValue=""
99197
render={({ field: { ref, ...field } }) => (
100198
<Input
101199
{...field}
102-
id={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.jitterSeconds}
200+
id="create-schedule-form-jitter"
103201
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
104202
inputRef={ref}
105203
aria-label="Jitter duration"
@@ -119,20 +217,18 @@ export default function DomainSchedulesCreateAdvancedForm({
119217

120218
<DomainSchedulesHorizontalField
121219
label="Workflow Id Prefix"
122-
description={
123-
CREATE_SCHEDULE_ADVANCED_FIELD_DESCRIPTIONS.workflowIdPrefix
124-
}
125-
htmlFor={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.workflowIdPrefix}
220+
description="Prefix text to add into started workflows. Ids are formed as `${Prefix}+{auto generated postfix}`."
221+
htmlFor="create-schedule-form-workflow-id-prefix"
126222
error={getFieldErrorMessage(fieldErrors, 'workflowIdPrefix')}
127223
>
128224
<Controller
129225
name="workflowIdPrefix"
130226
control={control}
131-
defaultValue=""
132227
render={({ field: { ref, ...field } }) => (
133228
<Input
134229
{...field}
135-
id={CREATE_SCHEDULE_ADVANCED_FIELD_IDS.workflowIdPrefix}
230+
value={field.value ?? ''}
231+
id="create-schedule-form-workflow-id-prefix"
136232
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
137233
inputRef={ref}
138234
aria-label="Workflow Id Prefix"

src/views/domain-schedules/domain-schedules-create-form/domain-schedules-create-form.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export default function DomainSchedulesCreateForm({ control, trigger }: Props) {
5151
<Controller
5252
name="workflowType.name"
5353
control={control}
54-
defaultValue=""
5554
render={({ field: { ref, ...field } }) => (
5655
<Input
5756
{...field}
@@ -103,7 +102,6 @@ export default function DomainSchedulesCreateForm({ control, trigger }: Props) {
103102
<Controller
104103
name="taskList.name"
105104
control={control}
106-
defaultValue=""
107105
render={({ field: { ref, ...field } }) => (
108106
<Input
109107
{...field}
@@ -170,7 +168,6 @@ export default function DomainSchedulesCreateForm({ control, trigger }: Props) {
170168
<Controller
171169
name="input"
172170
control={control}
173-
defaultValue={['']}
174171
render={({ field }) => (
175172
<MultiJsonInput
176173
label="JSON input arguments (optional)"
@@ -284,7 +281,6 @@ export default function DomainSchedulesCreateForm({ control, trigger }: Props) {
284281
<Controller
285282
name="pauseOnFailure"
286283
control={control}
287-
defaultValue={false}
288284
render={({ field: { value, onChange, ref, ...field } }) => (
289285
<Checkbox
290286
{...field}

0 commit comments

Comments
 (0)