|
1 | 1 | import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; |
2 | 2 | import { parseWithZod } from "@conform-to/zod"; |
| 3 | +import { ScheduleWindow } from "@trigger.dev/core/v3"; |
3 | 4 | import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; |
4 | 5 | import { |
5 | 6 | type FetcherWithComponents, |
@@ -150,6 +151,15 @@ type CronPatternResult = |
150 | 151 | error: string; |
151 | 152 | }; |
152 | 153 |
|
| 154 | +type ScheduleWindowResult = |
| 155 | + | { |
| 156 | + isValid: true; |
| 157 | + } |
| 158 | + | { |
| 159 | + isValid: false; |
| 160 | + error: string; |
| 161 | + }; |
| 162 | + |
153 | 163 | export function UpsertScheduleForm({ |
154 | 164 | schedule, |
155 | 165 | possibleTasks, |
@@ -179,6 +189,7 @@ export function UpsertScheduleForm({ |
179 | 189 | const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC"); |
180 | 190 | const isUtc = selectedTimezone === "UTC"; |
181 | 191 | const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? ""); |
| 192 | + const [scheduleWindowValue, setScheduleWindowValue] = useState<string>(schedule?.window ?? ""); |
182 | 193 | const navigation = useNavigation(); |
183 | 194 | const isLoading = submitFetcher ? submitFetcher.state !== "idle" : navigation.state !== "idle"; |
184 | 195 | const organization = useOrganization(); |
@@ -210,8 +221,16 @@ export function UpsertScheduleForm({ |
210 | 221 | }); |
211 | 222 |
|
212 | 223 | let cronPatternResult: CronPatternResult | undefined = undefined; |
| 224 | + let scheduleWindowResult: ScheduleWindowResult | undefined = undefined; |
213 | 225 | let nextRuns: Date[] | undefined = undefined; |
214 | 226 |
|
| 227 | + if (scheduleWindowValue !== "") { |
| 228 | + const result = ScheduleWindow.safeParse(scheduleWindowValue); |
| 229 | + scheduleWindowResult = result.success |
| 230 | + ? { isValid: true } |
| 231 | + : { isValid: false, error: result.error.errors[0].message }; |
| 232 | + } |
| 233 | + |
215 | 234 | if (cronPattern !== "") { |
216 | 235 | const result = CronPattern.safeParse(cronPattern); |
217 | 236 |
|
@@ -327,9 +346,19 @@ export function UpsertScheduleForm({ |
327 | 346 | {cronPatternResult === undefined ? ( |
328 | 347 | <Hint>Enter a CRON pattern or use natural language above.</Hint> |
329 | 348 | ) : cronPatternResult.isValid ? ( |
330 | | - <ValidCronMessage isValid={true} message={`${cronPatternResult.description}.`} /> |
| 349 | + <ValidationMessage |
| 350 | + isValid={true} |
| 351 | + validLabel="Valid pattern:" |
| 352 | + invalidLabel="Invalid pattern:" |
| 353 | + message={`${cronPatternResult.description}.`} |
| 354 | + /> |
331 | 355 | ) : ( |
332 | | - <ValidCronMessage isValid={false} message={cronPatternResult.error} /> |
| 356 | + <ValidationMessage |
| 357 | + isValid={false} |
| 358 | + validLabel="Valid pattern:" |
| 359 | + invalidLabel="Invalid pattern:" |
| 360 | + message={cronPatternResult.error} |
| 361 | + /> |
333 | 362 | )} |
334 | 363 | </InputGroup> |
335 | 364 | <InputGroup> |
@@ -364,19 +393,45 @@ export function UpsertScheduleForm({ |
364 | 393 | <Input |
365 | 394 | {...getInputProps(scheduleWindow, { type: "text" })} |
366 | 395 | placeholder="30m or 25%" |
367 | | - defaultValue={schedule?.window} |
| 396 | + value={scheduleWindowValue} |
| 397 | + aria-invalid={scheduleWindowResult?.isValid === false ? true : undefined} |
| 398 | + aria-describedby={ |
| 399 | + scheduleWindowResult === undefined ? undefined : scheduleWindow.errorId |
| 400 | + } |
| 401 | + onChange={(event) => setScheduleWindowValue(event.target.value)} |
368 | 402 | /> |
369 | | - <Hint> |
370 | | - Assigns each run a stable time after its CRON time. Use minutes, hours, or a |
371 | | - percentage of the interval. Schedules always use at least a 60-second placement |
372 | | - range. |
373 | | - </Hint> |
374 | | - <FormError id={scheduleWindow.errorId}>{scheduleWindow.errors}</FormError> |
| 403 | + {scheduleWindowResult === undefined ? ( |
| 404 | + <Hint> |
| 405 | + Assigns each run a stable time after its CRON time. Use minutes, hours, or a |
| 406 | + percentage of the interval. |
| 407 | + </Hint> |
| 408 | + ) : scheduleWindowResult.isValid ? ( |
| 409 | + <ValidationMessage |
| 410 | + id={scheduleWindow.errorId} |
| 411 | + isValid={true} |
| 412 | + validLabel="Valid window:" |
| 413 | + invalidLabel="Invalid window:" |
| 414 | + message="Runs will be assigned a stable time within this window." |
| 415 | + /> |
| 416 | + ) : ( |
| 417 | + <ValidationMessage |
| 418 | + id={scheduleWindow.errorId} |
| 419 | + isValid={false} |
| 420 | + validLabel="Valid window:" |
| 421 | + invalidLabel="Invalid window:" |
| 422 | + message={scheduleWindowResult.error} |
| 423 | + /> |
| 424 | + )} |
375 | 425 | </InputGroup> |
376 | 426 | {nextRuns !== undefined && ( |
377 | 427 | <div className="flex flex-col gap-1"> |
378 | | - <Header3>Next 5 CRON times</Header3> |
379 | | - <Hint>Assigned times are calculated after the schedule is saved.</Hint> |
| 428 | + <Header3>Next 5 runs</Header3> |
| 429 | + {scheduleWindowValue !== "" && ( |
| 430 | + <Hint> |
| 431 | + Actual run times will get a fixed offset based on the window, displayed after |
| 432 | + creation. |
| 433 | + </Hint> |
| 434 | + )} |
380 | 435 | <Table> |
381 | 436 | <TableHeader> |
382 | 437 | <TableRow> |
@@ -525,17 +580,29 @@ function buttonText(mode: "edit" | "new", isLoading: boolean) { |
525 | 580 | } |
526 | 581 | } |
527 | 582 |
|
528 | | -function ValidCronMessage({ isValid, message }: { isValid: boolean; message: string }) { |
| 583 | +function ValidationMessage({ |
| 584 | + id, |
| 585 | + isValid, |
| 586 | + validLabel, |
| 587 | + invalidLabel, |
| 588 | + message, |
| 589 | +}: { |
| 590 | + id?: string; |
| 591 | + isValid: boolean; |
| 592 | + validLabel: string; |
| 593 | + invalidLabel: string; |
| 594 | + message: string; |
| 595 | +}) { |
529 | 596 | return ( |
530 | | - <Paragraph variant="small"> |
| 597 | + <Paragraph id={id} variant="small"> |
531 | 598 | <span className="mr-1"> |
532 | 599 | {isValid ? ( |
533 | 600 | <CheckIcon className="-mt-0.5 mr-1 inline-block h-4 w-4 text-success" /> |
534 | 601 | ) : ( |
535 | 602 | <XMarkIcon className="-mt-0.5 mr-1 inline-block h-4 w-4 text-error" /> |
536 | 603 | )} |
537 | 604 | <span className={isValid ? "text-success" : "text-error"}> |
538 | | - {isValid ? "Valid pattern:" : "Invalid pattern:"} |
| 605 | + {isValid ? validLabel : invalidLabel} |
539 | 606 | </span> |
540 | 607 | </span> |
541 | 608 | <span>{message}</span> |
|
0 commit comments