feat: 增加编排详情界面

This commit is contained in:
ssongliu 2022-12-06 15:06:42 +08:00 committed by ssongliu
parent 415200492a
commit cecc108784
10 changed files with 584 additions and 234 deletions

View File

@ -9,3 +9,9 @@ type ComposeTemplate struct {
Path string `gorm:"type:varchar(64)" json:"path"`
Content string `gorm:"type:longtext" json:"content"`
}
type Compose struct {
BaseModel
Name string `gorm:"type:varchar(256)" json:"name"`
}

View File

@ -14,6 +14,9 @@ type IComposeTemplateRepo interface {
Create(compose *model.ComposeTemplate) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
CreateRecord(compose *model.Compose) error
DeleteRecord(opts ...DBOption) error
}
func NewIComposeTemplateRepo() IComposeTemplateRepo {
@ -67,3 +70,23 @@ func (u *ComposeTemplateRepo) Delete(opts ...DBOption) error {
}
return db.Delete(&model.ComposeTemplate{}).Error
}
func (u *ComposeTemplateRepo) ListRecord() ([]model.Compose, error) {
var composes []model.Compose
if err := global.DB.Find(&composes).Error; err != nil {
return nil, err
}
return composes, nil
}
func (u *ComposeTemplateRepo) CreateRecord(compose *model.Compose) error {
return global.DB.Create(compose).Error
}
func (u *ComposeTemplateRepo) DeleteRecord(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Compose{}).Error
}

View File

