From 72311617d9c30b0ea80d6c09f0f8c8c50e960d24 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:11:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A1=E5=88=92=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E9=A2=84=E8=A7=88=E6=9C=80=E8=BF=91=E4=BA=94?= =?UTF-8?q?=E6=AC=A1=E6=89=A7=E8=A1=8C=E6=97=B6=E9=97=B4=20(#6685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #4831 --- agent/app/api/v2/cronjob.go | 22 ++ agent/app/dto/cronjob.go | 27 ++- agent/app/model/cronjob.go | 7 +- agent/app/service/cronjob.go | 19 ++ agent/init/migration/migrations/init.go | 2 +- agent/router/ro_cronjob.go | 1 + frontend/src/api/interface/cronjob.ts | 4 + frontend/src/api/modules/cronjob.ts | 4 + frontend/src/lang/modules/en.ts | 2 + frontend/src/lang/modules/tw.ts | 2 + frontend/src/lang/modules/zh.ts | 2 + frontend/src/views/cronjob/helper.ts | 50 ++-- frontend/src/views/cronjob/index.vue | 2 +- frontend/src/views/cronjob/operate/index.vue | 230 +++++++++++++------ 14 files changed, 264 insertions(+), 110 deletions(-) diff --git a/agent/app/api/v2/cronjob.go b/agent/app/api/v2/cronjob.go index 8724bc220..7d25e17ac 100644 --- a/agent/app/api/v2/cronjob.go +++ b/agent/app/api/v2/cronjob.go @@ -32,6 +32,28 @@ func (b *BaseApi) CreateCronjob(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags Cronjob +// @Summary Load cronjob spec time +// @Description 预览最近五次执行时间 +// @Accept json +// @Param request body dto.CronjobSpec true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /cronjobs/next [post] +func (b *BaseApi) LoadNextHandle(c *gin.Context) { + var req dto.CronjobSpec + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + list, err := cronjobService.LoadNextHandle(req.Spec) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, list) +} + // @Tags Cronjob // @Summary Page cronjobs // @Description 获取计划任务分页 diff --git a/agent/app/dto/cronjob.go b/agent/app/dto/cronjob.go index 5332bfe17..f17307ca0 100644 --- a/agent/app/dto/cronjob.go +++ b/agent/app/dto/cronjob.go @@ -11,10 +11,15 @@ type PageCronjob struct { Order string `json:"order" validate:"required,oneof=null ascending descending"` } -type CronjobCreate struct { - Name string `json:"name" validate:"required"` - Type string `json:"type" validate:"required"` +type CronjobSpec struct { Spec string `json:"spec" validate:"required"` +} + +type CronjobCreate struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + SpecCustom bool `json:"specCustom"` + Spec string `json:"spec" validate:"required"` Script string `json:"script"` Command string `json:"command"` @@ -34,9 +39,10 @@ type CronjobCreate struct { } type CronjobUpdate struct { - ID uint `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Spec string `json:"spec" validate:"required"` + ID uint `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + SpecCustom bool `json:"specCustom"` + Spec string `json:"spec" validate:"required"` Script string `json:"script"` Command string `json:"command"` @@ -77,10 +83,11 @@ type CronjobBatchDelete struct { } type CronjobInfo struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Spec string `json:"spec"` + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + SpecCustom bool `json:"specCustom"` + Spec string `json:"spec"` Script string `json:"script"` Command string `json:"command"` diff --git a/agent/app/model/cronjob.go b/agent/app/model/cronjob.go index a88f8d30a..5709ee082 100644 --- a/agent/app/model/cronjob.go +++ b/agent/app/model/cronjob.go @@ -9,9 +9,10 @@ import ( type Cronjob struct { BaseModel - Name string `gorm:"not null" json:"name"` - Type string `gorm:"not null" json:"type"` - Spec string `gorm:"not null" json:"spec"` + Name string `gorm:"not null" json:"name"` + Type string `gorm:"not null" json:"type"` + SpecCustom bool `json:"specCustom"` + Spec string `gorm:"not null" json:"spec"` Command string `json:"command"` ContainerName string `json:"containerName"` diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index e893d4481..8ba9d54c1 100644 --- a/agent/app/service/cronjob.go +++ b/agent/app/service/cronjob.go @@ -24,6 +24,7 @@ type ICronjobService interface { SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) Create(cronjobDto dto.CronjobCreate) error + LoadNextHandle(spec string) ([]string, error) HandleOnce(id uint) error Update(id uint, req dto.CronjobUpdate) error UpdateStatus(id uint, status string) error @@ -77,6 +78,23 @@ func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interfac return total, dtoCronjobs, err } +func (u *CronjobService) LoadNextHandle(specStr string) ([]string, error) { + spec := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + sched, err := spec.Parse(specStr) + if err != nil { + return nil, err + } + now := time.Now() + var nexts [5]string + for i := 0; i < 5; i++ { + nextTime := sched.Next(now) + nexts[i] = nextTime.Format("2006-01-02 15:04:05") + fmt.Println(nextTime) + now = nextTime + } + return nexts[:], nil +} + func (u *CronjobService) LoadRecordLog(req dto.OperateByID) string { record, err := cronjobRepo.GetRecord(commonRepo.WithByID(req.ID)) if err != nil { @@ -261,6 +279,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error { } upMap["name"] = req.Name + upMap["spec_custom"] = req.SpecCustom upMap["spec"] = spec upMap["script"] = req.Script upMap["command"] = req.Command diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 15c19b131..0d68399f7 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -16,7 +16,7 @@ import ( ) var AddTable = &gormigrate.Migration{ - ID: "20240903-add-table", + ID: "20241009-add-table", Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate( &model.AppDetail{}, diff --git a/agent/router/ro_cronjob.go b/agent/router/ro_cronjob.go index ab064212c..667e71ead 100644 --- a/agent/router/ro_cronjob.go +++ b/agent/router/ro_cronjob.go @@ -12,6 +12,7 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) { baseApi := v2.ApiGroupApp.BaseApi { cmdRouter.POST("", baseApi.CreateCronjob) + cmdRouter.POST("/next", baseApi.LoadNextHandle) cmdRouter.POST("/del", baseApi.DeleteCronjob) cmdRouter.POST("/update", baseApi.UpdateCronjob) cmdRouter.POST("/status", baseApi.UpdateCronjobStatus) diff --git a/frontend/src/api/interface/cronjob.ts b/frontend/src/api/interface/cronjob.ts index 0ccd9582d..89d385285 100644 --- a/frontend/src/api/interface/cronjob.ts +++ b/frontend/src/api/interface/cronjob.ts @@ -5,7 +5,9 @@ export namespace Cronjob { id: number; name: string; type: string; + specCustom: boolean; spec: string; + specs: Array; specObjs: Array; script: string; @@ -31,6 +33,7 @@ export namespace Cronjob { export interface CronjobCreate { name: string; type: string; + specCustom: boolean; spec: string; specObjs: Array; @@ -57,6 +60,7 @@ export namespace Cronjob { } export interface CronjobUpdate { id: number; + specCustom: boolean; spec: string; script: string; diff --git a/frontend/src/api/modules/cronjob.ts b/frontend/src/api/modules/cronjob.ts index 25ba7c972..47b8baf0a 100644 --- a/frontend/src/api/modules/cronjob.ts +++ b/frontend/src/api/modules/cronjob.ts @@ -7,6 +7,10 @@ export const getCronjobPage = (params: SearchWithPage) => { return http.post>(`/cronjobs/search`, params); }; +export const loadNextHandle = (spec: string) => { + return http.post>(`/cronjobs/next`, { spec: spec }); +}; + export const getRecordLog = (id: number) => { return http.post(`/cronjobs/records/log`, { id: id }); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a296e2891..64bb6019f 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -857,6 +857,7 @@ const message = { enableMsg: 'Enabling the scheduled task will allow the task to automatically execute on a regular basis. Do you want to continue?', taskType: 'Type', + nextTime: 'Next 5 executions', record: 'Records', shell: 'Shell', log: 'Backup Logs', @@ -902,6 +903,7 @@ const message = { retainCopiesHelper1: 'Number of copies to retain for backup files', retainCopiesUnit: ' copies (View)', cronSpecRule: 'The execution period format in line {0} is incorrect. Please check and try again!', + cronSpecRule2: 'Execution period format is incorrect, please check and try again!', perMonth: 'Every monthly', perWeek: 'Every week', perHour: 'Every hour', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 75672ffd1..0030044cc 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -818,6 +818,7 @@ const message = { disableMsg: '停止計劃任務會導致該任務不再自動執行。是否繼續?', enableMsg: '啟用計劃任務會讓該任務定期自動執行。是否繼續?', taskType: '任務類型', + nextTime: '近 5 次執行', record: '報告', shell: 'Shell 腳本', log: '備份日誌', @@ -857,6 +858,7 @@ const message = { retainCopiesHelper1: '備份文件保留份数', retainCopiesUnit: ' 份 (查看)', cronSpecRule: '第 {0} 行中執行週期格式錯誤,請檢查後重試!', + cronSpecRule2: '執行週期格式錯誤,請檢查後重試!', perMonth: '每月', perWeek: '每周', perHour: '每小時', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 1982fcadf..04aca3b54 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -819,6 +819,7 @@ const message = { disableMsg: '停止计划任务会导致该任务不再自动执行。是否继续?', enableMsg: '启用计划任务会让该任务定期自动执行。是否继续?', taskType: '任务类型', + nextTime: '近 5 次执行', record: '报告', shell: 'Shell 脚本', log: '备份日志', @@ -858,6 +859,7 @@ const message = { retainCopiesHelper1: '备份文件保留份数', retainCopiesUnit: ' 份 (查看)', cronSpecRule: '第 {0} 行中执行周期格式错误,请检查后重试!', + cronSpecRule2: '执行周期格式错误,请检查后重试!', perMonth: '每月', perWeek: '每周', perHour: '每小时', diff --git a/frontend/src/views/cronjob/helper.ts b/frontend/src/views/cronjob/helper.ts index e0bb3684b..98d17798d 100644 --- a/frontend/src/views/cronjob/helper.ts +++ b/frontend/src/views/cronjob/helper.ts @@ -39,35 +39,22 @@ export function loadDefaultSpec(type: string) { item.second = 0; switch (type) { case 'shell': + case 'clean': + case 'website': + case 'log': + case 'snapshot': + case 'curl': item.specType = 'perWeek'; item.week = 1; item.hour = 1; item.minute = 30; break; case 'app': - item.specType = 'perDay'; - item.hour = 2; - item.minute = 30; - break; case 'database': item.specType = 'perDay'; item.hour = 2; item.minute = 30; break; - case 'clean': - case 'website': - item.specType = 'perWeek'; - item.week = 1; - item.hour = 1; - item.minute = 30; - break; - case 'log': - case 'snapshot': - item.specType = 'perWeek'; - item.week = 1; - item.hour = 1; - item.minute = 30; - break; case 'directory': case 'cutWebsiteLog': case 'ntp': @@ -75,16 +62,31 @@ export function loadDefaultSpec(type: string) { item.hour = 1; item.minute = 30; break; - case 'curl': - item.specType = 'perWeek'; - item.week = 1; - item.hour = 1; - item.minute = 30; - break; } return item; } +export function loadDefaultSpecCustom(type: string) { + switch (type) { + case 'shell': + case 'clean': + case 'website': + case 'log': + case 'snapshot': + case 'curl': + return '30 1 * * 1'; + case 'app': + case 'database': + return '30 2 * * *'; + case 'directory': + case 'cutWebsiteLog': + case 'ntp': + return '30 1 * * *'; + default: + return '30 1 * * 1'; + } +} + export function transObjToSpec(specType: string, week, day, hour, minute, second): string { switch (specType) { case 'perMonth': diff --git a/frontend/src/views/cronjob/index.vue b/frontend/src/views/cronjob/index.vue index d3a8ff99e..d5285f530 100644 --- a/frontend/src/views/cronjob/index.vue +++ b/frontend/src/views/cronjob/index.vue @@ -79,7 +79,7 @@
- {{ transSpecToStr(item) }} + {{ row.specCustom ? item : transSpecToStr(item) }}
diff --git a/frontend/src/views/cronjob/operate/index.vue b/frontend/src/views/cronjob/operate/index.vue index b67d0dd1b..6beced29b 100644 --- a/frontend/src/views/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/operate/index.vue @@ -68,67 +68,113 @@ - -
- - - - - - - - - - - - - - - - - - - - {{ $t('commons.button.delete') }} - - -
+ + - - {{ $t('commons.button.add') }} - +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ {{ time }} +
+ +
+ + {{ $t('commons.button.delete') }} + + +
+
+ + {{ $t('commons.button.add') }} + +
+ +
+ +
+ + +
+ {{ time }} +
+ +
+ + {{ $t('commons.button.delete') }} + + +
+
+ + {{ $t('commons.button.add') }} + +
@@ -336,7 +382,7 @@ import { getBackupList } from '@/api/modules/backup'; import i18n from '@/lang'; import { ElForm } from 'element-plus'; import { Cronjob } from '@/api/interface/cronjob'; -import { addCronjob, editCronjob } from '@/api/modules/cronjob'; +import { addCronjob, editCronjob, loadNextHandle } from '@/api/modules/cronjob'; import { listDbItems } from '@/api/modules/database'; import { GetWebsiteOptions } from '@/api/modules/website'; import { MsgError, MsgSuccess } from '@/utils/message'; @@ -344,7 +390,14 @@ import { useRouter } from 'vue-router'; import { listContainer } from '@/api/modules/container'; import { Database } from '@/api/interface/database'; import { ListAppInstalled } from '@/api/modules/app'; -import { loadDefaultSpec, specOptions, transObjToSpec, transSpecToObj, weekOptions } from './../helper'; +import { + loadDefaultSpec, + loadDefaultSpecCustom, + specOptions, + transObjToSpec, + transSpecToObj, + weekOptions, +} from './../helper'; const router = useRouter(); interface DialogProps { @@ -357,16 +410,22 @@ const drawerVisible = ref(false); const dialogData = ref({ title: '', }); +const nextTimes = ref([]); const acceptParams = (params: DialogProps): void => { dialogData.value = params; - if (dialogData.value.rowData?.spec) { + if (!dialogData.value.rowData?.specCustom && dialogData.value.rowData?.spec) { let objs = []; for (const item of dialogData.value.rowData.spec.split(',')) { objs.push(transSpecToObj(item)); } dialogData.value.rowData.specObjs = objs; } + dialogData.value.rowData.specObjs = dialogData.value.rowData.specObjs || []; + if (dialogData.value.rowData?.specCustom && dialogData.value.rowData?.spec) { + dialogData.value.rowData.specs = dialogData.value.rowData.spec.split(','); + } + dialogData.value.rowData.specs = dialogData.value.rowData.specs || []; if (dialogData.value.title === 'create') { changeType(); dialogData.value.rowData.dbType = 'mysql'; @@ -548,6 +607,22 @@ const hasHour = (item: any) => { return item.specType !== 'perHour' && item.specType !== 'perNMinute' && item.specType !== 'perNSecond'; }; +const loadNext = async (spec: any) => { + nextTimes.value = []; + let specItem = ''; + if (!dialogData.value.rowData.specCustom) { + specItem = transObjToSpec(spec.specType, spec.week, spec.day, spec.hour, spec.minute, spec.second); + } else { + specItem = spec; + } + if (!specItem) { + MsgError(i18n.global.t('cronjob.cronSpecRule2')); + return; + } + const data = await loadNextHandle(specItem); + nextTimes.value = data.data || []; +}; + const loadDatabases = async (dbType: string) => { const data = await listDbItems(dbType); dbInfo.dbs = data.data || []; @@ -555,6 +630,7 @@ const loadDatabases = async (dbType: string) => { const changeType = () => { dialogData.value.rowData!.specObjs = [loadDefaultSpec(dialogData.value.rowData.type)]; + dialogData.value.rowData!.specs = [loadDefaultSpecCustom(dialogData.value.rowData.type)]; }; const changeSpecType = (index: number) => { @@ -598,10 +674,18 @@ const handleSpecAdd = () => { dialogData.value.rowData!.specObjs.push(item); }; +const handleSpecCustomAdd = () => { + dialogData.value.rowData!.specs.push(''); +}; + const handleSpecDelete = (index: number) => { dialogData.value.rowData!.specObjs.splice(index, 1); }; +const handleSpecCustomDelete = (index: number) => { + dialogData.value.rowData!.specs.splice(index, 1); +}; + const loadBackups = async () => { const res = await getBackupList(); backupOptions.value = []; @@ -679,14 +763,18 @@ function hasScript() { } const onSubmit = async (formEl: FormInstance | undefined) => { - const specs = []; - for (const item of dialogData.value.rowData.specObjs) { - const itemSpec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second); - if (itemSpec === '') { - MsgError(i18n.global.t('cronjob.cronSpecHelper')); - return; + let specs = []; + if (!dialogData.value.rowData.specCustom) { + for (const item of dialogData.value.rowData.specObjs) { + const itemSpec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second); + if (itemSpec === '') { + MsgError(i18n.global.t('cronjob.cronSpecHelper')); + return; + } + specs.push(itemSpec); } - specs.push(itemSpec); + } else { + specs = dialogData.value.rowData.specs; } dialogData.value.rowData.sourceAccountIDs = dialogData.value.rowData.sourceAccounts.join(','); dialogData.value.rowData.spec = specs.join(','); @@ -719,7 +807,7 @@ defineExpose({