8 Commits

Author SHA1 Message Date
ssongliu
3e6cd1cab1 feat: 远程数据库增加连接性测试 (#1928)
Refs #1924
2023-08-11 14:20:15 +00:00
ssongliu
98df3806f5 fix: 优化容器清理提示信息 (#1927) 2023-08-11 14:18:17 +00:00
ssongliu
18e8af6234 fix: 容器终端断开连接时,保持抽屉打开 (#1926) 2023-08-11 08:08:14 +00:00
zhengkunwang
5bbda8f842 fix: 解决应用升级镜像拉取失败导致应用服务异常的问题 (#1921) 2023-08-11 07:52:13 +00:00
zhengkunwang
85f8c1e634 feat: 守护进程增加操作列 (#1919) 2023-08-11 07:50:13 +00:00
ssongliu
85c935ee46 fix: 容器创建启动命令提示信息修改 (#1917) 2023-08-11 06:04:12 +00:00
ssongliu
b4033471e7 fix: 修改表格分页偶发的数据丢失问题 (#1911)
Refs #1834
2023-08-10 14:36:13 +00:00
ssongliu
8dca519068 feat: 远程数据库地址支持域名 (#1909) 2023-08-10 14:34:23 +00:00
26 changed files with 555 additions and 190 deletions

View File

@@ -34,6 +34,28 @@ func (b *BaseApi) CreateRemoteDB(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Database
// @Summary Check remote database
// @Description 检测远程数据库连接性
// @Accept json
// @Param request body dto.RemoteDBCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /databases/remote/check [post]
// @x-panel-log {"bodyKeys":["name", "type"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"检测远程数据库 [name][type] 连接性","formatEN":"check if remote database [name][type] is connectable"}
func (b *BaseApi) CheckeRemoteDB(c *gin.Context) {
var req dto.RemoteDBCreate
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
}
helper.SuccessWithData(c, remoteDBService.CheckeRemoteDB(req))
}
// @Tags Database
// @Summary Page remote databases
// @Description 获取远程数据库列表分页

View File

@@ -341,6 +341,24 @@ func upgradeInstall(installId uint, detailId uint, backup bool) error {
install.Version = detail.Version
install.AppDetailId = detailId
images, err := getImages(install)
if err != nil {
upErr = err
return
}
dockerCli, err := composeV2.NewClient()
if err != nil {
upErr = err
return
}
for _, image := range images {
if err = dockerCli.PullImage(image, true); err != nil {
upErr = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return
}
}
if out, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" {
upErr = errors.New(out)
@@ -398,6 +416,26 @@ func getContainerNames(install model.AppInstall) ([]string, error) {
return containerNames, nil
}
func getImages(install model.AppInstall) ([]string, error) {
envStr, err := coverEnvJsonToStr(install.Env)
if err != nil {
return nil, err
}
project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true)
if err != nil {
return nil, err
}
imagesMap := make(map[string]struct{})
for _, service := range project.AllServices() {
imagesMap[service.Image] = struct{}{}
}
var images []string
for k := range imagesMap {
images = append(images, k)
}
return images, nil
}
func coverEnvJsonToStr(envJson string) (string, error) {
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(envJson), &envMap)

View File

@@ -16,6 +16,7 @@ type RemoteDBService struct{}
type IRemoteDBService interface {
Get(name string) (dto.RemoteDBInfo, error)
SearchWithPage(search dto.RemoteDBSearch) (int64, interface{}, error)
CheckeRemoteDB(req dto.RemoteDBCreate) bool
Create(req dto.RemoteDBCreate) error
Update(req dto.RemoteDBUpdate) error
Delete(id uint) error
@@ -68,6 +69,20 @@ func (u *RemoteDBService) List(dbType string) ([]dto.RemoteDBOption, error) {
return datas, err
}
func (u *RemoteDBService) CheckeRemoteDB(req dto.RemoteDBCreate) bool {
if _, err := mysql.NewMysqlClient(client.DBInfo{
From: "remote",
Address: req.Address,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 6,
}); err != nil {
return false
}
return true
}
func (u *RemoteDBService) Create(req dto.RemoteDBCreate) error {
db, _ := remoteDBRepo.Get(commonRepo.WithByName(req.Name))
if db.ID != 0 {
@@ -79,7 +94,7 @@ func (u *RemoteDBService) Create(req dto.RemoteDBCreate) error {
Port: req.Port,
Username: req.Username,
Password: req.Password,
Timeout: 300,
Timeout: 6,
}); err != nil {
return err
}

View File

@@ -61,3 +61,18 @@ func WithMap(Key string, maps map[string]interface{}, err error) BusinessError {
Err: err,
}
}
func WithNameAndErr(Key string, name string, err error) BusinessError {
paramMap := map[string]interface{}{}
if name != "" {
paramMap["name"] = name
}
if err != nil {
paramMap["err"] = err.Error()
}
return BusinessError{
Msg: Key,
Map: paramMap,
Err: err,
}
}

View File

@@ -45,6 +45,7 @@ ErrImagePullTimeOut: 'Image pull timeout'
ErrContainerNotFound: '{{ .name }} container does not exist'
ErrContainerMsg: '{{ .name }} container is abnormal, please check the log on the container page for details'
ErrAppBackup: '{{ .name }} application backup failed err {{.err}}'
ErrImagePull: '{{ .name }} image pull failed err {{.err}}'
#file
ErrFileCanNotRead: "File can not read"

View File

@@ -45,6 +45,7 @@ ErrImagePullTimeOut: "鏡像拉取超時"
ErrContainerNotFound: '{{ .name }} 容器不存在'
ErrContainerMsg: '{{ .name }} 容器異常,具體請在容器頁面查看日誌'
ErrAppBackup: '{{ .name }} 應用備份失敗 err {{.err}}'
ErrImagePull: '{{ .name }} 鏡像拉取失敗 err {{.err}}'
#file
ErrFileCanNotRead: "此文件不支持預覽"

View File

@@ -45,6 +45,7 @@ ErrImagePullTimeOut: '镜像拉取超时'
ErrContainerNotFound: '{{ .name }} 容器不存在'
ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日志'
ErrAppBackup: '{{ .name }} 应用备份失败 err {{.err}}'
ErrImagePull: '镜像拉取失败 {{.err}}'
#file
ErrFileCanNotRead: "此文件不支持预览"

View File

@@ -43,6 +43,7 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/conffile/update", baseApi.UpdateRedisConfByFile)
cmdRouter.POST("/redis/persistence/update", baseApi.UpdateRedisPersistenceConf)
cmdRouter.POST("/remote/check", baseApi.CheckeRemoteDB)
cmdRouter.POST("/remote", baseApi.CreateRemoteDB)
cmdRouter.GET("/remote/:name", baseApi.GetRemoteDB)
cmdRouter.GET("/remote/list/:type", baseApi.ListRemoteDB)

View File

@@ -4,11 +4,7 @@ import (
"context"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/docker/client"
"github.com/joho/godotenv"
"path"
"regexp"
@@ -21,24 +17,6 @@ type ComposeService struct {
project *types.Project
}
func NewComposeService(ops ...command.DockerCliOption) (*ComposeService, error) {
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}
ops = append(ops, command.WithAPIClient(apiClient), command.WithDefaultContextStoreConfig())
cli, err := command.NewDockerCli(ops...)
if err != nil {
return nil, err
}
cliOp := flags.NewClientOptions()
if err := cli.Initialize(cliOp); err != nil {
return nil, err
}
service := compose.NewComposeService(cli)
return &ComposeService{service, nil}, nil
}
func (s *ComposeService) SetProject(project *types.Project) {
s.project = project
for i, s := range project.Services {

View File

@@ -72,6 +72,22 @@ func (c Client) DeleteImage(imageID string) error {
return nil
}
func (c Client) PullImage(imageName string, force bool) error {
if !force {
exist, err := c.CheckImageExist(imageName)
if err != nil {
return err
}
if exist {
return nil
}
}
if _, err := c.cli.ImagePull(context.Background(), imageName, types.ImagePullOptions{}); err != nil {
return err
}
return nil
}
func (c Client) GetImageIDByName(imageName string) (string, error) {
filter := filters.NewArgs()
filter.Add("reference", imageName)
@@ -87,6 +103,18 @@ func (c Client) GetImageIDByName(imageName string) (string, error) {
return "", nil
}
func (c Client) CheckImageExist(imageName string) (bool, error) {
filter := filters.NewArgs()
filter.Add("reference", imageName)
list, err := c.cli.ImageList(context.Background(), types.ImageListOptions{
Filters: filter,
})
if err != nil {
return false, err
}
return len(list) > 0, nil
}
func (c Client) NetworkExist(name string) bool {
var options types.NetworkListOptions
options.Filters = filters.NewArgs(filters.Arg("name", name))

View File

@@ -1,8 +1,10 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -38,9 +40,16 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) {
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(conn.Timeout)*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, err
}
if ctx.Err() == context.DeadlineExceeded {
return nil, buserr.New(constant.ErrExecTimeOut)
}
return client.NewRemote(client.Remote{
Client: db,
From: conn.From,

View File

@@ -4410,6 +4410,49 @@ const docTemplate = `{
}
}
},
"/databases/remote/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "检测远程数据库连接性",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RemoteDBCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"type"
],
"formatEN": "check if remote database [name][type] is connectable",
"formatZH": "检测远程数据库 [name][type] 连接性",
"paramKeys": []
}
}
},
"/databases/remote/del": {
"post": {
"security": [
@@ -11621,7 +11664,6 @@ const docTemplate = `{
"dto.ChangeDBInfo": {
"type": "object",
"required": [
"from",
"value"
],
"properties": {
@@ -12923,13 +12965,15 @@ const docTemplate = `{
"type": "integer"
},
"password": {
"type": "string"
"type": "string",
"maxLength": 256
},
"protocol": {
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 256
}
}
},
@@ -13776,7 +13820,8 @@ const docTemplate = `{
]
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"
@@ -13817,7 +13862,8 @@ const docTemplate = `{
"type": "integer"
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"

View File

@@ -4403,6 +4403,49 @@
}
}
},
"/databases/remote/check": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "检测远程数据库连接性",
"consumes": [
"application/json"
],
"tags": [
"Database"
],
"summary": "Check remote database",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RemoteDBCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"type"
],
"formatEN": "check if remote database [name][type] is connectable",
"formatZH": "检测远程数据库 [name][type] 连接性",
"paramKeys": []
}
}
},
"/databases/remote/del": {
"post": {
"security": [
@@ -11614,7 +11657,6 @@
"dto.ChangeDBInfo": {
"type": "object",
"required": [
"from",
"value"
],
"properties": {
@@ -12916,13 +12958,15 @@
"type": "integer"
},
"password": {
"type": "string"
"type": "string",
"maxLength": 256
},
"protocol": {
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 256
}
}
},
@@ -13769,7 +13813,8 @@
]
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"
@@ -13810,7 +13855,8 @@
"type": "integer"
},
"name": {
"type": "string"
"type": "string",
"maxLength": 256
},
"password": {
"type": "string"

View File

@@ -127,7 +127,6 @@ definitions:
value:
type: string
required:
- from
- value
type: object
dto.ChangeHostGroup:
@@ -1000,10 +999,12 @@ definitions:
id:
type: integer
password:
maxLength: 256
type: string
protocol:
type: string
username:
maxLength: 256
type: string
type: object
dto.ImageSave:
@@ -1565,6 +1566,7 @@ definitions:
- remote
type: string
name:
maxLength: 256
type: string
password:
type: string
@@ -1599,6 +1601,7 @@ definitions:
id:
type: integer
name:
maxLength: 256
type: string
password:
type: string
@@ -6648,6 +6651,34 @@ paths:
summary: Get remote databases
tags:
- Database
/databases/remote/check:
post:
consumes:
- application/json
description: 检测远程数据库连接性
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.RemoteDBCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Check remote database
tags:
- Database
x-panel-log:
BeforeFuntions: []
bodyKeys:
- name
- type
formatEN: check if remote database [name][type] is connectable
formatZH: 检测远程数据库 [name][type] 连接性
paramKeys: []
/databases/remote/del:
post:
consumes:

View File

@@ -100,11 +100,14 @@ export const searchRemoteDBs = (params: Database.SearchRemoteDBPage) => {
export const listRemoteDBs = (type: string) => {
return http.get<Array<Database.RemoteDBOption>>(`/databases/remote/list/${type}`);
};
export const checkRemoteDB = (params: Database.RemoteDBCreate) => {
return http.post<boolean>(`/databases/remote/check`, params, 40000);
};
export const addRemoteDB = (params: Database.RemoteDBCreate) => {
return http.post(`/databases/remote`, params);
return http.post(`/databases/remote`, params, 40000);
};
export const editRemoteDB = (params: Database.RemoteDBUpdate) => {
return http.post(`/databases/remote/update`, params);
return http.post(`/databases/remote/update`, params, 40000);
};
export const deleteRemoteDB = (id: number) => {
return http.post(`/databases/remote/del`, { id: id });

View File

@@ -3,23 +3,6 @@
<div class="complex-table__header" v-if="$slots.header || header">
<slot name="header">{{ header }}</slot>
</div>
<div v-if="$slots.toolbar && !searchConfig" style="margin-bottom: 10px">
<slot name="toolbar"></slot>
</div>
<template v-if="searchConfig">
<fu-filter-bar v-bind="searchConfig" @exec="search">
<template #tl>
<slot name="toolbar"></slot>
</template>
<template #default>
<slot name="complex"></slot>
</template>
<template #buttons>
<slot name="buttons"></slot>
</template>
</fu-filter-bar>
</template>
<div class="complex-table__body">
<fu-table v-bind="$attrs" ref="tableRef" @selection-change="handleSelectionChange">
@@ -30,13 +13,14 @@
</fu-table>
</div>
<div class="complex-table__pagination" v-if="$slots.pagination || paginationConfig">
<div class="complex-table__pagination" v-if="props.paginationConfig">
<slot name="pagination">
<fu-table-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
v-bind="paginationConfig"
@change="search"
:total="paginationConfig.total"
@size-change="sizeChange"
@current-change="currentChange"
:small="mobile"
:layout="mobile ? 'total, prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
/>
@@ -49,15 +33,15 @@ import { ref, computed } from 'vue';
import { GlobalStore } from '@/store';
defineOptions({ name: 'ComplexTable' });
defineProps({
const props = defineProps({
header: String,
searchConfig: Object,
paginationConfig: {
type: Object,
required: false,
default: () => {},
},
});
const emit = defineEmits(['search', 'update:selects']);
const emit = defineEmits(['search', 'update:selects', 'update:paginationConfig']);
const globalStore = GlobalStore();
@@ -65,13 +49,15 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
const condition = ref({});
const tableRef = ref();
function search(conditions: any, e: any) {
if (conditions) {
condition.value = conditions;
}
emit('search', condition.value, e);
function currentChange() {
emit('search');
}
function sizeChange() {
props.paginationConfig.currentPage = 1;
emit('search');
}
function handleSelectionChange(row: any) {

View File

@@ -13,20 +13,7 @@ const terminalElement = ref<HTMLDivElement | null>(null);
const fitAddon = new FitAddon();
const termReady = ref(false);
const webSocketReady = ref(false);
const term = ref(
new Terminal({
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true,
cursorStyle: 'underline',
scrollback: 100,
tabStopWidth: 4,
}),
);
const term = ref();
const terminalSocket = ref<WebSocket>();
const heartbeatTimer = ref<number>();
const latency = ref(0);
@@ -56,6 +43,21 @@ const acceptParams = (props: WsProps) => {
});
};
const newTerm = () => {
term.value = new Terminal({
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true,
cursorStyle: 'underline',
scrollback: 100,
tabStopWidth: 4,
});
};
const init = (endpoint: string, args: string) => {
if (initTerminal(true)) {
initWebSocket(endpoint, args);
@@ -78,11 +80,13 @@ function onClose(isKeepShow: boolean = false) {
term.value.dispose();
} catch {}
}
terminalElement.value.innerHTML = '';
}
// terminal 相关代码 start
const initTerminal = (online: boolean = false): boolean => {
newTerm();
if (terminalElement.value) {
term.value.open(terminalElement.value);
term.value.loadAddon(fitAddon);

View File

@@ -479,7 +479,10 @@ const message = {
rename: 'Rename',
remove: 'Remove',
containerPrune: 'Container prune',
containerPruneHelper: 'Remove all stopped containers. Do you want to continue?',
containerPruneHelper1: 'Cleaning containers will delete all containers that are in a stopped state.',
containerPruneHelper2:
'If the containers are from the app store, after performing the cleanup, you need to go to the [Installed] list in the [App Store] and click the [Rebuild] button to reinstall them.',
containerPruneHelper3: 'This operation cannot be rolled back. Do you want to continue?',
imagePrune: 'Image prune',
imagePruneSome: 'Clean unlabeled',
imagePruneSomeHelper: 'Remove all unused and unlabeled container images',
@@ -533,7 +536,7 @@ const message = {
exposePort: 'Expose port',
exposeAll: 'Expose all',
cmd: 'Command',
cmdHelper: 'Example: echo "hello"',
cmdHelper: "Separate multiple commands with ' ' as delimiter, such as 'nginx' '-g' 'daemon off;'",
autoRemove: 'Auto remove',
cpuQuota: 'NacosCPU',
memoryLimit: 'Memory',
@@ -1684,6 +1687,14 @@ const message = {
restartHelper:
'Initialization will restart the service, causing all the original daemon processes to close',
msg: 'Message',
RUNNING: 'Running',
STOPPED: 'Stopped',
STOPPING: 'Stopping',
STARTING: 'Starting',
FATAL: 'Failed to start',
BACKOFF: 'Start exception',
statusCode: 'Status code',
manage: 'Management',
},
},
};

View File

@@ -469,7 +469,10 @@ const message = {
rename: '重命名',
remove: '刪除',
containerPrune: '清理容器',
containerPruneHelper: '清理容器 將刪除所有處於停止狀態的容器該操作無法回滾是否繼續',
containerPruneHelper1: '清理容器 將刪除所有處於停止狀態的容器',
containerPruneHelper2:
'若容器來自於應用商店在執行清理操作後您需要前往 [應用商店] [已安裝] 列表點擊 [重建] 按鈕進行重新安裝',
containerPruneHelper3: '該操作無法回滾是否繼續',
imagePrune: '清理鏡像',
imagePruneSome: '未標簽鏡像',
imagePruneSomeHelper: '清理標簽為 none 且未被任何容器使用的鏡像',
@@ -519,7 +522,7 @@ const message = {
exposePort: '暴露端口',
exposeAll: '暴露所有',
cmd: '啟動命令',
cmdHelper: 'echo "hello"',
cmdHelper: "多個命令間請用 ' ' 分隔開,如 'nginx' '-g' 'daemon off;'",
autoRemove: '容器退出後自動刪除容器',
cpuQuota: 'CPU 限製',
memoryLimit: '內存限製',
@@ -1596,6 +1599,14 @@ const message = {
serviceNameHelper: 'systemctl 管理的 Supervisor 服務名稱一般為 supervisor supervisord',
restartHelper: '初始化會重啟服務導致原有的守護進程全部關閉',
msg: '信息',
RUNNING: '運行中',
STOPPED: '已停止',
STOPPING: '停止中',
STARTING: '啟動中',
FATAL: '啟動失敗',
BACKOFF: '啟動異常',
statusCode: '狀態碼',
manage: '管理',
},
},
};

View File

@@ -469,7 +469,10 @@ const message = {
rename: '重命名',
remove: '删除',
containerPrune: '清理容器',
containerPruneHelper: '清理容器 将删除所有处于停止状态的容器该操作无法回滚是否继续',
containerPruneHelper1: '清理容器 将删除所有处于停止状态的容器',
containerPruneHelper2:
'若容器来自于应用商店在执行清理操作后您需要前往 [应用商店] [已安装] 列表点击 [重建] 按钮进行重新安装',
containerPruneHelper3: '该操作无法回滚是否继续',
imagePrune: '清理镜像',
imagePruneSome: '未标签镜像',
imagePruneSomeHelper: '清理标签为 none 且未被任何容器使用的镜像',
@@ -519,7 +522,7 @@ const message = {
exposePort: '暴露端口',
exposeAll: '暴露所有',
cmd: '启动命令',
cmdHelper: 'echo "hello"',
cmdHelper: "多个命令间请用 ' ' 分隔开,如 'nginx' '-g' 'daemon off;'",
autoRemove: '容器退出后自动删除容器',
cpuQuota: 'CPU 限制',
memoryLimit: '内存限制',
@@ -1598,6 +1601,14 @@ const message = {
serviceNameHelper: 'systemctl 管理的 Supervisor 服务名称一般为 supervisorsupervisord',
restartHelper: '初始化会重启服务导致原有的守护进程全部关闭',
msg: '信息',
RUNNING: '运行中',
STOPPED: '已停止',
STOPPING: '停止中',
STARTING: '启动中',
FATAL: '启动失败',
BACKOFF: '启动异常',
statusCode: '状态码',
manage: '管理',
},
},
};

View File

@@ -157,6 +157,7 @@
</LayoutContent>
<CodemirrorDialog ref="mydetail" />
<PruneDialog @search="search" ref="dialogPruneRef" />
<ReNameDialog @search="search" ref="dialogReNameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" />
@@ -172,6 +173,7 @@
<script lang="ts" setup>
import Tooltip from '@/components/tooltip/index.vue';
import TableSetting from '@/components/table-setting/index.vue';
import PruneDialog from '@/views/container/container/prune/index.vue';
import ReNameDialog from '@/views/container/container/rename/index.vue';
import OperateDialog from '@/views/container/container/operate/index.vue';
import UpgraeDialog from '@/views/container/container/upgrade/index.vue';
@@ -185,7 +187,6 @@ import { reactive, onMounted, ref, computed } from 'vue';
import {
containerListStats,
containerOperator,
containerPrune,
inspect,
loadContainerInfo,
loadDockerStatus,
@@ -196,7 +197,6 @@ import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import router from '@/routers';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { computeSize } from '@/utils/util';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
@@ -264,6 +264,7 @@ const mydetail = ref();
const dialogContainerLogRef = ref();
const dialogReNameRef = ref();
const dialogPruneRef = ref();
const search = async (column?: any) => {
let filterItem = props.filters ? props.filters : '';
@@ -360,31 +361,7 @@ const onInspect = async (id: string) => {
};
const onClean = () => {
ElMessageBox.confirm(i18n.global.t('container.containerPruneHelper'), i18n.global.t('container.containerPrune'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
let params = {
pruneType: 'container',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
search();
})
.catch(() => {
loading.value = false;
});
});
dialogPruneRef.value!.acceptParams();
};
const checkStatus = (operation: string, row: Container.ContainerInfo | null) => {

View File

@@ -110,7 +110,8 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('container.cmd')" prop="cmdStr">
<el-input :placeholder="$t('container.cmdHelper')" v-model="dialogData.rowData!.cmdStr" />
<el-input v-model="dialogData.rowData!.cmdStr" />
<span class="input-help">{{ $t('container.cmdHelper') }}</span>
</el-form-item>
<el-form-item prop="autoRemove">
<el-checkbox v-model="dialogData.rowData!.autoRemove">

View File

@@ -0,0 +1,71 @@
<template>
<el-dialog v-model="dialogVisiable" :title="$t('container.containerPrune')" :destroy-on-close="true" width="30%">
<div>
<ul class="help-ul">
<li lineClass style="color: red">{{ $t('container.containerPruneHelper1') }}</li>
<li class="lineClass">{{ $t('container.containerPruneHelper2') }}</li>
<li class="lineClass">{{ $t('container.containerPruneHelper3') }}</li>
</ul>
</div>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="dialogVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onClean()">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { containerPrune } from '@/api/modules/container';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { ref } from 'vue';
import { computeSize } from '@/utils/util';
const loading = ref(false);
const dialogVisiable = ref<boolean>(false);
const emit = defineEmits<{ (e: 'search'): void }>();
const onClean = async () => {
loading.value = true;
let params = {
pruneType: 'container',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
dialogVisiable.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
});
};
const acceptParams = (): void => {
dialogVisiable.value = true;
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.lineClass {
line-height: 30px;
}
</style>

View File

@@ -43,7 +43,7 @@
<el-button v-if="!terminalOpen" @click="initTerm(formRef)">
{{ $t('commons.button.conn') }}
</el-button>
<el-button v-else @click="handleClose()">{{ $t('commons.button.disconn') }}</el-button>
<el-button v-else @click="onClose()">{{ $t('commons.button.disconn') }}</el-button>
<Terminal style="height: calc(100vh - 302px)" ref="terminalRef"></Terminal>
</el-form>
</el-drawer>
@@ -99,10 +99,14 @@ const initTerm = (formEl: FormInstance | undefined) => {
});
};
function handleClose() {
const onClose = () => {
terminalRef.value?.onClose();
terminalVisiable.value = false;
terminalOpen.value = false;
};
function handleClose() {
onClose();
terminalVisiable.value = false;
}
defineExpose({

View File

@@ -8,7 +8,7 @@
:back="handleClose"
/>
</template>
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-form ref="formRef" v-loading="loading" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('commons.table.name')" prop="name">
@@ -20,7 +20,7 @@
<el-tag v-else>{{ dialogData.rowData!.name }}</el-tag>
</el-form-item>
<el-form-item :label="$t('database.version')" prop="version">
<el-select v-model="dialogData.rowData!.version">
<el-select @change="isOK = false" v-model="dialogData.rowData!.version">
<el-option value="5.6" label="5.6" />
<el-option value="5.7" label="5.7" />
<el-option value="8.0" label="8.0" />
@@ -28,17 +28,23 @@
<span class="input-help">{{ $t('database.versionHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('database.address')" prop="address">
<el-input clearable v-model.trim="dialogData.rowData!.address" />
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.address" />
</el-form-item>
<el-form-item :label="$t('commons.table.port')" prop="port">
<el-input clearable v-model.number="dialogData.rowData!.port" />
<el-input @change="isOK = false" clearable v-model.number="dialogData.rowData!.port" />
</el-form-item>
<el-form-item :label="$t('commons.login.username')" prop="username">
<el-input clearable v-model.trim="dialogData.rowData!.username" />
<el-input @change="isOK = false" clearable v-model.trim="dialogData.rowData!.username" />
<span class="input-help">{{ $t('database.userHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input type="password" clearable show-password v-model.trim="dialogData.rowData!.password" />
<el-input
@change="isOK = false"
type="password"
clearable
show-password
v-model.trim="dialogData.rowData!.password"
/>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input clearable v-model.trim="dialogData.rowData!.description" />
@@ -49,7 +55,10 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
<el-button @click="onSubmit(formRef, 'check')">
{{ $t('terminal.testConn') }}
</el-button>
<el-button type="primary" :disabled="!isOK" @click="onSubmit(formRef, dialogData.title)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -63,9 +72,9 @@ import i18n from '@/lang';
import { ElForm } from 'element-plus';
import { Database } from '@/api/interface/database';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { addRemoteDB, editRemoteDB } from '@/api/modules/database';
import { addRemoteDB, checkRemoteDB, editRemoteDB } from '@/api/modules/database';
interface DialogProps {
title: string;
@@ -77,6 +86,9 @@ const drawerVisiable = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const isOK = ref(false);
const loading = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('database.' + dialogData.value.title + 'RemoteDB');
@@ -91,7 +103,7 @@ const handleClose = () => {
const rules = reactive({
name: [Rules.requiredInput],
version: [Rules.requiredSelect],
address: [Rules.ip],
address: [Rules.host],
port: [Rules.port],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
@@ -100,40 +112,65 @@ const rules = reactive({
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {
const onSubmit = async (formEl: FormInstance | undefined, operation: string) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.title === 'create') {
let param = {
name: dialogData.value.rowData.name,
type: 'mysql',
version: dialogData.value.rowData.version,
from: 'remote',
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await addRemoteDB(param);
}
if (dialogData.value.title === 'edit') {
let param = {
id: dialogData.value.rowData.id,
version: dialogData.value.rowData.version,
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
await editRemoteDB(param);
let param = {
id: dialogData.value.rowData.id,
name: dialogData.value.rowData.name,
type: 'mysql',
version: dialogData.value.rowData.version,
from: 'remote',
address: dialogData.value.rowData.address,
port: dialogData.value.rowData.port,
username: dialogData.value.rowData.username,
password: dialogData.value.rowData.password,
description: dialogData.value.rowData.description,
};
loading.value = true;
if (operation === 'check') {
await checkRemoteDB(param)
.then((res) => {
loading.value = false;
if (res.data) {
isOK.value = true;
MsgSuccess(i18n.global.t('terminal.connTestOk'));
} else {
MsgError(i18n.global.t('terminal.connTestFailed'));
}
})
.catch(() => {
loading.value = false;
MsgError(i18n.global.t('terminal.connTestFailed'));
});
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
if (operation === 'create') {
await addRemoteDB(param)
.then(() => {
loading.value = true;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
}
if (operation === 'edit') {
await editRemoteDB(param)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisiable.value = false;
})
.catch(() => {
loading.value = false;
});
}
});
};

View File

@@ -39,6 +39,39 @@
prop="numprocs"
width="100px"
></el-table-column>
<el-table-column :label="$t('tool.supervisor.manage')" width="100px">
<template #default="{ row }">
<div v-if="row.status && row.status.length > 0">
<el-button
v-if="checkStatus(row.status) === 'RUNNING'"
link
type="success"
:icon="VideoPlay"
@click="operate('stop', row.name)"
>
{{ $t('commons.status.running') }}
</el-button>
<el-button
v-else-if="checkStatus(row.status) === 'WARNING'"
link
type="warning"
:icon="RefreshRight"
@click="operate('restart', row.name)"
>
{{ $t('commons.status.unhealthy') }}
</el-button>
<el-button
v-else
link
type="danger"
:icon="VideoPause"
@click="operate('start', row.name)"
>
{{ $t('commons.status.stopped') }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" width="100px">
<template #default="{ row }">
<div v-if="row.status">
@@ -48,7 +81,7 @@
{{ $t('website.check') }}
</el-button>
<el-button type="primary" link v-else>
<span>{{ row.status[0].status }}</span>
<span>{{ $t('tool.supervisor.' + row.status[0].status) }}</span>
</el-button>
</template>
<el-table :data="row.status">
@@ -60,7 +93,7 @@
/>
<el-table-column
property="status"
:label="$t('commons.table.status')"
:label="$t('tool.supervisor.statusCode')"
width="100px"
/>
<el-table-column property="PID" label="PID" width="100px" />
@@ -85,7 +118,7 @@
:buttons="buttons"
:label="$t('commons.table.operate')"
:fixed="mobile ? false : 'right'"
width="350px"
width="250px"
fix
/>
</ComplexTable>
@@ -110,6 +143,7 @@ import { GlobalStore } from '@/store';
import i18n from '@/lang';
import { HostTool } from '@/api/interface/host-tool';
import { MsgSuccess } from '@/utils/message';
import { VideoPlay, VideoPause, RefreshRight } from '@element-plus/icons-vue';
const globalStore = GlobalStore();
const loading = ref(false);
@@ -170,6 +204,20 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
const checkStatus = (status: HostTool.ProcessStatus[]): string => {
if (!status || status.length === 0) return 'STOPPED';
const statusCounts = status.reduce((acc, curr) => {
acc[curr.status] = (acc[curr.status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
if (statusCounts['STARTING']) return 'STARTING';
if (statusCounts['RUNNING'] === status.length) return 'RUNNING';
if (statusCounts['RUNNING'] > 0) return 'WARNING';
return 'STOPPED';
};
const operate = async (operation: string, name: string) => {
try {
ElMessageBox.confirm(
@@ -224,42 +272,11 @@ const buttons = [
getFile(row.name, 'out.log');
},
},
{
label: i18n.global.t('app.start'),
click: function (row: HostTool.SupersivorProcess) {
operate('start', row.name);
},
disabled: (row: any) => {
if (row.status == undefined) {
return true;
} else {
return row.status && row.status[0].status == 'RUNNING';
}
},
},
{
label: i18n.global.t('app.stop'),
click: function (row: HostTool.SupersivorProcess) {
operate('stop', row.name);
},
disabled: (row: any) => {
if (row.status == undefined) {
return true;
}
return row.status && row.status[0].status != 'RUNNING';
},
},
{
label: i18n.global.t('commons.button.restart'),
click: function (row: HostTool.SupersivorProcess) {
operate('restart', row.name);
},
disabled: (row: any): boolean => {
if (row.status == undefined) {
return true;
}
return row.status && row.status[0].status != 'RUNNING';
},
},
{
label: i18n.global.t('commons.button.delete'),