fix(Cron Parser): handle aws, next executions and TZ
Handle AWS Cron syntax and distinguishe from standard syntax (fix #855) Add show crontab next 5 execution times (taken from #1283) Add Timezone handling: fix #261
This commit is contained in:
		
							parent
							
								
									cb5b462e11
								
							
						
					
					
						commit
						48b4904cf1
					
				
							
								
								
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -132,6 +132,7 @@ 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'] | ||||
|     NGi: typeof import('naive-ui')['NGi'] | ||||
|     NGrid: typeof import('naive-ui')['NGrid'] | ||||
| @ -143,8 +144,12 @@ declare module '@vue/runtime-core' { | ||||
|     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'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NSpin: typeof import('naive-ui')['NSpin'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     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'] | ||||
|     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||
| @ -159,6 +164,7 @@ declare module '@vue/runtime-core' { | ||||
|     RouterLink: typeof import('vue-router')['RouterLink'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] | ||||
|     SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] | ||||
|     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] | ||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] | ||||
|  | ||||
| @ -51,16 +51,20 @@ | ||||
|     "change-case": "^4.1.2", | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize-ts": "^0.6.2", | ||||
|     "countries-and-timezones": "^3.6.0", | ||||
|     "country-code-lookup": "^0.1.0", | ||||
|     "cron-parser": "^4.9.0", | ||||
|     "cron-validator": "^1.3.1", | ||||
|     "cronstrue": "^2.26.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "dompurify": "^3.0.6", | ||||
|     "emojilib": "^3.0.10", | ||||
|     "event-cron-parser": "^1.0.34", | ||||
|     "figlet": "^1.7.0", | ||||
|     "figue": "^1.2.0", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "get-timezone-offset": "^1.0.5", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
|     "ibantools": "^4.3.3", | ||||
|  | ||||
							
								
								
									
										61
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										61
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -53,9 +53,15 @@ dependencies: | ||||
|   composerize-ts: | ||||
|     specifier: ^0.6.2 | ||||
|     version: 0.6.2 | ||||
|   countries-and-timezones: | ||||
|     specifier: ^3.6.0 | ||||
|     version: 3.6.0 | ||||
|   country-code-lookup: | ||||
|     specifier: ^0.1.0 | ||||
|     version: 0.1.0 | ||||
|   cron-parser: | ||||
|     specifier: ^4.9.0 | ||||
|     version: 4.9.0 | ||||
|   cron-validator: | ||||
|     specifier: ^1.3.1 | ||||
|     version: 1.3.1 | ||||
| @ -74,6 +80,9 @@ dependencies: | ||||
|   emojilib: | ||||
|     specifier: ^3.0.10 | ||||
|     version: 3.0.10 | ||||
|   event-cron-parser: | ||||
|     specifier: ^1.0.34 | ||||
|     version: 1.0.34 | ||||
|   figlet: | ||||
|     specifier: ^1.7.0 | ||||
|     version: 1.7.0 | ||||
| @ -83,6 +92,9 @@ dependencies: | ||||
|   fuse.js: | ||||
|     specifier: ^6.6.2 | ||||
|     version: 6.6.2 | ||||
|   get-timezone-offset: | ||||
|     specifier: ^1.0.5 | ||||
|     version: 1.0.5 | ||||
|   highlight.js: | ||||
|     specifier: ^11.7.0 | ||||
|     version: 11.7.0 | ||||
| @ -3351,7 +3363,7 @@ packages: | ||||
|     dependencies: | ||||
|       '@unhead/dom': 0.5.1 | ||||
|       '@unhead/schema': 0.5.1 | ||||
|       '@vueuse/shared': 10.7.2(vue@3.3.4) | ||||
|       '@vueuse/shared': 11.1.0(vue@3.3.4) | ||||
|       unhead: 0.5.1 | ||||
|       vue: 3.3.4 | ||||
|     transitivePeerDependencies: | ||||
| @ -3993,10 +4005,10 @@ packages: | ||||
|       - vue | ||||
|     dev: false | ||||
| 
 | ||||
