1
0
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:
ssongliu 2025-03-05 14:21:06 +08:00 committed by GitHub
parent 8d2ea2f233
commit a37fa5c77c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1468 additions and 525 deletions

View File

@ -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{},

View File

@ -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)

View File

@ -18,4 +18,5 @@ var (
groupService = service.NewIGroupService()
commandService = service.NewICommandService()
appLauncherService = service.NewIAppLauncher()
scriptService = service.NewIScriptService()
)

View File

@ -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
}

View 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
}
}

View File

@ -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"`

View File

@ -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"`

View File

@ -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"`
}

View 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"`
}

View 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"`
}

View 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+"%")
}
}

View File

@ -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()
)

View File

@ -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":

View File

@ -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))

View 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))
}

View File

@ -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

View File

@ -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=

View File

@ -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!"

View File

@ -27,6 +27,7 @@ ErrDemoEnvironment: "デモサーバーではこの操作は許可されてい
ErrCmdTimeout: "コマンドの実行がタイムアウトしました!"
ErrEntrance: "セキュリティ情報エラー、再確認してください!"
ErrGroupIsDefault: "デフォルトグループの削除はできません"
ErrGroupIsInUse: "グループは使用中のため、削除できません。"
ErrLocalDelete: "ローカルノードは削除できません!"
ErrPortInUsed: "{{ .name }} ポートはすでに使用されています!"

View File

@ -27,6 +27,7 @@ ErrDemoEnvironment: "데모 서버에서는 이 작업이 금지되어 있습니
ErrCmdTimeout: "명령 실행 시간 초과!"
ErrEntrance: "보안 정보 오류입니다. 확인 후 다시 시도하십시오!"
ErrGroupIsDefault: "기본 그룹은 삭제할 수 없습니다"
ErrGroupIsInUse: "그룹이 사용 중이므로 삭제할 수 없습니다."
ErrLocalDelete: "로컬 노드는 삭제할 수 없습니다!"
ErrPortInUsed: "{{ .name }} 포트가 이미 사용 중입니다!"

View File

@ -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!"

View File

@ -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!"

View File

@ -27,6 +27,7 @@ ErrDemoEnvironment: "Демонстрационный сервер, эта оп
ErrCmdTimeout: "Время выполнения команды истекло!"
ErrEntrance: "Ошибка информации о безопасном входе, проверьте и повторите попытку!"
ErrGroupIsDefault: "Группу по умолчанию нельзя удалить"
ErrGroupIsInUse: "Группа используется и не может быть удалена."
ErrLocalDelete: "Локальный узел нельзя удалить!"
ErrPortInUsed: "Порт {{ .name }} уже используется!"

View File

@ -27,6 +27,7 @@ ErrDemoEnvironment: "演示伺服器,禁止此操作!"
ErrCmdTimeout: "指令執行逾時!"
ErrEntrance: "安全入口資訊錯誤,請檢查後再試!"
ErrGroupIsDefault: "預設分組無法刪除"
ErrGroupIsInUse: "分組正被使用,無法刪除。"
ErrLocalDelete: "無法刪除本地節點!"
ErrPortInUsed: "{{ .name }} 埠已被佔用!"

View File

@ -27,6 +27,7 @@ ErrDemoEnvironment: "演示服务器,禁止此操作!"
ErrCmdTimeout: "命令执行超时!"
ErrEntrance: "安全入口信息错误,请检查后重试!"
ErrGroupIsDefault: "默认分组,无法删除"
ErrGroupIsInUse: "分组正被使用,无法删除"
ErrLocalDelete: "无法删除本地节点!"
ErrPortInUsed: "{{ .name }} 端口已被占用!"

View File

@ -23,6 +23,7 @@ func Init() {
migrations.AddMFAInterval,
migrations.UpdateXpackHideMemu,
migrations.AddSystemIP,
migrations.InitScriptLibrary,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -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
},
}

View File

@ -10,5 +10,6 @@ func commonGroups() []CommonRouter {
&HostRouter{},
&GroupRouter{},
&AppLauncherRouter{},
&ScriptRouter{},
}
}

View 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)
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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 });
};

View File

@ -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();

View File

@ -1033,6 +1033,12 @@ const message = {
requestExpirationTime: 'Upload Request Expiration TimeHours',
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',

View File

@ -994,6 +994,12 @@ const message = {
requestExpirationTime: 'リクエストの有効期限時間のアップロード',
unitHours: 'ユニット:時間',
alertTitle: '計画タスク - {0}{1}タスク障害アラート',
library: {
script: 'スクリプト',
library: 'スクリプトライブラリ',
create: 'スクリプトを追加',
edit: 'スクリプトを編集',
},
},
monitor: {
globalFilter: 'グローバルフィルター',

View File

@ -987,6 +987,12 @@ const message = {
requestExpirationTime: '업로드 요청 만료 시간(시간)',
unitHours: '단위: 시간',
alertTitle: '예정된 작업 - {0} {1} 작업 실패 경고',
library: {
script: '스크립트',
library: '스크립트 라이브러리',
create: '스크립트 추가',
edit: '스크립트 수정',
},
},
monitor: {
globalFilter: '전역 필터',

View File

@ -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',

View File

@ -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',

View File

@ -1017,6 +1017,12 @@ const message = {
requestExpirationTime: 'Время истечения запроса на загрузку (часы)',
unitHours: 'Единица: часы',
alertTitle: 'Плановая задача - {0} «{1}» Оповещение о сбое задачи',
library: {
script: 'Скрипт',
library: 'Библиотека скриптов',
create: 'Добавить скрипт',
edit: 'Редактировать скрипт',
},
},
monitor: {
globalFilter: 'Глобальный фильтр',

View File

@ -980,6 +980,12 @@ const message = {
requestExpirationTime: '上傳請求過期時間小時',
unitHours: '單位小時',
alertTitle: '計畫任務-{0}{1}任務失敗告警',
library: {
script: '腳本',
library: '腳本庫',
create: '添加腳本',
edit: '修改腳本',
},
},
monitor: {
globalFilter: '全局過濾',

View File

@ -978,6 +978,12 @@ const message = {
requestExpirationTime: '上传请求过期时间小时',
unitHours: '单位小时',
alertTitle: '计划任务-{0} {1} 任务失败告警',
library: {
script: '脚本',
library: '脚本库',
create: '添加脚本',
edit: '修改脚本',
},
},
monitor: {
globalFilter: '全局过滤',

View File

@ -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,
},
},
],
},
],
};

View 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>

View File

@ -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);

View File

@ -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>

View 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>

View 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>

View 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}&current_node=${globalStore.currentNode}`,
error: '',
initCmd: '',
});
};
const onClose = () => {
terminalRef.value?.onClose();
terminalVisible.value = false;
};
function handleClose() {
onClose();
terminalVisible.value = false;
}
defineExpose({
acceptParams,
});
</script>

View File

@ -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'"

View File

@ -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':