@ -157,7 +157,7 @@ var AddTableApp = &gormigrate.Migration{
var AddTableImageRepo = &gormigrate.Migration{
ID: "20201009-add-table-imagerepo",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ImageRepo{}, &model.ComposeTemplate{}); err != nil {
if err := tx.AutoMigrate(&model.ImageRepo{}, &model.ComposeTemplate{}, &model.Compose{}); err != nil {
return err
}
item := &model.ImageRepo{

View File

@ -11,7 +11,7 @@ const containerRouter = {
},
children: [
{
path: ':filters?',
path: '',
name: 'Container',
component: () => import('@/views/container/container/index.vue'),
props: true,
@ -20,6 +20,16 @@ const containerRouter = {
activeMenu: '/containers',
},
},
{
path: 'composeDetail/:filters?',
name: 'ComposeDetail',
component: () => import('@/views/container/compose/detail/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
},
},
{
path: 'image',
name: 'Image',

View File

@ -0,0 +1,271 @@
<template>
<div v-loading="loading">
<Submenu activeName="compose" />
<el-card style="margin-top: 20px">
<LayoutContent :header="'Compose-' + composeName" back-name="Compose" :reload="true">
<div>
<el-button icon="VideoPause">停止</el-button>
<el-button icon="Delete" plain type="danger">删除</el-button>
</div>
<el-card style="margin-top: 20px">
<template #header>
<div class="card-header">
<span>Containers</span>
</div>
</template>
<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>
</template>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="100"
prop="name"
fix
>
<template #default="{ row }">
<el-link @click="onInspect(row.containerID)" type="primary">{{ row.name }}</el-link>
</template>
</el-table-column>
<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
width="200px"
:ellipsis="10"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
<CodemirrorDialog ref="mydetail" />
<ReNameDialog @search="search" ref="dialogReNameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" />
<CreateDialog @search="search" ref="dialogCreateRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
</el-card>
</LayoutContent>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import Submenu from '@/views/container/index.vue';
import LayoutContent from '@/layout/layout-content.vue';
import ReNameDialog from '@/views/container/container/rename/index.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
import ContainerLogDialog from '@/views/container/container/log/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import CodemirrorDialog from '@/components/codemirror-dialog/codemirror.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util';
import { ContainerOperator, inspect, searchContainer } from '@/api/modules/container';
import { ElMessage, ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
interface Filters {
filters?: string;
}
const props = withDefaults(defineProps<Filters>(), {
filters: '',
});
const composeName = ref();
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
page: 1,
pageSize: 10,
total: 0,
});
const loading = ref(false);
const search = async () => {
let filterItem = props.filters ? props.filters : '';
composeName.value = props.filters.replaceAll('com.docker.compose.project=', '');
let params = {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
filters: filterItem,
};
await searchContainer(params).then((res) => {
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
});
};
const detailInfo = ref();
const mydetail = ref();
const onInspect = async (id: string) => {
const res = await inspect({ id: id, type: 'container' });
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
let param = {
header: i18n.global.t('commons.button.view'),
detailInfo: detailInfo.value,
};
mydetail.value!.acceptParams(param);
};
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' || item.state === 'exited') {
return true;
}
}
return false;
case 'pause':
for (const item of selects.value) {
if (item.state === 'paused' || item.state === 'exited') {
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', [i18n.global.t('container.' + operation)]),
i18n.global.t('container.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(() => {
let 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 dialogMonitorRef = ref();
const onMonitor = (containerID: string) => {
dialogMonitorRef.value!.acceptParams({ containerID: containerID });
};
const dialogTerminalRef = ref();
const onTerminal = (containerID: string) => {
dialogTerminalRef.value!.acceptParams({ containerID: containerID });
};
const dialogContainerLogRef = ref();
const dialogReNameRef = ref();
const buttons = [
{
label: i18n.global.t('file.terminal'),
disabled: (row: Container.ContainerInfo) => {
return row.state !== 'running';
},
click: (row: Container.ContainerInfo) => {
onTerminal(row.containerID);
},
},
{
label: i18n.global.t('container.monitor'),
disabled: (row: Container.ContainerInfo) => {
return row.state !== 'running';
},
click: (row: Container.ContainerInfo) => {
onMonitor(row.containerID);
},
},
{
label: i18n.global.t('container.rename'),
click: (row: Container.ContainerInfo) => {
dialogReNameRef.value!.acceptParams({ containerID: row.containerID, container: row.name });
},
},
{
label: i18n.global.t('commons.button.log'),
click: (row: Container.ContainerInfo) => {
dialogContainerLogRef.value!.acceptParams({ containerID: row.containerID });
},
},
];
onMounted(() => {
search();
});
</script>

View File

@ -7,28 +7,7 @@
<el-button icon="Plus" type="primary" @click="onOpenDialog()">
{{ $t('commons.button.create') }}
</el-button>
<el-button-group style="margin-left: 10px">
<el-button :disabled="selects.length === 0" @click="onOperate('up')">
{{ $t('container.start') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onOperate('stop')">
{{ $t('container.stop') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onOperate('pause')">
{{ $t('container.pause') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onOperate('unpause')">
{{ $t('container.unpause') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onOperate('restart')">
{{ $t('container.restart') }}
</el-button>
<el-button :disabled="selects.length === 0" @click="onOperate('down')">
{{ $t('container.down') }}
</el-button>
</el-button-group>
</template>
<el-table-column type="selection" fix></el-table-column>
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
@ -42,38 +21,35 @@
</el-table-column>
<el-table-column :label="$t('container.from')" prop="createdBy" min-width="80" fix />
<el-table-column :label="$t('container.containerNumber')" prop="containerNumber" min-width="80" fix />
<el-table-column :label="$t('container.container')" prop="contaienrs" min-width="80" fix>
<template #default="{ row }">
<div v-for="(item, index) in row.containers" :key="index">
<div v-if="row.expand || (!row.expand && index < 3)">
<el-tag>{{ item.name }} [{{ item.state }}]</el-tag>
</div>
</div>
<div v-if="!row.expand && row.containers.length > 3">
<el-button type="primary" link @click="row.expand = true">
{{ $t('commons.button.expand') }}...
</el-button>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.createdAt')" prop="createdAt" min-width="80" fix />
<fu-table-operations
width="200px"
:ellipsis="10"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</el-card>
<OperatorDialog @search="search" ref="dialogRef" />
<EditDialog ref="dialogEditRef" />
<CreateDialog @search="search" ref="dialogRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import { reactive, onMounted, ref } from 'vue';
import OperatorDialog from '@/views/container/compose/operator/index.vue';
import CreateDialog from '@/views/container/compose/create/index.vue';
import EditDialog from '@/views/container/compose/edit/index.vue';
import Submenu from '@/views/container/index.vue';
import { ComposeOperator, searchCompose } from '@/api/modules/container';
import { composeOperator, searchCompose } from '@/api/modules/container';
import i18n from '@/lang';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessage } from 'element-plus';
import router from '@/routers';
import { Container } from '@/api/interface/container';
import { useDeleteData } from '@/hooks/use-delete-data';
import { LoadFile } from '@/api/modules/files';
const data = ref();
const selects = ref<any>([]);
@ -103,7 +79,7 @@ const search = async () => {
};
const goContainer = async (name: string) => {
router.push({ name: 'Container', params: { filters: 'com.docker.compose.project=' + name } });
router.push({ name: 'ComposeDetail', params: { filters: 'com.docker.compose.project=' + name } });
};
const dialogRef = ref();
@ -111,35 +87,41 @@ const onOpenDialog = async () => {
dialogRef.value!.acceptParams();
};
const onOperate = async (operation: string) => {
ElMessageBox.confirm(
i18n.global.t('container.operatorComposeHelper', [i18n.global.t('container.' + operation)]),
i18n.global.t('container.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(() => {
let ps = [];
for (const item of selects.value) {
const param = {
path: item.path,
operation: operation,
};
ps.push(ComposeOperator(param));
}
Promise.all(ps)
.then(() => {
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
search();
});
});
const onDelete = async (row: Container.ComposeInfo) => {
const param = {
name: row.name,
path: row.path,
operation: 'down',
};
await useDeleteData(composeOperator, param, 'commons.msg.delete');
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
};
const dialogEditRef = ref();
const onEdit = async (row: Container.ComposeInfo) => {
const res = await LoadFile({ path: row.path });
let params = {
path: row.path,
content: res.data,
};
dialogEditRef.value!.acceptParams(params);
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: (row: Container.ComposeInfo) => {
onEdit(row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: Container.ComposeInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
});

View File

@ -69,75 +69,8 @@
<CodemirrorDialog ref="mydetail" />
<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="search()"
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="$t('container.newName')" :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>
<ReNameDialog @search="search" ref="dialogReNameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" />
<CreateDialog @search="search" ref="dialogCreateRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
@ -146,20 +79,18 @@
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import ReNameDialog from '@/views/container/container/rename/index.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
import ContainerLogDialog from '@/views/container/container/log/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import CodemirrorDialog from '@/components/codemirror-dialog/codemirror.vue';
import Submenu from '@/views/container/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, inspect, logContainer, searchContainer } from '@/api/modules/container';
import { dateFromat } from '@/utils/util';
import { ContainerOperator, inspect, searchContainer } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import { ElForm, ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import i18n from '@/lang';
const data = ref();
@ -180,45 +111,8 @@ const props = withDefaults(defineProps<Filters>(), {
const detailInfo = ref();
const mydetail = 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 dialogContainerLogRef = ref();
const dialogReNameRef = ref();
const search = async () => {
let filterItem = props.filters ? props.filters : '';
@ -228,9 +122,8 @@ const search = async () => {
filters: filterItem,
};
await searchContainer(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
});
};
@ -259,50 +152,6 @@ const onInspect = async (id: string) => {
mydetail.value!.acceptParams(param);
};
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 logContainer(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;
await 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;
@ -390,13 +239,13 @@ const buttons = [
{
label: i18n.global.t('container.rename'),
click: (row: Container.ContainerInfo) => {
onRename(row);
dialogReNameRef.value!.acceptParams({ containerID: row.containerID, container: row.name });
},
},
{
label: i18n.global.t('commons.button.log'),
click: (row: Container.ContainerInfo) => {
onLog(row);
dialogContainerLogRef.value!.acceptParams({ containerID: row.containerID });
},
},
];

View File

@ -0,0 +1,131 @@
<template>
<el-dialog
v-model="logVisiable"
@close="onCloseLog"
: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>
</template>
<script lang="ts" setup>
import { logContainer } from '@/api/modules/container';
import i18n from '@/lang';
import { dateFromatForName } from '@/utils/util';
import { reactive, ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
const extensions = [javascript(), oneDark];
const logVisiable = ref(false);
const logInfo = ref();
const logSearch = reactive({
isWatch: false,
container: '',
containerID: '',
mode: 'all',
});
let timer: NodeJS.Timer | null = null;
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 onCloseLog = async () => {
clearInterval(Number(timer));
};
const searchLogs = async () => {
const res = await logContainer(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);
};
interface DialogProps {
container: string;
containerID: string;
}
const acceptParams = (props: DialogProps): void => {
logVisiable.value = true;
logSearch.containerID = props.containerID;
logSearch.mode = 'all';
logSearch.isWatch = false;
logSearch.container = props.container;
searchLogs();
timer = setInterval(() => {
if (logSearch.isWatch) {
searchLogs();
}
}, 1000 * 5);
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,78 @@
<template>
<el-dialog
@close="onClose()"
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="$t('container.newName')" :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 type="primary" @click="onSubmitName(newNameRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ContainerOperator } from '@/api/modules/container';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
const renameForm = reactive({
containerID: '',
operation: 'rename',
newName: '',
});
const newNameRef = ref<FormInstance>();
const newNameVisiable = ref<boolean>(false);
type FormInstance = InstanceType<typeof ElForm>;
const emit = defineEmits<{ (e: 'search'): void }>();
const onSubmitName = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
await ContainerOperator(renameForm);
emit('search');
newNameVisiable.value = false;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
});
};
interface DialogProps {
containerID: string;
}
const acceptParams = (props: DialogProps): void => {
renameForm.containerID = props.containerID;
renameForm.newName = '';
newNameVisiable.value = true;
};
const onClose = async () => {
emit('search');
};
defineExpose({
acceptParams,
});
</script>