|   /@vueuse/shared@10.7.2(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} | ||||
|   /@vueuse/shared@11.1.0(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} | ||||
|     dependencies: | ||||
|       vue-demi: 0.14.6(vue@3.3.4) | ||||
|       vue-demi: 0.14.10(vue@3.3.4) | ||||
|     transitivePeerDependencies: | ||||
|       - '@vue/composition-api' | ||||
|       - vue | ||||
| @ -4613,6 +4625,11 @@ packages: | ||||
|       browserslist: 4.22.1 | ||||
|     dev: true | ||||
| 
 | ||||
|   /countries-and-timezones@3.6.0: | ||||
|     resolution: {integrity: sha512-8/nHBCs1eKeQ1jnsZVGdqrLYxS8nPcfJn8PnmxdJXWRLZdXsGFR8gnVhRjatGDBjqmPm7H+FtYpBYTPWd0Eiqg==} | ||||
|     engines: {node: '>=8.x', npm: '>=5.x'} | ||||
|     dev: false | ||||
| 
 | ||||
|   /country-code-lookup@0.1.0: | ||||
|     resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==} | ||||
|     dev: false | ||||
| @ -4621,6 +4638,13 @@ packages: | ||||
|     resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} | ||||
|     dev: false | ||||
| 
 | ||||
|   /cron-parser@4.9.0: | ||||
|     resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} | ||||
|     engines: {node: '>=12.0.0'} | ||||
|     dependencies: | ||||
|       luxon: 3.5.0 | ||||
|     dev: false | ||||
| 
 | ||||
|   /cron-validator@1.3.1: | ||||
|     resolution: {integrity: sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==} | ||||
|     dev: false | ||||
| @ -5512,6 +5536,12 @@ packages: | ||||
|     engines: {node: '>=0.10.0'} | ||||
|     dev: true | ||||
| 
 | ||||
|   /event-cron-parser@1.0.34: | ||||
|     resolution: {integrity: sha512-ytqZmMrNfSvzHWriiHdoNOpYKFr4d2fDoC4Rgq0F8lEA37abCWkYhSsqslC/kngWwnTGq7L0Q9VlMreKe6EJbQ==} | ||||
|     dependencies: | ||||
|       number-to-words: 1.2.4 | ||||
|     dev: false | ||||
| 
 | ||||
|   /event-stream@3.3.4: | ||||
|     resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} | ||||
|     dependencies: | ||||
| @ -5806,6 +5836,11 @@ packages: | ||||
|       get-intrinsic: 1.2.2 | ||||
|     dev: true | ||||
| 
 | ||||
|   /get-timezone-offset@1.0.5: | ||||
|     resolution: {integrity: sha512-+B+/vEJ9qJgZheDVNmuY+4il8sJhTFXRvSiiqyRfwiCEhTaZqn/yCoNToDzQL+Mv9DLKlyO1bSIP5nUCJQN9Aw==} | ||||
|     engines: {node: '>=4.0.0'} | ||||
|     dev: false | ||||
| 
 | ||||
|   /get-tsconfig@4.7.2: | ||||
|     resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} | ||||
|     dependencies: | ||||
| @ -6753,6 +6788,11 @@ packages: | ||||
|     dependencies: | ||||
|       yallist: 4.0.0 | ||||
| 
 | ||||
|   /luxon@3.5.0: | ||||
|     resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} | ||||
|     engines: {node: '>=12'} | ||||
|     dev: false | ||||
| 
 | ||||
|   /magic-string@0.25.9: | ||||
|     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} | ||||
|     dependencies: | ||||
| @ -7106,6 +7146,10 @@ packages: | ||||
|       boolbase: 1.0.0 | ||||
|     dev: true | ||||
| 
 | ||||
|   /number-to-words@1.2.4: | ||||
|     resolution: {integrity: sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==} | ||||
|     dev: false | ||||
| 
 | ||||
