mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-14 01:34:47 +08:00
feat: Support script library management (#8067)
This commit is contained in:
parent
8d2ea2f233
commit
a37fa5c77c
@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
var AddTable = &gormigrate.Migration{
|
||||
ID: "20240108-add-table",
|
||||
ID: "20250108-add-table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.AppDetail{},
|
||||
|
@ -1,11 +1,8 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
@ -82,58 +79,6 @@ func (c *ConnInfo) Close() {
|
||||
_ = c.Client.Close()
|
||||
}
|
||||
|
||||
type SshConn struct {
|
||||
StdinPipe io.WriteCloser
|
||||
ComboOutput *wsBufferWriter
|
||||
Session *gossh.Session
|
||||
}
|
||||
|
||||
func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
|
||||
sshSession, err := c.Client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinP, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comboWriter := new(wsBufferWriter)
|
||||
sshSession.Stdout = comboWriter
|
||||
sshSession.Stderr = comboWriter
|
||||
|
||||
modes := gossh.TerminalModes{
|
||||
gossh.ECHO: 1,
|
||||
gossh.TTY_OP_ISPEED: 14400,
|
||||
gossh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
|
||||
}
|
||||
|
||||
func (s *SshConn) Close() {
|
||||
if s.Session != nil {
|
||||
s.Session.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type wsBufferWriter struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *wsBufferWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Write(p)
|
||||
}
|
||||
|
||||
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
|
||||
if len(passPhrase) != 0 {
|
||||
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
|
||||
|
@ -18,4 +18,5 @@ var (
|
||||
groupService = service.NewIGroupService()
|
||||
commandService = service.NewICommandService()
|
||||
appLauncherService = service.NewIAppLauncher()
|
||||
scriptService = service.NewIScriptService()
|
||||
)
|
||||
|
@ -111,13 +111,13 @@ func (b *BaseApi) HostTree(c *gin.Context) {
|
||||
// @Tags Host
|
||||
// @Summary Page host
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchHostWithPage true "request"
|
||||
// @Param request body dto.SearchPageWithGroup true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /core/hosts/search [post]
|
||||
func (b *BaseApi) SearchHost(c *gin.Context) {
|
||||
var req dto.SearchHostWithPage
|
||||
var req dto.SearchPageWithGroup
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
@ -304,7 +304,7 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, true, client.Client, wsConn)
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
|
194
core/app/api/v2/script_library.go
Normal file
194
core/app/api/v2/script_library.go
Normal file
@ -0,0 +1,194 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/service"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/ssh"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/terminal"
|
||||
"github.com/1Panel-dev/1Panel/core/utils/xpack"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// @Tags ScriptLibrary
|
||||
// @Summary Add script
|
||||
// @Accept json
|
||||
// @Param request body dto.ScriptOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /script [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加脚本库脚本 [name]","formatEN":"add script [name]"}
|
||||
func (b *BaseApi) CreateScript(c *gin.Context) {
|
||||
var req dto.ScriptOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := scriptService.Create(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags ScriptLibrary
|
||||
// @Summary Page script
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchPageWithGroup true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /script/search [post]
|
||||
func (b *BaseApi) SearchScript(c *gin.Context) {
|
||||
var req dto.SearchPageWithGroup
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := scriptService.Search(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags ScriptLibrary
|
||||
// @Summary Delete script
|
||||
// @Accept json
|
||||
// @Param request body dto.BatchDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /script/del [post]
|
||||
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"script_librarys","output_column":"name","output_value":"names"}],"formatZH":"删除脚本库脚本 [names]","formatEN":"delete script [names]"}
|
||||
func (b *BaseApi) DeleteScript(c *gin.Context) {
|
||||
var req dto.OperateByIDs
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := scriptService.Delete(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags ScriptLibrary
|
||||
// @Summary Update script
|
||||
// @Accept json
|
||||
// @Param request body dto.ScriptOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /script/update [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"cronjobs","output_column":"name","output_value":"name"}],"formatZH":"更新脚本库脚本 [name]","formatEN":"update script [name]"}
|
||||
func (b *BaseApi) UpdateScript(c *gin.Context) {
|
||||
var req dto.ScriptOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := scriptService.Update(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
func (b *BaseApi) RunScript(c *gin.Context) {
|
||||
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
if global.CONF.Base.IsDemo {
|
||||
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
|
||||
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
|
||||
return
|
||||
}
|
||||
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
|
||||
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
|
||||
return
|
||||
}
|
||||
scriptID := c.Query("script_id")
|
||||
currentNode := c.Query("current_node")
|
||||
intNum, _ := strconv.Atoi(scriptID)
|
||||
if intNum == 0 {
|
||||
if wshandleError(wsConn, fmt.Errorf(" no such script %v in library, please check and try again!", scriptID)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
scriptItem, err := service.LoadScriptInfo(uint(intNum))
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
|
||||
fileName := strings.ReplaceAll(scriptItem.Name, " ", "_")
|
||||
quitChan := make(chan bool, 3)
|
||||
if currentNode == "local" {
|
||||
tmpFile := path.Join(global.CONF.Base.InstallDir, "1panel/tmp/script")
|
||||
initCmd := fmt.Sprintf("d=%s && mkdir -p $d && echo %s > $d/%s && clear && bash $d/%s", tmpFile, scriptItem.Script, fileName, fileName)
|
||||
slave, err := terminal.NewCommand(initCmd)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
tty.Start(quitChan)
|
||||
go slave.Wait(quitChan)
|
||||
} else {
|
||||
connInfo, installDir, err := xpack.LoadNodeInfo(currentNode)
|
||||
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
|
||||
return
|
||||
}
|
||||
tmpFile := path.Join(installDir, "1panel/tmp/script")
|
||||
initCmd := fmt.Sprintf("d=%s && mkdir -p $d && echo %s > $d/%s && clear && bash $d/%s", tmpFile, scriptItem.Script, fileName, fileName)
|
||||
client, err := ssh.NewClient(*connInfo)
|
||||
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, initCmd)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer sws.Close()
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
|
||||
global.LOG.Info("websocket finished")
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
}
|
@ -5,13 +5,13 @@ type SearchCommandWithPage struct {
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name command createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
GroupID uint `json:"groupID"`
|
||||
Type string `josn:"type" validate:"required,oneof=redis command"`
|
||||
Type string `json:"type" validate:"required,oneof=redis command"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type CommandOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `josn:"type"`
|
||||
Type string `json:"type"`
|
||||
GroupID uint `json:"groupID"`
|
||||
GroupBelong string `json:"groupBelong"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
|
@ -11,6 +11,12 @@ type SearchPageWithType struct {
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type SearchPageWithGroup struct {
|
||||
PageInfo
|
||||
GroupID uint `json:"groupID"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Page int `json:"page" validate:"required,number"`
|
||||
PageSize int `json:"pageSize" validate:"required,number"`
|
||||
|
@ -30,12 +30,6 @@ type HostConnTest struct {
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
}
|
||||
|
||||
type SearchHostWithPage struct {
|
||||
PageInfo
|
||||
GroupID uint `json:"groupID"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
type SearchForTree struct {
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
23
core/app/dto/script_library.go
Normal file
23
core/app/dto/script_library.go
Normal file
@ -0,0 +1,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"`
|
||||
}
|
||||
|
||||
type ScriptOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Groups string `json:"groups"`
|
||||
Description string `json:"description"`
|
||||
}
|
11
core/app/model/script_library.go
Normal file
11
core/app/model/script_library.go
Normal file
@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type ScriptLibrary struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;"`
|
||||
Lable string `json:"lable"`
|
||||
Script string `json:"script" gorm:"not null;"`
|
||||
Groups string `json:"groups"`
|
||||
IsSystem bool `json:"isSystem"`
|
||||
Description string `json:"description"`
|
||||
}
|
78
core/app/repo/script_library.go
Normal file
78
core/app/repo/script_library.go
Normal file
@ -0,0 +1,78 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IScriptRepo interface {
|
||||
Get(opts ...global.DBOption) (model.ScriptLibrary, error)
|
||||
GetList(opts ...global.DBOption) ([]model.ScriptLibrary, error)
|
||||
Create(snap *model.ScriptLibrary) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Page(limit, offset int, opts ...global.DBOption) (int64, []model.ScriptLibrary, error)
|
||||
Delete(opts ...global.DBOption) error
|
||||
|
||||
WithByInfo(info string) global.DBOption
|
||||
}
|
||||
|
||||
func NewIScriptRepo() IScriptRepo {
|
||||
return &ScriptRepo{}
|
||||
}
|
||||
|
||||
type ScriptRepo struct{}
|
||||
|
||||
func (u *ScriptRepo) Get(opts ...global.DBOption) (model.ScriptLibrary, error) {
|
||||
var ScriptLibrary model.ScriptLibrary
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.First(&ScriptLibrary).Error
|
||||
return ScriptLibrary, err
|
||||
}
|
||||
|
||||
func (u *ScriptRepo) GetList(opts ...global.DBOption) ([]model.ScriptLibrary, error) {
|
||||
var snaps []model.ScriptLibrary
|
||||
db := global.DB.Model(&model.ScriptLibrary{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&snaps).Error
|
||||
return snaps, err
|
||||
}
|
||||
|
||||
func (u *ScriptRepo) Page(page, size int, opts ...global.DBOption) (int64, []model.ScriptLibrary, error) {
|
||||
var users []model.ScriptLibrary
|
||||
db := global.DB.Model(&model.ScriptLibrary{})
|
||||
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 *ScriptRepo) Create(ScriptLibrary *model.ScriptLibrary) error {
|
||||
return global.DB.Create(ScriptLibrary).Error
|
||||
}
|
||||
|
||||
func (u *ScriptRepo) Update(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.ScriptLibrary{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *ScriptRepo) Delete(opts ...global.DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.ScriptLibrary{}).Error
|
||||
}
|
||||
|
||||
func (u *ScriptRepo) WithByInfo(info string) global.DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("name LIKE ? OR description LIKE ?", "%"+info+"%", "%"+info+"%")
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ var (
|
||||
launcherRepo = repo.NewILauncherRepo()
|
||||
upgradeLogRepo = repo.NewIUpgradeLogRepo()
|
||||
|
||||
taskRepo = repo.NewITaskRepo()
|
||||
|
||||
agentRepo = repo.NewIAgentRepo()
|
||||
taskRepo = repo.NewITaskRepo()
|
||||
agentRepo = repo.NewIAgentRepo()
|
||||
scriptRepo = repo.NewIScriptRepo()
|
||||
)
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
@ -74,6 +76,22 @@ func (u *GroupService) Delete(id uint) error {
|
||||
if group.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if group.Type == "script" {
|
||||
list, _ := scriptRepo.GetList()
|
||||
if len(list) == 0 {
|
||||
return groupRepo.Delete(repo.WithByID(id))
|
||||
}
|
||||
for _, itemData := range list {
|
||||
groupIDs := strings.Split(itemData.Groups, ",")
|
||||
for _, idItem := range groupIDs {
|
||||
groupID, _ := strconv.Atoi(idItem)
|
||||
if uint(groupID) == id {
|
||||
return buserr.New("ErrGroupIsInUse")
|
||||
}
|
||||
}
|
||||
}
|
||||
return groupRepo.Delete(repo.WithByID(id))
|
||||
}
|
||||
if group.IsDefault {
|
||||
return buserr.New("ErrGroupIsDefault")
|
||||
}
|
||||
@ -84,6 +102,8 @@ func (u *GroupService) Delete(id uint) error {
|
||||
switch group.Type {
|
||||
case "host":
|
||||
err = hostRepo.UpdateGroup(id, defaultGroup.ID)
|
||||
case "script":
|
||||
err = hostRepo.UpdateGroup(id, defaultGroup.ID)
|
||||
case "command":
|
||||
err = commandRepo.UpdateGroup(id, defaultGroup.ID)
|
||||
case "node":
|
||||
|
@ -22,7 +22,7 @@ type IHostService interface {
|
||||
TestByInfo(req dto.HostConnTest) bool
|
||||
GetHostByID(id uint) (*dto.HostInfo, error)
|
||||
SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error)
|
||||
SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error)
|
||||
SearchWithPage(search dto.SearchPageWithGroup) (int64, interface{}, error)
|
||||
Create(hostDto dto.HostOperate) (*dto.HostInfo, error)
|
||||
Update(id uint, upMap map[string]interface{}) (*dto.HostInfo, error)
|
||||
Delete(id []uint) error
|
||||
@ -124,7 +124,7 @@ func (u *HostService) TestLocalConn(id uint) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *HostService) SearchWithPage(req dto.SearchHostWithPage) (int64, interface{}, error) {
|
||||
func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interface{}, error) {
|
||||
var options []global.DBOption
|
||||
if len(req.Info) != 0 {
|
||||
options = append(options, hostRepo.WithByInfo(req.Info))
|
||||
|
118
core/app/service/script_library.go
Normal file
118
core/app/service/script_library.go
Normal file
@ -0,0 +1,118 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/core/app/model"
|
||||
"github.com/1Panel-dev/1Panel/core/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/core/buserr"
|
||||
"github.com/1Panel-dev/1Panel/core/global"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
type ScriptService struct{}
|
||||
|
||||
type IScriptService interface {
|
||||
Search(req dto.SearchPageWithGroup) (int64, interface{}, error)
|
||||
Create(req dto.ScriptOperate) error
|
||||
Update(req dto.ScriptOperate) error
|
||||
Delete(ids dto.OperateByIDs) error
|
||||
}
|
||||
|
||||
func NewIScriptService() IScriptService {
|
||||
return &ScriptService{}
|
||||
}
|
||||
|
||||
func (u *ScriptService) Search(req dto.SearchPageWithGroup) (int64, interface{}, error) {
|
||||
options := []global.DBOption{repo.WithOrderBy("created_at desc")}
|
||||
if len(req.Info) != 0 {
|
||||
options = append(options, scriptRepo.WithByInfo(req.Info))
|
||||
}
|
||||
list, err := scriptRepo.GetList(options...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
groups, _ := groupRepo.GetList(repo.WithByType("script"))
|
||||
groupMap := make(map[uint]string)
|
||||
for _, item := range groups {
|
||||
groupMap[item.ID] = item.Name
|
||||
}
|
||||
var data []dto.ScriptInfo
|
||||
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)
|
||||
}
|
||||
matchGourp := false
|
||||
groupIDs := strings.Split(itemData.Groups, ",")
|
||||
for _, idItem := range groupIDs {
|
||||
id, _ := strconv.Atoi(idItem)
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if uint(id) == req.GroupID {
|
||||
matchGourp = true
|
||||
}
|
||||
item.GroupList = append(item.GroupList, uint(id))
|
||||
item.GroupBelong = append(item.GroupBelong, groupMap[uint(id)])
|
||||
}
|
||||
if req.GroupID == 0 {
|
||||
data = append(data, item)
|
||||
continue
|
||||
}
|
||||
if matchGourp {
|
||||
data = append(data, item)
|
||||
}
|
||||
}
|
||||
var records []dto.ScriptInfo
|
||||
total, start, end := len(data), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
records = make([]dto.ScriptInfo, 0)
|
||||
} else {
|
||||
if end >= total {
|
||||
end = total
|
||||
}
|
||||
records = data[start:end]
|
||||
}
|
||||
return int64(total), records, nil
|
||||
}
|
||||
|
||||
func (u *ScriptService) Create(req dto.ScriptOperate) error {
|
||||
itemData, _ := scriptRepo.Get(repo.WithByName(req.Name))
|
||||
if itemData.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
}
|
||||
if err := copier.Copy(&itemData, &req); err != nil {
|
||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
if err := scriptRepo.Create(&itemData); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ScriptService) Delete(req dto.OperateByIDs) error {
|
||||
return scriptRepo.Delete(repo.WithByIDs(req.IDs))
|
||||
}
|
||||
|
||||
func (u *ScriptService) Update(req dto.ScriptOperate) error {
|
||||
itemData, _ := scriptRepo.Get(repo.WithByID(req.ID))
|
||||
if itemData.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
updateMap := make(map[string]interface{})
|
||||
updateMap["name"] = req.Name
|
||||
updateMap["script"] = req.Script
|
||||
updateMap["groups"] = req.Groups
|
||||
updateMap["description"] = req.Description
|
||||
if err := scriptRepo.Update(req.ID, updateMap); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadScriptInfo(id uint) (model.ScriptLibrary, error) {
|
||||
return scriptRepo.Get(repo.WithByID(id))
|
||||
}
|
@ -5,7 +5,7 @@ go 1.23
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/creack/pty v1.1.9
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
|
@ -70,8 +70,9 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "Demo server, this operation is prohibited!"
|
||||
ErrCmdTimeout: "Command execution timeout!"
|
||||
ErrEntrance: "Security entrance information error, please check and try again!"
|
||||
ErrGroupIsDefault: "Default group, unable to delete"
|
||||
ErrGroupIsInUse: "The group is in use and cannot be deleted."
|
||||
ErrLocalDelete: "Cannot delete the local node!"
|
||||
ErrPortInUsed: "The {{ .name }} port is already in use!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "デモサーバーではこの操作は許可されてい
|
||||
ErrCmdTimeout: "コマンドの実行がタイムアウトしました!"
|
||||
ErrEntrance: "セキュリティ情報エラー、再確認してください!"
|
||||
ErrGroupIsDefault: "デフォルトグループの削除はできません"
|
||||
ErrGroupIsInUse: "グループは使用中のため、削除できません。"
|
||||
ErrLocalDelete: "ローカルノードは削除できません!"
|
||||
ErrPortInUsed: "{{ .name }} ポートはすでに使用されています!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "데모 서버에서는 이 작업이 금지되어 있습니
|
||||
ErrCmdTimeout: "명령 실행 시간 초과!"
|
||||
ErrEntrance: "보안 정보 오류입니다. 확인 후 다시 시도하십시오!"
|
||||
ErrGroupIsDefault: "기본 그룹은 삭제할 수 없습니다"
|
||||
ErrGroupIsInUse: "그룹이 사용 중이므로 삭제할 수 없습니다."
|
||||
ErrLocalDelete: "로컬 노드는 삭제할 수 없습니다!"
|
||||
ErrPortInUsed: "{{ .name }} 포트가 이미 사용 중입니다!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "Pelayan demo, operasi ini dilarang!"
|
||||
ErrCmdTimeout: "Perintah telah tamat masa!"
|
||||
ErrEntrance: "Maklumat pintu masuk keselamatan salah, sila periksa dan cuba lagi!"
|
||||
ErrGroupIsDefault: "Kumpulan lalai tidak boleh dihapuskan"
|
||||
ErrGroupIsInUse: "Kumpulan sedang digunakan dan tidak boleh dipadam."
|
||||
ErrLocalDelete: "Nod tempatan tidak boleh dihapuskan!"
|
||||
ErrPortInUsed: "Port {{ .name }} telah digunakan!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "Servidor de demonstração, essa operação é proibida!"
|
||||
ErrCmdTimeout: "Tempo de execução do comando esgotado!"
|
||||
ErrEntrance: "Erro nas informações de entrada de segurança, por favor, verifique e tente novamente!"
|
||||
ErrGroupIsDefault: "Grupo padrão não pode ser excluído"
|
||||
ErrGroupIsInUse: "O grupo está em uso e não pode ser excluído."
|
||||
ErrLocalDelete: "O nó local não pode ser excluído!"
|
||||
ErrPortInUsed: "A porta {{ .name }} já está em uso!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "Демонстрационный сервер, эта оп
|
||||
ErrCmdTimeout: "Время выполнения команды истекло!"
|
||||
ErrEntrance: "Ошибка информации о безопасном входе, проверьте и повторите попытку!"
|
||||
ErrGroupIsDefault: "Группу по умолчанию нельзя удалить"
|
||||
ErrGroupIsInUse: "Группа используется и не может быть удалена."
|
||||
ErrLocalDelete: "Локальный узел нельзя удалить!"
|
||||
ErrPortInUsed: "Порт {{ .name }} уже используется!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "演示伺服器,禁止此操作!"
|
||||
ErrCmdTimeout: "指令執行逾時!"
|
||||
ErrEntrance: "安全入口資訊錯誤,請檢查後再試!"
|
||||
ErrGroupIsDefault: "預設分組無法刪除"
|
||||
ErrGroupIsInUse: "分組正被使用,無法刪除。"
|
||||
ErrLocalDelete: "無法刪除本地節點!"
|
||||
ErrPortInUsed: "{{ .name }} 埠已被佔用!"
|
||||
|
||||
|
@ -27,6 +27,7 @@ ErrDemoEnvironment: "演示服务器,禁止此操作!"
|
||||
ErrCmdTimeout: "命令执行超时!"
|
||||
ErrEntrance: "安全入口信息错误,请检查后重试!"
|
||||
ErrGroupIsDefault: "默认分组,无法删除"
|
||||
ErrGroupIsInUse: "分组正被使用,无法删除"
|
||||
ErrLocalDelete: "无法删除本地节点!"
|
||||
ErrPortInUsed: "{{ .name }} 端口已被占用!"
|
||||
|
||||
|
@ -23,6 +23,7 @@ func Init() {
|
||||
migrations.AddMFAInterval,
|
||||
migrations.UpdateXpackHideMemu,
|
||||
migrations.AddSystemIP,
|
||||
migrations.InitScriptLibrary,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -328,3 +328,21 @@ var AddSystemIP = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var InitScriptLibrary = &gormigrate.Migration{
|
||||
ID: "20250303-init-script-library",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.ScriptLibrary{}); err != nil {
|
||||
return err
|
||||
}
|
||||
// defaultGroup := []model.Group{
|
||||
// {Name: "docker", Type: "script", IsDefault: false},
|
||||
// {Name: "install", Type: "script", IsDefault: false},
|
||||
// {Name: "uninstall", Type: "script", IsDefault: false},
|
||||
// }
|
||||
// if err := tx.Create(&defaultGroup).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -10,5 +10,6 @@ func commonGroups() []CommonRouter {
|
||||
&HostRouter{},
|
||||
&GroupRouter{},
|
||||
&AppLauncherRouter{},
|
||||
&ScriptRouter{},
|
||||
}
|
||||
}
|
||||
|
24
core/router/ro_script_library.go
Normal file
24
core/router/ro_script_library.go
Normal file
@ -0,0 +1,24 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v2 "github.com/1Panel-dev/1Panel/core/app/api/v2"
|
||||
"github.com/1Panel-dev/1Panel/core/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ScriptRouter struct{}
|
||||
|
||||
func (s *ScriptRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
scriptRouter := Router.Group("script").
|
||||
Use(middleware.JwtAuth()).
|
||||
Use(middleware.SessionAuth()).
|
||||
Use(middleware.PasswordExpired())
|
||||
baseApi := v2.ApiGroupApp.BaseApi
|
||||
{
|
||||
scriptRouter.POST("", baseApi.CreateScript)
|
||||
scriptRouter.POST("/search", baseApi.SearchScript)
|
||||
scriptRouter.POST("/del", baseApi.DeleteScript)
|
||||
scriptRouter.POST("/update", baseApi.UpdateScript)
|
||||
scriptRouter.GET("/run", baseApi.RunScript)
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
@ -93,58 +90,6 @@ func (c *SSHClient) Close() {
|
||||
_ = c.Client.Close()
|
||||
}
|
||||
|
||||
type SshConn struct {
|
||||
StdinPipe io.WriteCloser
|
||||
ComboOutput *wsBufferWriter
|
||||
Session *gossh.Session
|
||||
}
|
||||
|
||||
func (c *SSHClient) NewSshConn(cols, rows int) (*SshConn, error) {
|
||||
sshSession, err := c.Client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinP, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comboWriter := new(wsBufferWriter)
|
||||
sshSession.Stdout = comboWriter
|
||||
sshSession.Stderr = comboWriter
|
||||
|
||||
modes := gossh.TerminalModes{
|
||||
gossh.ECHO: 1,
|
||||
gossh.TTY_OP_ISPEED: 14400,
|
||||
gossh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
|
||||
}
|
||||
|
||||
func (s *SshConn) Close() {
|
||||
if s.Session != nil {
|
||||
s.Session.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type wsBufferWriter struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *wsBufferWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Write(p)
|
||||
}
|
||||
|
||||
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
|
||||
if len(passPhrase) != 0 {
|
||||
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
|
||||
|
@ -25,13 +25,21 @@ type LocalCommand struct {
|
||||
pty *os.File
|
||||
}
|
||||
|
||||
func NewCommand(commands []string) (*LocalCommand, error) {
|
||||
cmd := exec.Command("docker", commands...)
|
||||
|
||||
func NewCommand(initCmd string) (*LocalCommand, error) {
|
||||
cmd := exec.Command("bash")
|
||||
if term := os.Getenv("TERM"); term != "" {
|
||||
cmd.Env = append(os.Environ(), "TERM="+term)
|
||||
} else {
|
||||
cmd.Env = append(os.Environ(), "TERM=xterm")
|
||||
}
|
||||
pty, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to start command")
|
||||
}
|
||||
if len(initCmd) != 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _ = pty.Write([]byte(initCmd + "\n"))
|
||||
}
|
||||
|
||||
lcmd := &LocalCommand{
|
||||
closeSignal: DefaultCloseSignal,
|
||||
|
@ -59,7 +59,7 @@ type LogicSshWsSession struct {
|
||||
IsFlagged bool
|
||||
}
|
||||
|
||||
func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
|
||||
func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websocket.Conn, initCmd string) (*LogicSshWsSession, error) {
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -87,6 +87,10 @@ func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, w
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(initCmd) != 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _ = stdinP.Write([]byte(initCmd + "\n"))
|
||||
}
|
||||
return &LogicSshWsSession{
|
||||
stdinPipe: stdinP,
|
||||
comboOutput: comboWriter,
|
||||
@ -94,7 +98,7 @@ func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, w
|
||||
inputFilterBuff: inputBuf,
|
||||
session: sshSession,
|
||||
wsConn: wsConn,
|
||||
isAdmin: isAdmin,
|
||||
isAdmin: true,
|
||||
IsFlagged: false,
|
||||
}, nil
|
||||
}
|
||||
@ -110,6 +114,7 @@ func (sws *LogicSshWsSession) Close() {
|
||||
sws.comboOutput = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
|
||||
go sws.receiveWsMsg(quitChan)
|
||||
go sws.sendComboOutput(quitChan)
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/utils/ssh"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -32,3 +33,7 @@ func LoadRequestTransport() *http.Transport {
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
@ -120,4 +120,22 @@ export namespace Cronjob {
|
||||
targetPath: string;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
export interface ScriptInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
script: string;
|
||||
groups: string;
|
||||
groupList: Array<number>;
|
||||
groupBelong: Array<string>;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
export interface ScriptOperate {
|
||||
id: number;
|
||||
name: string;
|
||||
script: string;
|
||||
groups: string;
|
||||
description: string;
|
||||
}
|
||||
}
|
||||
|
@ -56,3 +56,16 @@ export const downloadRecord = (params: Cronjob.Download) => {
|
||||
export const handleOnce = (id: number) => {
|
||||
return http.post(`cronjobs/handle`, { id: id });
|
||||
};
|
||||
|
||||
export const searchScript = (params: SearchWithPage) => {
|
||||
return http.post<ResPage<Cronjob.ScriptInfo>>(`/core/script/search`, params);
|
||||
};
|
||||
export const addScript = (params: Cronjob.ScriptOperate) => {
|
||||
return http.post(`/core/script`, params);
|
||||
};
|
||||
export const editScript = (params: Cronjob.ScriptOperate) => {
|
||||
return http.post(`/core/script/update`, params);
|
||||
};
|
||||
export const deleteScript = (ids: Array<number>) => {
|
||||
return http.post(`/core/script/del`, { ids: ids });
|
||||
};
|
||||
|
@ -52,7 +52,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
v-if="!row.edit && !row.isDefault && !row.isDelete"
|
||||
v-if="hideDefaultButton && !row.edit && !row.isDefault && !row.isDelete"
|
||||
type="primary"
|
||||
@click="setDefault(row)"
|
||||
>
|
||||
@ -75,6 +75,7 @@ import { Rules } from '@/global/form-rules';
|
||||
import { FormInstance } from 'element-plus';
|
||||
|
||||
const open = ref(false);
|
||||
const hideDefaultButton = ref(false);
|
||||
const type = ref();
|
||||
const data = ref();
|
||||
const handleClose = () => {
|
||||
@ -84,10 +85,12 @@ const handleClose = () => {
|
||||
};
|
||||
interface DialogProps {
|
||||
type: string;
|
||||
hideDefaultButton: boolean;
|
||||
}
|
||||
|
||||
const groupForm = ref<FormInstance>();
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
hideDefaultButton.value = params.hideDefaultButton;
|
||||
type.value = params.type;
|
||||
open.value = true;
|
||||
search();
|
||||
|
@ -1033,6 +1033,12 @@ const message = {
|
||||
requestExpirationTime: 'Upload Request Expiration Time(Hours)',
|
||||
unitHours: 'Unit: Hours',
|
||||
alertTitle: 'Planned Task - {0} 「{1}」 Task Failure Alert',
|
||||
library: {
|
||||
script: 'Script',
|
||||
library: 'Script Library',
|
||||
create: 'Add Script',
|
||||
edit: 'Edit Script',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Global Filter',
|
||||
|
@ -994,6 +994,12 @@ const message = {
|
||||
requestExpirationTime: 'リクエストの有効期限(時間)のアップロード',
|
||||
unitHours: 'ユニット:時間',
|
||||
alertTitle: '計画タスク - {0}「{1}」タスク障害アラート',
|
||||
library: {
|
||||
script: 'スクリプト',
|
||||
library: 'スクリプトライブラリ',
|
||||
create: 'スクリプトを追加',
|
||||
edit: 'スクリプトを編集',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'グローバルフィルター',
|
||||
|
@ -987,6 +987,12 @@ const message = {
|
||||
requestExpirationTime: '업로드 요청 만료 시간(시간)',
|
||||
unitHours: '단위: 시간',
|
||||
alertTitle: '예정된 작업 - {0} 「{1}」 작업 실패 경고',
|
||||
library: {
|
||||
script: '스크립트',
|
||||
library: '스크립트 라이브러리',
|
||||
create: '스크립트 추가',
|
||||
edit: '스크립트 수정',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: '전역 필터',
|
||||
|
@ -1023,6 +1023,12 @@ const message = {
|
||||
requestExpirationTime: 'Waktu luput permintaan muat naik (Jam)',
|
||||
unitHours: 'Unit: Jam',
|
||||
alertTitle: 'Tugas Terancang - {0} 「{1}」 Amaran Kegagalan Tugas',
|
||||
library: {
|
||||
script: 'Skrip',
|
||||
library: 'Perpustakaan Skrip',
|
||||
create: 'Tambah Skrip',
|
||||
edit: 'Sunting Skrip',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Penapis Global',
|
||||
|
@ -1012,6 +1012,12 @@ const message = {
|
||||
requestExpirationTime: 'Tempo de expiração da solicitação de upload (Horas)',
|
||||
unitHours: 'Unidade: Horas',
|
||||
alertTitle: 'Tarefa Planejada - {0} 「{1}」 Alerta de Falha na Tarefa',
|
||||
library: {
|
||||
script: 'Script',
|
||||
library: 'Biblioteca de Scripts',
|
||||
create: 'Adicionar Script',
|
||||
edit: 'Editar Script',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Filtro global',
|
||||
|
@ -1017,6 +1017,12 @@ const message = {
|
||||
requestExpirationTime: 'Время истечения запроса на загрузку (часы)',
|
||||
unitHours: 'Единица: часы',
|
||||
alertTitle: 'Плановая задача - {0} «{1}» Оповещение о сбое задачи',
|
||||
library: {
|
||||
script: 'Скрипт',
|
||||
library: 'Библиотека скриптов',
|
||||
create: 'Добавить скрипт',
|
||||
edit: 'Редактировать скрипт',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: 'Глобальный фильтр',
|
||||
|
@ -980,6 +980,12 @@ const message = {
|
||||
requestExpirationTime: '上傳請求過期時間(小時)',
|
||||
unitHours: '單位:小時',
|
||||
alertTitle: '計畫任務-{0}「{1}」任務失敗告警',
|
||||
library: {
|
||||
script: '腳本',
|
||||
library: '腳本庫',
|
||||
create: '添加腳本',
|
||||
edit: '修改腳本',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: '全局過濾',
|
||||
|
@ -978,6 +978,12 @@ const message = {
|
||||
requestExpirationTime: '上传请求过期时间(小时)',
|
||||
unitHours: '单位:小时',
|
||||
alertTitle: '计划任务-{0}「 {1} 」任务失败告警',
|
||||
library: {
|
||||
script: '脚本',
|
||||
library: '脚本库',
|
||||
create: '添加脚本',
|
||||
edit: '修改脚本',
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
globalFilter: '全局过滤',
|
||||
|
@ -5,7 +5,7 @@ const cronRouter = {
|
||||
path: '/cronjobs',
|
||||
name: 'Cronjob-Menu',
|
||||
component: Layout,
|
||||
redirect: '/cronjobs',
|
||||
redirect: '/cronjobs/cronjob',
|
||||
meta: {
|
||||
icon: 'p-plan',
|
||||
title: 'menu.cronjob',
|
||||
@ -14,10 +14,31 @@ const cronRouter = {
|
||||
{
|
||||
path: '/cronjobs',
|
||||
name: 'Cronjob',
|
||||
redirect: '/cronjobs/cronjob',
|
||||
component: () => import('@/views/cronjob/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: false,
|
||||
},
|
||||
meta: {},
|
||||
children: [
|
||||
{
|
||||
path: 'cronjob',
|
||||
name: 'CronjobItem',
|
||||
component: () => import('@/views/cronjob/cronjob/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/cronjobs',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
name: 'Library',
|
||||
component: () => import('@/views/cronjob/library/index.vue'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/cronjobs',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
391
frontend/src/views/cronjob/cronjob/index.vue
Normal file
391
frontend/src/views/cronjob/cronjob/index.vue
Normal file
@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutContent v-loading="loading" v-if="!isRecordShow" :title="$t('menu.cronjob')">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onOpenDialog('create')">
|
||||
{{ $t('commons.button.create') }}{{ $t('menu.cronjob') }}
|
||||
</el-button>
|
||||
<el-button-group class="ml-4">
|
||||
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('enable')">
|
||||
{{ $t('commons.button.enable') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('disable')">
|
||||
{{ $t('commons.button.disable') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
<TableSetting title="cronjob-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column
|
||||
:label="$t('cronjob.taskName')"
|
||||
:min-width="120"
|
||||
prop="name"
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="loadDetail(row)">
|
||||
{{ row.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="80" prop="status" sortable>
|
||||
<template #default="{ row }">
|
||||
<Status
|
||||
v-if="row.status === 'Enable'"
|
||||
@click="onChangeStatus(row.id, 'disable')"
|
||||
:status="row.status"
|
||||
/>
|
||||
<Status v-else @click="onChangeStatus(row.id, 'enable')" :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) of row.spec.split(',')" :key="index">
|
||||
<div v-if="row.expand || (!row.expand && index < 3)">
|
||||
<span>
|
||||
{{ row.specCustom ? item : transSpecToStr(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = true">
|
||||
{{ $t('commons.button.expand') }}...
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = false">
|
||||
{{ $t('commons.button.collapse') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.retainCopies')" :min-width="120" prop="retainCopies">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="hasBackup(row.type)" @click="loadBackups(row)" plain size="small">
|
||||
{{ row.retainCopies }}{{ $t('cronjob.retainCopiesUnit') }}
|
||||
</el-button>
|
||||
<span v-else>{{ row.retainCopies }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.lastRecordTime')" :min-width="120" prop="lastRecordTime">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.lastRecordStatus === 'Success'" icon="Select" link type="success" />
|
||||
<el-button v-if="row.lastRecordStatus === 'Failed'" icon="CloseBold" link type="danger" />
|
||||
<el-button v-if="row.lastRecordStatus === 'Waiting'" icon="SemiSelect" link type="info" />
|
||||
{{ row.lastRecordTime }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :min-width="120" :label="$t('setting.backupAccount')">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!hasBackup(row.type)">-</span>
|
||||
<div v-else>
|
||||
<div v-for="(item, index) of row.sourceAccounts" :key="index">
|
||||
<div v-if="row.accountExpand || (!row.accountExpand && index < 3)">
|
||||
<div v-if="row.expand || (!row.expand && index < 3)">
|
||||
<span type="info">
|
||||
<span>
|
||||
{{ loadName(item) }}
|
||||
</span>
|
||||
<el-icon
|
||||
v-if="item === row.downloadAccount"
|
||||
size="12"
|
||||
class="relative top-px left-1"
|
||||
>
|
||||
<Star />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!row.accountExpand && row.sourceAccounts?.length > 3">
|
||||
<el-button type="primary" link @click="row.accountExpand = true">
|
||||
{{ $t('commons.button.expand') }}...
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.accountExpand && row.sourceAccounts?.length > 3">
|
||||
<el-button type="primary" link @click="row.accountExpand = false">
|
||||
{{ $t('commons.button.collapse') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
width="300px"
|
||||
:buttons="buttons"
|
||||
:ellipsis="10"
|
||||
:label="$t('commons.table.operate')"
|
||||
min-width="mobile ? 'auto' : 200"
|
||||
:fixed="mobile ? false : 'right'"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
<el-form class="mt-4 mb-1" v-if="showClean" ref="deleteForm" label-position="left">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
|
||||
<span class="input-help">
|
||||
{{ $t('cronjob.cleanDataHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<Records @search="search" ref="dialogRecordRef" />
|
||||
<Backups @search="search" ref="dialogBackupRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import OperateDialog from '@/views/cronjob/cronjob/operate/index.vue';
|
||||
import Records from '@/views/cronjob/cronjob/record/index.vue';
|
||||
import Backups from '@/views/cronjob/cronjob/backup/index.vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
|
||||
import i18n from '@/lang';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { transSpecToStr } from './helper';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
|
||||
const loading = ref();
|
||||
const selects = ref<any>([]);
|
||||
const isRecordShow = ref();
|
||||
const operateIDs = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const showClean = ref();
|
||||
const cleanData = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'cronjob-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
let params = {
|
||||
info: searchName.value,
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
};
|
||||
loading.value = true;
|
||||
await getCronjobPage(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const dialogRecordRef = ref();
|
||||
const dialogBackupRef = ref();
|
||||
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Cronjob.CronjobInfo> = {
|
||||
specObjs: [
|
||||
{
|
||||
specType: 'perMonth',
|
||||
week: 1,
|
||||
day: 3,
|
||||
hour: 1,
|
||||
minute: 30,
|
||||
second: 30,
|
||||
},
|
||||
],
|
||||
type: 'shell',
|
||||
retainCopies: 7,
|
||||
},
|
||||
) => {
|
||||
let params = {
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
};
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const onDelete = async (row: Cronjob.CronjobInfo | null) => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
showClean.value = false;
|
||||
cleanData.value = false;
|
||||
if (row) {
|
||||
ids = [row.id];
|
||||
names = [row.name];
|
||||
if (hasBackup(row.type)) {
|
||||
showClean.value = true;
|
||||
}
|
||||
} else {
|
||||
for (const item of selects.value) {
|
||||
names.push(item.name);
|
||||
ids.push(item.id);
|
||||
if (hasBackup(item.type)) {
|
||||
showClean.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
operateIDs.value = ids;
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('menu.cronjob'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: null,
|
||||
params: null,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitDelete = async () => {
|
||||
loading.value = true;
|
||||
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeStatus = async (id: number, status: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
|
||||
await updateStatus({ id: id, status: itemStatus });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
});
|
||||
};
|
||||
|
||||
const onBatchChangeStatus = async (status: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
|
||||
for (const item of selects.value) {
|
||||
await updateStatus({ id: item.id, status: itemStatus });
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
});
|
||||
};
|
||||
|
||||
const loadBackups = async (row: any) => {
|
||||
dialogBackupRef.value!.acceptParams({ cronjobID: row.id, cronjob: row.name });
|
||||
};
|
||||
|
||||
const onHandle = async (row: Cronjob.CronjobInfo) => {
|
||||
loading.value = true;
|
||||
await handleOnce(row.id)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const hasBackup = (type: string) => {
|
||||
return (
|
||||
type === 'app' ||
|
||||
type === 'website' ||
|
||||
type === 'database' ||
|
||||
type === 'directory' ||
|
||||
type === 'snapshot' ||
|
||||
type === 'log'
|
||||
);
|
||||
};
|
||||
|
||||
const loadDetail = (row: any) => {
|
||||
isRecordShow.value = true;
|
||||
let params = {
|
||||
rowData: { ...row },
|
||||
};
|
||||
dialogRecordRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const loadName = (from: any) => {
|
||||
let items = from.split(' - ');
|
||||
return i18n.global.t('setting.' + items[0]) + ' ' + items[1];
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.handle'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onHandle(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onOpenDialog('edit', row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('cronjob.record'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
loadDetail(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
@ -229,7 +229,7 @@ import { MsgSuccess } from '@/utils/message';
|
||||
import { listDbItems } from '@/api/modules/database';
|
||||
import { listAppInstalled } from '@/api/modules/app';
|
||||
import { shortcuts } from '@/utils/shortcuts';
|
||||
import TaskLog from '@/components/log/task/log-without-dialog.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
|
||||
const loading = ref();
|
||||
const refresh = ref(false);
|
@ -1,399 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton
|
||||
:buttons="[
|
||||
{
|
||||
label: i18n.global.t('menu.cronjob'),
|
||||
path: '/cronjobs',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<LayoutContent v-loading="loading" v-if="!isRecordShow" :title="$t('menu.cronjob')">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onOpenDialog('create')">
|
||||
{{ $t('commons.button.create') }}{{ $t('menu.cronjob') }}
|
||||
</el-button>
|
||||
<el-button-group class="ml-4">
|
||||
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('enable')">
|
||||
{{ $t('commons.button.enable') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('disable')">
|
||||
{{ $t('commons.button.disable') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
<TableSetting title="cronjob-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@sort-change="search"
|
||||
@search="search"
|
||||
:data="data"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column
|
||||
:label="$t('cronjob.taskName')"
|
||||
:min-width="120"
|
||||
prop="name"
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="loadDetail(row)">
|
||||
{{ row.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" :min-width="80" prop="status" sortable>
|
||||
<template #default="{ row }">
|
||||
<Status
|
||||
v-if="row.status === 'Enable'"
|
||||
@click="onChangeStatus(row.id, 'disable')"
|
||||
:status="row.status"
|
||||
/>
|
||||
<Status v-else @click="onChangeStatus(row.id, 'enable')" :status="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) of row.spec.split(',')" :key="index">
|
||||
<div v-if="row.expand || (!row.expand && index < 3)">
|
||||
<span>
|
||||
{{ row.specCustom ? item : transSpecToStr(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = true">
|
||||
{{ $t('commons.button.expand') }}...
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.expand && row.spec.split(',').length > 3">
|
||||
<el-button type="primary" link @click="row.expand = false">
|
||||
{{ $t('commons.button.collapse') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.retainCopies')" :min-width="120" prop="retainCopies">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="hasBackup(row.type)" @click="loadBackups(row)" plain size="small">
|
||||
{{ row.retainCopies }}{{ $t('cronjob.retainCopiesUnit') }}
|
||||
</el-button>
|
||||
<span v-else>{{ row.retainCopies }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('cronjob.lastRecordTime')" :min-width="120" prop="lastRecordTime">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.lastRecordStatus === 'Success'" icon="Select" link type="success" />
|
||||
<el-button v-if="row.lastRecordStatus === 'Failed'" icon="CloseBold" link type="danger" />
|
||||
<el-button v-if="row.lastRecordStatus === 'Waiting'" icon="SemiSelect" link type="info" />
|
||||
{{ row.lastRecordTime }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :min-width="120" :label="$t('setting.backupAccount')">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!hasBackup(row.type)">-</span>
|
||||
<div v-else>
|
||||
<div v-for="(item, index) of row.sourceAccounts" :key="index">
|
||||
<div v-if="row.accountExpand || (!row.accountExpand && index < 3)">
|
||||
<div v-if="row.expand || (!row.expand && index < 3)">
|
||||
<span type="info">
|
||||
<span>
|
||||
{{ loadName(item) }}
|
||||
</span>
|
||||
<el-icon
|
||||
v-if="item === row.downloadAccount"
|
||||
size="12"
|
||||
class="relative top-px left-1"
|
||||
>
|
||||
<Star />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!row.accountExpand && row.sourceAccounts?.length > 3">
|
||||
<el-button type="primary" link @click="row.accountExpand = true">
|
||||
{{ $t('commons.button.expand') }}...
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="row.accountExpand && row.sourceAccounts?.length > 3">
|
||||
<el-button type="primary" link @click="row.accountExpand = false">
|
||||
{{ $t('commons.button.collapse') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
width="300px"
|
||||
:buttons="buttons"
|
||||
:ellipsis="10"
|
||||
:label="$t('commons.table.operate')"
|
||||
min-width="mobile ? 'auto' : 200"
|
||||
:fixed="mobile ? false : 'right'"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
<RouterButton :buttons="buttons" />
|
||||
<LayoutContent>
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
|
||||
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()">
|
||||
<template #content>
|
||||
<el-form class="mt-4 mb-1" v-if="showClean" ref="deleteForm" label-position="left">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="cleanData" :label="$t('cronjob.cleanData')" />
|
||||
<span class="input-help">
|
||||
{{ $t('cronjob.cleanDataHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</OpDialog>
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<Records @search="search" ref="dialogRecordRef" />
|
||||
<Backups @search="search" ref="dialogBackupRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import OperateDialog from '@/views/cronjob/operate/index.vue';
|
||||
import Records from '@/views/cronjob/record/index.vue';
|
||||
import Backups from '@/views/cronjob/backup/index.vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
|
||||
import i18n from '@/lang';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { transSpecToStr } from './helper';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
|
||||
const loading = ref();
|
||||
const selects = ref<any>([]);
|
||||
const isRecordShow = ref();
|
||||
const operateIDs = ref();
|
||||
|
||||
const opRef = ref();
|
||||
const showClean = ref();
|
||||
const cleanData = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'cronjob-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
let params = {
|
||||
info: searchName.value,
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
};
|
||||
loading.value = true;
|
||||
await getCronjobPage(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data.items || [];
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const dialogRecordRef = ref();
|
||||
const dialogBackupRef = ref();
|
||||
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Cronjob.CronjobInfo> = {
|
||||
specObjs: [
|
||||
{
|
||||
specType: 'perMonth',
|
||||
week: 1,
|
||||
day: 3,
|
||||
hour: 1,
|
||||
minute: 30,
|
||||
second: 30,
|
||||
},
|
||||
],
|
||||
type: 'shell',
|
||||
retainCopies: 7,
|
||||
},
|
||||
) => {
|
||||
let params = {
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
};
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const onDelete = async (row: Cronjob.CronjobInfo | null) => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
showClean.value = false;
|
||||
cleanData.value = false;
|
||||
if (row) {
|
||||
ids = [row.id];
|
||||
names = [row.name];
|
||||
if (hasBackup(row.type)) {
|
||||
showClean.value = true;
|
||||
}
|
||||
} else {
|
||||
for (const item of selects.value) {
|
||||
names.push(item.name);
|
||||
ids.push(item.id);
|
||||
if (hasBackup(item.type)) {
|
||||
showClean.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
operateIDs.value = ids;
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('menu.cronjob'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: null,
|
||||
params: null,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitDelete = async () => {
|
||||
loading.value = true;
|
||||
await deleteCronjob({ ids: operateIDs.value, cleanData: cleanData.value })
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeStatus = async (id: number, status: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
|
||||
await updateStatus({ id: id, status: itemStatus });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
});
|
||||
};
|
||||
|
||||
const onBatchChangeStatus = async (status: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
|
||||
for (const item of selects.value) {
|
||||
await updateStatus({ id: item.id, status: itemStatus });
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
});
|
||||
};
|
||||
|
||||
const loadBackups = async (row: any) => {
|
||||
dialogBackupRef.value!.acceptParams({ cronjobID: row.id, cronjob: row.name });
|
||||
};
|
||||
|
||||
const onHandle = async (row: Cronjob.CronjobInfo) => {
|
||||
loading.value = true;
|
||||
await handleOnce(row.id)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const hasBackup = (type: string) => {
|
||||
return (
|
||||
type === 'app' ||
|
||||
type === 'website' ||
|
||||
type === 'database' ||
|
||||
type === 'directory' ||
|
||||
type === 'snapshot' ||
|
||||
type === 'log'
|
||||
);
|
||||
};
|
||||
|
||||
const loadDetail = (row: any) => {
|
||||
isRecordShow.value = true;
|
||||
let params = {
|
||||
rowData: { ...row },
|
||||
};
|
||||
dialogRecordRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const loadName = (from: any) => {
|
||||
let items = from.split(' - ');
|
||||
return i18n.global.t('setting.' + items[0]) + ' ' + items[1];
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.handle'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onHandle(row);
|
||||
},
|
||||
label: i18n.global.t('menu.cronjob'),
|
||||
path: '/cronjobs/cronjob',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onOpenDialog('edit', row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('cronjob.record'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
loadDetail(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Cronjob.CronjobInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
label: i18n.global.t('cronjob.library.library'),
|
||||
path: '/cronjobs/library',
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
|
225
frontend/src/views/cronjob/library/index.vue
Normal file
225
frontend/src/views/cronjob/library/index.vue
Normal file
@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutContent v-loading="loading" :title="$t('logs.login')">
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onOpenDialog('create')">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="onOpenGroupDialog()">
|
||||
{{ $t('commons.table.group') }}
|
||||
</el-button>
|
||||
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="group" @change="search()" clearable class="p-w-200 mr-2.5">
|
||||
<template #prefix>{{ $t('commons.table.group') }}</template>
|
||||
<div v-for="item in groupOptions" :key="item.id">
|
||||
<el-option :label="item.name" :value="item.id" />
|
||||
</div>
|
||||
</el-select>
|
||||
<TableSearch @search="search()" v-model:searchName="searchInfo" />
|
||||
<TableRefresh @search="search()" />
|
||||
<TableSetting title="script-refresh" @search="search()" />
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@search="search"
|
||||
:heightDiff="370"
|
||||
>
|
||||
<el-table-column type="selection" fix />
|
||||
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="60">
|
||||
<template #default="{ row }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="showScript(row.script)">
|
||||
{{ row.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="60">
|
||||
<template #default="{ row }">
|
||||
<el-tag round v-if="row.isSystem || row.name === '1panel-network'">system</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.group')" min-width="120" prop="group">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="ml-1 mt-1" v-if="!row.isSystem">system</el-tag>
|
||||
<span v-if="row.groupBelong">
|
||||
<el-tag class="ml-1 mt-1" v-for="(item, index) in row.groupBelong" :key="index">
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
min-width="120"
|
||||
:label="$t('commons.table.description')"
|
||||
show-overflow-tooltip
|
||||
prop="description"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations
|
||||
width="300px"
|
||||
:buttons="buttons"
|
||||
:ellipsis="10"
|
||||
:label="$t('commons.table.operate')"
|
||||
min-width="mobile ? 'auto' : 200"
|
||||
:fixed="mobile ? false : 'right'"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<OpDialog ref="opRef" @search="search"></OpDialog>
|
||||
<OperateDialog @search="search" ref="dialogRef" />
|
||||
<GroupDialog @search="loadGroupOptions" :hideDefaultButton="false" ref="dialogGroupRef" />
|
||||
<CodemirrorDialog ref="myDetail" />
|
||||
<TerminalDialog ref="runRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import GroupDialog from '@/components/group/index.vue';
|
||||
import OperateDialog from '@/views/cronjob/library/operate/index.vue';
|
||||
import TerminalDialog from '@/views/cronjob/library/run/index.vue';
|
||||
import { deleteScript, searchScript } from '@/api/modules/cronjob';
|
||||
import { onMounted, reactive, ref } from '@vue/runtime-core';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import i18n from '@/lang';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { getGroupList } from '@/api/modules/group';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
const myDetail = ref();
|
||||
|
||||
const loading = ref();
|
||||
const selects = ref<any>([]);
|
||||
const opRef = ref();
|
||||
|
||||
const runRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'script-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
const searchInfo = ref<string>('');
|
||||
const group = ref<string>('');
|
||||
const groupOptions = ref();
|
||||
|
||||
const dialogGroupRef = ref();
|
||||
const onOpenGroupDialog = () => {
|
||||
dialogGroupRef.value!.acceptParams({ type: 'script' });
|
||||
};
|
||||
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async (
|
||||
title: string,
|
||||
rowData: Partial<Cronjob.ScriptOperate> = {
|
||||
name: '',
|
||||
},
|
||||
) => {
|
||||
let params = {
|
||||
title,
|
||||
rowData: { ...rowData },
|
||||
};
|
||||
dialogRef.value!.acceptParams(params);
|
||||
};
|
||||
|
||||
const showScript = async (script: string) => {
|
||||
let param = {
|
||||
header: i18n.global.t('commons.button.view'),
|
||||
detailInfo: script,
|
||||
};
|
||||
myDetail.value!.acceptParams(param);
|
||||
};
|
||||
|
||||
const onDelete = async (row: Cronjob.ScriptInfo | null) => {
|
||||
let names = [];
|
||||
let ids = [];
|
||||
if (row) {
|
||||
ids = [row.id];
|
||||
names = [row.name];
|
||||
} else {
|
||||
for (const item of selects.value) {
|
||||
names.push(item.name);
|
||||
ids.push(item.id);
|
||||
}
|
||||
}
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('menu.cronjob'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: deleteScript,
|
||||
params: ids,
|
||||
});
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
let params = {
|
||||
info: searchInfo.value,
|
||||
groupID: Number(group.value),
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
};
|
||||
loading.value = true;
|
||||
await searchScript(params)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getGroupList('script');
|
||||
groupOptions.value = res.data || [];
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.handle'),
|
||||
click: (row: Cronjob.ScriptInfo) => {
|
||||
runRef.value!.acceptParams({ scriptID: row.id, scriptName: row.name });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Cronjob.ScriptInfo) => {
|
||||
onOpenDialog('edit', row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: Cronjob.ScriptInfo) => {
|
||||
onDelete(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
loadGroupOptions();
|
||||
});
|
||||
</script>
|
126
frontend/src/views/cronjob/library/operate/index.vue
Normal file
126
frontend/src/views/cronjob/library/operate/index.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="drawerVisible"
|
||||
:header="title"
|
||||
@close="handleClose"
|
||||
:resource="dialogData.title === 'create' ? '' : dialogData.rowData?.name"
|
||||
size="large"
|
||||
>
|
||||
<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-form-item>
|
||||
<el-form-item :label="$t('commons.table.group')" prop="groupList">
|
||||
<el-select filterable v-model="dialogData.rowData!.groupList" multiple>
|
||||
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('cronjob.shellContent')" prop="script" class="mt-5">
|
||||
<CodemirrorPro
|
||||
v-model="dialogData.rowData!.script"
|
||||
placeholder="#Define or paste the content of your script file here"
|
||||
mode="javascript"
|
||||
:heightDiff="400"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input clearable v-model="dialogData.rowData!.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { Cronjob } from '@/api/interface/cronjob';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { addScript, editScript } from '@/api/modules/cronjob';
|
||||
import { getGroupList } from '@/api/modules/group';
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
rowData?: Cronjob.ScriptInfo;
|
||||
getTableList?: () => Promise<any>;
|
||||
}
|
||||
const title = ref<string>('');
|
||||
const drawerVisible = ref(false);
|
||||
const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const loading = ref();
|
||||
const groupOptions = ref();
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value = params;
|
||||
title.value = i18n.global.t('cronjob.library.' + dialogData.value.title);
|
||||
loadGroupOptions();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput],
|
||||
script: [Rules.requiredInput],
|
||||
groupList: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
dialogData.value.rowData.groups = dialogData.value.rowData.groupList.join(',');
|
||||
if (dialogData.value.title === 'create') {
|
||||
await addScript(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await editScript(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const loadGroupOptions = async () => {
|
||||
const res = await getGroupList('script');
|
||||
groupOptions.value = res.data || [];
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
70
frontend/src/views/cronjob/library/run/index.vue
Normal file
70
frontend/src/views/cronjob/library/run/index.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="terminalVisible"
|
||||
:header="$t('menu.terminal')"
|
||||
@close="handleClose"
|
||||
:resource="scriptName"
|
||||
size="large"
|
||||
>
|
||||
<template #content>
|
||||
<el-alert type="error" :closable="false">
|
||||
<template #title>
|
||||
<span>{{ $t('commons.msg.disConn', ['exit']) }}</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
<Terminal style="height: calc(100vh - 235px); margin-top: 18px" ref="terminalRef"></Terminal>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onClose()">{{ $t('commons.button.disConn') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import Terminal from '@/components/terminal/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const terminalVisible = ref(false);
|
||||
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
||||
const scriptID = ref();
|
||||
const scriptName = ref();
|
||||
|
||||
interface DialogProps {
|
||||
scriptID: number;
|
||||
scriptName: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
terminalVisible.value = true;
|
||||
scriptID.value = params.scriptID;
|
||||
scriptName.value = params.scriptName;
|
||||
initTerm();
|
||||
};
|
||||
|
||||
const initTerm = async () => {
|
||||
await nextTick();
|
||||
terminalRef.value!.acceptParams({
|
||||
endpoint: '/api/v2/core/script/run',
|
||||
args: `script_id=${scriptID.value}¤t_node=${globalStore.currentNode}`,
|
||||
error: '',
|
||||
initCmd: '',
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
terminalRef.value?.onClose();
|
||||
terminalVisible.value = false;
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
onClose();
|
||||
terminalVisible.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -18,7 +18,6 @@
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="group" @change="search()" clearable class="p-w-200 mr-2.5">
|
||||
<template #prefix>{{ $t('commons.table.group') }}</template>
|
||||
<el-option :label="$t('commons.table.all')" value=""></el-option>
|
||||
<div v-for="item in groupList" :key="item.id">
|
||||
<el-option
|
||||
v-if="item.name === 'default'"
|
||||
|
@ -638,7 +638,6 @@ function load18n(label: string) {
|
||||
return i18n.global.t('logs.task');
|
||||
case 'Database':
|
||||
case 'Cronjob':
|
||||
case 'Database':
|
||||
case 'Container':
|
||||
case 'App':
|
||||
case 'System':
|
||||
|
Loading…
x
Reference in New Issue
Block a user