Merge fa01008dc8 into b47d132839
				
					
				
			This commit is contained in:
		
						commit
						f49dc46ed7
					
				
							
								
								
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -135,13 +135,18 @@ declare module '@vue/runtime-core' { | |||||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] |     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||||
|     NDivider: typeof import('naive-ui')['NDivider'] |     NDivider: typeof import('naive-ui')['NDivider'] | ||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|  |     NForm: typeof import('naive-ui')['NForm'] | ||||||
|  |     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||||
|     NH1: typeof import('naive-ui')['NH1'] |     NH1: typeof import('naive-ui')['NH1'] | ||||||
|     NH3: typeof import('naive-ui')['NH3'] |     NH3: typeof import('naive-ui')['NH3'] | ||||||
|     NIcon: typeof import('naive-ui')['NIcon'] |     NIcon: typeof import('naive-ui')['NIcon'] | ||||||
|     NLayout: typeof import('naive-ui')['NLayout'] |     NLayout: typeof import('naive-ui')['NLayout'] | ||||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] |     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||||
|     NMenu: typeof import('naive-ui')['NMenu'] |     NMenu: typeof import('naive-ui')['NMenu'] | ||||||
|  |     NRadio: typeof import('naive-ui')['NRadio'] | ||||||
|  |     NRadioGroup: typeof import('naive-ui')['NRadioGroup'] | ||||||
|     NSpace: typeof import('naive-ui')['NSpace'] |     NSpace: typeof import('naive-ui')['NSpace'] | ||||||
|  |     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||||
|     NTable: typeof import('naive-ui')['NTable'] |     NTable: typeof import('naive-ui')['NTable'] | ||||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] |     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'] |     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||||
|  | |||||||
| @ -55,7 +55,9 @@ | |||||||
|     "change-case": "^4.1.2", |     "change-case": "^4.1.2", | ||||||
|     "colord": "^2.9.3", |     "colord": "^2.9.3", | ||||||
|     "composerize-ts": "^0.6.2", |     "composerize-ts": "^0.6.2", | ||||||
|  |     "countries-and-timezones": "^3.6.0", | ||||||
|     "country-code-lookup": "^0.1.0", |     "country-code-lookup": "^0.1.0", | ||||||
|  |     "cron-parser": "^4.9.0", | ||||||
|     "cron-validator": "^1.3.1", |     "cron-validator": "^1.3.1", | ||||||
|     "cronstrue": "^2.26.0", |     "cronstrue": "^2.26.0", | ||||||
|     "crypto-js": "^4.1.1", |     "crypto-js": "^4.1.1", | ||||||
| @ -63,9 +65,11 @@ | |||||||
|     "dompurify": "^3.0.6", |     "dompurify": "^3.0.6", | ||||||
|     "email-normalizer": "^1.0.0", |     "email-normalizer": "^1.0.0", | ||||||
|     "emojilib": "^3.0.10", |     "emojilib": "^3.0.10", | ||||||
|  |     "event-cron-parser": "^1.0.34", | ||||||
|     "figlet": "^1.7.0", |     "figlet": "^1.7.0", | ||||||
|     "figue": "^1.2.0", |     "figue": "^1.2.0", | ||||||
|     "fuse.js": "^6.6.2", |     "fuse.js": "^6.6.2", | ||||||
|  |     "get-timezone-offset": "^1.0.5", | ||||||
|     "highlight.js": "^11.7.0", |     "highlight.js": "^11.7.0", | ||||||
|     "iarna-toml-esm": "^3.0.5", |     "iarna-toml-esm": "^3.0.5", | ||||||
|     "ibantools": "^4.3.3", |     "ibantools": "^4.3.3", | ||||||
|  | |||||||
							
								
								
									
										50
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										50
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -62,9 +62,15 @@ dependencies: | |||||||
|   composerize-ts: |   composerize-ts: | ||||||
|     specifier: ^0.6.2 |     specifier: ^0.6.2 | ||||||
|     version: 0.6.2 |     version: 0.6.2 | ||||||
|  |   countries-and-timezones: | ||||||
|  |     specifier: ^3.6.0 | ||||||
|  |     version: 3.6.0 | ||||||
|   country-code-lookup: |   country-code-lookup: | ||||||
|     specifier: ^0.1.0 |     specifier: ^0.1.0 | ||||||
|     version: 0.1.0 |     version: 0.1.0 | ||||||
|  |   cron-parser: | ||||||
|  |     specifier: ^4.9.0 | ||||||
|  |     version: 4.9.0 | ||||||
|   cron-validator: |   cron-validator: | ||||||
|     specifier: ^1.3.1 |     specifier: ^1.3.1 | ||||||
|     version: 1.3.1 |     version: 1.3.1 | ||||||
| @ -86,6 +92,9 @@ dependencies: | |||||||
|   emojilib: |   emojilib: | ||||||
|     specifier: ^3.0.10 |     specifier: ^3.0.10 | ||||||
|     version: 3.0.10 |     version: 3.0.10 | ||||||
|  |   event-cron-parser: | ||||||
|  |     specifier: ^1.0.34 | ||||||
|  |     version: 1.0.34 | ||||||
|   figlet: |   figlet: | ||||||
|     specifier: ^1.7.0 |     specifier: ^1.7.0 | ||||||
|     version: 1.7.0 |     version: 1.7.0 | ||||||
| @ -95,6 +104,9 @@ dependencies: | |||||||
|   fuse.js: |   fuse.js: | ||||||
|     specifier: ^6.6.2 |     specifier: ^6.6.2 | ||||||
|     version: 6.6.2 |     version: 6.6.2 | ||||||
|  |   get-timezone-offset: | ||||||
|  |     specifier: ^1.0.5 | ||||||
|  |     version: 1.0.5 | ||||||
|   highlight.js: |   highlight.js: | ||||||
|     specifier: ^11.7.0 |     specifier: ^11.7.0 | ||||||
|     version: 11.7.0 |     version: 11.7.0 | ||||||
| @ -3412,7 +3424,7 @@ packages: | |||||||
|     dependencies: |     dependencies: | ||||||
|       '@unhead/dom': 0.5.1 |       '@unhead/dom': 0.5.1 | ||||||
|       '@unhead/schema': 0.5.1 |       '@unhead/schema': 0.5.1 | ||||||
|       '@vueuse/shared': 11.0.3(vue@3.3.4) |       '@vueuse/shared': 11.1.0(vue@3.3.4) | ||||||
|       unhead: 0.5.1 |       unhead: 0.5.1 | ||||||
|       vue: 3.3.4 |       vue: 3.3.4 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @ -4054,8 +4066,8 @@ packages: | |||||||
|       - vue |       - vue | ||||||
|     dev: false |     dev: false | ||||||
| 
 | 
 | ||||||
