mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-18 22:22:59 +08:00
feat: 实现容器启停、列表、日志等功能
This commit is contained in:
parent
20a57cc5ab
commit
a79ba71ef4
82
backend/app/api/v1/container.go
Normal file
82
backend/app/api/v1/container.go
Normal file
@ -0,0 +1,82 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
var req dto.PageContainer
|
||||
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
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *BaseApi) ContainerOperation(c *gin.Context) {
|
||||
var req dto.ContainerOperation
|
||||
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
|
||||
}
|
||||
if err := containerService.ContainerOperation(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) ContainerDetail(c *gin.Context) {
|
||||
id, ok := c.Params.Get("id")
|
||||
if !ok {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error id in path"))
|
||||
return
|
||||
}
|
||||
result, err := containerService.ContainerInspect(id)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
func (b *BaseApi) ContainerLogs(c *gin.Context) {
|
||||
var req dto.ContainerLog
|
||||
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
|
||||
}
|
||||
logs, err := containerService.ContainerLogs(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, logs)
|
||||
}
|
@ -13,6 +13,7 @@ var (
|
||||
hostService = service.ServiceGroupApp.HostService
|
||||
backupService = service.ServiceGroupApp.BackupService
|
||||
groupService = service.ServiceGroupApp.GroupService
|
||||
containerService = service.ServiceGroupApp.ContainerService
|
||||
commandService = service.ServiceGroupApp.CommandService
|
||||
operationService = service.ServiceGroupApp.OperationService
|
||||
fileService = service.ServiceGroupApp.FileService
|
||||
|
27
backend/app/dto/container.go
Normal file
27
backend/app/dto/container.go
Normal file
@ -0,0 +1,27 @@
|
||||
package dto
|
||||
|
||||
type PageContainer struct {
|
||||
PageInfo
|
||||
Status string `json:"status" validate:"required,oneof=all running"`
|
||||
}
|
||||
|
||||
type ContainerInfo struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
Name string `json:"name"`
|
||||
ImageId string `json:"imageID"`
|
||||
ImageName string `json:"imageName"`
|
||||
CreateTime string `json:"createTime"`
|
||||
State string `json:"state"`
|
||||
RunTime string `json:"runTime"`
|
||||
}
|
||||
|
||||
type ContainerLog struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Mode string `json:"mode" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerOperation struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop reStart kill pause unPause reName remove"`
|
||||
NewName string `json:"newName"`
|
||||
}
|
140
backend/app/service/container.go
Normal file
140
backend/app/service/container.go
Normal file
@ -0,0 +1,140 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/1Panel-dev/1Panel/utils/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
)
|
||||
|
||||
type ContainerService struct{}
|
||||
|
||||
type IContainerService interface {
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
ContainerOperation(req dto.ContainerOperation) error
|
||||
ContainerLogs(param dto.ContainerLog) (string, error)
|
||||
ContainerInspect(id string) (string, error)
|
||||
}
|
||||
|
||||
func NewIContainerService() IContainerService {
|
||||
return &ContainerService{}
|
||||
}
|
||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
||||
var (
|
||||
records []types.Container
|
||||
list []types.Container
|
||||
backDatas []dto.ContainerInfo
|
||||
)
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
list, err = client.ContainerList(context.Background(), types.ContainerListOptions{All: req.Status == "all"})
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
records = make([]types.Container, 0)
|
||||
} else {
|
||||
if end >= total {
|
||||
end = total
|
||||
}
|
||||
records = list[start:end]
|
||||
}
|
||||
|
||||
for _, container := range records {
|
||||
backDatas = append(backDatas, dto.ContainerInfo{
|
||||
ContainerID: container.ID,
|
||||
CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
|
||||
Name: container.Names[0][1:],
|
||||
ImageId: strings.Split(container.ImageID, ":")[1],
|
||||
ImageName: container.Image,
|
||||
State: container.State,
|
||||
RunTime: container.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return int64(total), backDatas, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
dc, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch req.Operation {
|
||||
case constant.ContainerOpStart:
|
||||
err = dc.ContainerStart(ctx, req.ContainerID, types.ContainerStartOptions{})
|
||||
case constant.ContainerOpStop:
|
||||
err = dc.ContainerStop(ctx, req.ContainerID, nil)
|
||||
case constant.ContainerOpRestart:
|
||||
err = dc.ContainerRestart(ctx, req.ContainerID, nil)
|
||||
case constant.ContainerOpKill:
|
||||
err = dc.ContainerKill(ctx, req.ContainerID, "SIGKILL")
|
||||
case constant.ContainerOpPause:
|
||||
err = dc.ContainerPause(ctx, req.ContainerID)
|
||||
case constant.ContainerOpUnpause:
|
||||
err = dc.ContainerUnpause(ctx, req.ContainerID)
|
||||
case constant.ContainerOpRename:
|
||||
err = dc.ContainerRename(ctx, req.ContainerID, req.NewName)
|
||||
case constant.ContainerOpRemove:
|
||||
err = dc.ContainerRemove(ctx, req.ContainerID, types.ContainerRemoveOptions{RemoveVolumes: true, RemoveLinks: true, Force: true})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *ContainerService) ContainerInspect(id string) (string, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
inspect, err := client.ContainerInspect(context.Background(), id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bytes, err := json.Marshal(inspect)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
|
||||
var (
|
||||
options types.ContainerLogsOptions
|
||||
logs io.ReadCloser
|
||||
buf *bytes.Buffer
|
||||
err error
|
||||
)
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
options = types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
Timestamps: true,
|
||||
}
|
||||
if req.Mode != "all" {
|
||||
options.Since = req.Mode
|
||||
}
|
||||
if logs, err = client.ContainerLogs(context.Background(), req.ContainerID, options); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer logs.Close()
|
||||
buf = new(bytes.Buffer)
|
||||
if _, err = stdcopy.StdCopy(buf, nil, logs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
@ -7,6 +7,7 @@ type ServiceGroup struct {
|
||||
HostService
|
||||
BackupService
|
||||
GroupService
|
||||
ContainerService
|
||||
CommandService
|
||||
OperationService
|
||||
FileService
|
||||
|
12
backend/constant/container.go
Normal file
12
backend/constant/container.go
Normal file
@ -0,0 +1,12 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ContainerOpStart = "start"
|
||||
ContainerOpStop = "stop"
|
||||
ContainerOpRestart = "reStart"
|
||||
ContainerOpKill = "kill"
|
||||
ContainerOpPause = "pause"
|
||||
ContainerOpUnpause = "unPause"
|
||||
ContainerOpRename = "reName"
|
||||
ContainerOpRemove = "remove"
|
||||
)
|
@ -69,6 +69,7 @@ func Routers() *gin.Engine {
|
||||
systemRouter.InitHostRouter(PrivateGroup)
|
||||
systemRouter.InitBackupRouter(PrivateGroup)
|
||||
systemRouter.InitGroupRouter(PrivateGroup)
|
||||
systemRouter.InitContainerRouter(PrivateGroup)
|
||||
systemRouter.InitCommandRouter(PrivateGroup)
|
||||
systemRouter.InitTerminalRouter(PrivateGroup)
|
||||
systemRouter.InitMonitorRouter(PrivateGroup)
|
||||
|
28
backend/router/container.go
Normal file
28
backend/router/container.go
Normal file
@ -0,0 +1,28 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ContainerRouter struct{}
|
||||
|
||||
func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
||||
baRouter := Router.Group("containers").
|
||||
Use(middleware.JwtAuth()).
|
||||
Use(middleware.SessionAuth()).
|
||||
Use(middleware.PasswordExpired())
|
||||
withRecordRouter := Router.Group("containers").
|
||||
Use(middleware.JwtAuth()).
|
||||
Use(middleware.SessionAuth()).
|
||||
Use(middleware.PasswordExpired()).
|
||||
Use(middleware.OperationRecord())
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
baRouter.POST("/search", baseApi.SearchContainer)
|
||||
baRouter.GET("/detail/:id", baseApi.ContainerDetail)
|
||||
withRecordRouter.POST("operate", baseApi.ContainerOperation)
|
||||
withRecordRouter.POST("/log", baseApi.ContainerLogs)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ type RouterGroup struct {
|
||||
HostRouter
|
||||
BackupRouter
|
||||
GroupRouter
|
||||
ContainerRouter
|
||||
CommandRouter
|
||||
MonitorRouter
|
||||
OperationLogRouter
|
||||
|
@ -2,6 +2,7 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
@ -22,6 +23,14 @@ func NewClient() (Client, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewDockerClient() (*client.Client, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func (c Client) ListAllContainers() ([]types.Container, error) {
|
||||
var options types.ContainerListOptions
|
||||
containers, err := c.cli.ContainerList(context.Background(), options)
|
||||
|
5357
frontend/package-lock.json
generated
5357
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,8 @@
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.0",
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"@kangc/v-md-editor": "^2.3.15",
|
||||
"@vueuse/core": "^8.0.1",
|
||||
@ -40,6 +42,7 @@
|
||||
"unplugin-vue-define-options": "^0.7.3",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vue": "^3.2.25",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue3-seamless-scroll": "^1.2.0",
|
||||
|
24
frontend/src/api/interface/container.ts
Normal file
24
frontend/src/api/interface/container.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ReqPage } from '.';
|
||||
|
||||
export namespace Container {
|
||||
export interface ContainerOperate {
|
||||
containerID: string;
|
||||
operation: string;
|
||||
newName: string;
|
||||
}
|
||||
export interface ContainerSearch extends ReqPage {
|
||||
status: string;
|
||||
}
|
||||
export interface ContainerInfo {
|
||||
containerID: string;
|
||||
name: string;
|
||||
imageName: string;
|
||||
createTime: string;
|
||||
state: string;
|
||||
runTime: string;
|
||||
}
|
||||
export interface ContainerLogSearch {
|
||||
containerID: string;
|
||||
mode: string;
|
||||
}
|
||||
}
|
19
frontend/src/api/modules/container.ts
Normal file
19
frontend/src/api/modules/container.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import http from '@/api';
|
||||
import { ResPage } from '../interface';
|
||||
import { Container } from '../interface/container';
|
||||
|
||||
export const getContainerPage = (params: Container.ContainerSearch) => {
|
||||
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
|
||||
};
|
||||
|
||||
export const getContainerLog = (params: Container.ContainerLogSearch) => {
|
||||
return http.post<string>(`/containers/log`, params);
|
||||
};
|
||||
|
||||
export const ContainerOperator = (params: Container.ContainerOperate) => {
|
||||
return http.post(`/containers/operate`, params);
|
||||
};
|
||||
|
||||
export const getContainerInspect = (containerID: string) => {
|
||||
return http.get<string>(`/containers/detail/${containerID}`);
|
||||
};
|
@ -18,6 +18,7 @@ export default {
|
||||
login: 'Login',
|
||||
close: 'Close',
|
||||
view: 'View',
|
||||
watch: 'Watch',
|
||||
handle: 'Handle',
|
||||
expand: 'Expand',
|
||||
log: 'Log',
|
||||
@ -147,6 +148,28 @@ export default {
|
||||
header: {
|
||||
logout: 'Logout',
|
||||
},
|
||||
container: {
|
||||
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
reStart: 'ReStart',
|
||||
kill: 'Kill',
|
||||
pause: 'Pause',
|
||||
unPause: 'UnPause',
|
||||
reName: 'ReName',
|
||||
remove: 'Remove',
|
||||
container: 'Container',
|
||||
image: 'Image',
|
||||
network: 'Network',
|
||||
storage: 'Storage',
|
||||
schedule: 'Schedule',
|
||||
upTime: 'UpTime',
|
||||
all: 'All',
|
||||
lastDay: 'Last Day',
|
||||
last4Hour: 'Last 4 Hours',
|
||||
lastHour: 'Last Hour',
|
||||
last10Min: 'Last 10 Minutes',
|
||||
},
|
||||
cronjob: {
|
||||
cronTask: 'Task',
|
||||
taskType: 'Task type',
|
||||
|
@ -18,6 +18,7 @@ export default {
|
||||
login: '登录',
|
||||
close: '关闭',
|
||||
view: '详情',
|
||||
watch: '追踪',
|
||||
handle: '执行',
|
||||
expand: '展开',
|
||||
log: '日志',
|
||||
@ -144,6 +145,28 @@ export default {
|
||||
header: {
|
||||
logout: '退出登录',
|
||||
},
|
||||
container: {
|
||||
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
||||
start: '启动',
|
||||
stop: '停止',
|
||||
reStart: '重启',
|
||||
kill: '强制停止',
|
||||
pause: '暂停',
|
||||
unPause: '恢复',
|
||||
reName: '重命名',
|
||||
remove: '移除',
|
||||
container: '容器',
|
||||
image: '镜像',
|
||||
network: '网络',
|
||||
storage: '数据卷',
|
||||
schedule: '编排',
|
||||
upTime: '运行时长',
|
||||
all: '全部',
|
||||
lastDay: '最近一天',
|
||||
last4Hour: '最近 4 小时',
|
||||
lastHour: '最近 1 小时',
|
||||
last10Min: '最近 10 分钟',
|
||||
},
|
||||
cronjob: {
|
||||
cronTask: '计划任务',
|
||||
taskType: '任务类型',
|
||||
|
361
frontend/src/views/container/container/index.vue
Normal file
361
frontend/src/views/container/container/index.vue
Normal file
@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<el-card style="margin-top: 20px">
|
||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||
<template #toolbar>
|
||||
<el-button-group>
|
||||
<el-button :disabled="checkStatus('start')" @click="onOperate('start')">
|
||||
{{ $t('container.start') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
|
||||
{{ $t('container.stop') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('reStart')" @click="onOperate('reStart')">
|
||||
{{ $t('container.reStart') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
|
||||
{{ $t('container.kill') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
|
||||
{{ $t('container.pause') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('unPause')" @click="onOperate('unPause')">
|
||||
{{ $t('container.unPause') }}
|
||||
</el-button>
|
||||
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
|
||||
{{ $t('container.remove') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button icon="Plus" style="margin-left: 10px" @click="onCreate()">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<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('container.image')" show-overflow-tooltip min-width="100" prop="imageName" />
|
||||
<el-table-column :label="$t('commons.table.status')" min-width="50" prop="state" fix />
|
||||
<el-table-column :label="$t('container.upTime')" min-width="100" prop="runTime" fix />
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFromat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||
</ComplexTable>
|
||||
<el-dialog v-model="detailVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="70%">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('commons.button.views') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
placeholder="None data"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="max-height: 500px"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="detailInfo"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
@close="onCloseLog"
|
||||
v-model="logVisiable"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="70%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('commons.button.log') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-select @change="searchLogs" style="width: 10%; float: left" v-model="logSearch.mode">
|
||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
||||
</el-select>
|
||||
<div style="margin-left: 20px; float: left">
|
||||
<el-checkbox border v-model="logSearch.isWatch">{{ $t('commons.button.watch') }}</el-checkbox>
|
||||
</div>
|
||||
<el-button style="margin-left: 20px" @click="onDownload" icon="Download">
|
||||
{{ $t('file.download') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
placeholder="None data"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="margin-top: 10px; max-height: 500px"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="logInfo"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="logVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
@close="onCloseLog"
|
||||
v-model="newNameVisiable"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="30%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('container.reName') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="newNameRef" :model="reNameForm">
|
||||
<el-form-item label="新名称" :rules="Rules.requiredInput" prop="newName">
|
||||
<el-input v-model="reNameForm.newName"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="newNameVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button @click="onSubmitName(newNameRef)">{{ $t('commons.button.confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { reactive, onMounted, ref } from 'vue';
|
||||
import { dateFromat, dateFromatForName } from '@/utils/util';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { ContainerOperator, getContainerInspect, getContainerLog, getContainerPage } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { ElForm, ElMessage, ElMessageBox, FormInstance } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const data = ref();
|
||||
const selects = ref<any>([]);
|
||||
const paginationConfig = reactive({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
total: 0,
|
||||
});
|
||||
const containerSearch = reactive({
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
status: 'all',
|
||||
});
|
||||
|
||||
const detailVisiable = ref<boolean>(false);
|
||||
const detailInfo = ref();
|
||||
const extensions = [javascript(), oneDark];
|
||||
const logVisiable = ref<boolean>(false);
|
||||
const logInfo = ref();
|
||||
const logSearch = reactive({
|
||||
isWatch: false,
|
||||
container: '',
|
||||
containerID: '',
|
||||
mode: 'all',
|
||||
});
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const newNameVisiable = ref<boolean>(false);
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const newNameRef = ref<FormInstance>();
|
||||
const reNameForm = reactive({
|
||||
containerID: '',
|
||||
operation: 'reName',
|
||||
newName: '',
|
||||
});
|
||||
|
||||
const timeOptions = ref([
|
||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
||||
{
|
||||
label: i18n.global.t('container.lastDay'),
|
||||
value: new Date(new Date().getTime() - 3600 * 1000 * 24 * 1).getTime() / 1000 + '',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.last4Hour'),
|
||||
value: new Date(new Date().getTime() - 3600 * 1000 * 4).getTime() / 1000 + '',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.lastHour'),
|
||||
value: new Date(new Date().getTime() - 3600 * 1000).getTime() / 1000 + '',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.last10Min'),
|
||||
value: new Date(new Date().getTime() - 600 * 1000).getTime() / 1000 + '',
|
||||
},
|
||||
]);
|
||||
|
||||
const search = async () => {
|
||||
containerSearch.page = paginationConfig.page;
|
||||
containerSearch.pageSize = paginationConfig.pageSize;
|
||||
await getContainerPage(containerSearch).then((res) => {
|
||||
if (res.data) {
|
||||
data.value = res.data.items;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCreate = async () => {};
|
||||
const onDetail = async (row: Container.ContainerInfo) => {
|
||||
const res = await getContainerInspect(row.containerID);
|
||||
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
|
||||
detailVisiable.value = true;
|
||||
};
|
||||
const onLog = async (row: Container.ContainerInfo) => {
|
||||
logSearch.container = row.name;
|
||||
logSearch.containerID = row.containerID;
|
||||
searchLogs();
|
||||
logVisiable.value = true;
|
||||
timer = setInterval(() => {
|
||||
if (logVisiable.value && logSearch.isWatch) {
|
||||
searchLogs();
|
||||
}
|
||||
}, 1000 * 5);
|
||||
};
|
||||
const onCloseLog = async () => {
|
||||
clearInterval(Number(timer));
|
||||
};
|
||||
const searchLogs = async () => {
|
||||
const res = await getContainerLog(logSearch);
|
||||
logInfo.value = res.data;
|
||||
};
|
||||
const onDownload = async () => {
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([logInfo.value]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
a.download = logSearch.container + '-' + dateFromatForName(new Date()) + '.log';
|
||||
const event = new MouseEvent('click');
|
||||
a.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onRename = async (row: Container.ContainerInfo) => {
|
||||
reNameForm.containerID = row.containerID;
|
||||
reNameForm.newName = '';
|
||||
newNameVisiable.value = true;
|
||||
};
|
||||
const onSubmitName = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
ContainerOperator(reNameForm);
|
||||
search();
|
||||
newNameVisiable.value = false;
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const checkStatus = (operation: string) => {
|
||||
if (selects.value.length < 1) {
|
||||
return true;
|
||||
}
|
||||
switch (operation) {
|
||||
case 'start':
|
||||
for (const item of selects.value) {
|
||||
if (item.state === 'running') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'stop':
|
||||
for (const item of selects.value) {
|
||||
if (item.state === 'stopped') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'pause':
|
||||
for (const item of selects.value) {
|
||||
if (item.state === 'paused') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'unPause':
|
||||
for (const item of selects.value) {
|
||||
if (item.state !== 'paused') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const onOperate = async (operation: string) => {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('container.operatorHelper', [operation]),
|
||||
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 = {
|
||||
containerID: item.containerID,
|
||||
operation: operation,
|
||||
newName: '',
|
||||
};
|
||||
ps.push(ContainerOperator(param));
|
||||
}
|
||||
Promise.all(ps)
|
||||
.then(() => {
|
||||
search();
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
search();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('container.reName'),
|
||||
click: (row: Container.ContainerInfo) => {
|
||||
onRename(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.log'),
|
||||
click: (row: Container.ContainerInfo) => {
|
||||
onLog(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.view'),
|
||||
click: (row: Container.ContainerInfo) => {
|
||||
onDetail(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
0
frontend/src/views/container/image/index.vue
Normal file
0
frontend/src/views/container/image/index.vue
Normal file
@ -1,7 +1,71 @@
|
||||
<template>
|
||||
<LayoutContent></LayoutContent>
|
||||
<div>
|
||||
<el-card class="topCard">
|
||||
<el-radio-group v-model="activeNames">
|
||||
<el-radio-button class="topButton" size="large" label="container">
|
||||
{{ $t('container.container') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button class="topButton" size="large" label="image">
|
||||
{{ $t('container.image') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button class="topButton" size="large" label="network">
|
||||
{{ $t('container.network') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button class="topButton" size="large" label="storage">
|
||||
{{ $t('container.storage') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button class="topButton" size="large" label="schedule">
|
||||
{{ $t('container.schedule') }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-card>
|
||||
<Container v-if="activeNames === 'container'" />
|
||||
<Safe v-if="activeNames === 'image'" />
|
||||
<Backup v-if="activeNames === 'network'" />
|
||||
<Monitor v-if="activeNames === 'storage'" />
|
||||
<About v-if="activeNames === 'schedule'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { ref } from 'vue';
|
||||
import Container from '@/views/container/container/index.vue';
|
||||
import Safe from '@/views/setting/tabs/safe.vue';
|
||||
import Backup from '@/views/setting/tabs/backup.vue';
|
||||
import Monitor from '@/views/setting/tabs/monitor.vue';
|
||||
import About from '@/views/setting/tabs/about.vue';
|
||||
|
||||
const activeNames = ref('container');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.topCard {
|
||||
--el-card-border-color: var(--el-border-color-light);
|
||||
--el-card-border-radius: 4px;
|
||||
--el-card-padding: 0px;
|
||||
--el-card-bg-color: var(--el-fill-color-blank);
|
||||
}
|
||||
.topButton .el-radio-button__inner {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
background: var(--el-button-bg-color, var(--el-fill-color-blank));
|
||||
border: 0;
|
||||
font-weight: 350;
|
||||
border-left: 0;
|
||||
color: var(--el-button-text-color, var(--el-text-color-regular));
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: var(--el-transition-all);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
padding: 8px 15px;
|
||||
font-size: var(--el-font-size-base);
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -24,7 +24,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP" prop="ip" />
|
||||
<el-table-column align="left" :label="$t('operations.request')" prop="path">
|
||||
<el-table-column :label="$t('operations.request')" prop="path">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-popover :width="500" v-if="row.body" placement="left-start" trigger="click">
|
||||
@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" :label="$t('operations.response')" prop="path">
|
||||
<el-table-column :label="$t('operations.response')" prop="path">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-popover :width="500" v-if="row.resp" placement="left-start" trigger="click">
|
||||
|
@ -184,7 +184,7 @@ const form = withDefaults(defineProps<Props>(), {
|
||||
settingInfo: {
|
||||
serverPort: '',
|
||||
securityEntrance: '',
|
||||
ExpirationTime: '',
|
||||
expirationTime: '',
|
||||
complexityVerification: '',
|
||||
mfaStatus: '',
|
||||
mfaSecret: '',
|
||||
@ -237,14 +237,14 @@ const submitTimeout = async (formEl: FormInstance | undefined) => {
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
let time = new Date(new Date().getTime() + 3600 * 1000 * 24 * timeoutForm.days);
|
||||
await updateSetting({ key: 'ExpirationTime', value: dateFromat(0, 0, time) });
|
||||
form.settingInfo.ExpirationTime = dateFromat(0, 0, time);
|
||||
await updateSetting({ key: 'expirationTime', value: dateFromat(0, 0, time) });
|
||||
form.settingInfo.expirationTime = dateFromat(0, 0, time);
|
||||
timeoutVisiable.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
function loadTimeOut() {
|
||||
let staytimeGap = new Date(form.settingInfo.ExpirationTime).getTime() - new Date().getTime();
|
||||
let staytimeGap = new Date(form.settingInfo.expirationTime).getTime() - new Date().getTime();
|
||||
return Math.floor(staytimeGap / (3600 * 1000 * 24));
|
||||
}
|
||||
</script>
|
||||
|
1
go.mod
1
go.mod
@ -143,4 +143,5 @@ require (
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gotest.tools/v3 v3.3.0 // indirect
|
||||
)
|
||||
|
1
go.sum
1
go.sum
@ -883,6 +883,7 @@ gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
|
||||
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
76
package-lock.json
generated
Normal file
76
package-lock.json
generated
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@codemirror/commands": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.1.tgz",
|
||||
"integrity": "sha512-ibDohwkk7vyu3VsnZNlQhwk0OETBtlkYV+6AHfn5Zgq0sxa+yGVX+apwtC3M4wh6AH7yU5si/NysoECs5EGS3Q==",
|
||||
"requires": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@codemirror/language": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz",
|
||||
"integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==",
|
||||
"requires": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@codemirror/state": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.2.tgz",
|
||||
"integrity": "sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA=="
|
||||
},
|
||||
"@codemirror/view": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.3.0.tgz",
|
||||
"integrity": "sha512-jMN9OGKmzRPJ+kksfMrB5e/A9heQncirHsz8XNBpgEbYONCk5tWHMKVWKTNwznkUGD5mnigXI1i5YIcWpscSPg==",
|
||||
"requires": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"style-mod": "^4.0.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"@lezer/common": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz",
|
||||
"integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw=="
|
||||
},
|
||||
"@lezer/highlight": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.1.tgz",
|
||||
"integrity": "sha512-duv9D23O9ghEDnnUDmxu+L8pJy4nYo4AbCOHIudUhscrLSazqeJeK1V50EU6ZufWF1zv0KJwu/frFRyZWXxHBQ==",
|
||||
"requires": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@lezer/lr": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.3.tgz",
|
||||
"integrity": "sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==",
|
||||
"requires": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"style-mod": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
|
||||
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
||||
},
|
||||
"w3c-keyname": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
|
||||
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg=="
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user