Skip to content

Commit 6331c9d

Browse files
committed
feat: add cron validation and next run time features to scheduled tasks
1 parent 7746003 commit 6331c9d

File tree

5 files changed

+71
-20
lines changed

5 files changed

+71
-20
lines changed

frontend/src/lang/locale/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ export default {
549549
empty: 'The scheduled task list is empty. Please{action}a scheduled task first.',
550550
run: 'Run now',
551551
log: 'View log',
552+
next: 'Next Run Time',
552553
},
553554
settings: {
554555
personalization: 'Personalization',

frontend/src/lang/locale/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ export default {
548548
empty: '计划任务列表为空,请先{action}计划任务。',
549549
run: '立即运行',
550550
log: '查看日志',
551+
next: '下次运行时间',
551552
},
552553
settings: {
553554
personalization: '个性化',

frontend/src/utils/is.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Cron } from 'croner'
12
import { parse } from 'yaml'
23

34
export const isValidBase64 = (str: string) => {
@@ -69,3 +70,12 @@ export const isValidJson = (str: string) => {
6970
}
7071

7172
export const isNumber = (v: any) => typeof v === 'number'
73+
74+
export const isValidCron = (pattern: string) => {
75+
try {
76+
const instance = new Cron(pattern, { paused: true })
77+
return { ok: true, reason: null, instance: instance }
78+
} catch (error: any) {
79+
return { ok: false, reason: error.message || error, instance: null }
80+
}
81+
}

frontend/src/views/ScheduledTasksView/components/ScheduledTaskForm.vue

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import { Cron } from 'croner'
32
import { ref, inject, h } from 'vue'
43
import { useI18n } from 'vue-i18n'
54
@@ -11,7 +10,7 @@ import {
1110
useRulesetsStore,
1211
usePluginsStore,
1312
} from '@/stores'
14-
import { deepClone, message, sampleID } from '@/utils'
13+
import { alert, deepClone, formatDate, isValidCron, message, sampleID } from '@/utils'
1514
1615
import Button from '@/components/Button/index.vue'
1716
@@ -46,18 +45,15 @@ const rulesetsStore = useRulesetsStore()
4645
const pluginsStore = usePluginsStore()
4746
4847
const handleCancel = inject('cancel') as any
48+
const handleSubmit = inject('submit') as any
4949
50-
const handleSubmit = async () => {
51-
try {
52-
const job = new Cron(task.value.cron, () => {})
53-
job.stop()
54-
} catch (error: any) {
55-
message.error(error.message)
50+
const handleSave = async () => {
51+
const { ok, reason } = isValidCron(task.value.cron)
52+
if (!ok) {
53+
message.error(reason)
5654
return
5755
}
5856
59-
loading.value = true
60-
6157
switch (task.value.type) {
6258
case ScheduledTasksType.UpdateSubscription:
6359
task.value.subscriptions = task.value.subscriptions.filter((id) =>
@@ -73,13 +69,15 @@ const handleSubmit = async () => {
7369
break
7470
}
7571
72+
loading.value = true
73+
7674
try {
7775
if (props.id) {
7876
await scheduledTasksStore.editScheduledTask(props.id, task.value)
7977
} else {
8078
await scheduledTasksStore.addScheduledTask(task.value)
8179
}
82-
handleCancel()
80+
await handleSubmit()
8381
} catch (error: any) {
8482
console.error(error)
8583
message.error(error)
@@ -97,6 +95,28 @@ const handleUse = (list: string[], id: string) => {
9795
}
9896
}
9997
98+
const handleValidate = () => {
99+
const { ok, reason } = isValidCron(task.value.cron)
100+
if (!ok) {
101+
message.error(reason)
102+
return
103+
}
104+
message.success('common.success')
105+
}
106+
107+
const handleViewNextRuns = () => {
108+
const { ok, reason, instance } = isValidCron(task.value.cron)
109+
if (!ok) {
110+
message.error(reason)
111+
return
112+
}
113+
const list = instance!.nextRuns(99).map((v, i) => {
114+
const index = (i + 1).toString().padStart(2, '0')
115+
return index + ' - '.repeat(14) + formatDate(v.getTime(), 'YYYY/MM/DD HH:mm:ss')
116+
})
117+
alert('Next Run Time', list.join('\n'))
118+
}
119+
100120
if (props.id) {
101121
const s = scheduledTasksStore.getScheduledTaskById(props.id)
102122
if (s) {
@@ -121,7 +141,7 @@ const modalSlots = {
121141
type: 'primary',
122142
loading: loading.value,
123143
disabled: !task.value.name || !task.value.cron,
124-
onClick: handleSubmit,
144+
onClick: handleSave,
125145
},
126146
() => t('common.save'),
127147
),
@@ -141,7 +161,14 @@ defineExpose({ modalSlots })
141161
<div class="form-item">
142162
{{ t('scheduledtask.cron') }} *
143163
<div class="min-w-[75%]">
144-
<Input v-model="task.cron" :placeholder="t('scheduledtask.cronTips')" class="w-full" />
164+
<Input v-model="task.cron" :placeholder="t('scheduledtask.cronTips')" class="w-full">
165+
<template #suffix>
166+
<Button @click="handleValidate" type="primary" size="small">Validate</Button>
167+
<Button @click="handleViewNextRuns" type="primary" size="small" class="ml-4">
168+
Next Run Time
169+
</Button>
170+
</template>
171+
</Input>
145172
</div>
146173
</div>
147174
<div class="form-item">

frontend/src/views/ScheduledTasksView/index.vue

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script setup lang="ts">
2+
import { Cron } from 'croner'
23
import { useI18n, I18nT } from 'vue-i18n'
34
4-
import { DraggableOptions } from '@/constant'
5+
import { DraggableOptions, ViewOptions } from '@/constant'
56
import { View } from '@/enums/app'
67
import { useAppSettingsStore, useScheduledTasksStore } from '@/stores'
7-
import { debounce, formatRelativeTime, formatDate, message } from '@/utils'
8+
import { debounce, formatRelativeTime, formatDate, message, alert } from '@/utils'
89
910
import { useModal } from '@/components/Modal'
1011
@@ -20,6 +21,19 @@ const menuList: Menu[] = [
2021
scheduledTasksStore.runScheduledTask(id)
2122
},
2223
},
24+
{
25+
label: 'scheduledtasks.next',
26+
handler: (id: string) => {
27+
const task = scheduledTasksStore.getScheduledTaskById(id)
28+
if (task) {
29+
const list = new Cron(task.cron).nextRuns(99).map((v, i) => {
30+
const index = (i + 1).toString().padStart(2, '0')
31+
return index + ' - '.repeat(14) + formatDate(v.getTime(), 'YYYY/MM/DD HH:mm:ss')
32+
})
33+
alert('Next Run Time', list.join('\n'))
34+
}
35+
},
36+
},
2337
{
2438
label: 'scheduledtasks.log',
2539
handler: (id: string) => {
@@ -93,12 +107,10 @@ const onSortUpdate = debounce(scheduledTasksStore.saveScheduledTasks, 1000)
93107
<div v-else class="grid-list-header">
94108
<Radio
95109
v-model="appSettingsStore.app.scheduledtasksView"
96-
:options="[
97-
{ label: 'common.grid', value: View.Grid },
98-
{ label: 'common.list', value: View.List },
99-
]"
110+
:options="ViewOptions"
111+
class="mr-auto"
100112
/>
101-
<Button @click="handleShowTaskLogs()" type="text" class="ml-auto">
113+
<Button @click="handleShowTaskLogs()" type="text">
102114
{{ t('scheduledtasks.logs') }}
103115
</Button>
104116
<Button @click="handleShowTaskForm()" type="primary" icon="add" class="ml-16">

0 commit comments

Comments
 (0)