Compare commits

..
26 Commits
Author SHA1 Message Date
CityFun 04b9cbd87a fix: Fixed issue with recover postgres-cluster database failed (#9732) 2025-07-29 16:19:39 +08:00
ssongliu 4565c5b300 fix: Fix fatabase backup cronjobimport failures (#9731) 2025-07-29 16:03:55 +08:00
ssongliu bf9a9bf17a fix: Fix execution refresh issues for cronjob (#9728) 2025-07-29 14:51:34 +08:00
ssongliu 70df4dc6c4 fix: Fix import failures for cronjob (#9727) 2025-07-29 14:32:25 +08:00
ssongliu ed3e738fff fix: Fix import issues of shell cronjob (#9726) 2025-07-29 04:09:49 +00:00
ssongliu bc376ec495 fix: script-type scheduled task import/export issues (#9713) 2025-07-28 19:04:35 +08:00
Snrat 40314e9079 Fix Issue with Podman Unable to Change PHP Runtime in Website Page (#9711)
* Update container.go

* Update website.go
2025-07-28 18:34:05 +08:00
ssongliu 82f978c683 fix: Add file upload restrictions for cronjob (#9710) 2025-07-28 18:33:15 +08:00
CityFun ed288368ea fix: Fixed issue with recover mysql-cluster with backup files (#9707) 2025-07-28 18:01:05 +08:00
sheng.wang db8fa144eb fix: Fix crash when adding custom spec if specs is null (#9708) 2025-07-28 18:00:35 +08:00
ssongliu 57b76d97ec fix: Optimize task log tracking mechanism (#9704) 2025-07-28 17:12:42 +08:00
蘭 28d8210ce6 fix: add nil checks for transport.DialContext (#9702) 2025-07-28 17:02:46 +08:00
ssongliu 80ce60c56c fix: Fix port forwarding error issues (#9700)
Refs #9658
2025-07-28 08:29:40 +00:00
Snrat 2758ab07dc Fix Issue with Podman Unable to Create PHP Runtime Website (#9697)
* Update website.go

* Update container.go
2025-07-28 15:38:27 +08:00
CityFun 0828804de2 fix: Fix the issue where the host IP is shown as 127.0.0.1 after enabling 'allowPort' in the application parameter settings. (#9699) 2025-07-28 15:36:25 +08:00
ssongliu 134b17d2f0 fix: Resolve certificate validate failure Issues (#9698) 2025-07-28 15:36:10 +08:00
ssongliu 3af254cdda fix: Add lightweight object storage compatibility for COS backup acco… (#9673)
Refs #9494
2025-07-25 11:07:36 +00:00
蘭 f3d3d54722 feat: Customize login page buttons and link colors (#9671)
refs  #9061
2025-07-25 11:05:37 +00:00
CityFun fa419809f6 fix: Fixed issue with create website with openresty defaultHttpsPort (#9672) 2025-07-25 19:04:19 +08:00
CityFun da606826c6 fix: Fixed Changing the runtime container name may cause it to fail to start (#9670)
Refs https://github.com/1Panel-dev/1Panel/issues/9666
2025-07-25 11:03:35 +00:00
ssongliu b3d9cc156a fix: Optimize filename generation for directory backup (#9669)
Refs #9433
2025-07-25 09:21:35 +00:00
ssongliu ba03d26b2c fix: Optimize cronjob report loading (#9665)
Refs #9412
2025-07-25 08:35:36 +00:00
ssongliu 40f02889fe feat: Add import/export support for cronjob (#9663)
Refs #5157
2025-07-25 07:15:35 +00:00
CityFun 4f8fb26d89 feat: Adapt default website domain to non-standard OpenResty port during creation (#9664) 2025-07-25 15:15:33 +08:00
ssongliu 0ca11e2b88 fix: Fix node switching style issues (#9641) 2025-07-24 11:30:08 +08:00
ssongliu 0daf0d690c fix: Resolve deleted node status display issue (#9633) 2025-07-24 10:23:45 +08:00
54 changed files with 1217 additions and 268 deletions
+2 -2
View File
@@ -466,12 +466,12 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
}
switch req.Type {
case "mysql", "mariadb":
case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlRecoverByUpload(req); err != nil {
helper.InternalServer(c, err)
return
}
case constant.AppPostgresql:
case constant.AppPostgresql, constant.AppPostgresqlCluster:
if err := backupService.PostgresqlRecoverByUpload(req); err != nil {
helper.InternalServer(c, err)
return
+45
View File
@@ -1,6 +1,8 @@
package v2
import (
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
@@ -53,6 +55,49 @@ func (b *BaseApi) LoadCronjobInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Cronjob
// @Summary Export cronjob list
// @Accept json
// @Param request body dto.OperateByIDs true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /cronjobs/export [post]
func (b *BaseApi) ExportCronjob(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := cronjobService.Export(req)
if err != nil {
helper.InternalServer(c, err)
return
}
http.ServeContent(c.Writer, c.Request, "", time.Now(), strings.NewReader(content))
}
// @Tags Cronjob
// @Summary Import cronjob list
// @Accept json
// @Param request body dto.CronjobImport true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /cronjobs/import [post]
func (b *BaseApi) ImportCronjob(c *gin.Context) {
var req dto.CronjobImport
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := cronjobService.Import(req.Cronjobs); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Cronjob
// @Summary Load script options
// @Success 200 {array} dto.ScriptOptions
+3
View File
@@ -28,6 +28,9 @@ type OperationWithName struct {
type OperateByID struct {
ID uint `json:"id" validate:"required"`
}
type OperateByIDs struct {
IDs []uint `json:"ids"`
}
type Operate struct {
Operation string `json:"operation" validate:"required"`
+51
View File
@@ -123,6 +123,57 @@ type CronjobInfo struct {
AlertCount uint `json:"alertCount"`
}
type CronjobImport struct {
Cronjobs []CronjobTrans `json:"cronjobs"`
}
type CronjobTrans struct {
Name string `json:"name"`
Type string `json:"type"`
SpecCustom bool `json:"specCustom"`
Spec string `json:"spec"`
Executor string `json:"executor"`
ScriptMode string `json:"scriptMode"`
Script string `json:"script"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
User string `json:"user"`
URL string `json:"url"`
ScriptName string `json:"scriptName"`
Apps []TransHelper `json:"apps"`
Websites []string `json:"websites"`
DBType string `json:"dbType"`
DBNames []TransHelper `json:"dbName"`
ExclusionRules string `json:"exclusionRules"`
IsDir bool `json:"isDir"`
SourceDir string `json:"sourceDir"`
RetainCopies uint64 `json:"retainCopies"`
RetryTimes uint `json:"retryTimes"`
Timeout uint `json:"timeout"`
IgnoreErr bool `json:"ignoreErr"`
SnapshotRule SnapshotTransHelper `json:"snapshotRule"`
Secret string `json:"secret"`
SourceAccounts []string `json:"sourceAccounts"`
DownloadAccount string `json:"downloadAccount"`
AlertCount uint `json:"alertCount"`
AlertTitle string `json:"alertTitle"`
AlertMethod string `json:"alertMethod"`
}
type TransHelper struct {
Name string `json:"name"`
DetailName string `json:"detailName"`
}
type SnapshotTransHelper struct {
WithImage bool `json:"withImage"`
IgnoreApps []TransHelper `json:"ignoreApps"`
}
type ScriptOptions struct {
ID uint `json:"id"`
Name string `json:"name"`
+8 -6
View File
@@ -1,8 +1,9 @@
package response
import (
"github.com/1Panel-dev/1Panel/agent/utils/files"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/files"
)
type FileInfo struct {
@@ -37,11 +38,12 @@ type FileWgetRes struct {
}
type FileLineContent struct {
Content string `json:"content"`
End bool `json:"end"`
Path string `json:"path"`
Total int `json:"total"`
Lines []string `json:"lines"`
Content string `json:"content"`
End bool `json:"end"`
Path string `json:"path"`
Total int `json:"total"`
TaskStatus string `json:"taskStatus"`
Lines []string `json:"lines"`
}
type FileExist struct {
+21
View File
@@ -36,6 +36,7 @@ type IAppInstallRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error)
BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error
LoadBaseInfo(key string, name string) (*RootInfo, error)
LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error)
GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error)
}
@@ -246,3 +247,23 @@ func (a *AppInstallRepo) LoadBaseInfo(key string, name string) (*RootInfo, error
info.Status = appInstall.Status
return &info, nil
}
func (a *AppInstallRepo) LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error) {
var (
app model.App
appInstall model.AppInstall
)
if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil {
return nil, err
}
if len(name) == 0 {
if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil {
return nil, err
}
} else {
if err := global.DB.Where("app_id = ? AND name = ?", app.ID, name).First(&appInstall).Error; err != nil {
return nil, err
}
}
return &appInstall, nil
}
+26
View File
@@ -1240,6 +1240,32 @@ func checkImageExist(client *client.Client, imageItem string) bool {
return false
}
func checkImageLike(client *client.Client, imageName string) bool {
if client == nil {
var err error
client, err = docker.NewDockerClient()
if err != nil {
return false
}
}
images, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
return false
}
for _, img := range images {
for _, tag := range img.RepoTags {
parts := strings.Split(tag, "/")
imageNameWithTag := parts[len(parts)-1]
if imageNameWithTag == imageName {
return true
}
}
}
return false
}
func pullImages(task *task.Task, client *client.Client, imageName string) error {
dockerCli := docker.NewClientWithExist(client)
options := image.PullOptions{}
+292
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -15,6 +16,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
@@ -35,6 +37,8 @@ type ICronjobService interface {
StartJob(cronjob *model.Cronjob, isUpdate bool) (string, error)
CleanRecord(req dto.CronjobClean) error
Export(req dto.OperateByIDs) (string, error)
Import(req []dto.CronjobTrans) error
LoadScriptOptions() []dto.ScriptOptions
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
@@ -102,6 +106,290 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
return &item, err
}
func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
cronjobs, err := cronjobRepo.List(repo.WithByIDs(req.IDs))
if err != nil {
return "", err
}
var data []dto.CronjobTrans
for _, cronjob := range cronjobs {
item := dto.CronjobTrans{
Name: cronjob.Name,
Type: cronjob.Type,
SpecCustom: cronjob.SpecCustom,
Spec: cronjob.Spec,
Executor: cronjob.Executor,
ScriptMode: cronjob.ScriptMode,
Script: cronjob.Script,
Command: cronjob.Command,
ContainerName: cronjob.ContainerName,
User: cronjob.User,
URL: cronjob.URL,
DBType: cronjob.DBType,
ExclusionRules: cronjob.ExclusionRules,
IsDir: cronjob.IsDir,
SourceDir: cronjob.SourceDir,
RetainCopies: cronjob.RetainCopies,
RetryTimes: cronjob.RetryTimes,
Timeout: cronjob.Timeout,
IgnoreErr: cronjob.IgnoreErr,
Secret: cronjob.Secret,
}
switch cronjob.Type {
case "app":
if cronjob.AppID == "all" {
break
}
apps := loadAppsForJob(cronjob)
for _, app := range apps {
item.Apps = append(item.Apps, dto.TransHelper{Name: app.App.Key, DetailName: app.Name})
}
case "website", "cutWebsiteLog":
if cronjob.Website == "all" {
break
}
websites := loadWebsForJob(cronjob)
for _, website := range websites {
item.Websites = append(item.Websites, website.Alias)
}
case "database":
if cronjob.DBName == "all" {
break
}
databases := loadDbsForJob(cronjob)
for _, db := range databases {
item.DBNames = append(item.DBNames, dto.TransHelper{Name: db.Database, DetailName: db.Name})
}
case "shell":
if cronjob.ScriptMode == "library" {
script, err := scriptRepo.Get(repo.WithByID(cronjob.ScriptID))
if err != nil {
return "", err
}
item.ScriptName = script.Name
}
case "snapshot":
if len(cronjob.SnapshotRule) == 0 {
break
}
var snapRule dto.SnapshotRule
if err := json.Unmarshal([]byte(cronjob.SnapshotRule), &snapRule); err != nil {
return "", err
}
item.SnapshotRule.WithImage = snapRule.WithImage
if len(snapRule.IgnoreAppIDs) != 0 {
ignoreApps, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(snapRule.IgnoreAppIDs))
for _, app := range ignoreApps {
item.SnapshotRule.IgnoreApps = append(item.SnapshotRule.IgnoreApps, dto.TransHelper{Name: app.App.Key, DetailName: app.Name})
}
}
}
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
if alertInfo.SendCount != 0 {
item.AlertCount = alertInfo.SendCount
item.AlertTitle = alertInfo.Title
item.AlertMethod = alertInfo.Method
} else {
item.AlertCount = 0
}
data = append(data, item)
}
itemJson, err := json.Marshal(&data)
if err != nil {
return "", err
}
return string(itemJson), nil
}
func (u *CronjobService) Import(req []dto.CronjobTrans) error {
for _, item := range req {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 {
continue
}
cronjob := model.Cronjob{
Name: item.Name,
Type: item.Type,
SpecCustom: item.SpecCustom,
Spec: item.Spec,
Executor: item.Executor,
ScriptMode: item.ScriptMode,
Command: item.Command,
ContainerName: item.ContainerName,
User: item.User,
URL: item.URL,
DBType: item.DBType,
ExclusionRules: item.ExclusionRules,
IsDir: item.IsDir,
RetainCopies: item.RetainCopies,
RetryTimes: item.RetryTimes,
Timeout: item.Timeout,
IgnoreErr: item.IgnoreErr,
Secret: item.Secret,
}
hasNotFound := false
switch item.Type {
case "app":
if len(item.Apps) == 0 {
cronjob.AppID = "all"
break
}
var appIDs []string
for _, app := range item.Apps {
appItem, err := appInstallRepo.LoadInstallAppByKeyAndName(app.Name, app.DetailName)
if err != nil {
hasNotFound = true
continue
}
appIDs = append(appIDs, fmt.Sprintf("%v", appItem.ID))
}
cronjob.AppID = strings.Join(appIDs, ",")
case "website", "cutWebsiteLog":
if len(item.Websites) == 0 {
cronjob.Website = "all"
break
}
var webIDs []string
for _, web := range item.Websites {
webItem, err := websiteRepo.GetFirst(websiteRepo.WithAlias(web))
if err != nil {
hasNotFound = true
continue
}
webIDs = append(webIDs, fmt.Sprintf("%v", webItem.ID))
}
cronjob.Website = strings.Join(webIDs, ",")
case "database":
if len(item.DBNames) == 0 {
cronjob.DBName = "all"
break
}
var dbIDs []string
if strings.Contains(cronjob.DBType, "postgresql") {
for _, db := range item.DBNames {
dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(db.Name), repo.WithByName(db.DetailName))
if err != nil {
hasNotFound = true
continue
}
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
}
} else {
for _, db := range item.DBNames {
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName))
if err != nil {
hasNotFound = true
continue
}
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
}
}
cronjob.DBName = strings.Join(dbIDs, ",")
case "shell":
if len(item.ContainerName) != 0 {
cronjob.Script = item.Script
client, err := docker.NewDockerClient()
if err != nil {
hasNotFound = true
break
}
defer client.Close()
if _, err := client.ContainerStats(context.Background(), item.ContainerName, false); err != nil {
hasNotFound = true
break
}
}
switch item.ScriptMode {
case "library":
library, _ := scriptRepo.Get(repo.WithByName(item.ScriptName))
if library.ID == 0 {
hasNotFound = true
break
}
cronjob.ScriptID = library.ID
case "select":
if _, err := os.Stat(item.Script); err != nil {
hasNotFound = true
break
}
cronjob.Script = item.Script
case "input":
cronjob.Script = item.Script
}
case "directory":
if item.IsDir {
if _, err := os.Stat(item.SourceDir); err != nil {
hasNotFound = true
break
}
cronjob.SourceDir = item.SourceDir
} else {
fileList := strings.Split(item.SourceDir, ",")
var newFiles []string
for _, item := range fileList {
if len(item) == 0 {
continue
}
if _, err := os.Stat(item); err != nil {
hasNotFound = true
continue
}
newFiles = append(newFiles, item)
}
cronjob.SourceDir = strings.Join(newFiles, ",")
}
case "snapshot":
if len(item.SnapshotRule.IgnoreApps) == 0 && !item.SnapshotRule.WithImage {
break
}
var itemRules dto.SnapshotRule
itemRules.WithImage = item.SnapshotRule.WithImage
for _, app := range item.SnapshotRule.IgnoreApps {
appItem, err := appInstallRepo.LoadInstallAppByKeyAndName(app.Name, app.DetailName)
if err != nil {
hasNotFound = true
continue
}
itemRules.IgnoreAppIDs = append(itemRules.IgnoreAppIDs, appItem.ID)
}
itemRulesStr, _ := json.Marshal(itemRules)
cronjob.SnapshotRule = string(itemRulesStr)
}
var acIDs []string
for _, ac := range item.SourceAccounts {
backup, err := backupRepo.Get(repo.WithByName(ac))
if err != nil {
hasNotFound = true
continue
}
if ac == item.DownloadAccount {
cronjob.DownloadAccountID = backup.ID
}
acIDs = append(acIDs, fmt.Sprintf("%v", backup.ID))
}
cronjob.SourceAccountIDs = strings.Join(acIDs, ",")
if hasNotFound {
cronjob.Status = constant.StatusPending
} else {
cronjob.Status = constant.StatusDisable
}
_ = cronjobRepo.Create(&cronjob)
if item.AlertCount != 0 {
createAlert := dto.AlertCreate{
Title: item.AlertTitle,
SendCount: item.AlertCount,
Method: item.AlertMethod,
Type: cronjob.Type,
Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable,
}
_ = NewIAlertService().CreateAlert(createAlert)
}
}
return nil
}
func (u *CronjobService) LoadScriptOptions() []dto.ScriptOptions {
scripts, err := scriptRepo.List()
if err != nil {
@@ -411,6 +699,9 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
}
}
if cronModel.Status == constant.StatusPending {
upMap["status"] = constant.StatusEnable
}
upMap["name"] = req.Name
upMap["spec_custom"] = req.SpecCustom
upMap["spec"] = spec
@@ -428,6 +719,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
upMap["db_type"] = req.DBType
upMap["db_name"] = req.DBName
upMap["url"] = req.URL
upMap["is_dir"] = req.IsDir
upMap["source_dir"] = req.SourceDir
upMap["snapshot_rule"] = cronjob.SnapshotRule
+34 -17
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
@@ -24,22 +25,7 @@ import (
)
func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
var apps []model.AppInstall
if cronjob.AppID == "all" {
apps, _ = appInstallRepo.ListBy(context.Background())
} else {
appIds := strings.Split(cronjob.AppID, ",")
var idItems []uint
for i := 0; i < len(appIds); i++ {
itemID, _ := strconv.Atoi(appIds[i])
idItems = append(idItems, uint(itemID))
}
appItems, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems))
if err != nil {
return err
}
apps = appItems
}
apps := loadAppsForJob(cronjob)
if len(apps) == 0 {
return errors.New("no such app in database!")
}
@@ -214,7 +200,10 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
if err != nil {
return err
}
fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(2))
if cronjob.IsDir || len(strings.Split(cronjob.SourceDir, ",")) == 1 {
fileName = loadFileName(cronjob.SourceDir)
}
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/%s/%s", cronjob.Type, cronjob.Name))
fileOp := files.NewFileOp()
@@ -344,6 +333,23 @@ func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.J
return nil
}
func loadAppsForJob(cronjob model.Cronjob) []model.AppInstall {
var apps []model.AppInstall
if cronjob.AppID == "all" {
apps, _ = appInstallRepo.ListBy(context.Background())
} else {
appIds := strings.Split(cronjob.AppID, ",")
var idItems []uint
for i := 0; i < len(appIds); i++ {
itemID, _ := strconv.Atoi(appIds[i])
idItems = append(idItems, uint(itemID))
}
appItems, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems))
apps = appItems
}
return apps
}
type DatabaseHelper struct {
ID uint
DBType string
@@ -546,3 +552,14 @@ func loadSnapWithRule(cronjob model.Cronjob) (dto.SnapshotData, error) {
}
return itemData, nil
}
func loadFileName(src string) string {
dirs := strings.Split(filepath.ToSlash(src), "/")
var keyPart string
if len(dirs) >= 3 {
keyPart = filepath.Join(dirs[len(dirs)-3], dirs[len(dirs)-2], dirs[len(dirs)-1])
}
cleanName := strings.ReplaceAll(keyPart, string(filepath.Separator), "_")
timestamp := time.Now().Format(constant.DateTimeSlimLayout)
return fmt.Sprintf("%s_%s_%s.tar.gz", cleanName, timestamp, common.RandStrAndNum(2))
}
+13 -9
View File
@@ -4,10 +4,6 @@ import (
"bufio"
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/jinzhu/copier"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/simplifiedchinese"
"io"
"io/fs"
"os"
@@ -20,6 +16,11 @@ import (
"time"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/jinzhu/copier"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/simplifiedchinese"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -478,6 +479,7 @@ func (f *FileService) DepthDirSize(req request.DirSizeReq) ([]response.DepthDirS
func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) {
logFilePath := ""
taskStatus := ""
switch req.Type {
case constant.TypeWebsite:
website, err := websiteRepo.GetFirst(repo.WithByID(req.ID))
@@ -533,6 +535,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
return nil, err
}
logFilePath = taskModel.LogFile
taskStatus = taskModel.Status
case "mysql-slow-logs":
logFilePath = path.Join(global.Dir.DataDir, fmt.Sprintf("apps/mysql/%s/data/1Panel-slow.log", req.Name))
case "mariadb-slow-logs":
@@ -551,11 +554,12 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
lines = append(preLines, lines...)
}
res := &response.FileLineContent{
Content: strings.Join(lines, "\n"),
End: isEndOfFile,
Path: logFilePath,
Total: total,
Lines: lines,
Content: strings.Join(lines, "\n"),
End: isEndOfFile,
Path: logFilePath,
Total: total,
TaskStatus: taskStatus,
Lines: lines,
}
return res, nil
}
+1
View File
@@ -450,6 +450,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err := checkContainerName(containerName.(string)); err != nil {
return err
}
runtime.ContainerName = containerName.(string)
}
projectDir := path.Join(global.Dir.RuntimeDir, runtime.Type, runtime.Name)
+5 -5
View File
@@ -257,7 +257,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
var (
domains []model.WebsiteDomain
)
domains, _, _, err = getWebsiteDomains(create.Domains, defaultHttpPort, 0)
domains, _, _, err = getWebsiteDomains(create.Domains, defaultHttpPort, nginxInstall.HttpsPort, 0)
if err != nil {
return err
}
@@ -388,7 +388,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
switch runtime.Type {
case constant.RuntimePHP:
if runtime.Resource == constant.ResourceAppstore {
if !checkImageExist(nil, runtime.Image) {
if !checkImageLike(nil, runtime.Image) {
return buserr.WithName("ErrImageNotExist", runtime.Name)
}
website.Proxy = fmt.Sprintf("127.0.0.1:%s", runtime.Port)
@@ -680,7 +680,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
domainModels []model.WebsiteDomain
addPorts []int
)
httpPort, _, err := getAppInstallPort(constant.AppOpenresty)
httpPort, httpsPort, err := getAppInstallPort(constant.AppOpenresty)
if err != nil {
return nil, err
}
@@ -689,7 +689,7 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
return nil, err
}
domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, create.WebsiteID)
domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, httpsPort, create.WebsiteID)
if err != nil {
return nil, err
}
@@ -1354,7 +1354,7 @@ func (w WebsiteService) ChangePHPVersion(req request.WebsitePHPVersionReq) error
return err
}
defer client.Close()
if !checkImageExist(client, oldRuntime.Image) {
if !checkImageLike(client, oldRuntime.Image) {
return buserr.WithName("ErrImageNotExist", oldRuntime.Name)
}
}
+4 -4
View File
@@ -1031,7 +1031,7 @@ func chownRootDir(path string) error {
return nil
}
func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) {
func getWebsiteDomains(domains []request.WebsiteDomain, defaultHTTPPort, defaultHTTPsPort int, websiteID uint) (domainModels []model.WebsiteDomain, addPorts []int, addDomains []string, err error) {
var (
ports = make(map[int]struct{})
existPort = make(map[int]struct{})
@@ -1056,7 +1056,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
domainModel.Domain = strings.ToLower(domainModel.Domain)
domainModel.Port = domain.Port
if domain.Port == 0 {
domain.Port = defaultPort
domain.Port = defaultHTTPPort
}
domainModel.SSL = domain.SSL
domainModel.WebsiteID = websiteID
@@ -1077,7 +1077,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
}
for port := range ports {
if port == defaultPort {
if port == defaultHTTPPort || port == defaultHTTPsPort {
addPorts = append(addPorts, port)
continue
}
@@ -1098,7 +1098,7 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultPort int, website
err = buserr.WithMap("ErrPortExist", errMap, nil)
return
}
if port != 443 && common.ScanPort(port) {
if port != defaultHTTPsPort && common.ScanPort(port) {
err = buserr.WithDetail("ErrPortInUsed", port, nil)
return
}
+1
View File
@@ -5,6 +5,7 @@ const (
StatusCanceled = "Canceled"
StatusDone = "Done"
StatusWaiting = "Waiting"
StatusPending = "Pending"
StatusSuccess = "Success"
StatusFailed = "Failed"
StatusUploading = "Uploading"
+20 -8
View File
@@ -1,13 +1,15 @@
package middleware
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/gin-gonic/gin"
)
@@ -17,13 +19,8 @@ func Certificate() gin.HandlerFunc {
c.Next()
return
}
if !c.Request.TLS.HandshakeComplete || len(c.Request.TLS.PeerCertificates) == 0 {
helper.InternalServer(c, errors.New("no such tls peer certificates"))
return
}
cert := c.Request.TLS.PeerCertificates[0]
if cert.Subject.CommonName != "panel_client" {
helper.InternalServer(c, fmt.Errorf("err certificate"))
if !xpack.ValidateCertificate(c) {
CloseDirectly(c)
return
}
conn := c.Request.Header.Get("Connection")
@@ -40,3 +37,18 @@ func Certificate() gin.HandlerFunc {
c.Next()
}
}
func CloseDirectly(c *gin.Context) {
hijacker, ok := c.Writer.(http.Hijacker)
if !ok {
c.AbortWithStatus(http.StatusForbidden)
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
c.AbortWithStatus(http.StatusForbidden)
return
}
_ = conn.(*net.TCPConn).SetLinger(0)
conn.Close()
}
+2
View File
@@ -13,6 +13,8 @@ func (s *CronjobRouter) InitRouter(Router *gin.RouterGroup) {
{
cmdRouter.POST("", baseApi.CreateCronjob)
cmdRouter.POST("/next", baseApi.LoadNextHandle)
cmdRouter.POST("/import", baseApi.ImportCronjob)
cmdRouter.POST("/export", baseApi.ExportCronjob)
cmdRouter.POST("/load/info", baseApi.LoadCronjobInfo)
cmdRouter.GET("/script/options", baseApi.LoadScriptOptions)
cmdRouter.POST("/del", baseApi.DeleteCronjob)
+10 -1
View File
@@ -2,6 +2,7 @@ package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
@@ -78,9 +79,17 @@ func Start() {
fmt.Printf("failed to load X.509 key pair: %s\n", err)
return
}
server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{tlsCert},
ClientAuth: tls.RequireAnyClientCert,
ClientAuth: tls.RequireAndVerifyClientCert,
}
caItem, _ := settingRepo.GetValueByKey("RootCrt")
if len(caItem) != 0 {
caCertPool := x509.NewCertPool()
rootCrt, _ := encrypt.StringDecrypt(caItem)
caCertPool.AppendCertsFromPEM([]byte(rootCrt))
server.TLSConfig.ClientCAs = caCertPool
}
business.Init()
global.LOG.Infof("listen at https://0.0.0.0:%s", global.CONF.Base.Port)
+3 -3
View File
@@ -132,7 +132,7 @@ func isValidHeader(key, value string) bool {
func sendWithSSL(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil {
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
@@ -163,7 +163,7 @@ func sendWithSSL(config SMTPConfig, addr string, toList []string, msg string, tr
func sendWithStartTLS(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil {
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
@@ -192,7 +192,7 @@ func sendWithStartTLS(config SMTPConfig, addr string, toList []string, msg strin
func sendPlaintext(config SMTPConfig, addr string, toList []string, msg string, transport *http.Transport) error {
var err error
var conn net.Conn
if transport != nil {
if transport != nil && transport.DialContext != nil {
conn, err = transport.DialContext(context.Background(), "tcp", addr)
} else {
conn, err = net.Dial("tcp", addr)
+3 -13
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -38,20 +39,9 @@ func NewIptables() (*Iptables, error) {
return iptables, nil
}
func (iptables *Iptables) run(rule string) error {
stdout, err := cmd.RunDefaultWithStdoutBashCf("%s iptables -t nat %s", iptables.CmdStr, rule)
if err != nil {
return fmt.Errorf("%s, %s", err, stdout)
}
if stdout != "" {
return fmt.Errorf("iptables error: %s", stdout)
}
return nil
}
func (iptables *Iptables) outf(tab, rule string, a ...any) (stdout string, err error) {
stdout, err = cmd.RunDefaultWithStdoutBashCf("%s iptables -t %s %s", iptables.CmdStr, tab, fmt.Sprintf(rule, a...))
cmdMgr := cmd.NewCommandMgr(cmd.WithIgnoreExist1(), cmd.WithTimeout(20*time.Second))
stdout, err = cmdMgr.RunWithStdoutBashCf("%s iptables -t %s %s", iptables.CmdStr, tab, fmt.Sprintf(rule, a...))
if err != nil && stdout != "" {
global.LOG.Errorf("iptables failed, err: %s", stdout)
}
+4 -1
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
@@ -99,7 +100,9 @@ func (f *Ufw) ListPort() ([]FireInfo, error) {
}
func (f *Ufw) ListForward() ([]FireInfo, error) {
_ = f.EnableForward()
if err := f.EnableForward(); err != nil {
global.LOG.Errorf("init port forward failed, err: %v", err)
}
iptables, err := NewIptables()
if err != nil {
return nil, err
+7 -2
View File
@@ -253,7 +253,12 @@ func (r *Local) Backup(info BackupInfo) error {
func (r *Local) Recover(info RecoverInfo) error {
fi, _ := os.Open(info.SourceFile)
defer fi.Close()
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, r.Type, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
mysqlCli := r.Type
if mysqlCli == "mysql-cluster" {
mysqlCli = "mysql"
}
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, mysqlCli, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
if strings.HasSuffix(info.SourceFile, ".gz") {
gzipFile, err := os.Open(info.SourceFile)
if err != nil {
@@ -271,7 +276,7 @@ func (r *Local) Recover(info RecoverInfo) error {
}
stdout, err := cmd.CombinedOutput()
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
return errors.New(stdStr)
}
+8 -2
View File
@@ -136,7 +136,11 @@ func (r *Local) Backup(info BackupInfo) error {
}
defer outfile.Close()
global.LOG.Infof("start to pg_dump | gzip > %s.gzip", info.TargetDir+"/"+info.FileName)
cmd := exec.Command("docker", "exec", "-e", fmt.Sprintf("PGPASSWORD=%s", r.Password), r.ContainerName, "pg_dump", "-F", "c", "-U", r.Username, "-d", info.Name)
cmd := exec.Command(
"docker", "exec", "-i", r.ContainerName,
"sh", "-c",
fmt.Sprintf("PGPASSWORD=%s pg_dump -F c -U %s -d %s", r.Password, r.Username, info.Name),
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -155,7 +159,9 @@ func (r *Local) Backup(info BackupInfo) error {
func (r *Local) Recover(info RecoverInfo) error {
fi, _ := os.Open(info.SourceFile)
defer fi.Close()
cmd := exec.Command("docker", "exec", "-i", r.ContainerName, "pg_restore", "-F", "c", "-c", "-U", r.Username, "-d", info.Name)
cmd := exec.Command("docker", "exec", r.ContainerName, "sh", "-c",
fmt.Sprintf("PGPASSWORD=%s pg_dump -F c -U %s -d %s", r.Password, r.Username, info.Name),
)
if strings.HasSuffix(info.SourceFile, ".gz") {
gzipFile, err := os.Open(info.SourceFile)
if err != nil {
+5
View File
@@ -15,6 +15,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/gin-gonic/gin"
)
func RemoveTamper(website string) {}
@@ -72,3 +73,7 @@ func LoadRequestTransport() *http.Transport {
IdleConnTimeout: 15 * time.Second,
}
}
func ValidateCertificate(c *gin.Context) bool {
return true
}
+3
View File
@@ -56,6 +56,9 @@ func NewClient(c ConnInfo) (*SSHClient, error) {
return nil, err
}
sshClient := &SSHClient{Client: client}
if c.User == "root" {
return sshClient, nil
}
if _, err := sshClient.Run("sudo -n ls"); err == nil {
sshClient.SudoItem = "sudo"
}
+46
View File
@@ -101,6 +101,52 @@ export namespace Cronjob {
alertTitle: string;
alertMethod: string;
}
export interface CronjobTrans {
name: string;
type: string;
specCustom: boolean;
spec: string;
group: string;
executor: string;
scriptMode: string;
script: string;
command: string;
containerName: string;
user: string;
url: string;
scriptName: string;
apps: Array<TransHelper>;
websites: Array<string>;
dbType: string;
dbNames: Array<TransHelper>;
exclusionRules: string;
isDir: boolean;
sourceDir: string;
retainCopies: number;
retryTimes: number;
timeout: number;
ignoreErr: boolean;
snapshotRule: string;
secret: string;
sourceAccounts: Array<string>;
downloadAccount: string;
alertCount: number;
}
export interface TransHelper {
name: string;
detailName: string;
}
export interface snapshotTransHelper {
withImage: boolean;
ignoreApps: Array<TransHelper>;
}
export interface snapshotRule {
withImage: boolean;
ignoreAppIDs: Array<Number>;
+7
View File
@@ -11,6 +11,13 @@ export const loadNextHandle = (spec: string) => {
return http.post<Array<String>>(`/cronjobs/next`, { spec: spec });
};
export const importCronjob = (trans: Array<Cronjob.CronjobTrans>) => {
return http.post('cronjobs/import', { cronjobs: trans }, TimeoutEnum.T_60S);
};
export const exportCronjob = (params: { ids: Array<number> }) => {
return http.download<BlobPart>('cronjobs/export', params, { responseType: 'blob', timeout: TimeoutEnum.T_40S });
};
export const loadCronjobInfo = (id: number) => {
return http.post<Cronjob.CronjobOperate>(`/cronjobs/load/info`, { id: id });
};
+1 -1
View File
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import { computed, useSlots } from 'vue';
import { computed, useSlots, ref } from 'vue';
defineOptions({ name: 'DrawerPro' });
import i18n from '@/lang';
import { GlobalStore } from '@/store';
+17 -2
View File
@@ -1,7 +1,14 @@
<template>
<div v-loading="firstLoading">
<div v-if="defaultButton">
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)" v-if="showTail">
<el-checkbox
border
:disabled="isTailDisabled"
v-model="tailLog"
class="float-left"
@change="changeTail(false)"
v-if="showTail"
>
{{ $t('commons.button.watch') }}
</el-checkbox>
<el-button
@@ -106,7 +113,7 @@ const stopSignals = [
'image push successful!',
'[TASK-END]',
];
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading', 'stop-reading']);
const tailLog = ref(true);
const loading = ref(props.loading);
const readReq = reactive({
@@ -129,6 +136,8 @@ const minPage = ref(0);
let timer: NodeJS.Timer | null = null;
const logPath = ref('');
const isTailDisabled = ref();
const firstLoading = ref(false);
const logs = ref<string[]>([]);
const logContainer = ref<HTMLElement | null>(null);
@@ -213,6 +222,10 @@ const getContent = async (pre: boolean) => {
firstLoading.value = false;
}
if (res.data.taskStatus && res.data.taskStatus !== 'Executing') {
isTailDisabled.value = true;
}
logPath.value = res.data.path;
firstLoading.value = false;
@@ -298,6 +311,7 @@ const onCloseLog = async () => {
timer = null;
}
timer = null;
emit('stop-reading');
isLoading.value = false;
emit('update:isReading', false);
bus.emit('refreshTask', true);
@@ -329,6 +343,7 @@ const containerStyle = computed(() => ({
onMounted(async () => {
logs.value = [];
isTailDisabled.value = false;
firstLoading.value = true;
await init();
nextTick(() => {
+2
View File
@@ -188,6 +188,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
case 'mysql':
case 'mariadb':
case 'postgresql':
case 'mysql-cluster':
case 'postgresql-cluster':
title.value = name.value + ' [ ' + detailName.value + ' ]';
if (detailName.value) {
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${name.value}/${detailName.value}/`;
+1
View File
@@ -12,6 +12,7 @@ export const useLogo = async () => {
globalStore.themeConfig.loginImage = res.data?.loginImage;
globalStore.themeConfig.loginBgType = res.data?.loginBgType;
globalStore.themeConfig.loginBackground = res.data?.loginBackground;
globalStore.themeConfig.loginBtnLinkColor = res.data?.loginBtnLinkColor;
globalStore.themeConfig.favicon = res.data.favicon;
}
+14 -4
View File
@@ -50,6 +50,7 @@ const message = {
verify: 'Verify',
saveAndEnable: 'Save and enable',
import: 'Import',
export: 'Export',
power: 'Authorization',
search: 'Search',
refresh: 'Refresh',
@@ -298,6 +299,7 @@ const message = {
normal: 'Normal',
building: 'Building',
upgrading: 'Upgrading',
pending: 'Pending Edit',
rebuilding: 'Rebuilding',
deny: 'Denied',
accept: 'Accepted',
@@ -988,6 +990,10 @@ const message = {
cronjob: {
create: 'Create cron job',
edit: 'Edit cron job',
errImport: 'File content exception:',
errImportFormat: 'The scheduled task data or format is abnormal. Please check and try again!',
importHelper:
'Duplicate scheduled tasks will be automatically skipped during import. Tasks will be set to [Disabled] status by default, and set to [Pending Edit] status when data association is abnormal.',
changeStatus: 'Change status',
disableMsg: 'This will stop the scheduled task from automatically executing. Do you want to continue?',
enableMsg: 'This will allow the scheduled task to automatically execute. Do you want to continue?',
@@ -1607,7 +1613,7 @@ const message = {
apiKeyHelper: 'API key is used for third-party applications to access the API.',
ipWhiteList: 'IP allowlist',
ipWhiteListEgs: 'One per line. For example,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IPs within the allowlist can access the API.',
ipWhiteListHelper: 'IPs within the allowlist can access the API,0.0.0.0/0 (all IPv4), ::/0 (all IPv6)',
apiKeyValidityTime: 'Validity period of interface key',
apiKeyValidityTimeEgs: 'Validity period of interface key (in minutes)',
apiKeyValidityTimeHelper:
@@ -1716,10 +1722,11 @@ const message = {
typeStandard_IA: 'Standard_IA',
typeArchive: 'Archive',
typeDeep_Archive: 'Deep_Archive',
scLighthouse: 'Default, Lightweight object storage only supports this storage type',
scStandard:
'Standard Storage is suitable for business scenarios with a large number of hot files that require real-time access, frequent data interaction, and so on.',
'Standard storage, suitable for business scenarios with large amounts of frequently accessed hot files and frequent data interactions.',
scStandard_IA:
'Low-frequency storage is suitable for business scenarios with relatively low access frequency, and stores data for at least 30 days.',
'Infrequent access storage, suitable for business scenarios with lower access frequency (e.g., average monthly access frequency of 1-2 times), minimum storage duration of 30 days.',
scArchive: 'Archival storage is suitable for business scenarios with extremely low access frequency.',
scDeep_Archive: 'Durable cold storage is suitable for business scenarios with extremely low access frequency.',
archiveHelper:
@@ -2863,7 +2870,7 @@ const message = {
apiInterfaceHelper:
'Supports panel API interface access (this feature needs to be enabled for the panel app)',
apiInterfaceHelper1:
"Panel app access requires adding the visitor to the whitelist; for non-fixed IPs, it's recommended to add 0.0.0.0/0",
"Panel app access requires adding the visitor to the whitelist; for non-fixed IPs, it's recommended to add 0.0.0.0/0 (all IPv4), ::/0 (all IPv6)",
qrCodeExpired: 'Refresh time',
apiLeakageHelper: 'Do not disclose the QR code. Ensure it is used only in trusted environments.',
},
@@ -3282,6 +3289,9 @@ const message = {
loginBgColorHelper: 'Displayed as background color on the login page',
image: 'Image',
bgColor: 'Background Color',
loginGroup: 'Login Page',
loginBtnLinkColor: 'Button/Link Color',
loginBtnLinkColorHelper: 'Will be displayed as the button/link color on the login page',
},
helper: {
wafTitle1: 'Interception Map',
+12 -2
View File
@@ -48,6 +48,7 @@ const message = {
verify: '確認する',
saveAndEnable: '保存して有効にします',
import: '輸入',
export: 'エクスポート',
power: '認可',
search: '検索',
refresh: 'リロード',
@@ -287,6 +288,7 @@ const message = {
normal: '普通',
building: '建物',
upgrading: 'アップグレード',
pending: '編集待ち',
rebuilding: '再構築',
deny: '拒否されました',
accept: '受け入れられました',
@@ -958,6 +960,10 @@ const message = {
cronjob: {
create: 'Cronジョブを作成します',
edit: 'Cronジョブを編集します',
errImport: 'ファイル内容異常:',
errImportFormat: 'インポートしたスケジュールタスクのデータまたは形式が異常です。確認して再試行してください!',
importHelper:
'インポート時に同名のスケジュールタスクは自動的にスキップされます。タスクはデフォルトで【無効】状態に設定され、データ関連付け異常時には【編集待ち】状態に設定されます。',
changeStatus: 'ステータスを変更します',
disableMsg: 'これにより、スケジュールされたタスクが自動的に実行されなくなります。続けたいですか?',
enableMsg: 'これにより、スケジュールされたタスクが自動的に実行されます。続けたいですか?',
@@ -1551,7 +1557,7 @@ const message = {
apiKeyHelper: 'APIキーは、サードパーティアプリケーションに使用されてAPIにアクセスします。',
ipWhiteList: 'IP AllowList',
ipWhiteListEgs: '1行に1つ。たとえば、 n172.161.10.111 n172.161.10.0/24',
ipWhiteListHelper: 'AllowList内のIPSはAPIにアクセスできます。',
ipWhiteListHelper: 'AllowList内のIPSはAPIにアクセスできます、0.0.0.0/0(すべての IPv4)、::/0(すべての IPv6)',
apiKeyReset: 'インターフェイスキーリセット',
apiKeyResetHelper: '関連するキーサービスは無効になります。サービスに新しいキーを追加してください',
confDockerProxy: 'Dockerプロキシを構成します',
@@ -1637,6 +1643,7 @@ const message = {
typeStandard_IA: '低頻度ストレージ',
typeArchive: 'アーカイブストレージ',
typeDeep_Archive: '深層アーカイブストレージ',
scLighthouse: 'デフォルト:軽量オブジェクトストレージはこのストレージタイプのみサポートしています',
scStandard:
'標準ストレージは、リアルタイムアクセスが多いホットファイルや頻繁なデータ交換などの業務シナリオに適しています。',
scStandard_IA:
@@ -2741,7 +2748,7 @@ const message = {
apiInterfaceHelper:
'パネルAPIインターフェースアクセスをサポート(この機能はパネルアプリで有効にする必要があります)',
apiInterfaceHelper1:
'パネルアプリのアクセスには訪問者をホワイトリストに追加する必要があります。固定IPでない場合、0.0.0.0/0 を追加することをお勧めします',
'パネルアプリのアクセスには訪問者をホワイトリストに追加する必要があります。固定IPでない場合、0.0.0.0/0(すべての IPv4)、::/0(すべての IPv6)を追加することをお勧めします',
qrCodeExpired: 'リフレッシュ時間',
apiLeakageHelper: 'QRコードを漏洩しないでください。信頼できる環境でのみ使用してください。',
},
@@ -3151,6 +3158,9 @@ const message = {
loginBgColorHelper: 'ログインページの背景色として表示されます',
image: '画像',
bgColor: '背景色',
loginGroup: 'ログインページ',
loginBtnLinkColor: 'ボタン/リンクの色',
loginBtnLinkColorHelper: 'ログインページに表示されるボタン/リンクの色になります',
},
helper: {
wafTitle1: 'インターセプションマップ',
+12 -2
View File
@@ -48,6 +48,7 @@ const message = {
verify: '검증',
saveAndEnable: '저장 및 활성화',
import: '가져오기',
export: '내보내기',
power: '권한 부여',
search: '검색',
refresh: '새로고침',
@@ -289,6 +290,7 @@ const message = {
normal: '정상',
building: '빌드 중',
upgrading: '업그레이드 중',
pending: '편집 대기',
rebuilding: '재빌드 중',
deny: '거부됨',
accept: '수락됨',
@@ -948,6 +950,10 @@ const message = {
cronjob: {
create: '크론 작업 생성',
edit: '크론 작업 수정',
errImport: '파일 내용 이상:',
errImportFormat: '가져온 예약 작업 데이터 또는 형식이 이상합니다. 확인 후 다시 시도하십시오!',
importHelper:
'가져오기 시 동일한 이름의 예약 작업은 자동으로 건너뜁니다. 작업은 기본적으로 【비활성화】 상태로 설정되며, 데이터 연동 이상 시 【편집 대기】 상태로 설정됩니다.',
changeStatus: '상태 변경',
disableMsg: '이 작업은 예약된 작업이 자동으로 실행되지 않도록 멈춥니다. 계속하시겠습니까?',
enableMsg: '이 작업은 예약된 작업이 자동으로 실행되도록 허용합니다. 계속하시겠습니까?',
@@ -1532,7 +1538,7 @@ const message = {
apiKeyHelper: 'API 키는 서드파티 애플리케이션이 API 에 접근하는 데 사용됩니다.',
ipWhiteList: 'IP 허용 목록',
ipWhiteListEgs: '한 줄에 하나씩 입력하십시오. 예:\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: '허용 목록에 있는 IP만 API 에 접근할 수 있습니다.',
ipWhiteListHelper: '허용 목록에 있는 IP만 API 에 접근할 수 있습니다. 0.0.0.0/0(모든 IPv4), ::/0(모든 IPv6)',
apiKeyValidityTime: '인터페이스 키 유효 기간',
apiKeyValidityTimeEgs: '인터페이스 키 유효 기간 (분 단위)',
apiKeyValidityTimeHelper:
@@ -1620,6 +1626,7 @@ const message = {
typeStandard_IA: '저빈도 표준',
typeArchive: '아카이브',
typeDeep_Archive: '심층 아카이브',
scLighthouse: '기본값: 경량 오브젝트 스토리지는 이 스토리지 유형만 지원합니다',
scStandard:
'표준 스토리지는 실시간 접근, 빈번한 데이터 상호작용이 필요한 핫 파일이 많은 비즈니스 시나리오에 적합합니다.',
scStandard_IA:
@@ -2694,7 +2701,7 @@ const message = {
apiStatusHelper: '패널 APP 는 API 인터페이스 기능을 활성화해야 합니다',
apiInterfaceHelper: '패널 API 인터페이스 액세스를 지원합니다(패널 앱에서 이 기능을 활성화해야 합니다)',
apiInterfaceHelper1:
'패널 앱의 액세스에는 방문자를 화이트리스트에 추가해야 하며, 고정 IP가 아닌 경우 0.0.0.0/0 을 추가하는 것이 좋습니다',
'패널 앱의 액세스에는 방문자를 화이트리스트에 추가해야 하며, 고정 IP가 아닌 경우 0.0.0.0/0(모든 IPv4), ::/0(모든 IPv6) 을 추가하는 것이 좋습니다',
qrCodeExpired: '새로 고침 시간',
apiLeakageHelper: 'QR 코드를 누출하지 마십시오. 신뢰할 수 있는 환경에서만 사용하십시오.',
},
@@ -3099,6 +3106,9 @@ const message = {
loginBgColorHelper: '로그인 페이지의 배경 색상으로 표시됩니다',
image: '이미지',
bgColor: '배경 색상',
loginGroup: '로그인 페이지',
loginBtnLinkColor: '버튼/링크 색상',
loginBtnLinkColorHelper: '로그인 페이지의 버튼/링크 색상으로 표시됩니다',
},
helper: {
wafTitle1: '차단 지도',
+12 -2
View File
@@ -48,6 +48,7 @@ const message = {
verify: 'Sahkan',
saveAndEnable: 'Simpan dan aktifkan',
import: 'Import',
export: 'Eksport',
power: 'Pemberian Kuasa',
search: 'Cari',
refresh: 'Segarkan',
@@ -295,6 +296,7 @@ const message = {
normal: 'Normal',
building: 'Sedang Membina',
upgrading: 'Sedang Meningkatkan',
pending: 'Menunggu Edit',
rebuilding: 'Sedang Membina Semula',
deny: 'Ditolak',
accept: 'Diterima',
@@ -979,6 +981,10 @@ const message = {
cronjob: {
create: 'Cipta tugas cron',
edit: 'Edit tugas cron',
errImport: 'Kandungan fail tidak normal:',
errImportFormat: 'Data atau format tugas terjadual yang diimport tidak normal. Sila semak dan cuba lagi!',
importHelper:
'Tugas terjadual dengan nama sama akan dilangkau secara automatik semasa import. Tugas akan ditetapkan ke status 【Lumpuh】 secara lalai, dan ditetapkan ke status 【Menunggu Edit】 apabila perkaitan data tidak normal.',
changeStatus: 'Tukar status',
disableMsg:
'Ini akan menghentikan tugas berjadual daripada dilaksanakan secara automatik. Adakah anda mahu meneruskan?',
@@ -1595,7 +1601,7 @@ const message = {
apiKeyHelper: 'Kunci API digunakan untuk aplikasi pihak ketiga mengakses API.',
ipWhiteList: 'Senarai putih IP',
ipWhiteListEgs: 'Satu per baris. Contoh,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IP dalam senarai putih boleh mengakses API.',
ipWhiteListHelper: 'IP dalam senarai putih boleh mengakses API, 0.0.0.0/0 (semua IPv4), ::/0 (semua IPv6)',
apiKeyValidityTime: 'Tempoh sah kunci antara muka',
apiKeyValidityTimeEgs: 'Tempoh sah kunci antara muka (dalam minit)',
apiKeyValidityTimeHelper:
@@ -1689,6 +1695,7 @@ const message = {
typeStandard_IA: 'Standard_IA',
typeArchive: 'Archive',
typeDeep_Archive: 'Deep_Archive',
scLighthouse: 'Lalai, Penyimpanan objek ringan hanya menyokong jenis penyimpanan ini',
scStandard:
'Storan Standard sesuai untuk senario perniagaan dengan sejumlah besar fail panas yang memerlukan akses masa nyata, interaksi data yang kerap, dan sebagainya.',
scStandard_IA:
@@ -2802,7 +2809,7 @@ const message = {
apiInterfaceHelper:
'Menyokong akses antara muka API panel (fungsi ini perlu diaktifkan untuk aplikasi panel)',
apiInterfaceHelper1:
'Akses aplikasi panel memerlukan penambahan pelawat ke dalam senarai putih, untuk IP yang tidak tetap, disyorkan untuk menambah 0.0.0.0/0',
'Akses aplikasi panel memerlukan penambahan pelawat ke dalam senarai putih, untuk IP yang tidak tetap, disyorkan untuk menambah 0.0.0.0/0(semua IPv4), ::/0 (semua IPv6)',
qrCodeExpired: 'Masa penyegaran',
apiLeakageHelper: 'Jangan dedahkan kod QR. Pastikan ia hanya digunakan di persekitaran yang dipercayai.',
},
@@ -3222,6 +3229,9 @@ const message = {
loginBgColorHelper: 'Akan dipaparkan sebagai warna latar belakang halaman log masuk',
image: 'Imej',
bgColor: 'Warna Latar',
loginGroup: 'Halaman Log Masuk',
loginBtnLinkColor: 'Warna Butang/Pautan',
loginBtnLinkColorHelper: 'Akan dipaparkan sebagai warna butang/pautan di halaman log masuk',
},
helper: {
wafTitle1: 'Peta Pencegahan',
+14 -2
View File
@@ -48,6 +48,7 @@ const message = {
verify: 'Verificar',
saveAndEnable: 'Salvar e ativar',
import: 'Importar',
export: 'Exportar',
power: 'Autorização',
search: 'Pesquisar',
refresh: 'Atualizar',
@@ -293,6 +294,7 @@ const message = {
normal: 'Normal',
building: 'Construindo',
upgrading: 'Atualizando',
pending: 'Aguardando Edição',
rebuilding: 'Reconstruindo',
deny: 'Negado',
accept: 'Aceito',
@@ -975,6 +977,11 @@ const message = {
cronjob: {
create: 'Criar tarefa cron',
edit: 'Editar tarefa cron',
errImport: 'Conteúdo do arquivo anormal:',
errImportFormat:
'Os dados ou formato da tarefa agendada estão anormais. Por favor, verifique e tente novamente!',
importHelper:
'Tarefas agendadas duplicadas serão automaticamente ignoradas durante a importação. As tarefas serão definidas como status 【Desativado】 por padrão, e como status 【Aguardando Edição】 quando a associação de dados for anormal.',
changeStatus: 'Alterar status',
disableMsg: 'Isso irá parar a execução automática da tarefa agendada. Você deseja continuar?',
enableMsg: 'Isso permitirá que a tarefa agendada seja executada automaticamente. Você deseja continuar?',
@@ -1583,7 +1590,8 @@ const message = {
apiKeyHelper: 'A chave da API é usada para aplicativos de terceiros acessarem a API.',
ipWhiteList: 'Lista de IPs permitidos',
ipWhiteListEgs: 'Um por linha. Exemplo: \n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IPs na lista de permitidos podem acessar a API.',
ipWhiteListHelper:
'IPs na lista de permitidos podem acessar a API, 0.0.0.0/0 (todos os IPv4), ::/0 (todos os IPv6)',
apiKeyReset: 'Redefinir chave da interface',
apiKeyResetHelper:
'O serviço associado à chave se tornará inválido. Por favor, adicione uma nova chave ao serviço',
@@ -1673,6 +1681,7 @@ const message = {
typeStandard_IA: 'Padrão_IA',
typeArchive: 'Arquivo',
typeDeep_Archive: 'Arquivo Profundo',
scLighthouse: 'Padrão, O armazenamento de objetos leve só suporta este tipo de armazenamento',
scStandard:
'O armazenamento padrão é adequado para cenários de negócios com grande volume de arquivos quentes que exigem acesso em tempo real, interação frequente de dados, etc.',
scStandard_IA:
@@ -2804,7 +2813,7 @@ const message = {
apiInterfaceHelper:
'Suporta acesso à interface de API do painel (essa funcionalidade precisa ser ativada no aplicativo do painel)',
apiInterfaceHelper1:
'O acesso ao aplicativo do painel requer que o visitante seja adicionado à lista de permissões; para IPs não fixos, recomenda-se adicionar 0.0.0.0/0',
'O acesso ao aplicativo do painel requer que o visitante seja adicionado à lista de permissões; para IPs não fixos, recomenda-se adicionar 0.0.0.0/0(todos os IPv4), ::/0 (todos os IPv6)',
qrCodeExpired: 'Tempo de atualização',
apiLeakageHelper: 'Não revele o QR code. Garanta que ele seja usado apenas em ambientes confiáveis.',
},
@@ -3229,6 +3238,9 @@ const message = {
loginBgColorHelper: 'Será exibida como cor de fundo na página de login',
image: 'Imagem',
bgColor: 'Cor de Fundo',
loginGroup: 'Página de login',
loginBtnLinkColor: 'Cor do botão/link',
loginBtnLinkColorHelper: 'Será exibido como a cor do botão/link na página de login',
},
helper: {
wafTitle1: 'Mapa de Interceptação',
+14 -2
View File
@@ -48,6 +48,7 @@ const message = {
verify: 'Проверить',
saveAndEnable: 'Сохранить и включить',
import: 'Импорт',
export: 'Экспорт',
power: 'Авторизация',
search: 'Поиск',
refresh: 'Обновить',
@@ -290,6 +291,7 @@ const message = {
normal: 'Нормально',
building: 'Сборка',
upgrading: 'Обновление',
pending: 'Ожидает редактирования',
rebuilding: 'Пересборка',
deny: 'Отказано',
accept: 'Принято',
@@ -972,6 +974,11 @@ const message = {
cronjob: {
create: 'Создать задачу cron',
edit: 'Редактировать задачу cron',
errImport: 'Аномальное содержимое файла:',
errImportFormat:
'Данные или формат запланированной задачи ненормальны. Пожалуйста, проверьте и повторите попытку!',
importHelper:
'Повторяющиеся запланированные задачи будут автоматически пропущены при импорте. По умолчанию задачи устанавливаются в статус 【Отключено】, а при аномальной ассоциации данных - в статус 【Ожидает редактирования】.',
changeStatus: 'Изменить статус',
disableMsg: 'Это остановит автоматическое выполнение запланированной задачи. Хотите продолжить?',
enableMsg: 'Это позволит запланированной задаче автоматически выполняться. Хотите продолжить?',
@@ -1582,7 +1589,8 @@ const message = {
apiKeyHelper: 'API ключ используется сторонними приложениями для доступа к API.',
ipWhiteList: 'Белый список IP',
ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API.',
ipWhiteListHelper:
'IP-адреса из белого списка могут получить доступ к API, 0.0.0.0/0 (все IPv4), ::/0 (все IPv6)',
apiKeyValidityTime: 'Срок действия ключа интерфейса',
apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)',
apiKeyValidityTimeHelper:
@@ -1675,6 +1683,7 @@ const message = {
typeStandard_IA: 'Стандартный_IA',
typeArchive: 'Архивный',
typeDeep_Archive: 'Глубокий_Архив',
scLighthouse: 'По умолчанию, Легковесное объектное хранилище поддерживает только этот тип хранилища',
scStandard:
'Стандартное хранилище подходит для бизнес-сценариев с большим количеством горячих файлов, требующих мгновенного доступа, частого обмена данными и т.д.',
scStandard_IA:
@@ -2796,7 +2805,7 @@ const message = {
apiInterfaceHelper:
'Поддержка доступа к API-интерфейсу панели (эта функция должна быть включена для мобильного приложения панели)',
apiInterfaceHelper1:
'Для доступа к приложению панели необходимо добавить посетителя в белый список. Для нестабильных IP рекомендуется добавить 0.0.0.0/0',
'Для доступа к приложению панели необходимо добавить посетителя в белый список. Для нестабильных IP рекомендуется добавить 0.0.0.0/0 (все IPv4), ::/0 (все IPv6)',
qrCodeExpired: 'Время обновления',
apiLeakageHelper: 'Не раскрывайте QR-код. Убедитесь, что он используется только в доверенных средах.',
},
@@ -3214,6 +3223,9 @@ const message = {
loginBgColorHelper: 'Будет отображаться как цвет фона страницы входа',
image: 'Изображение',
bgColor: 'Цвет фона',
loginGroup: 'Страница входа',
loginBtnLinkColor: 'Цвет кнопки/ссылки',
loginBtnLinkColorHelper: 'Будет отображаться как цвет кнопки/ссылки на странице входа',
},
helper: {
wafTitle1: 'Карта Перехватов',
+12 -2
View File
@@ -50,6 +50,7 @@ const message = {
verify: 'Doğrula',
saveAndEnable: 'Kaydet ve etkinleştir',
import: 'İçe Aktar',
export: 'Dışa Aktar',
power: 'Yetkilendirme',
search: 'Ara',
refresh: 'Yenile',
@@ -302,6 +303,7 @@ const message = {
normal: 'Normal',
building: 'İnşa Ediliyor',
upgrading: 'Yükseltiliyor',
pending: 'Düzenleme Bekliyor',
rebuilding: 'Yeniden İnşa Ediliyor',
deny: 'Reddedildi',
accept: 'Kabul Edildi',
@@ -1000,6 +1002,10 @@ const message = {
cronjob: {
create: 'Cron görevi oluştur',
edit: 'Cron görevini düzenle',
errImport: 'Dosya içeriği anormal:',
errImportFormat: 'Zamanlanmış görev verileri veya biçimi anormal. Lütfen kontrol edip tekrar deneyin!',
importHelper:
'İçe aktarım sırasında aynı isimli zamanlanmış görevler otomatik olarak atlanacaktır. Görevler varsayılan olarak 【Devre Dışı】 durumuna ayarlanır ve veri ilişkilendirme anormalse 【Düzenleme Bekliyor】 durumuna ayarlanır.',
changeStatus: 'Durumu değiştir',
disableMsg: 'Bu, zamanlanmış görevin otomatik olarak yürütülmesini durduracaktır. Devam etmek istiyor musunuz?',
enableMsg:
@@ -1624,7 +1630,7 @@ const message = {
apiKeyHelper: 'API anahtarı, üçüncü taraf uygulamaların API’ye erişmesi için kullanılır.',
ipWhiteList: 'IP izin listesi',
ipWhiteListEgs: 'Her satıra bir tane. Örneğin,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'İzin listesindeki IP’ler API’ye erişebilir.',
ipWhiteListHelper: 'İzin listesindeki IP’ler API’ye erişebilir, 0.0.0.0/0 (tüm IPv4), ::/0 (tüm IPv6)',
apiKeyValidityTime: 'Arayüz anahtarının geçerlilik süresi',
apiKeyValidityTimeEgs: 'Arayüz anahtarının geçerlilik süresi (dakika cinsinden)',
apiKeyValidityTimeHelper:
@@ -1735,6 +1741,7 @@ const message = {
typeStandard_IA: 'Standart_IA',
typeArchive: 'Arşiv',
typeDeep_Archive: 'Derin_Arşiv',
scLighthouse: 'Varsayılan, Hafif nesne depolama yalnızca bu depolama türünü destekler',
scStandard:
'Standart Depolama, gerçek zamanlı erişim gerektiren çok sayıda sıcak dosya içeren iş senaryoları için uygundur, sık veri etkileşimi vb.',
scStandard_IA:
@@ -2900,7 +2907,7 @@ const message = {
apiInterfaceHelper:
'Panel API arayüz erişimini destekler (bu özellik, panel uygulaması için etkinleştirilmelidir)',
apiInterfaceHelper1:
'Panel uygulama erişimi, ziyaretçinin beyaz listeye eklenmesini gerektirir; sabit olmayan IP’ler için 0.0.0.0/0 eklenmesi önerilir',
'Panel uygulama erişimi, ziyaretçinin beyaz listeye eklenmesini gerektirir; sabit olmayan IP’ler için 0.0.0.0/0(tüm IPv4), ::/0 (tüm IPv6) eklenmesi önerilir',
qrCodeExpired: 'Yenileme zamanı',
apiLeakageHelper: 'QR kodunu paylaşmayın. Yalnızca güvenilir ortamlarda kullanıldığından emin olun.',
},
@@ -3316,6 +3323,9 @@ const message = {
loginBgColorHelper: 'Giriş sayfasında arka plan rengi olarak gösterilir',
image: 'Görüntü',
bgColor: 'Arka Plan Rengi',
loginGroup: 'Giriş Sayfası',
loginBtnLinkColor: 'Buton/Bağlantı Rengi',
loginBtnLinkColorHelper: 'Giriş sayfasındaki buton/bağlantı rengi olarak gösterilecektir',
},
helper: {
wafTitle1: 'Engelleme Haritası',
+14 -2
View File
@@ -51,6 +51,7 @@ const message = {
saveAndEnable: '保存並啟用',
import: '導入',
power: '授權',
export: '導出',
search: '搜索',
refresh: '刷新',
get: '獲取',
@@ -289,6 +290,7 @@ const message = {
normal: '正常',
building: '製作鏡像中',
upgrading: '升級中',
pending: '待編輯',
rebuilding: '重建中',
deny: '已屏蔽',
accept: '已放行',
@@ -942,6 +944,10 @@ const message = {
cronjob: {
create: '創建計劃任務',
edit: '編輯計劃任務',
errImport: '文件內容異常:',
errImportFormat: '導入的計劃任務數據或格式異常,請檢查後重試!',
importHelper:
'導入時將自動跳過重名計劃任務。任務默認設置為【停用】狀態,數據關聯異常時,設置為【待編輯】狀態。',
changeStatus: '狀態修改',
disableMsg: '停止計劃任務會導致該任務不再自動執行。是否繼續?',
enableMsg: '啟用計劃任務會讓該任務定期自動執行。是否繼續?',
@@ -1528,7 +1534,8 @@ const message = {
apiKeyHelper: '接口密钥用於外部應用訪問 API 接口',
ipWhiteList: 'IP白名單',
ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24',
ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口',
ipWhiteListHelper:
'必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
apiKeyValidityTime: '介面金鑰有效期',
apiKeyValidityTimeEgs: '介面金鑰有效期(組織分)',
apiKeyValidityTimeHelper: '介面時間戳記到請求時的當前時間戳之間有效(組織分),設定為0時,不做時間戳記校驗',
@@ -1620,6 +1627,7 @@ const message = {
typeStandard_IA: '低頻存儲',
typeArchive: '歸檔存儲',
typeDeep_Archive: '深度歸檔存儲',
scLighthouse: '預設,輕量物件儲存僅支援該儲存類型',
scStandard: '標準存儲,適用於實時訪問的大量熱點文件、頻繁的數據交互等業務場景。',
scStandard_IA: '低頻存儲,適用於較低訪問頻率(例如平均每月訪問頻率1到2次)的業務場景,最少存儲30天。',
scArchive: '歸檔存儲,適用於極低訪問頻率(例如半年訪問1次)的業務場景。',
@@ -2668,7 +2676,8 @@ const message = {
qrCode: '二維碼',
apiStatusHelper: '面板 APP 需要開啟 API 接口功能',
apiInterfaceHelper: '支持面板 API 接口訪問功能(面板 APP 需要開啟該功能)',
apiInterfaceHelper1: '面板 APP 訪問需將訪問者添加至白名單,非固定 IP 建議添加 0.0.0.0/0',
apiInterfaceHelper1:
'面板 APP 訪問需將訪問者添加至白名單,非固定 IP 建議添加 0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
qrCodeExpired: '刷新時間',
apiLeakageHelper: '請勿洩露二維碼,確保僅在受信任的環境中使用',
},
@@ -3062,6 +3071,9 @@ const message = {
loginBgColorHelper: '將會顯示在登入頁面背景顏色',
image: '圖片',
bgColor: '背景色',
loginGroup: '登入頁面',
loginBtnLinkColor: '按鈕顏色',
loginBtnLinkColorHelper: '將顯示為登入頁面上的按鈕顏色',
},
helper: {
wafTitle1: '攔截地圖',
+14 -2
View File
@@ -50,6 +50,7 @@ const message = {
verify: '验证',
saveAndEnable: '保存并启用',
import: '导入',
export: '导出',
power: '授权',
search: '搜索',
refresh: '刷新',
@@ -287,6 +288,7 @@ const message = {
normal: '正常',
building: '制作镜像中',
upgrading: '升级中',
pending: '待编辑',
rebuilding: '重建中',
deny: '已屏蔽',
accept: '已放行',
@@ -940,6 +942,10 @@ const message = {
cronjob: {
create: '创建计划任务',
edit: '编辑计划任务',
errImport: '文件内容异常:',
errImportFormat: '导入的计划任务数据或格式异常,请检查后重试!',
importHelper:
'导入时将自动跳过重名计划任务。任务默认设置为【停用】状态,数据关联异常时,设置为【待编辑】状态。',
changeStatus: '状态修改',
disableMsg: '停止计划任务会导致该任务不再自动执行。是否继续?',
enableMsg: '启用计划任务会让该任务定期自动执行。是否继续?',
@@ -1523,7 +1529,8 @@ const message = {
apiKeyHelper: '接口密钥用于外部应用访问 API 接口',
ipWhiteList: 'IP 白名单',
ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24',
ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口',
ipWhiteListHelper:
'必需在 IP 白名单列表中的 IP 才能访问面板 API 接口,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
apiKeyValidityTime: '接口密钥有效期',
apiKeyValidityTimeEgs: '接口密钥有效期(单位分)',
apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效(单位分),设置为 0 时,不做时间戳校验',
@@ -1615,6 +1622,7 @@ const message = {
typeStandard_IA: '低频存储',
typeArchive: '归档存储',
typeDeep_Archive: '深度归档存储',
scLighthouse: '默认,轻量对象存储仅支持该存储类型',
scStandard: '标准存储,适用于实时访问的大量热点文件、频繁的数据交互等业务场景。',
scStandard_IA: '低频存储,适用于较低访问频率(例如平均每月访问频率1到2次)的业务场景,最少存储30天。',
scArchive: '归档存储,适用于极低访问频率(例如半年访问1次)的业务场景。',
@@ -3032,6 +3040,9 @@ const message = {
loginBgColorHelper: '将会显示在登录页面背景颜色',
image: '图片',
bgColor: '背景色',
loginGroup: '登录页',
loginBtnLinkColor: '按钮/链接颜色',
loginBtnLinkColorHelper: '将会显示在登录页面按钮/链接颜色',
},
helper: {
wafTitle1: '拦截地图',
@@ -3399,7 +3410,8 @@ const message = {
qrCode: '二维码',
apiStatusHelper: '面板 APP 需要开启 API 接口功能',
apiInterfaceHelper: '支持面板 API 接口访问功能(面板 APP 需要开启该功能)',
apiInterfaceHelper1: '面板 APP 访问需将访问者添加至白名单,非固定 IP 建议添加 0.0.0.0/0',
apiInterfaceHelper1:
'面板 APP 访问需将访问者添加至白名单,非固定 IP 建议添加 0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)',
qrCodeExpired: '刷新时间',
apiLeakageHelper: '请勿泄露二维码,确保仅在受信任的环境中使用',
},
@@ -87,6 +87,7 @@ import { ref, watch } from 'vue';
import bus from '@/global/bus';
import { logOutApi } from '@/api/modules/auth';
import router from '@/routers';
import { loadProductProFromDB } from '@/utils/xpack';
const filter = ref();
const globalStore = GlobalStore();
@@ -176,6 +177,7 @@ const changeNode = (command: string) => {
if (command == 'local') {
globalStore.currentNode = command || 'local';
globalStore.currentNodeAddr = '';
loadProductProFromDB();
router.push({ name: 'home' });
return;
}
@@ -195,6 +197,7 @@ const changeNode = (command: string) => {
}
globalStore.currentNode = command || 'local';
globalStore.currentNodeAddr = item.addr;
loadProductProFromDB();
router.push({ name: 'home' });
}
}
+1
View File
@@ -13,6 +13,7 @@ export interface ThemeConfigProp {
loginImage: string;
loginBgType: string;
loginBackground: string;
loginBtnLinkColor: string;
themeColor: string;
}
+1
View File
@@ -25,6 +25,7 @@ const GlobalStore = defineStore({
loginImage: '',
loginBackground: '',
loginBgType: '',
loginBtnLinkColor: '',
},
openMenuTabs: false,
isFullScreen: false,
+1 -1
View File
@@ -98,7 +98,7 @@ html.dark {
--panel-logs-bg-color: var(--panel-main-bg-color-9);
--panel-alert-bg-color: var(--panel-main-bg-color-10);
--el-menu-item-bg-color: var(--panel-main-bg-color-10);
--el-menu-item-bg-color: var(--panel-main-bg-color-9);
--el-menu-item-bg-color-active: var(--panel-main-bg-color-8);
--el-menu-hover-bg-color: var(--panel-main-bg-color-8);
--el-menu-text-color: var(--panel-main-bg-color-2);
+1
View File
@@ -114,6 +114,7 @@ export async function getXpackSettingForTheme() {
globalStore.themeConfig.loginImage = res2.data?.loginImage;
globalStore.themeConfig.loginBgType = res2.data?.loginBgType;
globalStore.themeConfig.loginBackground = res2.data?.loginBackground;
globalStore.themeConfig.loginBtnLinkColor = res2.data?.loginBtnLinkColor;
globalStore.themeConfig.themeColor = res2.data?.themeColor;
if (res2.data?.theme) {
globalStore.themeConfig.theme = res2.data.theme;
@@ -78,7 +78,12 @@
></el-input>
</el-form-item>
<el-form-item prop="allowPort" v-if="!paramModel.isHostMode">
<el-checkbox v-model="paramModel.allowPort" :label="$t('app.allowPort')" size="large" />
<el-checkbox
v-model="paramModel.allowPort"
:label="$t('app.allowPort')"
size="large"
@change="changeAllowPort"
/>
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="paramModel.allowPort" prop="specifyIP">
@@ -223,6 +228,12 @@ const editParam = () => {
edit.value = !edit.value;
};
const changeAllowPort = () => {
if (paramModel.value.allowPort) {
paramModel.value.specifyIP = '';
}
};
const get = async () => {
try {
loading.value = true;
@@ -4,67 +4,68 @@
:header="$t('commons.button.' + operateReq.operate)"
:resource="resourceName"
@close="handleClose"
v-loading="loading"
>
<el-descriptions direction="vertical">
<el-descriptions-item>
<el-link @click="toLink(app.website)">
<el-icon><OfficeBuilding /></el-icon>
<span>{{ $t('app.appOfficeWebsite') }}</span>
</el-link>
</el-descriptions-item>
<el-descriptions-item>
<el-link @click="toLink(app.document)">
<el-icon><Document /></el-icon>
<span>{{ $t('app.document') }}</span>
</el-link>
</el-descriptions-item>
<el-descriptions-item>
<el-link @click="toLink(app.github)">
<el-icon><Link /></el-icon>
<span>{{ $t('app.github') }}</span>
</el-link>
</el-descriptions-item>
</el-descriptions>
<el-form @submit.prevent ref="updateRef" :rules="rules" label-position="top" :model="operateReq">
<el-form-item :label="$t('app.versionSelect')" prop="detailId">
<el-select v-model="operateReq.version" @change="getVersions(operateReq.version)">
<el-option
v-if="operateReq.operate == 'ignore'"
:value="'all'"
:label="$t('commons.table.all') + $t('app.version')"
></el-option>
<el-option
v-for="(version, index) in versions"
:key="index"
:value="version.version"
:label="version.version"
></el-option>
</el-select>
</el-form-item>
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
<span class="input-help">
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
</span>
</el-form-item>
<el-form-item prop="pullImage" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
</el-form>
<div v-loading="loading">
<el-descriptions direction="vertical">
<el-descriptions-item>
<el-link @click="toLink(app.website)">
<el-icon><OfficeBuilding /></el-icon>
<span>{{ $t('app.appOfficeWebsite') }}</span>
</el-link>
</el-descriptions-item>
<el-descriptions-item>
<el-link @click="toLink(app.document)">
<el-icon><Document /></el-icon>
<span>{{ $t('app.document') }}</span>
</el-link>
</el-descriptions-item>
<el-descriptions-item>
<el-link @click="toLink(app.github)">
<el-icon><Link /></el-icon>
<span>{{ $t('app.github') }}</span>
</el-link>
</el-descriptions-item>
</el-descriptions>
<el-form @submit.prevent ref="updateRef" :rules="rules" label-position="top" :model="operateReq">
<el-form-item :label="$t('app.versionSelect')" prop="detailId">
<el-select v-model="operateReq.version" @change="getVersions(operateReq.version)">
<el-option
v-if="operateReq.operate == 'ignore'"
:value="'all'"
:label="$t('commons.table.all') + $t('app.version')"
></el-option>
<el-option
v-for="(version, index) in versions"
:key="index"
:value="version.version"
:label="version.version"
></el-option>
</el-select>
</el-form-item>
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
<span class="input-help">
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
</span>
</el-form-item>
<el-form-item prop="pullImage" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
</el-form>
<div v-if="operateReq.operate === 'upgrade'">
<el-text type="warning">{{ $t('app.upgradeWarn') }}</el-text>
<br />
<el-button @click="openDiff()" type="primary">{{ $t('app.showDiff') }}</el-button>
<div>
<el-checkbox v-model="useNewCompose" :label="$t('app.useCustom')" size="large" />
<div v-if="operateReq.operate === 'upgrade'">
<el-text type="warning">{{ $t('app.upgradeWarn') }}</el-text>
<br />
<el-button @click="openDiff()" type="primary">{{ $t('app.showDiff') }}</el-button>
<div>
<el-checkbox v-model="useNewCompose" :label="$t('app.useCustom')" size="large" />
</div>
<div v-if="useNewCompose">
<el-text type="danger">{{ $t('app.useCustomHelper') }}</el-text>
</div>
<CodemirrorPro v-if="useNewCompose" v-model="newCompose" mode="yaml"></CodemirrorPro>
</div>
<div v-if="useNewCompose">
<el-text type="danger">{{ $t('app.useCustomHelper') }}</el-text>
</div>
<CodemirrorPro v-if="useNewCompose" v-model="newCompose" mode="yaml"></CodemirrorPro>
</div>
<template #footer>
<span class="dialog-footer">
@@ -0,0 +1,185 @@
<template>
<DialogPro v-model="visible" :title="$t('commons.button.import')" size="large">
<div>
<el-alert :closable="false" show-icon type="info" :title="$t('cronjob.importHelper')" />
<el-upload
action="#"
:auto-upload="false"
ref="uploadRef"
class="upload mt-2"
:show-file-list="false"
:limit="1"
:accept="'.json'"
:on-change="fileOnChange"
:on-exceed="handleExceed"
v-model:file-list="uploaderFiles"
>
<el-button class="float-left" type="primary">{{ $t('commons.button.upload') }}</el-button>
</el-upload>
<el-button :disabled="selects.length === 0" @click="onImport" class="ml-2 mt-2">
{{ $t('commons.button.import') }}
</el-button>
<el-card class="mt-2 w-full" v-loading="loading">
<el-table :data="data" @selection-change="handleSelectionChange">
<el-table-column type="selection" fix />
<el-table-column
:label="$t('cronjob.taskName')"
:min-width="120"
prop="name"
show-overflow-tooltip
/>
<el-table-column
:label="$t('commons.table.type')"
:min-width="120"
prop="type"
show-overflow-tooltip
>
<template #default="{ row }">
<el-tag>{{ $t('cronjob.' + row.type) }}</el-tag>
</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" />
</el-table>
</el-card>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">
{{ $t('commons.button.cancel') }}
</el-button>
</span>
</template>
</DialogPro>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { transSpecToStr } from './../helper';
import { genFileId, UploadFile, UploadFiles, UploadProps, UploadRawFile } from 'element-plus';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { importCronjob } from '@/api/modules/cronjob';
import { Cronjob } from '@/api/interface/cronjob';
const emit = defineEmits<{ (e: 'search'): void }>();
const visible = ref(false);
const loading = ref();
const selects = ref<any>([]);
const data = ref();
const uploadRef = ref();
const uploaderFiles = ref();
const acceptParams = (): void => {
visible.value = true;
data.value = [];
};
const handleSelectionChange = (val: any) => {
selects.value = val;
};
const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
loading.value = true;
data.value = [];
uploaderFiles.value = uploadFiles;
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result as string;
const parsed = JSON.parse(content) as Cronjob.CronjobTrans;
if (!Array.isArray(parsed)) {
MsgError(i18n.global.t('cronjob.errImportFormat'));
loading.value = false;
return;
}
for (const item of parsed) {
if (!checkDataFormat(item)) {
MsgError(i18n.global.t('cronjob.errImportFormat'));
loading.value = false;
return;
}
}
data.value = parsed;
loading.value = false;
} catch (error) {
MsgError(i18n.global.t('cronjob.errImport') + error.message);
loading.value = false;
}
};
reader.readAsText(_uploadFile.raw);
};
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
file.uid = genFileId();
uploadRef.value!.handleStart(file);
};
const checkDataFormat = (item: any) => {
if (!item.name) {
return false;
}
const cronjobTypes = [
'shell',
'app',
'website',
'database',
'directory',
'log',
'curl',
'cutWebsiteLog',
'clean',
'snapshot',
'ntp',
];
if (!item.type || cronjobTypes.indexOf(item.type) === -1) {
return false;
}
return true;
};
const onImport = async () => {
await importCronjob(selects.value).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
visible.value = false;
emit('search');
});
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.upload {
width: 60px;
float: left;
}
</style>
+62 -2
View File
@@ -16,6 +16,15 @@
{{ $t('commons.button.delete') }}
</el-button>
</el-button-group>
<el-button-group class="ml-4">
<el-button @click="onImport">
{{ $t('commons.button.import') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onExport">
{{ $t('commons.button.export') }}
</el-button>
</el-button-group>
</template>
<template #rightToolBar>
<TableSearch @search="search()" v-model:searchName="searchName" />
@@ -54,11 +63,12 @@
:operate="true"
/>
<Status
v-else
v-if="row.status === 'Disable'"
@click="onChangeStatus(row.id, 'enable')"
:status="row.status"
:operate="true"
/>
<Status v-if="row.status === 'Pending'" :status="row.status" />
</template>
</el-table-column>
<el-table-column :label="$t('cronjob.cronSpec')" show-overflow-tooltip :min-width="120">
@@ -166,7 +176,9 @@
</el-form>
</template>
</OpDialog>
<OpDialog ref="opExportRef" @search="search" @submit="onSubmitExport()" />
<Records @search="search" ref="dialogRecordRef" />
<Import @search="search" ref="dialogImportRef" />
<Backups @search="search" ref="dialogBackupRef" />
</div>
</template>
@@ -174,8 +186,9 @@
<script lang="ts" setup>
import Records from '@/views/cronjob/cronjob/record/index.vue';
import Backups from '@/views/cronjob/cronjob/backup/index.vue';
import Import from '@/views/cronjob/cronjob/import/index.vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { deleteCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
import { deleteCronjob, exportCronjob, getCronjobPage, handleOnce, updateStatus } from '@/api/modules/cronjob';
import i18n from '@/lang';
import { Cronjob } from '@/api/interface/cronjob';
import { ElMessageBox } from 'element-plus';
@@ -183,6 +196,7 @@ import { MsgSuccess } from '@/utils/message';
import { hasBackup, transSpecToStr } from './helper';
import { GlobalStore } from '@/store';
import router from '@/routers';
import { getCurrentDateFormatted } from '@/utils/util';
const globalStore = GlobalStore();
const mobile = computed(() => {
@@ -198,6 +212,8 @@ const opRef = ref();
const showClean = ref();
const cleanData = ref();
const cleanRemoteData = ref();
const opExportRef = ref();
const dialogImportRef = ref();
const data = ref();
const paginationConfig = reactive({
@@ -286,6 +302,47 @@ const onSubmitDelete = async () => {
});
};
const onImport = () => {
dialogImportRef.value.acceptParams();
};
const onExport = async () => {
let names = [];
let ids = [];
for (const item of selects.value) {
names.push(item.name);
ids.push(item.id);
}
operateIDs.value = ids;
opExportRef.value.acceptParams({
title: i18n.global.t('commons.button.export'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('menu.cronjob'),
i18n.global.t('commons.button.export'),
]),
api: null,
params: null,
});
};
const onSubmitExport = async () => {
loading.value = true;
await exportCronjob({ ids: operateIDs.value })
.then((res) => {
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = '1panel-cronjob-' + getCurrentDateFormatted() + '.json';
const event = new MouseEvent('click');
a.dispatchEvent(event);
})
.finally(() => {
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'),
@@ -343,6 +400,9 @@ const buttons = [
click: (row: Cronjob.CronjobInfo) => {
onHandle(row);
},
disabled: (row: any) => {
return row.status === 'Pending';
},
},
{
label: i18n.global.t('commons.button.edit'),
@@ -834,7 +834,7 @@ const search = async () => {
form.type = res.data.type;
form.specCustom = res.data.specCustom;
form.spec = res.data.spec;
form.specs = res.data.specs;
form.specs = res.data.specs || [];
if (!form.specCustom && form.spec) {
let objs = [];
for (const item of res.data.spec.split(',')) {
@@ -864,16 +864,16 @@ const search = async () => {
form.executor !== 'python3';
}
form.scriptID = res.data.scriptID;
form.scriptID = res.data.scriptID || null;
form.appID = res.data.appID;
form.appIdList = res.data.appID.split(',') || [];
form.appIdList = res.data.appID ? res.data.appID.split(',') : [];
form.website = res.data.website;
form.websiteList = res.data.website.split(',') || [];
form.websiteList = res.data.website ? res.data.website.split(',') : [];
form.exclusionRules = res.data.exclusionRules;
form.ignoreFiles = res.data.exclusionRules.split(',');
form.ignoreFiles = res.data.exclusionRules ? res.data.exclusionRules.split(',') : [];
form.dbType = res.data.dbType;
form.dbName = res.data.dbName;
form.dbNameList = res.data.dbName.split(',') || [];
form.dbNameList = res.data.dbName ? res.data.dbName.split(',') : [];
form.url = res.data.url;
form.withImage = res.data.snapshotRule.withImage;
form.ignoreAppIDs = res.data.snapshotRule.ignoreAppIDs;
@@ -30,10 +30,15 @@
<Status class="mt-0.5" :status="dialogData.rowData.status" />
</div>
<div class="mt-0.5">
<el-button type="primary" @click="onHandle(dialogData.rowData)" link>
<el-button
type="primary"
:disabled="dialogData.rowData.status === 'Pending'"
@click="onHandle(dialogData.rowData)"
link
>
{{ $t('commons.button.handle') }}
</el-button>
<el-divider direction="vertical" />
<el-divider direction="vertical" v-if="dialogData.rowData.status !== 'Pending'" />
<el-button
type="primary"
v-if="dialogData.rowData.status === 'Enable'"
@@ -63,7 +68,7 @@
<template #rightToolBar>
<el-date-picker
class="mr-2.5"
@change="search()"
@change="search(true)"
v-model="timeRangeLoad"
type="datetimerange"
range-separator="-"
@@ -71,7 +76,7 @@
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
></el-date-picker>
<el-select @change="search()" v-model="searchInfo.status" class="p-w-200">
<el-select @change="search(true)" v-model="searchInfo.status" class="p-w-200">
<template #prefix>{{ $t('commons.table.status') }}</template>
<el-option :label="$t('commons.table.all')" value="" />
<el-option :label="$t('commons.status.success')" value="Success" />
@@ -119,7 +124,7 @@
</div>
</el-col>
<el-col :span="17">
<el-form label-position="top" :v-key="refresh">
<el-form label-position="top">
<el-row type="flex" justify="center">
<el-form-item class="descriptionWide">
<template #label>
@@ -158,22 +163,12 @@
{{ currentRecord?.message }}
</el-form-item>
</el-row>
<el-row v-if="currentRecord?.records">
<span>{{ $t('commons.table.records') }}</span>
<div class="editor-main">
<highlightjs
ref="mymirror"
language="JavaScript"
:autodetect="false"
:code="currentRecordDetail"
></highlightjs>
</div>
</el-row>
<el-row v-if="currentRecord?.taskID && currentRecord?.taskID != ''">
<LogFile
:defaultButton="false"
:defaultButton="true"
class="w-full"
:key="currentRecord?.taskID"
@stop-reading="search(false)"
:heightDiff="410"
:config="{ type: 'task', taskID: currentRecord?.taskID, tail: true }"
/>
@@ -218,9 +213,9 @@
</template>
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
import { reactive, ref } from 'vue';
import { Cronjob } from '@/api/interface/cronjob';
import { searchRecords, handleOnce, updateStatus, cleanRecords, getRecordLog } from '@/api/modules/cronjob';
import { searchRecords, handleOnce, updateStatus, cleanRecords } from '@/api/modules/cronjob';
import { dateFormat } from '@/utils/util';
import LogFile from '@/components/log/file/index.vue';
import i18n from '@/lang';
@@ -232,14 +227,8 @@ import { shortcuts } from '@/utils/shortcuts';
import { hasBackup } from '../helper';
const loading = ref();
const refresh = ref(false);
const hasRecords = ref();
let timer: NodeJS.Timer | null = null;
const mymirror = ref();
const scrollerElement = ref<HTMLElement | null>(null);
interface DialogProps {
rowData: Cronjob.CronjobInfo;
}
@@ -247,7 +236,6 @@ const recordShow = ref(false);
const dialogData = ref();
const records = ref<Array<Cronjob.Record>>([]);
const currentRecord = ref<Cronjob.Record>();
const currentRecordDetail = ref<string>('');
const open = ref();
const delLoading = ref();
@@ -282,20 +270,17 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
}
}
}
search();
timer = setInterval(() => {
search();
}, 1000 * 5);
search(true);
};
const handleSizeChange = (val: number) => {
searchInfo.pageSize = val;
localStorage.setItem(searchInfo.cacheSizeKey, val + '');
search();
search(true);
};
const handleCurrentChange = (val: number) => {
searchInfo.page = val;
search();
search(false);
};
const timeRangeLoad = ref<[Date, Date]>([
@@ -319,7 +304,7 @@ const onHandle = async (row: Cronjob.CronjobInfo) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
search(true);
})
.catch(() => {
loading.value = false;
@@ -338,7 +323,7 @@ const onChangeStatus = async (id: number, status: string) => {
});
};
const search = async () => {
const search = async (changeToLatest: boolean) => {
if (timeRangeLoad.value && timeRangeLoad.value.length === 2) {
searchInfo.startTime = timeRangeLoad.value[0];
searchInfo.endTime = timeRangeLoad.value[1];
@@ -361,44 +346,20 @@ const search = async () => {
if (!hasRecords.value) {
return;
}
if (!currentRecord.value) {
if (changeToLatest) {
currentRecord.value = records.value[0];
} else {
let beDelete = true;
for (const item of records.value) {
if (item.id === currentRecord.value.id) {
beDelete = false;
currentRecord.value = item;
break;
}
}
if (beDelete) {
currentRecord.value = records.value[0];
}
return;
}
if (currentRecord.value?.records) {
loadRecord(currentRecord.value);
for (const item of records.value) {
if (item.id === currentRecord.value.id) {
currentRecord.value = item;
break;
}
}
};
const forDetail = async (row: Cronjob.Record) => {
currentRecord.value = row;
loadRecord(row);
};
const loadRecord = async (row: Cronjob.Record) => {
currentRecord.value = row;
if (row.records) {
const res = await getRecordLog(row.id);
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
if (currentRecordDetail.value === log) {
return;
}
currentRecordDetail.value = log;
nextTick(() => {
initLog();
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
});
}
};
const onClean = async () => {
@@ -412,7 +373,7 @@ const onClean = async () => {
.then(() => {
delLoading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
search(true);
})
.catch(() => {
delLoading.value = false;
@@ -430,26 +391,13 @@ const cleanRecord = async () => {
delLoading.value = false;
open.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
search(true);
})
.catch(() => {
delLoading.value = false;
});
};
const initLog = () => {
if (mymirror.value && scrollerElement.value == undefined) {
scrollerElement.value = mymirror.value.$el as HTMLElement;
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
hljsDom.style['min-height'] = '500px';
}
};
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
});
defineExpose({
acceptParams,
});
+1 -1
View File
@@ -12,7 +12,7 @@ import i18n from '@/lang';
const buttons = [
{
label: i18n.global.t('menu.settings', 2),
label: i18n.global.t('menu.config', 2),
path: '/hosts/ssh/ssh',
},
{
@@ -200,6 +200,7 @@ const errMfaInfo = ref(false);
const isDemo = ref(false);
const isIntl = ref(true);
const open = ref(false);
const loginBtnLinkColor = ref<string | null>(null);
type FormInstance = InstanceType<typeof ElForm>;
const _isMobile = () => {
@@ -420,6 +421,47 @@ const getSetting = async () => {
} catch (error) {}
};
function adjustColorToRGBA(color: string, percent: number, opacity: number): string {
let r = 0,
g = 0,
b = 0,
a = opacity;
color = color.trim();
if (color.startsWith('#')) {
if (color.length === 4) {
r = parseInt(color[1] + color[1], 16);
g = parseInt(color[2] + color[2], 16);
b = parseInt(color[3] + color[3], 16);
} else if (color.length === 7) {
r = parseInt(color.slice(1, 3), 16);
g = parseInt(color.slice(3, 5), 16);
b = parseInt(color.slice(5, 7), 16);
} else {
return color;
}
} else if (color.startsWith('rgb')) {
const result = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/);
if (!result) return color;
r = parseInt(result[1], 10);
g = parseInt(result[2], 10);
b = parseInt(result[3], 10);
if (result[4] !== undefined) {
a = parseFloat(result[4]);
}
} else {
return color;
}
r = Math.min(255, Math.max(0, Math.round(r * (1 + percent / 100))));
g = Math.min(255, Math.max(0, Math.round(g * (1 + percent / 100))));
b = Math.min(255, Math.max(0, Math.round(b * (1 + percent / 100))));
a = Math.min(1, Math.max(0, opacity / 100));
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
onMounted(() => {
globalStore.isOnRestart = false;
getSetting();
@@ -428,6 +470,17 @@ onMounted(() => {
loginVerify();
}
document.title = globalStore.themeConfig.panelName;
loginBtnLinkColor.value = globalStore.themeConfig.loginBtnLinkColor || '#005eeb';
document.documentElement.style.setProperty('--login-btn-link-color', loginBtnLinkColor.value);
document.documentElement.style.setProperty(
'--login-btn-link-hover-color',
adjustColorToRGBA(loginBtnLinkColor.value, -10, 80),
);
document.documentElement.style.setProperty(
'--login-loading-mask-color',
adjustColorToRGBA(loginBtnLinkColor.value, 30, 15),
);
loginForm.agreeLicense = globalStore.agreeLicense;
document.onkeydown = (e: any) => {
e = window.event || e;
@@ -467,12 +520,12 @@ onMounted(() => {
.login-form {
.login-button {
background-color: #005eeb;
border-color: #005eeb;
background-color: var(--login-btn-link-color);
border-color: var(--login-btn-link-color);
color: #ffffff;
&:hover {
background-color: #196eed !important;
border-color: #196eed !important;
background-color: var(--login-btn-link-hover-color) !important;
border-color: var(--login-btn-link-hover-color) !important;
outline: none !important;
}
}
@@ -486,7 +539,7 @@ onMounted(() => {
background: none !important;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #005eeb inset !important;
box-shadow: 0 0 0 1px var(--login-btn-link-color) inset !important;
}
.demo {
@@ -497,16 +550,16 @@ onMounted(() => {
}
.agree-title {
color: #005eeb;
color: var(--login-btn-link-color);
}
.agree {
white-space: pre-wrap;
color: #005eeb;
color: var(--login-btn-link-color);
}
:deep(a) {
color: #005eeb;
color: var(--login-btn-link-color);
&:hover {
opacity: 75%;
}
@@ -514,16 +567,16 @@ onMounted(() => {
:deep(.el-checkbox__input .el-checkbox__inner) {
background-color: #fff !important;
border-color: #e5eefd !important;
border-color: var(--login-btn-link-color) !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #005eeb !important;
border-color: #005eeb !important;
background-color: var(--login-btn-link-color) !important;
border-color: var(--login-btn-link-color) !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
border-color: #e5eefd !important;
border-color: #ffffff !important;
}
:deep(.el-input__inner) {
@@ -543,22 +596,25 @@ onMounted(() => {
}
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
color: #005eeb !important;
background-color: #e5eefd !important;
background-color: var(--login-btn-link-color) !important;
color: #fff !important;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
color: #005eeb !important;
background-color: #e5eefd !important;
background-color: var(--login-btn-link-color) !important;
color: #fff !important;
}
:deep(.el-loading-mask) {
background-color: rgba(229, 238, 253, 0.8) !important;
background-color: var(--login-loading-mask-color) !important;
.el-loading-spinner .path {
stroke: var(--login-btn-link-color);
}
}
.login-footer-btn {
.el-button--primary {
border-color: #005eeb !important;
background-color: #005eeb !important;
border-color: var(--login-btn-link-color) !important;
background-color: var(--login-btn-link-color) !important;
}
}
</style>
@@ -200,6 +200,7 @@
:rules="[Rules.requiredSelect]"
>
<el-select v-model="dialogData.rowData!.varsJson['scType']">
<el-option value="DEFAULT" :label="$t('setting.scLighthouse')" />
<el-option value="Standard" :label="$t('setting.scStandard')" />
<el-option value="Standard_IA" :label="$t('setting.scStandard_IA')" />
<el-option value="Archive" :label="$t('setting.scArchive')" />
@@ -68,6 +68,7 @@
</template>
<script lang="ts" setup>
import { checkAppInstalled } from '@/api/modules/app';
import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgError } from '@/utils/message';
@@ -89,9 +90,10 @@ const rules = ref({
type: Array,
},
});
const defaultPort = ref(80);
const initDomain = () => ({
domain: '',
port: 80,
port: defaultPort.value,
ssl: false,
});
const create = ref({
@@ -128,16 +130,16 @@ const gengerateDomains = () => {
const exists = (domain: string, port: number): boolean => {
return create.value.domains.some((info) => info.domain === domain && info.port === port);
};
if (exists(domain, port ? Number(port) : 80)) {
if (exists(domain, port ? Number(port) : defaultPort.value)) {
return;
}
if (create.value.domains[0].domain == '') {
create.value.domains[0].domain = domain;
create.value.domains[0].port = port ? Number(port) : 80;
create.value.domains[0].port = port ? Number(port) : defaultPort.value;
} else {
create.value.domains.push({
domain,
port: port ? Number(port) : 80,
port: port ? Number(port) : defaultPort.value,
ssl: false,
});
}
@@ -149,7 +151,19 @@ const handleParams = () => {
props.form.domains = create.value.domains;
};
const getOprensty = async () => {
try {
await checkAppInstalled('openresty', '')
.then((res) => {
defaultPort.value = res.data.httpPort || 80;
create.value.domains[0].port = defaultPort.value;
})
.catch(() => {});
} catch (error) {}
};
onMounted(() => {
getOprensty();
handleParams();
});
</script>