|   /nwsapi@2.2.7: | ||||
|     resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} | ||||
|     dev: true | ||||
| @ -9136,8 +9180,8 @@ packages: | ||||
|       vue: 3.3.4 | ||||
|     dev: false | ||||
| 
 | ||||
|   /vue-demi@0.14.5(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} | ||||
|   /vue-demi@0.14.10(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} | ||||
|     engines: {node: '>=12'} | ||||
|     hasBin: true | ||||
|     requiresBuild: true | ||||
| @ -9151,8 +9195,8 @@ packages: | ||||
|       vue: 3.3.4 | ||||
|     dev: false | ||||
| 
 | ||||
|   /vue-demi@0.14.6(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} | ||||
|   /vue-demi@0.14.5(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} | ||||
|     engines: {node: '>=12'} | ||||
|     hasBin: true | ||||
|     requiresBuild: true | ||||
| @ -9442,6 +9486,7 @@ packages: | ||||
| 
 | ||||
|   /workbox-google-analytics@7.0.0: | ||||
|     resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} | ||||
|     deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained | ||||
|     dependencies: | ||||
|       workbox-background-sync: 7.0.0 | ||||
|       workbox-core: 7.0.0 | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import { useRouteQuery } from '@vueuse/router'; | ||||
| import { computed } from 'vue'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| 
 | ||||
| export { useQueryParam }; | ||||
| export { useQueryParam, useQueryParamOrStorage }; | ||||
| 
 | ||||
