feat: shell 脚本定时任务功能完成

This commit is contained in:
ssongliu 2022-09-23 17:21:27 +08:00 committed by ssongliu
parent f0a7ce855a
commit ada58ecb8c
20 changed files with 734 additions and 87 deletions

View File

@ -1,6 +1,9 @@
package v1
import (
"os"
"time"
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
@ -44,6 +47,29 @@ func (b *BaseApi) SearchCronjob(c *gin.Context) {
})
}
func (b *BaseApi) SearchJobRecords(c *gin.Context) {
var req dto.SearchRecord
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if global.CONF.System.DbType == "sqlite" {
req.StartTime = req.StartTime.Add(8 * time.Hour)
req.EndTime = req.EndTime.Add(8 * time.Hour)
}
total, list, err := cronjobService.SearchRecords(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) DeleteCronjob(c *gin.Context) {
var req dto.BatchDeleteReq
if err := c.ShouldBindJSON(&req); err != nil {
@ -78,25 +104,33 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) {
return
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name
upMap["spec_type"] = req.SpecType
upMap["week"] = req.Week
upMap["day"] = req.Day
upMap["hour"] = req.Hour
upMap["minute"] = req.Minute
upMap["script"] = req.Script
upMap["website"] = req.Website
upMap["database"] = req.Database
upMap["source_dir"] = req.SourceDir
upMap["target_dir_id"] = req.TargetDirID
upMap["exclusion_rules"] = req.ExclusionRules
upMap["retain_copies"] = req.RetainCopies
upMap["status"] = req.Status
if err := cronjobService.Update(id, upMap); err != nil {
if err := cronjobService.Save(id, req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) LoadRecordDetail(c *gin.Context) {
var req dto.DetailFile
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
file, err := os.Open(req.Path)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
defer file.Close()
buf := make([]byte, 1024*2)
if _, err := file.Read(buf); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, string(buf))
}

View File

@ -80,7 +80,7 @@ func SuccessWithMsg(ctx *gin.Context, msg string) {
func GetParamID(c *gin.Context) (uint, error) {
idParam, ok := c.Params.Get("id")
if !ok {
return 0, errors.New("error name")
return 0, errors.New("error id in path")
}
intNum, _ := strconv.Atoi(idParam)
return uint(intNum), nil

View File

@ -1,5 +1,7 @@
package dto
import "time"
type CronjobCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
@ -39,6 +41,10 @@ type CronjobUpdate struct {
Status string `json:"status"`
}
type DetailFile struct {
Path string `json:"path" validate:"required"`
}
type CronjobInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
@ -61,3 +67,21 @@ type CronjobInfo struct {
Status string `json:"status"`
}
type SearchRecord struct {
PageInfo
CronjobID int `json:"cronjobID"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Status string `json:"status"`
}
type Record struct {
ID uint `json:"id"`
StartTime time.Time `json:"startTime"`
Records string `json:"records"`
Status string `json:"status"`
Message string `json:"message"`
TargetPath string `json:"targetPath"`
Interval int `json:"interval"`
}

View File

@ -3,7 +3,7 @@ package model
import "time"
type BaseModel struct {
ID uint `gorm:"primarykey"`
ID uint `gorm:"primarykey;AUTO_INCREMENT"`
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1,5 +1,7 @@
package model
import "time"
type Cronjob struct {
BaseModel
@ -21,5 +23,19 @@ type Cronjob struct {
ExclusionRules string `gorm:"longtext" json:"exclusionRules"`
RetainCopies uint64 `gorm:"type:decimal" json:"retainCopies"`
Status string `gorm:"type:varchar(64)" json:"status"`
Status string `gorm:"type:varchar(64)" json:"status"`
EntryID uint64 `gorm:"type:decimal" json:"entryID"`
Records []JobRecords `json:"records"`
}
type JobRecords struct {
BaseModel
CronjobID uint `gorm:"type:varchar(64);not null" json:"cronjobID"`
StartTime time.Time `gorm:"type:datetime" json:"startTime"`
Interval float64 `gorm:"type:float" json:"interval"`
Records string `gorm:"longtext" json:"records"`
Status string `gorm:"type:varchar(64)" json:"status"`
Message string `gorm:"longtext" json:"message"`
TargetPath string `gorm:"type:varchar(256)" json:"targetPath"`
}

View File

@ -15,17 +15,17 @@ type MonitorBase struct {
type MonitorIO struct {
BaseModel
Name string `json:"name"`
ReadCount uint64 `gorm:"type:decimal" json:"readCount"`
WriteCount uint64 `gorm:"type:decimal" json:"writeCount"`
ReadTime uint64 `gorm:"type:decimal" json:"readTime"`
WriteTime uint64 `gorm:"type:decimal" json:"writeTime"`
ReadCount uint64 `json:"readCount"`
WriteCount uint64 `json:"writeCount"`
ReadTime uint64 `json:"readTime"`
WriteTime uint64 `json:"writeTime"`
ReadByte uint64 `gorm:"type:decimal(32)" json:"readByte"`
WriteByte uint64 `gorm:"type:decimal(32)" json:"writeByte"`
Read uint64 `gorm:"type:decimal" json:"read"`
Write uint64 `gorm:"type:decimal" json:"write"`
Count uint64 `gorm:"type:decimal" json:"count"`
Time uint64 `gorm:"type:decimal" json:"time"`
Read uint64 `json:"read"`
Write uint64 `json:"write"`
Count uint64 `json:"count"`
Time uint64 `json:"time"`
}
type MonitorNetwork struct {

View File

@ -32,6 +32,15 @@ func (c *CommonRepo) WithByType(name string) DBOption {
}
}
func (c *CommonRepo) WithByStatus(status string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(status) == 0 {
return g
}
return g.Where("status = ?", status)
}
}
func (c *CommonRepo) WithLikeName(name string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("name like ?", "%"+name+"%")

View File

@ -1,8 +1,12 @@
package repo
import (
"time"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"gorm.io/gorm"
)
type CronjobRepo struct{}
@ -11,8 +15,13 @@ type ICronjobRepo interface {
Get(opts ...DBOption) (model.Cronjob, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Cronjob, error)
Create(cronjob *model.Cronjob) error
WithByDate(startTime, endTime time.Time) DBOption
WithByJobID(id int) DBOption
Save(id uint, cronjob model.Cronjob) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
StartRecords(cronjobID uint, targetPath string) model.JobRecords
EndRecords(record model.JobRecords, status, message, records string)
}
func NewICronjobService() ICronjobRepo {
@ -41,10 +50,57 @@ func (u *CronjobRepo) Page(page, size int, opts ...DBOption) (int64, []model.Cro
return count, users, err
}
func (u *CronjobRepo) PageRecords(page, size int, opts ...DBOption) (int64, []model.JobRecords, error) {
var users []model.JobRecords
db := global.DB.Model(&model.JobRecords{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (u *CronjobRepo) Create(cronjob *model.Cronjob) error {
return global.DB.Create(cronjob).Error
}
func (c *CronjobRepo) WithByDate(startTime, endTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time > ? AND start_time < ?", startTime, endTime)
}
}
func (c *CronjobRepo) WithByJobID(id int) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("cronjob_id = ?", id)
}
}
func (u *CronjobRepo) StartRecords(cronjobID uint, targetPath string) model.JobRecords {
var record model.JobRecords
record.StartTime = time.Now()
record.CronjobID = cronjobID
record.Status = constant.StatusRunning
if err := global.DB.Create(&record).Error; err != nil {
global.LOG.Errorf("create record status failed, err: %v", err)
}
return record
}
func (u *CronjobRepo) EndRecords(record model.JobRecords, status, message, records string) {
errMap := make(map[string]interface{})
errMap["records"] = records
errMap["status"] = status
errMap["message"] = message
errMap["interval"] = time.Since(record.StartTime).Milliseconds()
if err := global.DB.Model(&model.JobRecords{}).Where("id = ?", record.ID).Updates(errMap).Error; err != nil {
global.LOG.Errorf("update record status failed, err: %v", err)
}
}
func (u *CronjobRepo) Save(id uint, cronjob model.Cronjob) error {
return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Save(&cronjob).Error
}
func (u *CronjobRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Cronjob{}).Where("id = ?", id).Updates(vars).Error
}

View File

@ -1,10 +1,16 @@
package service
import (
"bufio"
"fmt"
"os"
"os/exec"
"time"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
@ -13,8 +19,9 @@ type CronjobService struct{}
type ICronjobService interface {
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
Create(cronjobDto dto.CronjobCreate) error
Update(id uint, upMap map[string]interface{}) error
Save(id uint, req dto.CronjobUpdate) error
Delete(ids []uint) error
}
@ -43,6 +50,24 @@ func (u *CronjobService) SearchWithPage(search dto.SearchWithPage) (int64, inter
return total, dtoCronjobs, err
}
func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) {
total, records, err := cronjobRepo.PageRecords(
search.Page,
search.PageSize,
commonRepo.WithByStatus(search.Status),
cronjobRepo.WithByJobID(search.CronjobID),
cronjobRepo.WithByDate(search.StartTime, search.EndTime))
var dtoCronjobs []dto.Record
for _, record := range records {
var item dto.Record
if err := copier.Copy(&item, &record); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoCronjobs = append(dtoCronjobs, item)
}
return total, dtoCronjobs, err
}
func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
cronjob, _ := cronjobRepo.Get(commonRepo.WithByName(cronjobDto.Name))
if cronjob.ID != 0 {
@ -51,23 +76,20 @@ func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
if err := copier.Copy(&cronjob, &cronjobDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
switch cronjobDto.SpecType {
case "perMonth":
cronjob.Spec = fmt.Sprintf("%v %v %v * *", cronjobDto.Minute, cronjobDto.Hour, cronjobDto.Day)
case "perWeek":
cronjob.Spec = fmt.Sprintf("%v %v * * %v", cronjobDto.Minute, cronjobDto.Hour, cronjobDto.Week)
case "perNDay":
cronjob.Spec = fmt.Sprintf("%v %v */%v * *", cronjobDto.Minute, cronjobDto.Hour, cronjobDto.Day)
case "perNHour":
cronjob.Spec = fmt.Sprintf("%v */%v * * *", cronjobDto.Minute, cronjobDto.Hour)
case "perHour":
cronjob.Spec = fmt.Sprintf("%v * * * *", cronjobDto.Minute)
case "perNMinute":
cronjob.Spec = fmt.Sprintf("@every %vm", cronjobDto.Minute)
}
cronjob.Status = constant.StatusEnable
cronjob.Spec = loadSpec(cronjob)
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
switch cronjobDto.Type {
case "shell":
entryID, err := u.AddShellJob(&cronjob)
if err != nil {
return err
}
_ = cronjobRepo.Update(cronjob.ID, map[string]interface{}{"entry_id": entryID})
}
return nil
}
@ -82,6 +104,73 @@ func (u *CronjobService) Delete(ids []uint) error {
return cronjobRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *CronjobService) Update(id uint, upMap map[string]interface{}) error {
return cronjobRepo.Update(id, upMap)
func (u *CronjobService) Save(id uint, req dto.CronjobUpdate) error {
var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
return cronjobRepo.Save(id, cronjob)
}
func (u *CronjobService) AddShellJob(cronjob *model.Cronjob) (int, error) {
addFunc := func() {
record := cronjobRepo.StartRecords(cronjob.ID, "")
cmd := exec.Command(cronjob.Script)
stdout, err := cmd.Output()
if err != nil {
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), "ERR_GENERAGE_STDOUT")
return
}
record.Records, err = mkdirAndWriteFile(cronjob.ID, cronjob.Name, record.StartTime, stdout)
if err != nil {
record.Records = "ERR_CREATE_FILE"
global.LOG.Errorf("save file %s failed, err: %v", record.Records, err)
}
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
}
entryID, err := global.Cron.AddFunc(cronjob.Spec, addFunc)
if err != nil {
return 0, err
}
return int(entryID), nil
}
func mkdirAndWriteFile(id uint, name string, startTime time.Time, msg []byte) (string, error) {
dir := fmt.Sprintf("/opt/1Panel/data/cron/%s%v", name, id)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return "", err
}
}
path := fmt.Sprintf("%s/%s", dir, startTime.Format("20060102150405"))
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return "", err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(string(msg))
write.Flush()
return path, nil
}
func loadSpec(cronjob model.Cronjob) string {
switch cronjob.SpecType {
case "perMonth":
return fmt.Sprintf("%v %v %v * *", cronjob.Minute, cronjob.Hour, cronjob.Day)
case "perWeek":
return fmt.Sprintf("%v %v * * %v", cronjob.Minute, cronjob.Hour, cronjob.Week)
case "perNDay":
return fmt.Sprintf("%v %v */%v * *", cronjob.Minute, cronjob.Hour, cronjob.Day)
case "perNHour":
return fmt.Sprintf("%v */%v * * *", cronjob.Minute, cronjob.Hour)
case "perHour":
return fmt.Sprintf("%v * * * *", cronjob.Minute)
case "perNMinute":
return fmt.Sprintf("@every %vm", cronjob.Minute)
default:
return ""
}
}

View File

@ -0,0 +1,11 @@
package constant
const (
StatusRunning = "Running"
StatusStoped = "Stoped"
StatusWaiting = "Waiting"
StatusSuccess = "Success"
StatusFailed = "Failed"
StatusEnable = "Enable"
StatusDisable = "Disable"
)

View File

@ -11,6 +11,14 @@ import (
func Run() {
nyc, _ := time.LoadLocation("Asia/Shanghai")
Cron := cron.New(cron.WithLocation(nyc))
// var Cronjobs []model.Cronjob
// if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&Cronjobs).Error; err != nil {
// global.LOG.Errorf("start my cronjob failed, err: %v", err)
// }
// for _, cronjob := range Cronjobs {
// switch cronjob.Type {}
// }
_, err := Cron.AddJob("@every 1m", job.NewMonitorJob())
if err != nil {
global.LOG.Errorf("can not add corn job: %s", err.Error())

View File

@ -140,7 +140,6 @@ var AddTableBackupAccount = &gormigrate.Migration{
var AddTableCronjob = &gormigrate.Migration{
ID: "20200921-add-table-cronjob",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Cronjob{})
return tx.AutoMigrate(&model.Cronjob{}, &model.JobRecords{})
},
}

View File

@ -18,5 +18,7 @@ func (s *CronjobRouter) InitCronjobRouter(Router *gin.RouterGroup) {
withRecordRouter.POST("/del", baseApi.DeleteCronjob)
withRecordRouter.PUT(":id", baseApi.UpdateCronjob)
cmdRouter.POST("/search", baseApi.SearchCronjob)
cmdRouter.POST("/search/records", baseApi.SearchJobRecords)
cmdRouter.POST("/search/detail", baseApi.LoadRecordDetail)
}
}

View File

@ -1,3 +1,5 @@
import { ReqPage } from '.';
export namespace Cronjob {
export interface CronjobInfo {
id: number;
@ -56,4 +58,20 @@ export namespace Cronjob {
retainCopies: number;
status: string;
}
export interface SearchRecord extends ReqPage {
cronjobID: number;
startTime: Date;
endTime: Date;
status: string;
}
export interface Record {
id: number;
startTime: Date;
endTime: Date;
records: string;
status: string;
message: string;
targetPath: string;
interval: number;
}
}

View File

@ -17,3 +17,11 @@ export const editCronjob = (params: Cronjob.CronjobUpdate) => {
export const deleteCronjob = (params: { ids: number[] }) => {
return http.post(`/cronjobs/del`, params);
};
export const searchRecords = (params: Cronjob.SearchRecord) => {
return http.post<ResPage<Cronjob.Record>>(`cronjobs/search/records`, params);
};
export const getRecordDetail = (params: string) => {
return http.post<string>(`cronjobs/search/detail`, { path: params });
};

View File

@ -18,6 +18,8 @@ export default {
login: '登录',
close: '关闭',
view: '详情',
expand: '展开',
log: '日志',
saveAndEnable: '保存并启用',
},
search: {
@ -31,6 +33,9 @@ export default {
name: '名称',
type: '类型',
status: '状态',
statusSuccess: '成功',
statusFailed: '失败',
records: '任务输出',
group: '组',
createdAt: '创建时间',
date: '时间',
@ -38,6 +43,7 @@ export default {
operate: '操作',
message: '信息',
description: '描述信息',
interval: '耗时',
},
msg: {
delete: '此操作不可回滚,是否继续',
@ -48,6 +54,7 @@ export default {
notSupportOperation: '不支持的当前操作',
requestTimeout: '请求超时,请稍后重试',
infoTitle: '提示',
notRecords: '当前任务未产生执行记录',
sureLogOut: '您是否确认退出登录?',
createSuccess: '新建成功',
updateSuccess: '更新成功',
@ -139,6 +146,7 @@ export default {
taskType: '任务类型',
shell: 'Shell 脚本',
website: '备份网站',
failedFilter: '失败任务过滤',
all: '所有',
database: '备份数据库',
missBackupAccount: '未能找到备份账号',

View File

@ -13,17 +13,6 @@
{{ $t('commons.button.delete') }}
</el-button>
</template>
<el-table-column type="expand">
<template #default="{ row }">
<ul>
<li>{{ row.name }} {{ $t('cronjob.handle') }}记录 1</li>
<li>{{ row.name }} {{ $t('cronjob.handle') }}记录 2</li>
<li>{{ row.name }} {{ $t('cronjob.handle') }}记录 3</li>
<li>{{ row.name }} {{ $t('cronjob.handle') }}记录 4</li>
</ul>
</template>
</el-table-column>
<el-table-column type="selection" fix />
<el-table-column :label="$t('cronjob.taskName')" prop="name" />
<el-table-column :label="$t('commons.table.status')" prop="status">
@ -33,11 +22,12 @@
:before-change="beforeChangeStatus"
v-model="row.status"
inline-prompt
size="default"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="Y"
inactive-text="N"
active-value="running"
inactive-value="stoped"
active-value="Enable"
inactive-value="Disable"
/>
</template>
</el-table-column>
@ -71,12 +61,15 @@
</ComplexTable>
<OperatrDialog @search="search" ref="dialogRef" />
<RecordDialog ref="dialogRecordRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import OperatrDialog from '@/views/cronjob/operate/index.vue';
import RecordDialog from '@/views/cronjob/record/index.vue';
import { loadZero } from '@/views/cronjob/options';
import { onMounted, reactive, ref } from 'vue';
import { loadBackupName } from '@/views/setting/helper';
import { deleteCronjob, editCronjob, getCronjobPage } from '@/api/modules/cronjob';
@ -85,6 +78,7 @@ import i18n from '@/lang';
import { Cronjob } from '@/api/interface/cronjob';
import { useDeleteData } from '@/hooks/use-delete-data';
import { ElMessage } from 'element-plus';
const selects = ref<any>([]);
const switchState = ref<boolean>(false);
@ -104,7 +98,7 @@ const search = async () => {
logSearch.page = paginationConfig.currentPage;
logSearch.pageSize = paginationConfig.pageSize;
const res = await getCronjobPage(logSearch);
data.value = res.data.items;
data.value = res.data.items || [];
for (const item of data.value) {
if (item.targetDir !== '-') {
item.targetDir = loadBackupName(item.targetDir);
@ -113,6 +107,8 @@ const search = async () => {
paginationConfig.total = res.data.total;
};
const dialogRecordRef = ref<DialogExpose>();
interface DialogExpose {
acceptParams: (params: any) => void;
}
@ -131,7 +127,6 @@ const onOpenDialog = async (
let params = {
title,
rowData: { ...rowData },
isView: title === 'view',
};
dialogRef.value!.acceptParams(params);
};
@ -154,7 +149,6 @@ const beforeChangeStatus = () => {
};
const onChangeStatus = async (row: Cronjob.CronjobInfo) => {
if (switchState.value) {
console.log(row.status);
await editCronjob(row);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
search();
@ -169,13 +163,6 @@ const buttons = [
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.view'),
icon: 'View',
click: (row: Cronjob.CronjobInfo) => {
onOpenDialog('view', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
icon: 'Delete',
@ -183,11 +170,20 @@ const buttons = [
onBatchDelete(row);
},
},
{
label: i18n.global.t('commons.button.log'),
icon: 'Clock',
click: (row: Cronjob.CronjobInfo) => {
onOpenRecordDialog(row);
},
},
];
function loadZero(i: number) {
return i < 10 ? '0' + i : '' + i;
}
const onOpenRecordDialog = async (rowData: Partial<Cronjob.CronjobInfo> = {}) => {
let params = {
rowData: { ...rowData },
};
dialogRecordRef.value!.acceptParams(params);
};
onMounted(() => {
search();

View File

@ -5,15 +5,7 @@
<span>{{ title }}{{ $t('cronjob.cronTask') }}</span>
</div>
</template>
<el-form
:disabled="dialogData.isView"
ref="formRef"
:model="dialogData.rowData"
label-position="left"
:rules="rules"
label-width="120px"
:hide-required-asterisk="dialogData.isView"
>
<el-form ref="formRef" :model="dialogData.rowData" label-position="left" :rules="rules" label-width="120px">
<el-form-item :label="$t('cronjob.taskType')" prop="type">
<el-select
@change="changeName(true, dialogData.rowData!.type, dialogData.rowData!.website)"
@ -134,7 +126,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="cronjobVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-show="!dialogData.isView" type="primary" @click="onSubmit(formRef)">
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@ -146,7 +138,7 @@
import { onMounted, reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import { loadBackupName } from '@/views/setting/helper';
import { typeOptions, specOptions, weekOptions } from '../options';
import { typeOptions, specOptions, weekOptions } from '@/views/cronjob/options';
import FileList from '@/components/file-list/index.vue';
import { getBackupList } from '@/api/modules/backup';
import i18n from '@/lang';
@ -156,14 +148,12 @@ import { addCronjob, editCronjob } from '@/api/modules/cronjob';
interface DialogProps {
title: string;
isView: boolean;
rowData?: Cronjob.CronjobInfo;
getTableList?: () => Promise<any>;
}
const title = ref<string>('');
const cronjobVisiable = ref(false);
const dialogData = ref<DialogProps>({
isView: false,
title: '',
});
const acceptParams = (params: DialogProps): void => {

View File

@ -36,3 +36,6 @@ export const loadWeek = (i: number) => {
}
return '';
};
export const loadZero = (i: number) => {
return i < 10 ? '0' + i : '' + i;
};

View File

@ -0,0 +1,376 @@
<template>
<el-dialog v-model="cronjobVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="70%">
<template #header>
<div class="card-header">
<span>{{ title }}{{ $t('cronjob.cronTask') }}</span>
</div>
</template>
<el-date-picker
@change="search()"
v-model="timeRangeLoad"
type="datetimerange"
:range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
></el-date-picker>
<el-checkbox style="margin-left: 20px" @change="search()" v-model="searchInfo.status">
{{ $t('cronjob.failedFilter') }}
</el-checkbox>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="6">
<el-card>
<ul v-infinite-scroll="nextPage" class="infinite-list" style="overflow: auto">
<li
v-for="(item, index) in records"
:key="index"
@click="forDetail(item)"
class="infinite-list-item"
>
<el-icon v-if="item.status === 'Success'"><Select /></el-icon>
<el-icon v-if="item.status === 'Failed'"><CloseBold /></el-icon>
{{ dateFromat(0, 0, item.startTime) }}
</li>
</ul>
</el-card>
</el-col>
<el-col :span="18">
<el-card style="height: 340px">
<el-form>
<el-row>
<el-col :span="8">
<el-form-item :label="$t('cronjob.taskType')">
{{ dialogData.rowData?.type }}
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('cronjob.taskName')">
{{ dialogData.rowData?.name }}
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('cronjob.cronSpec')">
<span
v-if="
dialogData.rowData?.specType.indexOf('N') === -1 ||
dialogData.rowData?.specType === 'perWeek'
"
>
{{ $t('cronjob.' + dialogData.rowData?.specType) }}
</span>
<span v-else>{{ $t('cronjob.per') }}</span>
<span v-if="dialogData.rowData?.specType === 'perMonth'">
{{ dialogData.rowData?.day }}{{ $t('cronjob.day') }}
{{ loadZero(dialogData.rowData?.hour) }} :
{{ loadZero(dialogData.rowData?.minute) }}
</span>
<span v-if="dialogData.rowData?.specType === 'perWeek'">
{{ loadWeek(dialogData.rowData?.week) }}
{{ loadZero(dialogData.rowData?.hour) }} :
{{ loadZero(dialogData.rowData?.minute) }}
</span>
<span v-if="dialogData.rowData?.specType === 'perNDay'">
{{ dialogData.rowData?.day }}{{ $t('cronjob.day1') }},
{{ loadZero(dialogData.rowData?.hour) }} :
{{ loadZero(dialogData.rowData?.minute) }}
</span>
<span v-if="dialogData.rowData?.specType === 'perNHour'">
{{ dialogData.rowData?.hour }}{{ $t('cronjob.hour') }},
{{ loadZero(dialogData.rowData?.minute) }}
</span>
<span v-if="dialogData.rowData?.specType === 'perHour'">
{{ loadZero(dialogData.rowData?.minute) }}
</span>
<span v-if="dialogData.rowData?.specType === 'perNMinute'">
{{ dialogData.rowData?.minute }}{{ $t('cronjob.minute') }}
</span>
{{ $t('cronjob.handle') }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="hasScript()">
<el-form-item :label="$t('cronjob.shellContent')">
<el-popover
placement="right"
:width="600"
trigger="click"
style="white-space: pre-wrap"
>
<div style="margin-left: 20px">
<span style="white-space: pre-wrap">{{ dialogData.rowData!.script }}</span>
</div>
<template #reference>
<el-button link>{{ $t('commons.button.expand') }}</el-button>
</template>
</el-popover>
</el-form-item>
</el-col>
<el-col :span="8" v-if="dialogData.rowData!.type === 'website'">
<el-form-item :label="$t('cronjob.website')">
{{ dialogData.rowData!.website }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="dialogData.rowData!.type === 'database'">
<el-form-item :label="$t('cronjob.database')">
{{ dialogData.rowData!.database }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="dialogData.rowData!.type === 'directory'">
<el-form-item :label="$t('cronjob.directory')">
{{ dialogData.rowData!.sourceDir }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="isBackup()">
<el-form-item :label="$t('cronjob.target')">
{{ dialogData.rowData!.targetDirID }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="isBackup()">
<el-form-item :label="$t('cronjob.retainCopies')">
{{ dialogData.rowData!.retainCopies }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="dialogData.rowData!.type === 'curl'">
<el-form-item :label="$t('cronjob.url') + 'URL'">
{{ dialogData.rowData!.url }}
</el-form-item>
</el-col>
<el-col
:span="8"
v-if="dialogData.rowData!.type === 'website' || dialogData.rowData!.type === 'directory'"
>
<el-form-item :label="$t('cronjob.exclusionRules')">
{{ dialogData.rowData!.exclusionRules }}
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item :label="$t('commons.search.timeStart')">
{{ dateFromat(0, 0, currentRecord?.startTime) }}
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('commons.table.interval')">
<span v-if="currentRecord?.interval! <= 1000">
{{ currentRecord?.interval }} ms
</span>
<span v-if="currentRecord?.interval! > 1000">
{{ currentRecord?.interval! / 1000 }} s
</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('commons.table.status')">
<el-tooltip
v-if="currentRecord?.status === 'Failed'"
class="box-item"
:content="currentRecord?.message"
placement="top"
>
{{ $t('commons.table.statusFailed') }}
</el-tooltip>
<span v-else>{{ $t('commons.table.statusSuccess') }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('commons.table.records')">
<el-popover
placement="right"
:width="600"
trigger="click"
style="white-space: pre-wrap"
>
<div style="margin-left: 20px">
<span style="white-space: pre-wrap">
{{ currentRecordDetail }}
</span>
</div>
<template #reference>
<el-button link @click="loadRecord(currentRecord?.records!)">
{{ $t('commons.button.expand') }}
</el-button>
</template>
</el-popover>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="cronjobVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Cronjob } from '@/api/interface/cronjob';
import { loadZero, loadWeek } from '@/views/cronjob/options';
import { searchRecords, getRecordDetail } from '@/api/modules/cronjob';
import { dateFromat } from '@/utils/util';
import i18n from '@/lang';
import { ElMessage } from 'element-plus';
interface DialogProps {
rowData?: Cronjob.CronjobInfo;
}
const title = ref<string>('');
const cronjobVisiable = ref(false);
const dialogData = ref<DialogProps>({});
const records = ref<Array<Cronjob.Record>>();
const currentRecord = ref<Cronjob.Record>();
const currentRecordDetail = ref<string>('');
const acceptParams = async (params: DialogProps): Promise<void> => {
dialogData.value = params;
let itemSearch = {
page: searchInfo.page,
pageSize: searchInfo.pageSize,
cronjobID: dialogData.value.rowData!.id,
startTime: searchInfo.startTime,
endTime: searchInfo.endTime,
status: searchInfo.status ? 'Stoped' : '',
};
const res = await searchRecords(itemSearch);
records.value = res.data.items || [];
if (records.value.length === 0) {
ElMessage.info(i18n.global.t('commons.msg.notRecords'));
return;
}
currentRecord.value = records.value[0];
searchInfo.recordTotal = res.data.total;
cronjobVisiable.value = true;
};
const shortcuts = [
{
text: i18n.global.t('monitor.today'),
value: () => {
const end = new Date();
const start = new Date(new Date().setHours(0, 0, 0, 0));
return [start, end];
},
},
{
text: i18n.global.t('monitor.yestoday'),
value: () => {
const yestoday = new Date(new Date().getTime() - 3600 * 1000 * 24 * 1);
const end = new Date(yestoday.setHours(23, 59, 59, 999));
const start = new Date(yestoday.setHours(0, 0, 0, 0));
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [3]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 3);
const end = new Date();
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [7]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 7);
const end = new Date();
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [30]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 30);
const end = new Date();
return [start, end];
},
},
];
const timeRangeLoad = ref<Array<any>>([new Date(new Date().setHours(0, 0, 0, 0)), new Date()]);
const searchInfo = reactive({
page: 1,
pageSize: 5,
recordTotal: 0,
cronjobID: 0,
startTime: new Date(new Date().getTime() - 3600 * 1000 * 24 * 30),
endTime: new Date(),
status: false,
});
const search = async () => {
let params = {
page: searchInfo.page,
pageSize: searchInfo.pageSize,
cronjobID: dialogData.value.rowData!.id,
startTime: searchInfo.startTime,
endTime: searchInfo.endTime,
status: searchInfo.status ? 'Failed' : '',
};
const res = await searchRecords(params);
records.value = res.data.items || [];
searchInfo.recordTotal = res.data.total;
};
const nextPage = async () => {
if (searchInfo.pageSize >= searchInfo.recordTotal) {
return;
}
searchInfo.pageSize = searchInfo.pageSize + 3;
search();
};
const forDetail = async (row: Cronjob.Record) => {
currentRecord.value = row;
};
const loadRecord = async (path: string) => {
const res = await getRecordDetail(path);
currentRecordDetail.value = res.data;
};
function isBackup() {
return (
dialogData.value.rowData!.type === 'website' ||
dialogData.value.rowData!.type === 'database' ||
dialogData.value.rowData!.type === 'directory'
);
}
function hasScript() {
return dialogData.value.rowData!.type === 'shell' || dialogData.value.rowData!.type === 'sync';
}
defineExpose({
acceptParams,
});
</script>
<style scoped>
.infinite-list {
height: 300px;
padding: 0;
margin: 0;
list-style: none;
}
.infinite-list .infinite-list-item {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
background: var(--el-color-primary-light-9);
margin: 10px;
color: var(--el-color-primary);
cursor: pointer;
}
.infinite-list .infinite-list-item:hover {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
background: var(--el-color-primary-light-9);
margin: 10px;
font-weight: 500;
color: red;
}
</style>