feat: Support the selection of scripts from the script library for cr… (#8552)

This commit is contained in:
ssongliu 2025-05-06 18:09:37 +08:00 committed by GitHub
parent 91843382c3
commit 64a14a6b2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 407 additions and 191 deletions

View file

@ -53,6 +53,16 @@ func (b *BaseApi) LoadCronjobInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Cronjob
// @Summary Load script options
// @Success 200 {array} dto.ScriptOptions
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /cronjobs/script/options [get]
func (b *BaseApi) LoadScriptOptions(c *gin.Context) {
helper.SuccessWithData(c, cronjobService.LoadScriptOptions())
}
// @Tags Cronjob
// @Summary Load cronjob spec time
// @Accept json

View file

@ -29,6 +29,7 @@ type CronjobOperate struct {
ContainerName string `json:"containerName"`
User string `json:"user"`
ScriptID uint `json:"scriptID"`
AppID string `json:"appID"`
Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"`
@ -84,6 +85,7 @@ type CronjobInfo struct {
ContainerName string `json:"containerName"`
User string `json:"user"`
ScriptID uint `json:"scriptID"`
AppID string `json:"appID"`
Website string `json:"website"`
ExclusionRules string `json:"exclusionRules"`
@ -109,6 +111,11 @@ type CronjobInfo struct {
AlertCount uint `json:"alertCount"`
}
type ScriptOptions struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type SearchRecord struct {
PageInfo
CronjobID int `json:"cronjobID"`

View file

@ -21,6 +21,7 @@ type Cronjob struct {
Script string `json:"script"`
User string `json:"user"`
ScriptID uint `json:"scriptID"`
Website string `json:"website"`
AppID string `json:"appID"`
DBType string `json:"dbType"`
@ -55,3 +56,9 @@ type JobRecords struct {
Status string `json:"status"`
Message string `json:"message"`
}
type ScriptLibrary struct {
BaseModel
Name string `json:"name" gorm:"not null;"`
Script string `json:"script" gorm:"not null;"`
}

76
agent/app/repo/script.go Normal file
View file

@ -0,0 +1,76 @@
package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
)
type ScriptRepo struct{}
type IScriptRepo interface {
Get(opts ...DBOption) (model.ScriptLibrary, error)
List(opts ...DBOption) ([]model.ScriptLibrary, error)
SyncAll(data []model.ScriptLibrary) error
}
func NewIScriptRepo() IScriptRepo {
return &ScriptRepo{}
}
func (u *ScriptRepo) Get(opts ...DBOption) (model.ScriptLibrary, error) {
var script model.ScriptLibrary
db := global.DB
if global.IsMaster {
db = global.CoreDB
}
for _, opt := range opts {
db = opt(db)
}
err := db.First(&script).Error
return script, err
}
func (u *ScriptRepo) List(opts ...DBOption) ([]model.ScriptLibrary, error) {
var ops []model.ScriptLibrary
itemDB := global.DB
if global.IsMaster {
itemDB = global.CoreDB
}
db := itemDB.Model(&model.ScriptLibrary{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&ops).Error
return ops, err
}
func (u *ScriptRepo) SyncAll(data []model.ScriptLibrary) error {
tx := global.DB.Begin()
var oldScripts []model.ScriptLibrary
_ = tx.Where("1 = 1").Find(&oldScripts).Error
oldScriptMap := make(map[string]uint)
for _, item := range oldScripts {
oldScriptMap[item.Name] = item.ID
}
for _, item := range data {
if val, ok := oldScriptMap[item.Name]; ok {
item.ID = val
delete(oldScriptMap, item.Name)
} else {
item.ID = 0
}
if err := tx.Model(model.ScriptLibrary{}).Where("id = ?", item.ID).Save(&item).Error; err != nil {
tx.Rollback()
return err
}
}
for _, val := range oldScriptMap {
if err := tx.Where("id = ?", val).Delete(&model.ScriptLibrary{}).Error; err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
return nil
}

View file

@ -2,6 +2,7 @@ package service
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path"
@ -37,6 +38,8 @@ type ICronjobService interface {
StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error)
CleanRecord(req dto.CronjobClean) error
LoadScriptOptions() []dto.ScriptOptions
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
LoadRecordLog(req dto.OperateByID) string
}
@ -95,6 +98,27 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
return &item, err
}
func (u *CronjobService) LoadScriptOptions() []dto.ScriptOptions {
scripts, err := scriptRepo.List()
if err != nil {
return nil
}
var options []dto.ScriptOptions
for _, script := range scripts {
var item dto.ScriptOptions
item.ID = script.ID
var translations = make(map[string]string)
_ = json.Unmarshal([]byte(script.Name), &translations)
if name, ok := translations["en"]; ok {
item.Name = strings.ReplaceAll(name, " ", "_")
} else {
item.Name = strings.ReplaceAll(script.Name, " ", "_")
}
options = append(options, item)
}
return options
}
func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) {
total, records, err := cronjobRepo.PageRecords(
search.Page,
@ -362,6 +386,8 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
upMap["container_name"] = req.ContainerName
upMap["executor"] = req.Executor
upMap["user"] = req.User
upMap["script_id"] = req.ScriptID
upMap["app_id"] = req.AppID
upMap["website"] = req.Website
upMap["exclusion_rules"] = req.ExclusionRules

View file

@ -79,6 +79,14 @@ func (u *CronjobService) handleJob(cronjob *model.Cronjob, record model.JobRecor
)
switch cronjob.Type {
case "shell":
if cronjob.ScriptMode == "library" {
scriptItem, _ := scriptRepo.Get(repo.WithByID(cronjob.ScriptID))
if scriptItem.ID == 0 {
return nil, fmt.Errorf("load script from db failed, err: %v", err)
}
cronjob.Script = scriptItem.Script
cronjob.ScriptMode = "input"
}
if len(cronjob.Script) == 0 {
return nil, fmt.Errorf("the script content is empty and is skipped")
}

View file

@ -22,6 +22,7 @@ var (
imageRepoRepo = repo.NewIImageRepoRepo()
composeRepo = repo.NewIComposeTemplateRepo()
scriptRepo = repo.NewIScriptRepo()
cronjobRepo = repo.NewICronjobRepo()
hostRepo = repo.NewIHostRepo()

View file

@ -19,7 +19,7 @@ import (
)
var AddTable = &gormigrate.Migration{
ID: "20250413-add-table",
ID: "20250507-add-table",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.AppDetail{},
@ -43,6 +43,7 @@ var AddTable = &gormigrate.Migration{
&model.Firewall{},
&model.Ftp{},
&model.ImageRepo{},
&model.ScriptLibrary{},
&model.JobRecords{},
&model.MonitorBase{},
&model.MonitorIO{},

View file

@ -14,6 +14,7 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("", baseApi.CreateCronjob)
cmdRouter.POST("/next", baseApi.LoadNextHandle)
cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo)
cmdRouter.GET("/script/options", baseApi.LoadScriptOptions)
cmdRouter.POST("/del", baseApi.DeleteCronjob)
cmdRouter.POST("/update", baseApi.UpdateCronjob)
cmdRouter.POST("/status", baseApi.UpdateCronjobStatus)

View file

@ -3,21 +3,23 @@ package dto
import "time"
type ScriptInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Lable string `json:"lable"`
Script string `json:"script"`
GroupList []uint `json:"groupList"`
GroupBelong []string `json:"groupBelong"`
IsSystem bool `json:"isSystem"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
Name string `json:"name"`
IsInteractive bool `json:"isInteractive"`
Lable string `json:"lable"`
Script string `json:"script"`
GroupList []uint `json:"groupList"`
GroupBelong []string `json:"groupBelong"`
IsSystem bool `json:"isSystem"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
}
type ScriptOperate struct {
ID uint `json:"id"`
Name string `json:"name"`
Script string `json:"script"`
Groups string `json:"groups"`
Description string `json:"description"`
ID uint `json:"id"`
IsInteractive bool `json:"isInteractive"`
Name string `json:"name"`
Script string `json:"script"`
Groups string `json:"groups"`
Description string `json:"description"`
}

View file

@ -2,9 +2,10 @@ package model
type ScriptLibrary struct {
BaseModel
Name string `json:"name" gorm:"not null;"`
Script string `json:"script" gorm:"not null;"`
Groups string `json:"groups"`
IsSystem bool `json:"isSystem"`
Description string `json:"description"`
Name string `json:"name" gorm:"not null;"`
IsInteractive bool `json:"isInteractive"`
Script string `json:"script" gorm:"not null;"`
Groups string `json:"groups"`
IsSystem bool `json:"isSystem"`
Description string `json:"description"`
}

View file

@ -20,6 +20,7 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/1Panel-dev/1Panel/core/utils/files"
"github.com/1Panel-dev/1Panel/core/utils/req_helper"
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"gopkg.in/yaml.v2"
@ -57,7 +58,7 @@ func (u *ScriptService) Search(ctx *gin.Context, req dto.SearchPageWithGroup) (i
for _, itemData := range list {
var item dto.ScriptInfo
if err := copier.Copy(&item, &itemData); err != nil {
global.LOG.Errorf("copy backup account to dto backup info failed, err: %v", err)
global.LOG.Errorf("copy scripts to dto backup info failed, err: %v", err)
}
if item.IsSystem {
lang := strings.ToLower(common.GetLang(ctx))
@ -117,6 +118,14 @@ func (u *ScriptService) Create(req dto.ScriptOperate) error {
if err := scriptRepo.Create(&itemData); err != nil {
return err
}
go func() {
if req.IsInteractive {
return
}
if err := xpack.Sync(constant.SyncScripts); err != nil {
global.LOG.Errorf("sync scripts to node failed, err: %v", err)
}
}()
return nil
}
@ -130,6 +139,11 @@ func (u *ScriptService) Delete(req dto.OperateByIDs) error {
return err
}
}
go func() {
if err := xpack.Sync(constant.SyncScripts); err != nil {
global.LOG.Errorf("sync scripts to node failed, err: %v", err)
}
}()
return nil
}
@ -142,10 +156,16 @@ func (u *ScriptService) Update(req dto.ScriptOperate) error {
updateMap["name"] = req.Name
updateMap["script"] = req.Script
updateMap["groups"] = req.Groups
updateMap["is_interactive"] = req.IsInteractive
updateMap["description"] = req.Description
if err := scriptRepo.Update(req.ID, updateMap); err != nil {
return err
}
go func() {
if err := xpack.Sync(constant.SyncScripts); err != nil {
global.LOG.Errorf("sync scripts to node failed, err: %v", err)
}
}()
return nil
}
@ -179,6 +199,7 @@ func (u *ScriptService) Sync() error {
if err != nil {
return fmt.Errorf("load scripts data.yaml from remote failed, err: %v", err)
}
fmt.Println(string(dataRes))
syncTask.Log("download successful!")
var scripts Scripts
@ -205,10 +226,11 @@ func (u *ScriptService) Sync() error {
itemDescription, _ := json.Marshal(item.Description)
shell, _ := os.ReadFile(fmt.Sprintf("%s/scripts/sh/%s.sh", tmpDir, item.Key))
scriptItem := model.ScriptLibrary{
Name: string(itemName),
IsSystem: true,
Script: string(shell),
Description: string(itemDescription),
Name: string(itemName),
IsInteractive: item.Interactive,
IsSystem: true,
Script: string(shell),
Description: string(itemDescription),
}
scriptsForDB = append(scriptsForDB, scriptItem)
}
@ -243,5 +265,6 @@ type ScriptHelper struct {
Sort uint `json:"sort"`
Groups string `json:"groups"`
Name map[string]string `json:"name"`
Interactive bool `json:"interactive"`
Description map[string]string `json:"description"`
}

View file

@ -141,6 +141,12 @@ func (u *SettingService) Update(key, value string) error {
}
case "UserName", "Password":
_ = global.SESSION.Clean()
case "Language":
go func() {
if err := xpack.Sync(constant.SyncLanguage); err != nil {
global.LOG.Errorf("sync language to node failed, err: %v", err)
}
}()
}
return nil

View file

@ -44,9 +44,11 @@ const (
const (
SyncSystemProxy = "SyncSystemProxy"
SyncScripts = "SyncScripts"
SyncBackupAccounts = "SyncBackupAccounts"
SyncAlertSetting = "SyncAlertSetting"
SyncCustomApp = "SyncCustomApp"
SyncLanguage = "SyncLanguage"
)
var WebUrlMap = map[string]struct{}{

View file

@ -176,7 +176,3 @@ func GetMsgWithMapForCmd(key string, maps map[string]interface{}) string {
return content
}
}
func GetLanguageFromDB() {
}

View file

@ -16,7 +16,7 @@ import (
)
var AddTable = &gormigrate.Migration{
ID: "20240109-add-table",
ID: "20240506-add-table",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.OperationLog{},
@ -27,6 +27,7 @@ var AddTable = &gormigrate.Migration{
&model.Host{},
&model.Command{},
&model.UpgradeLog{},
&model.ScriptLibrary{},
)
},
}

View file

@ -105,6 +105,11 @@ export namespace Cronjob {
recordID: number;
backupAccountID: number;
}
export interface ScriptOptions {
id: number;
name: string;
script: string;
}
export interface SearchRecord extends ReqPage {
cronjobID: number;
startTime: Date;

View file

@ -15,6 +15,10 @@ export const loadCronjobInfo = (id: number) => {
return http.post<Cronjob.CronjobOperate>(`/cronjobs/load/info`, { id: id });
};
export const loadScriptOptions = () => {
return http.get<Cronjob.ScriptOptions>(`/cronjobs/script/options`);
};
export const getRecordLog = (id: number) => {
return http.post<string>(`/cronjobs/records/log`, { id: id });
};

View file

@ -0,0 +1,16 @@
<template>
<el-col :xs="24" :sm="span" :md="span" :lg="span" :xl="span">
<slot />
</el-col>
</template>
<script>
export default {
props: {
span: {
type: Number,
default: 10,
},
},
};
</script>

View file

@ -1069,6 +1069,9 @@ const message = {
alertTitle: 'Planned Task - {0} {1} Task Failure Alert',
library: {
script: 'Script',
isInteractive: 'Interactive',
interactive: 'Interactive script',
interactiveHelper: 'Requires user input during execution and cannot be used in scheduled tasks.',
library: 'Script Library',
create: 'Add Script',
edit: 'Edit Script',

View file

@ -1029,6 +1029,9 @@ const message = {
alertTitle: '計画タスク - {0}{1}タスク障害アラート',
library: {
script: 'スクリプト',
isInteractive: '対話型',
interactive: '対話型スクリプト',
interactiveHelper: '実行中にユーザー入力が必要でスケジュールタスクでは使用できません',
library: 'スクリプトライブラリ',
create: 'スクリプトを追加',
edit: 'スクリプトを編集',

View file

@ -1023,6 +1023,9 @@ const message = {
alertTitle: '예정된 작업 - {0} {1} 작업 실패 경고',
library: {
script: '스크립트',
isInteractive: '대화형',
interactive: '대화형 스크립트',
interactiveHelper: '실행 사용자 입력이 필요하며 예약 작업에서는 사용할 없습니다.',
library: '스크립트 라이브러리',
create: '스크립트 추가',
edit: '스크립트 수정',

View file

@ -1059,6 +1059,10 @@ const message = {
alertTitle: 'Tugas Terancang - {0} {1} Amaran Kegagalan Tugas',
library: {
script: 'Skrip',
isInteractive: 'Interaktif',
interactive: 'Skrip interaktif',
interactiveHelper:
'Memerlukan input pengguna semasa pelaksanaan dan tidak boleh digunakan dalam tugas terjadual.',
library: 'Perpustakaan Skrip',
create: 'Tambah Skrip',
edit: 'Sunting Skrip',

View file

@ -1048,6 +1048,10 @@ const message = {
alertTitle: 'Tarefa Planejada - {0} {1} Alerta de Falha na Tarefa',
library: {
script: 'Script',
isInteractive: 'Interativo',
interactive: 'Script interativo',
interactiveHelper:
'Requer entrada do usuário durante a execução e não pode ser usado em tarefas agendadas.',
library: 'Biblioteca de Scripts',
create: 'Adicionar Script',
edit: 'Editar Script',

View file

@ -1053,6 +1053,10 @@ const message = {
alertTitle: 'Плановая задача - {0} «{1}» Оповещение о сбое задачи',
library: {
script: 'Скрипт',
isInteractive: 'Интерактивный',
interactive: 'Интерактивный скрипт',
interactiveHelper:
'Требует ввода пользователя во время выполнения и не может использоваться в запланированных задачах.',
library: 'Библиотека скриптов',
create: 'Добавить скрипт',
edit: 'Редактировать скрипт',

View file

@ -1017,6 +1017,9 @@ const message = {
alertTitle: '計畫任務-{0}{1}任務失敗告警',
library: {
script: '腳本',
isInteractive: '交互式',
interactive: '交互式腳本',
interactiveHelper: '在腳本執行過程中需要用戶輸入參數或做出選擇且無法用於計劃任務中',
library: '腳本庫',
create: '添加腳本',
edit: '修改腳本',

View file

@ -1015,6 +1015,9 @@ const message = {
alertTitle: '计划任务-{0} {1} 任务失败告警',
library: {
script: '脚本',
isInteractive: '交互式',
interactive: '交互式脚本',
interactiveHelper: '在脚本执行过程中需要用户输入参数或做出选择且无法用于计划任务中',
library: '脚本库',
create: '添加脚本',
edit: '修改脚本',

View file

@ -2,70 +2,25 @@
<div v-loading="loading">
<docker-status v-model:isActive="isActive" v-model:isExist="isExist" @search="search" />
<div class="card-interval" v-if="isExist && isActive">
<el-tag @click="searchWithStatus('all')" v-if="countItem.all" effect="plain" size="large">
{{ $t('commons.table.all') }} * {{ countItem.all }}
</el-tag>
<el-tag
@click="searchWithStatus('running')"
v-if="countItem.running"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.running') }} * {{ countItem.running }}
</el-tag>
<el-tag
@click="searchWithStatus('created')"
v-if="countItem.created"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.created') }} * {{ countItem.created }}
</el-tag>
<el-tag
@click="searchWithStatus('paused')"
v-if="countItem.paused"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.paused') }} * {{ countItem.paused }}
</el-tag>
<el-tag
@click="searchWithStatus('restarting')"
v-if="countItem.restarting"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.restarting') }} * {{ countItem.restarting }}
</el-tag>
<el-tag
@click="searchWithStatus('removing')"
v-if="countItem.removing"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.removing') }} * {{ countItem.removing }}
</el-tag>
<el-tag
@click="searchWithStatus('exited')"
v-if="countItem.exited"
effect="plain"
size="large"
class="ml-2"
>
{{ $t('commons.status.exited') }} * {{ countItem.exited }}
</el-tag>
<el-tag @click="searchWithStatus('dead')" v-if="countItem.dead" effect="plain" size="large" class="ml-2">
{{ $t('commons.status.dead') }} * {{ countItem.dead }}
</el-tag>
</div>
<LayoutContent :title="$t('menu.container')" v-if="isExist" :class="{ mask: !isActive }">
<template #search>
<div class="card-interval" v-if="isExist && isActive">
<div v-for="item in tags" :key="item.key" class="inline">
<el-button
v-if="item.count"
class="tag-button"
:class="activeTag === item.key ? '' : 'no-active'"
@click="searchWithStatus(item.key)"
:type="activeTag === item.key ? 'primary' : ''"
:plain="activeTag !== item.key"
>
{{ item.key === 'all' ? $t('commons.table.all') : $t('commons.status.' + item.key) }} *
{{ item.count }}
</el-button>
</div>
</div>
</template>
<template #leftToolBar>
<el-button type="primary" @click="onContainerOperate('')">
{{ $t('container.create') }}
@ -102,7 +57,11 @@
<el-tooltip
:content="includeAppStore ? $t('container.includeAppstore') : $t('container.excludeAppstore')"
>
<el-button @click="searchWithAppShow(!includeAppStore)" :icon="includeAppStore ? 'View' : 'Hide'" />
<el-button
:type="includeAppStore ? '' : 'primary'"
@click="searchWithAppShow(!includeAppStore)"
:icon="includeAppStore ? 'View' : 'Hide'"
/>
</el-tooltip>
<TableRefresh @search="search()" />
<TableSetting title="container-refresh" @search="refresh()" />
@ -455,16 +414,8 @@ const opRef = ref();
const includeAppStore = ref();
const columns = ref([]);
const countItem = reactive({
all: 0,
created: 0,
running: 0,
paused: 0,
restarting: 0,
removing: 0,
exited: 0,
dead: 0,
});
const tags = ref([]);
const activeTag = ref('all');
const goDashboard = async (port: any) => {
if (port.indexOf('127.0.0.1') !== -1) {
@ -527,8 +478,9 @@ const search = async (column?: any) => {
});
};
const searchWithStatus = (item: any) => {
paginationConfig.state = item;
const searchWithStatus = (item: string) => {
activeTag.value = item;
paginationConfig.state = activeTag.value;
search();
};
@ -539,14 +491,15 @@ const searchWithAppShow = (item: any) => {
const loadContainerCount = async () => {
await loadContainerStatus().then((res) => {
countItem.all = res.data.all;
countItem.running = res.data.running;
countItem.paused = res.data.paused;
countItem.restarting = res.data.restarting;
countItem.removing = res.data.removing;
countItem.created = res.data.created;
countItem.dead = res.data.dead;
countItem.exited = res.data.exited;
tags.value = [];
tags.value.push({ key: 'all', count: res.data.all });
tags.value.push({ key: 'running', count: res.data.running });
tags.value.push({ key: 'paused', count: res.data.paused });
tags.value.push({ key: 'restarting', count: res.data.restarting });
tags.value.push({ key: 'removing', count: res.data.removing });
tags.value.push({ key: 'created', count: res.data.created });
tags.value.push({ key: 'dead', count: res.data.dead });
tags.value.push({ key: 'exited', count: res.data.exited });
});
};
@ -810,4 +763,12 @@ onMounted(() => {
position: relative !important;
z-index: 1 !important;
}
.tag-button {
margin-top: -5px;
margin-right: 10px;
&.no-active {
background: none;
border: none;
}
}
</style>

View file

@ -214,7 +214,7 @@
<el-card class="mt-5">
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isWebsite()">
<LayoutCol v-if="isWebsite()">
<el-form-item
:label="form.type === 'website' ? $t('cronjob.website') : $t('menu.website')"
prop="website"
@ -241,8 +241,8 @@
{{ $t('cronjob.cutWebsiteLogHelper') }}
</span>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="form.type === 'app'">
</LayoutCol>
<LayoutCol v-if="form.type === 'app'">
<el-form-item :label="$t('cronjob.app')" prop="appID">
<el-select clearable v-model="form.appID">
<el-option
@ -260,8 +260,8 @@
</div>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="form.type === 'database'">
</LayoutCol>
<LayoutCol v-if="form.type === 'database'">
<el-form-item :label="$t('cronjob.database')">
<el-select v-model="form.dbType" @change="loadDatabases">
<el-option label="MySQL" value="mysql" />
@ -269,8 +269,8 @@
<el-option label="PostgreSQL" value="postgresql" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="form.type === 'database'">
</LayoutCol>
<LayoutCol v-if="form.type === 'database'">
<el-form-item :label="$t('cronjob.database')" prop="dbName">
<el-select clearable v-model="form.dbName">
<el-option
@ -298,34 +298,32 @@
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="form.type === 'directory'">
</LayoutCol>
<LayoutCol v-if="form.type === 'directory'">
<el-form-item :label="$t('cronjob.backupContent')">
<el-radio-group v-model="form.isDir">
<el-radio-group v-model="form.isDir" class="w-full">
<el-radio :value="true">{{ $t('file.dir') }}</el-radio>
<el-radio :value="false">{{ $t('menu.files') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="form.type === 'curl'">
</LayoutCol>
<LayoutCol v-if="form.type === 'curl'">
<el-form-item :label="$t('cronjob.url')" prop="url">
<el-input clearable v-model.trim="form.url" />
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<div v-if="hasScript()" class="w-full">
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<el-form-item>
<el-checkbox v-model="form.inContainer">
{{ $t('cronjob.containerCheckBox') }}
</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24" v-if="hasScript()">
<el-form-item>
<el-checkbox v-model="form.inContainer">
{{ $t('cronjob.containerCheckBox') }}
</el-checkbox>
</el-form-item>
<el-row :gutter="20" v-if="form.inContainer">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<LayoutCol>
<el-form-item :label="$t('cronjob.containerName')" prop="containerName">
<el-select v-model="form.containerName">
<el-option
@ -336,8 +334,8 @@
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
</LayoutCol>
<LayoutCol>
<el-form-item
:label="$t('container.command')"
prop="command"
@ -364,10 +362,10 @@
v-model="form.command"
/>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<el-row :gutter="20" v-if="!form.inContainer">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<LayoutCol>
<el-form-item :label="$t('commons.table.user')" prop="user">
<el-select filterable v-model="form.user">
<div v-for="item in userOptions" :key="item">
@ -375,8 +373,8 @@
</div>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
</LayoutCol>
<LayoutCol>
<el-form-item :label="$t('cronjob.executor')" prop="executor">
<el-checkbox border v-model="form.isCustom">
{{ $t('container.custom') }}
@ -397,39 +395,51 @@
v-model="form.executor"
/>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<el-form-item :label="$t('cronjob.shellContent')" prop="script" class="mt-5">
<el-form-item :label="$t('cronjob.shellContent')" prop="scriptMode">
<el-radio-group @change="form.script = ''" v-model="form.scriptMode">
<el-radio value="input">{{ $t('commons.button.edit') }}</el-radio>
<el-radio value="library">{{ $t('cronjob.library.library') }}</el-radio>
<el-radio value="select">{{ $t('container.pathSelect') }}</el-radio>
</el-radio-group>
</el-form-item>
<CodemirrorPro
v-if="form.scriptMode === 'input'"
v-model="form.script"
placeholder="#Define or paste the content of your shell file here"
mode="javascript"
:heightDiff="400"
class="mb-5"
/>
<el-row :gutter="20" v-if="form.scriptMode === 'select'">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<el-input
:placeholder="$t('commons.example') + '/tmp/test.sh'"
v-model="form.script"
>
<template #prepend>
<FileList @choose="loadScriptDir" :dir="false"></FileList>
</template>
</el-input>
</el-col>
<el-form-item class="-mt-4" v-if="form.scriptMode === 'input'" prop="script">
<CodemirrorPro
v-model="form.script"
placeholder="#Define or paste the content of your shell file here"
mode="javascript"
:heightDiff="400"
/>
</el-form-item>
<el-row :gutter="20" class="-mt-4">
<LayoutCol>
<el-form-item prop="scriptID" v-if="form.scriptMode === 'library'">
<el-select filterable v-model="form.scriptID">
<el-option
v-for="item in scriptOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</el-form-item>
<el-form-item prop="script" v-if="form.scriptMode === 'select'">
<el-input
:placeholder="$t('commons.example') + '/tmp/test.sh'"
v-model="form.script"
>
<template #prepend>
<FileList @choose="loadScriptDir" :dir="false"></FileList>
</template>
</el-input>
</el-form-item>
</LayoutCol>
</el-row>
</div>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isDir() && form.isDir">
<LayoutCol v-if="isDir() && form.isDir">
<el-form-item prop="sourceDir">
<el-input v-model="form.sourceDir">
<template #prepend>
@ -437,8 +447,8 @@
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isDir() && !form.isDir">
</LayoutCol>
<LayoutCol v-if="isDir() && !form.isDir">
<el-input class="mb-5">
<template #prepend>
<FileList @choose="loadFile" :dir="false" />
@ -462,11 +472,11 @@
</ComplexTable>
</div>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isBackup()">
<LayoutCol v-if="isBackup()">
<el-form-item :label="$t('setting.backupAccount')" prop="sourceAccountItems">
<el-select multiple v-model="form.sourceAccountItems" @change="changeAccount">
<div v-for="item in backupOptions" :key="item.id">
@ -493,8 +503,8 @@
</el-link>
</span>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isBackup()">
</LayoutCol>
<LayoutCol v-if="isBackup()">
<el-form-item :label="$t('cronjob.default_download_path')" prop="downloadAccountID">
<el-select v-model="form.downloadAccountID">
<div v-for="item in accountOptions" :key="item.id">
@ -510,16 +520,16 @@
</div>
</el-select>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" v-if="isBackup() && !isDatabase()">
<LayoutCol v-if="isBackup() && !isDatabase()">
<el-form-item :label="$t('setting.compressPassword')" prop="secret">
<el-input v-model="form.secret" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
</LayoutCol>
<LayoutCol>
<el-form-item :label="$t('cronjob.retainCopies')" prop="retainCopies">
<el-input-number
class="selectClass"
@ -533,10 +543,10 @@
</span>
<span v-else class="input-help">{{ $t('cronjob.retainCopiesHelper') }}</span>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
<el-row :gutter="20">
<el-col :xs="24" :sm="20" :md="20" :lg="20" :xl="20" v-if="hasExclusionRules()">
<LayoutCol :span="20" v-if="hasExclusionRules()">
<el-form-item :label="$t('cronjob.exclusionRules')" prop="exclusionRules">
<el-input
:placeholder="$t('cronjob.rulesHelper')"
@ -545,14 +555,14 @@
/>
<span class="input-help">{{ $t('cronjob.exclusionRulesHelper') }}</span>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
</el-card>
<el-card class="mt-5">
<div v-if="!globalStore.isIntl">
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<LayoutCol>
<el-form-item prop="hasAlert">
<el-checkbox v-model="form.hasAlert" :label="$t('xpack.alert.isAlert')" />
<span class="input-help">{{ $t('xpack.alert.cronJobHelper') }}</span>
@ -564,8 +574,8 @@
</el-link>
</span>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
</LayoutCol>
<LayoutCol>
<el-form-item
prop="alertCount"
v-if="form.hasAlert && isProductPro"
@ -580,12 +590,12 @@
></el-input-number>
<span class="input-help">{{ $t('xpack.alert.alertCountHelper') }}</span>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
</div>
<el-row :gutter="20">
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
<LayoutCol>
<el-form-item :label="$t('cronjob.timeout')" prop="timeoutItem">
<el-input type="number" class="selectClass" v-model.number="form.timeoutItem">
<template #append>
@ -597,8 +607,8 @@
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10">
</LayoutCol>
<LayoutCol>
<el-form-item :label="$t('cronjob.retryTimes')" prop="retryTimes">
<el-input-number
class="selectClass"
@ -609,7 +619,7 @@
></el-input-number>
<span class="input-help">{{ $t('cronjob.retryTimesHelper') }}</span>
</el-form-item>
</el-col>
</LayoutCol>
</el-row>
</el-card>
@ -638,8 +648,9 @@ import { listBackupOptions } from '@/api/modules/backup';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { Cronjob } from '@/api/interface/cronjob';
import { addCronjob, editCronjob, loadCronjobInfo, loadNextHandle } from '@/api/modules/cronjob';
import { addCronjob, editCronjob, loadCronjobInfo, loadNextHandle, loadScriptOptions } from '@/api/modules/cronjob';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import LayoutCol from '@/components/layout-col/form.vue';
import { listDbItems } from '@/api/modules/database';
import { getWebsiteOptions } from '@/api/modules/website';
import { MsgError, MsgSuccess } from '@/utils/message';
@ -802,6 +813,7 @@ const search = async () => {
loadShellUsers();
loadWebsites();
loadContainers();
loadScripts();
if (form.dbType) {
loadDatabases(form.dbType);
} else {
@ -819,6 +831,7 @@ const backupOptions = ref([]);
const accountOptions = ref([]);
const appOptions = ref([]);
const userOptions = ref([]);
const scriptOptions = ref([]);
const dbInfo = reactive({
isExist: false,
@ -968,6 +981,7 @@ const rules = reactive({
],
script: [{ validator: verifyScript, trigger: 'blur', required: true }],
scriptID: [Rules.requiredSelect],
containerName: [Rules.requiredSelect],
appID: [Rules.requiredSelect],
website: [Rules.requiredSelect],
@ -1040,6 +1054,11 @@ const loadNext = async (spec: any) => {
nextTimes.value = data.data || [];
};
const loadScripts = async () => {
const res = await loadScriptOptions();
scriptOptions.value = res.data || [];
};
const loadDatabases = async (dbType: string) => {
const data = await listDbItems(dbType);
dbInfo.dbs = data.data || [];
@ -1288,6 +1307,7 @@ onMounted(() => {
}
.selectClass {
width: 100%;
padding-left: 0px;
}
.tagClass {
float: right;

View file

@ -47,6 +47,14 @@
</el-text>
</template>
</el-table-column>
<el-table-column :label="$t('cronjob.library.isInteractive')" prop="isInteractive" min-width="60">
<template #default="{ row }">
<div class="-mb-1">
<el-icon v-if="row.isInteractive"><Check /></el-icon>
<el-icon v-else><Minus /></el-icon>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.group')" min-width="120" prop="group">
<template #default="{ row }">
<el-button class="mr-3" size="small" v-if="row.isSystem">{{ $t('menu.system') }}</el-button>

View file

@ -8,7 +8,14 @@
>
<el-form ref="formRef" v-loading="loading" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model="dialogData.rowData!.name" />
<el-tag v-if="dialogData.title === 'edit'">{{ dialogData.rowData!.name }}</el-tag>
<el-input v-else v-model="dialogData.rowData!.name" />
</el-form-item>
<el-form-item prop="isInteractive">
<el-checkbox v-model="dialogData.rowData!.isInteractive">
{{ $t('cronjob.library.interactive') }}
</el-checkbox>
<span class="input-help">{{ $t('cronjob.library.interactiveHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupList">
<el-select filterable v-model="dialogData.rowData!.groupList" multiple>