|   /@vueuse/shared@11.0.3(vue@3.3.4): |   /@vueuse/shared@11.1.0(vue@3.3.4): | ||||||
|     resolution: {integrity: sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==} |     resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} | ||||||
|     dependencies: |     dependencies: | ||||||
|       vue-demi: 0.14.10(vue@3.3.4) |       vue-demi: 0.14.10(vue@3.3.4) | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
| @ -4674,6 +4686,11 @@ packages: | |||||||
|       browserslist: 4.22.1 |       browserslist: 4.22.1 | ||||||
|     dev: true |     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: |   /country-code-lookup@0.1.0: | ||||||
|     resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==} |     resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==} | ||||||
|     dev: false |     dev: false | ||||||
| @ -4682,6 +4699,13 @@ packages: | |||||||
|     resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} |     resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} | ||||||
|     dev: false |     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: |   /cron-validator@1.3.1: | ||||||
|     resolution: {integrity: sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==} |     resolution: {integrity: sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==} | ||||||
|     dev: false |     dev: false | ||||||
| @ -5583,6 +5607,12 @@ packages: | |||||||
|     engines: {node: '>=0.10.0'} |     engines: {node: '>=0.10.0'} | ||||||
|     dev: true |     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: |   /event-stream@3.3.4: | ||||||
|     resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} |     resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -5877,6 +5907,11 @@ packages: | |||||||
|       get-intrinsic: 1.2.2 |       get-intrinsic: 1.2.2 | ||||||
|     dev: true |     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: |   /get-tsconfig@4.7.2: | ||||||
|     resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} |     resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -6834,6 +6869,11 @@ packages: | |||||||
|     dependencies: |     dependencies: | ||||||
|       yallist: 4.0.0 |       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: |   /magic-string@0.25.9: | ||||||
|     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} |     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} | ||||||
|     dependencies: |     dependencies: | ||||||
| @ -7203,6 +7243,10 @@ packages: | |||||||
|       boolbase: 1.0.0 |       boolbase: 1.0.0 | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /number-to-words@1.2.4: | ||||||
|  |     resolution: {integrity: sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==} | ||||||
|  |     dev: false | ||||||
|  | 
 | ||||||
