mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 08:00:27 +00:00
Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96ab4c53af | ||
|
|
96cf46d834 | ||
|
|
208414704d | ||
|
|
7f453aa145 | ||
|
|
123997fa13 | ||
|
|
7faef2f8a7 | ||
|
|
f93cf3b996 | ||
|
|
df5b69756c | ||
|
|
745bb98abe | ||
|
|
cf1d02771f | ||
|
|
52377b0042 | ||
|
|
270501a237 | ||
|
|
2e5283b8d3 | ||
|
|
8cad47b29e | ||
|
|
2458d60d80 | ||
|
|
9a79dc4cdd | ||
|
|
e531b509d6 | ||
|
|
d287fc1186 | ||
|
|
f47f7c7361 | ||
|
|
7a6c99d57d | ||
|
|
5105114321 | ||
|
|
60beebcb4c | ||
|
|
fa03b4226a | ||
|
|
b047a053c5 | ||
|
|
42907cd228 | ||
|
|
425892cfc4 | ||
|
|
16fd804e93 | ||
|
|
2f147dde13 | ||
|
|
0d04f07f2a | ||
|
|
96485218b7 | ||
|
|
227cc57426 | ||
|
|
b99061777a | ||
|
|
8749dc649a | ||
|
|
65da51a478 | ||
|
|
0c31ba1c5f | ||
|
|
acf94b052b | ||
|
|
556441d811 | ||
|
|
efcf17e47c | ||
|
|
02b7744995 | ||
|
|
c86947b69c | ||
|
|
839b8c2844 | ||
|
|
e4a634e6e8 | ||
|
|
eb99c10ad0 | ||
|
|
175b7b89e4 | ||
|
|
5374afb226 | ||
|
|
1a44f48fad | ||
|
|
50dbe7cfc0 | ||
|
|
c7e576c879 | ||
|
|
a3e4847744 | ||
|
|
903f8ed5a7 | ||
|
|
b091c7f364 | ||
|
|
e069db3c45 | ||
|
|
047264dbbc | ||
|
|
0c216ba87a | ||
|
|
31cdc28143 | ||
|
|
e03b728240 | ||
|
|
4c275ba52f | ||
|
|
2ac9a888a8 | ||
|
|
f9bdcf8938 | ||
|
|
c3bef26ae6 | ||
|
|
634849c347 | ||
|
|
c557b737f6 | ||
|
|
1d326afa93 | ||
|
|
cb6c58cb5d | ||
|
|
ec4d708569 | ||
|
|
05b19cefa5 | ||
|
|
fdd57d0801 | ||
|
|
c16d6d1644 | ||
|
|
656b39c880 | ||
|
|
753c996291 | ||
|
|
dcdd0cc2bb | ||
|
|
2f32df16d9 | ||
|
|
9caa9c47b4 | ||
|
|
55bec8a911 | ||
|
|
43f95fd40e | ||
|
|
641812e7c1 | ||
|
|
95d3e75425 | ||
|
|
7f06c7e92e | ||
|
|
0e6fdffeec | ||
|
|
f77681b9c9 | ||
|
|
535d4bbe1b |
@@ -120,6 +120,28 @@ func (b *BaseApi) SearchSnapshot(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load system snapshot size
|
||||
// @Description 获取系统快照大小
|
||||
// @Accept json
|
||||
// @Param request body dto.PageSnapshot true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/snapshot/size [post]
|
||||
func (b *BaseApi) LoadSnapshotSize(c *gin.Context) {
|
||||
var req dto.PageSnapshot
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := snapshotService.LoadSize(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Recover system backup
|
||||
// @Description 从系统快照恢复
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dto
|
||||
|
||||
type CreateOrUpdateAlert struct {
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertType string `json:"alertType"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
EntryID uint `json:"entryID"`
|
||||
}
|
||||
|
||||
type AlertBase struct {
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
}
|
||||
|
||||
type PushAlert struct {
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
Param string `json:"param"`
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type ClamInfo struct {
|
||||
LastHandleDate string `json:"lastHandleDate"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
}
|
||||
|
||||
type ClamLogSearch struct {
|
||||
@@ -71,6 +72,8 @@ type ClamCreate struct {
|
||||
InfectedDir string `json:"infectedDir"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type ClamUpdate struct {
|
||||
@@ -82,6 +85,8 @@ type ClamUpdate struct {
|
||||
InfectedDir string `json:"infectedDir"`
|
||||
Spec string `json:"spec"`
|
||||
Description string `json:"description"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type ClamUpdateStatus struct {
|
||||
|
||||
@@ -116,7 +116,7 @@ type PortHelper struct {
|
||||
|
||||
type ContainerOperation struct {
|
||||
Names []string `json:"names" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause remove"`
|
||||
Operation string `json:"operation" validate:"required,oneof=up start stop restart kill pause unpause remove"`
|
||||
}
|
||||
|
||||
type ContainerRename struct {
|
||||
@@ -200,6 +200,7 @@ type ComposeInfo struct {
|
||||
Workdir string `json:"workdir"`
|
||||
Path string `json:"path"`
|
||||
Containers []ComposeContainer `json:"containers"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
type ComposeContainer struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
@@ -208,22 +209,24 @@ type ComposeContainer struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
type ComposeCreate struct {
|
||||
Name string `json:"name"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
Template uint `json:"template"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
Template uint `json:"template"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
type ComposeOperation struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop down"`
|
||||
Operation string `json:"operation" validate:"required,oneof=up start stop down delete"`
|
||||
WithFile bool `json:"withFile"`
|
||||
}
|
||||
type ComposeUpdate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
|
||||
type ContainerLog struct {
|
||||
|
||||
@@ -31,10 +31,13 @@ type CronjobCreate struct {
|
||||
DefaultDownload string `json:"defaultDownload"`
|
||||
RetainCopies int `json:"retainCopies" validate:"number,min=1"`
|
||||
Secret string `json:"secret"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type CronjobUpdate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Spec string `json:"spec" validate:"required"`
|
||||
|
||||
@@ -53,6 +56,8 @@ type CronjobUpdate struct {
|
||||
DefaultDownload string `json:"defaultDownload"`
|
||||
RetainCopies int `json:"retainCopies" validate:"number,min=1"`
|
||||
Secret string `json:"secret"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
}
|
||||
|
||||
type CronjobUpdateStatus struct {
|
||||
@@ -99,6 +104,7 @@ type CronjobInfo struct {
|
||||
LastRecordTime string `json:"lastRecordTime"`
|
||||
Status string `json:"status"`
|
||||
Secret string `json:"secret"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
}
|
||||
|
||||
type SearchRecord struct {
|
||||
|
||||
@@ -175,21 +175,22 @@ type WebsiteUpdateDirPermission struct {
|
||||
}
|
||||
|
||||
type WebsiteProxyConfig struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Operate string `json:"operate" validate:"required"`
|
||||
Enable bool `json:"enable" `
|
||||
Cache bool `json:"cache" `
|
||||
CacheTime int `json:"cacheTime" `
|
||||
CacheUnit string `json:"cacheUnit"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Modifier string `json:"modifier"`
|
||||
Match string `json:"match" validate:"required"`
|
||||
ProxyPass string `json:"proxyPass" validate:"required"`
|
||||
ProxyHost string `json:"proxyHost" validate:"required"`
|
||||
Content string `json:"content"`
|
||||
FilePath string `json:"filePath"`
|
||||
Replaces map[string]string `json:"replaces"`
|
||||
SNI bool `json:"sni"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Operate string `json:"operate" validate:"required"`
|
||||
Enable bool `json:"enable" `
|
||||
Cache bool `json:"cache" `
|
||||
CacheTime int `json:"cacheTime" `
|
||||
CacheUnit string `json:"cacheUnit"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Modifier string `json:"modifier"`
|
||||
Match string `json:"match" validate:"required"`
|
||||
ProxyPass string `json:"proxyPass" validate:"required"`
|
||||
ProxyHost string `json:"proxyHost" validate:"required"`
|
||||
Content string `json:"content"`
|
||||
FilePath string `json:"filePath"`
|
||||
Replaces map[string]string `json:"replaces"`
|
||||
SNI bool `json:"sni"`
|
||||
ProxySSLName string `json:"proxySSLName"`
|
||||
}
|
||||
|
||||
type WebsiteProxyReq struct {
|
||||
|
||||
@@ -41,6 +41,7 @@ type WebsiteSSLApply struct {
|
||||
ID uint `json:"ID" validate:"required"`
|
||||
SkipDNSCheck bool `json:"skipDNSCheck"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
DisableLog bool `json:"disableLog"`
|
||||
}
|
||||
|
||||
type WebsiteAcmeAccountCreate struct {
|
||||
|
||||
@@ -30,6 +30,7 @@ type SettingInfo struct {
|
||||
ServerPort string `json:"serverPort"`
|
||||
SSL string `json:"ssl"`
|
||||
SSLType string `json:"sslType"`
|
||||
AutoRestart string `json:"autoRestart"`
|
||||
BindDomain string `json:"bindDomain"`
|
||||
AllowIPs string `json:"allowIPs"`
|
||||
SecurityEntrance string `json:"securityEntrance"`
|
||||
@@ -73,12 +74,13 @@ type SettingUpdate struct {
|
||||
}
|
||||
|
||||
type SSLUpdate struct {
|
||||
SSLType string `json:"sslType" validate:"required,oneof=self select import import-paste import-local"`
|
||||
Domain string `json:"domain"`
|
||||
SSL string `json:"ssl" validate:"required,oneof=enable disable"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
SSLID uint `json:"sslID"`
|
||||
SSLType string `json:"sslType" validate:"required,oneof=self select import import-paste import-local"`
|
||||
Domain string `json:"domain"`
|
||||
SSL string `json:"ssl" validate:"required,oneof=enable disable"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
SSLID uint `json:"sslID"`
|
||||
AutoRestart string `json:"autoRestart"`
|
||||
}
|
||||
type SSLInfo struct {
|
||||
Domain string `json:"domain"`
|
||||
@@ -147,9 +149,8 @@ type SnapshotInfo struct {
|
||||
DefaultDownload string `json:"defaultDownload"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"`
|
||||
|
||||
InterruptStep string `json:"interruptStep"`
|
||||
RecoverStatus string `json:"recoverStatus"`
|
||||
@@ -160,6 +161,12 @@ type SnapshotInfo struct {
|
||||
LastRollbackedAt string `json:"lastRollbackedAt"`
|
||||
}
|
||||
|
||||
type SnapshotFile struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type UpgradeInfo struct {
|
||||
TestVersion string `json:"testVersion"`
|
||||
NewVersion string `json:"newVersion"`
|
||||
|
||||
@@ -12,4 +12,5 @@ type Compose struct {
|
||||
BaseModel
|
||||
|
||||
Name string `gorm:"type:varchar(256)" json:"name"`
|
||||
Path string `gorm:"type:varchar(256)" json:"path"`
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type IComposeTemplateRepo interface {
|
||||
CreateRecord(compose *model.Compose) error
|
||||
DeleteRecord(opts ...DBOption) error
|
||||
ListRecord() ([]model.Compose, error)
|
||||
UpdateRecord(name string, vars map[string]interface{}) error
|
||||
}
|
||||
|
||||
func NewIComposeTemplateRepo() IComposeTemplateRepo {
|
||||
@@ -102,3 +103,7 @@ func (u *ComposeTemplateRepo) DeleteRecord(opts ...DBOption) error {
|
||||
}
|
||||
return db.Delete(&model.Compose{}).Error
|
||||
}
|
||||
|
||||
func (u *ComposeTemplateRepo) UpdateRecord(name string, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.Compose{}).Where("name = ?", name).Updates(vars).Error
|
||||
}
|
||||
|
||||
@@ -1342,6 +1342,18 @@ func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool)
|
||||
continue
|
||||
}
|
||||
lastVersion := versions[0]
|
||||
if app.Key == constant.AppMysql {
|
||||
for _, version := range versions {
|
||||
majorVersion := getMajorVersion(installed.Version)
|
||||
if !strings.HasPrefix(version, majorVersion) {
|
||||
continue
|
||||
} else {
|
||||
lastVersion = version
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if common.IsCrossVersion(installed.Version, lastVersion) {
|
||||
installDTO.CanUpdate = app.CrossVersionUpdate
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -154,6 +155,16 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa
|
||||
}
|
||||
datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout)
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: "clams",
|
||||
EntryID: datas[i].ID,
|
||||
}
|
||||
alertCount := xpack.GetAlert(alertBase)
|
||||
if alertCount != 0 {
|
||||
datas[i].AlertCount = alertCount
|
||||
} else {
|
||||
datas[i].AlertCount = 0
|
||||
}
|
||||
}
|
||||
return total, datas, err
|
||||
}
|
||||
@@ -180,6 +191,19 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
|
||||
if err := clamRepo.Create(&clam); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.AlertCount != 0 {
|
||||
createAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertCount: req.AlertCount,
|
||||
AlertType: "clams",
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.CreateAlert(createAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,6 +249,16 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
|
||||
if err := clamRepo.Update(req.ID, upMap); err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertType: "clams",
|
||||
AlertCount: req.AlertCount,
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.UpdateAlert(updateAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -265,6 +299,14 @@ func (c *ClamService) Delete(req dto.ClamDelete) error {
|
||||
if err := clamRepo.Delete(commonRepo.WithByID(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: "clams",
|
||||
EntryID: clam.ID,
|
||||
}
|
||||
err := xpack.DeleteAlert(alertBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -305,6 +347,7 @@ func (c *ClamService) HandleOnce(req dto.OperateByID) error {
|
||||
}
|
||||
global.LOG.Debugf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
|
||||
stdout, err := cmd.Execf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
|
||||
handleAlert(stdout, clam.Name, clam.ID)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err)
|
||||
}
|
||||
@@ -585,3 +628,28 @@ func (c *ClamService) loadLogPath(name string) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleAlert(stdout, clamName string, clamId uint) {
|
||||
if strings.Contains(stdout, "- SCAN SUMMARY -") {
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Infected files: ") {
|
||||
var infectedFiles = 0
|
||||
infectedFiles, _ = strconv.Atoi(strings.TrimPrefix(line, "Infected files: "))
|
||||
if infectedFiles > 0 {
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: clamName,
|
||||
AlertType: "clams",
|
||||
EntryID: clamId,
|
||||
Param: strconv.Itoa(infectedFiles),
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clamdscan push failed, err: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,6 +898,10 @@ func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) s
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
|
||||
filePath = composeItem.Path
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return ""
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -31,6 +32,12 @@ const composeConfigLabel = "com.docker.compose.project.config_files"
|
||||
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
|
||||
const composeCreatedBy = "createdBy"
|
||||
|
||||
type DockerCompose struct {
|
||||
Version string `yaml:"version"`
|
||||
Services map[string]map[string]interface{} `yaml:"services"`
|
||||
Networks map[string]interface{} `yaml:"networks"`
|
||||
}
|
||||
|
||||
func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
var (
|
||||
records []dto.ComposeInfo
|
||||
@@ -52,6 +59,20 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
|
||||
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
||||
|
||||
composeLocalMap := make(map[string]dto.ComposeInfo)
|
||||
for _, localItem := range composeCreatedByLocal {
|
||||
composeItemLocal := dto.ComposeInfo{
|
||||
ContainerNumber: 0,
|
||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||
ConfigFile: localItem.Path,
|
||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
||||
}
|
||||
composeItemLocal.CreatedBy = "1Panel"
|
||||
composeItemLocal.Path = localItem.Path
|
||||
composeLocalMap[localItem.Name] = composeItemLocal
|
||||
}
|
||||
|
||||
composeMap := make(map[string]dto.ComposeInfo)
|
||||
for _, container := range list {
|
||||
if name, ok := container.Labels[composeProjectLabel]; ok {
|
||||
@@ -95,12 +116,24 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range composeCreatedByLocal {
|
||||
if err := composeRepo.DeleteRecord(commonRepo.WithByID(item.ID)); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
mergedMap := make(map[string]dto.ComposeInfo)
|
||||
for key, localItem := range composeLocalMap {
|
||||
mergedMap[key] = localItem
|
||||
}
|
||||
for key, item := range composeMap {
|
||||
if existingItem, exists := mergedMap[key]; exists {
|
||||
if item.ContainerNumber > 0 {
|
||||
if existingItem.ContainerNumber <= 0 {
|
||||
mergedMap[key] = item
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergedMap[key] = item
|
||||
}
|
||||
}
|
||||
for key, value := range composeMap {
|
||||
|
||||
for key, value := range mergedMap {
|
||||
value.Name = key
|
||||
records = append(records, value)
|
||||
}
|
||||
@@ -127,7 +160,26 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
BackDatas = records[start:end]
|
||||
}
|
||||
return int64(total), BackDatas, nil
|
||||
listItem := loadEnv(BackDatas)
|
||||
return int64(total), listItem, nil
|
||||
}
|
||||
|
||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||
for i := 0; i < len(list); i++ {
|
||||
envFilePath := filepath.Join(path.Dir(list[i].Path), "1panel.env")
|
||||
file, err := os.ReadFile(envFilePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for _, line := range lines {
|
||||
lineItem := strings.TrimSpace(line)
|
||||
if len(lineItem) != 0 && !strings.HasPrefix(lineItem, "#") {
|
||||
list[i].Env = append(list[i].Env, lineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
@@ -141,6 +193,9 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
if err := u.loadPath(&req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmd := exec.Command("docker-compose", "-f", req.Path, "config")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -173,6 +228,9 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return "", err
|
||||
}
|
||||
go func() {
|
||||
defer file.Close()
|
||||
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
|
||||
@@ -186,7 +244,7 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error)
|
||||
return
|
||||
}
|
||||
global.LOG.Infof("docker-compose up %s successful!", req.Name)
|
||||
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name})
|
||||
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name, Path: req.Path})
|
||||
_, _ = file.WriteString("docker-compose up successful!")
|
||||
}()
|
||||
|
||||
@@ -200,17 +258,26 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
if _, err := os.Stat(req.Path); err != nil {
|
||||
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
|
||||
}
|
||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
|
||||
if req.Operation == "down" {
|
||||
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
||||
if req.Operation == "delete" {
|
||||
if stdout, err := compose.Operate(req.Path, "down"); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
if req.WithFile {
|
||||
_ = os.RemoveAll(path.Dir(req.Path))
|
||||
}
|
||||
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Operation == "up" {
|
||||
if stdout, err := compose.Up(req.Path); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
} else {
|
||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
}
|
||||
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -232,6 +299,10 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
write.Flush()
|
||||
|
||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path)
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stdout, err := compose.Up(req.Path); err != nil {
|
||||
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
||||
return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err)
|
||||
@@ -280,3 +351,26 @@ func recreateCompose(content, path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newComposeEnv(pathItem string, env []string) error {
|
||||
if len(env) == 0 {
|
||||
return nil
|
||||
}
|
||||
envFilePath := path.Join(path.Dir(pathItem), "1panel.env")
|
||||
|
||||
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("failed to create env file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
for _, env := range env {
|
||||
envItem := strings.TrimSpace(env)
|
||||
if _, err := file.WriteString(fmt.Sprintf("%s\n", envItem)); err != nil {
|
||||
global.LOG.Errorf("failed to write env to file: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
@@ -53,6 +54,16 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
||||
} else {
|
||||
item.LastRecordTime = "-"
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertCount := xpack.GetAlert(alertBase)
|
||||
if alertCount != 0 {
|
||||
item.AlertCount = alertCount
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
dtoCronjobs = append(dtoCronjobs, item)
|
||||
}
|
||||
return total, dtoCronjobs, err
|
||||
@@ -190,6 +201,18 @@ func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
|
||||
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||
return err
|
||||
}
|
||||
if cronjobDto.AlertCount != 0 {
|
||||
createAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: cronjobDto.AlertTitle,
|
||||
AlertCount: cronjobDto.AlertCount,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
err := xpack.CreateAlert(createAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -232,6 +255,14 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
alertBase := dto.AlertBase{
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
err := xpack.DeleteAlert(alertBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -281,7 +312,21 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
|
||||
upMap["default_download"] = req.DefaultDownload
|
||||
upMap["retain_copies"] = req.RetainCopies
|
||||
upMap["secret"] = req.Secret
|
||||
return cronjobRepo.Update(id, upMap)
|
||||
err = cronjobRepo.Update(id, upMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.CreateOrUpdateAlert{
|
||||
AlertTitle: req.AlertTitle,
|
||||
AlertType: cronModel.Type,
|
||||
AlertCount: req.AlertCount,
|
||||
EntryID: cronModel.ID,
|
||||
}
|
||||
err = xpack.UpdateAlert(updateAlert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
@@ -293,6 +338,7 @@ func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
entryIDs string
|
||||
err error
|
||||
)
|
||||
|
||||
if status == constant.StatusEnable {
|
||||
entryIDs, err = u.StartJob(&cronjob, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -30,6 +31,9 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appItems) == 0 {
|
||||
return errors.New("no such app in database!")
|
||||
}
|
||||
apps = appItems
|
||||
}
|
||||
accountMap, err := loadClientMap(cronjob.BackupAccounts)
|
||||
@@ -65,6 +69,9 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) e
|
||||
|
||||
func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time) error {
|
||||
webs := loadWebsForJob(cronjob)
|
||||
if len(webs) == 0 {
|
||||
return errors.New("no such website in database!")
|
||||
}
|
||||
accountMap, err := loadClientMap(cronjob.BackupAccounts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -98,6 +105,9 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
|
||||
|
||||
func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time) error {
|
||||
dbs := loadDbsForJob(cronjob)
|
||||
if len(dbs) == 0 {
|
||||
return errors.New("no such db in database!")
|
||||
}
|
||||
accountMap, err := loadClientMap(cronjob.BackupAccounts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/i18n"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/ntp"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -85,12 +87,12 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
_ = cronjobRepo.UpdateRecords(record.ID, map[string]interface{}{"records": record.Records})
|
||||
err = u.handleSnapshot(*cronjob, record.StartTime, record.Records)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if len(message) != 0 {
|
||||
record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message)
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
return
|
||||
}
|
||||
if len(message) != 0 {
|
||||
@@ -392,3 +394,17 @@ func (u *CronjobService) generateLogsPath(cronjob model.Cronjob, startTime time.
|
||||
func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
Param: cronjob.Type,
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("cronjob alert push failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/shirou/gopsutil/v3/load"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
network "net"
|
||||
"os"
|
||||
"sort"
|
||||
@@ -14,6 +10,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/shirou/gopsutil/v3/load"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
@@ -268,13 +269,13 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" || strings.TrimSpace(fields[1]) == "overlay" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(fields[6], "docker") {
|
||||
if strings.Contains(fields[6], "docker") || strings.Contains(fields[6], "podman") || strings.Contains(fields[6], "containerd") || strings.HasPrefix(fields[6], "/var/lib/containers") {
|
||||
continue
|
||||
}
|
||||
isExclude := false
|
||||
|
||||
@@ -4,14 +4,17 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -198,9 +201,8 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
|
||||
}
|
||||
if len(daemonMap) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -211,10 +213,12 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
|
||||
if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -258,9 +262,12 @@ func (u *DockerService) UpdateLogOption(req dto.LogOption) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -298,9 +305,12 @@ func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -308,9 +318,8 @@ func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
|
||||
func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
|
||||
if len(req.File) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -327,21 +336,42 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
|
||||
_, _ = write.WriteString(req.File)
|
||||
write.Flush()
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
service := "docker"
|
||||
if req.Operation == "stop" {
|
||||
service = "docker.socket"
|
||||
}
|
||||
stdout, err := cmd.Execf("systemctl %s %s ", req.Operation, service)
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
dockerCmd, err := getDockerRestartCommand()
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
return err
|
||||
}
|
||||
if req.Operation == "stop" {
|
||||
isSocketActive, _ := systemctl.IsActive("docker.socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Operation == "restart" {
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stdout, err := cmd.Execf("%s %s %s", dockerCmd, req.Operation, service)
|
||||
if err != nil {
|
||||
return errors.New(stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -394,3 +424,41 @@ func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize st
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateDockerConfig() error {
|
||||
if !cmd.Which("dockerd") {
|
||||
return nil
|
||||
}
|
||||
stdout, err := cmd.Exec("dockerd --validate")
|
||||
if strings.Contains(stdout, "unknown flag: --validate") {
|
||||
return nil
|
||||
}
|
||||
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
|
||||
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDockerRestartCommand() (string, error) {
|
||||
stdout, err := cmd.Exec("which docker")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find docker: %v", err)
|
||||
}
|
||||
dockerPath := stdout
|
||||
if strings.Contains(dockerPath, "snap") {
|
||||
return "snap", nil
|
||||
}
|
||||
return "systemctl", nil
|
||||
}
|
||||
|
||||
func restartDocker() error {
|
||||
restartCmd, err := getDockerRestartCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.Execf("%s restart docker", restartCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart Docker: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ func (u *FirewallService) OperateFirewall(operation string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
needRestartDocker := false
|
||||
switch operation {
|
||||
case "start":
|
||||
if err := client.Start(); err != nil {
|
||||
@@ -188,26 +189,30 @@ func (u *FirewallService) OperateFirewall(operation string) error {
|
||||
_ = client.Stop()
|
||||
return err
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl restart docker")
|
||||
return nil
|
||||
needRestartDocker = true
|
||||
case "stop":
|
||||
if err := client.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl restart docker")
|
||||
return nil
|
||||
needRestartDocker = true
|
||||
case "restart":
|
||||
if err := client.Restart(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl restart docker")
|
||||
return nil
|
||||
needRestartDocker = true
|
||||
case "disablePing":
|
||||
return u.updatePingStatus("0")
|
||||
case "enablePing":
|
||||
return u.updatePingStatus("1")
|
||||
default:
|
||||
return fmt.Errorf("not supported operation: %s", operation)
|
||||
}
|
||||
return fmt.Errorf("not support such operation: %s", operation)
|
||||
if needRestartDocker {
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool) error {
|
||||
|
||||
@@ -86,9 +86,11 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
|
||||
}
|
||||
if req.Protocol == "http" {
|
||||
_ = u.handleRegistries(req.DownloadUrl, "", "create")
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -159,12 +161,13 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
|
||||
if repo.Auth {
|
||||
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
|
||||
}
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
upMap := make(map[string]interface{})
|
||||
upMap["download_url"] = req.DownloadUrl
|
||||
upMap["protocol"] = req.Protocol
|
||||
|
||||
@@ -56,7 +56,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
|
||||
opts []repo.DBOption
|
||||
)
|
||||
if create.Name != "" {
|
||||
opts = append(opts, commonRepo.WithLikeName(create.Name))
|
||||
opts = append(opts, commonRepo.WithByName(create.Name))
|
||||
}
|
||||
if create.Type != "" {
|
||||
opts = append(opts, commonRepo.WithByType(create.Type))
|
||||
@@ -83,7 +83,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
|
||||
if exist != nil {
|
||||
return nil, buserr.New(constant.ErrImageExist)
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
if !fileOp.Stat(create.CodeDir) {
|
||||
return nil, buserr.New(constant.ErrPathNotFound)
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
|
||||
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
runtime.Port = create.Port
|
||||
if err = handleNodeAndJava(create, runtime, fileOp, appVersionDir); err != nil {
|
||||
return nil, err
|
||||
@@ -217,7 +217,7 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
|
||||
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
|
||||
}
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
|
||||
if out != "" {
|
||||
return errors.New(out)
|
||||
@@ -300,7 +300,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
||||
}
|
||||
}
|
||||
res.AppParams = appParams
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
res.Params = make(map[string]interface{})
|
||||
envs, err := gotenv.Unmarshal(runtime.Env)
|
||||
if err != nil {
|
||||
@@ -308,7 +308,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
||||
}
|
||||
for k, v := range envs {
|
||||
switch k {
|
||||
case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP", "JAVA_APP_PORT", "GO_APP_PORT":
|
||||
case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP", "JAVA_APP_PORT", "GO_APP_PORT", "APP_PORT":
|
||||
port, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -361,7 +361,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
if exist != nil {
|
||||
return buserr.New(constant.ErrImageExist)
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
if runtime.Port != req.Port {
|
||||
if err = checkPortExist(req.Port); err != nil {
|
||||
return err
|
||||
@@ -441,7 +441,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
return err
|
||||
}
|
||||
go buildRuntime(runtime, imageID, req.Rebuild)
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
runtime.Version = req.Version
|
||||
runtime.CodeDir = req.CodeDir
|
||||
runtime.Port = req.Port
|
||||
@@ -495,6 +495,9 @@ func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error {
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
_ = docker.CreateDefaultDockerNetwork()
|
||||
}()
|
||||
switch req.Operate {
|
||||
case constant.RuntimeUp:
|
||||
if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -218,7 +219,10 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
|
||||
_ = logFile.Close()
|
||||
}()
|
||||
|
||||
cmd := exec.Command("docker-compose", "-f", composePath, "build")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker-compose", "-f", composePath, "build")
|
||||
multiWriterStdout := io.MultiWriter(os.Stdout, logFile)
|
||||
cmd.Stdout = multiWriterStdout
|
||||
var stderrBuf bytes.Buffer
|
||||
@@ -228,7 +232,11 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + stderrBuf.String()
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + buserr.New("ErrCmdTimeout").Error()
|
||||
} else {
|
||||
runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + stderrBuf.String()
|
||||
}
|
||||
} else {
|
||||
runtime.Status = constant.RuntimeNormal
|
||||
runtime.Message = ""
|
||||
@@ -338,6 +346,14 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case constant.RuntimePython:
|
||||
create.Params["CODE_DIR"] = create.CodeDir
|
||||
create.Params["PYTHON_VERSION"] = create.Version
|
||||
create.Params["PANEL_APP_PORT_HTTP"] = create.Port
|
||||
composeContent, err = handleCompose(env, composeContent, create, projectDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newMap := make(map[string]string)
|
||||
@@ -384,7 +400,8 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
|
||||
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}")
|
||||
case constant.RuntimeGo:
|
||||
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}")
|
||||
|
||||
case constant.RuntimePython:
|
||||
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${APP_PORT}")
|
||||
}
|
||||
|
||||
for i, port := range create.ExposedPorts {
|
||||
|
||||
@@ -322,6 +322,9 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
|
||||
if err := settingRepo.Update("SSL", req.SSL); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.Update("AutoRestart", req.AutoRestart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sID, _ := c.Cookie(constant.SessionName)
|
||||
c.SetCookie(constant.SessionName, sID, 0, "", "", true, true)
|
||||
|
||||
@@ -28,6 +28,7 @@ type SnapshotService struct {
|
||||
|
||||
type ISnapshotService interface {
|
||||
SearchWithPage(req dto.PageSnapshot) (int64, interface{}, error)
|
||||
LoadSize(req dto.PageSnapshot) ([]dto.SnapshotFile, error)
|
||||
SnapshotCreate(req dto.SnapshotCreate) error
|
||||
SnapshotRecover(req dto.SnapshotRecover) error
|
||||
SnapshotRollback(req dto.SnapshotRecover) error
|
||||
@@ -47,15 +48,31 @@ func NewISnapshotService() ISnapshotService {
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SearchWithPage(req dto.PageSnapshot) (int64, interface{}, error) {
|
||||
total, systemBackups, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
|
||||
total, records, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
dtoSnap, err := loadSnapSize(systemBackups)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
var data []dto.SnapshotInfo
|
||||
for _, record := range records {
|
||||
var item dto.SnapshotInfo
|
||||
if err := copier.Copy(&item, &record); err != nil {
|
||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
data = append(data, item)
|
||||
}
|
||||
return total, dtoSnap, err
|
||||
return total, data, err
|
||||
}
|
||||
|
||||
func (u *SnapshotService) LoadSize(req dto.PageSnapshot) ([]dto.SnapshotFile, error) {
|
||||
_, records, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := loadSnapSize(records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
|
||||
@@ -510,15 +527,12 @@ func loadOs() string {
|
||||
}
|
||||
}
|
||||
|
||||
func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotInfo, error) {
|
||||
var datas []dto.SnapshotInfo
|
||||
func loadSnapSize(records []model.Snapshot) ([]dto.SnapshotFile, error) {
|
||||
var datas []dto.SnapshotFile
|
||||
clientMap := make(map[string]loadSizeHelper)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < len(records); i++ {
|
||||
var item dto.SnapshotInfo
|
||||
if err := copier.Copy(&item, &records[i]); err != nil {
|
||||
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
item := dto.SnapshotFile{Name: records[i].Name, ID: records[i].ID}
|
||||
itemPath := fmt.Sprintf("system_snapshot/%s.tar.gz", item.Name)
|
||||
if _, ok := clientMap[records[i].DefaultDownload]; !ok {
|
||||
backup, err := backupRepo.Get(commonRepo.WithByType(records[i].DefaultDownload))
|
||||
|
||||
@@ -231,7 +231,9 @@ func recoverDaemonJson(src string, fileOp files.FileOp) error {
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = cmd.Exec("systemctl restart docker")
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,16 @@ func (u *SSHService) OperateSSH(operation string) error {
|
||||
if operation == "enable" || operation == "disable" {
|
||||
serviceName += ".service"
|
||||
}
|
||||
if operation == "stop" {
|
||||
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName)
|
||||
if err != nil {
|
||||
if strings.Contains(stdout, "alias name or linked unit file") {
|
||||
|
||||
@@ -352,7 +352,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
}
|
||||
website.Proxy = proxy
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port)
|
||||
}
|
||||
}
|
||||
@@ -1667,6 +1667,9 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
|
||||
}
|
||||
if req.SNI {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
|
||||
if req.ProxySSLName != "" {
|
||||
location.UpdateDirective("proxy_ssl_name", []string{req.ProxySSLName})
|
||||
}
|
||||
} else {
|
||||
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
|
||||
}
|
||||
@@ -1749,6 +1752,9 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
|
||||
if directive.GetName() == "proxy_ssl_server_name" {
|
||||
proxyConfig.SNI = directive.GetParameters()[0] == "on"
|
||||
}
|
||||
if directive.GetName() == "proxy_ssl_name" {
|
||||
proxyConfig.ProxySSLName = directive.GetParameters()[0]
|
||||
}
|
||||
}
|
||||
res = append(res, proxyConfig)
|
||||
}
|
||||
|
||||
@@ -217,15 +217,14 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if req.Domains != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
for _, domain := range domainArray {
|
||||
if !common.IsValidDomain(domain) {
|
||||
err = buserr.WithName("ErrDomainFormat", domain)
|
||||
return nil, err
|
||||
} else {
|
||||
if ipAddress := net.ParseIP(domain); ipAddress == nil {
|
||||
domains = append(domains, domain)
|
||||
} else {
|
||||
ips = append(ips, ipAddress)
|
||||
if ipAddress := net.ParseIP(domain); ipAddress == nil {
|
||||
if !common.IsValidDomain(domain) {
|
||||
err = buserr.WithName("ErrDomainFormat", domain)
|
||||
return nil, err
|
||||
}
|
||||
domains = append(domains, domain)
|
||||
} else {
|
||||
ips = append(ips, ipAddress)
|
||||
}
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
|
||||
@@ -181,6 +181,13 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{}, disableLog bool) {
|
||||
if disableLog {
|
||||
return
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap(msgKey, params))
|
||||
}
|
||||
|
||||
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
var (
|
||||
err error
|
||||
@@ -272,11 +279,13 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
defer logFile.Close()
|
||||
logger := log.New(logFile, "", log.LstdFlags)
|
||||
legoLogger.Logger = logger
|
||||
startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)})
|
||||
if websiteSSL.Provider == constant.DNSAccount {
|
||||
startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
|
||||
if !apply.DisableLog {
|
||||
startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)})
|
||||
if websiteSSL.Provider == constant.DNSAccount {
|
||||
startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
|
||||
}
|
||||
legoLogger.Logger.Println(startMsg)
|
||||
}
|
||||
legoLogger.Logger.Println(startMsg)
|
||||
resource, err := client.ObtainSSL(domains, privateKey)
|
||||
if err != nil {
|
||||
handleError(websiteSSL, err)
|
||||
@@ -296,7 +305,7 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
websiteSSL.Type = cert.Issuer.CommonName
|
||||
websiteSSL.Organization = cert.Issuer.Organization[0]
|
||||
websiteSSL.Status = constant.SSLReady
|
||||
legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}))
|
||||
printSSLLog(logger, "ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}, apply.DisableLog)
|
||||
saveCertificateFile(websiteSSL, logger)
|
||||
|
||||
if websiteSSL.ExecShell {
|
||||
@@ -304,11 +313,11 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellStart"))
|
||||
printSSLLog(logger, "ExecShellStart", nil, apply.DisableLog)
|
||||
if err = cmd.ExecShellWithTimeOut(websiteSSL.Shell, workDir, logger, 30*time.Minute); err != nil {
|
||||
legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrExecShell", map[string]interface{}{"err": err.Error()}))
|
||||
printSSLLog(logger, "ErrExecShell", map[string]interface{}{"err": err.Error()}, apply.DisableLog)
|
||||
} else {
|
||||
legoLogger.Logger.Println(i18n.GetMsgByKey("ExecShellSuccess"))
|
||||
printSSLLog(logger, "ExecShellSuccess", nil, apply.DisableLog)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,9 +329,9 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID))
|
||||
if len(websites) > 0 {
|
||||
for _, website := range websites {
|
||||
legoLogger.Logger.Println(i18n.GetMsgWithMap("ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain}))
|
||||
printSSLLog(logger, "ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain}, apply.DisableLog)
|
||||
if err := createPemFile(website, *websiteSSL); err != nil {
|
||||
legoLogger.Logger.Println(i18n.GetMsgWithMap("ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()}))
|
||||
printSSLLog(logger, "ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()}, apply.DisableLog)
|
||||
}
|
||||
}
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
@@ -330,10 +339,10 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
return
|
||||
}
|
||||
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
|
||||
legoLogger.Logger.Println(i18n.GetMsgByKey(constant.ErrSSLApply))
|
||||
printSSLLog(logger, constant.ErrSSLApply, nil, apply.DisableLog)
|
||||
return
|
||||
}
|
||||
legoLogger.Logger.Println(i18n.GetMsgByKey("ApplyWebSiteSSLSuccess"))
|
||||
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil, apply.DisableLog)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
@@ -13,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/i18n"
|
||||
@@ -276,7 +277,7 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
|
||||
server.UpdateRoot(rootIndex)
|
||||
server.UpdatePHPProxy([]string{website.Proxy}, "")
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython:
|
||||
proxy := fmt.Sprintf("http://127.0.0.1:%d", runtime.Port)
|
||||
server.UpdateRootProxy([]string{proxy})
|
||||
}
|
||||
@@ -1001,20 +1002,23 @@ func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetSystemSSL() (bool, uint) {
|
||||
func GetSystemSSL() (bool, bool, uint) {
|
||||
sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load service ssl from setting failed, err: %v", err)
|
||||
return false, 0
|
||||
return false, false, 0
|
||||
}
|
||||
if sslSetting.Value == "enable" {
|
||||
sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID"))
|
||||
idValue, _ := strconv.Atoi(sslID.Value)
|
||||
if idValue > 0 {
|
||||
return true, uint(idValue)
|
||||
if idValue <= 0 {
|
||||
return false, false, 0
|
||||
}
|
||||
|
||||
auto, _ := settingRepo.Get(settingRepo.WithByKey("AutoRestart"))
|
||||
return true, auto.Value == "enable", uint(idValue)
|
||||
}
|
||||
return false, 0
|
||||
return false, false, 0
|
||||
}
|
||||
|
||||
func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error {
|
||||
@@ -1033,7 +1037,7 @@ func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error {
|
||||
return buserr.WithErr(constant.ErrSSLApply, err)
|
||||
}
|
||||
}
|
||||
enable, sslID := GetSystemSSL()
|
||||
enable, auto, sslID := GetSystemSSL()
|
||||
if enable && sslID == websiteSSL.ID {
|
||||
fileOp := files.NewFileOp()
|
||||
secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret")
|
||||
@@ -1045,6 +1049,9 @@ func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error {
|
||||
global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
|
||||
return err
|
||||
}
|
||||
if auto {
|
||||
_, _ = cmd.Exec("systemctl restart 1panel.service")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ const (
|
||||
RuntimeUnhealthy = "unhealthy"
|
||||
RuntimeCreating = "creating"
|
||||
|
||||
RuntimePHP = "php"
|
||||
RuntimeNode = "node"
|
||||
RuntimeJava = "java"
|
||||
RuntimeGo = "go"
|
||||
RuntimePHP = "php"
|
||||
RuntimeNode = "node"
|
||||
RuntimeJava = "java"
|
||||
RuntimeGo = "go"
|
||||
RuntimePython = "python"
|
||||
|
||||
RuntimeProxyUnix = "unix"
|
||||
RuntimeProxyTcp = "tcp"
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/service"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func NewSSLJob() *ssl {
|
||||
}
|
||||
|
||||
func (ssl *ssl) Run() {
|
||||
systemSSLEnable, sslID := service.GetSystemSSL()
|
||||
systemSSLEnable, auto, sslID := service.GetSystemSSL()
|
||||
sslRepo := repo.NewISSLRepo()
|
||||
sslService := service.NewIWebsiteSSLService()
|
||||
sslList, _ := sslRepo.List()
|
||||
@@ -51,7 +52,8 @@ func (ssl *ssl) Run() {
|
||||
}
|
||||
} else {
|
||||
if err := sslService.ObtainSSL(request.WebsiteSSLApply{
|
||||
ID: s.ID,
|
||||
ID: s.ID,
|
||||
DisableLog: true,
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error())
|
||||
continue
|
||||
@@ -69,6 +71,9 @@ func (ssl *ssl) Run() {
|
||||
global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error())
|
||||
continue
|
||||
}
|
||||
if auto {
|
||||
_, _ = cmd.Exec("systemctl restart 1panel.service")
|
||||
}
|
||||
}
|
||||
global.LOG.Infof("The SSL certificate for the [%s] domain has been successfully updated", s.PrimaryDomain)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -126,11 +128,24 @@ func handleSnapStatus() {
|
||||
}
|
||||
|
||||
func handleCronjobStatus() {
|
||||
_ = global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constant.StatusFailed,
|
||||
"message": "the task was interrupted due to the restart of the 1panel service",
|
||||
}).Error
|
||||
var jobRecords []model.JobRecords
|
||||
_ = global.DB.Where("status = ?", constant.StatusWaiting).Find(&jobRecords).Error
|
||||
for _, record := range jobRecords {
|
||||
err := global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constant.StatusFailed,
|
||||
"message": "the task was interrupted due to the restart of the 1panel service",
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to update job ID: %v, Error:%v", record.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var cronjob *model.Cronjob
|
||||
_ = global.DB.Where("id = ?", record.CronjobID).First(&cronjob).Error
|
||||
handleCronJobAlert(cronjob)
|
||||
}
|
||||
}
|
||||
|
||||
func loadLocalDir() {
|
||||
@@ -210,3 +225,20 @@ func initDir() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
if cronjob.Type == "snapshot" {
|
||||
return
|
||||
}
|
||||
pushAlert := dto.PushAlert{
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
Param: cronjob.Type,
|
||||
}
|
||||
err := xpack.PushAlert(pushAlert)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("cronjob alert push failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,9 @@ func Init() {
|
||||
migrations.AddClam,
|
||||
migrations.AddClamStatus,
|
||||
migrations.AddAlertMenu,
|
||||
migrations.AddComposeColumn,
|
||||
|
||||
migrations.AddAutoRestart,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -314,3 +314,23 @@ var AddAlertMenu = &gormigrate.Migration{
|
||||
return tx.Model(&model.Setting{}).Where("key", "XpackHideMenu").Updates(map[string]interface{}{"value": string(data)}).Error
|
||||
},
|
||||
}
|
||||
|
||||
var AddComposeColumn = &gormigrate.Migration{
|
||||
ID: "20240906-add-compose-command",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.Compose{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddAutoRestart = &gormigrate.Migration{
|
||||
ID: "20241021-add-auto-restart",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&model.Setting{Key: "AutoRestart", Value: "enable"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ var AddTablePHPExtensions = &gormigrate.Migration{
|
||||
if err := tx.Create(&model.PHPExtensions{Name: "WordPress", Extensions: "exif,igbinary,imagick,intl,zip,apcu,memcached,opcache,redis,bc,image,shmop,mysqli,pdo_mysql,gd"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.PHPExtensions{Name: "BookStack", Extensions: "gd,dom,iconv,mbstring,mysqlnd,openssl,pdo,pdo_mysql,tokenizer,xml"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&model.PHPExtensions{Name: "Flarum", Extensions: "curl,gd,pdo_mysql,mysqli,bz2,exif,yaf,imap"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ var whiteUrlList = map[string]struct{}{
|
||||
"/api/v1/logs/operation": {},
|
||||
"/api/v1/logs/login": {},
|
||||
"/api/v1/auth/logout": {},
|
||||
"/api/v1/dashboard/current": {},
|
||||
|
||||
"/api/v1/apps/installed/loadport": {},
|
||||
"/api/v1/apps/installed/check": {},
|
||||
|
||||
@@ -37,6 +37,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
|
||||
settingRouter.POST("/snapshot/status", baseApi.LoadSnapShotStatus)
|
||||
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
|
||||
settingRouter.POST("/snapshot/size", baseApi.LoadSnapshotSize)
|
||||
settingRouter.POST("/snapshot/import", baseApi.ImportSnapshot)
|
||||
settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot)
|
||||
settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot)
|
||||
|
||||
@@ -27,12 +27,16 @@ func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
|
||||
if len(scType) == 0 {
|
||||
scType = "Standard"
|
||||
}
|
||||
mode := loadParamFromVars("mode", vars)
|
||||
if len(mode) == 0 {
|
||||
mode = "virtual hosted"
|
||||
}
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(false),
|
||||
S3ForcePathStyle: aws.Bool(mode == "path"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -204,7 +204,7 @@ func SudoHandleCmd() string {
|
||||
|
||||
func Which(name string) bool {
|
||||
stdout, err := Execf("which %s", name)
|
||||
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
|
||||
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -29,10 +29,17 @@ type FirewallClient interface {
|
||||
}
|
||||
|
||||
func NewFirewallClient() (FirewallClient, error) {
|
||||
if _, err := os.Stat("/usr/sbin/firewalld"); err == nil {
|
||||
_, firewalldErr := os.Stat("/usr/sbin/firewalld")
|
||||
_, ufwErr := os.Stat("/usr/sbin/ufw")
|
||||
|
||||
if firewalldErr == nil && ufwErr == nil {
|
||||
return nil, buserr.New("firewalld and ufw both found, only one firewall should be active")
|
||||
}
|
||||
|
||||
if firewalldErr == nil {
|
||||
return client.NewFirewalld()
|
||||
}
|
||||
if _, err := os.Stat("/usr/sbin/ufw"); err == nil {
|
||||
if ufwErr == nil {
|
||||
return client.NewUfw()
|
||||
}
|
||||
return nil, buserr.New(constant.ErrFirewall)
|
||||
|
||||
@@ -35,6 +35,7 @@ var repeatKeys = map[string]struct {
|
||||
"include": {},
|
||||
"sub_filter": {},
|
||||
"add_header": {},
|
||||
"error_page": {},
|
||||
}
|
||||
|
||||
func IsRepeatKey(key string) bool {
|
||||
|
||||
@@ -234,11 +234,13 @@ func (l *Location) AddSubFilter(subFilters map[string]string) {
|
||||
}
|
||||
l.UpdateDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
|
||||
l.UpdateDirective("sub_filter_once", []string{"off"})
|
||||
l.UpdateDirective("sub_filter_types", []string{"*"})
|
||||
}
|
||||
|
||||
func (l *Location) RemoveSubFilter() {
|
||||
l.RemoveDirective("sub_filter", []string{})
|
||||
l.RemoveDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
|
||||
l.RemoveDirective("sub_filter_once", []string{"off"})
|
||||
l.RemoveDirective("sub_filter_types", []string{"*"})
|
||||
l.Replaces = nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnspod"
|
||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
@@ -72,6 +73,7 @@ const (
|
||||
NameCom DnsType = "NameCom"
|
||||
Godaddy DnsType = "Godaddy"
|
||||
TencentCloud DnsType = "TencentCloud"
|
||||
HuaweiCloud DnsType = "HuaweiCloud"
|
||||
)
|
||||
|
||||
type DNSParam struct {
|
||||
@@ -84,6 +86,7 @@ type DNSParam struct {
|
||||
APIUser string `json:"apiUser"`
|
||||
APISecret string `json:"apiSecret"`
|
||||
SecretID string `json:"secretID"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -166,6 +169,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
|
||||
tencentCloudConfig.PollingInterval = pollingInterval
|
||||
tencentCloudConfig.TTL = ttl
|
||||
p, err = tencentcloud.NewDNSProviderConfig(tencentCloudConfig)
|
||||
case HuaweiCloud:
|
||||
huaweiCloudConfig := huaweicloud.NewDefaultConfig()
|
||||
huaweiCloudConfig.AccessKeyID = param.AccessKey
|
||||
huaweiCloudConfig.SecretAccessKey = param.SecretKey
|
||||
huaweiCloudConfig.Region = param.Region
|
||||
huaweiCloudConfig.PropagationTimeout = propagationTimeout
|
||||
huaweiCloudConfig.PollingInterval = pollingInterval
|
||||
huaweiCloudConfig.TTL = int32(ttl)
|
||||
p, err = huaweicloud.NewDNSProviderConfig(huaweiCloudConfig)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -187,7 +199,7 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
|
||||
dns01.CondOption(len(nameservers) > 0,
|
||||
dns01.AddRecursiveNameservers(nameservers)),
|
||||
dns01.CondOption(websiteSSL.SkipDNS,
|
||||
dns01.DisableCompletePropagationRequirement()),
|
||||
dns01.DisableAuthoritativeNssPropagationRequirement()),
|
||||
dns01.AddDNSTimeout(10*time.Minute),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
@@ -39,3 +40,23 @@ func LoadXpuInfo() []interface{} {
|
||||
func StartClam(startClam model.Clam, isUpdate bool) (int, error) {
|
||||
return 0, buserr.New(constant.ErrXpackNotFound)
|
||||
}
|
||||
|
||||
func CreateAlert(createAlert dto.CreateOrUpdateAlert) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateAlert(updateAlert dto.CreateOrUpdateAlert) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteAlert(alertBase dto.AlertBase) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAlert(alertBase dto.AlertBase) uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
func PushAlert(pushAlert dto.PushAlert) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ location ^~ /test {
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
add_header Cache-Control no-cache;
|
||||
proxy_ssl_server_name off;
|
||||
proxy_ssl_name $proxy_host;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -41,7 +41,7 @@
|
||||
"highlight.js": "^11.9.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"md-editor-v3": "^2.11.3",
|
||||
"monaco-editor": "^0.51.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^1.6.1",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -241,6 +241,8 @@ export namespace Container {
|
||||
path: string;
|
||||
containers: Array<ComposeContainer>;
|
||||
expand: boolean;
|
||||
envStr: string;
|
||||
env: Array<string>;
|
||||
}
|
||||
export interface ComposeContainer {
|
||||
name: string;
|
||||
@@ -254,6 +256,8 @@ export namespace Container {
|
||||
file: string;
|
||||
path: string;
|
||||
template: number;
|
||||
env: Array<string>;
|
||||
envStr: string;
|
||||
}
|
||||
export interface ComposeOperation {
|
||||
name: string;
|
||||
@@ -265,6 +269,8 @@ export namespace Container {
|
||||
name: string;
|
||||
path: string;
|
||||
content: string;
|
||||
env: Array<string>;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface TemplateCreate {
|
||||
|
||||
@@ -30,6 +30,9 @@ export namespace Cronjob {
|
||||
retainCopies: number;
|
||||
status: string;
|
||||
secret: string;
|
||||
hasAlert: boolean;
|
||||
alertCount: number;
|
||||
alertTitle: string;
|
||||
}
|
||||
export interface CronjobCreate {
|
||||
name: string;
|
||||
|
||||
@@ -29,6 +29,7 @@ export namespace Setting {
|
||||
bindAddress: string;
|
||||
ssl: string;
|
||||
sslType: string;
|
||||
autoRestart: string;
|
||||
allowIPs: string;
|
||||
bindDomain: string;
|
||||
securityEntrance: string;
|
||||
@@ -134,7 +135,7 @@ export namespace Setting {
|
||||
description: string;
|
||||
status: string;
|
||||
message: string;
|
||||
createdAt: DateTimeFormats;
|
||||
created_at: DateTimeFormats;
|
||||
version: string;
|
||||
interruptStep: string;
|
||||
recoverStatus: string;
|
||||
@@ -145,6 +146,11 @@ export namespace Setting {
|
||||
lastRollbackedAt: string;
|
||||
secret: string;
|
||||
}
|
||||
export interface SnapshotFile {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
export interface SnapshotStatus {
|
||||
panel: string;
|
||||
panelInfo: string;
|
||||
@@ -169,11 +175,13 @@ export namespace Setting {
|
||||
assigneeName: string;
|
||||
productPro: string;
|
||||
trial: boolean;
|
||||
offline: boolean;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
export interface LicenseStatus {
|
||||
productPro: string;
|
||||
trial: boolean;
|
||||
status: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,9 @@ export namespace Toolbox {
|
||||
spec: string;
|
||||
specObj: Cronjob.SpecObj;
|
||||
description: string;
|
||||
hasAlert: boolean;
|
||||
alertCount: number;
|
||||
alertTitle: string;
|
||||
}
|
||||
export interface ClamCreate {
|
||||
name: string;
|
||||
|
||||
@@ -373,6 +373,8 @@ export namespace Website {
|
||||
content?: string;
|
||||
proxyAddress?: string;
|
||||
proxyProtocol?: string;
|
||||
sni: boolean;
|
||||
proxySSLName: string;
|
||||
}
|
||||
|
||||
export interface ProxReplace {
|
||||
|
||||
@@ -43,7 +43,7 @@ export const containerRename = (params: Container.ContainerRename) => {
|
||||
return http.post(`/containers/rename`, params);
|
||||
};
|
||||
export const containerOperator = (params: Container.ContainerOperate) => {
|
||||
return http.post(`/containers/operate`, params);
|
||||
return http.post(`/containers/operate`, params, TimeoutEnum.T_60S);
|
||||
};
|
||||
export const containerPrune = (params: Container.ContainerPrune) => {
|
||||
return http.post<Container.ContainerPruneReport>(`/containers/prune`, params);
|
||||
|
||||
@@ -196,6 +196,9 @@ export const snapshotRollback = (param: Setting.SnapshotRecover) => {
|
||||
export const searchSnapshotPage = (param: SearchWithPage) => {
|
||||
return http.post<ResPage<Setting.SnapshotInfo>>(`/settings/snapshot/search`, param);
|
||||
};
|
||||
export const loadSnapshotSize = (param: SearchWithPage) => {
|
||||
return http.post<Array<Setting.SnapshotFile>>(`/settings/snapshot/size`, param);
|
||||
};
|
||||
|
||||
// upgrade
|
||||
export const loadUpgradeInfo = () => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -107,7 +107,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { CheckAppInstalled, InstalledOp } from '@/api/modules/app';
|
||||
import router from '@/routers';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
@@ -125,21 +125,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.appKey,
|
||||
(val) => {
|
||||
key.value = val;
|
||||
onCheck();
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.appName,
|
||||
(val) => {
|
||||
name.value = val;
|
||||
onCheck();
|
||||
},
|
||||
);
|
||||
|
||||
let key = ref('');
|
||||
let name = ref('');
|
||||
|
||||
@@ -250,4 +235,8 @@ onMounted(() => {
|
||||
name.value = props.appName;
|
||||
onCheck();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
onCheck,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -95,15 +95,14 @@ const logSearch = reactive({
|
||||
const handleClose = () => {
|
||||
terminalSocket.value?.send('close conn');
|
||||
open.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
|
||||
watch(logVisible, (val) => {
|
||||
@@ -214,6 +213,9 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fullScreen {
|
||||
border: none;
|
||||
}
|
||||
.selectWidth {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<body>
|
||||
<div>
|
||||
<el-row type="flex" justify="center">
|
||||
<h1>{{ loadErrInfo() }}</h1>
|
||||
</el-row>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<el-row type="flex" justify="center"><span>nginx</span></el-row>
|
||||
</div>
|
||||
</body>
|
||||
<div>
|
||||
<el-row type="flex" justify="center">
|
||||
<h1>{{ loadErrInfo() }}</h1>
|
||||
</el-row>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<el-row type="flex" justify="center"><span>nginx</span></el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const paths = ref<string[]>([]);
|
||||
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
|
||||
const selectRow = ref();
|
||||
const selectRow = ref({ path: '', name: '' });
|
||||
const rowRefs = ref();
|
||||
const popoverVisible = ref(false);
|
||||
const newFolder = ref();
|
||||
@@ -183,12 +183,12 @@ const selectFile = () => {
|
||||
|
||||
const closePage = () => {
|
||||
popoverVisible.value = false;
|
||||
selectRow.value = {};
|
||||
selectRow.value = { path: '', name: '' };
|
||||
};
|
||||
|
||||
const openPage = () => {
|
||||
popoverVisible.value = true;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? props.path || '/' : '';
|
||||
rowName.value = '';
|
||||
};
|
||||
|
||||
@@ -216,7 +216,7 @@ const open = async (row: File.File) => {
|
||||
}
|
||||
await search(req);
|
||||
}
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
};
|
||||
|
||||
@@ -230,7 +230,7 @@ const jump = async (index: number) => {
|
||||
}
|
||||
path = path || '/';
|
||||
req.path = path;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
await search(req);
|
||||
popoverVisible.value = true;
|
||||
@@ -286,7 +286,7 @@ const cancelFolder = (row: any) => {
|
||||
data.value.shift();
|
||||
row.isCreate = false;
|
||||
disBtn.value = false;
|
||||
selectRow.value = {};
|
||||
selectRow.value.path = props.dir ? req.path : '';
|
||||
rowName.value = '';
|
||||
newFolder.value = '';
|
||||
};
|
||||
|
||||
@@ -108,6 +108,9 @@ const showBack = computed(() => {
|
||||
.el-button + .el-button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.el-button-group .el-button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container_form {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:before-close="handleClose"
|
||||
:size="globalStore.isFullScreen ? '100%' : '50%'"
|
||||
>
|
||||
<template #header>
|
||||
@@ -21,10 +22,10 @@
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
import screenfull from 'screenfull';
|
||||
import { GlobalStore } from '@/store';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
@@ -45,19 +46,22 @@ const open = ref(false);
|
||||
const config = ref();
|
||||
const em = defineEmits(['close']);
|
||||
|
||||
const handleClose = (search: boolean) => {
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
em('close', search);
|
||||
em('close', false);
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
|
||||
watch(open, (val) => {
|
||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||
});
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
|
||||
const acceptParams = (props: LogProps) => {
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<template>
|
||||
<div v-loading="isLoading">
|
||||
<div v-loading="firstLoading">
|
||||
<div v-if="defaultButton">
|
||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
|
||||
{{ $t('commons.button.watch') }}
|
||||
</el-checkbox>
|
||||
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
||||
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="logs.length === 0">
|
||||
{{ $t('file.download') }}
|
||||
</el-button>
|
||||
<span v-if="$slots.button" class="ml-2.5">
|
||||
<slot name="button"></slot>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2.5">
|
||||
<highlightjs
|
||||
ref="editorRef"
|
||||
class="editor-main"
|
||||
language="JavaScript"
|
||||
:autodetect="false"
|
||||
:code="content"
|
||||
></highlightjs>
|
||||
<div class="log-container" ref="logContainer" @scroll="onScroll">
|
||||
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
|
||||
<div
|
||||
v-for="(log, index) in visibleLogs"
|
||||
:key="startIndex + index"
|
||||
class="log-item"
|
||||
:style="{ top: `${(startIndex + index) * logHeight}px` }"
|
||||
>
|
||||
<span>{{ log }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import { ReadByLine } from '@/api/modules/files';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const editorRef = ref();
|
||||
<script lang="ts" setup>
|
||||
import { ReadByLine } from '@/api/modules/files';
|
||||
import { ref, computed, onMounted, watch, nextTick, reactive } from 'vue';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
|
||||
interface LogProps {
|
||||
id?: number;
|
||||
type: string;
|
||||
name?: string;
|
||||
tail?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as () => LogProps | null,
|
||||
@@ -63,46 +63,6 @@ const props = defineProps({
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const data = ref({
|
||||
enable: false,
|
||||
content: '',
|
||||
path: '',
|
||||
});
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const tailLog = ref(false);
|
||||
const content = ref('');
|
||||
const end = ref(false);
|
||||
const scrollerElement = ref<HTMLElement | null>(null);
|
||||
const minPage = ref(1);
|
||||
const maxPage = ref(1);
|
||||
const logs = ref([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
const readReq = reactive({
|
||||
id: 0,
|
||||
type: '',
|
||||
name: '',
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
latest: false,
|
||||
});
|
||||
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
|
||||
|
||||
const loading = ref(props.loading);
|
||||
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newLoading) => {
|
||||
loading.value = newLoading;
|
||||
},
|
||||
);
|
||||
|
||||
const changeLoading = () => {
|
||||
loading.value = !loading.value;
|
||||
emit('update:loading', loading.value);
|
||||
};
|
||||
|
||||
const stopSignals = [
|
||||
'docker-compose up failed!',
|
||||
'docker-compose up successful!',
|
||||
@@ -113,123 +73,63 @@ const stopSignals = [
|
||||
'image push failed!',
|
||||
'image push successful!',
|
||||
];
|
||||
|
||||
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
|
||||
const tailLog = ref(false);
|
||||
const loading = ref(props.loading);
|
||||
const readReq = reactive({
|
||||
id: 0,
|
||||
type: '',
|
||||
name: '',
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
latest: false,
|
||||
});
|
||||
const isLoading = ref(false);
|
||||
const end = ref(false);
|
||||
const lastLogs = ref([]);
|
||||
const maxPage = ref(0);
|
||||
const minPage = ref(0);
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const logPath = ref('');
|
||||
|
||||
const getContent = (pre: boolean) => {
|
||||
if (isLoading.value) {
|
||||
return;
|
||||
}
|
||||
emit('update:isReading', true);
|
||||
readReq.id = props.config.id;
|
||||
readReq.type = props.config.type;
|
||||
readReq.name = props.config.name;
|
||||
if (readReq.page < 1) {
|
||||
readReq.page = 1;
|
||||
}
|
||||
isLoading.value = true;
|
||||
ReadByLine(readReq).then((res) => {
|
||||
if (!end.value && res.data.end) {
|
||||
lastLogs.value = [...logs.value];
|
||||
}
|
||||
const firstLoading = ref(false);
|
||||
const logs = ref<string[]>([]);
|
||||
const logContainer = ref<HTMLElement | null>(null);
|
||||
const logHeight = 20;
|
||||
const logCount = ref(0);
|
||||
const totalHeight = computed(() => logHeight * logCount.value);
|
||||
const containerHeight = ref(500);
|
||||
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight)); // 计算可见日志条数(容器高度 / 日志高度)
|
||||
const startIndex = ref(0);
|
||||
|
||||
data.value = res.data;
|
||||
if (res.data.lines && res.data.lines.length > 0) {
|
||||
res.data.lines = res.data.lines.map((line) =>
|
||||
line.replace(/\\u(\w{4})/g, function (match, grp) {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
}),
|
||||
);
|
||||
const newLogs = res.data.lines;
|
||||
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
|
||||
readReq.page++;
|
||||
}
|
||||
const visibleLogs = computed(() => {
|
||||
return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
|
||||
});
|
||||
|
||||
if (
|
||||
readReq.type == 'php' &&
|
||||
logs.value.length > 0 &&
|
||||
newLogs.length > 0 &&
|
||||
newLogs[newLogs.length - 1] === logs.value[logs.value.length - 1]
|
||||
) {
|
||||
isLoading.value = false;
|
||||
const onScroll = () => {
|
||||
if (logContainer.value) {
|
||||
const scrollTop = logContainer.value.scrollTop;
|
||||
if (scrollTop == 0) {
|
||||
readReq.page = minPage.value - 1;
|
||||
if (readReq.page < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopSignals.some((signal) => newLogs[newLogs.length - 1].endsWith(signal))) {
|
||||
onCloseLog();
|
||||
}
|
||||
if (end.value) {
|
||||
if ((logs.value.length = 0)) {
|
||||
logs.value = newLogs;
|
||||
} else {
|
||||
logs.value = pre ? [...newLogs, ...lastLogs.value] : [...lastLogs.value, ...newLogs];
|
||||
}
|
||||
} else {
|
||||
if ((logs.value.length = 0)) {
|
||||
logs.value = newLogs;
|
||||
} else {
|
||||
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
|
||||
}
|
||||
}
|
||||
minPage.value = readReq.page;
|
||||
getContent(true);
|
||||
}
|
||||
|
||||
end.value = res.data.end;
|
||||
content.value = logs.value.join('\n');
|
||||
|
||||
emit('update:hasContent', content.value !== '');
|
||||
nextTick(() => {
|
||||
if (pre) {
|
||||
if (scrollerElement.value.scrollHeight > 2000) {
|
||||
scrollerElement.value.scrollTop = 2000;
|
||||
}
|
||||
} else {
|
||||
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
if (readReq.latest) {
|
||||
readReq.page = res.data.total;
|
||||
readReq.latest = false;
|
||||
maxPage.value = res.data.total;
|
||||
minPage.value = res.data.total;
|
||||
}
|
||||
if (logs.value && logs.value.length > 3000) {
|
||||
logs.value.splice(0, readReq.pageSize);
|
||||
if (minPage.value > 1) {
|
||||
minPage.value--;
|
||||
}
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
});
|
||||
startIndex.value = Math.floor(scrollTop / logHeight);
|
||||
}
|
||||
};
|
||||
|
||||
function throttle<T extends (...args: any[]) => any>(func: T, limit: number): (...args: Parameters<T>) => void {
|
||||
let inThrottle: boolean;
|
||||
let lastFunc: ReturnType<typeof setTimeout>;
|
||||
let lastRan: number;
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
lastRan = Date.now();
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
} else {
|
||||
clearTimeout(lastFunc);
|
||||
lastFunc = setTimeout(() => {
|
||||
if (Date.now() - lastRan >= limit) {
|
||||
func.apply(this, args);
|
||||
lastRan = Date.now();
|
||||
}
|
||||
}, limit - (Date.now() - lastRan));
|
||||
}
|
||||
};
|
||||
}
|
||||
const changeLoading = () => {
|
||||
loading.value = !loading.value;
|
||||
emit('update:loading', loading.value);
|
||||
};
|
||||
|
||||
const throttledGetContent = throttle(getContent, 3000);
|
||||
|
||||
const search = () => {
|
||||
throttledGetContent(false);
|
||||
const onDownload = async () => {
|
||||
changeLoading();
|
||||
downloadFile(logPath.value);
|
||||
changeLoading();
|
||||
};
|
||||
|
||||
const changeTail = (fromOutSide: boolean) => {
|
||||
@@ -238,36 +138,127 @@ const changeTail = (fromOutSide: boolean) => {
|
||||
}
|
||||
if (tailLog.value) {
|
||||
timer = setInterval(() => {
|
||||
search();
|
||||
getContent(false);
|
||||
}, 1000 * 3);
|
||||
} else {
|
||||
onCloseLog();
|
||||
}
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
changeLoading();
|
||||
downloadFile(data.value.path);
|
||||
changeLoading();
|
||||
const clearLog = (): void => {
|
||||
logs.value = [];
|
||||
readReq.page = 1;
|
||||
lastLogs.value = [];
|
||||
};
|
||||
|
||||
const getContent = async (pre: boolean) => {
|
||||
if (isLoading.value) {
|
||||
return;
|
||||
}
|
||||
readReq.id = props.config.id;
|
||||
readReq.type = props.config.type;
|
||||
readReq.name = props.config.name;
|
||||
if (readReq.page < 1) {
|
||||
readReq.page = 1;
|
||||
}
|
||||
isLoading.value = true;
|
||||
emit('update:isReading', true);
|
||||
|
||||
const res = await ReadByLine(readReq);
|
||||
logPath.value = res.data.path;
|
||||
firstLoading.value = false;
|
||||
|
||||
if (!end.value && res.data.end) {
|
||||
lastLogs.value = [...logs.value];
|
||||
}
|
||||
if (res.data.lines && res.data.lines.length > 0) {
|
||||
res.data.lines = res.data.lines.map((line) =>
|
||||
line.replace(/\\u(\w{4})/g, function (match, grp) {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
}),
|
||||
);
|
||||
const newLogs = res.data.lines;
|
||||
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
|
||||
readReq.page++;
|
||||
}
|
||||
if (
|
||||
readReq.type == 'php' &&
|
||||
logs.value.length > 0 &&
|
||||
newLogs.length > 0 &&
|
||||
newLogs[newLogs.length - 1] === logs.value[logs.value.length - 1]
|
||||
) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopSignals.some((signal) => newLogs[newLogs.length - 1].endsWith(signal))) {
|
||||
onCloseLog();
|
||||
}
|
||||
if (end.value) {
|
||||
if ((logs.value.length = 0)) {
|
||||
logs.value = newLogs;
|
||||
} else {
|
||||
logs.value = pre ? [...newLogs, ...lastLogs.value] : [...lastLogs.value, ...newLogs];
|
||||
}
|
||||
} else {
|
||||
if ((logs.value.length = 0)) {
|
||||
logs.value = newLogs;
|
||||
} else {
|
||||
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (pre) {
|
||||
logContainer.value.scrollTop = 2000;
|
||||
} else {
|
||||
logContainer.value.scrollTop = totalHeight.value;
|
||||
containerHeight.value = logContainer.value.getBoundingClientRect().height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logCount.value = logs.value.length;
|
||||
end.value = res.data.end;
|
||||
emit('update:hasContent', logs.value.length > 0);
|
||||
if (readReq.latest) {
|
||||
readReq.page = res.data.total;
|
||||
readReq.latest = false;
|
||||
maxPage.value = res.data.total;
|
||||
minPage.value = res.data.total;
|
||||
}
|
||||
if (logs.value && logs.value.length > 3000) {
|
||||
if (pre) {
|
||||
logs.value.splice(logs.value.length - readReq.pageSize, readReq.pageSize);
|
||||
if (maxPage.value > 1) {
|
||||
maxPage.value--;
|
||||
}
|
||||
} else {
|
||||
logs.value.splice(0, readReq.pageSize);
|
||||
if (minPage.value > 1) {
|
||||
minPage.value++;
|
||||
}
|
||||
}
|
||||
}
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
const onCloseLog = async () => {
|
||||
emit('update:isReading', false);
|
||||
tailLog.value = false;
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
isLoading.value = false;
|
||||
emit('update:isReading', false);
|
||||
};
|
||||
|
||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
||||
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight;
|
||||
}
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newLoading) => {
|
||||
loading.value = newLoading;
|
||||
},
|
||||
);
|
||||
|
||||
function isScrolledToTop(element: HTMLElement): boolean {
|
||||
return element.scrollTop === 0;
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
const init = async () => {
|
||||
if (props.config.tail) {
|
||||
tailLog.value = props.config.tail;
|
||||
} else {
|
||||
@@ -277,60 +268,49 @@ const init = () => {
|
||||
changeTail(false);
|
||||
}
|
||||
readReq.latest = true;
|
||||
search();
|
||||
|
||||
nextTick(() => {});
|
||||
await getContent(false);
|
||||
};
|
||||
|
||||
const clearLog = (): void => {
|
||||
content.value = '';
|
||||
};
|
||||
|
||||
const initCodemirror = () => {
|
||||
onMounted(async () => {
|
||||
firstLoading.value = true;
|
||||
await init();
|
||||
nextTick(() => {
|
||||
if (editorRef.value) {
|
||||
scrollerElement.value = editorRef.value.$el as HTMLElement;
|
||||
scrollerElement.value.addEventListener('scroll', function () {
|
||||
if (tailLog.value) {
|
||||
return;
|
||||
}
|
||||
if (isScrolledToBottom(scrollerElement.value)) {
|
||||
if (maxPage.value > 1) {
|
||||
readReq.page = maxPage.value;
|
||||
}
|
||||
search();
|
||||
}
|
||||
if (isScrolledToTop(scrollerElement.value)) {
|
||||
readReq.page = minPage.value - 1;
|
||||
if (readReq.page < 1) {
|
||||
return;
|
||||
}
|
||||
minPage.value = readReq.page;
|
||||
throttledGetContent(true);
|
||||
}
|
||||
});
|
||||
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
|
||||
hljsDom.style['min-height'] = '500px';
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = totalHeight.value;
|
||||
containerHeight.value = logContainer.value.getBoundingClientRect().height;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
onCloseLog();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initCodemirror();
|
||||
init();
|
||||
});
|
||||
|
||||
defineExpose({ changeTail, onDownload, clearLog });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.editor-main {
|
||||
height: calc(100vh - 480px);
|
||||
|
||||
<style scoped>
|
||||
.log-container {
|
||||
height: calc(100vh - 405px);
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
background-color: #1e1e1e;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.log-spacer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
color: #f5f5f5;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-item span {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: #cccccc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,8 +211,8 @@ const onRecover = async (row: File.File) => {
|
||||
currentRow.value = row;
|
||||
if (type.value !== 'app' && type.value !== 'website') {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('commons.msg.backupHelper', [name.value + '( ' + detailName.value + ' )']),
|
||||
i18n.global.t('commons.button.backup'),
|
||||
i18n.global.t('commons.msg.recoverHelper', [row.name]),
|
||||
i18n.global.t('commons.button.recover'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
|
||||
@@ -164,6 +164,10 @@ export const DNSTypes = [
|
||||
label: i18n.global.t('website.tencentCloud'),
|
||||
value: 'TencentCloud',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('website.huaweicloud'),
|
||||
value: 'HuaweiCloud',
|
||||
},
|
||||
{
|
||||
label: 'DNSPod (' + i18n.global.t('ssl.deprecated') + ')',
|
||||
value: 'DnsPod',
|
||||
|
||||
@@ -56,8 +56,8 @@ const message = {
|
||||
copy: 'Copy',
|
||||
random: 'Random',
|
||||
uninstall: 'Uninstall',
|
||||
fullscreen: 'Fullscreen',
|
||||
quitFullscreen: 'Quit Fullscreen',
|
||||
fullscreen: 'WebsiteFullscreen',
|
||||
quitFullscreen: 'Quit WebsiteFullscreen',
|
||||
update: 'Edit',
|
||||
showAll: 'Show All',
|
||||
hideSome: 'Hide Some',
|
||||
@@ -575,8 +575,13 @@ const message = {
|
||||
container: {
|
||||
create: 'Create',
|
||||
edit: 'Edit container',
|
||||
updateContainerHelper:
|
||||
'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?',
|
||||
updateHelper1: 'Detected that this container comes from the app store. Please note the following two points:',
|
||||
updateHelper2:
|
||||
'1. The current modifications will not be synchronized to the installed applications in the app store.',
|
||||
updateHelper3:
|
||||
'2. If you modify the application on the installed page, the currently edited content will become invalid.',
|
||||
updateHelper4:
|
||||
'Editing the container requires rebuilding, and any non-persistent data will be lost. Do you want to continue?',
|
||||
containerList: 'Container list',
|
||||
operatorHelper: '{0} will be performed on the following container, Do you want to continue?',
|
||||
operatorAppHelper:
|
||||
@@ -643,7 +648,7 @@ const message = {
|
||||
containerFromAppHelper:
|
||||
'Detected that this container originates from the app store. App operations may cause current edits to be invalidated.',
|
||||
containerFromAppHelper1:
|
||||
'Click the `Settings` button in the installed applications list to enter the editing page and modify the container name.',
|
||||
'Click the [Param] button in the installed applications list to enter the editing page and modify the container name.',
|
||||
command: 'Command',
|
||||
console: 'Console Interaction',
|
||||
tty: 'TTY (-t)',
|
||||
@@ -654,6 +659,8 @@ const message = {
|
||||
privileged: 'Privileged',
|
||||
privilegedHelper:
|
||||
'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!',
|
||||
editComposeHelper:
|
||||
'Note: The environment variables set will be written to the 1panel.env file by default.\nIf you want to use these parameters in the container, you also need to manually add an env_file reference in the compose file.',
|
||||
|
||||
upgradeHelper: 'Repository Name/Image Name: Image Version',
|
||||
upgradeWarning2:
|
||||
@@ -729,6 +736,8 @@ const message = {
|
||||
urlWarning: 'The URL prefix does not need to include http:// or https://. Please modify.',
|
||||
|
||||
network: 'Network',
|
||||
networkHelper:
|
||||
'Deleting the 1panel-network container network will affect the normal use of some applications and runtime environments. Do you want to continue?',
|
||||
createNetwork: 'Create',
|
||||
networkName: 'Name',
|
||||
driver: 'Driver',
|
||||
@@ -768,12 +777,14 @@ const message = {
|
||||
composeHelper:
|
||||
'The composition created through 1Panel editor or template will be saved in the {0}/docker/compose directory.',
|
||||
deleteFile: 'Delete file',
|
||||
allDelete: 'Permanently Delete',
|
||||
deleteComposeHelper:
|
||||
'Delete all files in the {0} directory, including persistent files in this directory. Please proceed with caution!',
|
||||
'Delete all files related to container compose, including configuration files and persistent files. Please proceed with caution!',
|
||||
deleteCompose: '" Delete this composition.',
|
||||
apps: 'Apps',
|
||||
local: 'Local',
|
||||
createCompose: 'Create',
|
||||
composeDirectory: 'Compose Directory',
|
||||
template: 'Template',
|
||||
composeTemplate: 'Compose template',
|
||||
createComposeTemplate: 'Create',
|
||||
@@ -781,11 +792,16 @@ const message = {
|
||||
content: 'Content',
|
||||
contentEmpty: 'Compose content cannot be empty, please enter and try again!',
|
||||
containerNumber: 'Container number',
|
||||
containerStatus: 'Container Status',
|
||||
exited: 'Exited',
|
||||
running: 'Running',
|
||||
down: 'Down',
|
||||
up: 'Up',
|
||||
composeDetailHelper:
|
||||
'The compose is created external to 1Panel. The start and stop operations are not supported.',
|
||||
composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?',
|
||||
composeDownHelper:
|
||||
'This will stop and remove all containers and networks under the {0} compose. Do you want to continue?',
|
||||
|
||||
setting: 'Setting',
|
||||
operatorStatusHelper: 'This action will {0} Docker service, do you want to continue?',
|
||||
@@ -926,6 +942,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: 'Upload Request Expiration Time(Hours)',
|
||||
unitHours: 'Unit: Hours',
|
||||
alertTitle: 'Planned Task - {0} 「{1}」 Task Failure Alert',
|
||||
},
|
||||
monitor: {
|
||||
monitor: 'Monitor',
|
||||
@@ -1123,6 +1140,8 @@ const message = {
|
||||
clamLog: 'Scan Logs',
|
||||
freshClam: 'Update Virus Definitions',
|
||||
freshClamLog: 'Update Virus Definitions Logs',
|
||||
alertHelper: 'Professional version supports scheduled scan and SMS alert',
|
||||
alertTitle: 'Virus scan task 「{0}」 detected infected file alert',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@@ -1410,6 +1429,7 @@ const message = {
|
||||
LOCAL: 'Server Disks',
|
||||
OSS: 'Ali OSS',
|
||||
S3: 'Amazon S3',
|
||||
mode: 'Mode',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@@ -1505,8 +1525,11 @@ const message = {
|
||||
bindDomain: 'Bind Domain',
|
||||
unBindDomain: 'Unbind domain',
|
||||
panelSSL: 'Panel SSL',
|
||||
panelSSLHelper:
|
||||
'After the automatic renewal of the panel SSL, you need to manually restart the 1Panel service for the changes to take effect.',
|
||||
sslAutoRestart: 'Restart 1Panel service after certificate auto-renewal',
|
||||
sslChangeHelper1:
|
||||
'Currently, automatic restart of 1Panel service is not selected. The certificate auto-renewal will not take effect immediately and will still require a manual restart of 1Panel.',
|
||||
sslChangeHelper2:
|
||||
'The 1Panel service will automatically restart after setting the panel SSL. Do you want to continue?',
|
||||
unBindDomainHelper:
|
||||
'The action of unbinding a domain name may cause system insecurity. Do you want to continue?',
|
||||
bindDomainHelper:
|
||||
@@ -1905,7 +1928,7 @@ const message = {
|
||||
applySSL: 'Certificate Application',
|
||||
SSLList: 'Certificate List',
|
||||
createDnsAccount: 'DNS Account',
|
||||
aliyun: 'Aliyun DNS',
|
||||
aliyun: 'Aliyun',
|
||||
manual: 'Manual parsing',
|
||||
key: 'Key',
|
||||
check: 'View',
|
||||
@@ -2128,6 +2151,7 @@ const message = {
|
||||
sni: 'Origin SNI',
|
||||
sniHelper:
|
||||
"When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.",
|
||||
huaweicloud: 'Huawei Cloud',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Short tag support',
|
||||
@@ -2378,6 +2402,8 @@ const message = {
|
||||
javaDirHelper: 'The directory must contain jar files, subdirectories are also acceptable',
|
||||
goHelper: 'Please provide a complete start command, for example: go run main.go or ./main',
|
||||
goDirHelper: 'The directory must contain go files or binary files, subdirectories are also acceptable',
|
||||
pythonHelper:
|
||||
'Please fill in the complete startup command, for example: pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
@@ -2448,6 +2474,14 @@ const message = {
|
||||
manage: 'Management',
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
isAlert: 'Alert',
|
||||
alertCount: 'Alert Count',
|
||||
clamHelper: 'Trigger SMS alert when scanning infected files',
|
||||
cronJobHelper: 'Trigger SMS alert when scheduled task execution fails',
|
||||
licenseHelper: 'Professional version supports SMS alert',
|
||||
alertCountHelper: 'Maximum daily alarm frequency',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -55,8 +55,8 @@ const message = {
|
||||
copy: '復製',
|
||||
random: '隨機密碼',
|
||||
uninstall: '卸載',
|
||||
fullscreen: '全屏',
|
||||
quitFullscreen: '退出全屏',
|
||||
fullscreen: '網頁全屏',
|
||||
quitFullscreen: '退出網頁全屏',
|
||||
update: '編輯',
|
||||
showAll: '顯示所有',
|
||||
hideSome: '隱藏部分',
|
||||
@@ -557,7 +557,10 @@ const message = {
|
||||
container: {
|
||||
create: '創建容器',
|
||||
edit: '編輯容器',
|
||||
updateContainerHelper: '容器編輯需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
updateHelper1: '檢測到該容器來源於應用商店,請注意以下兩點:',
|
||||
updateHelper2: '1. 當前修改內容不會同步到應用商店的已安裝應用。',
|
||||
updateHelper3: '2. 如果在已安裝頁面修改應用,當前編輯的部分內容將失效。',
|
||||
updateHelper4: '編輯容器需要重建,任何未持久化的數據將丟失,是否繼續?',
|
||||
containerList: '容器列表',
|
||||
operatorHelper: '將對以下容器進行 {0} 操作,是否繼續?',
|
||||
operatorAppHelper:
|
||||
@@ -619,7 +622,7 @@ const message = {
|
||||
inputIpv6: '請輸入 IPv6 地址',
|
||||
|
||||
containerFromAppHelper: '檢測到該容器來源於應用商店,應用操作可能會導致當前編輯失效',
|
||||
containerFromAppHelper1: '在已安裝應用程式列表點擊 `參數` 按鈕,進入編輯頁面即可修改容器名稱。',
|
||||
containerFromAppHelper1: '在已安裝應用程式列表點擊 [參數] 按鈕,進入編輯頁面即可修改容器名稱。',
|
||||
command: '命令',
|
||||
console: '控製臺交互',
|
||||
tty: '偽終端 ( -t )',
|
||||
@@ -629,6 +632,8 @@ const message = {
|
||||
emptyUser: '為空時,將使用容器默認的用戶登錄',
|
||||
privileged: '特權模式',
|
||||
privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!',
|
||||
editComposeHelper:
|
||||
'注意:設置的環境變數會默認寫入 1panel.env 文件。\n若需在容器中使用這些參數,還需在 compose 文件中手動添加 env_file 引用。',
|
||||
|
||||
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
||||
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
@@ -705,6 +710,7 @@ const message = {
|
||||
urlWarning: '路徑前綴不需要添加 http:// 或 https://,請修改',
|
||||
|
||||
network: '網絡',
|
||||
networkHelper: '刪除 1panel-network 容器網絡將影響部分應用和運行環境的正常使用,是否繼續?',
|
||||
createNetwork: '創建網絡',
|
||||
networkName: '網絡名',
|
||||
driver: '模式',
|
||||
@@ -739,11 +745,13 @@ const message = {
|
||||
composePathHelper: '配置文件保存路徑: {0}',
|
||||
composeHelper: '通過 1Panel 編輯或者模版創建的編排,將保存在 {0}/docker/compose 路徑下',
|
||||
deleteFile: '刪除文件',
|
||||
deleteComposeHelper: '刪除 {0} 目录下所有文件,包括該文件下的持久化文件等,請謹慎操作!',
|
||||
allDelete: '徹底刪除',
|
||||
deleteComposeHelper: '刪除容器編排的所有文件,包括配置文件和持久化文件,請謹慎操作!',
|
||||
deleteCompose: '" 刪除此編排',
|
||||
apps: '應用商店',
|
||||
local: '本地',
|
||||
createCompose: '創建編排',
|
||||
composeDirectory: '編排目錄',
|
||||
template: '模版',
|
||||
composeTemplate: '編排模版',
|
||||
createComposeTemplate: '創建編排模版',
|
||||
@@ -751,10 +759,14 @@ const message = {
|
||||
content: '內容',
|
||||
contentEmpty: '編排內容不能為空,請輸入後重試!',
|
||||
containerNumber: '容器數量',
|
||||
containerStatus: '容器狀態',
|
||||
exited: '已停止',
|
||||
running: '運行中',
|
||||
down: '刪除',
|
||||
up: '啟動',
|
||||
composeDetailHelper: '該 compose 為 1Panel 編排外部創建。暫不支持啟停操作。',
|
||||
composeOperatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?',
|
||||
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網絡,是否繼續?',
|
||||
|
||||
setting: '配置',
|
||||
operatorStatusHelper: '此操作將{0}Docker 服務,是否繼續?',
|
||||
@@ -881,6 +893,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: '上傳請求過期時間(小時)',
|
||||
unitHours: '單位:小時',
|
||||
alertTitle: '計畫任務-{0}「{1}」任務失敗告警',
|
||||
},
|
||||
monitor: {
|
||||
monitor: '監控',
|
||||
@@ -1061,6 +1074,8 @@ const message = {
|
||||
clamLog: '掃描日誌',
|
||||
freshClam: '病毒庫刷新配置',
|
||||
freshClamLog: '病毒庫刷新日誌',
|
||||
alertHelper: '專業版支持定時掃描和短信告警功能',
|
||||
alertTitle: '病毒掃描「{0}」任務检测到感染文件告警',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@@ -1323,6 +1338,7 @@ const message = {
|
||||
LOCAL: '服務器磁盤',
|
||||
OSS: '阿裏雲 OSS',
|
||||
S3: '亞馬遜 S3 雲存儲',
|
||||
mode: '模式',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@@ -1456,7 +1472,9 @@ const message = {
|
||||
bindDomain: '域名綁定',
|
||||
unBindDomain: '域名解綁',
|
||||
panelSSL: '面板 SSL',
|
||||
panelSSLHelper: '面板 SSL 自动续期后需要手动重启 1Panel 服务才可生效',
|
||||
sslAutoRestart: '證書自動續期後重啟 1Panel 服務',
|
||||
sslChangeHelper1: '當前未勾選自動重啟 1Panel 服務,證書自動續期後不會立即生效,仍需手動重啟 1Panel。',
|
||||
sslChangeHelper2: '設置面板 SSL 後將自動重啟 1Panel 服務,是否繼續?',
|
||||
unBindDomainHelper: '解除域名綁定可能造成系統不安全,是否繼續?',
|
||||
bindDomainHelper: '設置域名綁定後,僅能通過設置中域名訪問 1Panel 服務',
|
||||
bindDomainHelper1: '綁定域名為空時,則取消域名綁定',
|
||||
@@ -1769,7 +1787,7 @@ const message = {
|
||||
applySSL: '證書申請',
|
||||
SSLList: '證書列表',
|
||||
createDnsAccount: 'DNS賬戶',
|
||||
aliyun: '阿裏雲DNS',
|
||||
aliyun: '阿裏雲',
|
||||
manual: '手動解析',
|
||||
key: '密鑰',
|
||||
check: '查看',
|
||||
@@ -1978,6 +1996,7 @@ const message = {
|
||||
website404Helper: '網站 404 錯誤頁僅支援 PHP 運行環境網站和靜態網站',
|
||||
sni: '回源 SNI',
|
||||
sniHelper: '反代後端為 https 的時候可能需要設置回源 SNI,具體需要看 CDN 服務商文檔',
|
||||
huaweicloud: '華為雲',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短標簽支持',
|
||||
@@ -2205,6 +2224,8 @@ const message = {
|
||||
javaDirHelper: '目錄中要包含 jar 包,子目錄中包含也可',
|
||||
goHelper: '請填寫完整啟動命令,例如:go run main.go 或 ./main',
|
||||
goDirHelper: '目錄中要包含 go 文件或者二進制文件,子目錄中包含也可',
|
||||
pythonHelper:
|
||||
'請填入完整啟動指令,例如:pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
|
||||
},
|
||||
process: {
|
||||
pid: '進程ID',
|
||||
@@ -2273,6 +2294,14 @@ const message = {
|
||||
manage: '管理',
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
alertCount: '告警次數',
|
||||
clamHelper: '掃描到感染檔案時觸發簡訊告警',
|
||||
cronJobHelper: '定時任務執行失敗時將觸發簡訊告警',
|
||||
licenseHelper: '專業版支持簡訊告警功能',
|
||||
alertCountHelper: '每日最大告警次數',
|
||||
},
|
||||
};
|
||||
export default {
|
||||
...fit2cloudTwLocale,
|
||||
|
||||
@@ -55,8 +55,8 @@ const message = {
|
||||
copy: '复制',
|
||||
random: '随机密码',
|
||||
uninstall: '卸载',
|
||||
fullscreen: '全屏',
|
||||
quitFullscreen: '退出全屏',
|
||||
fullscreen: '网页全屏',
|
||||
quitFullscreen: '退出网页全屏',
|
||||
update: '编辑',
|
||||
showAll: '显示所有',
|
||||
hideSome: '隐藏部分',
|
||||
@@ -559,7 +559,10 @@ const message = {
|
||||
container: {
|
||||
create: '创建容器',
|
||||
edit: '编辑容器',
|
||||
updateContainerHelper: '容器编辑需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
updateHelper1: '检测到该容器来源于应用商店,请注意以下两点:',
|
||||
updateHelper2: '1. 当前修改内容不会同步到应用商店的已安装应用。',
|
||||
updateHelper3: '2. 如果在已安装页面修改应用,当前编辑的部分内容将失效。',
|
||||
updateHelper4: '编辑容器需要重建,任何未持久化的数据将丢失,是否继续?',
|
||||
containerList: '容器列表',
|
||||
operatorHelper: '将对以下容器进行 {0} 操作,是否继续?',
|
||||
operatorAppHelper:
|
||||
@@ -622,7 +625,7 @@ const message = {
|
||||
inputIpv6: '请输入 ipv6 地址',
|
||||
|
||||
containerFromAppHelper: '检测到该容器来源于应用商店,应用操作可能会导致当前编辑失效',
|
||||
containerFromAppHelper1: '在已安装应用列表点击 `参数` 按钮,进入编辑页面即可修改容器名称。',
|
||||
containerFromAppHelper1: '在应用商店的已安装页面,点击 [参数] 按钮,进入编辑页面修改容器名称。',
|
||||
command: '命令',
|
||||
console: '控制台交互',
|
||||
tty: '伪终端 ( -t )',
|
||||
@@ -632,7 +635,8 @@ const message = {
|
||||
emptyUser: '为空时,将使用容器默认的用户登录',
|
||||
privileged: '特权模式',
|
||||
privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!',
|
||||
|
||||
editComposeHelper:
|
||||
'注意:设置的环境变量会默认写入 1panel.env 文件。\n如需在容器中使用这些参数,还需在 compose 文件中手动添加 env_file 引用。',
|
||||
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
||||
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
oldImage: '当前镜像',
|
||||
@@ -708,6 +712,7 @@ const message = {
|
||||
urlWarning: '路径前缀不需要添加 http:// 或 https://, 请修改',
|
||||
|
||||
network: '网络',
|
||||
networkHelper: '删除 1panel-network 容器网络将影响部分应用和运行环境的正常使用,是否继续?',
|
||||
createNetwork: '创建网络',
|
||||
networkName: '网络名',
|
||||
driver: '模式',
|
||||
@@ -736,17 +741,18 @@ const message = {
|
||||
mirrorHelper:
|
||||
'当存在多个加速器时,需要换行显示,例: \nhttp://xxxxxx.m.daocloud.io \nhttps://xxxxxx.mirror.aliyuncs.com',
|
||||
registrieHelper: '当存在多个私有仓库时,需要换行显示,例:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: '编排',
|
||||
fromChangeHelper: '切换来源将清空当前已编辑内容,是否继续?',
|
||||
composePathHelper: '配置文件保存路径: {0}',
|
||||
composeHelper: '通过 1Panel 编辑或者模版创建的编排,将保存在 {0}/docker/compose 路径下',
|
||||
deleteFile: '删除文件',
|
||||
deleteComposeHelper: '删除 {0} 目录下所有文件,包括该文件下的持久化文件等,请谨慎操作!',
|
||||
allDelete: '彻底删除',
|
||||
deleteComposeHelper: '删除容器编排的所有文件,包括配置文件和持久化文件,请谨慎操作!',
|
||||
deleteCompose: '" 删除此编排',
|
||||
apps: '应用商店',
|
||||
local: '本地',
|
||||
createCompose: '创建编排',
|
||||
composeDirectory: '编排目录',
|
||||
template: '模版',
|
||||
composeTemplate: '编排模版',
|
||||
createComposeTemplate: '创建编排模版',
|
||||
@@ -754,10 +760,14 @@ const message = {
|
||||
content: '内容',
|
||||
contentEmpty: '编排内容不能为空,请输入后重试!',
|
||||
containerNumber: '容器数量',
|
||||
containerStatus: '容器状态',
|
||||
exited: '已停止',
|
||||
running: '运行中',
|
||||
down: '删除',
|
||||
up: '启动',
|
||||
composeDetailHelper: '该 compose 为 1Panel 编排外部创建。暂不支持启停操作。',
|
||||
composeOperatorHelper: '将对 {0} 进行 {1} 操作,是否继续?',
|
||||
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络,是否继续?',
|
||||
|
||||
setting: '配置',
|
||||
operatorStatusHelper: '此操作将{0}Docker 服务,是否继续?',
|
||||
@@ -884,6 +894,7 @@ const message = {
|
||||
|
||||
requestExpirationTime: '上传请求过期时间(小时)',
|
||||
unitHours: '单位:小时',
|
||||
alertTitle: '计划任务-{0}「 {1} 」任务失败告警',
|
||||
},
|
||||
monitor: {
|
||||
monitor: '监控',
|
||||
@@ -1065,6 +1076,8 @@ const message = {
|
||||
clamLog: '扫描日志',
|
||||
freshClam: '病毒库刷新配置',
|
||||
freshClamLog: '病毒库刷新日志',
|
||||
alertHelper: '专业版支持定时扫描和短信告警功能',
|
||||
alertTitle: '病毒扫描「 {0} 」任务检测到感染文件告警',
|
||||
},
|
||||
},
|
||||
logs: {
|
||||
@@ -1327,6 +1340,7 @@ const message = {
|
||||
LOCAL: '服务器磁盘',
|
||||
OSS: '阿里云 OSS',
|
||||
S3: '亚马逊 S3 云存储',
|
||||
mode: '模式',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@@ -1460,7 +1474,9 @@ const message = {
|
||||
bindDomain: '域名绑定',
|
||||
unBindDomain: '域名解绑',
|
||||
panelSSL: '面板 SSL',
|
||||
panelSSLHelper: '面板 SSL 自动续期后需要手动重启 1Panel 服务才可生效',
|
||||
sslAutoRestart: '证书自动续期后重启 1Panel 服务',
|
||||
sslChangeHelper1: '当前未勾选自动重启 1Panel 服务,证书自动续期后不会立即生效,仍需手动重启 1Panel。',
|
||||
sslChangeHelper2: '设置面板 SSL 后将自动重启 1Panel 服务,是否继续?',
|
||||
unBindDomainHelper: '解除域名绑定可能造成系统不安全,是否继续?',
|
||||
bindDomainHelper: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务',
|
||||
bindDomainHelper1: '绑定域名为空时,则取消域名绑定',
|
||||
@@ -1484,7 +1500,6 @@ const message = {
|
||||
mfaInterval: '刷新时间(秒)',
|
||||
mfaTitleHelper: '用于区分不同 1Panel 主机,修改后请重新扫描或手动添加密钥信息!',
|
||||
mfaIntervalHelper: '修改刷新时间后,请重新扫描或手动添加密钥信息!',
|
||||
sslChangeHelper: 'https 设置修改需要重启服务,是否继续?',
|
||||
sslDisable: '禁用',
|
||||
sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?',
|
||||
noAuthSetting: '未认证设置',
|
||||
@@ -1773,7 +1788,7 @@ const message = {
|
||||
applySSL: '证书申请',
|
||||
SSLList: '证书列表',
|
||||
createDnsAccount: 'DNS账户',
|
||||
aliyun: '阿里云DNS',
|
||||
aliyun: '阿里云',
|
||||
manual: '手动解析',
|
||||
key: '密钥',
|
||||
check: '查看',
|
||||
@@ -1982,6 +1997,7 @@ const message = {
|
||||
website404Helper: '网站 404 错误页仅支持 PHP 运行环境网站和静态网站',
|
||||
sni: '回源 SNI',
|
||||
sniHelper: '反代后端为 https 的时候可能需要设置回源 SNI,具体需要看 CDN 服务商文档',
|
||||
huaweicloud: '华为云',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短标签支持',
|
||||
@@ -2210,6 +2226,8 @@ const message = {
|
||||
javaDirHelper: '目录中要包含 jar 包,子目录中包含也可',
|
||||
goHelper: '请填写完整启动命令,例如:go run main.go 或 ./main',
|
||||
goDirHelper: '目录中要包含 go 文件或者二进制文件,子目录中包含也可',
|
||||
pythonHelper:
|
||||
'请填写完整启动命令,例如:pip install -r requirements.txt && python manage.py runserver 0.0.0.0:5000',
|
||||
},
|
||||
process: {
|
||||
pid: '进程ID',
|
||||
@@ -2278,6 +2296,14 @@ const message = {
|
||||
manage: '管理',
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
alertCount: '告警次数',
|
||||
clamHelper: '扫描到感染文件时触发短信告警',
|
||||
cronJobHelper: '定时任务执行失败时将触发短信告警',
|
||||
licenseHelper: '专业版支持短信告警功能',
|
||||
alertCountHelper: '每日最大告警次数',
|
||||
},
|
||||
};
|
||||
export default {
|
||||
...fit2cloudZhLocale,
|
||||
|
||||
@@ -64,7 +64,7 @@ const webSiteRouter = {
|
||||
hidden: true,
|
||||
component: () => import('@/views/website/runtime/java/index.vue'),
|
||||
meta: {
|
||||
activeMenu: '/websites/runtimes/java',
|
||||
activeMenu: '/websites/runtimes/php',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
@@ -74,7 +74,17 @@ const webSiteRouter = {
|
||||
hidden: true,
|
||||
component: () => import('@/views/website/runtime/go/index.vue'),
|
||||
meta: {
|
||||
activeMenu: '/websites/runtimes/go',
|
||||
activeMenu: '/websites/runtimes/php',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/websites/runtimes/python',
|
||||
name: 'python',
|
||||
hidden: true,
|
||||
component: () => import('@/views/website/runtime/python/index.vue'),
|
||||
meta: {
|
||||
activeMenu: '/websites/runtimes/php',
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface GlobalState {
|
||||
defaultNetwork: string;
|
||||
|
||||
isProductPro: boolean;
|
||||
isTrial: boolean;
|
||||
productProExpires: number;
|
||||
|
||||
errStatus: string;
|
||||
|
||||
@@ -38,6 +38,7 @@ const GlobalStore = defineStore({
|
||||
defaultNetwork: 'all',
|
||||
|
||||
isProductPro: false,
|
||||
isTrial: false,
|
||||
productProExpires: 0,
|
||||
|
||||
errStatus: '',
|
||||
|
||||
@@ -20,6 +20,11 @@ body,
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.el-switch--small,.is-checked .el-switch__core::after {
|
||||
margin-left: 12px !important;
|
||||
.el-switch--small.is-checked .el-switch__core::after {
|
||||
margin-left: -13px;
|
||||
}
|
||||
|
||||
.el-alert__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function loadProductProFromDB() {
|
||||
res.data.status === 'Enable' || res.data.status === 'Lost01' || res.data.status === 'Lost02';
|
||||
if (globalStore.isProductPro) {
|
||||
globalStore.productProExpires = Number(res.data.productPro);
|
||||
globalStore.isTrial = res.data.trial;
|
||||
}
|
||||
}
|
||||
switchTheme();
|
||||
@@ -81,6 +82,7 @@ export async function getXpackSettingForTheme() {
|
||||
res.data.status === 'Enable' || res.data.status === 'Lost01' || res.data.status === 'Lost02';
|
||||
if (globalStore.isProductPro) {
|
||||
globalStore.productProExpires = Number(res.data.productPro);
|
||||
globalStore.isTrial = res.data.trial;
|
||||
}
|
||||
if (!globalStore.isProductPro) {
|
||||
globalStore.isProductPro = false;
|
||||
|
||||
@@ -224,16 +224,11 @@ const search = async (req: App.AppReq) => {
|
||||
const openInstall = (app: App.App) => {
|
||||
switch (app.type) {
|
||||
case 'php':
|
||||
router.push({ path: '/websites/runtimes/php' });
|
||||
break;
|
||||
case 'node':
|
||||
router.push({ path: '/websites/runtimes/node' });
|
||||
break;
|
||||
case 'java':
|
||||
router.push({ path: '/websites/runtimes/java' });
|
||||
break;
|
||||
case 'go':
|
||||
router.push({ path: '/websites/runtimes/go' });
|
||||
case 'python':
|
||||
router.push({ path: '/websites/runtimes/' + app.type });
|
||||
break;
|
||||
default:
|
||||
const params = {
|
||||
|
||||
@@ -142,16 +142,11 @@ const toLink = (link: string) => {
|
||||
const openInstall = () => {
|
||||
switch (app.value.type) {
|
||||
case 'php':
|
||||
router.push({ path: '/websites/runtimes/php' });
|
||||
break;
|
||||
case 'node':
|
||||
router.push({ path: '/websites/runtimes/node' });
|
||||
break;
|
||||
case 'java':
|
||||
router.push({ path: '/websites/runtimes/java' });
|
||||
break;
|
||||
case 'go':
|
||||
router.push({ path: '/websites/runtimes/go' });
|
||||
case 'python':
|
||||
router.push({ path: '/websites/runtimes/' + app.value.type });
|
||||
break;
|
||||
default:
|
||||
const params = {
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
:title="$t('app.composeDiff')"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="60%"
|
||||
width="90%"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="22" :offset="1">
|
||||
<el-text type="warning">{{ $t('app.diffHelper') }}</el-text>
|
||||
<div ref="container" class="compose-diff"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div>
|
||||
<el-text type="warning">{{ $t('app.diffHelper') }}</el-text>
|
||||
<div ref="container" class="compose-diff"></div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="width: 100%; height: calc(100vh - 376px)"
|
||||
style="width: 100%; height: calc(100vh - 400px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
@@ -86,6 +86,26 @@
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.env')" prop="envStr">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('container.tagHelper')"
|
||||
:rows="3"
|
||||
v-model="form.envStr"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="input-help whitespace-break-spaces">{{ $t('container.editComposeHelper') }}</span>
|
||||
<codemirror
|
||||
v-model="form.envFileContent"
|
||||
:autofocus="true"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
></codemirror>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -143,6 +163,9 @@ const form = reactive({
|
||||
path: '',
|
||||
file: '',
|
||||
template: null as number,
|
||||
env: [],
|
||||
envStr: '',
|
||||
envFileContent: `env_file:\n - 1panel.env`,
|
||||
});
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.imageName],
|
||||
@@ -163,6 +186,8 @@ const acceptParams = (): void => {
|
||||
form.path = '';
|
||||
form.file = '';
|
||||
form.template = null;
|
||||
form.envStr = '';
|
||||
form.env = [];
|
||||
loadTemplates();
|
||||
loadPath();
|
||||
isStartReading.value = false;
|
||||
@@ -242,6 +267,9 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
MsgError(i18n.global.t('container.contentEmpty'));
|
||||
return;
|
||||
}
|
||||
if (form.envStr) {
|
||||
form.env = form.envStr.split('\n');
|
||||
}
|
||||
loading.value = true;
|
||||
await testCompose(form)
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<el-form ref="deleteForm" v-loading="loading">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="deleteFile" :label="$t('container.deleteFile')" />
|
||||
<span class="input-help">
|
||||
{{ $t('container.deleteComposeHelper', [loadComposeDir()]) }}
|
||||
<span class="input-help whitespace-break-spaces">
|
||||
{{ $t('container.deleteComposeHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -64,21 +64,12 @@ const acceptParams = async (prop: DialogProps) => {
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const loadComposeDir = () => {
|
||||
const parts = composePath.value.split('/');
|
||||
if (parts.length <= 2) {
|
||||
return '/';
|
||||
}
|
||||
const parentDirectory = parts.slice(0, -1).join('/');
|
||||
return parentDirectory;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
let params = {
|
||||
name: composeName.value,
|
||||
path: composePath.value,
|
||||
operation: 'down',
|
||||
operation: 'delete',
|
||||
withFile: deleteFile.value,
|
||||
};
|
||||
await composeOperator(params)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-tag effect="dark" type="success">{{ composeName }}</el-tag>
|
||||
</div>
|
||||
<div v-if="createdBy === '1Panel'" style="margin-left: 50px">
|
||||
<el-button link type="primary" @click="onComposeOperate('start')">
|
||||
<el-button link type="primary" @click="onComposeOperate('up')">
|
||||
{{ $t('container.start') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
@@ -143,7 +143,6 @@ const dialogContainerLogRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
|
||||
const emit = defineEmits<{ (e: 'back'): void }>();
|
||||
interface DialogProps {
|
||||
createdBy: string;
|
||||
name: string;
|
||||
@@ -260,15 +259,18 @@ const onOperate = async (op: string) => {
|
||||
};
|
||||
|
||||
const onComposeOperate = async (operation: string) => {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('container.composeOperatorHelper', [composeName.value, i18n.global.t('container.' + operation)]),
|
||||
i18n.global.t('container.' + operation),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
).then(async () => {
|
||||
let mes =
|
||||
operation === 'down'
|
||||
? i18n.global.t('container.composeDownHelper', [composeName.value])
|
||||
: i18n.global.t('container.composeOperatorHelper', [
|
||||
composeName.value,
|
||||
i18n.global.t('container.' + operation),
|
||||
]);
|
||||
ElMessageBox.confirm(mes, i18n.global.t('container.' + operation), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
let params = {
|
||||
name: composeName.value,
|
||||
path: composePath.value,
|
||||
@@ -280,11 +282,7 @@ const onComposeOperate = async (operation: string) => {
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
if (operation === 'down') {
|
||||
emit('back');
|
||||
} else {
|
||||
search();
|
||||
}
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
|
||||
@@ -9,20 +9,56 @@
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('commons.button.edit')" :resource="name" :back="handleClose" />
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="width: 100%; height: calc(100vh - 175px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="content"
|
||||
/>
|
||||
<div v-loading="loading" style="padding-bottom: 20px">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form ref="formRef" @submit.prevent label-position="top">
|
||||
<el-form-item>
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
:style="{ width: '100%', height: `calc(100vh - ${loadHeight()})` }"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="content"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div v-if="createdBy === '1Panel'">
|
||||
<el-form-item
|
||||
:label="$t('container.env')"
|
||||
prop="environmentStr"
|
||||
v-if="createdBy === '1Panel'"
|
||||
>
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('container.tagHelper')"
|
||||
:rows="3"
|
||||
v-model="environmentStr"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="input-help whitespace-break-spaces">
|
||||
{{ $t('container.editComposeHelper') }}
|
||||
</span>
|
||||
<codemirror
|
||||
v-model="envFileContent"
|
||||
:autofocus="true"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
></codemirror>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -45,26 +81,38 @@ import { composeUpdate } from '@/api/modules/container';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { ElForm } from 'element-plus';
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
const loading = ref(false);
|
||||
const composeVisible = ref(false);
|
||||
const extensions = [javascript(), oneDark];
|
||||
const path = ref();
|
||||
const content = ref();
|
||||
const name = ref();
|
||||
const environmentStr = ref();
|
||||
const environmentEnv = ref();
|
||||
const createdBy = ref();
|
||||
const envFileContent = `env_file:\n - 1panel.env`;
|
||||
|
||||
const onSubmitEdit = async () => {
|
||||
const param = {
|
||||
name: name.value,
|
||||
path: path.value,
|
||||
content: content.value,
|
||||
env: environmentStr.value,
|
||||
createdBy: createdBy.value,
|
||||
};
|
||||
if (environmentStr.value != undefined) {
|
||||
param.env = environmentStr.value.split('\n');
|
||||
}
|
||||
loading.value = true;
|
||||
await composeUpdate(param)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
composeVisible.value = false;
|
||||
emit('search');
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
@@ -75,13 +123,22 @@ interface DialogProps {
|
||||
name: string;
|
||||
path: string;
|
||||
content: string;
|
||||
env: Array<string>;
|
||||
envStr: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
const loadHeight = () => {
|
||||
return createdBy.value === '1Panel' ? '300px' : '200px';
|
||||
};
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
composeVisible.value = true;
|
||||
path.value = props.path;
|
||||
name.value = props.name;
|
||||
content.value = props.content;
|
||||
createdBy.value = props.createdBy;
|
||||
environmentEnv.value = props.env || [];
|
||||
environmentStr.value = environmentEnv.value.join('\n');
|
||||
};
|
||||
const handleClose = () => {
|
||||
composeVisible.value = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div v-show="isOnDetail">
|
||||
<ComposeDetail @back="backList" ref="composeDetailRef" />
|
||||
<ComposeDetail ref="composeDetailRef" />
|
||||
</div>
|
||||
<el-card v-if="dockerStatus != 'Running'" class="mask-prompt">
|
||||
<span>{{ $t('container.serviceUnavailable') }}</span>
|
||||
@@ -10,20 +10,6 @@
|
||||
</el-card>
|
||||
|
||||
<LayoutContent v-if="!isOnDetail" :title="$t('container.compose')" :class="{ mask: dockerStatus != 'Running' }">
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<span class="flx-align-center">
|
||||
<span>{{ $t('container.composeHelper', [baseDir]) }}</span>
|
||||
<el-button type="primary" link @click="toFolder">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<div class="flex justify-between gap-2 flex-wrap sm:flex-row">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@@ -48,6 +34,7 @@
|
||||
:label="$t('commons.table.name')"
|
||||
width="170"
|
||||
prop="name"
|
||||
sortable
|
||||
fix
|
||||
show-overflow-tooltip
|
||||
>
|
||||
@@ -64,12 +51,22 @@
|
||||
<span v-if="row.createdBy === '1Panel'">1Panel</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('container.containerNumber')"
|
||||
prop="containerNumber"
|
||||
min-width="80"
|
||||
fix
|
||||
/>
|
||||
<el-table-column :label="$t('container.composeDirectory')" min-width="80" fix>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="toComposeFolder(row)">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('container.containerStatus')" min-width="80" fix>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ getContainerStatus(scope.row.containers) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.createdAt')" prop="createdAt" min-width="80" fix />
|
||||
<fu-table-operations
|
||||
width="200px"
|
||||
@@ -82,7 +79,7 @@
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
||||
<EditDialog ref="dialogEditRef" />
|
||||
<EditDialog @search="search" ref="dialogEditRef" />
|
||||
<CreateDialog @search="search" ref="dialogRef" />
|
||||
<DeleteDialog @search="search" ref="dialogDelRef" />
|
||||
</div>
|
||||
@@ -97,7 +94,6 @@ import ComposeDetail from '@/views/container/compose/detail/index.vue';
|
||||
import { loadContainerLog, loadDockerStatus, searchCompose } from '@/api/modules/container';
|
||||
import i18n from '@/lang';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
import router from '@/routers';
|
||||
|
||||
const data = ref();
|
||||
@@ -105,7 +101,6 @@ const selects = ref<any>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isOnDetail = ref(false);
|
||||
const baseDir = ref();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'container-compose-page-size',
|
||||
@@ -135,13 +130,8 @@ const goSetting = async () => {
|
||||
router.push({ name: 'ContainerSetting' });
|
||||
};
|
||||
|
||||
const toFolder = async () => {
|
||||
router.push({ path: '/hosts/files', query: { path: baseDir.value + '/docker/compose' } });
|
||||
};
|
||||
|
||||
const loadPath = async () => {
|
||||
const pathRes = await loadBaseDir();
|
||||
baseDir.value = pathRes.data;
|
||||
const toComposeFolder = async (row: Container.ComposeInfo) => {
|
||||
router.push({ path: '/hosts/files', query: { path: row.workdir } });
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
@@ -173,9 +163,16 @@ const loadDetail = async (row: Container.ComposeInfo) => {
|
||||
isOnDetail.value = true;
|
||||
composeDetailRef.value!.acceptParams(params);
|
||||
};
|
||||
const backList = async () => {
|
||||
isOnDetail.value = false;
|
||||
search();
|
||||
const getContainerStatus = (containers) => {
|
||||
const safeContainers = containers || [];
|
||||
const runningCount = safeContainers.filter((container) => container.state.toLowerCase() === 'running').length;
|
||||
const totalCount = safeContainers.length;
|
||||
const statusText = runningCount > 0 ? 'Running' : 'Exited';
|
||||
if (statusText === 'Exited') {
|
||||
return i18n.global.t('container.exited');
|
||||
} else {
|
||||
return i18n.global.t('container.running') + ` (${runningCount}/${totalCount})`;
|
||||
}
|
||||
};
|
||||
|
||||
const dialogRef = ref();
|
||||
@@ -199,6 +196,8 @@ const onEdit = async (row: Container.ComposeInfo) => {
|
||||
name: row.name,
|
||||
path: row.path,
|
||||
content: res.data,
|
||||
env: row.env,
|
||||
createdBy: row.createdBy,
|
||||
};
|
||||
dialogEditRef.value!.acceptParams(params);
|
||||
};
|
||||
@@ -224,7 +223,6 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
onMounted(() => {
|
||||
loadPath();
|
||||
loadStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -369,7 +369,7 @@ const dialogUpgradeRef = ref();
|
||||
const dialogCommitRef = ref();
|
||||
const dialogPortJumpRef = ref();
|
||||
const opRef = ref();
|
||||
const includeAppStore = ref(true);
|
||||
const includeAppStore = ref();
|
||||
const columns = ref([]);
|
||||
|
||||
const dockerStatus = ref('Running');
|
||||
@@ -423,6 +423,7 @@ const dialogRenameRef = ref();
|
||||
const dialogPruneRef = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
|
||||
let filterItem = props.filters ? props.filters : '';
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
@@ -767,6 +768,8 @@ const buttons = [
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
let includeItem = localStorage.getItem('includeAppStore');
|
||||
includeAppStore.value = !includeItem || includeItem === 'true';
|
||||
loadStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -122,17 +122,16 @@ const timeOptions = ref([
|
||||
]);
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
const handleClose = async () => {
|
||||
terminalSocket.value?.send('close conn');
|
||||
logVisible.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
watch(logVisible, (val) => {
|
||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||
@@ -162,10 +161,8 @@ const searchLogs = async () => {
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
let msg =
|
||||
logSearch.tail === 0
|
||||
? i18n.global.t('container.downLogHelper1', [logSearch.container])
|
||||
: i18n.global.t('container.downLogHelper2', [logSearch.container, logSearch.tail]);
|
||||
logSearch.tail = 0;
|
||||
let msg = i18n.global.t('container.downLogHelper1', [logSearch.container]);
|
||||
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
>
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-alert
|
||||
v-if="dialogData.title === 'edit' && isFromApp(dialogData.rowData!)"
|
||||
:title="$t('container.containerFromAppHelper')"
|
||||
:closable="false"
|
||||
type="error"
|
||||
/>
|
||||
<el-form-item class="mt-5" :label="$t('commons.table.name')" prop="name">
|
||||
<el-input
|
||||
:disabled="isFromApp(dialogData.rowData!)"
|
||||
@@ -296,13 +290,39 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<el-dialog v-model="dialogVisible" width="30%" :title="$t('commons.button.edit')">
|
||||
<div v-if="dialogData.title === 'edit' && isFromApp(dialogData.rowData!)" class="leading-6">
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper1') }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper2') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper3') }}</span>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper4') }}</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="loading" @click="dialogVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="submit()">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { Rules, checkFloatNumberRange, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm, ElMessageBox } from 'element-plus';
|
||||
import { ElForm } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import {
|
||||
listImage,
|
||||
@@ -327,6 +347,7 @@ interface DialogProps {
|
||||
|
||||
const title = ref<string>('');
|
||||
const drawerVisible = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
@@ -386,6 +407,7 @@ const limits = ref<Container.ResourceLimit>({
|
||||
const handleClose = () => {
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
@@ -448,6 +470,7 @@ const loadNetworkOptions = async () => {
|
||||
const res = await listNetwork();
|
||||
networks.value = res.data;
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (dialogData.value.rowData!.volumes.length !== 0) {
|
||||
for (const item of dialogData.value.rowData!.volumes) {
|
||||
@@ -460,93 +483,87 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (dialogData.value.rowData?.envStr) {
|
||||
dialogData.value.rowData.env = dialogData.value.rowData!.envStr.split('\n');
|
||||
}
|
||||
if (dialogData.value.rowData?.labelsStr) {
|
||||
dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n');
|
||||
}
|
||||
dialogData.value.rowData!.cmd = [];
|
||||
if (dialogData.value.rowData?.cmdStr) {
|
||||
if (dialogData.value.rowData?.cmdStr.indexOf(`'`) !== -1) {
|
||||
let itemCmd = dialogData.value.rowData!.cmdStr.split(`'`);
|
||||
for (const cmd of itemCmd) {
|
||||
if (cmd && cmd !== ' ') {
|
||||
dialogData.value.rowData!.cmd.push(cmd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let itemCmd = dialogData.value.rowData!.cmdStr.split(` `);
|
||||
for (const cmd of itemCmd) {
|
||||
dialogVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
dialogVisible.value = false;
|
||||
if (dialogData.value.rowData?.envStr) {
|
||||
dialogData.value.rowData.env = dialogData.value.rowData!.envStr.split('\n');
|
||||
}
|
||||
if (dialogData.value.rowData?.labelsStr) {
|
||||
dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n');
|
||||
}
|
||||
dialogData.value.rowData!.cmd = [];
|
||||
if (dialogData.value.rowData?.cmdStr) {
|
||||
if (dialogData.value.rowData?.cmdStr.indexOf(`'`) !== -1) {
|
||||
let itemCmd = dialogData.value.rowData!.cmdStr.split(`'`);
|
||||
for (const cmd of itemCmd) {
|
||||
if (cmd && cmd !== ' ') {
|
||||
dialogData.value.rowData!.cmd.push(cmd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let itemCmd = dialogData.value.rowData!.cmdStr.split(` `);
|
||||
for (const cmd of itemCmd) {
|
||||
dialogData.value.rowData!.cmd.push(cmd);
|
||||
}
|
||||
}
|
||||
dialogData.value.rowData!.entrypoint = [];
|
||||
if (dialogData.value.rowData?.entrypointStr) {
|
||||
if (dialogData.value.rowData?.entrypointStr.indexOf(`'`) !== -1) {
|
||||
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(`'`);
|
||||
for (const entry of itemEntrypoint) {
|
||||
if (entry && entry !== ' ') {
|
||||
dialogData.value.rowData!.entrypoint.push(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(` `);
|
||||
for (const entry of itemEntrypoint) {
|
||||
}
|
||||
dialogData.value.rowData!.entrypoint = [];
|
||||
if (dialogData.value.rowData?.entrypointStr) {
|
||||
if (dialogData.value.rowData?.entrypointStr.indexOf(`'`) !== -1) {
|
||||
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(`'`);
|
||||
for (const entry of itemEntrypoint) {
|
||||
if (entry && entry !== ' ') {
|
||||
dialogData.value.rowData!.entrypoint.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dialogData.value.rowData!.publishAllPorts) {
|
||||
dialogData.value.rowData!.exposedPorts = [];
|
||||
} else {
|
||||
if (!checkPortValid()) {
|
||||
return;
|
||||
let itemEntrypoint = dialogData.value.rowData!.entrypointStr.split(` `);
|
||||
for (const entry of itemEntrypoint) {
|
||||
dialogData.value.rowData!.entrypoint.push(entry);
|
||||
}
|
||||
}
|
||||
dialogData.value.rowData!.memory = Number(dialogData.value.rowData!.memory);
|
||||
dialogData.value.rowData!.nanoCPUs = Number(dialogData.value.rowData!.nanoCPUs);
|
||||
|
||||
loading.value = true;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await createContainer(dialogData.value.rowData!)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('container.updateContainerHelper'),
|
||||
i18n.global.t('commons.button.edit'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
)
|
||||
.then(async () => {
|
||||
await updateContainer(dialogData.value.rowData!)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
updateContainerID();
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
if (dialogData.value.rowData!.publishAllPorts) {
|
||||
dialogData.value.rowData!.exposedPorts = [];
|
||||
} else {
|
||||
if (!checkPortValid()) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
dialogData.value.rowData!.memory = Number(dialogData.value.rowData!.memory);
|
||||
dialogData.value.rowData!.nanoCPUs = Number(dialogData.value.rowData!.nanoCPUs);
|
||||
|
||||
loading.value = true;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await createContainer(dialogData.value.rowData!)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
dialogVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
await updateContainer(dialogData.value.rowData!)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('search');
|
||||
drawerVisible.value = false;
|
||||
dialogVisible.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
updateContainerID();
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const updateContainerID = async () => {
|
||||
|
||||
@@ -198,20 +198,29 @@ const search = async () => {
|
||||
|
||||
const batchDelete = async (row: Container.NetworkInfo | null) => {
|
||||
let names: Array<string> = [];
|
||||
let hasPanelNetwork;
|
||||
if (row === null) {
|
||||
selects.value.forEach((item: Container.NetworkInfo) => {
|
||||
if (item.name === '1panel-network') {
|
||||
hasPanelNetwork = true;
|
||||
}
|
||||
names.push(item.name);
|
||||
});
|
||||
} else {
|
||||
if (row.name === '1panel-network') {
|
||||
hasPanelNetwork = true;
|
||||
}
|
||||
names.push(row.name);
|
||||
}
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('container.network'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
msg: hasPanelNetwork
|
||||
? i18n.global.t('container.networkHelper')
|
||||
: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('container.network'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: deleteNetwork,
|
||||
params: { names: names },
|
||||
});
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
size="50%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('commons.button.view') }}</span>
|
||||
</div>
|
||||
<DrawerHeader :header="$t('commons.button.view')" :back="handleClose" />
|
||||
</template>
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
@@ -38,6 +36,7 @@ import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { ref } from 'vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
const extensions = [javascript(), oneDark];
|
||||
|
||||
const detailVisible = ref(false);
|
||||
@@ -51,6 +50,10 @@ const acceptParams = (params: DialogProps): void => {
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
detailVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
:label="$t('commons.table.name')"
|
||||
min-width="100"
|
||||
prop="name"
|
||||
sortable
|
||||
fix
|
||||
show-overflow-tooltip
|
||||
>
|
||||
@@ -47,7 +48,7 @@
|
||||
<el-table-column :label="$t('container.description')" prop="description" min-width="200" fix />
|
||||
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
|
||||
<template #default="{ row }">
|
||||
{{ dateFormatSimple(row.createdAt) }}
|
||||
{{ dateFormat(0, 0, row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" />
|
||||
@@ -63,7 +64,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, onMounted, ref } from 'vue';
|
||||
import { dateFormatSimple } from '@/utils/util';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import DetailDialog from '@/views/container/template/detail/index.vue';
|
||||
import OperatorDialog from '@/views/container/template/operator/index.vue';
|
||||
|
||||
@@ -271,7 +271,11 @@
|
||||
>
|
||||
<el-input v-model="dialogData.rowData!.sourceDir">
|
||||
<template #prepend>
|
||||
<FileList @choose="loadDir" :dir="true"></FileList>
|
||||
<FileList
|
||||
@choose="loadDir"
|
||||
:dir="true"
|
||||
:path="dialogData.rowData!.sourceDir"
|
||||
></FileList>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@@ -316,6 +320,31 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="hasAlert">
|
||||
<el-checkbox v-model="dialogData.rowData!.hasAlert" :label="$t('alert.isAlert')" />
|
||||
<span class="input-help">{{ $t('alert.cronJobHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="alertCount"
|
||||
v-if="dialogData.rowData!.hasAlert && isProductPro"
|
||||
:label="$t('alert.alertCount')"
|
||||
>
|
||||
<el-input-number
|
||||
style="width: 200px"
|
||||
:min="1"
|
||||
step-strictly
|
||||
:step="1"
|
||||
v-model.number="dialogData.rowData!.alertCount"
|
||||
></el-input-number>
|
||||
<span class="input-help">{{ $t('alert.alertCountHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogData.rowData!.hasAlert && !isProductPro">
|
||||
<span>{{ $t('alert.licenseHelper') }}</span>
|
||||
<el-button link type="primary" @click="toUpload">
|
||||
{{ $t('license.levelUpPro') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('cronjob.retainCopies')" prop="retainCopies">
|
||||
<el-input-number
|
||||
style="width: 200px"
|
||||
@@ -357,6 +386,7 @@
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
<LicenseImport ref="licenseRef" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
@@ -378,6 +408,9 @@ import { listContainer } from '@/api/modules/container';
|
||||
import { Database } from '@/api/interface/database';
|
||||
import { ListAppInstalled } from '@/api/modules/app';
|
||||
import { loadDefaultSpec, specOptions, transObjToSpec, transSpecToObj, weekOptions } from './../helper';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { GlobalStore } from '@/store';
|
||||
import LicenseImport from '@/components/license-import/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -393,6 +426,10 @@ const dialogData = ref<DialogProps>({
|
||||
title: '',
|
||||
});
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
const licenseRef = ref();
|
||||
const { isProductPro } = storeToRefs(globalStore);
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
dialogData.value = params;
|
||||
if (dialogData.value.rowData?.spec) {
|
||||
@@ -419,6 +456,8 @@ const acceptParams = (params: DialogProps): void => {
|
||||
if (dialogData.value.rowData.dbName) {
|
||||
dialogData.value.rowData.dbNameList = dialogData.value.rowData.dbName.split(',');
|
||||
}
|
||||
dialogData.value.rowData.hasAlert = dialogData.value.rowData!.alertCount > 0;
|
||||
dialogData.value.rowData!.alertCount = dialogData.value.rowData!.alertCount || 3;
|
||||
dialogData.value.rowData!.command = dialogData.value.rowData!.command || 'sh';
|
||||
dialogData.value.rowData!.isCustom =
|
||||
dialogData.value.rowData!.command !== 'sh' &&
|
||||
@@ -556,6 +595,18 @@ const verifySpec = (rule: any, value: any, callback: any) => {
|
||||
callback();
|
||||
};
|
||||
|
||||
function checkSendCount(rule: any, value: any, callback: any) {
|
||||
if (value === '') {
|
||||
callback();
|
||||
}
|
||||
const regex = /^(?:[1-9]|[12][0-9]|30)$/;
|
||||
if (!regex.test(value)) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 30])));
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.noSpace],
|
||||
type: [Rules.requiredSelect],
|
||||
@@ -574,6 +625,7 @@ const rules = reactive({
|
||||
backupAccounts: [Rules.requiredSelect],
|
||||
defaultDownload: [Rules.requiredSelect],
|
||||
retainCopies: [Rules.number],
|
||||
alertCount: [Rules.integerNumber, { validator: checkSendCount, trigger: 'blur' }],
|
||||
});
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
@@ -752,6 +804,15 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (dialogData.value?.rowData?.exclusionRules) {
|
||||
dialogData.value.rowData.exclusionRules = dialogData.value.rowData.exclusionRules.replaceAll('\n', ',');
|
||||
}
|
||||
dialogData.value.rowData.alertCount =
|
||||
dialogData.value.rowData!.hasAlert && isProductPro.value ? dialogData.value.rowData.alertCount : 0;
|
||||
dialogData.value.rowData.alertTitle =
|
||||
dialogData.value.rowData!.hasAlert && isProductPro.value
|
||||
? i18n.global.t('cronjob.alertTitle', [
|
||||
i18n.global.t('cronjob.' + dialogData.value.rowData.type),
|
||||
dialogData.value.rowData.name,
|
||||
])
|
||||
: '';
|
||||
if (!dialogData.value.rowData) return;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await addCronjob(dialogData.value.rowData);
|
||||
@@ -766,6 +827,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
const toUpload = () => {
|
||||
licenseRef.value.acceptParams();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
v-model:mask-show="maskShow"
|
||||
@setting="onSetting"
|
||||
@is-exist="checkExist"
|
||||
ref="appStatusRef"
|
||||
></AppStatus>
|
||||
</template>
|
||||
|
||||
@@ -327,6 +328,8 @@ const dashboardName = ref();
|
||||
const dashboardKey = ref();
|
||||
const dashboardVisible = ref(false);
|
||||
|
||||
const appStatusRef = ref();
|
||||
|
||||
const dialogPortJumpRef = ref();
|
||||
|
||||
const data = ref();
|
||||
@@ -384,6 +387,7 @@ const onSetting = async () => {
|
||||
};
|
||||
|
||||
const changeDatabase = async () => {
|
||||
appStatusRef.value.onCheck();
|
||||
for (const item of dbOptionsLocal.value) {
|
||||
if (item.database == currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
v-model:mask-show="maskShow"
|
||||
@setting="onSetting"
|
||||
@is-exist="checkExist"
|
||||
ref="appStatusRef"
|
||||
></AppStatus>
|
||||
</template>
|
||||
|
||||
@@ -305,6 +306,7 @@ const postgresqlContainer = ref();
|
||||
const postgresqlStatus = ref();
|
||||
const postgresqlVersion = ref();
|
||||
|
||||
const appStatusRef = ref();
|
||||
const dialogRef = ref();
|
||||
const onOpenDialog = async () => {
|
||||
let params = {
|
||||
@@ -348,6 +350,7 @@ const onSetting = async () => {
|
||||
};
|
||||
|
||||
const changeDatabase = async () => {
|
||||
appStatusRef.value.onCheck();
|
||||
for (const item of dbOptionsLocal.value) {
|
||||
if (item.database == currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
|
||||
@@ -128,7 +128,7 @@ const onOpenDialog = async (
|
||||
rowData: Partial<Database.DatabaseInfo> = {
|
||||
name: '',
|
||||
type: 'postgresql',
|
||||
version: '16.x',
|
||||
version: '17.x',
|
||||
address: '',
|
||||
port: 5432,
|
||||
username: '',
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.version')" prop="version">
|
||||
<el-radio-group v-model="dialogData.rowData!.version" @change="isOK = false">
|
||||
<el-radio label="17.x" value="17.x" />
|
||||
<el-radio label="16.x" value="16.x" />
|
||||
<el-radio label="15.x" value="15.x" />
|
||||
<el-radio label="14.x" value="14.x" />
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@before="onBefore"
|
||||
@after="onAfter"
|
||||
@setting="onSetting"
|
||||
ref="appStatusRef"
|
||||
></AppStatus>
|
||||
</template>
|
||||
<template #search v-if="!isOnSetting && currentDB">
|
||||
@@ -164,6 +165,7 @@ const redisIsExist = ref(false);
|
||||
const redisStatus = ref();
|
||||
const terminalShow = ref(false);
|
||||
|
||||
const appStatusRef = ref();
|
||||
const commandVisible = ref(false);
|
||||
|
||||
const redisCliExist = ref();
|
||||
@@ -220,6 +222,7 @@ const goRouter = async (target: string) => {
|
||||
};
|
||||
|
||||
const changeDatabase = async () => {
|
||||
appStatusRef.value.onCheck();
|
||||
for (const item of dbOptionsLocal.value) {
|
||||
if (item.database == currentDBName.value) {
|
||||
currentDB.value = item;
|
||||
|
||||
@@ -81,8 +81,8 @@ const acceptParams = (props: BatchRoleProps) => {
|
||||
addForm.paths.push(file.path);
|
||||
});
|
||||
addForm.mode = Number.parseInt(String(props.files[0].mode), 8);
|
||||
addForm.group = props.files[0].group;
|
||||
addForm.user = props.files[0].user;
|
||||
addForm.group = props.files[0].group || props.files[0].gid;
|
||||
addForm.user = props.files[0].user || props.files[0].uid;
|
||||
addForm.sub = true;
|
||||
|
||||
mode.value = String(props.files[0].mode);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
v-if="!mobile"
|
||||
class="!border-none !bg-transparent !text-base !font-semibold !py-2 !px-1"
|
||||
icon="FullScreen"
|
||||
style="margin-right: 10px"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
|
||||
@@ -53,7 +53,9 @@
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<span v-html="$t('file.fileHeper')"></span>
|
||||
<span class="input-help whitespace-break-spaces">
|
||||
{{ $t('file.fileHeper') }}
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
@@ -476,11 +478,10 @@ import {
|
||||
SearchFavorite,
|
||||
} from '@/api/modules/files';
|
||||
import { computeSize, copyText, dateFormat, downloadFile, getFileType, getIcon, getRandomStr } from '@/utils/util';
|
||||
import { StarFilled, Star, Top, Right, Close } from '@element-plus/icons-vue';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { Mimetypes, Languages } from '@/global/mimetype';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Back, Refresh } from '@element-plus/icons-vue';
|
||||
import { Back, Refresh, StarFilled, Star, Top, Right, Close } from '@element-plus/icons-vue';
|
||||
import { MsgWarning } from '@/utils/message';
|
||||
import { useSearchable } from './hooks/searchable';
|
||||
import { ResultData } from '@/api/interface';
|
||||
@@ -610,7 +611,7 @@ const handleFilePatch = (command: string) => {
|
||||
openBatchRole(selects.value);
|
||||
break;
|
||||
case 'batchDelFiles':
|
||||
openDetail(selects.value);
|
||||
batchDelFiles();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -115,7 +115,9 @@ const acceptParams = (props: EditProps) => {
|
||||
isFullscreen.value = fileType.value === 'excel';
|
||||
|
||||
loading.value = true;
|
||||
fileUrl.value = `${import.meta.env.VITE_API_URL as string}/files/download?path=${encodeURIComponent(props.path)}`;
|
||||
fileUrl.value = `${import.meta.env.VITE_API_URL as string}/files/download?path=${encodeURIComponent(
|
||||
props.path,
|
||||
)}×tamp=${new Date().getTime()}`;
|
||||
open.value = true;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
<span class="agree" v-html="$t('commons.login.licenseHelper')"></span>
|
||||
</template>
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<div class="agree-helper">
|
||||
<span
|
||||
v-if="!loginForm.agreeLicense && !_isMobile()"
|
||||
class="input-error"
|
||||
@@ -133,7 +135,7 @@
|
||||
>
|
||||
{{ $t('commons.login.errorAgree') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="demo">
|
||||
<span v-if="isDemo">
|
||||
@@ -514,5 +516,11 @@ onMounted(() => {
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
|
||||
border-color: #fff !important;
|
||||
}
|
||||
|
||||
.agree-helper {
|
||||
min-height: 20px;
|
||||
margin-top: -20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user