48 Commits

Author SHA1 Message Date
ssongliu
42e522abe1 fix: 升级 1pctl 被覆盖后,手动替换 BASE_DIR 2023-03-14 23:17:54 +08:00
zhengkunwang223
fb5c3429e5 feat: 文件下载页面增加 loading 2023-03-14 23:06:19 +08:00
ssongliu
0fe1fd3c7b fix: 初始化安装时,增加默认版本 2023-03-14 19:27:46 +08:00
ssongliu
bdcca7f380 fix: 调整上传文件夹 2023-03-14 19:27:46 +08:00
ssongliu
796d47d60e fix: 计划任务分页调整 2023-03-14 19:27:46 +08:00
ssongliu
a9a45ce5ac fix: 调整升级逻辑 2023-03-14 19:27:46 +08:00
ssongliu
d04121c551 fix: 解决镜像构建失败的问题 2023-03-14 19:27:46 +08:00
ssongliu
dd06ff73e6 fix: redis aof 备份恢复与版本匹配 2023-03-14 19:27:46 +08:00
wangdan
24b3501f38 feat: 优化关于页面的版本信息 2023-03-14 19:07:54 +08:00
zhengkunwang223
ed11c0a4a6 fix: 解决删除MYSQL应用没有同步删除表数据的BUG 2023-03-14 18:18:49 +08:00
zhengkunwang223
7591a716f4 fix: 解决应用删除提示页面没有实时刷新的 BUG 2023-03-14 17:37:19 +08:00
zhengkunwang223
ce44ccdedb style: 修改接口过期时间 2023-03-14 17:27:11 +08:00
zhengkunwang223
521fca93bd style: 修改样式 2023-03-14 17:27:11 +08:00
zhengkunwang223
d6a0dc0125 feat: 修改前端超时时间为10秒 2023-03-14 15:44:36 +08:00
zhengkunwang223
0a483383b4 style: 删除无用代码 2023-03-14 15:44:36 +08:00
zhengkunwang223
6c99e04aee feat: 增加判断系统是否是 demo 接口 2023-03-14 15:05:44 +08:00
zhengkunwang223
e120bb0612 feat: 编辑器增加 plaintext 类型 2023-03-14 15:05:44 +08:00
zhengkunwang223
92a11863a7 feat: 已安装应用改为同步 2023-03-14 15:05:44 +08:00
zhengkunwang223
2091fdbe5d feat: 增加应用列表更新提示 2023-03-14 15:05:44 +08:00
zhengkunwang223
b518463c90 style: 取消网站页面的提示 2023-03-14 10:59:32 +08:00
ssongliu
94fbb265fa fix: 数据库权限去掉 localhost 2023-03-14 10:34:17 +08:00
ssongliu
daefd650a5 fix: 修复计划任务编辑失败的问题 2023-03-14 10:34:17 +08:00
ssongliu
4994cc39f1 fix: 解决主机密钥连接失败的问题 2023-03-14 10:34:17 +08:00
ssongliu
2dec0bfb3c fix: 容器创建增加端口判断 2023-03-14 10:34:17 +08:00
ssongliu
eb56b918a6 fix: 增加一些校验规则 2023-03-14 10:34:17 +08:00
ssongliu
35098ce79c fix: 解决删除应用时,未删除备份记录的问题 2023-03-14 10:34:17 +08:00
ssongliu
5a3a123be7 fix: 解决 compose 创建失败的问题 2023-03-14 10:34:17 +08:00
ssongliu
a94e78d31e fix: 解决面板设置部分界面密码过期仍然能访问的问题 2023-03-14 10:34:17 +08:00
ssongliu
5527ef73ad fix: 登录页背景图替换 2023-03-14 10:34:17 +08:00
ssongliu
f1ed976c17 fix: 解决错误容器终端参数导致页面卡死的问题 2023-03-14 10:34:17 +08:00
zhengkunwang223
471bbb5c43 feat: 解决 amd64 环境下的打包问题 2023-03-14 10:10:08 +08:00
zhengkunwang223
4ae8e580b9 style: 修改样式 2023-03-13 18:02:48 +08:00
zhengkunwang223
ffb0e72a5b feat: 删除无用代码 2023-03-13 18:02:48 +08:00
zhengkunwang223
7f9793e4bb feat: 增加 SystemUpgrade 组件 2023-03-13 18:02:48 +08:00
zhengkunwang223
c332d0284b feat: 修改 code-editor 样式 2023-03-13 18:02:48 +08:00
zhengkunwang223
8b3d84d667 feat: 增加右下角检查更新功能 2023-03-13 18:02:48 +08:00
zhengkunwang223
f009e6414a feat: 限制上传文件数量 2023-03-13 18:02:48 +08:00
zhengkunwang223
e36cbb0eb7 feat: 前端增加 msg-info 组件 2023-03-13 18:02:48 +08:00
zhengkunwang223
2fbddf3f30 fix: 解决zip文件压缩报错的BUG 2023-03-13 18:02:48 +08:00
zhengkunwang223
8927b59bae fix: 解决cc防护填写数字报错的BUG 2023-03-13 18:02:48 +08:00
zhengkunwang223
90c7f9cc2c fix: 解决上传报错的BUG 2023-03-13 18:02:48 +08:00
zhengkunwang223
83ca72e153 feat: 统一限制最大数字输入位数为15位 2023-03-13 18:02:48 +08:00
zhengkunwang223
9571d82932 fix: 解决创建软连接失败的BUG 2023-03-13 18:02:48 +08:00
zhengkunwang223
c54451c733 feat: 限制数字输入的最大长度 2023-03-13 18:02:48 +08:00
zhengkunwang223
e52ddd3f39 fix: 解决申请证书,域名无效导致的 panic 2023-03-13 18:02:48 +08:00
wangdan
c488507d96 feat: 优化样式 2023-03-13 17:37:34 +08:00
maninhill
db5853df7d Update README.md 2023-03-13 15:58:38 +08:00
maninhill
eb2e533d14 Update README.md 2023-03-13 15:57:30 +08:00
96 changed files with 891 additions and 733 deletions

View File

@@ -16,7 +16,7 @@ build_web:
build_bin:
cd $(SERVER_PATH) \
&& CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
&& CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -tags osusergo -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_linux_on_mac:
cd $(SERVER_PATH) \

View File

@@ -33,7 +33,7 @@
以 root 用户执行如下命令一键安装 1Panel:
```sh
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sh quick_start.sh
```
**学习资料**

View File

@@ -128,3 +128,18 @@ func (b *BaseApi) GetAppTags(c *gin.Context) {
}
helper.SuccessWithData(c, tags)
}
// @Tags App
// @Summary Get app list update
// @Description 获取应用更新版本
// @Success 200
// @Security ApiKeyAuth
// @Router /apps/checkupdate [get]
func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
res, err := appService.GetAppUpdate()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View File

@@ -167,6 +167,15 @@ func (b *BaseApi) InitUserInfo(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Auth
// @Summary Check System isDemo
// @Description 判断是否为demo环境
// @Success 200
// @Router /auth/demo [get]
func (b *BaseApi) CheckIsDemo(c *gin.Context) {
helper.SuccessWithData(c, global.CONF.System.IsDemo)
}
func saveLoginLogs(c *gin.Context, err error) {
var logs model.LoginLog
if err != nil {

View File

@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
@@ -17,6 +18,8 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
@@ -492,6 +495,105 @@ func (b *BaseApi) LoadFromFile(c *gin.Context) {
helper.SuccessWithData(c, string(content))
}
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int) error {
targetFile, err := os.Create(filepath.Join(dstDir, fileName))
if err != nil {
return err
}
defer targetFile.Close()
for i := 0; i < chunkCount; i++ {
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i))
chunkData, err := ioutil.ReadFile(chunkPath)
if err != nil {
return err
}
_, err = targetFile.Write(chunkData)
if err != nil {
return err
}
}
return files.NewFileOp().DeleteDir(fileDir)
}
// @Tags File
// @Summary ChunkUpload file
// @Description 分片上传文件
// @Param file formData file true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/chunkupload [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"上传文件 [path]","formatEN":"Upload file [path]"}
func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
fileForm, err := c.FormFile("chunk")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
uploadFile, err := fileForm.Open()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
fileOp := files.NewFileOp()
if err := fileOp.CreateDir("uploads", 0755); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
//fileID := uuid.New().String()
filename := c.PostForm("filename")
fileDir := filepath.Join(global.CONF.System.DataDir, "upload", filename)
os.MkdirAll(fileDir, 0755)
filePath := filepath.Join(fileDir, filename)
emptyFile, err := os.Create(filePath)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
emptyFile.Close()
chunkData, err := ioutil.ReadAll(uploadFile)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = ioutil.WriteFile(chunkPath, chunkData, 0644)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if chunkIndex+1 == chunkCount {
err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount)
if err != nil {
fmt.Println(err.Error())
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrAppDelete, err)
return
}
helper.SuccessWithData(c, true)
} else {
return
}
}
var wsUpgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true

View File

@@ -1,111 +0,0 @@
package v1
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/gin-gonic/gin"
)
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int) error {
//fileInfoList, err := ioutil.ReadDir(fileDir)
//if err != nil {
// return err
//}
targetFile, err := os.Create(filepath.Join(dstDir, fileName))
if err != nil {
return err
}
defer targetFile.Close()
for i := 0; i < chunkCount; i++ {
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i))
chunkData, err := ioutil.ReadFile(chunkPath)
if err != nil {
return err
}
_, err = targetFile.Write(chunkData)
if err != nil {
return err
}
}
return files.NewFileOp().DeleteDir(fileDir)
}
func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
fileForm, err := c.FormFile("chunk")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
uploadFile, err := fileForm.Open()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
fileOp := files.NewFileOp()
if err := fileOp.CreateDir("uploads", 0755); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
//fileID := uuid.New().String()
filename := c.PostForm("filename")
fileDir := filepath.Join(global.CONF.System.DataDir, "upload", filename)
_ = os.MkdirAll(fileDir, 0755)
filePath := filepath.Join(fileDir, filename)
emptyFile, err := os.Create(filePath)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
emptyFile.Close()
chunkData, err := ioutil.ReadAll(uploadFile)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = ioutil.WriteFile(chunkPath, chunkData, 0644)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if chunkIndex+1 == chunkCount {
err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount)
if err != nil {
fmt.Println(err.Error())
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrAppDelete, err)
return
}
helper.SuccessWithData(c, true)
} else {
return
}
}

View File