|   /nwsapi@2.2.7: |   /nwsapi@2.2.7: | ||||||
|     resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} |     resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} | ||||||
|     dev: true |     dev: true | ||||||
|  | |||||||
| @ -0,0 +1,63 @@ | |||||||
|  | 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', () => { | ||||||
|  |       // standard format
 | ||||||
|  |       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 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); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   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); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										47
									
								
								src/tools/crontab-generator/crontab-generator.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/tools/crontab-generator/crontab-generator.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | 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 }); | ||||||
|  |     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(cronExpression: string, cronType: CronType | 'any' = 'any') { | ||||||
|  |   const expressionCronType = getCronType(cronExpression); | ||||||
|  |   return cronType === 'any' ? !!expressionCronType : expressionCronType === cronType; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getCronType(cronExpression: string) { | ||||||
|  |   try { | ||||||
|  |     parseExpression(cronExpression); | ||||||
|  |     return 'standard'; | ||||||
|  |   } | ||||||
|  |   catch (_) { | ||||||
|  |     try { | ||||||
|  |       const parsed = new EventCronParser(cronExpression); | ||||||
|  |       parsed.validate(); | ||||||
|  |       return 'aws'; | ||||||
|  |     } | ||||||
|  |     catch (_) { | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return false; | ||||||
|  | } | ||||||
| @ -1,11 +1,10 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import cronstrue from 'cronstrue'; | import cronstrue from 'cronstrue'; | ||||||
| import { isValidCron } from 'cron-validator'; | import ctz from 'countries-and-timezones'; | ||||||
|  | import getTimezoneOffset from 'get-timezone-offset'; | ||||||
|  | import { type CronType, getLastExecutionTimes, isCronValid } from './crontab-generator.service'; | ||||||
| import { useStyleStore } from '@/stores/style.store'; | import { useStyleStore } from '@/stores/style.store'; | ||||||
| 
 | import { useQueryParamOrStorage } from '@/composable/queryParams'; | ||||||
| function isCronValid(v: string) { |  | ||||||
|   return isValidCron(v, { allowBlankDay: true, alias: true, seconds: true }); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| const styleStore = useStyleStore(); | const styleStore = useStyleStore(); | ||||||
| 
 | 
 | ||||||
| @ -15,9 +14,25 @@ const cronstrueConfig = reactive({ | |||||||
|   dayOfWeekStartIndexZero: true, |   dayOfWeekStartIndexZero: true, | ||||||
|   use24HourTimeFormat: true, |   use24HourTimeFormat: true, | ||||||
|   throwExceptionOnParseError: 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) => { | ||||||
|  |   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; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const commonHelpers = [ | ||||||
|   { |   { | ||||||
|     symbol: '*', |     symbol: '*', | ||||||
|     meaning: 'Any value', |     meaning: 'Any value', | ||||||
| @ -42,6 +57,10 @@ const helpers = [ | |||||||
|     example: '*/10 * * *', |     example: '*/10 * * *', | ||||||
|     equivalent: 'Every 10 minutes', |     equivalent: 'Every 10 minutes', | ||||||
|   }, |   }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | const standardHelpers = [ | ||||||
|  |   ...commonHelpers, | ||||||
|   { |   { | ||||||
|     symbol: '@yearly', |     symbol: '@yearly', | ||||||
|     meaning: 'Once every year at midnight of 1 January', |     meaning: 'Once every year at midnight of 1 January', | ||||||
| @ -92,6 +111,59 @@ const helpers = [ | |||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | const awsHelpers = [ | ||||||
|  |   ...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', | ||||||
|  |     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 defaultAWSCronExpression = '0 0 ? * 1 *'; | ||||||
|  | const defaultStandardCronExpression = '40 * * * *'; | ||||||
|  | const cronType = ref<CronType>('standard'); | ||||||
|  | watch(cronType, | ||||||
|  |   (newCronType) => { | ||||||
|  |     if (newCronType === 'aws') { | ||||||
|  |       if (!cron.value || cron.value === defaultStandardCronExpression) { | ||||||
|  |         cron.value = defaultAWSCronExpression; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     else if (newCronType === 'standard') { | ||||||
|  |       if (!cron.value || cron.value === defaultAWSCronExpression) { | ||||||
|  |         cron.value = defaultStandardCronExpression; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const getHelpers = computed(() => { | ||||||
|  |   if (cronType.value === 'aws') { | ||||||
|  |     return awsHelpers; | ||||||
|  |   } | ||||||
|  |   return standardHelpers; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| const cronString = computed(() => { | const cronString = computed(() => { | ||||||
|   if (isCronValid(cron.value)) { |   if (isCronValid(cron.value)) { | ||||||
|     return cronstrue.toString(cron.value, cronstrueConfig); |     return cronstrue.toString(cron.value, cronstrueConfig); | ||||||
| @ -101,10 +173,24 @@ const cronString = computed(() => { | |||||||
| 
 | 
 | ||||||
| const cronValidationRules = [ | const cronValidationRules = [ | ||||||
|   { |   { | ||||||
|     validator: (value: string) => isCronValid(value), |     validator: (value: string) => isCronValid(value, cronType.value), | ||||||
|     message: 'This cron is invalid', |     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> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -119,10 +205,27 @@ const cronValidationRules = [ | |||||||
|       /> |       /> | ||||||
|     </div> |     </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"> |     <div class="cron-string"> | ||||||
|       {{ cronString }} |       {{ cronString }} | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|  |     <div class="cron-execution-string"> | ||||||
|  |       {{ executionTimesString }} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|     <n-divider /> |     <n-divider /> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
| @ -136,11 +239,21 @@ const cronValidationRules = [ | |||||||
|         <n-form-item label="Days start at 0"> |         <n-form-item label="Days start at 0"> | ||||||
|           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> |           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||||
|         </n-form-item> |         </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> |       </n-form> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
|   <c-card> |   <c-card> | ||||||
|     <pre> |     <pre v-if="cronType === 'standard'"> | ||||||
|  |       -- Standard CRON Syntax -- | ||||||
| ┌──────────── [optional] seconds (0 - 59) | ┌──────────── [optional] seconds (0 - 59) | ||||||
| | ┌────────── minute (0 - 59) | | ┌────────── minute (0 - 59) | ||||||
| | | ┌──────── hour (0 - 23) | | | ┌──────── hour (0 - 23) | ||||||
| @ -150,8 +263,19 @@ const cronValidationRules = [ | |||||||
| | | | | | | | | | | | | | | ||||||
| * * * * * * command</pre> | * * * * * * 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"> |     <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> |         <div> | ||||||
|           Symbol: <strong>{{ symbol }}</strong> |           Symbol: <strong>{{ symbol }}</strong> | ||||||
|         </div> |         </div> | ||||||
| @ -168,7 +292,7 @@ const cronValidationRules = [ | |||||||
|       </c-card> |       </c-card> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <c-table v-else :data="helpers" /> |     <c-table v-else :data="getHelpers" /> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| @ -191,4 +315,12 @@ pre { | |||||||
|   overflow: auto; |   overflow: auto; | ||||||
|   padding: 10px 0; |   padding: 10px 0; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .cron-execution-string{ | ||||||
|  |   text-align: center; | ||||||
|  |   font-size: 14px; | ||||||
|  |   opacity: 0.8; | ||||||
|  |   margin: 5px 0 15px; | ||||||
|  |   white-space: pre-wrap; | ||||||
|  | } | ||||||
| </style> | </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', |     'day', | ||||||
|     'minute', |     'minute', | ||||||
|     'second', |     'second', | ||||||
|  |     'aws', | ||||||
|   ], |   ], | ||||||
|   component: () => import('./crontab-generator.vue'), |   component: () => import('./crontab-generator.vue'), | ||||||
|   icon: Alarm, |   icon: Alarm, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user