diff --git a/package.json b/package.json index 4155a5b0b..6a65b19c8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "copy-to-clipboard": "^3.3.3", + "cronstrue": "^3.9.0", "date-fns": "^4.1.0", "debounce": "^2.2.0", "deepmerge-ts": "^7.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7973ec5e6..78b07df0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: copy-to-clipboard: specifier: ^3.3.3 version: 3.3.3 + cronstrue: + specifier: ^3.9.0 + version: 3.9.0 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -2269,6 +2272,10 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cronstrue@3.9.0: + resolution: {integrity: sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==} + hasBin: true + cross-env@10.0.0: resolution: {integrity: sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==} engines: {node: '>=20'} @@ -6006,6 +6013,8 @@ snapshots: crelt@1.0.6: {} + cronstrue@3.9.0: {} + cross-env@10.0.0: dependencies: '@epic-web/invariant': 1.0.0 diff --git a/resources/scripts/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx index 88d86eab6..c697207a5 100644 --- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx +++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx @@ -1,6 +1,7 @@ import ModalContext from '@/context/ModalContext'; import { TZDate } from '@date-fns/tz'; import { Link, TriangleExclamation } from '@gravity-ui/icons'; +import { toString } from 'cronstrue'; import { format } from 'date-fns'; import { useStoreState } from 'easy-peasy'; import { Form, Formik, FormikHelpers } from 'formik'; @@ -101,6 +102,35 @@ const formatTimezoneDisplay = (timezone: string, offset: string) => { return `${timezone} (${offset})`; }; +const getCronDescription = ( + minute: string, + hour: string, + dayOfMonth: string, + month: string, + dayOfWeek: string, +): string => { + try { + // Build cron expression: minute hour dayOfMonth month dayOfWeek + const cronExpression = `${minute} ${hour} ${dayOfMonth} ${month} ${dayOfWeek}`; + const description = toString(cronExpression, { + throwExceptionOnParseError: false, + verbose: true, + }); + + // Check if cronstrue returned an error message + if ( + description === + 'An error occurred when generating the expression description. Check the cron expression syntax.' + ) { + return 'Invalid cron expression'; + } + + return description; + } catch { + return 'Invalid cron expression.'; + } +}; + const EditScheduleModal = ({ schedule }: Props) => { const { addError, clearFlashes } = useFlash(); const { dismiss, setPropOverrides } = useContext(ModalContext); @@ -167,116 +197,130 @@ const EditScheduleModal = ({ schedule }: Props) => { } as Values } > - {({ isSubmitting }) => ( -
- - -
- - - - - -
+ {({ isSubmitting, values }) => { + const cronDescription = getCronDescription( + values.minute, + values.hour, + values.dayOfMonth, + values.month, + values.dayOfWeek, + ); -

- The schedule system uses Cronjob syntax when defining when tasks should begin running. Use the - fields above to specify when these tasks should begin running. -

+ return ( + + + +
+ + + + + +
- {timezoneInfo.isDifferent && ( -
-
- -
-

Timezone Information

-

- Times shown here are configured for the server timezone. - {timezoneInfo.difference !== 'same time' && ( - - {' '} - The server is {timezoneInfo.difference} your timezone. - - )} -

-
-
- Your timezone: - - {' '} - {formatTimezoneDisplay( - timezoneInfo.user.timezone, - timezoneInfo.user.offset, - )} - -
-
- Server timezone: - - {' '} - {formatTimezoneDisplay( - timezoneInfo.server.timezone, - timezoneInfo.server.offset, - )} - +
+

{cronDescription}

+
+ +

+ The schedule system uses Cronjob syntax when defining when tasks should begin running. Use + the fields above to specify when these tasks should begin running. +

+ + {timezoneInfo.isDifferent && ( +
+
+ +
+

Timezone Information

+

+ Times shown here are configured for the server timezone. + {timezoneInfo.difference !== 'same time' && ( + + {' '} + The server is {timezoneInfo.difference} your timezone. + + )} +

+
+
+ Your timezone: + + {' '} + {formatTimezoneDisplay( + timezoneInfo.user.timezone, + timezoneInfo.user.offset, + )} + +
+
+ Server timezone: + + {' '} + {formatTimezoneDisplay( + timezoneInfo.server.timezone, + timezoneInfo.server.offset, + )} + +
-
- )} + )} -
- - setShowCheetsheet((s) => !s)} - labelClasses='cursor-pointer' - > - - - - {/* This table would be pretty awkward to make look nice +
+ + setShowCheetsheet((s) => !s)} + labelClasses='cursor-pointer' + > + + + + {/* This table would be pretty awkward to make look nice Maybe there could be an element for a dropdown later? */} - {/* {showCheatsheet && ( + {/* {showCheatsheet && (
)} */} - - -
-
- - {schedule ? 'Save changes' : 'Create schedule'} - -
- - )} + + +
+
+ + {schedule ? 'Save changes' : 'Create schedule'} + +
+ + ); + }} ); };