feat: 实现容器启停、列表、日志等功能

This commit is contained in:
ssongliu 2022-10-08 18:32:02 +08:00 committed by ssongliu
parent 20a57cc5ab
commit a79ba71ef4
24 changed files with 3688 additions and 2582 deletions

View 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)
}

View File

@ -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

View 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"`
}

View 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
}

View File

@ -7,6 +7,7 @@ type ServiceGroup struct {
HostService
BackupService
GroupService
ContainerService
CommandService
OperationService
FileService

View File

@ -0,0 +1,12 @@
package constant
const (
ContainerOpStart = "start"
ContainerOpStop = "stop"
ContainerOpRestart = "reStart"
ContainerOpKill = "kill"
ContainerOpPause = "pause"
ContainerOpUnpause = "unPause"
ContainerOpRename = "reName"
ContainerOpRemove = "remove"
)

View File

@ -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)

View 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)
}
}

View File

@ -5,6 +5,7 @@ type RouterGroup struct {
HostRouter
BackupRouter
GroupRouter
ContainerRouter
CommandRouter
MonitorRouter
OperationLogRouter

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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;
}
}

View 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}`);
};

View File

@ -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',

View File

@ -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: '任务类型',

View 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>

View 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>

View File

@ -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">

View File

@ -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
View File

@ -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
View File

@ -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
View 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=="
}
}
}