39 Commits

Author SHA1 Message Date
ssongliu
887db0aff7 fix: 页面切换时,隐藏默认配置提示 2023-03-18 09:28:30 +00:00
ssongliu
4a974b7e0a fix: 解决计划任务备份文件失效仍能下载的问题 2023-03-18 03:06:31 +00:00
ssongliu
fb286d2def fix: 解决慢日志开启后未刷新导致无法关闭的问题 2023-03-17 14:58:31 +00:00
ssongliu
89cb9e6693 fix: 解决 mysql 数据库带 - 字符授权失败的问题 2023-03-17 11:48:30 +00:00
ssongliu
927def4472 fix: 解决 compose 创建错误未存库的问题 2023-03-17 11:40:30 +00:00
ssongliu
84fcd31704 fix: 升级版本判断逻辑修改 2023-03-17 10:12:30 +00:00
maninhill
005e5cc01f Update README.md 2023-03-17 17:46:34 +08:00
ssongliu
2896409b3a fix: 解决计划任务保存到第三方备份账号不生效的问题 2023-03-17 09:08:31 +00:00
zhengkunwang223
791641f3e1 feat: 修改 .gitignore 规则 2023-03-17 09:02:29 +00:00
wangdan
79f266bbda fix: 修复国际化问题 2023-03-17 08:06:30 +00:00
maninhill
c9edaf0d1d Update README.md 2023-03-17 14:37:53 +08:00
ssongliu
ac5f73c687 fix: 国际化换行问题修改 2023-03-17 04:20:29 +00:00
ssongliu
cc0667429a fix: redis 最大连接数限制 2023-03-17 04:18:29 +00:00
zhengkunwang223
92a5d6faeb style: 网站跳转按钮挪到网站域名设置页面 2023-03-17 04:16:30 +00:00
ssongliu
1111b6b494 fix: 增加 ip 或 域名正则校验 2023-03-17 03:28:29 +00:00
zhengkunwang223
be5a7c99e1 feat: 删除自动关闭 issue 的 action 2023-03-17 03:16:29 +00:00
ssongliu
d6a963c087 fix: 容器创建默认值修改 2023-03-17 02:04:29 +00:00
ssongliu
af753bdffe fix: 表格可点击列超长样式优化 2023-03-16 14:50:29 +00:00
zhengkunwang223
59b025353f fix: 解决手动解析模式 申请证书失败的BUG 2023-03-16 11:34:28 +00:00
zhengkunwang223
355a6b0205 feat: 网站一键部署应用,名称与单独部署应用保持一致 2023-03-16 09:46:30 +00:00
zhengkunwang223
7de80e9d5a fix: 解决打开文件编辑,控制台报错的问题 2023-03-16 09:44:28 +00:00
ssongliu
4c6d8cd20c fix: 备份恢复增加一些失败日志打印 2023-03-16 09:42:28 +00:00
ssongliu
31da89d63a fix: 数据库重启等操作增加 loading 2023-03-16 09:40:32 +00:00
zhengkunwang223
e044ca7d12 style: 修改停用提示的样式 2023-03-16 07:52:28 +00:00
ssongliu
7c037b68cd fix: 容器删除使用中对象提示信息优化 2023-03-16 07:48:28 +00:00
ssongliu
9080824a59 fix: 解决 mysql 慢日志设置修改失败的问题 2023-03-16 06:06:32 +00:00
zhengkunwang223
11f4bc2c89 fix: 解决文件和网站名称过长只显示...的问题 2023-03-16 06:04:28 +00:00
zhengkunwang223
a64ddd1eb8 feat: 修改 action name 2023-03-15 18:25:34 +08:00
1Panel-bot
2fdaecfafe feat: 删除不必要的action (#228)
feat: 删除不必要的action
2023-03-15 10:20:26 +00:00
1Panel-bot
6d1fe20736 feat: 增加 actions (#226)
feat: 增加 actions
2023-03-15 10:08:28 +00:00
wanghe
f61bc047cd Update README.md 2023-03-15 17:47:18 +08:00
ssongliu
4f52580938 fix: 容器部分已知问题修改 (#224)
1. 删除存储卷增加使用中判断
2. 容器操作时增加来源判断
3. 解决删除镜像后页面没有自动刷新的问题
2023-03-15 08:48:26 +00:00
ssongliu
281d0cf880 fix: 敏感字符增加传输加密 (#219)
1. 敏感字符增加传输加密
2023-03-15 07:58:26 +00:00
zhengkunwang223
fdf9215d43 fix: 解决打开应用详情页,控制台报错的 BUG (#218) 2023-03-15 07:56:29 +00:00
zhengkunwang223
8c182e907d feat: 增加 OWNERS 文件 2023-03-15 15:39:02 +08:00
ssongliu
e55af04568 fix: 移除调试打印信息 2023-03-15 13:00:31 +08:00
wanghe-fit2cloud
2462ffdbab refactor: issue template 2023-03-15 11:42:45 +08:00
ssongliu
bfeb57e24f fix: oss 改用分片上传与下载 2023-03-15 11:19:25 +08:00
maninhill
9bb28cda27 Update README.md 2023-03-15 09:28:37 +08:00
112 changed files with 1480 additions and 553 deletions

View File

@@ -25,7 +25,7 @@ body:
required: true
attributes:
label: "1Panel 版本"
description: "可通过系统右上角下拉菜单中的`关于`选项,或查看安装目录中的 version 文件获取。"
description: "登录 1Panel Web 控制台,在页面右下角查看当前版本。"
- type: markdown
id: details
attributes:

View File

@@ -14,7 +14,7 @@ body:
required: true
attributes:
label: "1Panel 版本"
description: "可通过系统右上角下拉菜单中的`关于`选项,或查看安装目录中的 version 文件获取。"
description: "登录 1Panel Web 控制台,在页面右下角查看当前版本。"
- type: markdown
id: details
attributes:

17
.github/workflows/add-labels-for-pr.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
on: pull_request
name: 1Panel 通用 PR 处理
permissions:
pull-requests: write
jobs:
generic_handler:
name: 为 PR 添加标签
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUBTOKEN }}
labels: ${{ github.base_ref }}

View File

@@ -0,0 +1,17 @@
on:
push:
branches:
- 'pr@**'
- 'repr@**'
name: 针对特定分支名自动创建 PR
jobs:
generic_handler:
name: 自动创建 PR
runs-on: ubuntu-latest
steps:
- name: Create pull request
uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}

16
.github/workflows/issue-close.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Issue Close Check
on:
issues:
types: [closed]
jobs:
issue-close-remove-labels:
runs-on: ubuntu-latest
steps:
- name: Remove labels
uses: actions-cool/issues-helper@v2
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'remove-labels'
labels: '状态:待处理'

38
.github/workflows/issue-comment.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
on:
issue_comment:
types: [created]
name: Add issues workflow labels
jobs:
add-label-if-is-author:
runs-on: ubuntu-latest
if: ${{ (github.event.issue.user.id == github.event.comment.user.id) && (!github.event.issue.pull_request) }}
steps:
- name: Add require handle label
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '状态:待处理'
- name: Remove require reply label
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '状态:待用户反馈'
add-label-if-not-author:
runs-on: ubuntu-latest
if: ${{ (github.event.issue.user.id != github.event.comment.user.id) && (!github.event.issue.pull_request) && (github.event.issue.state == 'open') }}
steps:
- name: Add require replay label
uses: actions-cool/issues-helper@v2
with:
actions: 'add-labels'
labels: '状态:待用户反馈'
- name: Remove require handle label
uses: actions-cool/issues-helper@v2
with:
actions: 'remove-labels'
labels: '状态:待处理'

16
.github/workflows/issue-open.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Issue Open Check
on:
issues:
types: [opened]
jobs:
issue-open-add-labels:
runs-on: ubuntu-latest
steps:
- name: Add labels
uses: actions-cool/issues-helper@v2
if: ${{ !github.event.issue.pull_request }}
with:
actions: 'add-labels'
labels: '状态:待处理'

View File

@@ -0,0 +1,17 @@
on:
schedule:
- cron: "0 1 * * *"
name: Check recent handle issues
jobs:
check-recent-issues-not-handle:
runs-on: ubuntu-latest
steps:
- name: Check recent issues and send msg
uses: jumpserver/action-issues-alert@master
with:
hook: ${{ secrets.WECHAT_GROUP_WEB_HOOK }}
type: recent
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,17 @@
on:
schedule:
- cron: "0 9 * * 1-5"
name: Check untimely handle issues
jobs:
check-untimely-handle-issues:
runs-on: ubuntu-latest
steps:
- name: Check untimely issues and send msg
uses: jumpserver/action-issues-alert@master
with:
hook: ${{ secrets.WECHAT_GROUP_WEB_HOOK }}
type: untimely
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -22,5 +22,4 @@ cmd/server/__debug_bin
cmd/server/web/assets
cmd/server/web/monacoeditorwork
cmd/server/web/index.html
cmd/server/web/favicon.png
frontend/auto-imports.d.ts

11
OWNERS Normal file
View File

@@ -0,0 +1,11 @@
reviewers:
- zhengkunwang223
- ssongliu
- wanghe-fit2cloud
- wangdan-fit2cloud
approvers:
- zhengkunwang223
- ssongliu
- wanghe-fit2cloud
- wangdan-fit2cloud

View File

@@ -11,8 +11,8 @@
1Panel 是一个现代化、开源的 Linux 服务器运维管理面板。1Panel 的功能和优势包括:
- **快速建站**:深度集成 Wordpress 和 Halo域名绑定、SSL 证书配置等一键搞定;
- **高效管理**:通过 Web 端轻松管理 Linux 服务器,包括主机监控、文件管理、数据库管理、容器管理及常用应用软件管理
- **快速建站**:深度集成 Wordpress 和 [Halo](https://github.com/halo-dev/halo/)域名绑定、SSL 证书配置等一键搞定;
- **高效管理**:通过 Web 端轻松管理 Linux 服务器,包括应用管理、主机监控、文件管理、数据库管理、容器管理
- **安全可靠**:最小漏洞暴露面,提供防火墙和安全审计等功能;
- **一键备份**:支持一键备份和恢复,备份数据云端存储,永不丢失。
@@ -30,16 +30,16 @@
**一键安装**
以 root 用户执行如下命令一键安装 1Panel:
执行如下命令一键安装 1Panel:
```sh
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sh quick_start.sh
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
```
**学习资料**
- [在线文档](https://1panel.cn/docs/)
- [入门视频](https://1panel.cn/video.html)
- [教学视频](https://space.bilibili.com/510493147/channel/collectiondetail?sid=1199760)
## 社区
@@ -56,6 +56,10 @@ curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_
- 邮箱support@fit2cloud.com
- 电话400-052-0755
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/1Panel&type=Date)](https://star-history.com/#1Panel-dev/1Panel&Date)
## License
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.

View File

@@ -1,6 +1,8 @@
package v1
import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -27,6 +29,23 @@ func (b *BaseApi) CreateBackup(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Credential) != 0 {
credential, err := base64.StdEncoding.DecodeString(req.Credential)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Credential = string(credential)
}
if len(req.AccessKey) != 0 {
accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.AccessKey = string(accessKey)
}
if err := backupService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
@@ -52,6 +71,23 @@ func (b *BaseApi) ListBuckets(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Credential) != 0 {
credential, err := base64.StdEncoding.DecodeString(req.Credential)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Credential = string(credential)
}
if len(req.AccessKey) != 0 {
accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.AccessKey = string(accessKey)
}
buckets, err := backupService.GetBuckets(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
@@ -139,7 +175,7 @@ func (b *BaseApi) DownloadRecord(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
c.File(filePath)
helper.SuccessWithData(c, filePath)
}
// @Tags Backup Account
@@ -188,6 +224,23 @@ func (b *BaseApi) UpdateBackup(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Credential) != 0 {
credential, err := base64.StdEncoding.DecodeString(req.Credential)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Credential = string(credential)
}
if len(req.AccessKey) != 0 {
accessKey, err := base64.StdEncoding.DecodeString(req.AccessKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.AccessKey = string(accessKey)
}
if err := backupService.Update(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@@ -198,7 +198,7 @@ func (b *BaseApi) TargetDownload(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
c.File(filePath)
helper.SuccessWithData(c, filePath)
}
// @Tags Cronjob

View File

@@ -2,6 +2,7 @@ package v1
import (
"context"
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@@ -29,6 +30,15 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(password)
}
if _, err := mysqlService.Create(context.Background(), req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
@@ -81,6 +91,15 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Value) != 0 {
value, err := base64.StdEncoding.DecodeString(req.Value)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Value = string(value)
}
if err := mysqlService.ChangePassword(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@@ -2,6 +2,7 @@ package v1
import (
"bufio"
"encoding/base64"
"fmt"
"os"
@@ -106,6 +107,15 @@ func (b *BaseApi) ChangeRedisPassword(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if len(req.Value) != 0 {
value, err := base64.StdEncoding.DecodeString(req.Value)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Value = string(value)
}
if err := redisService.ChangePassword(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View File

@@ -3,6 +3,14 @@ package v1
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
@@ -14,13 +22,6 @@ import (
websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
// @Tags File
@@ -444,6 +445,28 @@ func (b *BaseApi) Download(c *gin.Context) {
c.File(filePath)
}
// @Tags File
// @Summary Download file with path
// @Description 下载指定文件
// @Accept json
// @Param request body request.FilePath true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/download/bypath [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"下载文件 [path]","formatEN":"Download file [path]"}
func (b *BaseApi) DownloadFile(c *gin.Context) {
var req dto.FilePath
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
c.File(req.Path)
}
// @Tags File
// @Summary Load file size
// @Description 获取文件夹大小
@@ -558,7 +581,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
filename := c.PostForm("filename")
fileDir := filepath.Join(global.CONF.System.DataDir, "upload", filename)
os.MkdirAll(fileDir, 0755)
_ = os.MkdirAll(fileDir, 0755)
filePath := filepath.Join(fileDir, filename)
emptyFile, err := os.Create(filePath)
@@ -584,7 +607,6 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
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
}

View File

@@ -1,6 +1,8 @@
package v1
import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -29,6 +31,23 @@ func (b *BaseApi) CreateHost(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(password)
}
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.PrivateKey = string(privateKey)
}
host, err := hostService.Create(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
@@ -55,6 +74,22 @@ func (b *BaseApi) TestByInfo(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(password)
}
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.PrivateKey = string(privateKey)
}
var connInfo ssh.ConnInfo
_ = copier.Copy(&connInfo, &req)
@@ -211,6 +246,22 @@ func (b *BaseApi) UpdateHost(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(password)
}
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.PrivateKey = string(privateKey)
}
upMap := make(map[string]interface{})
upMap["name"] = req.Name

View File

@@ -22,6 +22,7 @@ type ContainerInfo struct {
State string `json:"state"`
RunTime string `json:"runTime"`
IsFromApp bool `json:"isFromApp"`
IsFromCompose bool `json:"isFromCompose"`
}

View File

@@ -24,7 +24,7 @@ type ICronjobRepo interface {
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
DeleteRecord(opts ...DBOption) error
StartRecords(cronjobID uint, targetPath string) model.JobRecords
StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords
EndRecords(record model.JobRecords, status, message, records string)
}
@@ -112,10 +112,11 @@ func (c *CronjobRepo) WithByJobID(id int) DBOption {
}
}
func (u *CronjobRepo) StartRecords(cronjobID uint, targetPath string) model.JobRecords {
func (u *CronjobRepo) StartRecords(cronjobID uint, fromLocal bool, targetPath string) model.JobRecords {
var record model.JobRecords
record.StartTime = time.Now()
record.CronjobID = cronjobID
record.FromLocal = fromLocal
record.Status = constant.StatusWaiting
if err := global.DB.Create(&record).Error; err != nil {
global.LOG.Errorf("create record status failed, err: %v", err)

View File

@@ -134,7 +134,7 @@ func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error)
tempPath := fmt.Sprintf("%sdownload%s", constant.DataDir, info.FileDir)
if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(tempPath, os.ModePerm); err != nil {
fmt.Println(err)
global.LOG.Errorf("mkdir %s failed, err: %v", tempPath, err)
}
}
targetPath := tempPath + info.FileName

View File

@@ -181,16 +181,19 @@ func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback
return err
}
if err := handleMysqlRecover(mysqlInfo, tmpPath, db.Name, fmt.Sprintf("%s.sql.gz", install.Name), true); err != nil {
global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err)
return err
}
}
if err := handleUnTar(tmpPath+"/app.tar.gz", fmt.Sprintf("%s/%s", constant.AppInstallDir, install.App.Key)); err != nil {
global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err)
return err
}
oldInstall.Status = constant.Running
if err := appInstallRepo.Save(install); err != nil {
global.LOG.Errorf("save db app install failed, err: %v", err)
return err
}
isOk = true

View File

@@ -106,8 +106,8 @@ func handleWebsiteRecover(website *model.Website, recoverFile string, isRollback
return fmt.Errorf("backup website %s for rollback before recover failed, err: %v", website.Alias, err)
}
defer func() {
global.LOG.Info("recover failed, start to rollback now")
if !isOk {
global.LOG.Info("recover failed, start to rollback now")
if err := handleWebsiteRecover(website, rollbackFile, true); err != nil {
global.LOG.Errorf("rollback website %s from %s failed, err: %v", website.Alias, rollbackFile, err)
return
@@ -126,6 +126,7 @@ func handleWebsiteRecover(website *model.Website, recoverFile string, isRollback
}
nginxConfPath := fmt.Sprintf("%s/openresty/%s/conf/conf.d", constant.AppInstallDir, nginxInfo.Name)
if err := fileOp.CopyFile(fmt.Sprintf("%s/%s.conf", tmpPath, website.Alias), nginxConfPath); err != nil {
global.LOG.Errorf("handle recover from conf.d failed, err: %v", err)
return err
}
@@ -135,22 +136,27 @@ func handleWebsiteRecover(website *model.Website, recoverFile string, isRollback
return err
}
if err := handleAppRecover(&app, fmt.Sprintf("%s/%s.app.tar.gz", tmpPath, website.Alias), true); err != nil {
global.LOG.Errorf("handle recover from app.tar.gz failed, err: %v", err)
return err
}
if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, app.App.Key, app.Name)); err != nil {
global.LOG.Errorf("docker-compose restart failed, err: %v", err)
return err
}
}
siteDir := fmt.Sprintf("%s/openresty/%s/www/sites", constant.AppInstallDir, nginxInfo.Name)
if err := handleUnTar(fmt.Sprintf("%s/%s.web.tar.gz", tmpPath, website.Alias), siteDir); err != nil {
global.LOG.Errorf("handle recover from web.tar.gz failed, err: %v", err)
return err
}
stdout, err := cmd.Execf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName)
if err != nil {
global.LOG.Errorf("nginx -s reload failed, err: %s", stdout)
return errors.New(string(stdout))
}
if err := websiteRepo.SaveWithoutCtx(&oldWebsite); err != nil {
global.LOG.Errorf("handle save website data failed, err: %v", err)
return err
}
isOk = true

View File

@@ -98,6 +98,10 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
if _, ok := container.Labels[composeProjectLabel]; ok {
IsFromCompose = true
}
IsFromApp := false
if created, ok := container.Labels[composeCreatedBy]; ok && created == "Apps" {
IsFromApp = true
}
backDatas = append(backDatas, dto.ContainerInfo{
ContainerID: container.ID,
CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
@@ -106,6 +110,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
ImageName: container.Image,
State: container.State,
RunTime: container.Status,
IsFromApp: IsFromApp,
IsFromCompose: IsFromCompose,
})
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"path"
"sort"
"strings"
"time"
@@ -152,11 +153,16 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
req.Path = path
}
global.LOG.Infof("docker-compose.yml %s create successful, start to docker-compose up", req.Name)
if stdout, err := compose.Up(req.Path); err != nil {
return errors.New(string(stdout))
}
if req.From == "path" {
req.Name = path.Base(strings.ReplaceAll(req.Path, "/docker-compose.yml", ""))
}
if stdout, err := compose.Up(req.Path); err != nil {
_, _ = compose.Down(req.Path)
return errors.New(stdout)
}
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name})
return nil
}

View File

@@ -7,6 +7,8 @@ import (
"strings"
"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/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
@@ -80,6 +82,9 @@ func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error {
}
for _, id := range req.Names {
if err := client.NetworkRemove(context.TODO(), id); err != nil {
if strings.Contains(err.Error(), "has active endpoints") {
return buserr.WithDetail(constant.ErrInUsed, id, nil)
}
return err
}
}

View File

@@ -2,12 +2,12 @@ package service
import (
"context"
"fmt"
"sort"
"strings"
"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/utils/docker"
"github.com/docker/docker/api/types"
@@ -96,6 +96,9 @@ func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error {
}
for _, id := range req.Names {
if err := client.VolumeRemove(context.TODO(), id, true); err != nil {
if strings.Contains(err.Error(), "volume is in use") {
return buserr.WithDetail(constant.ErrInUsed, id, nil)
}
return err
}
}
@@ -122,12 +125,8 @@ func (u *ContainerService) CreateVolume(req dto.VolumeCreat) error {
DriverOpts: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
stat, err := client.VolumeCreate(context.TODO(), options)
if err != nil {
if _, err := client.VolumeCreate(context.TODO(), options); err != nil {
return err
}
// if len(stat.CreatedAt) != 0 {
fmt.Println(stat)
// }
return nil
}

View File

@@ -2,17 +2,14 @@ package service
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cloud_storage"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
@@ -92,69 +89,28 @@ func (u *CronjobService) Download(down dto.CronjobDownload) (string, error) {
if cronjob.ID == 0 {
return "", constant.ErrRecordNotFound
}
global.LOG.Infof("start to download records %s from %s", cronjob.Type, backup.Type)
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
if backup.Type == "LOCAL" {
if _, err := os.Stat(record.File); err != nil && os.IsNotExist(err) {
return "", constant.ErrRecordNotFound
}
return record.File, nil
}
if record.FromLocal {
local, _ := loadLocalDir()
if _, err := os.Stat(local + "/" + record.File); err == nil {
return local + "/" + record.File, nil
}
}
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
return "", err
}
varMap["type"] = backup.Type
if backup.Type != "LOCAL" {
varMap["bucket"] = backup.Bucket
switch backup.Type {
case constant.Sftp:
varMap["username"] = backup.AccessKey
varMap["password"] = backup.Credential
case constant.OSS, constant.S3, constant.MinIo:
varMap["accessKey"] = backup.AccessKey
varMap["secretKey"] = backup.Credential
}
backClient, err := cloud_storage.NewCloudStorageClient(varMap)
if err != nil {
return "", fmt.Errorf("new cloud storage client failed, err: %v", err)
}
global.LOG.Info("new backup client successful")
commonDir := fmt.Sprintf("%s/%s/", cronjob.Type, cronjob.Name)
name := fmt.Sprintf("%s%s.tar.gz", commonDir, record.StartTime.Format("20060102150405"))
if cronjob.Type == "database" {
name = fmt.Sprintf("%s%s.gz", commonDir, record.StartTime.Format("20060102150405"))
}
tempPath := fmt.Sprintf("%s/download/%s", constant.DataDir, commonDir)
if _, err := os.Stat(tempPath); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(tempPath, os.ModePerm); err != nil {
fmt.Println(err)
}
}
global.LOG.Infof("download records %s from %s to %s", name, commonDir, tempPath)
targetPath := tempPath + strings.ReplaceAll(name, commonDir, "")
if _, err = os.Stat(targetPath); err != nil && os.IsNotExist(err) {
isOK, err := backClient.Download(name, targetPath)
if !isOK {
return "", fmt.Errorf("cloud storage download failed, err: %v", err)
}
}
return targetPath, nil
}
if _, ok := varMap["dir"]; !ok {
return "", errors.New("load local backup dir failed")
}
global.LOG.Infof("record is save in local dir %s", varMap["dir"])
switch cronjob.Type {
case "website":
return fmt.Sprintf("%v/website/%s/website_%s_%s.tar.gz", varMap["dir"], cronjob.Website, cronjob.Website, record.StartTime.Format("20060102150405")), nil
case "database":
mysqlInfo, err := appInstallRepo.LoadBaseInfo("mysql", "")
if err != nil {
return "", fmt.Errorf("load mysqlInfo failed, err: %v", err)
}
return fmt.Sprintf("%v/database/mysql/%s/%s/db_%s_%s.sql.gz", varMap["dir"], mysqlInfo.Name, cronjob.DBName, cronjob.DBName, record.StartTime.Format("20060102150405")), nil
case "directory":
return fmt.Sprintf("%v/%s/%s/directory%s_%s.tar.gz", varMap["dir"], cronjob.Type, cronjob.Name, strings.ReplaceAll(cronjob.SourceDir, "/", "_"), record.StartTime.Format("20060102150405")), nil
default:
return "", fmt.Errorf("not support type %s", cronjob.Type)
tempPath := fmt.Sprintf("%s/download/%s", constant.DataDir, record.File)
isOK, _ := client.Download(record.File, tempPath)
if !isOK || err != nil {
return "", constant.ErrRecordNotFound
}
return tempPath, nil
}
func (u *CronjobService) HandleOnce(id uint) error {

View File

@@ -22,8 +22,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
message []byte
err error
)
record := cronjobRepo.StartRecords(cronjob.ID, "")
record.FromLocal = cronjob.KeepLocal
record := cronjobRepo.StartRecords(cronjob.ID, cronjob.KeepLocal, "")
go func() {
switch cronjob.Type {
case "shell":
@@ -118,6 +117,8 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
return "", err
}
}
itemFileDir := strings.ReplaceAll(backupDir, localDir+"/", "")
if len(record.Name) != 0 {
record.FileName = fileName
record.FileDir = backupDir
@@ -125,7 +126,7 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
record.BackupType = backup.Type
if !cronjob.KeepLocal && backup.Type != "LOCAL" {
record.Source = backup.Type
record.FileDir = strings.ReplaceAll(backupDir, localDir+"/", "")
record.FileDir = itemFileDir
}
if err := backupRepo.CreateRecord(&record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)
@@ -133,7 +134,10 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
}
}
fullPath := fmt.Sprintf("%s/%s", record.FileDir, fileName)
fullPath := fmt.Sprintf("%s/%s", backupDir, fileName)
if backup.Type != "LOCAL" {
fullPath = fmt.Sprintf("%s/%s", itemFileDir, fileName)
}
if backup.Type == "LOCAL" {
u.HandleRmExpired(backup.Type, backupDir, cronjob, nil)
return fullPath, nil
@@ -148,10 +152,13 @@ func (u *CronjobService) HandleBackup(cronjob *model.Cronjob, startTime time.Tim
if err != nil {
return fullPath, err
}
if _, err = client.Upload(backupDir+"/"+fileName, fullPath); err != nil {
if _, err = client.Upload(backupDir+"/"+fileName, itemFileDir+"/"+fileName); err != nil {
return fullPath, err
}
u.HandleRmExpired(backup.Type, backupDir, cronjob, client)
u.HandleRmExpired(backup.Type, itemFileDir, cronjob, client)
if cronjob.KeepLocal {
u.HandleRmExpired("LOCAL", backupDir, cronjob, client)
}
return fullPath, nil
}
@@ -185,9 +192,7 @@ func (u *CronjobService) HandleRmExpired(backType, backupDir string, cronjob *mo
for i := 0; i < len(currentObjs)-int(cronjob.RetainCopies); i++ {
_, _ = backClient.Delete(currentObjs[i].(string))
}
if !cronjob.KeepLocal {
return
}
return
}
files, err := ioutil.ReadDir(backupDir)
if err != nil {

View File

@@ -292,7 +292,7 @@ func (u *MysqlService) ChangeAccess(info dto.ChangeDBInfo) error {
if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create user if not exists '%s'@'%s' identified by '%s';", mysql.Username, info.Value, mysql.Password)); err != nil {
return err
}
grantStr := fmt.Sprintf("grant all privileges on %s.* to '%s'@'%s'", mysql.Name, mysql.Username, info.Value)
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to '%s'@'%s'", mysql.Name, mysql.Username, info.Value)
if app.Version == "5.7.39" {
grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, mysql.Password)
}

View File

@@ -15,6 +15,7 @@ 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/docker"
@@ -378,8 +379,14 @@ func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
if err != nil {
return err
}
for _, ids := range req.Names {
if _, err := client.ImageRemove(context.TODO(), ids, types.ImageRemoveOptions{Force: true, PruneChildren: true}); err != nil {
for _, id := range req.Names {
if _, err := client.ImageRemove(context.TODO(), id, types.ImageRemoveOptions{Force: true, PruneChildren: true}); err != nil {
if strings.Contains(err.Error(), "image is being used") {
if strings.Contains(id, "sha256:") {
return buserr.New(constant.ErrObjectInUsed)
}
return buserr.WithDetail(constant.ErrInUsed, id, nil)
}
return err
}
}

View File

@@ -4,12 +4,15 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type LogService struct{}
const logs = "https://resource.fit2cloud.com/installation-log.sh"
type ILogService interface {
CreateLoginLog(operation model.LoginLog) error
PageLoginLog(search dto.SearchLgLogWithPage) (int64, interface{}, error)
@@ -77,3 +80,7 @@ func (u *LogService) CleanLogs(logtype string) error {
}
return logRepo.CleanLogin()
}
func writeLogs(version string) {
_, _ = cmd.Execf("curl -sfL %s | sh -s 1p upgrade %s", logs, version)
}

View File

@@ -6,13 +6,13 @@ import (
"net/http"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
)
@@ -43,8 +43,8 @@ func (u *UpgradeService) SearchUpgrade() (*dto.UpgradeInfo, error) {
if err != nil {
return nil, err
}
isNew, err := compareVersion(currentVersion.Value, string(version))
if !isNew || err != nil {
isNew := common.CompareVersion(string(version), currentVersion.Value)
if !isNew {
return nil, err
}
@@ -128,6 +128,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
}
global.LOG.Info("upgrade successful!")
go writeLogs(req.Version)
_ = settingRepo.Update("SystemVersion", req.Version)
_ = settingRepo.Update("SystemStatus", "Free")
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
@@ -175,43 +176,3 @@ func (u *UpgradeService) handleRollback(fileOp files.FileOp, originalDir string,
}
}
func compareVersion(version, newVersion string) (bool, error) {
if version == newVersion {
return false, nil
}
if len(version) == 0 || len(newVersion) == 0 {
return false, fmt.Errorf("incorrect version or new version entered %v -- %v", version, newVersion)
}
versions := strings.Split(strings.ReplaceAll(version, "v", ""), ".")
if len(versions) != 3 {
return false, fmt.Errorf("incorrect version input %v", version)
}
newVersions := strings.Split(strings.ReplaceAll(newVersion, "v", ""), ".")
if len(newVersions) != 3 {
return false, fmt.Errorf("incorrect newVersions input %v", version)
}
version1, _ := strconv.Atoi(versions[0])
newVersion1, _ := strconv.Atoi(newVersions[0])
if newVersion1 > version1 {
return true, nil
} else if newVersion1 == version1 {
version2, _ := strconv.Atoi(versions[1])
newVersion2, _ := strconv.Atoi(newVersions[1])
if newVersion2 > version2 {
return true, nil
} else if newVersion2 == version2 {
version3, _ := strconv.Atoi(versions[2])
newVersion3, _ := strconv.Atoi(newVersions[2])
if newVersion3 > version3 {
return true, nil
} else {
return false, nil
}
} else {
return false, nil
}
} else {
return false, nil
}
}

View File

@@ -99,6 +99,10 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
if err := client.UseHTTP(path.Join(constant.AppInstallDir, constant.AppOpenresty, appInstall.Name, "root")); err != nil {
return res, err
}
case constant.DnsManual:
if err := client.UseManualDns(); err != nil {
return res, err
}
}
domains := []string{create.PrimaryDomain}

View File

@@ -95,3 +95,9 @@ var (
var (
ErrTypeOfRedis = "ErrTypeOfRedis"
)
//container
var (
ErrInUsed = "ErrInUsed"
ErrObjectInUsed = "ErrObjectInUsed"
)

View File

@@ -53,3 +53,7 @@ ErrDatabaseIsExist: "The current database already exists. Please enter a new dat
#redis
ErrTypeOfRedis: "The recovery file type does not match the current persistence mode. Modify the file type and try again"
#container
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
ErrObjectInUsed: "This object is in use and cannot be deleted"

View File

@@ -53,3 +53,7 @@ ErrDatabaseIsExist: "当前数据库已存在,请重新输入"
#redis
ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试"
#container
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
ErrObjectInUsed: "该对象正被使用,无法删除"

View File

@@ -1,7 +1,6 @@
package app
import (
"fmt"
"path"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -35,12 +34,12 @@ func createDir(fileOp files.FileOp, dirPath string) {
func createDefaultDockerNetwork() {
cli, err := docker.NewClient()
if err != nil {
fmt.Println("init docker client error", err.Error())
global.LOG.Errorf("init docker client error", err.Error())
return
}
if !cli.NetworkExist("1panel-network") {
if err := cli.CreateNetwork("1panel-network"); err != nil {
fmt.Println("init docker client error", err.Error())
global.LOG.Errorf("init docker client error", err.Error())
return
}
}

View File

@@ -1,7 +1,6 @@
package business
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/global"
)
@@ -13,7 +12,6 @@ func Init() {
return
}
if setting.AppStoreVersion != "" {
fmt.Println(setting.AppStoreVersion)
global.LOG.Info("do not sync")
return
}

View File

@@ -1,9 +1,6 @@
package log
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"io/ioutil"
"log"
"os"
@@ -14,6 +11,9 @@ import (
"sync/atomic"
"time"
"unsafe"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
)
type Writer struct {
@@ -261,7 +261,6 @@ func (w *Writer) CompressFile(logFile string) error {
comFileName := path.Base(logFile) + ".gz"
filePath := path.Dir(logFile)
fmt.Println(path.Dir(logFile))
if err := op.Compress([]string{logFile}, filePath, comFileName, files.Gz); err != nil {
return err
}

View File

@@ -96,7 +96,6 @@ func OperationLog() gin.HandlerFunc {
var names []string
if funcs.IsList {
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColume, funcs.DB, funcs.InputColume)
fmt.Println(value)
_ = global.DB.Raw(sql, value).Scan(&names)
} else {
_ = global.DB.Raw(fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColume, funcs.DB, funcs.InputColume), value).Scan(&names)

View File

@@ -32,6 +32,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.POST("/download", baseApi.Download)
fileRouter.POST("/download/bypath", baseApi.DownloadFile)
fileRouter.POST("/size", baseApi.Size)
fileRouter.GET("/ws", baseApi.Ws)
fileRouter.GET("/keys", baseApi.Keys)

View File

@@ -77,7 +77,7 @@ func (oss ossClient) Upload(src, target string) (bool, error) {
if err != nil {
return false, err
}
err = bucket.PutObjectFromFile(target, src)
err = bucket.UploadFile(target, src, 200*1024*1024, osssdk.Routines(5), osssdk.Checkpoint(true, ""))
if err != nil {
return false, err
}
@@ -89,7 +89,7 @@ func (oss ossClient) Download(src, target string) (bool, error) {
if err != nil {
return false, err
}
err = bucket.GetObjectToFile(src, target)
err = bucket.DownloadFile(src, target, 200*1024*1024, osssdk.Routines(5), osssdk.Checkpoint(true, ""))
if err != nil {
return false, err
}

View File

@@ -7,7 +7,7 @@ import (
)
func Up(filePath string) (string, error) {
stdout, err := cmd.Execf("docker-compose -f %s up -d", filePath)
stdout, err := cmd.Execf("docker-compose -f %s up -d --quiet-pull", filePath)
return stdout, err
}

View File

@@ -1,21 +0,0 @@
package ssh
import (
"fmt"
"testing"
)
func TestSSH(t *testing.T) {
ss := ConnInfo{
Addr: "172.16.10.111",
Port: 22,
User: "root",
AuthMode: "password",
Password: "Calong@2015",
}
_, err := ss.NewClient()
if err != nil {
fmt.Println(err)
}
fmt.Println(ss.Run("ip a"))
}

View File

@@ -89,6 +89,7 @@ func NewPrivateKeyClient(email string, privateKey string) (*AcmeClient, error) {
func newConfig(user *AcmeUser) *lego.Config {
config := lego.NewConfig(user)
config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
//config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
config.UserAgent = "acm_go/0.0.1"
config.Certificate.KeyType = certcrypto.RSA2048
return config

View File

@@ -9,6 +9,7 @@ import (
"io/ioutil"
"os"
"testing"
"time"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/certcrypto"
@@ -154,10 +155,10 @@ func TestSSL(t *testing.T) {
// panic(err)
//}
// err = client.Challenge.SetDNS01Provider(&plainDnsProvider{}, dns01.AddDNSTimeout(6*time.Minute))
// if err != nil {
// panic(err)
// }
err = client.Challenge.SetDNS01Provider(&manualDnsProvider{}, dns01.AddDNSTimeout(6*time.Minute))
if err != nil {
panic(err)
}
core, err := api.New(config.HTTPClient, config.UserAgent, config.CADirURL, reg.URI, priKey)
if err != nil {

View File

@@ -115,22 +115,12 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string) error {
return c.Client.Challenge.SetDNS01Provider(p, dns01.AddDNSTimeout(3*time.Minute))
}
func (c *AcmeClient) UseManualDns(domains []string) (*Resolve, error) {
func (c *AcmeClient) UseManualDns() error {
p := &manualDnsProvider{}
if err := c.Client.Challenge.SetDNS01Provider(p, dns01.AddDNSTimeout(3*time.Minute)); err != nil {
return nil, nil
return err
}
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
_, err := c.Client.Certificate.Obtain(request)
if err != nil {
return nil, err
}
return p.Resolve, nil
return nil
}
func (c *AcmeClient) UseHTTP(path string) error {

View File

@@ -62,6 +62,25 @@ var doc = `{
}
}
},
"/apps/checkupdate": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取应用更新版本",
"tags": [
"App"
],
"summary": "Get app list update",
"responses": {
"200": {
"description": ""
}
}
}
},
"/apps/detail/:appId/:version": {
"get": {
"security": [
@@ -470,6 +489,48 @@ var doc = `{
}
}
},
"/apps/installed/params/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改应用参数",
"consumes": [
"application/json"
],
"tags": [
"App"
],
"summary": "Change app params",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.AppInstalledUpdate"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"installId"
],
"formatEN": "Application param update [installId]",
"formatZH": "应用参数修改 [installId]",
"paramKeys": []
}
}
},
"/apps/installed/port/change": {
"post": {
"security": [
@@ -683,6 +744,20 @@ var doc = `{
}
}
},
"/auth/demo": {
"get": {
"description": "判断是否为demo环境",
"tags": [
"Auth"
],
"summary": "Check System isDemo",
"responses": {
"200": {
"description": ""
}
}
}
},
"/auth/init": {
"post": {
"description": "初始化用户",
@@ -3899,6 +3974,43 @@ var doc = `{
}
}
},
"/files/chunkupload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "分片上传文件",
"tags": [
"File"
],
"summary": "ChunkUpload file",
"parameters": [
{
"type": "file",
"description": "request",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"path"
],
"formatEN": "Upload file [path]",
"formatZH": "上传文件 [path]",
"paramKeys": []
}
}
},
"/files/compress": {
"post": {
"security": [
@@ -4112,6 +4224,48 @@ var doc = `{
}
}
},
"/files/download/bypath": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载指定文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Download file with path",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FilePath"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"path"
],
"formatEN": "Download file [path]",
"formatZH": "下载文件 [path]",
"paramKeys": []
}
}
},
"/files/loadfile": {
"post": {
"security": [
@@ -8623,8 +8777,7 @@ var doc = `{
"dto.ComposeCreate": {
"type": "object",
"required": [
"from",
"name"
"from"
],
"properties": {
"file": {
@@ -8860,9 +9013,7 @@ var doc = `{
],
"properties": {
"day": {
"type": "integer",
"maximum": 31,
"minimum": 1
"type": "integer"
},
"dbName": {
"type": "string"
@@ -8871,17 +9022,13 @@ var doc = `{
"type": "string"
},
"hour": {
"type": "integer",
"maximum": 23,
"minimum": 0
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer",
"maximum": 59,
"minimum": 0
"type": "integer"
},
"name": {
"type": "string"
@@ -8942,9 +9089,7 @@ var doc = `{
],
"properties": {
"day": {
"type": "integer",
"maximum": 31,
"minimum": 1
"type": "integer"
},
"dbName": {
"type": "string"
@@ -8953,9 +9098,7 @@ var doc = `{
"type": "string"
},
"hour": {
"type": "integer",
"maximum": 23,
"minimum": 0
"type": "integer"
},
"id": {
"type": "integer"
@@ -8964,9 +9107,7 @@ var doc = `{
"type": "boolean"
},
"minute": {
"type": "integer",
"maximum": 59,
"minimum": 0
"type": "integer"
},
"name": {
"type": "string"
@@ -9060,15 +9201,9 @@ var doc = `{
},
"dto.DaemonJsonUpdateByFile": {
"type": "object",
"required": [
"path"
],
"properties": {
"file": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
@@ -9254,6 +9389,12 @@ var doc = `{
"restart",
"stop"
]
},
"stopService": {
"type": "boolean"
},
"stopSocket": {
"type": "boolean"
}
}
},
@@ -11042,6 +11183,22 @@ var doc = `{
}
}
},
"request.AppInstalledUpdate": {
"type": "object",
"required": [
"installId",
"params"
],
"properties": {
"installId": {
"type": "integer"
},
"params": {
"type": "object",
"additionalProperties": true
}
}
},
"request.AppSearch": {
"type": "object",
"required": [
@@ -11284,6 +11441,17 @@ var doc = `{
}
}
},
"request.FilePath": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"request.FilePathCheck": {
"type": "object",
"required": [
@@ -12042,7 +12210,22 @@ var doc = `{
"response.AppParam": {
"type": "object",
"properties": {
"label": {
"edit": {
"type": "boolean"
},
"key": {
"type": "string"
},
"labelEn": {
"type": "string"
},
"labelZh": {
"type": "string"
},
"rule": {
"type": "string"
},
"type": {
"type": "string"
},
"value": {}
@@ -12185,6 +12368,9 @@ var doc = `{
"appInstallId": {
"type": "integer"
},
"appName": {
"type": "string"
},
"createdAt": {
"type": "string"
},

View File

@@ -48,6 +48,25 @@
}
}
},
"/apps/checkupdate": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取应用更新版本",
"tags": [
"App"
],
"summary": "Get app list update",
"responses": {
"200": {
"description": ""
}
}
}
},
"/apps/detail/:appId/:version": {
"get": {
"security": [
@@ -456,6 +475,48 @@
}
}
},
"/apps/installed/params/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改应用参数",
"consumes": [
"application/json"
],
"tags": [
"App"
],
"summary": "Change app params",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.AppInstalledUpdate"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"installId"
],
"formatEN": "Application param update [installId]",
"formatZH": "应用参数修改 [installId]",
"paramKeys": []
}
}
},
"/apps/installed/port/change": {
"post": {
"security": [
@@ -669,6 +730,20 @@
}
}
},
"/auth/demo": {
"get": {
"description": "判断是否为demo环境",
"tags": [
"Auth"
],
"summary": "Check System isDemo",
"responses": {
"200": {
"description": ""
}
}
}
},
"/auth/init": {
"post": {
"description": "初始化用户",
@@ -3885,6 +3960,43 @@
}
}
},
"/files/chunkupload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "分片上传文件",
"tags": [
"File"
],
"summary": "ChunkUpload file",
"parameters": [
{
"type": "file",
"description": "request",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"path"
],
"formatEN": "Upload file [path]",
"formatZH": "上传文件 [path]",
"paramKeys": []
}
}
},
"/files/compress": {
"post": {
"security": [
@@ -4098,6 +4210,48 @@
}
}
},
"/files/download/bypath": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载指定文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Download file with path",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FilePath"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"path"
],
"formatEN": "Download file [path]",
"formatZH": "下载文件 [path]",
"paramKeys": []
}
}
},
"/files/loadfile": {
"post": {
"security": [
@@ -8609,8 +8763,7 @@
"dto.ComposeCreate": {
"type": "object",
"required": [
"from",
"name"
"from"
],
"properties": {
"file": {
@@ -8846,9 +8999,7 @@
],
"properties": {
"day": {
"type": "integer",
"maximum": 31,
"minimum": 1
"type": "integer"
},
"dbName": {
"type": "string"
@@ -8857,17 +9008,13 @@
"type": "string"
},
"hour": {
"type": "integer",
"maximum": 23,
"minimum": 0
"type": "integer"
},
"keepLocal": {
"type": "boolean"
},
"minute": {
"type": "integer",
"maximum": 59,
"minimum": 0
"type": "integer"
},
"name": {
"type": "string"
@@ -8928,9 +9075,7 @@
],
"properties": {
"day": {
"type": "integer",
"maximum": 31,
"minimum": 1
"type": "integer"
},
"dbName": {
"type": "string"
@@ -8939,9 +9084,7 @@
"type": "string"
},
"hour": {
"type": "integer",
"maximum": 23,
"minimum": 0
"type": "integer"
},
"id": {
"type": "integer"
@@ -8950,9 +9093,7 @@
"type": "boolean"
},
"minute": {
"type": "integer",
"maximum": 59,
"minimum": 0
"type": "integer"
},
"name": {
"type": "string"
@@ -9046,15 +9187,9 @@
},
"dto.DaemonJsonUpdateByFile": {
"type": "object",
"required": [
"path"
],
"properties": {
"file": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
@@ -9240,6 +9375,12 @@
"restart",
"stop"
]
},
"stopService": {
"type": "boolean"
},
"stopSocket": {
"type": "boolean"
}
}
},
@@ -11028,6 +11169,22 @@
}
}
},
"request.AppInstalledUpdate": {
"type": "object",
"required": [
"installId",
"params"
],
"properties": {
"installId": {
"type": "integer"
},
"params": {
"type": "object",
"additionalProperties": true
}
}
},
"request.AppSearch": {
"type": "object",
"required": [
@@ -11270,6 +11427,17 @@
}
}
},
"request.FilePath": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"request.FilePathCheck": {
"type": "object",
"required": [
@@ -12028,7 +12196,22 @@
"response.AppParam": {
"type": "object",
"properties": {
"label": {
"edit": {
"type": "boolean"
},
"key": {
"type": "string"
},
"labelEn": {
"type": "string"
},
"labelZh": {
"type": "string"
},
"rule": {
"type": "string"
},
"type": {
"type": "string"
},
"value": {}
@@ -12171,6 +12354,9 @@
"appInstallId": {
"type": "integer"
},
"appName": {
"type": "string"
},
"createdAt": {
"type": "string"
},

View File

@@ -152,7 +152,6 @@ definitions:
type: integer
required:
- from
- name
type: object
dto.ComposeOperation:
properties:
@@ -294,22 +293,16 @@ definitions:
dto.CronjobCreate:
properties:
day:
maximum: 31
minimum: 1
type: integer
dbName:
type: string
exclusionRules:
type: string
hour:
maximum: 23
minimum: 0
type: integer
keepLocal:
type: boolean
minute:
maximum: 59
minimum: 0
type: integer
name:
type: string
@@ -352,24 +345,18 @@ definitions:
dto.CronjobUpdate:
properties:
day:
maximum: 31
minimum: 1
type: integer
dbName:
type: string
exclusionRules:
type: string
hour:
maximum: 23
minimum: 0
type: integer
id:
type: integer
keepLocal:
type: boolean
minute:
maximum: 59
minimum: 0
type: integer
name:
type: string
@@ -439,10 +426,6 @@ definitions:
properties:
file:
type: string
path:
type: string
required:
- path
type: object
dto.DashboardBase:
properties:
@@ -564,6 +547,10 @@ definitions:
- restart
- stop
type: string
stopService:
type: boolean
stopSocket:
type: boolean
required:
- operation
type: object
@@ -1752,6 +1739,17 @@ definitions:
- page
- pageSize
type: object
request.AppInstalledUpdate:
properties:
installId:
type: integer
params:
additionalProperties: true
type: object
required:
- installId
- params
type: object
request.AppSearch:
properties:
name:
@@ -1914,6 +1912,13 @@ definitions:
showHidden:
type: boolean
type: object
request.FilePath:
properties:
path:
type: string
required:
- path
type: object
request.FilePathCheck:
properties:
path:
@@ -2421,7 +2426,17 @@ definitions:
type: object
response.AppParam:
properties:
label:
edit:
type: boolean
key:
type: string
labelEn:
type: string
labelZh:
type: string
rule:
type: string
type:
type: string
value: {}
type: object
@@ -2515,6 +2530,8 @@ definitions:
type: string
appInstallId:
type: integer
appName:
type: string
createdAt:
type: string
defaultServer:
@@ -2628,6 +2645,17 @@ paths:
summary: Search app by key
tags:
- App
/apps/checkupdate:
get:
description: 获取应用更新版本
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Get app list update
tags:
- App
/apps/detail/:appId/:version:
get:
consumes:
@@ -2887,6 +2915,33 @@ paths:
summary: Search params by appInstallId
tags:
- App
/apps/installed/params/update:
post:
consumes:
- application/json
description: 修改应用参数
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.AppInstalledUpdate'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Change app params
tags:
- App
x-panel-log:
BeforeFuntions: []
bodyKeys:
- installId
formatEN: Application param update [installId]
formatZH: 应用参数修改 [installId]
paramKeys: []
/apps/installed/port/change:
post:
consumes:
@@ -3022,6 +3077,15 @@ paths:
summary: Load captcha
tags:
- Auth
/auth/demo:
get:
description: 判断是否为demo环境
responses:
"200":
description: ""
summary: Check System isDemo
tags:
- Auth
/auth/init:
post:
consumes:
@@ -5065,6 +5129,30 @@ paths:
formatEN: Check whether file [path] exists
formatZH: 检测文件 [path] 是否存在
paramKeys: []
/files/chunkupload:
post:
description: 分片上传文件
parameters:
- description: request
in: formData
name: file
required: true
type: file
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: ChunkUpload file
tags:
- File
x-panel-log:
BeforeFuntions: []
bodyKeys:
- path
formatEN: Upload file [path]
formatZH: 上传文件 [path]
paramKeys: []
/files/compress:
post:
consumes:
@@ -5202,6 +5290,33 @@ paths:
formatEN: Download file [name]
formatZH: 下载文件 [name]
paramKeys: []
/files/download/bypath:
post:
consumes:
- application/json
description: 下载指定文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.FilePath'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Download file with path
tags:
- File
x-panel-log:
BeforeFuntions: []
bodyKeys:
- path
formatEN: Download file [path]
formatZH: 下载文件 [path]
paramKeys: []
/files/loadfile:
post:
consumes:

BIN
cmd/server/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -87,6 +87,7 @@ declare module 'vue' {
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']
Tooltip: typeof import('./src/components/tooltip/index.vue')['default']
Upload: typeof import('./src/components/upload/index.vue')['default']
VCharts: typeof import('./src/components/v-charts/index.vue')['default']
}

View File

@@ -30,7 +30,6 @@
"sass-loader": "^13.0.2",
"screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25",
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
@@ -10274,14 +10273,6 @@
"entities": "^2.0.0"
}
},
"node_modules/vite-plugin-monaco-editor": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
"peerDependencies": {
"monaco-editor": ">=0.33.0"
}
},
"node_modules/vite-plugin-vue-setup-extend": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",
@@ -10643,9 +10634,9 @@
}
},
"node_modules/webpack": {
"version": "5.75.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -18367,12 +18358,6 @@
}
}
},
"vite-plugin-monaco-editor": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
"requires": {}
},
"vite-plugin-vue-setup-extend": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",
@@ -18679,9 +18664,9 @@
}
},
"webpack": {
"version": "5.75.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"peer": true,
"requires": {
"@types/eslint-scope": "^3.7.3",

View File

@@ -42,7 +42,6 @@
"sass-loader": "^13.0.2",
"screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25",
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",

View File

@@ -43,7 +43,8 @@ export namespace Container {
state: string;
runTime: string;
isFromCompose: string;
isFromApp: boolean;
isFromCompose: boolean;
}
export interface ContainerStats {
cpuPercent: number;

View File

@@ -6,7 +6,7 @@ export const searchContainer = (params: Container.ContainerSearch) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
};
export const createContainer = (params: Container.ContainerCreate) => {
return http.post(`/containers`, params);
return http.post(`/containers`, params, 1200000);
};
export const logContainer = (params: Container.ContainerLogSearch) => {
return http.post<string>(`/containers/search/log`, params);
@@ -38,10 +38,10 @@ export const imagePush = (params: Container.ImagePush) => {
return http.post<string>(`/containers/image/push`, params);
};
export const imageLoad = (params: Container.ImageLoad) => {
return http.post(`/containers/image/load`, params);
return http.post(`/containers/image/load`, params, 1200000);
};
export const imageSave = (params: Container.ImageSave) => {
return http.post(`/containers/image/save`, params);
return http.post(`/containers/image/save`, params, 1200000);
};
export const imageTag = (params: Container.ImageTag) => {
return http.post(`/containers/image/tag`, params);
@@ -117,13 +117,13 @@ export const searchCompose = (params: SearchWithPage) => {
return http.post<ResPage<Container.ComposeInfo>>(`/containers/compose/search`, params);
};
export const upCompose = (params: Container.ComposeCreate) => {
return http.post(`/containers/compose`, params);
return http.post(`/containers/compose`, params, 600000);
};
export const composeOperator = (params: Container.ComposeOpration) => {
return http.post(`/containers/compose/operate`, params);
};
export const composeUpdate = (params: Container.ComposeUpdate) => {
return http.post(`/containers/compose/update`, params);
return http.post(`/containers/compose/update`, params, 600000);
};
// docker

View File

@@ -31,7 +31,7 @@ export const updateStatus = (params: Cronjob.UpdateStatus) => {
};
export const download = (params: Cronjob.Download) => {
return http.download<BlobPart>(`cronjobs/download`, params, { responseType: 'blob' });
return http.post<string>(`cronjobs/download`, params);
};
export const handleOnce = (id: number) => {

View File

@@ -1,4 +1,6 @@
import http from '@/api';
import { deepCopy } from '@/utils/util';
import { Base64 } from 'js-base64';
import { SearchWithPage, ResPage, DescriptionUpdate } from '../interface';
import { Database } from '../interface/database';
@@ -7,13 +9,21 @@ export const searchMysqlDBs = (params: SearchWithPage) => {
};
export const addMysqlDB = (params: Database.MysqlDBCreate) => {
return http.post(`/databases`, params);
let reqest = deepCopy(params) as Database.MysqlDBCreate;
if (reqest.password) {
reqest.password = Base64.encode(reqest.password);
}
return http.post(`/databases`, reqest);
};
export const updateMysqlAccess = (params: Database.ChangeInfo) => {
return http.post(`/databases/change/access`, params);
};
export const updateMysqlPassword = (params: Database.ChangeInfo) => {
return http.post(`/databases/change/password`, params);
let reqest = deepCopy(params) as Database.ChangeInfo;
if (reqest.value) {
reqest.value = Base64.encode(reqest.value);
}
return http.post(`/databases/change/password`, reqest);
};
export const updateMysqlDescription = (params: DescriptionUpdate) => {
return http.post(`/databases/description/update`, params);
@@ -58,7 +68,11 @@ export const redisPersistenceConf = () => {
return http.get<Database.RedisPersistenceConf>(`/databases/redis/persistence/conf`);
};
export const changeRedisPassword = (params: Database.ChangeInfo) => {
return http.post(`/databases/redis/password`, params);
let reqest = deepCopy(params) as Database.ChangeInfo;
if (reqest.value) {
reqest.value = Base64.encode(reqest.value);
}
return http.post(`/databases/redis/password`, reqest);
};
export const updateRedisPersistenceConf = (params: Database.RedisConfPersistenceUpdate) => {
return http.post(`/databases/redis/persistence/update`, params);

View File

@@ -79,6 +79,10 @@ export const DownloadFile = (params: File.FileDownload) => {
return http.download<BlobPart>('files/download', params, { responseType: 'blob', timeout: 20000 });
};
export const DownloadByPath = (path: string) => {
return http.download<BlobPart>('files/download/bypath', { path: path }, { responseType: 'blob', timeout: 40000 });
};
export const ComputeDirSize = (params: File.DirSizeReq) => {
return http.post<File.DirSizeRes>('files/size', params);
};

View File

@@ -3,6 +3,8 @@ import { ResPage } from '../interface';
import { Command } from '../interface/command';
import { Group } from '../interface/group';
import { Host } from '../interface/host';
import { Base64 } from 'js-base64';
import { deepCopy } from '@/utils/util';
export const searchHosts = (params: Host.SearchWithPage) => {
return http.post<ResPage<Host.Host>>(`/hosts/search`, params);
@@ -14,16 +16,37 @@ export const getHostInfo = (id: number) => {
return http.get<Host.Host>(`/hosts/` + id);
};
export const addHost = (params: Host.HostOperate) => {
return http.post<Host.HostOperate>(`/hosts`, params);
let reqest = deepCopy(params) as Host.HostOperate;
if (reqest.password) {
reqest.password = Base64.encode(reqest.password);
}
if (reqest.privateKey) {
reqest.privateKey = Base64.encode(reqest.privateKey);
}
return http.post<Host.HostOperate>(`/hosts`, reqest);
};
export const testByInfo = (params: Host.HostConnTest) => {
return http.post<boolean>(`/hosts/test/byinfo`, params);
let reqest = deepCopy(params) as Host.HostOperate;
if (reqest.password) {
reqest.password = Base64.encode(reqest.password);
}
if (reqest.privateKey) {
reqest.privateKey = Base64.encode(reqest.privateKey);
}
return http.post<boolean>(`/hosts/test/byinfo`, reqest);
};
export const testByID = (id: number) => {
return http.post<boolean>(`/hosts/test/byid/${id}`);
};
export const editHost = (params: Host.HostOperate) => {
return http.post(`/hosts/update`, params);
let reqest = deepCopy(params) as Host.HostOperate;
if (reqest.password) {
reqest.password = Base64.encode(reqest.password);
}
if (reqest.privateKey) {
reqest.privateKey = Base64.encode(reqest.privateKey);
}
return http.post(`/hosts/update`, reqest);
};
export const editHostGroup = (params: Host.GroupChange) => {
return http.post(`/hosts/update/group`, params);

View File

@@ -1,4 +1,6 @@
import http from '@/api';
import { deepCopy } from '@/utils/util';
import { Base64 } from 'js-base64';
import { ResPage, SearchWithPage, DescriptionUpdate } from '../interface';
import { Backup } from '../interface/backup';
import { Setting } from '../interface/setting';
@@ -52,16 +54,16 @@ export const loadBaseDir = () => {
// backup
export const handleBackup = (params: Backup.Backup) => {
return http.post(`/settings/backup/backup`, params);
return http.post(`/settings/backup/backup`, params, 400000);
};
export const handleRecover = (params: Backup.Recover) => {
return http.post(`/settings/backup/recover`, params);
return http.post(`/settings/backup/recover`, params, 400000);
};
export const handleRecoverByUpload = (params: Backup.Recover) => {
return http.post(`/settings/backup/recover/byupload`, params);
return http.post(`/settings/backup/recover/byupload`, params, 400000);
};
export const downloadBackupRecord = (params: Backup.RecordDownload) => {
return http.download<BlobPart>(`/settings/backup/record/download`, params, { responseType: 'blob' });
return http.post<string>(`/settings/backup/record/download`, params);
};
export const deleteBackupRecord = (params: { ids: number[] }) => {
return http.post(`/settings/backup/record/del`, params);
@@ -77,16 +79,37 @@ export const getFilesFromBackup = (type: string) => {
return http.post<Array<any>>(`/settings/backup/search/files`, { type: type });
};
export const addBackup = (params: Backup.BackupOperate) => {
return http.post<Backup.BackupOperate>(`/settings/backup`, params);
let reqest = deepCopy(params) as Backup.BackupOperate;
if (reqest.accessKey) {
reqest.accessKey = Base64.encode(reqest.accessKey);
}
if (reqest.credential) {
reqest.credential = Base64.encode(reqest.credential);
}
return http.post<Backup.BackupOperate>(`/settings/backup`, reqest);
};
export const editBackup = (params: Backup.BackupOperate) => {
return http.post(`/settings/backup/update`, params);
let reqest = deepCopy(params) as Backup.BackupOperate;
if (reqest.accessKey) {
reqest.accessKey = Base64.encode(reqest.accessKey);
}
if (reqest.credential) {
reqest.credential = Base64.encode(reqest.credential);
}
return http.post(`/settings/backup/update`, reqest);
};
export const deleteBackup = (params: { ids: number[] }) => {
return http.post(`/settings/backup/del`, params);
};
export const listBucket = (params: Backup.ForBucket) => {
return http.post(`/settings/backup/buckets`, params);
let reqest = deepCopy(params) as Backup.BackupOperate;
if (reqest.accessKey) {
reqest.accessKey = Base64.encode(reqest.accessKey);
}
if (reqest.credential) {
reqest.credential = Base64.encode(reqest.credential);
}
return http.post(`/settings/backup/buckets`, reqest);
};
// snapshot

View File

@@ -58,6 +58,7 @@ import DrawerHeader from '@/components/drawer-header/index.vue';
import { deleteBackupRecord, downloadBackupRecord, searchBackupRecords } from '@/api/modules/setting';
import { Backup } from '@/api/interface/backup';
import { MsgSuccess } from '@/utils/message';
import { DownloadByPath } from '@/api/modules/files';
const selects = ref<any>([]);
const loading = ref();
@@ -145,14 +146,16 @@ const onDownload = async (row: Backup.RecordInfo) => {
fileDir: row.fileDir,
fileName: row.fileName,
};
const res = await downloadBackupRecord(params);
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = row.fileName;
const event = new MouseEvent('click');
a.dispatchEvent(event);
await downloadBackupRecord(params).then(async (res) => {
const file = await DownloadByPath(res.data);
const downloadUrl = window.URL.createObjectURL(new Blob([file]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = row.fileName;
const event = new MouseEvent('click');
a.dispatchEvent(event);
});
};
const onBatchDelete = async (row: Backup.RecordInfo | null) => {

View File

@@ -53,7 +53,6 @@ defineProps({
const emit = defineEmits(['search', 'update:selects']);
const condition = ref({});
function search(conditions: any, e: any) {
console.log(conditions);
if (conditions) {
condition.value = conditions;
}

View File

@@ -77,7 +77,7 @@ const timeOptions = ref([
const searchLogs = async () => {
const res = await logContainer(logSearch);
logInfo.value = res.data;
logInfo.value = res.data || '';
nextTick(() => {
const state = view.value.state;
view.value.dispatch({

View File

@@ -100,7 +100,6 @@ const props = defineProps({
const em = defineEmits(['choose']);
const checkFile = (row: any) => {
console.log(row.path);
rowName.value = row.name;
em('choose', row.path);
popoverVisible.value = false;

View File

@@ -0,0 +1,52 @@
<template>
<div class="tooltip-container">
<el-tooltip :disabled="showTooltip">
<template #content>
<div :style="{ width: tootipWidth, 'word-break': 'break-all' }">{{ text }}</div>
</template>
<p ref="tooltipBox" class="text-box">
<span v-if="islink" ref="tooltipItem" class="table-link">{{ text }}</span>
<span v-else ref="tooltipItem" class="">{{ text }}</span>
</p>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const showTooltip = ref();
const tooltipBox = ref();
const tooltipItem = ref();
const tootipWidth = ref();
defineProps({
text: {
type: String,
default: '',
},
islink: {
type: Boolean,
default: true,
},
});
onMounted(() => {
const boxWidth = tooltipBox.value.offsetWidth;
const itemWidth = tooltipItem.value.offsetWidth;
tootipWidth.value = itemWidth > 250 ? '250px' : itemWidth + 'px';
showTooltip.value = boxWidth > itemWidth;
});
</script>
<style scoped lang="scss">
.tooltip-container {
width: 100%;
.text-box {
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View File

@@ -119,7 +119,11 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
if (type.value === 'website' || type.value === 'app') {
title.value = name.value;
}
baseDir.value = `${pathRes.data}/uploads/${type.value}/${name.value}/${detailName.value}/`;
if (detailName.value) {
baseDir.value = `${pathRes.data}/uploads/${type.value}/${name.value}/${detailName.value}/`;
} else {
baseDir.value = `${pathRes.data}/uploads/${type.value}/${name.value}/`;
}
upVisiable.value = true;
search();
};
@@ -198,7 +202,7 @@ const onSubmit = async () => {
MsgError(i18n.global.t('commons.msg.fileNameErr'));
return;
}
let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,50}$/;
let reg = /^[a-zA-Z0-9\u4e00-\u9fa5]{1}[a-z:A-Z0-9_.\u4e00-\u9fa5-]{0,256}$/;
if (!reg.test(uploaderFiles.value[0]!.raw.name)) {
MsgError(i18n.global.t('commons.msg.fileNameErr'));
return;

View File

@@ -15,6 +15,21 @@ const checkIp = (rule: any, value: any, callback: any) => {
}
};
const checkHost = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
} else {
const regIP =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
const regHost = /^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
if (!regIP.test(value) && !regHost.test(value)) {
callback(new Error(i18n.global.t('commons.rule.host')));
} else {
callback();
}
}
};
const complexityPassword = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.complexityPassword')));
@@ -265,6 +280,7 @@ interface CommonRule {
number: FormItemRule;
integerNumber: FormItemRule;
ip: FormItemRule;
host: FormItemRule;
port: FormItemRule;
domain: FormItemRule;
databaseName: FormItemRule;
@@ -363,6 +379,11 @@ export const Rules: CommonRule = {
required: true,
trigger: 'blur',
},
host: {
validator: checkHost,
required: true,
trigger: 'blur',
},
port: {
required: true,
trigger: 'blur',

View File

@@ -1,4 +1,5 @@
export default {
import fit2cloudEnLocale from 'fit2cloud-ui-plus/src/locale/lang/en';
const message = {
commons: {
true: 'true',
false: 'false',
@@ -96,7 +97,7 @@ export default {
unSupportSize: 'The uploaded file exceeds {0}M, please confirm!',
fileExist: 'The file already exists in the current folder. Repeat uploading is not supported!',
fileNameErr:
'You can upload only files whose name contains 1 to 50 characters, including English, Chinese, digits, or periods (.-_)',
'You can upload only files whose name contains 1 to 256 characters, including English, Chinese, digits, or periods (.-_)',
comfimNoNull: 'Make sure the value {0} is not empty',
},
login: {
@@ -107,8 +108,8 @@ export default {
welcome: 'Welcome back, please enter your username and password to log in!',
errorAuthInfo: 'The user name or password you entered is incorrect, please re-enter!',
errorMfaInfo: 'Incorrect authentication information, please try again!',
captchaHelper: 'Please enter the verification code',
errorCaptcha: 'Verification code error!',
captchaHelper: 'Captcha',
errorCaptcha: 'Captcha code error!',
safeEntrance: 'Please use the correct entry to log in to the panel',
reason: 'Cause of error:',
reasonHelper:
@@ -119,7 +120,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: 'Linux server operation and maintenance management panel',
title: 'Linux Server Management Panel',
},
rule: {
username: 'Please enter a username',
@@ -134,12 +135,13 @@ export default {
imageName: 'Support English, numbers, :/.-_, length 1-150',
volumeName: 'Support English, numbers, .-_, length 2-30',
complexityPassword:
'Enter a password that is longer than eight characters and contains at least two letters, digits, and special characters',
'Longer than eight characters and contains at least two combinations of letters, digits, and special characters',
commonPassword: 'Please enter a password with more than 6 characters',
email: 'Email format error',
number: 'Please enter the correct number',
integer: 'Please enter the correct positive integer',
ip: 'Please enter the correct IP address',
host: 'Enter the correct IP address or domain name',
port: 'Please enter the correct port',
selectHelper: 'Please select the correct {0} file',
domain: 'domain name format error',
@@ -407,6 +409,8 @@ export default {
createContainer: 'Create container',
containerList: 'Container list',
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
operatorAppHelper:
'There is a container from the App store. The {0} operation may affect the normal use of this service. Are you sure?',
start: 'Start',
stop: 'Stop',
restart: 'Restart',
@@ -994,7 +998,7 @@ export default {
addAccount: 'Add new account',
acmeAccount: 'Acme Account',
provider: 'Verification method',
dnsCommon: 'Manual resolution',
dnsManual: 'Manual resolution',
expireDate: 'Expiration Time',
brand: 'Issuer',
deploySSL: 'Deployment',
@@ -1161,3 +1165,8 @@ export default {
cookieBlockList: 'Cookie Blacklist',
},
};
export default {
...fit2cloudEnLocale,
...message,
};

View File

@@ -1,4 +1,5 @@
export default {
import fit2cloudZhLocale from 'fit2cloud-ui-plus/src/locale/lang/zh-cn';
const message = {
commons: {
true: '是',
false: '否',
@@ -101,7 +102,7 @@ export default {
unSupportType: '不支持当前文件类型',
unSupportSize: '上传文件超过 {0}M请确认',
fileExist: '当前文件夹已存在该文件不支持重复上传',
fileNameErr: '仅支持上传名称包含英文中文数字或者 .-_ ,长度 1-50 位的文件',
fileNameErr: '仅支持上传名称包含英文中文数字或者 .-_ ,长度 1-256 位的文件',
comfimNoNull: '请确认 {0} 值不为空',
},
login: {
@@ -112,7 +113,7 @@ export default {
welcome: '欢迎回来请输入用户名和密码登录',
errorAuthInfo: '您输入的用户名或密码不正确请重新输入',
errorMfaInfo: '错误的验证信息请重试',
captchaHelper: '请输入验证码',
captchaHelper: '验证码',
errorCaptcha: '验证码错误',
safeEntrance: '请使用正确的入口登录面板',
reason: '错误原因',
@@ -144,6 +145,7 @@ export default {
number: '请输入正确的数字',
integer: '请输入正确的正整数',
ip: '请输入正确的 IP 地址',
host: '请输入正确的 IP 或者域名',
port: '请输入正确的端口,1-65535',
selectHelper: '请选择正确的 {0} 文件',
domain: '域名格式错误',
@@ -419,6 +421,7 @@ export default {
createContainer: '创建容器',
containerList: '容器列表',
operatorHelper: '将对选中容器进行 {0} 操作是否继续',
operatorAppHelper: '存在来源于应用商店的容器{0} 操作可能会影响到该服务的正常使用是否确认',
start: '启动',
stop: '停止',
restart: '重启',
@@ -1003,7 +1006,7 @@ export default {
addAccount: '创建账户',
acmeAccount: 'Acme 账户',
provider: '验证方式',
dnsCommon: '手动解析',
dnsManual: '手动解析',
expireDate: '过期时间',
brand: '颁发者',
deploySSL: '部署',
@@ -1165,3 +1168,7 @@ export default {
cookieBlockList: 'Cookie 黑名单',
},
};
export default {
...fit2cloudZhLocale,
...message,
};

View File

@@ -133,4 +133,8 @@ const showBack = computed(() => {
border: 0;
border-top: var(--panel-border);
}
.main-box {
position: relative;
}
</style>

View File

@@ -15,11 +15,12 @@ import SvgIcon from './components/svg-icon/svg-icon.vue';
import ElementPlus from 'element-plus';
import Fit2CloudPlus from 'fit2cloud-ui-plus';
import * as Icons from '@element-plus/icons-vue';
const app = createApp(App);
app.component('SvgIcon', SvgIcon);
app.use(ElementPlus);
app.use(Fit2CloudPlus);
app.use(Fit2CloudPlus, { locale: I18n.global.messages.value[localStorage.getItem('lang') || 'zh'] });
Object.keys(Icons).forEach((key) => {
app.component(key, Icons[key as keyof typeof Icons]);
});

View File

@@ -176,9 +176,12 @@
.mask-prompt {
position: absolute;
z-index: 9999;
top: 25%;
left: 50%;
top: 220px;
left: 45%;
transform: translate(-50%, -50%);
width: 300px;
text-align: center;
font-size: 14px;
}
.table-button {
@@ -287,3 +290,12 @@
color: #646a73;
}
.table-link {
color: $primary-color;
font-weight: bold;
cursor: pointer;
}
.table-link:hover {
opacity: 0.6;
}

View File

@@ -5,7 +5,7 @@
<el-row :gutter="20">
<div>
<el-col :span="3">
<el-avatar shape="square" :size="180" :src="'data:image/png;base64,' + app.icon" />
<el-avatar shape="square" :size="180" :src="app.icon" />
</el-col>
</div>
<el-col :span="18">
@@ -120,6 +120,7 @@ const getApp = async () => {
try {
const res = await GetApp(props.appKey);
app.value = res.data;
app.value.icon = 'data:image/png;base64,' + res.data.icon;
version.value = app.value.versions[0];
getDetail(app.value.id, version.value);
} finally {

View File

@@ -101,7 +101,6 @@ const get = async () => {
if (res.data && res.data.length > 0) {
res.data.forEach((d) => {
if (d.edit) {
console.log(d.edit);
canEdit.value = true;
}
let value = d.value;

View File

@@ -69,15 +69,9 @@
@search="search"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
>
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix>
<template #default="{ row }">
<el-link @click="onInspect(row.containerID)" type="primary">{{ row.name }}</el-link>
<Tooltip @click="onInspect(row.containerID)" :text="row.name" />
</template>
</el-table-column>
<el-table-column
@@ -120,6 +114,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import Tooltip from '@/components/tooltip/index.vue';
import LayoutContent from '@/layout/layout-content.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
@@ -228,15 +223,18 @@ const checkStatus = (operation: string) => {
};
const onOperate = async (operation: string) => {
ElMessageBox.confirm(
i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]),
i18n.global.t('container.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(() => {
let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]);
for (const item of selects.value) {
if (item.isFromApp) {
msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + operation)]);
break;
}
}
ElMessageBox.confirm(msg, i18n.global.t('container.' + operation), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(() => {
let ps = [];
for (const item of selects.value) {
const param = {

View File

@@ -47,15 +47,9 @@
:data="data"
@search="search"
>
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
>
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix>
<template #default="{ row }">
<el-link @click="loadDetail(row)" type="primary">{{ row.name }}</el-link>
<Tooltip @click="loadDetail(row)" :text="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('container.from')" prop="createdBy" min-width="80" fix>
@@ -89,6 +83,7 @@
</template>
<script lang="ts" setup>
import Tooltip from '@/components/tooltip/index.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import { reactive, onMounted, ref } from 'vue';

View File

@@ -220,9 +220,9 @@ const form = reactive({
cmd: [] as Array<string>,
publishAllPorts: false,
exposedPorts: [] as Array<Container.Port>,
nanoCPUs: 1,
memory: 100,
memoryItem: 100,
nanoCPUs: 0,
memory: 0,
memoryItem: 0,
memoryUnit: 'MB',
cpuUnit: 'Core',
volumes: [] as Array<Container.Volume>,
@@ -250,9 +250,9 @@ const handlReset = () => {
form.cmd = [];
form.publishAllPorts = false;
form.exposedPorts = [];
form.nanoCPUs = 1;
form.memory = 100;
form.memoryItem = 100;
form.nanoCPUs = 0;
form.memory = 0;
form.memoryItem = 0;
form.memoryUnit = 'MB';
form.cpuUnit = 'Core';
form.volumes = [];

View File

@@ -66,15 +66,9 @@
@search="search"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="80"
prop="name"
fix
>
<el-table-column :label="$t('commons.table.name')" min-width="80" prop="name" fix>
<template #default="{ row }">
<el-link @click="onInspect(row.containerID)" type="primary">{{ row.name }}</el-link>
<Tooltip @click="onInspect(row.containerID)" :text="row.name" />
</template>
</el-table-column>
<el-table-column
@@ -118,6 +112,7 @@
<script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue';
import Tooltip from '@/components/tooltip/index.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import ReNameDialog from '@/views/container/container/rename/index.vue';
@@ -252,15 +247,18 @@ const checkStatus = (operation: string) => {
}
};
const onOperate = async (operation: string) => {
ElMessageBox.confirm(
i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]),
i18n.global.t('container.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(() => {
let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]);
for (const item of selects.value) {
if (item.isFromApp) {
msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + operation)]);
break;
}
}
ElMessageBox.confirm(msg, i18n.global.t('container.' + operation), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(() => {
let ps = [];
for (const item of selects.value) {
const param = {
@@ -306,7 +304,6 @@ const buttons = [
{
label: i18n.global.t('container.rename'),
click: (row: Container.ContainerInfo) => {
console.log(row.name);
dialogReNameRef.value!.acceptParams({ container: row.name });
},
disabled: (row: any) => {

View File

@@ -96,7 +96,7 @@ const handleClose = async () => {
const searchLogs = async () => {
const res = await logContainer(logSearch);
logInfo.value = res.data;
logInfo.value = res.data || '';
nextTick(() => {
const state = view.value.state;
view.value.dispatch({

View File

@@ -40,7 +40,11 @@
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column label="ID" show-overflow-tooltip prop="id" min-width="60" />
<el-table-column label="ID" prop="id" min-width="60">
<template #default="{ row }">
<Tooltip :islink="false" :text="row.id" />
</template>
</el-table-column>
<el-table-column :label="$t('container.tag')" prop="tags" min-width="160" fix>
<template #default="{ row }">
<el-tag style="margin-left: 5px" v-for="(item, index) of row.tags" :key="index">
@@ -76,6 +80,7 @@
</template>
<script lang="ts" setup>
import Tooltip from '@/components/tooltip/index.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import { reactive, onMounted, ref } from 'vue';
@@ -193,6 +198,7 @@ const buttons = [
click: async (row: Container.ImageInfo) => {
if (row.tags.length <= 1) {
await useDeleteData(imageRemove, { names: [row.id] }, 'commons.msg.delete');
search();
return;
}
let params = {

View File

@@ -47,16 +47,14 @@
@search="search"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="80"
prop="name"
fix
>
<el-table-column :label="$t('commons.table.name')" min-width="80" prop="name" fix>
<template #default="{ row }">
<el-link @click="onInspect(row.id)" type="primary">{{ row.name }}</el-link>
<el-tag effect="dark" round v-if="row.isSystem" style="margin-left: 5px">system</el-tag>
<Tooltip @click="onInspect(row.id)" :text="row.name" />
</template>
</el-table-column>
<el-table-column min-width="50">
<template #default="{ row }">
<el-tag effect="dark" round v-if="row.isSystem">system</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -99,6 +97,7 @@
<script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue';
import Tooltip from '@/components/tooltip/index.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import CreateDialog from '@/views/container/network/create/index.vue';

View File

@@ -272,7 +272,6 @@ const onSubmitSave = async () => {
const loadDockerConf = async () => {
const res = await loadDaemonJsonFile();
if (res.data === 'daemon.json is not find in path') {
console.log(res.data);
showDaemonJsonAlert.value = true;
} else {
dockerConf.value = res.data;

View File

@@ -47,15 +47,9 @@
@search="search"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
>
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix>
<template #default="{ row }">
<el-link @click="onOpenDetail(row)" type="primary">{{ row.name }}</el-link>
<Tooltip @click="onOpenDetail(row)" :text="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('container.description')" prop="description" min-width="200" fix />
@@ -75,6 +69,7 @@
</template>
<script lang="ts" setup>
import Tooltip from '@/components/tooltip/index.vue';
import LayoutContent from '@/layout/layout-content.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';

View File

@@ -45,17 +45,7 @@
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" min-width="80" prop="name" fix>
<template #default="{ row }">
<el-tooltip v-if="row.name.length > 20" effect="dark" placement="bottom">
<template #content>
<div style="width: 300px; word-break: break-all">{{ row.name }}</div>
</template>
<el-link @click="onInspect(row.name)" type="primary">
{{ row.name.substring(0, 20) }}...
</el-link>
</el-tooltip>
<el-link v-else @click="onInspect(row.name)" type="primary">
{{ row.name }}
</el-link>
<Tooltip @click="onInspect(row.name)" :text="row.name" />
</template>
</el-table-column>
<el-table-column
@@ -88,6 +78,7 @@
<script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue';
import Tooltip from '@/components/tooltip/index.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import CreateDialog from '@/views/container/volume/create/index.vue';

View File

@@ -45,17 +45,7 @@
<el-table-column type="selection" fix />
<el-table-column :label="$t('cronjob.taskName')" :min-width="120" prop="name">
<template #default="{ row }">
<el-tooltip effect="dark" v-if="row.name.length > 12" placement="top">
<template #content>
<div style="width: 300px; word-break: break-all">{{ row.name }}</div>
</template>
<el-link @click="loadDetail(row)" type="primary">
{{ row.name.substring(0, 15) }}...
</el-link>
</el-tooltip>
<el-link v-else @click="loadDetail(row)" type="primary">
{{ row.name }}
</el-link>
<Tooltip @click="loadDetail(row)" :text="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" :min-width="80" prop="status">
@@ -136,6 +126,7 @@
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import Tooltip from '@/components/tooltip/index.vue';
import OperatrDialog from '@/views/cronjob/operate/index.vue';
import Records from '@/views/cronjob/record/index.vue';
import LayoutContent from '@/layout/layout-content.vue';

View File

@@ -401,7 +401,6 @@ function hasScript() {
function checkScript() {
let row = dialogData.value.rowData;
console.log(row.specType, row.week, row.day, row.hour, row.minute);
switch (row.specType) {
case 'perMonth':
return row.day > 0 && row.day < 32 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;

View File

@@ -4,7 +4,7 @@
<el-card>
<div>
<el-tag style="float: left" effect="dark" type="success">
{{ $t('cronjob.' + dialogData.rowData.type) }} - {{ dialogData.rowData.name }}
{{ $t('cronjob.' + dialogData.rowData.type) }}
</el-tag>
<el-tag v-if="dialogData.rowData.status === 'Enable'" round class="status-content" type="success">
{{ $t('commons.status.running') }}
@@ -283,7 +283,7 @@ import { searchRecords, download, handleOnce, updateStatus } from '@/api/modules
import { dateFormat } from '@/utils/util';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { LoadFile } from '@/api/modules/files';
import { DownloadByPath, LoadFile } from '@/api/modules/files';
import LayoutContent from '@/layout/layout-content.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
@@ -472,17 +472,19 @@ const onDownload = async (record: any, backupID: number) => {
recordID: record.id,
backupAccountID: backupID,
};
const res = await download(params);
const downloadUrl = window.URL.createObjectURL(new Blob([res]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
if (record.file && record.file.indexOf('/') !== -1) {
let pathItem = record.file.split('/');
a.download = pathItem[pathItem.length - 1];
}
const event = new MouseEvent('click');
a.dispatchEvent(event);
await download(params).then(async (res) => {
const file = await DownloadByPath(res.data);
const downloadUrl = window.URL.createObjectURL(new Blob([file]));
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
if (record.file && record.file.indexOf('/') !== -1) {
let pathItem = record.file.split('/');
a.download = pathItem[pathItem.length - 1];
}
const event = new MouseEvent('click');
a.dispatchEvent(event);
});
};
const nextPage = async () => {

View File

@@ -4,7 +4,7 @@
<template #app>
<AppStatus
:app-key="'mysql'"
style="margin-top: 20px"
v-model:loading="loading"
@setting="onSetting"
@is-exist="checkExist"
></AppStatus>

View File

@@ -2,7 +2,7 @@
<div>
<el-drawer v-model="changeVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
<template #header>
<DrawerHeader :header="$t('database.changePassword')" :back="handleClose" />
<DrawerHeader :header="title" :back="handleClose" />
</template>
<el-form>
<el-form v-loading="loading" ref="changeFormRef" :model="changeForm" label-position="top">
@@ -78,6 +78,7 @@ const loading = ref();
const changeVisiable = ref(false);
type FormInstance = InstanceType<typeof ElForm>;
const changeFormRef = ref<FormInstance>();
const title = ref();
const changeForm = reactive({
id: 0,
mysqlName: '',
@@ -101,6 +102,10 @@ interface DialogProps {
value: string;
}
const acceptParams = (params: DialogProps): void => {
title.value =
params.operation === 'password'
? i18n.global.t('database.changePassword')
: i18n.global.t('database.permission');
changeForm.id = params.id;
changeForm.mysqlName = params.mysqlName;
changeForm.userName = params.username;

View File

@@ -244,6 +244,7 @@ const onSubmitChangeConf = async () => {
loading.value = true;
await updateMysqlConfByFile(param)
.then(() => {
useOld.value = false;
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
@@ -303,6 +304,7 @@ const loadSlowLogs = async () => {
};
const loadMysqlConf = async (path: string) => {
useOld.value = false;
const res = await LoadFile({ path: path });
loading.value = false;
mysqlConf.value = res.data;

View File

@@ -13,13 +13,15 @@
<div style="float: left">
<el-input type="number" v-model.number="variables.long_query_time" />
</div>
<el-button style="float: left; margin-left: 10px" @click="openSlowLogs">
<el-button style="float: left; margin-left: 10px" @click="changeSlowLogs">
{{ $t('commons.button.save') }}
</el-button>
<div style="float: left; margin-left: 20px">
<el-checkbox border v-model="isWatch">{{ $t('commons.button.watch') }}</el-checkbox>
<el-checkbox style="margin-top: 2px" :disabled="!currentStatus" border v-model="isWatch">
{{ $t('commons.button.watch') }}
</el-checkbox>
</div>
<el-button style="margin-left: 20px" @click="onDownload" icon="Download">
<el-button :disabled="!currentStatus" style="margin-left: 20px" @click="onDownload" icon="Download">
{{ $t('file.download') }}
</el-button>
</el-form-item>
@@ -29,7 +31,7 @@
:placeholder="$t('database.noData')"
:indent-with-tab="true"
:tabSize="4"
style="height: calc(100vh - 427px); width: 100%"
style="height: calc(100vh - 428px); width: 100%"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
@@ -56,7 +58,7 @@ import { updateMysqlVariables } from '@/api/modules/database';
import { dateFormatForName } from '@/utils/util';
import i18n from '@/lang';
import { loadBaseDir } from '@/api/modules/setting';
import { MsgError, MsgSuccess } from '@/utils/message';
import { MsgError, MsgInfo, MsgSuccess } from '@/utils/message';
const extensions = [javascript(), oneDark];
const slowLogs = ref();
@@ -65,7 +67,7 @@ const handleReady = (payload) => {
view.value = payload.view;
};
const detailShow = ref();
const isOnEdit = ref();
const currentStatus = ref();
const confirmDialogRef = ref();
@@ -77,7 +79,6 @@ const variables = reactive({
slow_query_log: 'OFF',
long_query_time: 10,
});
const oldVariables = ref();
interface DialogProps {
mysqlName: string;
@@ -88,29 +89,24 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
variables.slow_query_log = params.variables.slow_query_log;
variables.long_query_time = Number(params.variables.long_query_time);
const pathRes = await loadBaseDir();
let path = `${pathRes.data}/apps/mysql/${mysqlName.value}/data/1Panel-slow.log`;
if (variables.slow_query_log === 'ON') {
currentStatus.value = true;
detailShow.value = true;
const pathRes = await loadBaseDir();
let path = `${pathRes.data}/apps/mysql/${mysqlName.value}/data/1Panel-slow.log`;
loadMysqlSlowlogs(path);
timer = setInterval(() => {
if (variables.slow_query_log === 'ON' && isWatch.value) {
loadMysqlSlowlogs(path);
}
}, 1000 * 5);
}
timer = setInterval(() => {
if (variables.slow_query_log === 'ON' && isWatch.value) {
loadMysqlSlowlogs(path);
}
}, 1000 * 5);
oldVariables.value = { ...variables };
};
const emit = defineEmits(['loading']);
const handleSlowLogs = async () => {
if (variables.slow_query_log === 'ON') {
detailShow.value = true;
isOnEdit.value = true;
return;
}
if (isOnEdit.value) {
detailShow.value = false;
return;
}
let params = {
@@ -121,7 +117,7 @@ const handleSlowLogs = async () => {
confirmDialogRef.value!.acceptParams(params);
};
const openSlowLogs = () => {
const changeSlowLogs = () => {
if (!(variables.long_query_time > 0 && variables.long_query_time <= 600)) {
MsgError(i18n.global.t('database.thresholdRangeHelper'));
return;
@@ -135,14 +131,13 @@ const openSlowLogs = () => {
};
const onCancle = async () => {
variables.slow_query_log = variables.slow_query_log === 'ON' ? 'OFF' : 'ON';
variables.slow_query_log = currentStatus.value ? 'ON' : 'OFF';
detailShow.value = currentStatus.value;
};
const onSave = async () => {
let param = [] as Array<Database.VariablesUpdate>;
if (variables.slow_query_log !== oldVariables.value.slow_query_log) {
param.push({ param: 'slow_query_log', value: variables.slow_query_log });
}
param.push({ param: 'slow_query_log', value: variables.slow_query_log });
if (variables.slow_query_log === 'ON') {
param.push({ param: 'long_query_time', value: variables.long_query_time + '' });
param.push({ param: 'slow_query_log_file', value: '/var/lib/mysql/1Panel-slow.log' });
@@ -151,10 +146,8 @@ const onSave = async () => {
await updateMysqlVariables(param)
.then(() => {
emit('loading', false);
isOnEdit.value = false;
if (variables.slow_query_log !== 'ON') {
detailShow.value = false;
}
currentStatus.value = variables.slow_query_log === 'ON';
detailShow.value = variables.slow_query_log === 'ON';
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
@@ -163,6 +156,10 @@ const onSave = async () => {
};
const onDownload = async () => {
if (!slowLogs.value) {
MsgInfo(i18n.global.t('database.noData'));
return;
}
const downloadUrl = window.URL.createObjectURL(new Blob([slowLogs.value]));
const a = document.createElement('a');
a.style.display = 'none';
@@ -174,7 +171,7 @@ const onDownload = async () => {
const loadMysqlSlowlogs = async (path: string) => {
const res = await LoadFile({ path: path });
slowLogs.value = res.data;
slowLogs.value = res.data || '';
nextTick(() => {
const state = view.value.state;
view.value.dispatch({

View File

@@ -152,7 +152,7 @@ let mysqlVariables = reactive({
});
const variablesRules = reactive({
key_buffer_size: [Rules.number, checkNumberRange(1, 102400)],
query_cache_size: [Rules.number, checkNumberRange(1, 102400)],
query_cache_size: [Rules.number, checkNumberRange(0, 102400)],
tmp_table_size: [Rules.number, checkNumberRange(1, 102400)],
innodb_buffer_pool_size: [Rules.number, checkNumberRange(1, 102400)],
innodb_log_buffer_size: [Rules.number, checkNumberRange(1, 102400)],

View File

@@ -4,7 +4,7 @@
<template #app>
<AppStatus
:app-key="'redis'"
style="margin-top: 20px"
v-model:loading="loading"
@before="onBefore"
@setting="onSetting"
@is-exist="checkExist"
@@ -129,7 +129,6 @@ const closeTerminal = async (isKeepShow: boolean) => {
const onBefore = () => {
closeTerminal(true);
loading.value = true;
};
onBeforeUnmount(() => {
closeTerminal(false);

View File

@@ -163,7 +163,7 @@ const form = reactive({
const rules = reactive({
port: [Rules.port],
timeout: [Rules.number, checkNumberRange(0, 9999999)],
maxclients: [Rules.number, checkNumberRange(1, 999999)],
maxclients: [Rules.number, checkNumberRange(1, 65504)],
maxmemory: [Rules.number, checkNumberRange(0, 999999)],
});
@@ -244,7 +244,6 @@ const onChangePort = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
const result = await formEl.validateField('port', callback);
if (!result) {
console.log('daqdwq');
return;
}
let params = {
@@ -318,6 +317,7 @@ const submtiFile = async () => {
loading.value = true;
await updateRedisConfByFile(param)
.then(() => {
useOld.value = false;
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
@@ -338,6 +338,7 @@ const loadform = async () => {
const loadConfFile = async () => {
const pathRes = await loadBaseDir();
let path = `${pathRes.data}/apps/redis/${redisName.value}/conf/redis.conf`;
useOld.value = false;
loading.value = true;
await LoadFile({ path: path })
.then((res) => {

View File

@@ -9,12 +9,12 @@
>
<el-form :inline="true" :model="config">
<el-form-item :label="$t('file.theme')">
<el-select v-model="config.theme" @change="initEditor()">
<el-select v-model="config.theme" @change="changeTheme()">
<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-select v-model="config.language" @change="changeLanguage()">
<el-option v-for="lang in Languages" :key="lang.label" :value="lang.value" :label="lang.label" />
</el-select>
</el-form-item>
@@ -36,11 +36,34 @@ import { SaveFileContent } from '@/api/modules/files';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import * as monaco from 'monaco-editor';
import { reactive, ref } from 'vue';
import { nextTick, onBeforeUnmount, reactive, ref } from 'vue';
import { Languages } from '@/global/mimetype';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
self.MonacoEnvironment = {
getWorker(workerId, label) {
if (label === 'json') {
return new jsonWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker();
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker();
}
if (['typescript', 'javascript'].includes(label)) {
return new tsWorker();
}
return new EditorWorker();
},
};
interface EditProps {
language: string;
content: string;
@@ -90,29 +113,39 @@ const handleClose = () => {
}
em('close', false);
};
const changeLanguage = () => {
monaco.editor.setModelLanguage(editor.getModel(), config.language);
};
const changeTheme = () => {
monaco.editor.setTheme(config.theme);
};
const initEditor = () => {
if (editor) {
editor.dispose();
}
const codeBox = document.getElementById('codeBox');
editor = monaco.editor.create(codeBox as HTMLElement, {
theme: config.theme,
value: form.value.content,
readOnly: false,
automaticLayout: true,
language: config.language,
folding: true,
roundedSelection: false,
});
nextTick(() => {
const codeBox = document.getElementById('codeBox');
editor = monaco.editor.create(codeBox as HTMLElement, {
theme: config.theme,
value: form.value.content,
readOnly: false,
automaticLayout: true,
language: config.language,
folding: true,
roundedSelection: false,
overviewRulerBorder: false,
});
editor.onDidChangeModelContent(() => {
if (editor) {
form.value.content = editor.getValue();
}
});
editor.onDidChangeModelContent(() => {
if (editor) {
form.value.content = editor.getValue();
}
});
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, quickSave);
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, quickSave);
});
};
const quickSave = () => {
@@ -139,6 +172,12 @@ const onOpen = () => {
initEditor();
};
onBeforeUnmount(() => {
if (editor) {
editor.dispose();
}
});
defineExpose({ acceptParams });
</script>

View File

@@ -88,7 +88,7 @@
<template #default="{ row }">
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
<svg-icon v-else className="table-icon" :iconName="getIconName(row.extension)"></svg-icon>
<el-link :underline="false" @click="open(row)" type="primary">{{ row.name }}</el-link>
<span class="table-link" @click="open(row)" type="primary">{{ row.name }}</span>
<span v-if="row.isSymlink">-> {{ row.linkPath }}</span>
</template>
</el-table-column>

View File

@@ -79,7 +79,7 @@ import type { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { addHost, editHost, testByInfo, GetGroupList } from '@/api/modules/host';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
const loading = ref();
@@ -109,7 +109,7 @@ type FormInstance = InstanceType<typeof ElForm>;
const hostInfoRef = ref<FormInstance>();
const rules = reactive({
groupID: [Rules.requiredSelect],
addr: [Rules.requiredInput],
addr: [Rules.host],
port: [Rules.requiredInput, Rules.port],
user: [Rules.requiredInput],
authMode: [Rules.requiredSelect],
@@ -165,7 +165,7 @@ const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
if (res.data) {
MsgSuccess(i18n.global.t('terminal.connTestOk'));
} else {
MsgSuccess(i18n.global.t('terminal.connTestFailed'));
MsgError(i18n.global.t('terminal.connTestFailed'));
}
});
}

View File

@@ -95,7 +95,7 @@ let hostInfo = reactive<Host.HostOperate>({
});
const rules = reactive({
addr: [Rules.ip],
addr: [Rules.host],
port: [Rules.requiredInput, Rules.port],
user: [Rules.requiredInput],
authMode: [Rules.requiredSelect],

Some files were not shown because too many files have changed in this diff Show More