@@ -57,15 +57,14 @@ func (b *BaseApi) TestByInfo(c *gin.Context) {
}
var connInfo ssh.ConnInfo
if err := copier.Copy(&connInfo, &req); err != nil {
helper.SuccessWithData(c, false)
}
_ = copier.Copy(&connInfo, &req)
connInfo.PrivateKey = []byte(req.PrivateKey)
client, err := connInfo.NewClient()
if err != nil {
helper.SuccessWithData(c, false)
return
}
defer client.Close()
helper.SuccessWithData(c, true)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"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/copier"
"github.com/1Panel-dev/1Panel/backend/utils/ssh"
"github.com/1Panel-dev/1Panel/backend/utils/terminal"
@@ -39,10 +40,8 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
return
}
var connInfo ssh.ConnInfo
if err := copier.Copy(&connInfo, &host); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, constant.ErrStructTransform)
return
}
_ = copier.Copy(&connInfo, &host)
connInfo.PrivateKey = []byte(host.PrivateKey)
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -156,6 +155,15 @@ func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
}
defer wsConn.Close()
cmds := fmt.Sprintf("docker exec %s %s", containerID, command)
if len(user) != 0 {
cmds = fmt.Sprintf("docker exec -u %s %s %s", user, containerID, command)
}
stdout, err := cmd.Exec(cmds)
if wshandleError(wsConn, errors.WithMessage(err, stdout)) {
return
}
commands := fmt.Sprintf("docker exec -it %s %s", containerID, command)
if len(user) != 0 {
commands = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
@@ -187,8 +195,8 @@ func wshandleError(ws *websocket.Conn, err error) bool {
if err != nil {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if err := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); err != nil {
global.LOG.Errorf("websocket writes control message failed, err: %v", err)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
return true
}

View File

@@ -129,7 +129,7 @@ type ComposeContainer struct {
State string `json:"state"`
}
type ComposeCreate struct {
Name string `json:"name" validate:"required"`
Name string `json:"name"`
From string `json:"from" validate:"required,oneof=edit path template"`
File string `json:"file"`
Path string `json:"path"`

View File

@@ -8,7 +8,7 @@ type HostOperate struct {
ID uint `json:"id"`
GroupID uint `json:"groupID"`
Name string `json:"name"`
Addr string `json:"addr" validate:"required,ip"`
Addr string `json:"addr" validate:"required"`
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
User string `json:"user" validate:"required"`
AuthMode string `json:"authMode" validate:"oneof=password key"`
@@ -19,7 +19,7 @@ type HostOperate struct {
}
type HostConnTest struct {
Addr string `json:"addr" validate:"required,ip"`
Addr string `json:"addr" validate:"required"`
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
User string `json:"user" validate:"required"`
AuthMode string `json:"authMode" validate:"oneof=password key"`

View File

@@ -19,6 +19,7 @@ type IMysqlRepo interface {
Delete(ctx context.Context, opts ...DBOption) error
Update(id uint, vars map[string]interface{}) error
UpdateDatabaseInfo(id uint, vars map[string]interface{}) error
DeleteAll(ctx context.Context) error
}
func NewIMysqlRepo() IMysqlRepo {
@@ -65,6 +66,10 @@ func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error
}
func (u *MysqlRepo) DeleteAll(ctx context.Context) error {
return getTx(ctx).Where("1 = 1").Delete(&model.DatabaseMysql{}).Error
}
func (u *MysqlRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.DatabaseMysql{}).Where("id = ?", id).Updates(vars).Error
}

View File

@@ -34,6 +34,7 @@ type IAppService interface {
GetAppDetail(appId uint, version string) (response.AppDetailDTO, error)
Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error)
SyncAppList() error
GetAppUpdate() (*response.AppUpdateRes, error)
}
func NewIAppService() IAppService {

View File

@@ -275,16 +275,14 @@ func (a AppInstallService) SyncAll() error {
if err != nil {
return err
}
go func() {
for _, i := range allList {
if i.Status == constant.Installing {
continue
}
if err := syncById(i.ID); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
}
for _, i := range allList {
if i.Status == constant.Installing {
continue
}
}()
if err := syncById(i.ID); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
}
}
return nil
}

View File

@@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/utils/env"
"math"
"os"
"path"
@@ -13,6 +11,9 @@ import (
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/utils/env"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/buserr"
@@ -159,7 +160,10 @@ func deleteAppInstall(ctx context.Context, install model.AppInstall, deleteBacku
global.LOG.Infof("delete app %s-%s backups successful", install.App.Key, install.Name)
}
_ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType("app"), commonRepo.WithByName(install.App.Key), backupRepo.WithByDetailName(install.Name))
_ = backupRepo.DeleteRecord(ctx, commonRepo.WithByType(install.App.Key))
if install.App.Key == constant.AppMysql {
_ = mysqlRepo.DeleteAll(ctx)
}
return nil
}

View File

@@ -222,6 +222,12 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
if err := json.Unmarshal([]byte(req.Vars), &varMap); err != nil {
return err
}
oldVars := backup.Vars
oldDir, err := loadLocalDir()
if err != nil {
return err
}
upMap := make(map[string]interface{})
upMap["bucket"] = req.Bucket
upMap["credential"] = req.Credential
@@ -235,9 +241,8 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
if strings.HasSuffix(dirStr, "/") {
dirStr = dirStr[:strings.LastIndex(dirStr, "/")]
}
if err := updateBackupDir(dirStr); err != nil {
upMap["vars"] = backup.Vars
_ = backupRepo.Update(req.ID, upMap)
if err := updateBackupDir(dirStr, oldDir); err != nil {
_ = backupRepo.Update(req.ID, (map[string]interface{}{"vars": oldVars}))
return err
}
}
@@ -309,8 +314,7 @@ func loadLocalDir() (string, error) {
return "", fmt.Errorf("error type dir: %T", varMap["dir"])
}
func updateBackupDir(dir string) error {
oldDir := global.CONF.System.Backup
func updateBackupDir(dir, oldDir string) error {
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err
@@ -323,6 +327,5 @@ func updateBackupDir(dir string) error {
if err != nil {
return errors.New(string(stdout))
}
global.CONF.System.Backup = dir
return nil
}

View File

@@ -103,7 +103,7 @@ func handleAppBackup(install *model.AppInstall, backupDir, fileName string) erro
}
resource, _ := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(install.ID))
if resource.ID != 0 {
if resource.ID != 0 && resource.ResourceId != 0 {
mysqlInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMysql, "")
if err != nil {
return err

View File

@@ -121,7 +121,10 @@ func handleRedisRecover(redisInfo *repo.RootInfo, recoverFile string, isRollback
}
if appendonly == "yes" {
if !strings.HasSuffix(recoverFile, ".tar.gz") || !strings.HasSuffix(recoverFile, ".aof") {
if redisInfo.Version == "6.0.16" && !strings.HasSuffix(recoverFile, ".aof") {
return buserr.New(constant.ErrTypeOfRedis)
}
if redisInfo.Version == "7.0.5" && !strings.HasSuffix(recoverFile, ".tar.gz") {
return buserr.New(constant.ErrTypeOfRedis)
}
} else {
@@ -170,7 +173,7 @@ func handleRedisRecover(redisInfo *repo.RootInfo, recoverFile string, isRollback
}
} else {
itemName := "dump.rdb"
if redisInfo.Version == "6.0.16" {
if appendonly == "yes" && redisInfo.Version == "6.0.16" {
itemName = "appendonly.aof"
}
input, err := ioutil.ReadFile(recoverFile)
@@ -184,5 +187,6 @@ func handleRedisRecover(redisInfo *repo.RootInfo, recoverFile string, isRollback
if _, err := compose.Up(composeDir + "/docker-compose.yml"); err != nil {
return err
}
isOk = true
return nil
}

View File

@@ -12,8 +12,10 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@@ -136,6 +138,13 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
}
func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
if len(req.ExposedPorts) != 0 {
for _, port := range req.ExposedPorts {
if common.ScanPort(port.HostPort) {
return buserr.WithDetail(constant.ErrPortInUsed, port.HostPort, nil)
}
}
}
client, err := docker.NewDockerClient()
if err != nil {
return err

View File

@@ -170,7 +170,7 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
if req.Operation == "down" {
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
_ = os.RemoveAll(strings.ReplaceAll(req.Path, req.Name+"/docker-compose.yml", ""))
_ = os.RemoveAll(strings.ReplaceAll(req.Path, "/docker-compose.yml", ""))
}
return nil

View File

@@ -178,24 +178,27 @@ func (u *CronjobService) Create(cronjobDto dto.CronjobCreate) error {
cronjob.Spec = loadSpec(cronjob)
global.LOG.Infof("create cronjob %s successful, spec: %s", cronjob.Name, cronjob.Spec)
if err := u.StartJob(&cronjob); err != nil {
entryID, err := u.StartJob(&cronjob)
if err != nil {
return err
}
cronjob.EntryID = uint64(entryID)
if err := cronjobRepo.Create(&cronjob); err != nil {
return err
}
return nil
}
func (u *CronjobService) StartJob(cronjob *model.Cronjob) error {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
global.LOG.Infof("stop cronjob entryID: %d", cronjob.EntryID)
func (u *CronjobService) StartJob(cronjob *model.Cronjob) (int, error) {
if cronjob.EntryID != 0 {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
}
entryID, err := u.AddCronJob(cronjob)
if err != nil {
return err
return 0, err
}
_ = cronjobRepo.Update(cronjob.ID, map[string]interface{}{"entry_id": entryID})
return nil
global.LOG.Debug(global.Cron.Entries())
return entryID, nil
}
func (u *CronjobService) Delete(ids []uint) error {
@@ -225,12 +228,15 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error {
return constant.ErrRecordNotFound
}
cronjob.EntryID = cronModel.EntryID
cronjob.Type = cronModel.Type
cronjob.Spec = loadSpec(cronjob)
if err := u.StartJob(&cronjob); err != nil {
newEntryID, err := u.StartJob(&cronjob)
if err != nil {
return err
}
upMap := make(map[string]interface{})
upMap["entry_id"] = newEntryID
upMap["name"] = req.Name
upMap["script"] = req.Script
upMap["spec_type"] = req.SpecType
@@ -254,15 +260,20 @@ func (u *CronjobService) UpdateStatus(id uint, status string) error {
if cronjob.ID == 0 {
return errors.WithMessage(constant.ErrRecordNotFound, "record not found")
}
var (
entryID int
err error
)
if status == constant.StatusEnable {
if err := u.StartJob(&cronjob); err != nil {
entryID, err = u.StartJob(&cronjob)
if err != nil {
return err
}
} else {
global.Cron.Remove(cron.EntryID(cronjob.EntryID))
global.LOG.Infof("stop cronjob entryID: %d", cronjob.EntryID)
}
return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status})
return cronjobRepo.Update(cronjob.ID, map[string]interface{}{"status": status, "entry_id": entryID})
}
func (u *CronjobService) AddCronJob(cronjob *model.Cronjob) (int, error) {

View File

@@ -97,7 +97,7 @@ func (f FileService) Create(op request.FileCreate) error {
return fo.CreateDir(op.Path, fs.FileMode(op.Mode))
} else {
if op.IsLink {
if !fo.Stat(op.Path) {
if !fo.Stat(op.LinkPath) {
return buserr.New(constant.ErrLinkPathNotFound)
}
return fo.LinkFile(op.LinkPath, op.Path, op.IsSymlink)

View File

@@ -122,7 +122,7 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
return "", err
}
if req.From == "edit" {
dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, req.Name)
dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, strings.ReplaceAll(req.Name, ":", "_"))
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return "", err
@@ -139,6 +139,8 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
_, _ = write.WriteString(string(req.Dockerfile))
write.Flush()
req.Dockerfile = dir
} else {
req.Dockerfile = strings.ReplaceAll(req.Dockerfile, "/Dockerfile", "")
}
tar, err := archive.TarWithOptions(req.Dockerfile+"/", &archive.TarOptions{})
if err != nil {
@@ -161,13 +163,27 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
go func() {
defer file.Close()
defer tar.Close()
res, err := client.ImageBuild(context.TODO(), tar, opts)
res, err := client.ImageBuild(context.Background(), tar, opts)
if err != nil {
global.LOG.Errorf("build image %s failed, err: %v", req.Name, err)
_, _ = file.WriteString("image build failed!")
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
global.LOG.Errorf("build image %s failed, err: %v", req.Name, err)
_, _ = file.WriteString(fmt.Sprintf("build image %s failed, err: %v", req.Name, err))
_, _ = file.WriteString("image build failed!")
return
}
if strings.Contains(string(body), "error") && strings.Contains(string(body), "failed:") {
global.LOG.Errorf("build image %s failed", req.Name)
_, _ = file.Write(body)
_, _ = file.WriteString("image build failed!")
return
}
global.LOG.Infof("build image %s successful!", req.Name)
_, _ = io.Copy(file, res.Body)
_, _ = file.WriteString("image build successful!")

View File

@@ -172,17 +172,17 @@ func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
return
}
if err := u.handlePanelDatas(snap.ID, fileOp, "snapshot", global.CONF.BaseDir+"/1panel", backupPanelDir, localDir, dockerDataDir); err != nil {
if err := u.handlePanelDatas(snap.ID, fileOp, "snapshot", global.CONF.System.BaseDir+"/1panel", backupPanelDir, localDir, dockerDataDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
_, _ = cmd.Exec("systemctl restart docker")
snapJson := SnapshotJson{
BaseDir: global.CONF.BaseDir,
BaseDir: global.CONF.System.BaseDir,
DockerDataDir: dockerDataDir,
BackupDataDir: localDir,
PanelDataDir: global.CONF.BaseDir + "/1panel",
PanelDataDir: global.CONF.System.BaseDir + "/1panel",
LiveRestoreEnabled: liveRestoreStatus,
}
if err := u.saveJson(snapJson, rootDir); err != nil {
@@ -287,8 +287,8 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
u.OriginalPath = fmt.Sprintf("%s/original_%s", snapJson.BaseDir, snap.Name)
_ = os.MkdirAll(u.OriginalPath, os.ModePerm)
snapJson.OldBaseDir = global.CONF.BaseDir
snapJson.OldPanelDataDir = global.CONF.BaseDir + "/1panel"
snapJson.OldBaseDir = global.CONF.System.BaseDir
snapJson.OldPanelDataDir = global.CONF.System.BaseDir + "/1panel"
snapJson.OldBackupDataDir = localDir
recoverPanelDir := fmt.Sprintf("%s/%s/1panel", baseDir, snap.Name)
liveRestore := false

View File

@@ -81,7 +81,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", runtime.GOARCH)
_ = settingRepo.Update("SystemStatus", "Upgrading")
go func() {
if err := fileOp.DownloadFile(downloadPath+"/"+fileName, rootDir+"/service.tar.gz"); err != nil {
if err := fileOp.DownloadFile(downloadPath+"/"+fileName, rootDir+"/"+fileName); err != nil {
global.LOG.Errorf("download service file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return
@@ -90,11 +90,12 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
defer func() {
_ = os.Remove(rootDir)
}()
if err := fileOp.Decompress(rootDir+"/service.tar.gz", rootDir, files.TarGz); err != nil {
if err := handleUnTar(rootDir+"/"+fileName, rootDir); err != nil {
global.LOG.Errorf("decompress file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return
}
tmpDir := rootDir + "/" + strings.ReplaceAll(fileName, ".tar.gz", "")
if err := u.handleBackup(fileOp, originalDir); err != nil {
global.LOG.Errorf("handle backup original file failed, err: %v", err)
@@ -103,17 +104,24 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
}
global.LOG.Info("backup original data successful, now start to upgrade!")
if err := cpBinary(rootDir+"/1panel", "/usr/local/bin/1panel"); err != nil {
if err := cpBinary(tmpDir+"/1panel", "/usr/local/bin/1panel"); err != nil {
u.handleRollback(fileOp, originalDir, 1)
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
return
}
if err := cpBinary(fmt.Sprintf("%s/1panel-online-installer-%s/1pctl", rootDir, req.Version), "/usr/local/bin/1pctl"); err != nil {
if err := cpBinary(tmpDir+"/1pctl", "/usr/local/bin/1pctl"); err != nil {
u.handleRollback(fileOp, originalDir, 2)
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
return
}
if err := cpBinary(fmt.Sprintf("%s/1panel-online-installer-%s/1panel/conf/1panel.service", rootDir, req.Version), "/etc/systemd/system/1panel.service"); err != nil {
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl", global.CONF.System.BaseDir); err != nil {
u.handleRollback(fileOp, originalDir, 2)
global.LOG.Errorf("upgrade basedir in 1pctl failed, err: %v", err)
return
}
if err := cpBinary(tmpDir+"/1panel.service", "/etc/systemd/system/1panel.service"); err != nil {
u.handleRollback(fileOp, originalDir, 3)
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err)
return

View File

@@ -1,7 +1,6 @@
package configs
type ServerConfig struct {
BaseDir string `mapstructure:"base_dir"`
System System `mapstructure:"system"`
LogConfig LogConfig `mapstructure:"log"`
}

View File

@@ -13,5 +13,6 @@ type System struct {
BaseDir string `mapstructure:"base_dir"`
Mode string `mapstructure:"mode"`
RepoUrl string `mapstructure:"repo_url"`
Version string `mapstructure:"version"`
IsDemo bool `mapstructure:"is_demo"`
}

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/cron/job"
@@ -44,8 +45,12 @@ func Run() {
global.LOG.Errorf("start my cronjob failed, err: %v", err)
}
for _, cronjob := range cronJobs {
if err := service.ServiceGroupApp.StartJob(&cronjob); err != nil {
entryID, err := service.ServiceGroupApp.StartJob(&cronjob)
if err != nil {
global.LOG.Errorf("start %s job %s failed, err: %v", cronjob.Type, cronjob.Name, err)
}
if err := repo.NewICronjobRepo().Update(cronjob.ID, map[string]interface{}{"entry_id": entryID}); err != nil {
global.LOG.Errorf("update cronjob %s %s failed, err: %v", cronjob.Type, cronjob.Name, err)
}
}
}

View File

@@ -136,7 +136,7 @@ var AddTableSetting = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "DingVars", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: "v1.0.0"}).Error; err != nil {
if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: global.CONF.System.Version}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "SystemStatus", Value: "Free"}).Error; err != nil {

View File

@@ -20,6 +20,7 @@ func Init() {
baseDir := "/opt"
port := "9999"
mode := ""
version := ""
fileOp := files.NewFileOp()
v := viper.NewWithOptions()
v.SetConfigType("yaml")
@@ -38,23 +39,9 @@ func Init() {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
} else {
stdout, err := cmd.Exec("grep '^BASE_DIR=' /usr/bin/1pctl | cut -d'=' -f2")
if err != nil {
panic(err)
}
baseDir = strings.ReplaceAll(stdout, "\n", "")
if len(baseDir) == 0 {
panic("error `BASE_DIR` find in /usr/bin/1pctl")
}
stdoutPort, err := cmd.Exec("grep '^PANEL_PORT=' /usr/bin/1pctl | cut -d'=' -f2")
if err != nil {
panic(err)
}
port = strings.ReplaceAll(stdoutPort, "\n", "")
if len(port) == 0 {
panic("error `PANEL_PORT` find in /usr/bin/1pctl")
}
baseDir = loadParams("BASE_DIR")
port = loadParams("PANEL_PORT")
version = loadParams("ORIGINAL_INSTALLED_VERSION")
if strings.HasSuffix(baseDir, "/") {
baseDir = baseDir[:strings.LastIndex(baseDir, "/")]
@@ -79,16 +66,32 @@ func Init() {
if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") && serverConfig.System.Port != "" {
port = serverConfig.System.Port
}
if mode == "dev" && fileOp.Stat("/opt/1panel/conf/app.yaml") && serverConfig.System.Version != "" {
version = serverConfig.System.Version
}
global.CONF = serverConfig
global.CONF.BaseDir = baseDir
global.CONF.System.BaseDir = baseDir
global.CONF.System.IsDemo = v.GetBool("system.is_demo")
global.CONF.System.DataDir = global.CONF.BaseDir + "/1panel"
global.CONF.System.DataDir = global.CONF.System.BaseDir + "/1panel"
global.CONF.System.Cache = global.CONF.System.DataDir + "/cache"
global.CONF.System.Backup = global.CONF.System.DataDir + "/backup"
global.CONF.System.DbPath = global.CONF.System.DataDir + "/db"
global.CONF.System.LogPath = global.CONF.System.DataDir + "/log"
global.CONF.System.TmpDir = global.CONF.System.DataDir + "/tmp"
global.CONF.System.Port = port
global.CONF.System.Version = version
global.Viper = v
}
func loadParams(param string) string {
stdout, err := cmd.Execf("grep '^%s=' /usr/bin/1pctl | cut -d'=' -f2", param)
if err != nil {
panic(err)
}
baseDir := strings.ReplaceAll(stdout, "\n", "")
if len(baseDir) == 0 {
panic(fmt.Sprintf("error `%s` find in /usr/bin/1pctl", param))
}
return baseDir
}

View File

@@ -16,6 +16,7 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
appRouter.POST("/sync", baseApi.SyncApp)
appRouter.GET("/checkupdate", baseApi.GetAppListUpdate)
appRouter.POST("/search", baseApi.SearchApp)
appRouter.GET("/:key", baseApi.GetApp)
appRouter.GET("/detail/:appId/:version", baseApi.GetAppDetail)

View File

@@ -17,5 +17,6 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) {
baseRouter.GET("/status", baseApi.CheckIsFirstLogin)
baseRouter.POST("/init", baseApi.InitUserInfo)
baseRouter.POST("/logout", baseApi.LogOut)
baseRouter.GET("/demo", baseApi.CheckIsDemo)
}
}

View File

@@ -26,7 +26,8 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/content", baseApi.GetContent)
fileRouter.POST("/save", baseApi.SaveContent)
fileRouter.POST("/check", baseApi.CheckFile)
fileRouter.POST("/upload", baseApi.UploadChunkFiles)
fileRouter.POST("/upload", baseApi.UploadFiles)
fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles)
fileRouter.POST("/rename", baseApi.ChangeFileName)
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/move", baseApi.MoveFile)

View File

@@ -19,8 +19,8 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
router.POST("/search", baseApi.GetSettingInfo)
router.POST("/expired/handle", baseApi.HandlePasswordExpired)
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/expired/handle", baseApi.HandlePasswordExpired)
settingRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.POST("/port/update", baseApi.UpdatePort)
settingRouter.POST("/password/update", baseApi.UpdatePassword)

View File

@@ -1,6 +1,7 @@
package files
import (
"archive/zip"
"bufio"
"context"
"encoding/json"
@@ -12,6 +13,7 @@ import (
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -379,7 +381,9 @@ func getFormat(cType CompressType) archiver.CompressedArchive {
format.Compression = archiver.Gz{}
format.Archival = archiver.Tar{}
case Zip:
format.Archival = archiver.Zip{}
format.Archival = archiver.Zip{
Compression: zip.Deflate,
}
case Bz2:
format.Compression = archiver.Bz2{}
format.Archival = archiver.Tar{}
@@ -413,9 +417,19 @@ func (f FileOp) Compress(srcRiles []string, dst string, name string, cType Compr
return err
}
err = format.Archive(context.Background(), out, files)
if err != nil {
return err
switch cType {
case Zip:
if err := ZipFile(files, out); err != nil {
_ = f.DeleteFile(dstFile)
return err
}
default:
err = format.Archive(context.Background(), out, files)
if err != nil {
_ = f.DeleteFile(dstFile)
return err
}
break
}
return nil
}
@@ -488,3 +502,46 @@ func (f FileOp) CopyAndBackup(src string) (string, error) {
}
return backupPath, nil
}
func ZipFile(files []archiver.File, dst afero.File) error {
zw := zip.NewWriter(dst)
defer zw.Close()
for _, file := range files {
hdr, err := zip.FileInfoHeader(file)
if err != nil {
return err
}
hdr.Name = file.NameInArchive
if file.IsDir() {
if !strings.HasSuffix(hdr.Name, "/") {
hdr.Name += "/"
}
hdr.Method = zip.Store
}
w, err := zw.CreateHeader(hdr)
if err != nil {
return err
}
if file.IsDir() {
continue
}
if file.LinkTarget != "" {
_, err = w.Write([]byte(filepath.ToSlash(file.LinkTarget)))
if err != nil {
return err
}
} else {
fileReader, err := file.Open()
if err != nil {
return err
}
_, err = io.Copy(w, fileReader)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -4,11 +4,9 @@ import (
"bytes"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
gossh "golang.org/x/crypto/ssh"
)
@@ -46,7 +44,7 @@ func (c *ConnInfo) NewClient() (*ConnInfo, error) {
}
config.Timeout = c.DialTimeOut
config.HostKeyCallback = func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil }
config.HostKeyCallback = gossh.InsecureIgnoreHostKey()
client, err := gossh.Dial("tcp", addr, config)
if nil != err {
return c, err
@@ -73,9 +71,7 @@ func (c *ConnInfo) Run(shell string) (string, error) {
}
func (c *ConnInfo) Close() {
if err := c.Client.Close(); err != nil {
global.LOG.Errorf("close ssh client failed, err: %v", err)
}
_ = c.Client.Close()
}
type SshConn struct {

View File

@@ -194,11 +194,11 @@ func (p *manualDnsProvider) CleanUp(domain, token, keyAuth string) error {
func (c *AcmeClient) GetDNSResolve(domains []string) (map[string]Resolve, error) {
core, err := api.New(c.Config.HTTPClient, c.Config.UserAgent, c.Config.CADirURL, c.User.Registration.URI, c.User.Key)
if err != nil {
panic(err)
return nil, err
}
order, err := core.Orders.New(domains)
if err != nil {
panic(err)
return nil, err
}
resolves := make(map[string]Resolve)
resc, errc := make(chan acme.Authorization), make(chan domainError)

View File

@@ -19,7 +19,6 @@ declare module 'vue' {
ContainerLog: typeof import('./src/components/container-log/index.vue')['default']
DrawerHeader: typeof import('./src/components/drawer-header/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAlter: typeof import('element-plus/es')['ElAlter']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBadge: typeof import('element-plus/es')['ElBadge']
@@ -80,11 +79,13 @@ declare module 'vue' {
Loading: typeof import('element-plus/es')['ElLoadingDirective']
Logo: typeof import('./src/components/app-layout/menu/components/Logo.vue')['default']
Menu: typeof import('./src/components/app-layout/menu/index.vue')['default']
MsgInfo: typeof import('./src/components/msg-info/index.vue')['default']
Popover: typeof import('element-plus/es')['ElPopoverDirective']
RouterButton: typeof import('./src/components/router-button/index.vue')['default']
Status: typeof import('./src/components/status/index.vue')['default']
SubItem: typeof import('./src/components/app-layout/menu/components/sub-item.vue')['default']
SvgIcon: typeof import('./src/components/svg-icon/svg-icon.vue')['default']
SystemUpgrade: typeof import('./src/components/system-upgrade/index.vue')['default']
TableSetting: typeof import('./src/components/table-setting/index.vue')['default']
Upload: typeof import('./src/components/upload/index.vue')['default']
VCharts: typeof import('./src/components/v-charts/index.vue')['default']

View File

@@ -27,6 +27,11 @@ export namespace App {
items: App.App[];
}
export interface AppUpdateRes {
version: string;
canUpdate: boolean;
}
export interface AppDetail extends CommonModel {
appId: string;
icon: string;

View File

@@ -32,10 +32,3 @@ export interface DescriptionUpdate {
id: number;
description: string;
}
// * 文件上传模块
export namespace Upload {
export interface ResFileUrl {
fileUrl: string;
}
}

View File

@@ -6,6 +6,10 @@ export const SyncApp = () => {
return http.post<any>('apps/sync', {});
};
export const GetAppListUpdate = () => {
return http.get<App.AppUpdateRes>('apps/checkupdate');
};
export const SearchApp = (req: App.AppReq) => {
return http.post<App.AppResPage>('apps/search', req);
};

View File

@@ -28,6 +28,11 @@ export const loginStatus = () => {
export const checkIsFirst = () => {
return http.get<boolean>('/auth/status');
};
export const initUser = (params: Login.InitUser) => {
return http.post(`/auth/init`, params);
};
export const checkIsDemo = () => {
return http.get<boolean>('/auth/demo');
};

View File

@@ -59,6 +59,10 @@ export const UploadFileData = (params: FormData, config: AxiosRequestConfig) =>
return http.upload<File.File>('files/upload', params, config);
};
export const ChunkUploadFileData = (params: FormData, config: AxiosRequestConfig) => {
return http.upload<File.File>('files/chunkupload', params, config);
};
export const RenameRile = (params: File.FileRename) => {
return http.post<File.File>('files/rename', params);
};
@@ -72,7 +76,7 @@ export const MoveFile = (params: File.FileMove) => {
};
export const DownloadFile = (params: File.FileDownload) => {
return http.download<BlobPart>('files/download', params, { responseType: 'blob' });
return http.download<BlobPart>('files/download', params, { responseType: 'blob', timeout: 20000 });
};
export const ComputeDirSize = (params: File.DirSizeReq) => {

View File

@@ -1,17 +0,0 @@
import { Upload } from '@/api/interface/index';
import { PORT1 } from '@/api/config/service-port';
import http from '@/api';
/**
* @name 文件上传模块
*/
// * 图片上传
export const uploadImg = (params: FormData) => {
return http.post<Upload.ResFileUrl>(PORT1 + `/file/upload/img`, params);
};
// * 视频上传
export const uploadVideo = (params: FormData) => {
return http.post<Upload.ResFileUrl>(PORT1 + `/file/upload/video`, params);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -4,14 +4,18 @@
</div>
<div class="footer flx-justify-between">
<div class="footer-left">
<a href="https://fit2cloud.com/" target="_blank">杭州飞致云信息科技有限公司</a>
<a href="https://fit2cloud.com/" target="_blank">Copyright @2014-2023 FIT2CLOUND 飞致云</a>
</div>
<div class="footer-right">
<span>Version V1</span>
<SystemUpgrade />
</div>
</div>
</template>
<script setup lang="ts">
import SystemUpgrade from '@/components/system-upgrade/index.vue';
</script>
<style scoped lang="scss">
.footer {
height: 45px;

View File

@@ -57,15 +57,17 @@ const loadStatus = async () => {
await getSystemAvailable()
.then((res) => {
if (res) {
location.reload();
clearInterval(Number(timer));
timer = null;
}
})
.catch(() => {
location.reload();
clearInterval(Number(timer));
timer = null;
});
}, 1000 * 20);
}, 1000 * 10);
}
};

View File

@@ -28,7 +28,7 @@
<el-table-column :label="$t('commons.table.operate')">
<template #default="{ row, $index }">
<div>
<el-button link v-if="row.edit" type="primary" @click="saveGroup(groupForm, row, true)">
<el-button link v-if="row.edit" type="primary" @click="saveGroup(groupForm, row)">
{{ $t('commons.button.save') }}
</el-button>
<el-button link v-if="!row.edit" type="primary" @click="editGroup($index)">
@@ -46,12 +46,7 @@
<el-button link v-if="row.edit" type="primary" @click="search()">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
link
v-if="!row.edit && !row.isDefault"
type="primary"
@click="saveGroup(groupForm, row, false)"
>
<el-button link v-if="!row.edit && !row.isDefault" type="primary" @click="setDefault(row)">
{{ $t('website.setDefault') }}
</el-button>
</div>
@@ -83,7 +78,7 @@ interface DialogProps {
type: string;
}
const groupForm = ref();
const groupForm = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
type.value = params.type;
open.value = true;
@@ -106,16 +101,13 @@ const search = () => {
});
};
const saveGroup = async (formEl: FormInstance, group: Group.GroupInfo, isEdit: boolean) => {
const saveGroup = async (formEl: FormInstance, group: Group.GroupInfo) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
group.type = type.value;
if (!isEdit) {
group.isDefault = true;
}
if (group.id == 0) {
CreateGroup(group).then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
@@ -130,6 +122,14 @@ const saveGroup = async (formEl: FormInstance, group: Group.GroupInfo, isEdit: b
});
};
const setDefault = (group: Group.GroupInfo) => {
group.isDefault = true;
UpdateGroup(group).then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();
});
};
const openCreate = () => {
for (const d of data.value) {
if (d.name == '') {

View File

@@ -0,0 +1,34 @@
<template>
<el-tooltip placement="top-start">
<template #content>
<div class="info-break" :style="{ width: width + 'px' }">{{ info }}</div>
</template>
<div class="info-hidden" :style="{ width: width + 'px' }">{{ info }}</div>
</el-tooltip>
</template>
<script lang="ts" setup>
defineProps({
width: {
type: String,
default: '100',
},
info: {
type: String,
default: '',
},
});
</script>
<style scoped>
.info-hidden {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-break {
width: 100px;
word-break: break-all;
word-wrap: break-word;
}
</style>

View File

@@ -25,7 +25,7 @@ const getType = (status: string) => {
case 'error':
return 'danger';
case 'stopped':
return 'warning';
return 'danger';
default:
return '';
}

View File

@@ -0,0 +1,115 @@
<template>
<div>
<span class="version">{{ version }}</span>
<el-button v-if="version !== 'Waiting'" type="primary" link @click="onLoadUpgradeInfo">
{{ $t('setting.upgradeCheck') }}
</el-button>
<el-tag v-else round style="margin-left: 10px">{{ $t('setting.upgrading') }}</el-tag>
</div>
<el-drawer :close-on-click-modal="false" :key="refresh" v-model="drawerVisiable" size="50%" append-to-body>
<template #header>
<DrawerHeader :header="$t('setting.upgrade')" :back="handleClose" />
</template>
<div class="panel-MdEditor">
<div class="default-theme">
<h2 class="inline-block">{{ $t('setting.newVersion') }}</h2>
<el-tag class="inline-block tag">{{ upgradeInfo.newVersion }}</el-tag>
</div>
<MdEditor v-model="upgradeInfo.releaseNote" previewOnly />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onUpgrade">{{ $t('setting.upgradeNow') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { getSettingInfo, loadUpgradeInfo, upgrade } from '@/api/modules/setting';
import MdEditor from 'md-editor-v3';
import i18n from '@/lang';
import 'md-editor-v3/lib/style.css';
import { MsgSuccess } from '@/utils/message';
import { onMounted, ref } from 'vue';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const version = ref();
let loading = ref(false);
const drawerVisiable = ref(false);
const upgradeInfo = ref();
const refresh = ref();
const search = async () => {
const res = await getSettingInfo();
version.value = res.data.systemVersion;
};
const handleClose = () => {
drawerVisiable.value = false;
};
const onLoadUpgradeInfo = async () => {
loading.value = true;
await loadUpgradeInfo()
.then((res) => {
loading.value = false;
if (!res.data) {
MsgSuccess(i18n.global.t('setting.noUpgrade'));
return;
}
upgradeInfo.value = res.data;
drawerVisiable.value = true;
})
.catch(() => {
loading.value = false;
});
};
const onUpgrade = async () => {
ElMessageBox.confirm(i18n.global.t('setting.upgradeHelper', i18n.global.t('setting.upgrade')), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
globalStore.isLoading = true;
await upgrade(upgradeInfo.value.newVersion);
drawerVisiable.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
onMounted(() => {
search();
});
</script>
<style lang="scss" scoped>
.version {
font-size: 14px;
color: #858585;
text-decoration: none;
letter-spacing: 0.5px;
}
.panel-MdEditor {
height: calc(100vh - 330px);
margin-left: 70px;
.tag {
margin-left: 20px;
margin-top: -6px;
vertical-align: middle;
}
:deep(.md-editor-preview) {
font-size: 14px;
}
:deep(.default-theme h2) {
margin: 13px 0;
padding: 0;
font-size: 16px;
}
}
</style>

View File

@@ -115,12 +115,11 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
const pathRes = await loadBaseDir();
if (type.value === 'mysql') {
title.value = name.value + ' [ ' + detailName.value + ' ]';
baseDir.value = `${pathRes.data}/uploads/database/mysql/${name.value}/${detailName.value}/`;
}
if (type.value === 'website' || type.value === 'app') {
title.value = name.value;
baseDir.value = `${pathRes.data}/uploads/${type.value}/${name.value}/`;
}
baseDir.value = `${pathRes.data}/uploads/${type.value}/${name.value}/${detailName.value}/`;
upVisiable.value = true;
search();
};
@@ -204,7 +203,7 @@ const onSubmit = async () => {
MsgError(i18n.global.t('commons.msg.fileNameErr'));
return;
}
const res = await CheckFile(baseDir.value + '/' + uploaderFiles.value[0]!.raw.name);
const res = await CheckFile(baseDir.value + uploaderFiles.value[0]!.raw.name);
if (!res.data) {
MsgError(i18n.global.t('commons.msg.fileExist'));
return;

View File

@@ -7,7 +7,7 @@ export enum ResultEnum {
EXPIRED = 405,
ERRAUTH = 406,
ERRGLOBALLOADDING = 407,
TIMEOUT = 100000,
TIMEOUT = 20000,
TYPE = 'success',
}

View File

@@ -15,3 +15,66 @@ export const Mimetypes = new Map([
['gzip/document', CompressType.TarGz],
['application/x-xz', CompressType.Xz],
]);
export const Languages = [
{
label: 'plaintext',
value: 'plaintext',
},
{
label: 'json',
value: 'json',
},
{
label: 'go',
value: 'go',
},
{
label: 'html',
value: 'html',
},
{
label: 'javascript',
value: 'javascript',
},
{
label: 'java',
value: 'java',
},
{
label: 'kotlin',
value: 'kotlin',
},
{
label: 'markdown',
value: 'markdown',
},
{
label: 'mysql',
value: 'mysql',
},
{
label: 'php',
value: 'php',
},
{
label: 'redis',
value: 'redis',
},
{
label: 'shell',
value: 'shell',
},
{
label: 'sql',
value: 'sql',
},
{
label: 'yaml',
value: 'yaml',
},
{
label: 'css',
value: 'css',
},
];

View File

@@ -1,49 +0,0 @@
import { ElNotification } from 'element-plus';
/**
* @description 接收数据流生成blob创建链接下载文件
* @param {Function} api 导出表格的api方法(必传)
* @param {String} tempName 导出的文件名(必传)
* @param {Object} params 导出的参数(默认为空对象)
* @param {Boolean} isNotify 是否有导出消息提示(默认为 true)
* @param {String} fileType 导出的文件格式(默认为.xlsx)
* @return void
* */
export const useDownload = async (
api: (param: any) => Promise<any>,
tempName: string,
params: any = {},
isNotify: boolean = true,
fileType: string = '.xlsx',
) => {
if (isNotify) {
ElNotification({
title: '温馨提示',
message: '如果数据庞大会导致下载缓慢哦请您耐心等待',
type: 'info',
duration: 3000,
});
}
try {
const res = await api(params);
// 这个地方的type,经测试不传也没事因为zip文件不知道type是什么
// const blob = new Blob([res], {
// type: "application/vnd.ms-excel;charset=UTF-8"
// });
const blob = new Blob([res]);
// 兼容edge不支持createObjectURL方法
if ('msSaveOrOpenBlob' in navigator) return window.navigator.msSaveOrOpenBlob(blob, tempName + fileType);
const blobUrl = window.URL.createObjectURL(blob);
const exportFile = document.createElement('a');
exportFile.style.display = 'none';
exportFile.download = `${tempName}${fileType}`;
exportFile.href = blobUrl;
document.body.appendChild(exportFile);
exportFile.click();
// 去除下载对url的影响
document.body.removeChild(exportFile);
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.log(error);
}
};

View File

@@ -1,23 +0,0 @@
import { onUnmounted } from 'vue';
import * as echarts from 'echarts';
/**
* @description 使用Echarts(只是为了添加图表响应式)
* @param {Element} myChart Echarts实例(必传)
* @param {Object} options 绘制Echarts的参数(必传)
* @return void
* */
export const useEcharts = (myChart: echarts.ECharts, options: echarts.EChartsCoreOption) => {
if (options && typeof options === 'object') {
myChart.setOption(options);
}
const echartsResize = () => {
myChart && myChart.resize();
};
window.addEventListener('resize', echartsResize, false);
onUnmounted(() => {
window.removeEventListener('resize', echartsResize);
});
};

View File

@@ -1,27 +0,0 @@
import { ref, onMounted, onUnmounted } from 'vue';
/**
* @description 网络是否可用
* */
export const useOnline = () => {
const online = ref(true);
const showStatus = (val: any) => {
online.value = typeof val == 'boolean' ? val : val.target.online;
};
// 在页面加载后,设置正确的网络状态
navigator.onLine ? showStatus(true) : showStatus(false);
onMounted(() => {
// 开始监听网络状态的变化
window.addEventListener('online', showStatus);
window.addEventListener('offline', showStatus);
});
onUnmounted(() => {
// 移除监听网络状态的变化
window.removeEventListener('online', showStatus);
window.removeEventListener('offline', showStatus);
});
return { online };
};

View File

@@ -35,6 +35,7 @@ export default {
saveAndEnable: 'Save and enable',
import: 'Import',
search: 'Search',
refresh: 'Refresh',
},
search: {
timeStart: 'Time start',
@@ -118,7 +119,7 @@ export default {
warnning:
'Note: [Closing the security entrance] will make your panel login address directly exposed to the Internet, very dangerous, please exercise caution',
codeInput: 'Please enter the 6-digit verification code of the MFA validator',
title: 'ModernOpenSource Linux server operation and maintenance management panel',
title: 'Linux server operation and maintenance management panel',
},
rule: {
username: 'Please enter a username',
@@ -277,9 +278,8 @@ export default {
source: 'Source',
backup: 'Backup',
permission: 'Permission',
permissionLocal: 'Local server',
permissionForIP: 'IP',
permissionAll: 'All of them (unsafe)',
permissionAll: 'All of them(%)',
rootPassword: 'Root password',
backupList: 'Backup',
backList: 'Return',
@@ -747,6 +747,9 @@ export default {
type: 'Type',
list: 'File List',
sub: 'Include subdirectory',
downlodSuccess: 'Download Success',
theme: 'Theme',
language: 'Language',
},
setting: {
all: 'All',

View File

@@ -36,6 +36,7 @@ export default {
saveAndEnable: '保存并启用',
import: '导入',
search: '搜索',
refresh: '刷新',
},
search: {
timeStart: '开始时间',
@@ -122,7 +123,7 @@ export default {
warnning: '注意关闭安全入口将使您的面板登录地址被直接暴露在互联网上非常危险请谨慎操作',
codeInput: '请输入 MFA 验证器的 6 位验证码',
mfaTitle: 'MFA认证',
title: '现代化开源的 Linux 服务器运维管理面板',
title: 'Linux 服务器运维管理面板',
},
rule: {
username: '请输入用户名',
@@ -284,9 +285,8 @@ export default {
source: '来源',
backup: '备份',
permission: '权限',
permissionLocal: '本地服务器',
permissionForIP: '指定 IP',
permissionAll: '所有人不安全',
permissionAll: '所有人(%)',
rootPassword: 'root 密码',
backupList: '备份列表',
backList: '返回列表',
@@ -754,6 +754,9 @@ export default {
root: '根目录',
list: '文件列表',
sub: '包含子目录',
downlodSuccess: '下载完成',
theme: '主题',
language: '语言',
},
setting: {
all: '全部',

View File

@@ -286,3 +286,4 @@
font-size: 14px;
color: #646a73;
}

View File

@@ -38,6 +38,9 @@ html.dark {
.el-tag.el-tag--success {
--el-tag-border-color: var(--el-color-success);
}
.el-tag.el-tag--danger {
--el-tag-border-color: var(--el-color-danger);
}
.el-card {
--el-card-bg-color: #111417;
color: #ffffff;

View File

@@ -3,3 +3,4 @@
@use './element-dark.scss';
@use './reset.scss';
@use './var.scss';
@use 'md-editor-v3/lib/style.css';

View File

@@ -5,6 +5,11 @@ export function formatImageStdout(stdout: string) {
for (let i = 0; i < lines.length; i++) {
if (isJson(lines[i])) {
const data = JSON.parse(lines[i]);
if (data.errorDetail || data.error) {
lines[i] = data.errorDetail || data.errorDetail;
lines[i] = data.error || data.error;
continue;
}
if (data.stream) {
lines[i] = data.stream;
continue;

View File

@@ -95,7 +95,7 @@
import LayoutContent from '@/layout/layout-content.vue';
import { App } from '@/api/interface/app';
import { onMounted, reactive, ref } from 'vue';
import { GetAppTags, SearchApp, SyncApp } from '@/api/modules/app';
import { GetAppListUpdate, GetAppTags, SearchApp, SyncApp } from '@/api/modules/app';
import i18n from '@/lang';
import Detail from '../detail/index.vue';
import router from '@/routers';
@@ -147,6 +147,7 @@ const sync = () => {
SyncApp()
.then(() => {
MsgSuccess(i18n.global.t('app.syncSuccess'));
canUpdate.value = false;
search(req);
})
.finally(() => {
@@ -154,6 +155,11 @@ const sync = () => {
});
};
const getAppListUpdate = async () => {
const res = await GetAppListUpdate();
canUpdate.value = res.data.canUpdate;
};
const changeTag = (key: string) => {
req.tags = [];
activeTag.value = key;
@@ -169,6 +175,7 @@ const searchByName = (name: string) => {
};
onMounted(() => {
getAppListUpdate();
search(req);
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<LayoutContent :title="$t('app.detail')" :back-name="'App'" :v-loading="loadingDetail" :divider="true">
<LayoutContent :title="$t('app.detail')" :back-name="'App'" :divider="true">
<template #main>
<div class="brief">
<div class="brief" v-loading="loadingApp">
<el-row :gutter="20">
<div>
<el-col :span="3">
@@ -34,7 +34,7 @@
</div>
<br />
<div>
<div v-if="!loadingDetail">
<el-alert
style="width: 300px"
v-if="!appDetail.enable"
@@ -94,7 +94,6 @@
import { GetApp, GetAppDetail } from '@/api/modules/app';
import LayoutContent from '@/layout/layout-content.vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Install from './install/index.vue';
@@ -112,26 +111,30 @@ let app = ref<any>({});
let appDetail = ref<any>({});
let version = ref('');
let loadingDetail = ref(false);
let loadingApp = ref(false);
// let appKey = ref();
const installRef = ref();
const getApp = () => {
GetApp(props.appKey).then((res) => {
const getApp = async () => {
loadingApp.value = true;
try {
const res = await GetApp(props.appKey);
app.value = res.data;
version.value = app.value.versions[0];
getDetail(app.value.id, version.value);
});
} finally {
loadingApp.value = false;
}
};
const getDetail = (id: number, version: string) => {
const getDetail = async (id: number, version: string) => {
loadingDetail.value = true;
GetAppDetail(id, version)
.then((res) => {
appDetail.value = res.data;
})
.finally(() => {
loadingDetail.value = false;
});
try {
const res = await GetAppDetail(id, version);
appDetail.value = res.data;
} finally {
loadingDetail.value = false;
}
};
const toLink = (link: string) => {

View File

@@ -9,10 +9,10 @@
:disabled="p.disabled"
></el-input>
<el-input
v-model="form[p.envKey]"
v-model.number="form[p.envKey]"
@blur="form[p.envKey] = Number(form[p.envKey])"
v-if="p.type == 'number'"
:type="p.type"
maxlength="15"
@change="updateParam"
:disabled="p.disabled"
></el-input>

View File

@@ -8,7 +8,7 @@
show-icon
:closable="false"
/>
<el-col :span="20" :offset="2">
<el-col :span="20" :offset="2" v-if="open">
<br />
<el-descriptions border :column="1">
<el-descriptions-item v-for="(item, key) in map" :key="key">

View File

@@ -1,5 +1,5 @@
<template>
<LayoutContent v-loading="loading" :title="activeName" :divider="true">
<LayoutContent v-loading="loading || syncLoading" :title="activeName" :divider="true">
<template #toolbar>
<el-row :gutter="5">
<el-col :span="20">
@@ -203,6 +203,7 @@ import { MsgSuccess } from '@/utils/message';
let data = ref<any>();
let loading = ref(false);
let syncLoading = ref(false);
let timer: NodeJS.Timer | null = null;
const paginationConfig = reactive({
currentPage: 1,
@@ -235,14 +236,14 @@ let activeName = ref(i18n.global.t('app.installed'));
let mode = ref('installed');
const sync = () => {
loading.value = true;
syncLoading.value = true;
SyncInstalledApp()
.then(() => {
MsgSuccess(i18n.global.t('app.syncSuccess'));
search();
})
.finally(() => {
loading.value = false;
syncLoading.value = false;
});
};

View File

@@ -24,7 +24,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item v-if="form.from === 'edit'" prop="name">
<el-form-item v-if="form.from === 'edit' || form.from === 'template'" prop="name">
<el-input @input="changePath" v-model.trim="form.name">
<template #prepend>{{ $t('file.dir') }}</template>
</el-input>

View File

@@ -84,7 +84,7 @@
<el-form-item :label="$t('container.cpuQuota')" prop="nanoCPUs">
<el-input type="number" style="width: 40%" v-model.number="form.nanoCPUs">
<template #append>
<el-select v-model="form.cpuUnit" disabled style="width: 65px">
<el-select v-model="form.cpuUnit" disabled style="width: 85px">
<el-option label="Core" value="Core" />
</el-select>
</template>
@@ -94,7 +94,7 @@
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
<el-input style="width: 40%" v-model.number="form.memoryItem">
<template #append>
<el-select v-model="form.memoryUnit" placeholder="Select" style="width: 65px">
<el-select v-model="form.memoryUnit" placeholder="Select" style="width: 85px">
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="GB" />

View File

@@ -61,11 +61,10 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onBeforeUnmount, ref } from 'vue';
import { ContainerStats } from '@/api/modules/container';
import { dateFormatForSecond } from '@/utils/util';
import VCharts from '@/components/v-charts/index.vue';
// import * as echarts from 'echarts';
import i18n from '@/lang';
import DrawerHeader from '@/components/drawer-header/index.vue';
@@ -223,6 +222,10 @@ const handleClose = async () => {
chartsOption.value = { cpuChart: null, memoryChart: null, ioChart: null, networkChart: null };
};
onBeforeUnmount(() => {
handleClose;
});
defineExpose({
acceptParams,
});

View File

@@ -6,7 +6,7 @@
<el-form ref="newNameRef" v-loading="loading" :model="renameForm" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('container.newName')" :rules="Rules.requiredInput" prop="newName">
<el-form-item :label="$t('container.newName')" :rules="Rules.volumeName" prop="newName">
<el-input v-model="renameForm.newName"></el-input>
</el-form-item>
</el-col>

View File

@@ -117,7 +117,7 @@ const form = reactive({
tags: [] as Array<string>,
});
const varifyPath = (rule: any, value: any, callback: any) => {
if (value.indexOf('docker-compose.yml') === -1) {
if (value.indexOf('Dockerfile') === -1) {
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['Dockerfile'])));
}
callback();

View File

@@ -19,7 +19,7 @@
<el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" />
</el-select>
</el-form-item>
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="targetName">
<el-form-item :label="$t('container.imageName')" :rules="Rules.imageName" prop="targetName">
<el-input v-model="form.targetName">
<template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
</el-input>

View File

@@ -83,6 +83,15 @@ const form = reactive({
});
const acceptParams = (): void => {
form.name = '';
form.labelStr = '';
form.labels = [];
form.optionStr = '';
form.options = [];
form.driver = '';
form.subnet = '';
form.gateway = '';
form.scope = '';
drawerVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -92,7 +101,7 @@ const handleClose = () => {
};
const rules = reactive({
name: [Rules.requiredInput, Rules.imageName],
name: [Rules.requiredInput],
driver: [Rules.requiredSelect],
});

View File

@@ -39,7 +39,6 @@
:data="data"
@search="search"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="60" />
<el-table-column
:label="$t('container.downloadUrl')"
@@ -129,11 +128,6 @@ const search = async () => {
loading.value = false;
});
};
function selectable(row) {
return !(row.name === 'Docker Hub');
}
const dialogRef = ref();
const onOpenDialog = async (
title: string,

View File

@@ -219,6 +219,7 @@ const submitStop = async () => {
await dockerOperate(param)
.then(() => {
loading.value = false;
stopVisiable.value = false;
search();
changeMode();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));

View File

@@ -49,6 +49,10 @@
&nbsp;{{ $t('cronjob.handle') }}
</el-tag>
<span class="buttons">
<el-button type="primary" @click="onRefresh" link>
{{ $t('commons.button.refresh') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onHandle(dialogData.rowData)" link>
{{ $t('commons.button.handle') }}
</el-button>
@@ -134,7 +138,7 @@
</template>
<span class="status-count">{{ dialogData.rowData!.targetDir }}</span>
<el-button
v-if="currentRecord?.status! === 'Success'"
v-if="currentRecord?.status === 'Success'"
type="primary"
style="margin-left: 10px"
link
@@ -399,6 +403,7 @@ const onHandle = async (row: Cronjob.CronjobInfo) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
searchInfo.pageSize = searchInfo.pageSize * searchInfo.page;
searchInfo.page = 1;
records.value = [];
search();
@@ -450,6 +455,14 @@ const search = async () => {
loadRecord(currentRecord.value);
searchInfo.recordTotal = res.data.total;
};
const onRefresh = () => {
records.value = [];
searchInfo.pageSize = searchInfo.pageSize * searchInfo.page;
searchInfo.page = 1;
search();
};
const onDownload = async (record: any, backupID: number) => {
if (!record.file || record.file.indexOf('/') === -1) {
MsgError(i18n.global.t('cronjob.errPath', [record.file]));

View File

@@ -1,9 +1,9 @@
<template>
<div v-loading="loading">
<el-drawer v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('database.create')" :back="handleClose" />
</template>
<el-drawer v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('database.create')" :back="handleClose" />
</template>
<div v-loading="loading">
<el-form ref="formRef" label-position="top" :model="form" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
@@ -28,7 +28,6 @@
<el-form-item :label="$t('database.permission')" prop="permission">
<el-select v-model="form.permission">
<el-option value="localhost" :label="$t('database.permissionLocal')" />
<el-option value="%" :label="$t('database.permissionAll')" />
<el-option value="ip" :label="$t('database.permissionForIP')" />
</el-select>
@@ -42,16 +41,19 @@
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="createVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
@@ -94,7 +96,7 @@ const acceptParams = (params: DialogProps): void => {
form.format = 'utf8mb4';
form.username = '';
form.password = '';
form.permission = 'localhost';
form.permission = '%';
form.permissionIPs = '';
form.description = '';
createVisiable.value = true;

View File

@@ -331,7 +331,7 @@ const buttons = [
privilegeIPs: '',
password: '',
};
if (row.permission === '%' || row.permission === 'localhost') {
if (row.permission === '%') {
param.privilege = row.permission;
} else {
param.privilegeIPs = row.permission;

View File

@@ -28,7 +28,6 @@
<div v-if="changeForm.operation === 'privilege'">
<el-form-item :label="$t('database.permission')" prop="privilege">
<el-select style="width: 100%" v-model="changeForm.privilege">
<el-option value="localhost" :label="$t('database.permissionLocal')" />
<el-option value="%" :label="$t('database.permissionAll')" />
<el-option value="ip" :label="$t('database.permissionForIP')" />
</el-select>

View File

@@ -199,7 +199,6 @@
<script lang="ts" setup>
import { onMounted, onBeforeUnmount, ref, reactive } from 'vue';
// import * as echarts from 'echarts';
import Status from '@/views/home/status/index.vue';
import App from '@/views/home/app/index.vue';
import VCharts from '@/components/v-charts/index.vue';
@@ -211,8 +210,6 @@ import { useRouter } from 'vue-router';
import RouterButton from '@/components/router-button/index.vue';
import { loadBaseInfo, loadCurrentInfo } from '@/api/modules/dashboard';
import { getIOOptions, getNetworkOptions } from '@/api/modules/monitor';
// import { GlobalStore } from '@/store';
// const globalStore = GlobalStore();
const router = useRouter();
const statuRef = ref();
@@ -451,7 +448,6 @@ function loadUpTime(uptime: number) {
const loadData = async () => {
if (chartOption.value === 'io') {
chartsOption.value['ioChart'] = {
// title: i18n.global.t('home.io'),
xDatas: timeIODatas.value,
yDatas: [
{
@@ -465,54 +461,8 @@ const loadData = async () => {
],
formatStr: 'MB',
};
// let ioReadYDatas = {
// name: i18n.global.t('monitor.read'),
// type: 'line',
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: 'rgba(27, 143, 60, 1)',
// },
// {
// offset: 1,
// color: 'rgba(27, 143, 60, 0)',
// },
// ]),
// },
// data: ioReadBytes.value,
// showSymbol: false,
// };
// let ioWriteYDatas = {
// name: i18n.global.t('monitor.write'),
// type: 'line',
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: 'rgba(0, 94, 235, 1)',
// },
// {
// offset: 1,
// color: 'rgba(0, 94, 235, 0)',
// },
// ]),
// },
// data: ioWriteBytes.value,
// showSymbol: false,
// };
// freshChart(
// 'ioChart',
// [i18n.global.t('monitor.read'), i18n.global.t('monitor.write')],
// timeIODatas.value,
// [ioReadYDatas, ioWriteYDatas],
// i18n.global.t('home.io'),
// 'MB',
// );
} else {
chartsOption.value['networkChart'] = {
// title: i18n.global.t('home.network'),
xDatas: timeNetDatas.value,
yDatas: [
{
@@ -526,116 +476,9 @@ const loadData = async () => {
],
formatStr: 'KB/s',
};
// let netTxYDatas = {
// name: i18n.global.t('monitor.up'),
// type: 'line',
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: 'rgba(0, 94, 235, .5)',
// },
// {
// offset: 1,
// color: 'rgba(0, 94, 235, 0)',
// },
// ]),
// },
// data: netBytesRecvs.value,
// showSymbol: false,
// };
// let netRxYDatas = {
// name: i18n.global.t('monitor.down'),
// type: 'line',
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: 'rgba(27, 143, 60, .5)',
// },
// {
// offset: 1,
// color: 'rgba(27, 143, 60, 0)',
// },
// ]),
// },
// data: netBytesSents.value,
// showSymbol: false,
// };
// freshChart(
// 'networkChart',
// [i18n.global.t('monitor.up'), i18n.global.t('monitor.down')],
// timeNetDatas.value,
// [netTxYDatas, netRxYDatas],
// i18n.global.t('home.network'),
// 'KB/s',
// );
}
};
// function freshChart(chartName: string, legendDatas: any, xDatas: any, yDatas: any, yTitle: string, formatStr: string) {
// if (isInit.value) {
// echarts.init(document.getElementById(chartName) as HTMLElement);
// isInit.value = false;
// }
// let itemChart = echarts.getInstanceByDom(document.getElementById(chartName) as HTMLElement);
// const theme = globalStore.$state.themeConfig.theme || 'light';
// const option = {
// title: [
// {
// left: 'center',
// text: yTitle,
// show: false,
// },
// ],
// zlevel: 1,
// z: 1,
// tooltip: {
// trigger: 'axis',
// formatter: function (datas: any) {
// let res = datas[0].name + '<br/>';
// for (const item of datas) {
// res += item.marker + ' ' + item.seriesName + '' + item.data + formatStr + '<br/>';
// }
// return res;
// },
// },
// grid: { left: '7%', right: '7%', bottom: '20%' },
// legend: {
// data: legendDatas,
// right: 10,
// itemWidth: 8,
// textStyle: {
// color: '#646A73',
// },
// icon: 'circle',
// },
// xAxis: { data: xDatas, boundaryGap: false },
// yAxis: {
// name: '( ' + formatStr + ' )',
// splitLine: {
// //分隔辅助线
// lineStyle: {
// type: 'dashed', //线的类型 虚线0
// opacity: theme === 'dark' ? 0.1 : 1, //透明度
// },
// },
// },
// series: yDatas,
// dataZoom: [{ startValue: xDatas[0] }],
// };
// itemChart?.setOption(option, true);
// }
// function changeChartSize() {
// if (document.getElementById('ioChart') != null) {
// echarts.getInstanceByDom(document.getElementById('ioChart') as HTMLElement)?.resize();
// }
// if (document.getElementById('networkChart') != null) {
// echarts.getInstanceByDom(document.getElementById('networkChart') as HTMLElement)?.resize();
// }
// }
onMounted(() => {
onLoadNetworkOptions();
onLoadIOOptions();
@@ -645,7 +488,6 @@ onMounted(() => {
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
// window.removeEventListener('resize', changeChartSize);
});
</script>

View File

@@ -6,9 +6,20 @@
destroy-on-close
width="70%"
@opened="onOpen"
class="coder-dialog"
>
<div v-loading="loading">
<el-form :inline="true" :model="config">
<el-form-item :label="$t('file.theme')">
<el-select v-model="config.theme" @change="initEditor()">
<el-option v-for="item in themes" :key="item.label" :value="item.value" :label="item.label" />
</el-select>
</el-form-item>
<el-form-item :label="$t('file.language')">
<el-select v-model="config.language" @change="initEditor()">
<el-option v-for="lang in Languages" :key="lang.label" :value="lang.value" :label="lang.label" />
</el-select>
</el-form-item>
</el-form>
<div class="coder-editor" v-loading="loading">
<div id="codeBox" style="height: 60vh"></div>
</div>
<template #footer>
@@ -25,7 +36,8 @@ import { SaveFileContent } from '@/api/modules/files';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import * as monaco from 'monaco-editor';
import { ref } from 'vue';
import { reactive, ref } from 'vue';
import { Languages } from '@/global/mimetype';
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
@@ -36,9 +48,33 @@ interface EditProps {
name: string;
}
interface EditorConfig {
theme: string;
language: string;
}
let open = ref(false);
let loading = ref(false);
let language = ref('json');
let config = reactive<EditorConfig>({
theme: 'vs-dark',
language: 'plaintext',
});
const themes = [
{
label: 'Visual Studio',
value: 'vs',
},
{
label: 'Visual Studio Dark',
value: 'vs-dark',
},
{
label: 'High Contrast Dark',
value: 'hc-black',
},
];
let form = ref({
content: '',
@@ -61,13 +97,13 @@ const initEditor = () => {
}
const codeBox = document.getElementById('codeBox');
editor = monaco.editor.create(codeBox as HTMLElement, {
theme: 'vs-dark', //官方自带三种主题vs, hc-black, or vs-dark
theme: config.theme,
value: form.value.content,
readOnly: false,
automaticLayout: true,
language: language.value,
folding: true, //代码折叠
roundedSelection: false, // 右侧不显示编辑器预览框
language: config.language,
folding: true,
roundedSelection: false,
});
editor.onDidChangeModelContent(() => {
@@ -95,6 +131,7 @@ const saveContent = (closePage: boolean) => {
const acceptParams = (props: EditProps) => {
form.value.content = props.content;
form.value.path = props.path;
config.language = props.language;
open.value = true;
};
@@ -106,7 +143,7 @@ defineExpose({ acceptParams });
</script>
<style lang="scss">
.coder-dialog {
--el-dialog-margin-top: 10vh;
.coder-editor {
margin-top: 10px;
}
</style>

View File

@@ -165,7 +165,7 @@ import CodeEditor from './code-editor/index.vue';
import Wget from './wget/index.vue';
import Move from './move/index.vue';
import Download from './download/index.vue';
import { Mimetypes } from '@/global/mimetype';
import { Mimetypes, Languages } from '@/global/mimetype';
import Process from './process/index.vue';
// import Detail from './detail/index.vue';
import { useRouter } from 'vue-router';
@@ -183,7 +183,7 @@ let selects = ref<any>([]);
let req = reactive({
path: '/',
expand: true,
showHidden: false,
showHidden: true,
page: 1,
pageSize: 100,
search: '',
@@ -196,7 +196,7 @@ let pathWidth = ref(0);
const fileCreate = reactive({ path: '/', isDir: false, mode: 0o755 });
const fileCompress = reactive({ files: [''], name: '', dst: '', operate: 'compress' });
const fileDeCompress = reactive({ path: '', name: '', dst: '', mimeType: '' });
const fileEdit = reactive({ content: '', path: '', name: '' });
const fileEdit = reactive({ content: '', path: '', name: '', language: 'plaintext' });
const codeReq = reactive({ path: '', expand: false, page: 1, pageSize: 100 });
const fileUpload = reactive({ path: '' });
const fileRename = reactive({ path: '', oldName: '' });
@@ -382,12 +382,25 @@ const openDeCompress = (item: File.File) => {
const openCodeEditor = (row: File.File) => {
codeReq.path = row.path;
codeReq.expand = true;
GetFileContent(codeReq).then((res) => {
fileEdit.content = res.data.content;
fileEdit.path = res.data.path;
fileEdit.name = res.data.name;
codeEditorRef.value.acceptParams(fileEdit);
});
if (row.extension != '') {
Languages.forEach((language) => {
const ext = row.extension.substring(1);
if (language.value == ext) {
fileEdit.language = language.value;
}
});
}
GetFileContent(codeReq)
.then((res) => {
fileEdit.content = res.data.content;
fileEdit.path = res.data.path;
fileEdit.name = res.data.name;
codeEditorRef.value.acceptParams(fileEdit);
})
.catch(() => {});
};
const openUpload = () => {
@@ -441,6 +454,7 @@ const openDownload = () => {
fileDownload.name = selects.value.length > 1 ? getRandomStr(6) : selects.value[0].name;
downloadRef.value.acceptParams(fileDownload);
} else {
loading.value = true;
fileDownload.name = selects.value[0].name;
DownloadFile(fileDownload as File.FileDownload)
.then((res) => {

View File

@@ -7,7 +7,8 @@
:title="$t('file.downloadProcess')"
>
<div v-for="(value, index) in res" :key="index">
<span>{{ $t('file.downloading') }} {{ value['name'] }}</span>
<span>{{ value['percent'] === 100 ? $t('file.downlodSuccess') : $t('file.downloading') }}</span>
<MsgInfo :info="value['name']" width="250" />
<el-progress v-if="value['total'] == 0" :percentage="100" :indeterminate="true" :duration="1" />
<el-progress v-else :text-inside="true" :stroke-width="15" :percentage="value['percent']"></el-progress>
<span>
@@ -22,6 +23,7 @@
import { FileKeys } from '@/api/modules/files';
import { computeSize } from '@/utils/util';
import { onBeforeUnmount, ref, toRefs } from 'vue';
import MsgInfo from '@/components/msg-info/index.vue';
const props = defineProps({
open: {
@@ -60,7 +62,7 @@ const onClose = () => {};
const initProcess = () => {
let href = window.location.href;
let protocol = href.split('//')[0] === 'http' ? 'ws' : 'wss';
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
let ipLocal = href.split('//')[1].split('/')[0];
processSocket = new WebSocket(`${protocol}://${ipLocal}/api/v1/files/ws`);
processSocket.onopen = onOpenProcess;

View File

@@ -14,9 +14,10 @@
drag
:auto-upload="false"
ref="uploadRef"
:multiple="true"
:on-change="fileOnChange"
v-loading="loading"
:limit="1"
:on-exceed="handleExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
@@ -38,17 +39,18 @@
<script setup lang="ts">
import { ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance } from 'element-plus';
import { UploadFileData } from '@/api/modules/files';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import { ChunkUploadFileData } from '@/api/modules/files';
import i18n from '@/lang';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
interface UploadProps {
interface UploadFileProps {
path: string;
}
const uploadRef = ref<UploadInstance>();
const loading = ref(false);
let uploadPrecent = ref(0);
let open = ref(false);
@@ -67,10 +69,11 @@ const fileOnChange = (_uploadFile: UploadFile, uploadFiles: UploadFiles) => {
uploaderFiles.value = uploadFiles;
};
// const onProcess = (e: any) => {
// const { loaded, total } = e;
// uploadPrecent.value = ((loaded / total) * 100) | 0;
// };
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles();
const file = files[0] as UploadRawFile;
uploadRef.value!.handleStart(file);
};
const submit = async () => {
loading.value = true;
@@ -95,7 +98,7 @@ const submit = async () => {
formData.append('chunkCount', chunkCount.toString());
try {
await UploadFileData(formData, {
await ChunkUploadFileData(formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round(
((uploadedChunkCount + progressEvent.loaded / progressEvent.total) * 100) / chunkCount,
@@ -109,12 +112,13 @@ const submit = async () => {
}
if (uploadedChunkCount == chunkCount) {
loading.value = false;
uploadRef.value!.clearFiles();
MsgSuccess(i18n.global.t('file.uploadSuccess'));
}
}
};
const acceptParams = (props: UploadProps) => {
const acceptParams = (props: UploadFileProps) => {
path.value = props.path;
open.value = true;
uploadPrecent.value = 0;

View File

@@ -118,10 +118,12 @@ const rules = reactive({
const loadGroups = async () => {
const res = await GetGroupList({ type: 'host' });
groupList.value = res.data;
for (const item of groupList.value) {
if (item.isDefault) {
dialogData.value.rowData.groupID = item.id;
break;
if (dialogData.value.title === 'create') {
for (const item of groupList.value) {
if (item.isDefault) {
dialogData.value.rowData.groupID = item.id;
break;
}
}
}
};

View File

@@ -158,6 +158,11 @@
</el-button>
</el-form-item>
</el-form>
<div class="demo">
<span v-if="isDemo">
{{ $t('commons.login.username') }}:demo {{ $t('commons.login.password') }}:1panel
</span>
</div>
</div>
</div>
</div>
@@ -167,7 +172,7 @@
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import type { ElForm } from 'element-plus';
import { loginApi, getCaptcha, mfaLoginApi, checkIsFirst, initUser } from '@/api/modules/auth';
import { loginApi, getCaptcha, mfaLoginApi, checkIsFirst, initUser, checkIsDemo } from '@/api/modules/auth';
import { GlobalStore } from '@/store';
import { MenuStore } from '@/store/modules/menu';
import i18n from '@/lang';
@@ -180,6 +185,7 @@ const menuStore = MenuStore();
const errAuthInfo = ref(false);
const errCaptcha = ref(false);
const errMfaInfo = ref(false);
const isDemo = ref(false);
const isFirst = ref();
@@ -315,6 +321,11 @@ const checkStatus = async () => {
}
};
const checkIsSystemDemo = async () => {
const res = await checkIsDemo();
isDemo.value = res.data;
};
function checkPassword(rule: any, value: any, callback: any) {
if (registerForm.password !== registerForm.rePassword) {
return callback(new Error(i18n.global.t('commons.rule.rePassword')));
@@ -325,6 +336,7 @@ function checkPassword(rule: any, value: any, callback: any) {
onMounted(() => {
document.title = globalStore.themeConfig.panelName;
checkStatus();
checkIsSystemDemo();
document.onkeydown = (e: any) => {
e = window.event || e;
if (e.keyCode === 13) {
@@ -351,15 +363,11 @@ onMounted(() => {
}
.login-title {
// margin-top: 50px;
font-size: 30px;
letter-spacing: 0;
text-align: center;
color: #646a73;
margin-bottom: 30px;
// @media only screen and (max-width: 1280px) {
// margin-top: 20px;
// }
}
.no-border {
:deep(.el-input__wrapper) {
@@ -421,5 +429,12 @@ onMounted(() => {
height: 45px;
margin-top: 10px;
}
.demo {
text-align: center;
span {
color: red;
}
}
}
</style>

View File

@@ -55,19 +55,10 @@ const getStatus = async () => {
statusCode.value = 1;
};
// watch(
// () => screenWidth.value,
// (newVal) => {
// console.log()
// },
// );
onMounted(() => {
getStatus();
// 屏幕适配
screenWidth.value = document.body.clientWidth;
window.onresize = () => {
//屏幕尺寸变化就重新赋值
return (() => {
screenWidth.value = document.body.clientWidth;
})();
@@ -118,8 +109,6 @@ onMounted(() => {
}
.login-title {
// margin-top: 10%;
// margin-right: 15%;
margin-left: 10%;
span:first-child {
color: $primary-color;
@@ -138,11 +127,9 @@ onMounted(() => {
}
}
.login-container {
// margin-left: 15%;
margin-top: 40px;
padding: 40px 0;
width: 390px;
// height: 422px;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.55);
border-radius: 4px;

View File

@@ -8,71 +8,39 @@
</div>
<h3>{{ $t('setting.description') }}</h3>
<h3>
{{ version }}
<el-button v-if="version !== 'Waiting'" type="primary" link @click="onLoadUpgradeInfo">
{{ $t('setting.upgradeCheck') }}
</el-button>
<el-tag v-else round style="margin-left: 10px">{{ $t('setting.upgrading') }}</el-tag>
<SystemUpgrade />
</h3>
<div style="margin-top: 10px">
<el-link @click="toDoc">
<el-icon><Document /></el-icon>
<span>{{ $t('setting.doc') }}</span>
</el-link>
<el-link @click="toGithub" style="margin-left: 15px">
<svg-icon style="font-size: 7px; margin-bottom: 3px" iconName="p-huaban88"></svg-icon>
<span style="line-height: 20px">{{ $t('setting.project') }}</span>
<el-link @click="toGithub" class="system-link">
<svg-icon iconName="p-huaban88"></svg-icon>
<span>{{ $t('setting.project') }}</span>
</el-link>
<el-link @click="toIssue" style="margin-left: 15px">
<svg-icon style="font-size: 7px; margin-bottom: 3px" iconName="p-bug"></svg-icon>
<el-link @click="toIssue" class="system-link">
<svg-icon iconName="p-bug"></svg-icon>
<span>{{ $t('setting.issue') }}</span>
</el-link>
<el-link @click="toGithubStar" style="margin-left: 15px">
<svg-icon style="font-size: 7px; margin-bottom: 3px" iconName="p-star"></svg-icon>
<el-link @click="toGithubStar" class="system-link">
<svg-icon iconName="p-star"></svg-icon>
<span>{{ $t('setting.star') }}</span>
</el-link>
</div>
</div>
</template>
</LayoutContent>
<el-drawer :close-on-click-modal="false" :key="refresh" v-model="drawerVisiable" size="50%">
<template #header>
<DrawerHeader :header="$t('setting.upgrade')" :back="handleClose" />
</template>
<el-form label-width="120px">
<el-form-item :label="$t('setting.newVersion')">
<el-tag>{{ upgradeInfo.newVersion }}</el-tag>
</el-form-item>
<el-form-item :label="$t('setting.upgradeNotes')">
<MdEditor style="height: calc(100vh - 330px)" v-model="upgradeInfo.releaseNote" previewOnly />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onUpgrade">{{ $t('setting.upgradeNow') }}</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue';
import { getSettingInfo, loadUpgradeInfo, upgrade } from '@/api/modules/setting';
import { getSettingInfo, getSystemAvailable } from '@/api/modules/setting';
import { onMounted, ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import SystemUpgrade from '@/components/system-upgrade/index.vue';
const version = ref();
const upgradeInfo = ref();
const drawerVisiable = ref();
const refresh = ref();
const loading = ref();
const search = async () => {
const res = await getSettingInfo();
@@ -92,47 +60,22 @@ const toGithubStar = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank');
};
const handleClose = () => {
drawerVisiable.value = false;
};
const onLoadUpgradeInfo = async () => {
loading.value = true;
await loadUpgradeInfo()
.then((res) => {
loading.value = false;
if (!res.data) {
MsgSuccess(i18n.global.t('setting.noUpgrade'));
return;
}
upgradeInfo.value = res.data;
drawerVisiable.value = true;
})
.catch(() => {
loading.value = false;
});
};
const onUpgrade = async () => {
ElMessageBox.confirm(i18n.global.t('setting.upgradeHelper', i18n.global.t('setting.upgrade')), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(() => {
loading.value = true;
upgrade(upgradeInfo.value.newVersion)
.then(() => {
loading.value = false;
drawerVisiable.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
onMounted(() => {
search();
getSystemAvailable();
});
</script>
<style lang="scss" scoped>
.system-link {
margin-left: 15px;
.svg-icon {
font-size: 7px;
margin-bottom: 3px;
}
span {
line-height: 20px;
}
}
</style>

View File

@@ -49,7 +49,7 @@
import { onMounted, reactive, ref } from 'vue';
import { FormInstance } from 'element-plus';
import LayoutContent from '@/layout/layout-content.vue';
import { cleanMonitors, getSettingInfo, updateSetting } from '@/api/modules/setting';
import { cleanMonitors, getSettingInfo, getSystemAvailable, updateSetting } from '@/api/modules/setting';
import { useDeleteData } from '@/hooks/use-delete-data';
import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
@@ -110,5 +110,6 @@ const onClean = async () => {
onMounted(() => {
search();
getSystemAvailable();
});
</script>

View File

@@ -137,7 +137,7 @@ import { ElForm, ElMessageBox } from 'element-plus';
import { Setting } from '@/api/interface/setting';
import LayoutContent from '@/layout/layout-content.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateSetting, getMFA, bindMFA, getSettingInfo, updatePort } from '@/api/modules/setting';
import { updateSetting, getMFA, bindMFA, getSettingInfo, updatePort, getSystemAvailable } from '@/api/modules/setting';
import i18n from '@/lang';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { dateFormatSimple } from '@/utils/util';
@@ -326,5 +326,6 @@ function loadTimeOut() {
onMounted(() => {
search();
getSystemAvailable();
});
</script>

View File

@@ -16,15 +16,15 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('website.perserver')" prop="perserver">
<el-input v-model.number="form.perserver" min="1" max="65535" type="number"></el-input>
<el-input v-model.number="form.perserver" maxlength="15"></el-input>
<span class="input-help">{{ $t('website.perserverHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('website.perip')" prop="perip">
<el-input v-model.number="form.perip" type="number"></el-input>
<el-input v-model.number="form.perip" maxlength="15"></el-input>
<span class="input-help">{{ $t('website.peripHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('website.rate')" prop="rate">
<el-input v-model.number="form.rate" type="number"></el-input>
<el-input v-model.number="form.rate" maxlength="15"></el-input>
<span class="input-help">{{ $t('website.rateHelper') }}</span>
</el-form-item>
</el-form>
@@ -103,10 +103,10 @@ const search = (scopeReq: Website.NginxScopeReq) => {
for (const param of res.data.params) {
if (param.name === 'limit_conn') {
if (param.params[0] === 'perserver') {
form.perserver = Number(param.params[1]);
form.perserver = Number(param.params[1].match(/\d+/g));
}
if (param.params[0] === 'perip') {
form.perip = Number(param.params[1]);
form.perip = Number(param.params[1].match(/\d+/g));
}
}
if (param.name === 'limit_rate') {

View File

@@ -6,12 +6,12 @@
<el-switch v-model="form.enable" @change="updateEnable"></el-switch>
</el-form-item>
<el-form-item prop="cycle" :label="$t('website.cycle')">
<el-input v-model="form.cycle" type="number">
<el-input v-model.number="form.cycle" maxlength="15">
<template #append>{{ $t('website.seconds') }}</template>
</el-input>
</el-form-item>
<el-form-item prop="frequency" :label="$t('website.frequency')">
<el-input v-model="form.frequency" type="number">
<el-input v-model.number="form.frequency" maxlength="15">
<template #append>{{ $t('website.count') }}</template>
</el-input>
</el-form-item>

View File

@@ -75,14 +75,12 @@
>
<template #default="{ row }">
<el-button link :icon="Promotion" @click="openUrl(row)"></el-button>
<span>
<el-link type="primary" :underline="false" @click="openConfig(row.id)">
<span style="margin-left: 10px">{{ row.primaryDomain }}</span>
</el-link>
</span>
<el-link type="primary" :underline="false" @click="openConfig(row.id)">
<span style="margin-left: 10px">{{ row.primaryDomain }}</span>
</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.type')" fix prop="type">
<el-table-column :label="$t('commons.table.type')" fix show-overflow-tooltip prop="type">
<template #default="{ row }">
{{ $t('website.' + row.type) }}
<span v-if="row.type === 'deployment'">[{{ row.appName }}]</span>
@@ -104,12 +102,11 @@
</el-button>
</template>
</el-table-column>
<el-table-column
:label="$t('website.remark')"
show-overflow-tooltip
fix
prop="remark"
></el-table-column>
<el-table-column :label="$t('website.remark')" fix prop="remark">
<template #default="{ row }">
<MsgInfo :info="row.remark" width="120" />
</template>
</el-table-column>
<el-table-column :label="$t('website.protocol')" prop="protocol"></el-table-column>
<el-table-column :label="$t('website.expireDate')">
<template #default="{ row, $index }">
@@ -187,6 +184,7 @@ import { dateFormatSimple } from '@/utils/util';
import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n';
import { Promotion, VideoPlay, VideoPause } from '@element-plus/icons-vue';
import MsgInfo from '@/components/msg-info/index.vue';
const shortcuts = [
{
@@ -414,4 +412,10 @@ onMounted(() => {
float: right;
display: inline;
}
.tooltip {
white-space: pre-wrap; /* 自动换行 */
word-break: break-all; /* 单词内换行 */
max-width: 200px; /* 控制最大宽度 */
}
</style>

2
go.mod
View File

@@ -70,7 +70,7 @@ require (
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect

6
go.sum
View File

@@ -313,8 +313,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -955,7 +955,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=