mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-04 05:19:12 -04:00
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
9 changed files with 339 additions and 18 deletions
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…
Add table
Add a link
Reference in a new issue