1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

fix: 解决计划任务部分 bug

This commit is contained in:
ssongliu 2023-03-10 00:27:41 +08:00 committed by ssongliu
parent 6da43d7eb0
commit 015baec864
18 changed files with 365 additions and 333 deletions

1
.gitignore vendored
View File

@ -16,7 +16,6 @@ build/1panel
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
/build
/pkg/ /pkg/
backend/__debug_bin backend/__debug_bin
cmd/server/__debug_bin cmd/server/__debug_bin

View File

@ -86,7 +86,7 @@ func (u *CronjobRepo) Page(page, size int, opts ...DBOption) (int64, []model.Cro
func (u *CronjobRepo) RecordFirst(id uint) (model.JobRecords, error) { func (u *CronjobRepo) RecordFirst(id uint) (model.JobRecords, error) {
var record model.JobRecords var record model.JobRecords
err := global.DB.Order("created_at desc").First(&record).Error err := global.DB.Where("cronjob_id = ?", id).Order("created_at desc").First(&record).Error
return record, err return record, err
} }

View File

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
@ -13,6 +12,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cloud_storage" "github.com/1Panel-dev/1Panel/backend/utils/cloud_storage"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -232,6 +232,9 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
if backup.Type == "LOCAL" { if backup.Type == "LOCAL" {
if dir, ok := varMap["dir"]; ok { if dir, ok := varMap["dir"]; ok {
if dirStr, isStr := dir.(string); isStr { if dirStr, isStr := dir.(string); isStr {
if strings.HasSuffix(dirStr, "/") {
dirStr = dirStr[:strings.LastIndex(dirStr, "/")]
}
if err := updateBackupDir(dirStr); err != nil { if err := updateBackupDir(dirStr); err != nil {
upMap["vars"] = backup.Vars upMap["vars"] = backup.Vars
_ = backupRepo.Update(req.ID, upMap) _ = backupRepo.Update(req.ID, upMap)
@ -316,8 +319,7 @@ func updateBackupDir(dir string) error {
if strings.HasSuffix(oldDir, "/") { if strings.HasSuffix(oldDir, "/") {
oldDir = oldDir[:strings.LastIndex(oldDir, "/")] oldDir = oldDir[:strings.LastIndex(oldDir, "/")]
} }
cmd := exec.Command("cp", "-r", oldDir+"/*", dir) stdout, err := cmd.Execf("cp -r %s/* %s", oldDir, dir)
stdout, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return errors.New(string(stdout)) return errors.New(string(stdout))
} }

View File

@ -151,7 +151,7 @@ func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) {
} }
return fmt.Sprintf("%v/database/mysql/%s/%s/db_%s_%s.sql.gz", varMap["dir"], mysqlInfo.Name, cronjob.DBName, cronjob.DBName, record.StartTime.Format("20060102150405")), nil return fmt.Sprintf("%v/database/mysql/%s/%s/db_%s_%s.sql.gz", varMap["dir"], mysqlInfo.Name, cronjob.DBName, cronjob.DBName, record.StartTime.Format("20060102150405")), nil
case "directory": case "directory":
return fmt.Sprintf("%v/%s/%s/%s.tar.gz", varMap["dir"], cronjob.Type, cronjob.Name, record.StartTime.Format("20060102150405")), nil return fmt.Sprintf("%v/%s/%s/directory%s_%s.tar.gz", varMap["dir"], cronjob.Type, cronjob.Name, strings.ReplaceAll(cronjob.SourceDir, "/", "_"), record.StartTime.Format("20060102150405")), nil
default: default:
return "", fmt.Errorf("not support type %s", cronjob.Type) return "", fmt.Errorf("not support type %s", cronjob.Type)
} }
@ -220,6 +220,11 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
if err := copier.Copy(&cronjob, &req); err != nil { if err := copier.Copy(&cronjob, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error()) return errors.WithMessage(constant.ErrStructTransform, err.Error())
} }
cronModel, err := cronjobRepo.Get(commonRepo.WithByID(id))
if err != nil {
return constant.ErrRecordNotFound
}
cronjob.EntryID = cronModel.EntryID
cronjob.Spec = loadSpec(cronjob) cronjob.Spec = loadSpec(cronjob)
if err := u.StartJob(&cronjob); err != nil { if err := u.StartJob(&cronjob); err != nil {
return err return err

View File

@ -114,7 +114,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
fileName = fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format("20060102150405")) fileName = fmt.Sprintf("directory%s_%s.tar.gz", strings.ReplaceAll(cronjob.SourceDir, "/", "_"), startTime.Format("20060102150405"))
backupDir = fmt.Sprintf("%s/%s/%s", localDir, cronjob.Type, cronjob.Name) backupDir = fmt.Sprintf("%s/%s/%s", localDir, cronjob.Type, cronjob.Name)
global.LOG.Infof("handle tar %s to %s", backupDir, fileName) global.LOG.Infof("handle tar %s to %s", backupDir, fileName)
if err := handleTar(cronjob.SourceDir, localDir+"/"+backupDir, fileName, cronjob.ExclusionRules); err != nil { if err := handleTar(cronjob.SourceDir, backupDir, fileName, cronjob.ExclusionRules); err != nil {
return "", err return "", err
} }
} }
@ -197,21 +197,26 @@ func (u *CronjobService) HandleRmExpired(backType, backupDir string, cronjob *mo
if len(files) == 0 { if len(files) == 0 {
return return
} }
if cronjob.Type == "database" {
dbCopies := uint64(0) prefix := ""
for i := len(files) - 1; i >= 0; i-- { switch cronjob.Type {
if strings.HasPrefix(files[i].Name(), "db_") { case "database":
dbCopies++ prefix = "db_"
if dbCopies > cronjob.RetainCopies { case "website":
_ = os.Remove(backupDir + "/" + files[i].Name()) prefix = "website_"
_ = backupRepo.DeleteRecord(context.Background(), backupRepo.WithByFileName(files[i].Name())) case "directory":
} prefix = "directory_"
}
dbCopies := uint64(0)
for i := len(files) - 1; i >= 0; i-- {
if strings.HasPrefix(files[i].Name(), prefix) {
dbCopies++
if dbCopies > cronjob.RetainCopies {
_ = os.Remove(backupDir + "/" + files[i].Name())
_ = backupRepo.DeleteRecord(context.Background(), backupRepo.WithByFileName(files[i].Name()))
} }
} }
} else {
for i := 0; i < len(files)-int(cronjob.RetainCopies); i++ {
_ = os.Remove(backupDir + "/" + files[i].Name())
}
} }
records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID))) records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)))
if len(records) > int(cronjob.RetainCopies) { if len(records) > int(cronjob.RetainCopies) {

View File

@ -84,7 +84,7 @@ const checkImageName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) { if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.imageName'))); callback(new Error(i18n.global.t('commons.rule.imageName')));
} else { } else {
const reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,30}$/; const reg = /^[a-zA-Z0-9]{1}[a-z:A-Z0-9_/.-]{0,150}$/;
if (!reg.test(value) && value !== '') { if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.imageName'))); callback(new Error(i18n.global.t('commons.rule.imageName')));
} else { } else {

View File

@ -129,7 +129,7 @@ export default {
userName: 'Support English, Chinese, numbers and _ length 3-30', userName: 'Support English, Chinese, numbers and _ length 3-30',
simpleName: 'Support English, numbers and _ length 1-30', simpleName: 'Support English, numbers and _ length 1-30',
dbName: 'Support English, Chinese, numbers, .-, and _ length 1-16', dbName: 'Support English, Chinese, numbers, .-, and _ length 1-16',
imageName: 'Support English, Chinese, numbers, :.-_, length 1-30', imageName: 'Support English, numbers, :/.-_, length 1-150',
volumeName: 'Support English, numbers, .-_, length 1-30', volumeName: 'Support English, numbers, .-_, length 1-30',
complexityPassword: complexityPassword:
'Enter a password that is longer than eight characters and contains at least two letters, digits, and special characters', 'Enter a password that is longer than eight characters and contains at least two letters, digits, and special characters',

View File

@ -133,7 +133,7 @@ export default {
userName: '支持英文中文数字和_,长度3-30', userName: '支持英文中文数字和_,长度3-30',
simpleName: '支持英文数字_,长度1-30', simpleName: '支持英文数字_,长度1-30',
dbName: '支持英文中文数字.-_,长度1-16', dbName: '支持英文中文数字.-_,长度1-16',
imageName: '支持英文中文数字:.-_,长度1-30', imageName: '支持英文数字:/.-_,长度1-150',
volumeName: '支持英文数字.-和_,长度1-30', volumeName: '支持英文数字.-和_,长度1-30',
complexityPassword: '请输入长度大于 8 位且包含字母数字特殊字符至少两项的密码组合', complexityPassword: '请输入长度大于 8 位且包含字母数字特殊字符至少两项的密码组合',
commonPassword: '请输入 6 位以上长度密码', commonPassword: '请输入 6 位以上长度密码',

View File

@ -279,3 +279,10 @@
.middle-center { .middle-center {
vertical-align: middle; vertical-align: middle;
} }
.status-count {
font-size: 24px;
}
.status-label {
font-size: 14px;
color: #646a73;
}

View File

@ -25,7 +25,7 @@
<el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" /> <el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="imageName"> <el-form-item :label="$t('container.imageName')" :rules="Rules.imageName" prop="imageName">
<el-input v-model.trim="form.imageName"> <el-input v-model.trim="form.imageName">
<template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template> <template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
</el-input> </el-input>

View File

@ -27,7 +27,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('container.image')" :rules="Rules.requiredInput" prop="name"> <el-form-item :label="$t('container.image')" :rules="Rules.imageName" prop="name">
<el-input v-model.trim="form.name"> <el-input v-model.trim="form.name">
<template #prepend>{{ loadDetailInfo(form.repoID) }}/</template> <template #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
</el-input> </el-input>

View File

@ -45,7 +45,14 @@
<el-table-column type="selection" fix /> <el-table-column type="selection" fix />
<el-table-column :label="$t('cronjob.taskName')" :min-width="120" prop="name"> <el-table-column :label="$t('cronjob.taskName')" :min-width="120" prop="name">
<template #default="{ row }"> <template #default="{ row }">
<el-link @click="loadDetail(row)" type="primary">{{ row.name }}</el-link> <el-tooltip effect="dark" :content="row.name" v-if="row.name.length > 12" placement="top">
<el-link @click="loadDetail(row)" type="primary">
{{ row.name.substring(0, 15) }}...
</el-link>
</el-tooltip>
<el-link v-else @click="loadDetail(row)" type="primary">
{{ row.name }}
</el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.status')" :min-width="80" prop="status"> <el-table-column :label="$t('commons.table.status')" :min-width="80" prop="status">

View File

@ -7,13 +7,19 @@
<el-row type="flex" justify="center"> <el-row type="flex" justify="center">
<el-col :span="22"> <el-col :span="22">
<el-form-item :label="$t('cronjob.taskType')" prop="type"> <el-form-item :label="$t('cronjob.taskType')" prop="type">
<el-select style="width: 100%" @change="changeType" v-model="dialogData.rowData!.type"> <el-select
v-if="dialogData.title === 'create'"
style="width: 100%"
@change="changeType"
v-model="dialogData.rowData!.type"
>
<el-option value="shell" :label="$t('cronjob.shell')" /> <el-option value="shell" :label="$t('cronjob.shell')" />
<el-option value="website" :label="$t('cronjob.website')" /> <el-option value="website" :label="$t('cronjob.website')" />
<el-option value="database" :label="$t('cronjob.database')" /> <el-option value="database" :label="$t('cronjob.database')" />
<el-option value="directory" :label="$t('cronjob.directory')" /> <el-option value="directory" :label="$t('cronjob.directory')" />
<el-option value="curl" :label="$t('cronjob.curl')" /> <el-option value="curl" :label="$t('cronjob.curl')" />
</el-select> </el-select>
<el-tag v-else>{{ dialogData.rowData!.type }}</el-tag>
</el-form-item> </el-form-item>
<el-form-item :label="$t('cronjob.taskName')" prop="name"> <el-form-item :label="$t('cronjob.taskName')" prop="name">
@ -120,6 +126,8 @@
<el-input-number <el-input-number
:min="1" :min="1"
:max="30" :max="30"
step-strictly
:step="1"
v-model.number="dialogData.rowData!.retainCopies" v-model.number="dialogData.rowData!.retainCopies"
></el-input-number> ></el-input-number>
</el-form-item> </el-form-item>
@ -184,7 +192,9 @@ const dialogData = ref<DialogProps>({
}); });
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
dialogData.value = params; dialogData.value = params;
changeType(); if (dialogData.value.title === 'create') {
changeType();
}
title.value = i18n.global.t('commons.button.' + dialogData.value.title); title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisiable.value = true; drawerVisiable.value = true;
checkMysqlInstalled(); checkMysqlInstalled();

View File

@ -126,153 +126,136 @@
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<el-card style="height: 362px"> <el-form label-position="top">
<el-form> <el-row type="flex" justify="center">
<el-row v-if="hasScript()"> <el-form-item class="descriptionWide" v-if="isBackup()">
<span>{{ $t('cronjob.shellContent') }}</span> <template #label>
<codemirror <span class="status-label">{{ $t('cronjob.target') }}</span>
ref="mymirror" </template>
:autofocus="true" <span class="status-count">{{ dialogData.rowData!.targetDir }}</span>
placeholder="None data" <el-button
:indent-with-tab="true" v-if="currentRecord?.status! !== 'Failed'"
:tabSize="4" type="primary"
style="height: 100px; width: 100%; margin-top: 5px" style="margin-left: 10px"
:lineWrapping="true" link
:matchBrackets="true" icon="Download"
theme="cobalt" @click="onDownload(currentRecord, dialogData.rowData!.targetDirID)"
:styleActiveLine="true"
:extensions="extensions"
v-model="dialogData.rowData!.script"
:disabled="true"
/>
</el-row>
<el-row>
<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!.dbName }}
</el-form-item>
</el-col>
<el-col :span="8" v-if="dialogData.rowData!.type === 'directory'">
<el-form-item :label="$t('cronjob.directory')">
<span v-if="dialogData.rowData!.sourceDir.length <= 20">
{{ dialogData.rowData!.sourceDir }}
</span>
<div v-else>
<el-popover
placement="top-start"
trigger="hover"
width="250"
:content="dialogData.rowData!.sourceDir"
>
<template #reference>
{{ dialogData.rowData!.sourceDir.substring(0, 20) }}...
</template>
</el-popover>
</div>
</el-form-item>
</el-col>
<el-col :span="8" v-if="isBackup()">
<el-form-item :label="$t('cronjob.target')">
{{ dialogData.rowData!.targetDir }}
<el-button
v-if="currentRecord?.status! !== 'Failed'"
type="primary"
style="margin-left: 10px"
link
icon="Download"
@click="onDownload(currentRecord, dialogData.rowData!.targetDirID)"
>
{{ $t('file.download') }}
</el-button>
</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')">
{{ 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')"> {{ $t('file.download') }}
<div v-if="dialogData.rowData!.exclusionRules"> </el-button>
<div </el-form-item>
v-for="item in dialogData.rowData!.exclusionRules.split(';')" <el-form-item class="description" v-if="dialogData.rowData!.type === 'website'">
:key="item" <template #label>
> <span class="status-label">{{ $t('cronjob.website') }}</span>
<el-tag>{{ item }}</el-tag> </template>
</div> <span class="status-count">{{ dialogData.rowData!.website }}</span>
</div> </el-form-item>
<span v-else>-</span> <el-form-item class="description" v-if="dialogData.rowData!.type === 'database'">
</el-form-item> <template #label>
</el-col> <span class="status-label">{{ $t('cronjob.database') }}</span>
</el-row> </template>
<el-row> <span class="status-count">{{ dialogData.rowData!.dbName }}</span>
<el-col :span="8"> </el-form-item>
<el-form-item :label="$t('commons.search.timeStart')"> <el-form-item class="description" v-if="dialogData.rowData!.type === 'directory'">
{{ dateFormat(0, 0, currentRecord?.startTime) }} <template #label>
</el-form-item> <span class="status-label">{{ $t('cronjob.directory') }}</span>
</el-col> </template>
<el-col :span="8"> <span v-if="dialogData.rowData!.sourceDir.length <= 12" class="status-count">
<el-form-item :label="$t('commons.table.interval')"> {{ dialogData.rowData!.sourceDir }}
<span v-if="currentRecord?.interval! <= 1000"> </span>
{{ currentRecord?.interval }} ms <div v-else>
</span> <el-popover
<span v-if="currentRecord?.interval! > 1000"> placement="top-start"
{{ currentRecord?.interval! / 1000 }} s trigger="hover"
</span> width="250"
</el-form-item> :content="dialogData.rowData!.sourceDir"
</el-col> >
<el-col :span="8"> <template #reference>
<el-form-item :label="$t('commons.table.status')"> <span class="status-count">
<el-tooltip {{ dialogData.rowData!.sourceDir.substring(0, 12) }}...
v-if="currentRecord?.status === 'Failed'" </span>
class="box-item" </template>
:content="currentRecord?.message" </el-popover>
placement="top" </div>
> </el-form-item>
<el-tag type="danger">{{ $t('commons.table.statusFailed') }}</el-tag> <el-form-item class="description" v-if="isBackup()">
</el-tooltip> <template #label>
<el-tag type="success" v-if="currentRecord?.status === 'Success'"> <span class="status-label">{{ $t('cronjob.retainCopies') }}</span>
{{ $t('commons.table.statusSuccess') }} </template>
</el-tag> <span class="status-count">{{ dialogData.rowData!.retainCopies }}</span>
<el-tag type="info" v-if="currentRecord?.status === 'Waiting'"> </el-form-item>
{{ $t('commons.table.statusWaiting') }} </el-row>
</el-tag> <el-form-item
</el-form-item> class="description"
</el-col> v-if="dialogData.rowData!.type === 'website' || dialogData.rowData!.type === 'directory'"
</el-row> >
<el-row v-if="currentRecord?.records"> <template #label>
<span>{{ $t('commons.table.records') }}</span> <span class="status-label">{{ $t('cronjob.exclusionRules') }}</span>
<codemirror </template>
ref="mymirror" <div v-if="dialogData.rowData!.exclusionRules">
:autofocus="true" <div v-for="item in dialogData.rowData!.exclusionRules.split(';')" :key="item">
:placeholder="$t('cronjob.noLogs')" <el-tag>{{ item }}</el-tag>
:indent-with-tab="true" </div>
:tabSize="4" </div>
style="height: 130px; width: 100%; margin-top: 5px" <span class="status-count" v-else>-</span>
:lineWrapping="true" </el-form-item>
:matchBrackets="true" <el-row type="flex" justify="center">
theme="cobalt" <el-form-item class="descriptionWide">
:styleActiveLine="true" <template #label>
:extensions="extensions" <span class="status-label">{{ $t('commons.search.timeStart') }}</span>
v-model="currentRecordDetail" </template>
:disabled="true" <span class="status-count">{{ dateFormat(0, 0, currentRecord?.startTime) }}</span>
/> </el-form-item>
</el-row> <el-form-item class="description">
</el-form> <template #label>
</el-card> <span class="status-label">{{ $t('commons.table.interval') }}</span>
</template>
<span class="status-count" v-if="currentRecord?.interval! <= 1000">
{{ currentRecord?.interval }} ms
</span>
<span class="status-count" v-if="currentRecord?.interval! > 1000">
{{ currentRecord?.interval! / 1000 }} s
</span>
</el-form-item>
<el-form-item class="description">
<template #label>
<span class="status-label">{{ $t('commons.table.status') }}</span>
</template>
<el-tooltip
v-if="currentRecord?.status === 'Failed'"
class="box-item"
:content="currentRecord?.message"
placement="top"
>
<el-tag type="danger">{{ $t('commons.table.statusFailed') }}</el-tag>
</el-tooltip>
<el-tag type="success" v-if="currentRecord?.status === 'Success'">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tag type="info" v-if="currentRecord?.status === 'Waiting'">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
</el-form-item>
</el-row>
<el-row v-if="currentRecord?.records">
<span>{{ $t('commons.table.records') }}</span>
<codemirror
ref="mymirror"
:autofocus="true"
:placeholder="$t('cronjob.noLogs')"
:indent-with-tab="true"
:tabSize="4"
style="height: calc(100vh - 484px); width: 100%; margin-top: 5px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="currentRecordDetail"
:disabled="true"
/>
</el-row>
</el-form>
</el-col> </el-col>
</el-row> </el-row>
<div class="app-warn" v-if="!hasRecords"> <div class="app-warn" v-if="!hasRecords">
@ -416,6 +399,8 @@ const onHandle = async (row: Cronjob.CronjobInfo) => {
.then(() => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
searchInfo.page = 1;
records.value = [];
search(); search();
}) })
.catch(() => { .catch(() => {
@ -451,13 +436,14 @@ const search = async () => {
endTime: searchInfo.endTime, endTime: searchInfo.endTime,
status: searchInfo.status, status: searchInfo.status,
}; };
records.value = [];
const res = await searchRecords(params); const res = await searchRecords(params);
if (!res.data.items) { if (searchInfo.page === 1 && !res.data.items) {
hasRecords.value = false; hasRecords.value = false;
return; return;
} }
records.value = res.data.items; for (const item of res.data.items) {
records.value.push(item);
}
hasRecords.value = true; hasRecords.value = true;
currentRecord.value = records.value[0]; currentRecord.value = records.value[0];
currentRecordIndex.value = 0; currentRecordIndex.value = 0;
@ -490,7 +476,7 @@ const nextPage = async () => {
if (searchInfo.pageSize >= searchInfo.recordTotal) { if (searchInfo.pageSize >= searchInfo.recordTotal) {
return; return;
} }
searchInfo.pageSize = searchInfo.pageSize + 5; searchInfo.page = searchInfo.page + 1;
search(); search();
}; };
const forDetail = async (row: Cronjob.Record, index: number) => { const forDetail = async (row: Cronjob.Record, index: number) => {
@ -515,9 +501,6 @@ function isBackup() {
dialogData.value.rowData!.type === 'directory' dialogData.value.rowData!.type === 'directory'
); );
} }
function hasScript() {
return dialogData.value.rowData!.type === 'shell' || dialogData.value.rowData!.type === 'sync';
}
function loadWeek(i: number) { function loadWeek(i: number) {
for (const week of weekOptions) { for (const week of weekOptions) {
if (week.value === i) { if (week.value === i) {
@ -534,7 +517,7 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.infinite-list { .infinite-list {
height: 310px; height: calc(100vh - 435px);
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@ -593,4 +576,10 @@ defineExpose({
height: 300px; height: 300px;
} }
} }
.descriptionWide {
width: 40%;
}
.description {
width: 30%;
}
</style> </style>

View File

@ -213,13 +213,6 @@ defineExpose({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.status-count {
font-size: 24px;
}
.status-label {
font-size: 14px;
color: #646a73;
}
.devider { .devider {
display: block; display: block;
height: 1px; height: 1px;

View File

@ -169,13 +169,6 @@ defineExpose({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.status-count {
font-size: 24px;
}
.status-label {
font-size: 14px;
color: #646a73;
}
.devider { .devider {
display: block; display: block;
height: 1px; height: 1px;

View File

@ -1,126 +1,146 @@
<template> <template>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%"> <div v-loading="loading">
<template #header> <el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<DrawerHeader :header="title + $t('setting.backupAccount')" :back="handleClose" /> <template #header>
</template> <DrawerHeader :header="title + $t('setting.backupAccount')" :back="handleClose" />
<el-form ref="formRef" v-loading="loading" label-position="top" :model="dialogData.rowData" label-width="120px"> </template>
<el-row type="flex" justify="center"> <el-form
<el-col :span="22"> ref="formRef"
<el-form-item :label="$t('commons.table.type')" prop="type" :rules="Rules.requiredSelect"> v-loading="loading"
<el-tag>{{ $t('setting.' + dialogData.rowData!.type) }}</el-tag> label-position="top"
</el-form-item> :model="dialogData.rowData"
<el-form-item label-width="120px"
v-if="dialogData.rowData!.type === 'LOCAL'" >
:label="$t('setting.currentPath')" <el-row type="flex" justify="center">
prop="varsJson['dir']" <el-col :span="22">
:rules="Rules.requiredInput" <el-form-item :label="$t('commons.table.type')" prop="type" :rules="Rules.requiredSelect">
> <el-tag>{{ $t('setting.' + dialogData.rowData!.type) }}</el-tag>
<el-input v-model="dialogData.rowData!.varsJson['dir']">
<template #prepend>
<FileList @choose="loadDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item
v-if="hasBucket(dialogData.rowData!.type)"
label="Access Key ID"
prop="accessKey"
:rules="Rules.requiredInput"
>
<el-input v-model.trim="dialogData.rowData!.accessKey" />
</el-form-item>
<el-form-item
v-if="hasBucket(dialogData.rowData!.type)"
label="Secret Key"
prop="credential"
:rules="Rules.requiredInput"
>
<el-input show-password clearable v-model.trim="dialogData.rowData!.credential" />
</el-form-item>
<el-form-item
v-if="dialogData.rowData!.type === 'S3'"
label="Region"
prop="varsJson.region"
:rules="Rules.requiredInput"
>
<el-input v-model.trim="dialogData.rowData!.varsJson['region']" />
</el-form-item>
<el-form-item
v-if="hasBucket(dialogData.rowData!.type) && dialogData.rowData!.type !== 'MINIO'"
label="Endpoint"
prop="varsJson.endpoint"
:rules="Rules.requiredInput"
>
<el-input v-model.trim="dialogData.rowData!.varsJson['endpoint']" />
</el-form-item>
<el-form-item
v-if="dialogData.rowData!.type === 'MINIO'"
label="Endpoint"
prop="varsJson.endpointItem"
:rules="Rules.requiredInput"
>
<el-input v-model="dialogData.rowData!.varsJson['endpointItem']">
<template #prepend>
<el-select v-model.trim="endpoints" style="width: 80px">
<el-option label="http" value="http" />
<el-option label="https" value="https" />
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item
v-if="dialogData.rowData!.type !== '' && hasBucket(dialogData.rowData!.type)"
label="Bucket"
prop="bucket"
>
<el-select style="width: 80%" @change="errBuckets = false" v-model="dialogData.rowData!.bucket">
<el-option v-for="item in buckets" :key="item" :value="item" />
</el-select>
<el-button style="width: 20%" plain @click="getBuckets(formRef)">
{{ $t('setting.loadBucket') }}
</el-button>
<span v-if="errBuckets" class="input-error">{{ $t('commons.rule.requiredSelect') }}</span>
</el-form-item>
<div v-if="dialogData.rowData!.type === 'SFTP'">
<el-form-item :label="$t('setting.address')" prop="varsJson.address" :rules="Rules.ip">
<el-input v-model.trim="dialogData.rowData!.varsJson['address']" />
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.port')" prop="varsJson.port" :rules="[Rules.port]"> <el-form-item
<el-input-number v-if="dialogData.rowData!.type === 'LOCAL'"
:min="0" :label="$t('setting.currentPath')"
:max="65535" prop="varsJson['dir']"
v-model.number="dialogData.rowData!.varsJson['port']" :rules="Rules.requiredInput"
/> >
<el-input v-model="dialogData.rowData!.varsJson['dir']">
<template #prepend>
<FileList @choose="loadDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.username')" prop="accessKey" :rules="[Rules.requiredInput]"> <el-form-item
<el-input v-model="dialogData.rowData!.accessKey" /> v-if="hasBucket(dialogData.rowData!.type)"
label="Access Key ID"
prop="accessKey"
:rules="Rules.requiredInput"
>
<el-input v-model.trim="dialogData.rowData!.accessKey" />
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.password')" prop="credential" :rules="[Rules.requiredInput]"> <el-form-item
<el-input v-if="hasBucket(dialogData.rowData!.type)"
type="password" label="Secret Key"
clearable prop="credential"
show-password :rules="Rules.requiredInput"
v-model="dialogData.rowData!.credential" >
/> <el-input show-password clearable v-model.trim="dialogData.rowData!.credential" />
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.path')" prop="bucket" :rules="[Rules.requiredInput]"> <el-form-item
<el-input v-model="dialogData.rowData!.bucket" /> v-if="dialogData.rowData!.type === 'S3'"
label="Region"
prop="varsJson.region"
:rules="Rules.requiredInput"
>
<el-input v-model.trim="dialogData.rowData!.varsJson['region']" />
</el-form-item> </el-form-item>
</div> <el-form-item
</el-col> v-if="hasBucket(dialogData.rowData!.type) && dialogData.rowData!.type !== 'MINIO'"
</el-row> label="Endpoint"
</el-form> prop="varsJson.endpoint"
<template #footer> :rules="Rules.requiredInput"
<span class="dialog-footer"> >
<el-button :disabled="loading" @click="drawerVisiable = false"> <el-input v-model.trim="dialogData.rowData!.varsJson['endpoint']" />
{{ $t('commons.button.cancel') }} </el-form-item>
</el-button> <el-form-item
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)"> v-if="dialogData.rowData!.type === 'MINIO'"
{{ $t('commons.button.confirm') }} label="Endpoint"
</el-button> prop="varsJson.endpointItem"
</span> :rules="Rules.requiredInput"
</template> >
</el-drawer> <el-input v-model="dialogData.rowData!.varsJson['endpointItem']">
<template #prepend>
<el-select v-model.trim="endpoints" style="width: 80px">
<el-option label="http" value="http" />
<el-option label="https" value="https" />
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item
v-if="dialogData.rowData!.type !== '' && hasBucket(dialogData.rowData!.type)"
label="Bucket"
prop="bucket"
>
<el-select
style="width: 80%"
@change="errBuckets = false"
v-model="dialogData.rowData!.bucket"
>
<el-option v-for="item in buckets" :key="item" :value="item" />
</el-select>
<el-button style="width: 20%" plain @click="getBuckets(formRef)">
{{ $t('setting.loadBucket') }}
</el-button>
<span v-if="errBuckets" class="input-error">{{ $t('commons.rule.requiredSelect') }}</span>
</el-form-item>
<div v-if="dialogData.rowData!.type === 'SFTP'">
<el-form-item :label="$t('setting.address')" prop="varsJson.address" :rules="Rules.ip">
<el-input v-model.trim="dialogData.rowData!.varsJson['address']" />
</el-form-item>
<el-form-item :label="$t('setting.port')" prop="varsJson.port" :rules="[Rules.port]">
<el-input-number
:min="0"
:max="65535"
v-model.number="dialogData.rowData!.varsJson['port']"
/>
</el-form-item>
<el-form-item
:label="$t('setting.username')"
prop="accessKey"
:rules="[Rules.requiredInput]"
>
<el-input v-model="dialogData.rowData!.accessKey" />
</el-form-item>
<el-form-item
:label="$t('setting.password')"
prop="credential"
:rules="[Rules.requiredInput]"
>
<el-input
type="password"
clearable
show-password
v-model="dialogData.rowData!.credential"
/>
</el-form-item>
<el-form-item :label="$t('setting.path')" prop="bucket" :rules="[Rules.requiredInput]">
<el-input v-model="dialogData.rowData!.bucket" />
</el-form-item>
</div>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -213,7 +233,7 @@ const getBuckets = async (formEl: FormInstance | undefined) => {
}; };
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (!dialogData.value.rowData.bucket) { if (hasBucket(dialogData.value.rowData.type) && !dialogData.value.rowData.bucket) {
errBuckets.value = true; errBuckets.value = true;
return; return;
} }
@ -228,16 +248,29 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
dialogData.value.rowData!.varsJson['endpointItem'] = undefined; dialogData.value.rowData!.varsJson['endpointItem'] = undefined;
} }
dialogData.value.rowData.vars = JSON.stringify(dialogData.value.rowData!.varsJson); dialogData.value.rowData.vars = JSON.stringify(dialogData.value.rowData!.varsJson);
loading.value = true;
if (dialogData.value.title === 'create') { if (dialogData.value.title === 'create') {
await addBackup(dialogData.value.rowData); await addBackup(dialogData.value.rowData)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
} }
if (dialogData.value.title === 'edit') { await editBackup(dialogData.value.rowData)
await editBackup(dialogData.value.rowData); .then(() => {
} loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); emit('search');
emit('search'); drawerVisiable.value = false;
drawerVisiable.value = false; })
.catch(() => {
loading.value = false;
});
}); });
}; };

View File

@ -85,14 +85,3 @@ onMounted(() => {
get(); get();
}); });
</script> </script>
<style lang="scss" scoped>
.status-count {
font-size: 24px;
}
.status-label {
font-size: 14px;
color: #646a73;
}
</style>