From f1044b3e2bc3bf366ed030ea35ebf17b59c28637 Mon Sep 17 00:00:00 2001 From: sharevb Date: Wed, 1 May 2024 14:22:46 +0200 Subject: [PATCH 1/4] fix(Cron Parser): handle more patterns Just use validation of cronstrue package Fix #855 --- src/tools/crontab-generator/crontab-generator.vue | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index 97503e7d..eb3063ed 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -1,12 +1,7 @@ @@ -191,4 +326,12 @@ pre { overflow: auto; padding: 10px 0; } + +.cron-execution-string{ + text-align: center; + font-size: 14px; + opacity: 0.8; + margin: 5px 0 15px; + white-space: pre-wrap; +} diff --git a/src/tools/crontab-generator/get-timezone-offset.d.ts b/src/tools/crontab-generator/get-timezone-offset.d.ts new file mode 100644 index 00000000..3aa8e8db --- /dev/null +++ b/src/tools/crontab-generator/get-timezone-offset.d.ts @@ -0,0 +1,3 @@ +declare module "get-timezone-offset" { + export default function(timeZoneName: string, date: Date); +} diff --git a/src/tools/crontab-generator/index.ts b/src/tools/crontab-generator/index.ts index 429d6e14..c08c73ef 100644 --- a/src/tools/crontab-generator/index.ts +++ b/src/tools/crontab-generator/index.ts @@ -20,6 +20,7 @@ export const tool = defineTool({ 'day', 'minute', 'second', + 'aws', ], component: () => import('./crontab-generator.vue'), icon: Alarm, From 605229f4efdd715a73f0048ea5c940c9e07ac8ea Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sat, 21 Sep 2024 14:42:22 +0200 Subject: [PATCH 3/4] fix: helpers dup % --- .../crontab-generator/crontab-generator.vue | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index f5f060b5..654e278a 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -29,7 +29,7 @@ watchEffect(() => { cronstrueConfig.tzOffset = -getTimezoneOffset(currentTimezone.value, new Date()) / 60; }); -const standardHelpers = [ +const commonHelpers = [ { symbol: '*', meaning: 'Any value', @@ -54,6 +54,10 @@ const standardHelpers = [ example: '*/10 * * *', equivalent: 'Every 10 minutes', }, +]; + +const standardHelpers = [ + ...commonHelpers, { symbol: '@yearly', meaning: 'Once every year at midnight of 1 January', @@ -105,30 +109,7 @@ const standardHelpers = [ ]; const awsHelpers = [ - { - symbol: '*', - meaning: 'Any value', - example: '* * * *', - equivalent: 'Every minute', - }, - { - symbol: '-', - meaning: 'Range of values', - example: '1-10 * * *', - equivalent: 'Minutes 1 through 10', - }, - { - symbol: ',', - meaning: 'List of values', - example: '1,10 * * *', - equivalent: 'At minutes 1 and 10', - }, - { - symbol: '/', - meaning: 'Step values', - example: '*/10 * * *', - equivalent: 'Every 10 minutes', - }, + ...commonHelpers, { symbol: '?', meaning: 'One or another. In the Day-of-month field you could enter 7, and if you didn\'t care what day of the week the seventh was, you could enter ? in the Day-of-week field', From fa01008dc8bfa8765678ca425a0d7f0dbeab264f Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sun, 22 Sep 2024 12:20:44 +0200 Subject: [PATCH 4/4] fix: let user choice 'standard' vs 'aws' Let the user choose between cron format (since help is different) --- components.d.ts | 5 +++ .../crontab-generator.service.test.ts | 19 +++++++++ .../crontab-generator.service.ts | 13 +++--- .../crontab-generator/crontab-generator.vue | 42 +++++++++++-------- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/components.d.ts b/components.d.ts index 3e65c3cc..d77d814b 100644 --- a/components.d.ts +++ b/components.d.ts @@ -135,13 +135,18 @@ declare module '@vue/runtime-core' { NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + NForm: typeof import('naive-ui')['NForm'] + NFormItem: typeof import('naive-ui')['NFormItem'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] + NRadio: typeof import('naive-ui')['NRadio'] + NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NSpace: typeof import('naive-ui')['NSpace'] + NSwitch: typeof import('naive-ui')['NSwitch'] NTable: typeof import('naive-ui')['NTable'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] diff --git a/src/tools/crontab-generator/crontab-generator.service.test.ts b/src/tools/crontab-generator/crontab-generator.service.test.ts index 388a92bf..d45fadfe 100644 --- a/src/tools/crontab-generator/crontab-generator.service.test.ts +++ b/src/tools/crontab-generator/crontab-generator.service.test.ts @@ -4,6 +4,7 @@ import { getCronType, getLastExecutionTimes, isCronValid } from './crontab-gener describe('crontab-generator', () => { describe('isCronValid', () => { it('should return true for all valid formats', () => { + // standard format expect(isCronValid('0 0 * * 1-5')).toBe(true); expect(isCronValid('23 0-20/2 * * *')).toBe(true); @@ -11,6 +12,24 @@ describe('crontab-generator', () => { expect(isCronValid('0 11-22 ? * MON-FRI *')).toBe(true); expect(isCronValid('0 0 ? * 1 *')).toBe(true); }); + it('should check standard format', () => { + // standard format + expect(isCronValid('0 0 * * 1-5', 'standard')).toBe(true); + expect(isCronValid('23 0-20/2 * * *', 'standard')).toBe(true); + + // AWS format + expect(isCronValid('0 11-22 ? * MON-FRI *', 'standard')).toBe(false); + expect(isCronValid('0 0 ? * 1 *', 'standard')).toBe(false); + }); + it('should check aws format', () => { + // standard format + expect(isCronValid('0 0 * * 1-5', 'aws')).toBe(false); + expect(isCronValid('23 0-20/2 * * *', 'aws')).toBe(false); + + // AWS format + expect(isCronValid('0 11-22 ? * MON-FRI *', 'aws')).toBe(true); + expect(isCronValid('0 0 ? * 1 *', 'aws')).toBe(true); + }); it('should return false for all invalid formats', () => { expect(isCronValid('aert')).toBe(false); expect(isCronValid('40 *')).toBe(false); diff --git a/src/tools/crontab-generator/crontab-generator.service.ts b/src/tools/crontab-generator/crontab-generator.service.ts index 1d44405b..8e4b7b7e 100644 --- a/src/tools/crontab-generator/crontab-generator.service.ts +++ b/src/tools/crontab-generator/crontab-generator.service.ts @@ -1,6 +1,8 @@ import { parseExpression } from 'cron-parser'; import EventCronParser from 'event-cron-parser'; +export type CronType = 'standard' | 'aws'; + export function getLastExecutionTimes(cronExpression: string, tz: string | undefined = undefined, count: number = 5) { if (getCronType(cronExpression) === 'standard') { const interval = parseExpression(cronExpression, { tz }); @@ -22,18 +24,19 @@ export function getLastExecutionTimes(cronExpression: string, tz: string | undef return []; } -export function isCronValid(v: string) { - return !!getCronType(v); +export function isCronValid(cronExpression: string, cronType: CronType | 'any' = 'any') { + const expressionCronType = getCronType(cronExpression); + return cronType === 'any' ? !!expressionCronType : expressionCronType === cronType; } -export function getCronType(v: string) { +export function getCronType(cronExpression: string) { try { - parseExpression(v); + parseExpression(cronExpression); return 'standard'; } catch (_) { try { - const parsed = new EventCronParser(v); + const parsed = new EventCronParser(cronExpression); parsed.validate(); return 'aws'; } diff --git a/src/tools/crontab-generator/crontab-generator.vue b/src/tools/crontab-generator/crontab-generator.vue index 654e278a..4dc48123 100644 --- a/src/tools/crontab-generator/crontab-generator.vue +++ b/src/tools/crontab-generator/crontab-generator.vue @@ -2,7 +2,7 @@ import cronstrue from 'cronstrue'; import ctz from 'countries-and-timezones'; import getTimezoneOffset from 'get-timezone-offset'; -import { getCronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; +import { type CronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; import { useStyleStore } from '@/stores/style.store'; import { useQueryParamOrStorage } from '@/composable/queryParams'; @@ -20,10 +20,13 @@ const cronstrueConfig = reactive({ // getTimezoneOffset(tz.name, now) / 60 const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; -const allTimezones = Object.values(ctz.getAllTimezones()).map(tz => ({ - value: tz.name, - label: `${tz.name === browserTimezone ? 'Browser TZ - ' : ''}${tz.name} (${tz.utcOffset === tz.dstOffset ? tz.utcOffsetStr : `${tz.utcOffsetStr}/${tz.dstOffsetStr}`})`, -})); +const allTimezones = Object.values(ctz.getAllTimezones()).map((tz) => { + const timezoneUTCDSTOffset = tz.utcOffset === tz.dstOffset ? tz.utcOffsetStr : `${tz.utcOffsetStr}/${tz.dstOffsetStr}`; + return { + value: tz.name, + label: `${tz.name === browserTimezone ? 'Browser TZ - ' : ''}${tz.name} (${timezoneUTCDSTOffset})`, + }; +}); const currentTimezone = useQueryParamOrStorage({ name: 'tz', storageName: 'crongen:tz', defaultValue: browserTimezone }); watchEffect(() => { cronstrueConfig.tzOffset = -getTimezoneOffset(currentTimezone.value, new Date()) / 60; @@ -136,19 +139,24 @@ const awsHelpers = [ }, ]; -const cronType = computed({ - get() { - return getCronType(cron.value); - }, - set(newCronType) { +const defaultAWSCronExpression = '0 0 ? * 1 *'; +const defaultStandardCronExpression = '40 * * * *'; +const cronType = ref('standard'); +watch(cronType, + (newCronType) => { if (newCronType === 'aws') { - cron.value = '0 0 ? * 1 *'; + if (!cron.value || cron.value === defaultStandardCronExpression) { + cron.value = defaultAWSCronExpression; + } } - else { - cron.value = '40 * * * *'; + else if (newCronType === 'standard') { + if (!cron.value || cron.value === defaultAWSCronExpression) { + cron.value = defaultStandardCronExpression; + } } }, -}); +); + const getHelpers = computed(() => { if (cronType.value === 'aws') { return awsHelpers; @@ -165,7 +173,7 @@ const cronString = computed(() => { const cronValidationRules = [ { - validator: (value: string) => isCronValid(value), + validator: (value: string) => isCronValid(value, cronType.value), message: 'This cron is invalid', }, ]; @@ -245,7 +253,7 @@ const executionTimesString = computed(() => {
--- Standard CRON Syntax --
+      -- Standard CRON Syntax --
 ┌──────────── [optional] seconds (0 - 59)
 | ┌────────── minute (0 - 59)
 | | ┌──────── hour (0 - 23)
@@ -256,7 +264,7 @@ const executionTimesString = computed(() => {
 * * * * * * command
--- AWS CRON Syntax --
+      -- AWS CRON Syntax --
 ┌──────────── minute (0 - 59)
 | ┌────────── hour (0 - 23)
 | | ┌──────── day of month (1 - 31) OR ? OR L OR W