| const transformers = { | ||||
|   number: { | ||||
| @ -16,6 +17,12 @@ const transformers = { | ||||
|     fromQuery: (value: string) => value.toLowerCase() === 'true', | ||||
|     toQuery: (value: boolean) => (value ? 'true' : 'false'), | ||||
|   }, | ||||
|   object: { | ||||
|     fromQuery: (value: string) => { | ||||
|       return JSON.parse(value); | ||||
|     }, | ||||
|     toQuery: (value: object) => JSON.stringify(value), | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) { | ||||
| @ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) { | ||||
|   const type = typeof defaultValue; | ||||
|   const transformer = transformers[type as keyof typeof transformers] ?? transformers.string; | ||||
| 
 | ||||
|   const storageRef = useStorage(storageName, defaultValue); | ||||
|   const proxyDefaultValue = transformer.toQuery(defaultValue as never); | ||||
|   const proxy = useRouteQuery(name, proxyDefaultValue); | ||||
| 
 | ||||
|   const r = ref(defaultValue); | ||||
| 
 | ||||
|   watch(r, | ||||
|     (value) => { | ||||
|       proxy.value = transformer.toQuery(value as never); | ||||
|       storageRef.value = value as never; | ||||
|     }, | ||||
|     { deep: true }); | ||||
| 
 | ||||
|   r.value = (proxy.value && proxy.value !== proxyDefaultValue | ||||
|     ? transformer.fromQuery(proxy.value) as unknown as T | ||||
|     : storageRef.value as T) as never; | ||||
| 
 | ||||
|   return r; | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,44 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { getCronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; | ||||
| 
 | ||||
| describe('crontab-generator', () => { | ||||
|   describe('isCronValid', () => { | ||||
|     it('should return true for all valid formats', () => { | ||||
|       expect(isCronValid('0 0 * * 1-5')).toBe(true); | ||||
|       expect(isCronValid('23 0-20/2 * * *')).toBe(true); | ||||
| 
 | ||||
|       // AWS formats
 | ||||
|       expect(isCronValid('0 11-22 ? * MON-FRI *')).toBe(true); | ||||
|       expect(isCronValid('0 0 ? * 1 *')).toBe(true); | ||||
|     }); | ||||
|     it('should return false for all invalid formats', () => { | ||||
|       expect(isCronValid('aert')).toBe(false); | ||||
|       expect(isCronValid('40 *')).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getCronType', () => { | ||||
|     it('should return right type', () => { | ||||
|       expect(getCronType('0 0 * * 1-5')).toBe('standard'); | ||||
|       expect(getCronType('23 0-20/2 * * *')).toBe('standard'); | ||||
| 
 | ||||
|       // AWS formats
 | ||||
|       expect(getCronType('0 11-22 ? * MON-FRI *')).toBe('aws'); | ||||
|       expect(getCronType('0 0 ? * 1 *')).toBe('aws'); | ||||
| 
 | ||||
|       expect(getCronType('aert')).toBe(false); | ||||
|       expect(getCronType('40 *')).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getLastExecutionTimes', () => { | ||||
|     it('should return next valid datetimes', () => { | ||||
|       expect(getLastExecutionTimes('0 0 * * 1-5')).toHaveLength(5); | ||||
|       expect(getLastExecutionTimes('23 0-20/2 * * *')).toHaveLength(5); | ||||
| 
 | ||||
|       // AWS formats
 | ||||
|       expect(getLastExecutionTimes('0 11-22 ? * MON-FRI *')).toHaveLength(5); | ||||
|       expect(getLastExecutionTimes('0 0 ? * 1 *')).toHaveLength(5); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										44
									
								
								src/tools/crontab-generator/crontab-generator.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/tools/crontab-generator/crontab-generator.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| import { parseExpression } from 'cron-parser'; | ||||
| import EventCronParser from 'event-cron-parser'; | ||||
| 
 | ||||
| export function getLastExecutionTimes(cronExpression: string, tz: string | undefined = undefined, count: number = 5) { | ||||
|   if (getCronType(cronExpression) === 'standard') { | ||||
|     const interval = parseExpression(cronExpression, { tz }); | ||||
|     const times = []; | ||||
|     for (let i = 0; i < count; i++) { | ||||
|       times.push(interval.next().toJSON()); | ||||
|     } | ||||
|     return times; | ||||
|   } | ||||
|   if (getCronType(cronExpression) === 'aws') { | ||||
|     const parsed = new EventCronParser(cronExpression); | ||||
|     const times = []; | ||||
|     for (let i = 0; i < count; i++) { | ||||
|       times.push(JSON.stringify(parsed.next())); | ||||
|     } | ||||
|     return times; | ||||
|   } | ||||
| 
 | ||||
|   return []; | ||||
| } | ||||
| 
 | ||||
| export function isCronValid(v: string) { | ||||
|   return !!getCronType(v); | ||||
| } | ||||
| 
 | ||||
| export function getCronType(v: string) { | ||||
|   try { | ||||
|     parseExpression(v); | ||||
|     return 'standard'; | ||||
|   } | ||||
|   catch (_) { | ||||
|     try { | ||||
|       const parsed = new EventCronParser(v); | ||||
|       parsed.validate(); | ||||
|       return 'aws'; | ||||
|     } | ||||
|     catch (_) { | ||||
|     } | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
| @ -1,11 +1,10 @@ | ||||
| <script setup lang="ts"> | ||||
| import cronstrue from 'cronstrue'; | ||||
| import { isValidCron } from 'cron-validator'; | ||||
| import ctz from 'countries-and-timezones'; | ||||
| import getTimezoneOffset from 'get-timezone-offset'; | ||||
| import { getCronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| 
 | ||||
| function isCronValid(v: string) { | ||||
|   return isValidCron(v, { allowBlankDay: true, alias: true, seconds: true }); | ||||
| } | ||||
| import { useQueryParamOrStorage } from '@/composable/queryParams'; | ||||
| 
 | ||||
| const styleStore = useStyleStore(); | ||||
| 
 | ||||
| @ -15,9 +14,22 @@ const cronstrueConfig = reactive({ | ||||
|   dayOfWeekStartIndexZero: true, | ||||
|   use24HourTimeFormat: true, | ||||
|   throwExceptionOnParseError: true, | ||||
|   monthStartIndexZero: false, | ||||
|   tzOffset: (new Date()).getTimezoneOffset() / 60, | ||||
| }); | ||||
| 
 | ||||
| const helpers = [ | ||||
| // 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 currentTimezone = useQueryParamOrStorage({ name: 'tz', storageName: 'crongen:tz', defaultValue: browserTimezone }); | ||||
| watchEffect(() => { | ||||
|   cronstrueConfig.tzOffset = -getTimezoneOffset(currentTimezone.value, new Date()) / 60; | ||||
| }); | ||||
| 
 | ||||
| const standardHelpers = [ | ||||
|   { | ||||
|     symbol: '*', | ||||
|     meaning: 'Any value', | ||||
| @ -92,6 +104,77 @@ const helpers = [ | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| 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', | ||||
|   }, | ||||
|   { | ||||
|     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', | ||||
|     example: '9 * 7,9,11 5 ? 2021', | ||||
|     equivalent: 'At 9 minutes past the hour, every hour, on day 7, 9, and 11 of the month, only in May, only in 2021', | ||||
|   }, | ||||
|   { | ||||
|     symbol: 'L', | ||||
|     meaning: 'The L wildcard in the Day-of-month or Day-of-week fields specifies the last day of the month or week.', | ||||
|     example: '9 * L 5 ? 2019,2020', | ||||
|     equivalent: 'At 9 minutes past the hour, every hour, on the last day of the month, only in May, only in 2019 and 2020', | ||||
|   }, | ||||
|   { | ||||
|     symbol: 'W', | ||||
|     meaning: 'The W wildcard in the Day-of-month field specifies a weekday. In the Day-of-month field, 3W specifies the day closest to the third weekday of the month.', | ||||
|     example: '19 4 3W 9 ? 2019,2020', | ||||
|     equivalent: 'At 04:19 AM, on the weekday nearest day 3 of the month, only in September, only in 2019 and 2020', | ||||
|   }, | ||||
|   { | ||||
|     symbol: '#', | ||||
|     meaning: 'The # wildcard in the Day-of-week field specifies the nieth weekday of the month. 3#5 specifies the fifth Wednesday of the month', | ||||
|     example: '9 8-20 ? 12 3#5 2019,2020', | ||||
|     equivalent: 'At 9 minutes past the hour, between 08:00 AM and 08:59 PM, on the fifth Wednesday of the month, only in December, only in 2019 and 2020', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const cronType = computed({ | ||||
|   get() { | ||||
|     return getCronType(cron.value); | ||||
|   }, | ||||
|   set(newCronType) { | ||||
|     if (newCronType === 'aws') { | ||||
|       cron.value = '0 0 ? * 1 *'; | ||||
|     } | ||||
|     else { | ||||
|       cron.value = '40 * * * *'; | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| const getHelpers = computed(() => { | ||||
|   if (cronType.value === 'aws') { | ||||
|     return awsHelpers; | ||||
|   } | ||||
|   return standardHelpers; | ||||
| }); | ||||
| 
 | ||||
| const cronString = computed(() => { | ||||
|   if (isCronValid(cron.value)) { | ||||
|     return cronstrue.toString(cron.value, cronstrueConfig); | ||||
| @ -105,6 +188,20 @@ const cronValidationRules = [ | ||||
|     message: 'This cron is invalid', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const executionTimesString = computed(() => { | ||||
|   if (isCronValid(cron.value)) { | ||||
|     try { | ||||
|       const lastExecutionTimes = getLastExecutionTimes(cron.value, currentTimezone.value); | ||||
|       const executionTimesString = lastExecutionTimes.join('\n'); | ||||
|       return `Next 5 execution times:\n${executionTimesString}`; | ||||
|     } | ||||
|     catch (e: any) { | ||||
|       return e.toString(); | ||||
|     } | ||||
|   } | ||||
|   return ' '; | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| @ -119,10 +216,27 @@ const cronValidationRules = [ | ||||
|       /> | ||||
|     </div> | ||||
| 
 | ||||
|     <n-radio-group v-model:value="cronType" name="radiogroup" mb-2 flex justify-center> | ||||
|       <n-space> | ||||
|         <n-radio | ||||
|           value="standard" | ||||
|           label="Unix standard" | ||||
|         /> | ||||
|         <n-radio | ||||
|           value="aws" | ||||
|           label="AWS" | ||||
|         /> | ||||
|       </n-space> | ||||
|     </n-radio-group> | ||||
| 
 | ||||
|     <div class="cron-string"> | ||||
|       {{ cronString }} | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="cron-execution-string"> | ||||
|       {{ executionTimesString }} | ||||
|     </div> | ||||
| 
 | ||||
|     <n-divider /> | ||||
| 
 | ||||
|     <div flex justify-center> | ||||
| @ -136,11 +250,21 @@ const cronValidationRules = [ | ||||
|         <n-form-item label="Days start at 0"> | ||||
|           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||
|         </n-form-item> | ||||
|         <n-form-item label="Months start at 0"> | ||||
|           <n-switch v-model:value="cronstrueConfig.monthStartIndexZero" /> | ||||
|         </n-form-item> | ||||
|         <c-select | ||||
|           v-model:value="currentTimezone" | ||||
|           searchable | ||||
|           label="Timezone:" | ||||
|           :options="allTimezones" | ||||
|         /> | ||||
|       </n-form> | ||||
|     </div> | ||||
|   </c-card> | ||||
|   <c-card> | ||||
|     <pre> | ||||
|     <pre v-if="cronType === 'standard'"> | ||||
| -- Standard CRON Syntax -- | ||||
| ┌──────────── [optional] seconds (0 - 59) | ||||
| | ┌────────── minute (0 - 59) | ||||
| | | ┌──────── hour (0 - 23) | ||||
| @ -150,8 +274,19 @@ const cronValidationRules = [ | ||||
| | | | | | | | ||||
| * * * * * * command</pre> | ||||
| 
 | ||||
|     <pre v-if="cronType === 'aws'"> | ||||
| -- AWS CRON Syntax -- | ||||
| ┌──────────── minute (0 - 59) | ||||
| | ┌────────── hour (0 - 23) | ||||
| | | ┌──────── day of month (1 - 31) OR ? OR L OR W | ||||
| | | | ┌────── month (1 - 12) OR jan,feb,mar,apr ... | ||||
| | | | | ┌──── day of week (0 - 6, sunday=0) OR sun,mon OR L ... | ||||
| | | | | | ┌── year | ||||
| | | | | | | | ||||
| * * * * * *</pre> | ||||
| 
 | ||||
|     <div v-if="styleStore.isSmallScreen"> | ||||
|       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> | ||||
|       <c-card v-for="{ symbol, meaning, example, equivalent } in getHelpers" :key="symbol" mb-3 important:border-none> | ||||
|         <div> | ||||
|           Symbol: <strong>{{ symbol }}</strong> | ||||
|         </div> | ||||
| @ -168,7 +303,7 @@ const cronValidationRules = [ | ||||
|       </c-card> | ||||
|     </div> | ||||
| 
 | ||||
|     <c-table v-else :data="helpers" /> | ||||
|     <c-table v-else :data="getHelpers" /> | ||||
|   </c-card> | ||||
| </template> | ||||
| 
 | ||||
| @ -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; | ||||
| } | ||||
| </style> | ||||
|  | ||||
							
								
								
									
										3
									
								
								src/tools/crontab-generator/get-timezone-offset.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/tools/crontab-generator/get-timezone-offset.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| declare module "get-timezone-offset" { | ||||
|     export default function(timeZoneName: string, date: Date); | ||||
| } | ||||
| @ -20,6 +20,7 @@ export const tool = defineTool({ | ||||
|     'day', | ||||
|     'minute', | ||||
|     'second', | ||||
|     'aws', | ||||
|   ], | ||||
|   component: () => import('./crontab-generator.vue'), | ||||
|   icon: Alarm, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user