mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-18 05:19:19 +08:00
feat: Enhance log container and compose management with new UI components and improved functionality
This commit is contained in:
parent
7bd9b0c9a9
commit
912a9ad6b4
3 changed files with 564 additions and 750 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="showControl">
|
||||||
<el-select @change="searchLogs" class="fetchClass" v-model="logSearch.mode">
|
<el-select @change="searchLogs" class="fetchClass" v-model="logSearch.mode">
|
||||||
<template #prefix>{{ $t('container.fetch') }}</template>
|
<template #prefix>{{ $t('container.fetch') }}</template>
|
||||||
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
<el-option v-for="item in timeOptions" :key="item.label" :value="item.value" :label="item.label" />
|
||||||
|
|
@ -69,6 +69,14 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
showControl: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
defaultFollow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const styleVars = computed(() => ({
|
const styleVars = computed(() => ({
|
||||||
|
|
@ -80,10 +88,10 @@ const logContainer = ref<HTMLElement | null>(null);
|
||||||
const logs = ref<string[]>([]);
|
const logs = ref<string[]>([]);
|
||||||
let eventSource: EventSource | null = null;
|
let eventSource: EventSource | null = null;
|
||||||
const logSearch = reactive({
|
const logSearch = reactive({
|
||||||
isWatch: true,
|
isWatch: props.defaultFollow ? true : true,
|
||||||
container: '',
|
container: '',
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
tail: 100,
|
tail: props.defaultFollow ? 0 : 100,
|
||||||
compose: '',
|
compose: '',
|
||||||
});
|
});
|
||||||
const logHeight = 20;
|
const logHeight = 20;
|
||||||
|
|
|
||||||
|
|
@ -1,564 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-loading="pageLoading">
|
|
||||||
<el-card v-if="isExist && !isActive" class="mask-prompt">
|
|
||||||
<span>{{ $t('container.serviceUnavailable') }}</span>
|
|
||||||
<el-button type="primary" link class="bt" @click="goSetting">【 {{ $t('container.setting') }} 】</el-button>
|
|
||||||
<span>{{ $t('container.startIn') }}</span>
|
|
||||||
</el-card>
|
|
||||||
<NoSuchService v-if="!isExist" name="Docker" />
|
|
||||||
<LayoutContent v-if="isExist" backName="Compose" :title="composeTitle" :class="{ mask: !isActive }">
|
|
||||||
<template #main>
|
|
||||||
<el-empty v-if="!composeName" :description="$t('commons.msg.noneData')" />
|
|
||||||
<div v-else class="w-full">
|
|
||||||
<div class="app-status mb-4">
|
|
||||||
<el-card>
|
|
||||||
<div class="flex w-full flex-col gap-4 md:flex-row">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
plain
|
|
||||||
:loading="operateLoading && currentOperation === 'up'"
|
|
||||||
:disabled="disableOperate"
|
|
||||||
@click="handleComposeOperate('up')"
|
|
||||||
>
|
|
||||||
{{ $t('commons.operate.start') }}
|
|
||||||
</el-button>
|
|
||||||
<el-divider direction="vertical" />
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
:loading="operateLoading && currentOperation === 'stop'"
|
|
||||||
:disabled="disableOperate"
|
|
||||||
@click="handleComposeOperate('stop')"
|
|
||||||
>
|
|
||||||
{{ $t('commons.operate.stop') }}
|
|
||||||
</el-button>
|
|
||||||
<el-divider direction="vertical" />
|
|
||||||
<el-button
|
|
||||||
type="warning"
|
|
||||||
plain
|
|
||||||
:loading="operateLoading && currentOperation === 'restart'"
|
|
||||||
:disabled="disableOperate"
|
|
||||||
@click="handleComposeOperate('restart')"
|
|
||||||
>
|
|
||||||
{{ $t('commons.operate.restart') }}
|
|
||||||
</el-button>
|
|
||||||
<el-divider direction="vertical" />
|
|
||||||
<el-button type="primary" link :loading="detailLoading" @click="refreshDetail">
|
|
||||||
{{ $t('commons.button.refresh') }}
|
|
||||||
</el-button>
|
|
||||||
<el-divider direction="vertical" />
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
:disabled="!composeInfo?.workdir"
|
|
||||||
@click="openComposeFolder"
|
|
||||||
>
|
|
||||||
{{ $t('container.composeDirectory') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-alert
|
|
||||||
v-if="showOperateHelper"
|
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
:title="$t('container.composeDetailHelper')"
|
|
||||||
class="mt-3"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-card v-if="composeInfo && composeContainers.length > 0" class="mb-4" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<span class="text-sm font-medium">
|
|
||||||
{{ $t('container.containerStatus') }}
|
|
||||||
( {{ composeInfo?.containerCount || 0 }} / {{ composeInfo?.runningCount || 0 }} )
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<ComplexTable :data="tableData" :heightDiff="400">
|
|
||||||
<el-table-column
|
|
||||||
:label="$t('commons.table.name')"
|
|
||||||
min-width="150"
|
|
||||||
prop="name"
|
|
||||||
show-overflow-tooltip
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-text type="primary" class="cursor-pointer" @click="onInspectContainer(row)">
|
|
||||||
{{ row.name }}
|
|
||||||
</el-text>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="state">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<Status :key="row.state" :status="row.state"></Status>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
:label="$t('container.source')"
|
|
||||||
show-overflow-tooltip
|
|
||||||
prop="resource"
|
|
||||||
min-width="120"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="row.hasLoad" class="flex items-center">
|
|
||||||
<div class="text-xs">
|
|
||||||
<div>CPU: {{ row.cpuPercent.toFixed(2) }}%</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
{{ $t('monitor.memory') }}: {{ row.memoryPercent.toFixed(2) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-popover placement="right" width="500px" trigger="hover">
|
|
||||||
<template #reference>
|
|
||||||
<el-icon
|
|
||||||
class="cursor-pointer text-gray-500 hover:text-primary ml-1"
|
|
||||||
:size="16"
|
|
||||||
>
|
|
||||||
<Histogram />
|
|
||||||
</el-icon>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<el-descriptions direction="vertical" border :column="3" size="small">
|
|
||||||
<el-descriptions-item :label="$t('container.cpuUsage')">
|
|
||||||
{{ computeCPU(row.cpuTotalUsage) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('container.cpuTotal')">
|
|
||||||
{{ computeCPU(row.systemUsage) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('container.core')">
|
|
||||||
{{ row.percpuUsage }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :label="$t('container.memUsage')">
|
|
||||||
{{ computeSizeForDocker(row.memoryUsage) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('container.memCache')">
|
|
||||||
{{ computeSizeForDocker(row.memoryCache) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('container.memTotal')">
|
|
||||||
{{ computeSizeForDocker(row.memoryLimit) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item>
|
|
||||||
<template #label>
|
|
||||||
{{ $t('container.sizeRw') }}
|
|
||||||
<el-tooltip :content="$t('container.sizeRwHelper')">
|
|
||||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
{{ computeSize2(row.sizeRw) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('container.sizeRootFs')">
|
|
||||||
<template #label>
|
|
||||||
{{ $t('container.sizeRootFs') }}
|
|
||||||
<el-tooltip :content="$t('container.sizeRootFsHelper')">
|
|
||||||
<el-icon class="icon-item"><InfoFilled /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
{{ computeSize2(row.sizeRootFs) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</div>
|
|
||||||
<div v-if="!row.hasLoad">
|
|
||||||
<el-button link loading></el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="$t('commons.table.operate')" width="200px" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" link @click="onOpenTerminal(row)">
|
|
||||||
{{ $t('menu.terminal') }}
|
|
||||||
</el-button>
|
|
||||||
<el-divider direction="vertical" />
|
|
||||||
<el-button type="primary" link @click="onOpenLog(row)">
|
|
||||||
{{ $t('commons.button.log') }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</ComplexTable>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<div class="grid min-h-[600px] grid-cols-1 gap-4 xl:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">
|
|
||||||
<el-card class="h-full flex flex-col compose-detail-editor" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ $t('container.composeTemplate') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex-1">
|
|
||||||
<el-form label-position="top" class="flex h-full flex-col" @submit.prevent>
|
|
||||||
<el-form-item>
|
|
||||||
<CodemirrorPro
|
|
||||||
v-model="composeContent"
|
|
||||||
mode="yaml"
|
|
||||||
:disabled="disableEdit"
|
|
||||||
:heightDiff="200"
|
|
||||||
placeholder="#Define or paste the content of your docker-compose file here"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<div v-if="showEnvSetting" class="mt-4">
|
|
||||||
<el-form-item :label="$t('container.env')">
|
|
||||||
<el-input
|
|
||||||
v-model="envStr"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
:placeholder="$t('container.tagHelper')"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<span class="input-help whitespace-break-spaces">
|
|
||||||
{{ $t('container.editComposeHelper') }}
|
|
||||||
</span>
|
|
||||||
<CodemirrorPro
|
|
||||||
v-model="envFileContent"
|
|
||||||
:height="45"
|
|
||||||
:minHeight="45"
|
|
||||||
disabled
|
|
||||||
mode="yaml"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex justify-end gap-3">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="disableEdit"
|
|
||||||
:loading="saving"
|
|
||||||
@click="onSubmitEdit"
|
|
||||||
>
|
|
||||||
{{ $t('commons.button.save') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="h-full compose-detail-log" shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<span>{{ $t('commons.button.log') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<ContainerLog
|
|
||||||
v-if="composePath && shouldLoadLog"
|
|
||||||
:key="logKey"
|
|
||||||
:compose="composePath"
|
|
||||||
:resource="composeName"
|
|
||||||
:highlightDiff="logHeightDiff"
|
|
||||||
/>
|
|
||||||
<el-empty v-else :description="$t('commons.msg.noneData')" />
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</LayoutContent>
|
|
||||||
|
|
||||||
<ContainerInspectDialog ref="containerInspectRef" />
|
|
||||||
<TerminalDialog ref="terminalDialogRef" />
|
|
||||||
<ContainerLogDialog ref="containerLogDialogRef" :highlightDiff="210" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import NoSuchService from '@/components/layout-content/no-such-service.vue';
|
|
||||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
|
||||||
import ContainerLog from '@/components/log/container/index.vue';
|
|
||||||
import ContainerInspectDialog from '@/views/container/container/inspect/index.vue';
|
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
|
||||||
import Status from '@/components/status/index.vue';
|
|
||||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
|
||||||
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
|
|
||||||
import {
|
|
||||||
composeOperator,
|
|
||||||
composeUpdate,
|
|
||||||
containerListStats,
|
|
||||||
inspect,
|
|
||||||
loadDockerStatus,
|
|
||||||
searchCompose,
|
|
||||||
} from '@/api/modules/container';
|
|
||||||
import { Container } from '@/api/interface/container';
|
|
||||||
import { routerToFileWithPath, routerToName } from '@/utils/router';
|
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
|
||||||
import { computeCPU, computeSize2, computeSizeForDocker } from '@/utils/util';
|
|
||||||
import i18n from '@/lang';
|
|
||||||
import { Histogram } from '@element-plus/icons-vue';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const composeName = ref('');
|
|
||||||
const composeContent = ref('');
|
|
||||||
const envStr = ref('');
|
|
||||||
const envFileContent = ref(`env_file:\n - 1panel.env`);
|
|
||||||
const composeInfo = ref<Container.ComposeInfo>();
|
|
||||||
const isActive = ref(false);
|
|
||||||
const isExist = ref(false);
|
|
||||||
const dockerLoading = ref(false);
|
|
||||||
const detailLoading = ref(false);
|
|
||||||
const saving = ref(false);
|
|
||||||
const operateLoading = ref(false);
|
|
||||||
const currentOperation = ref('');
|
|
||||||
const logKey = ref(0);
|
|
||||||
const logHeightDiff = 220;
|
|
||||||
const containerInspectRef = ref();
|
|
||||||
const containerStats = ref<any[]>([]);
|
|
||||||
const terminalDialogRef = ref();
|
|
||||||
const containerLogDialogRef = ref();
|
|
||||||
const shouldLoadLog = ref(false);
|
|
||||||
|
|
||||||
const pageLoading = computed(() => dockerLoading.value || detailLoading.value);
|
|
||||||
const composeTitle = computed(() => {
|
|
||||||
if (!composeName.value) {
|
|
||||||
return i18n.global.t('container.compose');
|
|
||||||
}
|
|
||||||
return `${i18n.global.t('container.compose')} · ${composeName.value}`;
|
|
||||||
});
|
|
||||||
const composePath = computed(() => composeInfo.value?.path || (route.query.path as string) || '');
|
|
||||||
const composeContainers = computed(() => composeInfo.value?.containers || []);
|
|
||||||
const disableEdit = computed(() => composeInfo.value?.createdBy === 'Local');
|
|
||||||
const showEnvSetting = computed(() => composeInfo.value?.createdBy === '1Panel');
|
|
||||||
const showOperateHelper = computed(() => !composeInfo.value?.createdBy || composeInfo.value?.createdBy === 'Local');
|
|
||||||
const disableOperate = computed(
|
|
||||||
() => !composeInfo.value || !composePath.value || !isActive.value || !isExist.value || operateLoading.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const tableData = computed(() => {
|
|
||||||
return composeContainers.value.map((container) => {
|
|
||||||
const stats = containerStats.value.find((s) => s.containerID === container.containerID);
|
|
||||||
return {
|
|
||||||
...container,
|
|
||||||
hasLoad: !!stats,
|
|
||||||
cpuPercent: stats?.cpuPercent || 0,
|
|
||||||
memoryPercent: stats?.memoryPercent || 0,
|
|
||||||
cpuTotalUsage: stats?.cpuTotalUsage || 0,
|
|
||||||
systemUsage: stats?.systemUsage || 0,
|
|
||||||
percpuUsage: stats?.percpuUsage || 0,
|
|
||||||
memoryCache: stats?.memoryCache || 0,
|
|
||||||
memoryUsage: stats?.memoryUsage || 0,
|
|
||||||
memoryLimit: stats?.memoryLimit || 0,
|
|
||||||
sizeRw: stats?.sizeRw || 0,
|
|
||||||
sizeRootFs: stats?.sizeRootFs || 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncRouteParams = () => {
|
|
||||||
composeName.value = (route.query.name as string) || (route.params.name as string) || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadStatus = async () => {
|
|
||||||
dockerLoading.value = true;
|
|
||||||
await loadDockerStatus()
|
|
||||||
.then((res) => {
|
|
||||||
isActive.value = res.data.isActive;
|
|
||||||
isExist.value = res.data.isExist;
|
|
||||||
dockerLoading.value = false;
|
|
||||||
loadInitialDetail();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
dockerLoading.value = false;
|
|
||||||
isActive.value = false;
|
|
||||||
isExist.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadInitialDetail = async () => {
|
|
||||||
if (!composeName.value || !isActive.value || !isExist.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
detailLoading.value = true;
|
|
||||||
shouldLoadLog.value = false;
|
|
||||||
try {
|
|
||||||
await loadComposeContent();
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
await loadComposeInfo();
|
|
||||||
|
|
||||||
detailLoading.value = false;
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
shouldLoadLog.value = true;
|
|
||||||
logKey.value++;
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
await loadContainerStats();
|
|
||||||
} catch (error) {
|
|
||||||
detailLoading.value = false;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshDetail = async () => {
|
|
||||||
if (!composeName.value || !isActive.value || !isExist.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
detailLoading.value = true;
|
|
||||||
try {
|
|
||||||
await loadComposeInfo();
|
|
||||||
|
|
||||||
detailLoading.value = false;
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
await loadContainerStats();
|
|
||||||
} catch (error) {
|
|
||||||
detailLoading.value = false;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadComposeInfo = async () => {
|
|
||||||
const params = {
|
|
||||||
info: composeName.value,
|
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
};
|
|
||||||
const res = await searchCompose(params);
|
|
||||||
const items = res.data?.items || [];
|
|
||||||
const target = items.find((item) => item.name === composeName.value) || items[0];
|
|
||||||
if (!target) {
|
|
||||||
composeInfo.value = undefined;
|
|
||||||
envStr.value = '';
|
|
||||||
MsgError(i18n.global.t('commons.msg.noneData'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
composeInfo.value = target;
|
|
||||||
envStr.value = (target.env || []).join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadComposeContent = async () => {
|
|
||||||
const res = await inspect({ id: composeName.value, type: 'compose' });
|
|
||||||
composeContent.value = res.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadContainerStats = async () => {
|
|
||||||
try {
|
|
||||||
const res = await containerListStats();
|
|
||||||
containerStats.value = res.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
containerStats.value = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmitEdit = async () => {
|
|
||||||
if (!composeInfo.value || !composePath.value || disableEdit.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const param = {
|
|
||||||
name: composeName.value,
|
|
||||||
path: composePath.value,
|
|
||||||
content: composeContent.value,
|
|
||||||
createdBy: composeInfo.value.createdBy,
|
|
||||||
env: envStr.value ? envStr.value.split('\n') : [],
|
|
||||||
};
|
|
||||||
saving.value = true;
|
|
||||||
await composeUpdate(param)
|
|
||||||
.then(async () => {
|
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
await loadComposeContent();
|
|
||||||
refreshDetail();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
saving.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComposeOperate = async (operation: 'up' | 'stop' | 'restart') => {
|
|
||||||
if (!composeInfo.value || !composePath.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mes = i18n.global.t('container.composeOperatorHelper', [
|
|
||||||
composeInfo.value.name,
|
|
||||||
i18n.global.t('commons.operate.' + operation),
|
|
||||||
]);
|
|
||||||
ElMessageBox.confirm(mes, i18n.global.t('commons.operate.' + operation), {
|
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
|
||||||
type: 'info',
|
|
||||||
}).then(async () => {
|
|
||||||
currentOperation.value = operation;
|
|
||||||
operateLoading.value = true;
|
|
||||||
const params = {
|
|
||||||
name: composeInfo.value!.name,
|
|
||||||
path: composePath.value,
|
|
||||||
operation: operation,
|
|
||||||
withFile: false,
|
|
||||||
force: false,
|
|
||||||
};
|
|
||||||
await composeOperator(params)
|
|
||||||
.then(() => {
|
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
refreshDetail();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
operateLoading.value = false;
|
|
||||||
currentOperation.value = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const openComposeFolder = () => {
|
|
||||||
if (composeInfo.value?.workdir) {
|
|
||||||
routerToFileWithPath(composeInfo.value.workdir);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goSetting = async () => {
|
|
||||||
routerToName('ContainerSetting');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInspectContainer = async (item: any) => {
|
|
||||||
if (!item.containerID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await inspect({ id: item.containerID, type: 'container' });
|
|
||||||
containerInspectRef.value!.acceptParams({ data: res.data, ports: item.ports || [] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenTerminal = (row: any) => {
|
|
||||||
terminalDialogRef.value?.acceptParams({ container: row.name });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenLog = (row: any) => {
|
|
||||||
containerLogDialogRef.value?.acceptParams({ container: row.name });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
syncRouteParams();
|
|
||||||
loadStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
() => {
|
|
||||||
syncRouteParams();
|
|
||||||
loadInitialDetail();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.app-status {
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-detail-editor {
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-detail-log {
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -13,124 +13,440 @@
|
||||||
{{ $t('container.createCompose') }}
|
{{ $t('container.createCompose') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template #rightToolBar>
|
|
||||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
|
||||||
<TableRefresh @search="search()" />
|
|
||||||
<TableSetting title="compose-refresh" @search="search()" />
|
|
||||||
</template>
|
|
||||||
<template #main>
|
<template #main>
|
||||||
<ComplexTable
|
<div class="flex gap-4 h-full">
|
||||||
:pagination-config="paginationConfig"
|
<div class="w-45 flex-shrink-0 flex flex-col border-r pr-4">
|
||||||
v-model:selects="selects"
|
<el-input
|
||||||
:data="data"
|
v-model="searchName"
|
||||||
@search="search"
|
:placeholder="$t('commons.button.search')"
|
||||||
:heightDiff="350"
|
clearable
|
||||||
>
|
class="mb-4"
|
||||||
<el-table-column
|
@clear="search"
|
||||||
:label="$t('commons.table.name')"
|
@keyup.enter="search"
|
||||||
width="170"
|
>
|
||||||
prop="name"
|
<template #prefix>
|
||||||
sortable
|
<el-icon><Search /></el-icon>
|
||||||
fix
|
</template>
|
||||||
show-overflow-tooltip
|
</el-input>
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
<div class="flex-1 overflow-auto">
|
||||||
<el-text type="primary" class="cursor-pointer" @click="loadDetail(row)">
|
<div
|
||||||
{{ row.name }}
|
v-for="item in data"
|
||||||
</el-text>
|
:key="item.name"
|
||||||
</template>
|
class="compose-list-item p-3 mb-2 rounded cursor-pointer border"
|
||||||
</el-table-column>
|
:class="{
|
||||||
<el-table-column :label="$t('app.source')" prop="createdBy" min-width="80" fix>
|
'border-primary bg-blue-50': selectedCompose?.name === item.name,
|
||||||
<template #default="{ row }">
|
'border-transparent hover:bg-gray-50': selectedCompose?.name !== item.name,
|
||||||
<span v-if="row.createdBy === ''">{{ $t('commons.table.local') }}</span>
|
}"
|
||||||
<span v-if="row.createdBy === 'Apps'">{{ $t('menu.apps') }}</span>
|
@click="loadDetail(item)"
|
||||||
<span v-if="row.createdBy === '1Panel'">1Panel</span>
|
>
|
||||||
</template>
|
<div class="min-w-0">
|
||||||
</el-table-column>
|
<div class="font-medium truncate">{{ item.name }}</div>
|
||||||
<el-table-column :label="$t('container.composeDirectory')" min-width="80" fix>
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<template #default="{ row }">
|
<el-text v-if="item.containerCount === 0" type="danger" size="small">
|
||||||
<el-tooltip :content="row.workdir">
|
{{ $t('container.exited') }}
|
||||||
<el-button type="primary" link @click="toComposeFolder(row)">
|
</el-text>
|
||||||
<el-icon>
|
<el-text
|
||||||
<FolderOpened />
|
v-else
|
||||||
</el-icon>
|
:type="item.containerCount === item.runningCount ? 'success' : 'warning'"
|
||||||
</el-button>
|
size="small"
|
||||||
</el-tooltip>
|
>
|
||||||
</template>
|
{{ $t('container.running', [item.runningCount, item.containerCount]) }}
|
||||||
</el-table-column>
|
</el-text>
|
||||||
<el-table-column :label="$t('container.containerStatus')" min-width="80" fix>
|
</div>
|
||||||
<template #default="{ row }">
|
|
||||||
<el-text class="mx-1" v-if="row.containerCount == 0" type="danger">
|
|
||||||
{{ $t('container.exited') }}
|
|
||||||
</el-text>
|
|
||||||
<el-popover width="300px" v-else>
|
|
||||||
<template #reference>
|
|
||||||
<el-text
|
|
||||||
class="cursor-pointer"
|
|
||||||
size="small"
|
|
||||||
:type="row.containerCount === row.runningCount ? 'success' : 'warning'"
|
|
||||||
>
|
|
||||||
{{ $t('container.running', [row.runningCount, row.containerCount]) }}
|
|
||||||
</el-text>
|
|
||||||
</template>
|
|
||||||
<div v-for="(item, index) in row.containers" :key="index" class="mt-2">
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
<Status class="float-right" :key="item.state" :status="item.state" />
|
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</div>
|
||||||
</template>
|
<el-empty v-if="data.length === 0" :description="$t('commons.msg.noneData')" />
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column :label="$t('commons.table.createdAt')" prop="createdAt" min-width="80" fix />
|
</div>
|
||||||
<fu-table-operations
|
|
||||||
width="200px"
|
<div v-if="selectedCompose" class="flex-1 min-w-0">
|
||||||
:ellipsis="2"
|
<div v-loading="detailLoading" class="h-full flex flex-col gap-4">
|
||||||
:buttons="buttons"
|
<el-card shadow="never">
|
||||||
:label="$t('commons.table.operate')"
|
<div class="mb-3">
|
||||||
fix
|
<div class="flex items-center gap-4 mb-2">
|
||||||
/>
|
<div class="text-sm">
|
||||||
</ComplexTable>
|
<span class="text-gray-500">{{ $t('app.source') }}:</span>
|
||||||
|
<el-tag v-if="composeInfo?.createdBy === ''" size="small">
|
||||||
|
{{ $t('commons.table.local') }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="composeInfo?.createdBy === 'Apps'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ $t('menu.apps') }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else-if="composeInfo?.createdBy === '1Panel'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
1Panel
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
{{ $t('commons.table.createdAt') }}: {{ composeInfo?.createdAt }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:loading="operateLoading && currentOperation === 'up'"
|
||||||
|
:disabled="disableOperate"
|
||||||
|
@click="handleComposeOperate('up')"
|
||||||
|
>
|
||||||
|
{{ $t('commons.operate.start') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:loading="operateLoading && currentOperation === 'stop'"
|
||||||
|
:disabled="disableOperate"
|
||||||
|
@click="handleComposeOperate('stop')"
|
||||||
|
>
|
||||||
|
{{ $t('commons.operate.stop') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:loading="operateLoading && currentOperation === 'restart'"
|
||||||
|
:disabled="disableOperate"
|
||||||
|
@click="handleComposeOperate('restart')"
|
||||||
|
>
|
||||||
|
{{ $t('commons.operate.restart') }}
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:loading="detailLoading"
|
||||||
|
@click="refreshDetail"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.refresh') }}
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
:disabled="!composeInfo?.workdir"
|
||||||
|
@click="openComposeFolder"
|
||||||
|
>
|
||||||
|
{{ $t('container.composeDirectory') }}
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button type="danger" link size="small" @click="onDeleteCompose">
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="composeInfo && composeContainers.length > 0" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
{{ $t('container.containerStatus') }}
|
||||||
|
({{ composeInfo?.runningCount || 0 }}/{{ composeInfo?.containerCount || 0 }})
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="tableData" size="small" max-height="200">
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.name')"
|
||||||
|
prop="name"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-text
|
||||||
|
type="primary"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="onInspectContainer(row)"
|
||||||
|
>
|
||||||
|
{{ row.name }}
|
||||||
|
</el-text>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('commons.table.status')" prop="state" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Status :key="row.state" :status="row.state"></Status>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('container.source')"
|
||||||
|
show-overflow-tooltip
|
||||||
|
prop="resource"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.hasLoad" class="flex items-center">
|
||||||
|
<div class="text-xs">
|
||||||
|
<div>CPU: {{ row.cpuPercent.toFixed(2) }}%</div>
|
||||||
|
<div>
|
||||||
|
{{ $t('monitor.memory') }}: {{ row.memoryPercent.toFixed(2) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-popover placement="right" width="500px" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<el-icon
|
||||||
|
class="cursor-pointer text-gray-500 hover:text-primary ml-1"
|
||||||
|
:size="16"
|
||||||
|
>
|
||||||
|
<Histogram />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions
|
||||||
|
direction="vertical"
|
||||||
|
border
|
||||||
|
:column="3"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<el-descriptions-item :label="$t('container.cpuUsage')">
|
||||||
|
{{ computeCPU(row.cpuTotalUsage) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.cpuTotal')">
|
||||||
|
{{ computeCPU(row.systemUsage) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.core')">
|
||||||
|
{{ row.percpuUsage }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.memUsage')">
|
||||||
|
{{ computeSizeForDocker(row.memoryUsage) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.memCache')">
|
||||||
|
{{ computeSizeForDocker(row.memoryCache) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.memTotal')">
|
||||||
|
{{ computeSizeForDocker(row.memoryLimit) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('container.sizeRw') }}
|
||||||
|
<el-tooltip :content="$t('container.sizeRwHelper')">
|
||||||
|
<el-icon class="icon-item">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
{{ computeSize2(row.sizeRw) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('container.sizeRootFs')">
|
||||||
|
<template #label>
|
||||||
|
{{ $t('container.sizeRootFs') }}
|
||||||
|
<el-tooltip
|
||||||
|
:content="$t('container.sizeRootFsHelper')"
|
||||||
|
>
|
||||||
|
<el-icon class="icon-item">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
{{ computeSize2(row.sizeRootFs) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div v-if="!row.hasLoad">
|
||||||
|
<el-button link loading></el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('commons.table.operate')" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="onOpenTerminal(row)">
|
||||||
|
{{ $t('menu.terminal') }}
|
||||||
|
</el-button>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<el-button type="primary" link size="small" @click="onOpenLog(row)">
|
||||||
|
{{ $t('commons.button.log') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="flex-1 flex gap-4 min-h-0">
|
||||||
|
<div class="flex flex-col gap-4 min-h-0 min-w-0 flex-[1]">
|
||||||
|
<el-card shadow="never" class="flex flex-col">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-medium">{{ $t('container.compose') }}</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
:loading="saving"
|
||||||
|
@click="onSubmitEdit"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.save') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<CodemirrorPro
|
||||||
|
v-model="composeContent"
|
||||||
|
mode="yaml"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
:heightDiff="100"
|
||||||
|
placeholder="#Define or paste the content of your docker-compose file here"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card v-if="showEnvSetting" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-medium">.env</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
:loading="saving"
|
||||||
|
@click="onSubmitEdit"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.save') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form-item :label="$t('container.env')">
|
||||||
|
<el-input
|
||||||
|
v-model="envStr"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
:placeholder="$t('container.tagHelper')"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="flex flex-col min-h-0 min-w-0 flex-[2]">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="font-medium">{{ $t('commons.button.log') }}</span>
|
||||||
|
<el-button type="primary" size="small" @click="openComposeLogDrawer">
|
||||||
|
{{ $t('commons.button.view') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex-1 overflow-auto">
|
||||||
|
<ContainerLog
|
||||||
|
v-if="composePath && shouldLoadLog"
|
||||||
|
:key="logKey"
|
||||||
|
:compose="composePath"
|
||||||
|
:resource="composeName"
|
||||||
|
:highlightDiff="200"
|
||||||
|
:showControl="false"
|
||||||
|
:defaultFollow="true"
|
||||||
|
/>
|
||||||
|
<el-empty v-else :description="$t('commons.msg.noneData')" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
|
||||||
<ComposeLogs ref="composeLogRef" />
|
|
||||||
<EditDialog ref="dialogEditRef" @search="search" />
|
|
||||||
<CreateDialog @search="search" ref="dialogRef" />
|
<CreateDialog @search="search" ref="dialogRef" />
|
||||||
<DeleteDialog @search="search" ref="dialogDelRef" />
|
<DeleteDialog @search="search" ref="dialogDelRef" />
|
||||||
|
<ContainerInspectDialog ref="containerInspectRef" />
|
||||||
|
<TerminalDialog ref="terminalDialogRef" />
|
||||||
|
<ContainerLogDialog ref="containerLogDialogRef" :highlightDiff="210" />
|
||||||
|
<ComposeLogs ref="composeLogRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import EditDialog from '@/views/container/compose/edit/index.vue';
|
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||||
|
import ContainerLog from '@/components/log/container/index.vue';
|
||||||
|
import ContainerInspectDialog from '@/views/container/container/inspect/index.vue';
|
||||||
|
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||||
|
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
|
||||||
import CreateDialog from '@/views/container/compose/create/index.vue';
|
import CreateDialog from '@/views/container/compose/create/index.vue';
|
||||||
import DeleteDialog from '@/views/container/compose/delete/index.vue';
|
import DeleteDialog from '@/views/container/compose/delete/index.vue';
|
||||||
import ComposeLogs from '@/components/log/compose/index.vue';
|
import ComposeLogs from '@/components/log/compose/index.vue';
|
||||||
import { composeOperator, inspect, searchCompose } from '@/api/modules/container';
|
import { composeOperator, composeUpdate, containerListStats, inspect, searchCompose } from '@/api/modules/container';
|
||||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
|
import { routerToFileWithPath } from '@/utils/router';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
|
import { computeCPU, computeSize2, computeSizeForDocker } from '@/utils/util';
|
||||||
|
import { Histogram, Search } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
const data = ref();
|
const data = ref<any[]>([]);
|
||||||
const selects = ref<any>([]);
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const selectedCompose = ref<Container.ComposeInfo | null>(null);
|
||||||
const paginationConfig = reactive({
|
const detailLoading = ref(false);
|
||||||
cacheSizeKey: 'container-compose-page-size',
|
const operateLoading = ref(false);
|
||||||
currentPage: 1,
|
const currentOperation = ref('');
|
||||||
pageSize: Number(localStorage.getItem('container-compose-page-size')) || 20,
|
const saving = ref(false);
|
||||||
total: 0,
|
const composeName = ref('');
|
||||||
});
|
const composeContent = ref('');
|
||||||
const searchName = ref();
|
const envStr = ref('');
|
||||||
|
const composeInfo = ref<Container.ComposeInfo>();
|
||||||
|
const containerStats = ref<any[]>([]);
|
||||||
|
const logKey = ref(0);
|
||||||
|
const shouldLoadLog = ref(false);
|
||||||
|
const containerInspectRef = ref();
|
||||||
|
const terminalDialogRef = ref();
|
||||||
|
const containerLogDialogRef = ref();
|
||||||
const composeLogRef = ref();
|
const composeLogRef = ref();
|
||||||
|
const searchName = ref('');
|
||||||
|
|
||||||
const isActive = ref(false);
|
const isActive = ref(false);
|
||||||
const isExist = ref(false);
|
const isExist = ref(false);
|
||||||
|
|
||||||
const toComposeFolder = async (row: Container.ComposeInfo) => {
|
const composePath = computed(() => composeInfo.value?.path || selectedCompose.value?.path || '');
|
||||||
routerToFileWithPath(row.workdir);
|
const composeContainers = computed(() => composeInfo.value?.containers || []);
|
||||||
|
const disableEdit = computed(() => composeInfo.value?.createdBy === 'Local');
|
||||||
|
const showEnvSetting = computed(() => composeInfo.value?.createdBy === '1Panel');
|
||||||
|
const disableOperate = computed(
|
||||||
|
() => !composeInfo.value || !composePath.value || !isActive.value || !isExist.value || operateLoading.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
return composeContainers.value.map((container) => {
|
||||||
|
const stats = containerStats.value.find((s) => s.containerID === container.containerID);
|
||||||
|
return {
|
||||||
|
...container,
|
||||||
|
hasLoad: !!stats,
|
||||||
|
cpuPercent: stats?.cpuPercent || 0,
|
||||||
|
memoryPercent: stats?.memoryPercent || 0,
|
||||||
|
cpuTotalUsage: stats?.cpuTotalUsage || 0,
|
||||||
|
systemUsage: stats?.systemUsage || 0,
|
||||||
|
percpuUsage: stats?.percpuUsage || 0,
|
||||||
|
memoryCache: stats?.memoryCache || 0,
|
||||||
|
memoryUsage: stats?.memoryUsage || 0,
|
||||||
|
memoryLimit: stats?.memoryLimit || 0,
|
||||||
|
sizeRw: stats?.sizeRw || 0,
|
||||||
|
sizeRootFs: stats?.sizeRootFs || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeDetail = () => {
|
||||||
|
selectedCompose.value = null;
|
||||||
|
composeName.value = '';
|
||||||
|
composeInfo.value = undefined;
|
||||||
|
composeContent.value = '';
|
||||||
|
envStr.value = '';
|
||||||
|
shouldLoadLog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openComposeFolder = () => {
|
||||||
|
if (composeInfo.value?.workdir) {
|
||||||
|
routerToFileWithPath(composeInfo.value.workdir);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
|
|
@ -139,15 +455,14 @@ const search = async () => {
|
||||||
}
|
}
|
||||||
let params = {
|
let params = {
|
||||||
info: searchName.value,
|
info: searchName.value,
|
||||||
page: paginationConfig.currentPage,
|
page: 1,
|
||||||
pageSize: paginationConfig.pageSize,
|
pageSize: 100,
|
||||||
};
|
};
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await searchCompose(params)
|
await searchCompose(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
data.value = res.data.items || [];
|
data.value = res.data.items || [];
|
||||||
paginationConfig.total = res.data.total;
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
@ -155,7 +470,77 @@ const search = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadDetail = async (row: Container.ComposeInfo) => {
|
const loadDetail = async (row: Container.ComposeInfo) => {
|
||||||
routerToNameWithQuery('ComposeDetail', { name: row.name, path: row.path });
|
if (selectedCompose.value?.name === row.name) {
|
||||||
|
closeDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedCompose.value = row;
|
||||||
|
composeName.value = row.name;
|
||||||
|
detailLoading.value = true;
|
||||||
|
shouldLoadLog.value = false;
|
||||||
|
try {
|
||||||
|
await loadComposeContent();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
await loadComposeInfo();
|
||||||
|
detailLoading.value = false;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
shouldLoadLog.value = true;
|
||||||
|
logKey.value++;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
await loadContainerStats();
|
||||||
|
} catch (error) {
|
||||||
|
detailLoading.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadComposeInfo = async () => {
|
||||||
|
const params = {
|
||||||
|
info: composeName.value,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
};
|
||||||
|
const res = await searchCompose(params);
|
||||||
|
const items = res.data?.items || [];
|
||||||
|
const target = items.find((item) => item.name === composeName.value) || items[0];
|
||||||
|
if (!target) {
|
||||||
|
composeInfo.value = undefined;
|
||||||
|
envStr.value = '';
|
||||||
|
MsgError(i18n.global.t('commons.msg.noneData'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
composeInfo.value = target;
|
||||||
|
envStr.value = (target.env || []).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadComposeContent = async () => {
|
||||||
|
const res = await inspect({ id: composeName.value, type: 'compose' });
|
||||||
|
composeContent.value = res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadContainerStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await containerListStats();
|
||||||
|
containerStats.value = res.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
containerStats.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDetail = async () => {
|
||||||
|
if (!composeName.value || !isActive.value || !isExist.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailLoading.value = true;
|
||||||
|
try {
|
||||||
|
await loadComposeInfo();
|
||||||
|
detailLoading.value = false;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
await loadContainerStats();
|
||||||
|
} catch (error) {
|
||||||
|
detailLoading.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialogRef = ref();
|
const dialogRef = ref();
|
||||||
|
|
@ -163,111 +548,96 @@ const onOpenDialog = async () => {
|
||||||
dialogRef.value!.acceptParams();
|
dialogRef.value!.acceptParams();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDeleteCompose = () => {
|
||||||
|
if (!selectedCompose.value) return;
|
||||||
|
dialogDelRef.value.acceptParams({
|
||||||
|
name: selectedCompose.value.name,
|
||||||
|
path: selectedCompose.value.path,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const dialogDelRef = ref();
|
const dialogDelRef = ref();
|
||||||
const onDelete = async (row: Container.ComposeInfo) => {
|
|
||||||
const param = {
|
|
||||||
name: row.name,
|
|
||||||
path: row.path,
|
|
||||||
};
|
|
||||||
dialogDelRef.value.acceptParams(param);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogEditRef = ref();
|
const handleComposeOperate = async (operation: 'up' | 'stop' | 'restart') => {
|
||||||
const onEdit = async (row: Container.ComposeInfo) => {
|
if (!composeInfo.value || !composePath.value) {
|
||||||
const res = await inspect({ id: row.name, type: 'compose' });
|
return;
|
||||||
let params = {
|
}
|
||||||
name: row.name,
|
const mes = i18n.global.t('container.composeOperatorHelper', [
|
||||||
path: row.path,
|
composeInfo.value.name,
|
||||||
content: res.data,
|
i18n.global.t('commons.operate.' + operation),
|
||||||
env: row.env,
|
]);
|
||||||
createdBy: row.createdBy,
|
|
||||||
};
|
|
||||||
dialogEditRef.value!.acceptParams(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onComposeOperate = async (operation: string, row: any) => {
|
|
||||||
let mes =
|
|
||||||
operation === 'down'
|
|
||||||
? i18n.global.t('container.composeDownHelper', [row.name])
|
|
||||||
: i18n.global.t('container.composeOperatorHelper', [
|
|
||||||
row.name,
|
|
||||||
i18n.global.t('commons.operate.' + operation),
|
|
||||||
]);
|
|
||||||
ElMessageBox.confirm(mes, i18n.global.t('commons.operate.' + operation), {
|
ElMessageBox.confirm(mes, i18n.global.t('commons.operate.' + operation), {
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
let params = {
|
currentOperation.value = operation;
|
||||||
name: row.name,
|
operateLoading.value = true;
|
||||||
path: row.path,
|
const params = {
|
||||||
|
name: composeInfo.value!.name,
|
||||||
|
path: composePath.value,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
withFile: false,
|
withFile: false,
|
||||||
force: false,
|
force: false,
|
||||||
};
|
};
|
||||||
loading.value = true;
|
|
||||||
await composeOperator(params)
|
await composeOperator(params)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
loading.value = false;
|
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
refreshDetail();
|
||||||
search();
|
search();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
operateLoading.value = false;
|
||||||
|
currentOperation.value = '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openLog = (row: any) => {
|
const onSubmitEdit = async () => {
|
||||||
composeLogRef.value.acceptParams({
|
if (!composeInfo.value || !composePath.value || disableEdit.value) {
|
||||||
compose: row.path,
|
return;
|
||||||
resource: row.name,
|
}
|
||||||
container: row.container,
|
const param = {
|
||||||
});
|
name: composeName.value,
|
||||||
|
path: composePath.value,
|
||||||
|
content: composeContent.value,
|
||||||
|
createdBy: composeInfo.value.createdBy,
|
||||||
|
env: envStr.value ? envStr.value.split('\n') : [],
|
||||||
|
};
|
||||||
|
saving.value = true;
|
||||||
|
await composeUpdate(param)
|
||||||
|
.then(async () => {
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
await loadComposeContent();
|
||||||
|
refreshDetail();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
saving.value = false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons = [
|
const onInspectContainer = async (item: any) => {
|
||||||
{
|
if (!item.containerID) {
|
||||||
label: i18n.global.t('commons.button.edit'),
|
return;
|
||||||
click: (row: Container.ComposeInfo) => {
|
}
|
||||||
onEdit(row);
|
const res = await inspect({ id: item.containerID, type: 'container' });
|
||||||
},
|
containerInspectRef.value!.acceptParams({ data: res.data, ports: item.ports || [] });
|
||||||
disabled: (row: any) => {
|
};
|
||||||
return row.createdBy === 'Local';
|
|
||||||
},
|
const onOpenTerminal = (row: any) => {
|
||||||
},
|
terminalDialogRef.value?.acceptParams({ container: row.name });
|
||||||
{
|
};
|
||||||
label: i18n.global.t('commons.button.log'),
|
|
||||||
click: (row: Container.ComposeInfo) => {
|
const onOpenLog = (row: any) => {
|
||||||
openLog(row);
|
containerLogDialogRef.value?.acceptParams({ container: row.name });
|
||||||
},
|
};
|
||||||
},
|
|
||||||
{
|
const openComposeLogDrawer = () => {
|
||||||
label: i18n.global.t('commons.operate.start'),
|
if (!composePath.value || !composeName.value) return;
|
||||||
click: (row: Container.ComposeInfo) => {
|
composeLogRef.value?.acceptParams({
|
||||||
onComposeOperate('up', row);
|
compose: composePath.value,
|
||||||
},
|
resource: composeName.value,
|
||||||
},
|
container: '',
|
||||||
{
|
});
|
||||||
label: i18n.global.t('commons.operate.stop'),
|
};
|
||||||
click: (row: Container.ComposeInfo) => {
|
|
||||||
onComposeOperate('stop', row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('commons.operate.restart'),
|
|
||||||
click: (row: Container.ComposeInfo) => {
|
|
||||||
onComposeOperate('restart', row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n.global.t('commons.operate.delete'),
|
|
||||||
click: (row: Container.ComposeInfo) => {
|
|
||||||
onDelete(row);
|
|
||||||
},
|
|
||||||
disabled: (row: any) => {
|
|
||||||
return row.createdBy !== '1Panel';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue