add DockerContainerDetail.vue
This commit is contained in:
@@ -1,35 +1,24 @@
|
|||||||
import { axios } from "timi-web";
|
import { axios, Text } from "timi-web";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type {
|
import type { DockerContainerHistoryView, DockerContainerStatusView, DockerContainerSummaryView } from "@/types/Docker";
|
||||||
DockerContainerHistoryView,
|
|
||||||
DockerContainerStatusView,
|
|
||||||
DockerContainerSummaryView
|
|
||||||
} from "@/types/Docker";
|
|
||||||
|
|
||||||
async function getContainers(): Promise<DockerContainerSummaryView[]> {
|
async function getContainers(): Promise<DockerContainerSummaryView[]> {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container`, {
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container`);
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getContainerStatus(containerId: string): Promise<DockerContainerStatusView | null> {
|
async function getContainerStatus(containerId: string): Promise<DockerContainerStatusView | null> {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container/${encodeURIComponent(containerId)}/status`, {
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container/${encodeURIComponent(containerId)}/status`);
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getContainerHistory(containerId: string, params?: {
|
async function getContainerHistory(containerId: string, params?: {
|
||||||
window?: string;
|
window?: string;
|
||||||
}): Promise<DockerContainerHistoryView | null> {
|
}): Promise<DockerContainerHistoryView | null> {
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container/${encodeURIComponent(containerId)}/history`, {
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/docker/container/${encodeURIComponent(containerId)}/history?${Text.urlArgs({
|
||||||
params: {
|
window: params?.window
|
||||||
...(params?.window ? { window: params.window } : {})
|
})}`);
|
||||||
},
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="Toolkit.isNotEmpty(note)" class="note" v-text="note" />
|
<div v-if="Toolkit.isNotEmpty(note)" class="health" v-text="note" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="Toolkit.isNotEmpty(legendList)" class="legends">
|
<div v-if="Toolkit.isNotEmpty(legendList)" class="legends">
|
||||||
<div
|
<div
|
||||||
@@ -158,7 +158,7 @@ const legendList = computed<RenderProgressItem[]>(() => {
|
|||||||
transition: width 320ms var(--tui-bezier);
|
transition: width 320ms var(--tui-bezier);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.health {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: .25rem;
|
right: .25rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
762
src/pages/dashboard/DockerDashboard/DockerContainerDetail.vue
Normal file
762
src/pages/dashboard/DockerDashboard/DockerContainerDetail.vue
Normal file
@@ -0,0 +1,762 @@
|
|||||||
|
<template>
|
||||||
|
<div class="docker-container-detail">
|
||||||
|
<t-empty v-if="!containerId" description="缺少容器 ID 参数" />
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="isStatusLoading && !statusView" class="loading-wrap">
|
||||||
|
<t-loading text="加载容器状态..." />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="statusView">
|
||||||
|
<t-cell-group title="容器概览" theme="card">
|
||||||
|
<t-cell title="名称" :note="Text.display(statusView?.name)" />
|
||||||
|
<t-cell title="镜像" :note="Text.display(statusView?.image)" />
|
||||||
|
<t-cell title="状态" :note="resolveStateText(statusView?.state)" />
|
||||||
|
<t-cell title="状态详情" :note="Text.display(statusView?.status)" />
|
||||||
|
<t-cell title="健康检查" :note="resolveHealthText(statusView?.healthStatus)" />
|
||||||
|
<t-cell title="容器 ID" :note="Text.display(statusView?.id?.slice(0, 12))" />
|
||||||
|
<t-cell title="镜像 ID" :note="Text.display(statusView?.imageId?.slice(0, 12))" />
|
||||||
|
<t-cell title="创建时间" :note="Time.toPassedDateTime(statusView?.createdAt)" />
|
||||||
|
<t-cell title="启动时间" :note="resolveDockerRawTime(statusView?.startedAt)" />
|
||||||
|
<t-cell title="结束时间" :note="resolveDockerRawTime(statusView?.finishedAt)" />
|
||||||
|
<t-cell title="采样时间" :note="Time.toPassedDateTime(statusView?.updatedAt)" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group class="section resource" title="资源使用" theme="card">
|
||||||
|
<t-cell-info label="CPU 使用率" :value="Text.unit(statusView?.cpuPercent, '%')">
|
||||||
|
<progress-group mode="heap" :progress="cpuProgressList" />
|
||||||
|
</t-cell-info>
|
||||||
|
<t-cell-info label="内存使用率" :value="memoryUsageText">
|
||||||
|
<progress-group mode="heap" :progress="memoryProgressList" :note="Text.unit(statusView?.memoryPercent, '%')" />
|
||||||
|
</t-cell-info>
|
||||||
|
<t-cell title="进程数" :note="Text.display(statusView?.pids)" />
|
||||||
|
<t-cell title="重启次数" :note="Text.display(statusView?.restartCount)" />
|
||||||
|
<t-cell title="退出码" :note="Text.display(statusView?.exitCode)" />
|
||||||
|
<t-cell title="OOM Kill" :note="Text.displayBool(statusView?.oomKilled, '是', '否')" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group class="section traffic" title="网络与块设备" theme="card">
|
||||||
|
<t-cell title="网络接收" :note="IOSize.format(statusView?.networkRxBytes)" />
|
||||||
|
<t-cell title="网络发送" :note="IOSize.format(statusView?.networkTxBytes)" />
|
||||||
|
<t-cell title="块设备读取" :note="IOSize.format(statusView?.blockReadBytes)" />
|
||||||
|
<t-cell title="块设备写入" :note="IOSize.format(statusView?.blockWriteBytes)" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无容器状态数据" />
|
||||||
|
|
||||||
|
<t-cell-group class="section history" title="历史采样" theme="card">
|
||||||
|
<t-tabs class="metric-tabs" :value="historyMetric" @change="handleHistoryMetricChange">
|
||||||
|
<t-tab-panel v-for="item in historyMetricTabs" :key="item.value" :value="item.value" :label="item.label" />
|
||||||
|
</t-tabs>
|
||||||
|
<div v-if="isHistoryLoading && !historyItems.length" class="loading-wrap">
|
||||||
|
<t-loading text="加载容器历史..." />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="historyItems.length">
|
||||||
|
<v-chart
|
||||||
|
ref="chartRef"
|
||||||
|
class="chart"
|
||||||
|
:option="historyChartOption"
|
||||||
|
:update-options="historyChartUpdateOptions"
|
||||||
|
:autoresize="true"
|
||||||
|
/>
|
||||||
|
<div class="slider bg-white">
|
||||||
|
<span class="label">较早</span>
|
||||||
|
<t-slider
|
||||||
|
class="control"
|
||||||
|
:min="0"
|
||||||
|
:max="Math.max(historyItems.length - historyVisibleCount, 0)"
|
||||||
|
:step="1"
|
||||||
|
:value="historySliderValue"
|
||||||
|
@change="handleHistorySliderChange"
|
||||||
|
/>
|
||||||
|
<span class="label">最新</span>
|
||||||
|
</div>
|
||||||
|
<t-cell-group>
|
||||||
|
<t-cell v-for="item in historySummary" :key="item.label" :title="item.label" :note="item.value" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无容器历史数据" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import { use } from "echarts/core";
|
||||||
|
import { SVGRenderer } from "echarts/renderers";
|
||||||
|
import { LineChart } from "echarts/charts";
|
||||||
|
import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
|
||||||
|
import VChart from "vue-echarts";
|
||||||
|
import type { EChartsOption, SeriesOption } from "echarts";
|
||||||
|
import DockerAPI from "@/api/DockerAPI";
|
||||||
|
import type {
|
||||||
|
DockerContainerHistoryPointView,
|
||||||
|
DockerContainerStatusView
|
||||||
|
} from "@/types/Docker";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
import { IOSize, type LabelValue, Text, Time } from "timi-web";
|
||||||
|
import TCellInfo from "@/components/TCellInfo.vue";
|
||||||
|
import type { ProgressItem } from "@/components/ProgressGroup.vue";
|
||||||
|
import ProgressGroup from "@/components/ProgressGroup.vue";
|
||||||
|
|
||||||
|
type DockerHistoryMetric = "cpu" | "memory" | "network" | "block" | "pids";
|
||||||
|
|
||||||
|
use([
|
||||||
|
SVGRenderer,
|
||||||
|
LineChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent
|
||||||
|
]);
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DockerContainerDetail"
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
const containerId = computed(() => {
|
||||||
|
const queryContainerId = route.query.containerId;
|
||||||
|
if (typeof queryContainerId === "string") {
|
||||||
|
return queryContainerId;
|
||||||
|
}
|
||||||
|
if (Array.isArray(queryContainerId) && queryContainerId.length) {
|
||||||
|
return queryContainerId[0];
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const isStatusLoading = ref(false);
|
||||||
|
const isHistoryLoading = ref(false);
|
||||||
|
|
||||||
|
const statusView = ref<DockerContainerStatusView | null>(null);
|
||||||
|
const historyPoints = ref<DockerContainerHistoryPointView[]>([]);
|
||||||
|
|
||||||
|
const chartRef = ref<InstanceType<typeof VChart> | null>(null);
|
||||||
|
const historyChartUpdateOptions = Object.freeze({
|
||||||
|
notMerge: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 当前状态 ----------
|
||||||
|
|
||||||
|
const cpuProgressList = computed<ProgressItem[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
color: "var(--td-brand-color)",
|
||||||
|
value: normalizePercent(statusView.value?.cpuPercent)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const memoryUsageText = computed(() => {
|
||||||
|
const usage = IOSize.format(statusView.value?.memoryUsageBytes || 0);
|
||||||
|
const limitValue = statusView.value?.memoryLimitBytes || 0;
|
||||||
|
const limit = limitValue < 1 ? "无限制" : IOSize.format(limitValue);
|
||||||
|
return `${usage} / ${limit}`;
|
||||||
|
});
|
||||||
|
const memoryProgressList = computed<ProgressItem[]>(() => {
|
||||||
|
const limit = statusView.value?.memoryLimitBytes || 0;
|
||||||
|
const usage = statusView.value?.memoryUsageBytes || 0;
|
||||||
|
const fallback = limit < 1 ? 0 : usage / limit;
|
||||||
|
const percent = statusView.value?.memoryPercent;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
color: "var(--td-warning-color)",
|
||||||
|
value: normalizePercent(percent, fallback)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizePercent(percent?: number | null, fallback = 0): number {
|
||||||
|
const source = typeof percent === "number" ? percent / 100 : fallback;
|
||||||
|
if (Number.isNaN(source) || !Number.isFinite(source)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(source, 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStateText(state?: string | null): string {
|
||||||
|
if (!state) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
created: "已创建",
|
||||||
|
running: "运行中",
|
||||||
|
restarting: "重启中",
|
||||||
|
exited: "已退出",
|
||||||
|
paused: "已暂停",
|
||||||
|
dead: "已停止"
|
||||||
|
};
|
||||||
|
return map[state] || state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHealthText(health?: string | null): string {
|
||||||
|
if (!health) {
|
||||||
|
return "未配置";
|
||||||
|
}
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
healthy: "健康",
|
||||||
|
unhealthy: "异常",
|
||||||
|
starting: "启动中"
|
||||||
|
};
|
||||||
|
return map[health] || health;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDockerRawTime(value?: string | null): string {
|
||||||
|
if (!value || value.startsWith("0001-01-01")) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const dateValue = Date.parse(value);
|
||||||
|
if (Number.isNaN(dateValue)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return Time.toPassedDateTime(dateValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 历史采样 ----------
|
||||||
|
|
||||||
|
const historyMetric = ref<DockerHistoryMetric>("cpu");
|
||||||
|
|
||||||
|
const historyMetricTabs = computed(() => {
|
||||||
|
const labelMap: Record<DockerHistoryMetric, string> = {
|
||||||
|
cpu: "CPU",
|
||||||
|
memory: "内存",
|
||||||
|
network: "网络",
|
||||||
|
block: "块设备",
|
||||||
|
pids: "进程"
|
||||||
|
};
|
||||||
|
return (Object.keys(labelMap) as DockerHistoryMetric[]).map((item) => ({
|
||||||
|
value: item,
|
||||||
|
label: labelMap[item]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleHistoryMetricChange(value: string): void {
|
||||||
|
const nextValue = value as DockerHistoryMetric;
|
||||||
|
if (nextValue === "cpu" || nextValue === "memory" || nextValue === "network" || nextValue === "block" || nextValue === "pids") {
|
||||||
|
historyMetric.value = nextValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyItems = computed<DockerContainerHistoryPointView[]>(() => {
|
||||||
|
return historyPoints.value.slice().sort((left, right) => left.at - right.at);
|
||||||
|
});
|
||||||
|
const historySummary = computed<LabelValue<string, any>[]>(() => {
|
||||||
|
const lastPoint = historyItems.value[historyItems.value.length - 1];
|
||||||
|
if (!lastPoint) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "cpu") {
|
||||||
|
return [
|
||||||
|
{ label: "CPU 使用率", value: Text.unit(lastPoint.cpuPercent, "%") }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "memory") {
|
||||||
|
return [
|
||||||
|
{ label: "内存占用", value: IOSize.format(lastPoint.memoryUsageBytes || 0) },
|
||||||
|
{ label: "内存使用率", value: Text.unit(lastPoint.memoryPercent, "%") }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "network") {
|
||||||
|
return [
|
||||||
|
{ label: "累计接收", value: IOSize.format(lastPoint.networkRxBytes || 0) },
|
||||||
|
{ label: "累计发送", value: IOSize.format(lastPoint.networkTxBytes || 0) }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "block") {
|
||||||
|
return [
|
||||||
|
{ label: "累计读取", value: IOSize.format(lastPoint.blockReadBytes || 0) },
|
||||||
|
{ label: "累计写入", value: IOSize.format(lastPoint.blockWriteBytes || 0) }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ label: "进程数", value: Text.display(lastPoint.pids) }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHistorySliderTouched = ref(false);
|
||||||
|
const historySliderValue = ref(0);
|
||||||
|
const HISTORY_DEFAULT_VISIBLE_COUNT = 24;
|
||||||
|
|
||||||
|
const historyVisibleCount = computed(() => {
|
||||||
|
if (historyItems.value.length < 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.min(HISTORY_DEFAULT_VISIBLE_COUNT, historyItems.value.length);
|
||||||
|
});
|
||||||
|
const historySliderMaxStart = computed(() => {
|
||||||
|
return Math.max(historyItems.value.length - historyVisibleCount.value, 0);
|
||||||
|
});
|
||||||
|
const currentHistoryRange = computed(() => {
|
||||||
|
const length = historyItems.value.length;
|
||||||
|
if (length < 1) {
|
||||||
|
return { startValue: 0, endValue: 0 };
|
||||||
|
}
|
||||||
|
const startValue = Math.min(Math.max(Math.floor(historySliderValue.value), 0), historySliderMaxStart.value);
|
||||||
|
const endValue = Math.min(startValue + historyVisibleCount.value - 1, length - 1);
|
||||||
|
return { startValue, endValue };
|
||||||
|
});
|
||||||
|
const visibleHistoryItems = computed<DockerContainerHistoryPointView[]>(() => {
|
||||||
|
if (!historyItems.value.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return historyItems.value.slice(currentHistoryRange.value.startValue, currentHistoryRange.value.endValue + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => historyItems.value.length, (length) => {
|
||||||
|
if (length < 1) {
|
||||||
|
historySliderValue.value = 0;
|
||||||
|
isHistorySliderTouched.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isHistorySliderTouched.value) {
|
||||||
|
historySliderValue.value = historySliderMaxStart.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
historySliderValue.value = Math.min(historySliderValue.value, historySliderMaxStart.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleHistorySliderChange(value: number): void {
|
||||||
|
historySliderValue.value = Math.min(Math.max(Math.floor(value), 0), historySliderMaxStart.value);
|
||||||
|
isHistorySliderTouched.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHistorySlider(): void {
|
||||||
|
historySliderValue.value = 0;
|
||||||
|
isHistorySliderTouched.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 图表 ----------
|
||||||
|
|
||||||
|
const isDarkTheme = ref(false);
|
||||||
|
let themeObserver: MutationObserver | null = null;
|
||||||
|
|
||||||
|
const historyChartOption = computed((): EChartsOption => {
|
||||||
|
const axisColor = isDarkTheme.value ? "#9aa4b2" : "#7e8a9a";
|
||||||
|
const lineColor = isDarkTheme.value ? "rgba(255, 255, 255, .12)" : "rgba(18, 42, 66, .08)";
|
||||||
|
const legendColor = isDarkTheme.value ? "#d7dee8" : "#334155";
|
||||||
|
|
||||||
|
const commonOption: EChartsOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
confine: true,
|
||||||
|
position: (point, _params, _dom, _rect, size) => {
|
||||||
|
const gap = 6;
|
||||||
|
const fingerGap = 24;
|
||||||
|
const contentSize = size?.contentSize || [0, 0];
|
||||||
|
const viewSize = size?.viewSize || [0, 0];
|
||||||
|
const maxLeft = Math.max(viewSize[0] - contentSize[0] - gap, gap);
|
||||||
|
const maxTop = Math.max(viewSize[1] - contentSize[1] - gap, gap);
|
||||||
|
const clamp = (value: number, min: number, max: number): number => {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
};
|
||||||
|
const preferLeft = point[0] - contentSize[0] - gap;
|
||||||
|
const preferRight = point[0] + gap;
|
||||||
|
const left = point[0] < viewSize[0] / 2
|
||||||
|
? clamp(preferRight, gap, maxLeft)
|
||||||
|
: clamp(preferLeft, gap, maxLeft);
|
||||||
|
const preferTop = point[1] - contentSize[1] - fingerGap;
|
||||||
|
const preferBottom = point[1] + fingerGap;
|
||||||
|
const top = preferTop >= gap
|
||||||
|
? clamp(preferTop, gap, maxTop)
|
||||||
|
: clamp(preferBottom, gap, maxTop);
|
||||||
|
return [left, top];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: legendColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 40,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: visibleHistoryItems.value.map((item) => item.at),
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: val => Time.toShortTime(Number(val))
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!historyItems.value.length) {
|
||||||
|
return commonOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "cpu") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: params => formatPercentTooltip(params, " %")
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} %"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("CPU", visibleHistoryItems.value.map((item) => item.cpuPercent || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "memory") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: params => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => {
|
||||||
|
const currentName = item.seriesName || "";
|
||||||
|
if (currentName === "内存占用") {
|
||||||
|
return `${item.marker}${currentName}: ${IOSize.format(Number(item.value))}`;
|
||||||
|
}
|
||||||
|
return `${item.marker}${currentName}: ${Text.unit(Number(item.value), " %")}`;
|
||||||
|
});
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: (value: number) => IOSize.format(value)
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} %"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("内存占用", visibleHistoryItems.value.map((item) => item.memoryUsageBytes || 0)),
|
||||||
|
resolveLineSeries("内存使用率", visibleHistoryItems.value.map((item) => item.memoryPercent || 0), 1)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "network") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: params => formatSizeTooltip(params)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: (value: number) => IOSize.format(value)
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("接收", visibleHistoryItems.value.map((item) => item.networkRxBytes || 0)),
|
||||||
|
resolveLineSeries("发送", visibleHistoryItems.value.map((item) => item.networkTxBytes || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "block") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: params => formatSizeTooltip(params)
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: (value: number) => IOSize.format(value)
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("读取", visibleHistoryItems.value.map((item) => item.blockReadBytes || 0)),
|
||||||
|
resolveLineSeries("写入", visibleHistoryItems.value.map((item) => item.blockWriteBytes || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: params => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => `${item.marker}${item.seriesName}: ${Text.display(Number(item.value))}`);
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
minInterval: 1,
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("进程数", visibleHistoryItems.value.map((item) => item.pids || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatPercentTooltip(params: unknown, unit: string): string {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number((list[0] as { name?: string }).name || 0))}`;
|
||||||
|
const lines = list.map((item) => {
|
||||||
|
const current = item as { marker?: string; seriesName?: string; value?: number };
|
||||||
|
return `${current.marker || ""}${current.seriesName || ""}: ${Text.unit(Number(current.value || 0), unit)}`;
|
||||||
|
});
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSizeTooltip(params: unknown): string {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number((list[0] as { name?: string }).name || 0))}`;
|
||||||
|
const lines = list.map((item) => {
|
||||||
|
const current = item as { marker?: string; seriesName?: string; value?: number };
|
||||||
|
return `${current.marker || ""}${current.seriesName || ""}: ${IOSize.format(Number(current.value || 0))}`;
|
||||||
|
});
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLineSeries(name: string, data: Array<number | null>, yAxisIndex?: number): SeriesOption {
|
||||||
|
const series: SeriesOption = {
|
||||||
|
type: "line",
|
||||||
|
name,
|
||||||
|
showSymbol: false,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
opacity: .18
|
||||||
|
},
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof yAxisIndex === "number") {
|
||||||
|
series.yAxisIndex = yAxisIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 加载与自动刷新 ----------
|
||||||
|
|
||||||
|
let statusTimer: number | null = null;
|
||||||
|
let historyTimer: number | null = null;
|
||||||
|
|
||||||
|
watch(() => settingStore.dashboard.server, restartAutoRefresh, { deep: true });
|
||||||
|
watch(containerId, () => {
|
||||||
|
statusView.value = null;
|
||||||
|
historyPoints.value = [];
|
||||||
|
resetHistorySlider();
|
||||||
|
restartAutoRefresh();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateThemeState();
|
||||||
|
themeObserver = new MutationObserver(() => {
|
||||||
|
updateThemeState();
|
||||||
|
});
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearAutoRefreshTimer();
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect();
|
||||||
|
themeObserver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateThemeState(): void {
|
||||||
|
isDarkTheme.value = document.documentElement.classList.contains("theme-dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshContainerStatus(): Promise<void> {
|
||||||
|
if (!containerId.value) {
|
||||||
|
statusView.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isStatusLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isStatusLoading.value = true;
|
||||||
|
try {
|
||||||
|
statusView.value = await DockerAPI.getContainerStatus(containerId.value);
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isStatusLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshContainerHistory(): Promise<void> {
|
||||||
|
if (!containerId.value) {
|
||||||
|
historyPoints.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isHistoryLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isHistoryLoading.value = true;
|
||||||
|
try {
|
||||||
|
const historyView = await DockerAPI.getContainerHistory(containerId.value, {
|
||||||
|
window: "2h"
|
||||||
|
});
|
||||||
|
historyPoints.value = historyView?.points || [];
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isHistoryLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDashboard(): Promise<void> {
|
||||||
|
await refreshContainerStatus();
|
||||||
|
await refreshContainerHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartAutoRefresh(): void {
|
||||||
|
clearAutoRefreshTimer();
|
||||||
|
if (!containerId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void refreshDashboard();
|
||||||
|
|
||||||
|
const snapshotIntervalMs = Math.max(settingStore.dashboard.server.snapshotRefreshSeconds, 1) * 1000;
|
||||||
|
const historyIntervalMs = Math.max(settingStore.dashboard.server.historyRefreshSeconds, 1) * 1000;
|
||||||
|
|
||||||
|
statusTimer = window.setInterval(() => {
|
||||||
|
void refreshContainerStatus();
|
||||||
|
}, snapshotIntervalMs);
|
||||||
|
|
||||||
|
historyTimer = window.setInterval(() => {
|
||||||
|
void refreshContainerHistory();
|
||||||
|
}, historyIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAutoRefreshTimer(): void {
|
||||||
|
if (statusTimer !== null) {
|
||||||
|
window.clearInterval(statusTimer);
|
||||||
|
statusTimer = null;
|
||||||
|
}
|
||||||
|
if (historyTimer !== null) {
|
||||||
|
window.clearInterval(historyTimer);
|
||||||
|
historyTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.docker-container-detail {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
padding: var(--app-nav-offset) 0 1rem 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
|
||||||
|
&.history {
|
||||||
|
|
||||||
|
.metric-tabs {
|
||||||
|
width: 100%;
|
||||||
|
--td-tab-track-width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 16rem;
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
padding: .5rem 2rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: none;
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--app-sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-wrap {
|
||||||
|
display: flex;
|
||||||
|
min-height: 4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -733,7 +733,7 @@ onMounted(restartAutoRefresh);
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.health {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
color: var(--app-sub);
|
color: var(--app-sub);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
key-field="key"
|
key-field="key"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #before>
|
||||||
<div aria-hidden="true" class="spacer top"></div>
|
<div aria-hidden="true" class="spacer header"></div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #after>
|
<template #after>
|
||||||
<div aria-hidden="true" class="spacer bottom"></div>
|
<div aria-hidden="true" class="spacer content"></div>
|
||||||
</template>
|
</template>
|
||||||
</recycle-scroller>
|
</recycle-scroller>
|
||||||
</template>
|
</template>
|
||||||
@@ -94,11 +94,11 @@ function handleOpen(item: ExplorerItem): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.top {
|
&.header {
|
||||||
height: var(--top-gap);
|
height: var(--top-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bottom {
|
&.content {
|
||||||
height: var(--bottom-gap);
|
height: var(--bottom-gap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
key-field="path"
|
key-field="path"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #before>
|
||||||
<div aria-hidden="true" class="spacer top"></div>
|
<div aria-hidden="true" class="spacer header"></div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<t-swipe-cell :disabled="!isSwipeActionVisible(item)" :left="resolveLeftActions(item)">
|
<t-swipe-cell :disabled="!isSwipeActionVisible(item)" :left="resolveLeftActions(item)">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
@click="handleOpen(item)"
|
@click="handleOpen(item)"
|
||||||
>
|
>
|
||||||
<template #note>
|
<template #note>
|
||||||
<div class="note">
|
<div class="health">
|
||||||
<t-loading v-if="item.path === pendingPath" size="1rem" />
|
<t-loading v-if="item.path === pendingPath" size="1rem" />
|
||||||
<span v-if="item.type === 'file'" v-text="item.size"></span>
|
<span v-if="item.type === 'file'" v-text="item.size"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</t-swipe-cell>
|
</t-swipe-cell>
|
||||||
</template>
|
</template>
|
||||||
<template #after>
|
<template #after>
|
||||||
<div aria-hidden="true" class="spacer bottom"></div>
|
<div aria-hidden="true" class="spacer content"></div>
|
||||||
</template>
|
</template>
|
||||||
</recycle-scroller>
|
</recycle-scroller>
|
||||||
</template>
|
</template>
|
||||||
@@ -145,16 +145,16 @@ function handlePlay(item: ExplorerItem): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&.top {
|
&.header {
|
||||||
height: var(--top-gap);
|
height: var(--top-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bottom {
|
&.content {
|
||||||
height: var(--bottom-gap);
|
height: var(--bottom-gap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.health {
|
||||||
gap: .375rem;
|
gap: .375rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ThemeSetting from "@/pages/setting/ThemeSetting.vue";
|
|||||||
import ServerDetail from "@/pages/dashboard/ServerDashboard/ServerDetail.vue";
|
import ServerDetail from "@/pages/dashboard/ServerDashboard/ServerDetail.vue";
|
||||||
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
||||||
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
||||||
|
import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory("/"),
|
history: createWebHistory("/"),
|
||||||
@@ -109,6 +110,19 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
component: ServerPartitionsDetail
|
component: ServerPartitionsDetail
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/server/docker-container-detail",
|
||||||
|
name: "DockerContainerDetail",
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "容器详情",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F4F4F4"
|
||||||
|
},
|
||||||
|
component: DockerContainerDetail
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/settings/connect",
|
path: "/settings/connect",
|
||||||
name: "ConnectSetting",
|
name: "ConnectSetting",
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ export interface DockerContainerSummaryView {
|
|||||||
image: string;
|
image: string;
|
||||||
state: string;
|
state: string;
|
||||||
status: string;
|
status: string;
|
||||||
healthStatus?: string | null;
|
healthStatus?: string;
|
||||||
cpuPercent?: number | null;
|
cpuPercent?: number;
|
||||||
memoryUsageBytes?: number | null;
|
memoryUsageBytes?: number;
|
||||||
memoryLimitBytes?: number | null;
|
memoryLimitBytes?: number;
|
||||||
memoryPercent?: number | null;
|
memoryPercent?: number;
|
||||||
networkRxBytes?: number | null;
|
networkRxBytes?: number;
|
||||||
networkTxBytes?: number | null;
|
networkTxBytes?: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,21 +22,21 @@ export interface DockerContainerStatusView {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
state: string;
|
state: string;
|
||||||
status: string;
|
status: string;
|
||||||
healthStatus?: string | null;
|
healthStatus?: string;
|
||||||
startedAt?: string | null;
|
startedAt?: string;
|
||||||
finishedAt?: string | null;
|
finishedAt?: string;
|
||||||
exitCode?: number | null;
|
exitCode?: number;
|
||||||
restartCount: number;
|
restartCount: number;
|
||||||
oomKilled: boolean;
|
oomKilled: boolean;
|
||||||
cpuPercent?: number | null;
|
cpuPercent?: number;
|
||||||
memoryUsageBytes?: number | null;
|
memoryUsageBytes?: number;
|
||||||
memoryLimitBytes?: number | null;
|
memoryLimitBytes?: number;
|
||||||
memoryPercent?: number | null;
|
memoryPercent?: number;
|
||||||
networkRxBytes?: number | null;
|
networkRxBytes?: number;
|
||||||
networkTxBytes?: number | null;
|
networkTxBytes?: number;
|
||||||
blockReadBytes?: number | null;
|
blockReadBytes?: number;
|
||||||
blockWriteBytes?: number | null;
|
blockWriteBytes?: number;
|
||||||
pids?: number | null;
|
pids?: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +52,12 @@ export interface DockerContainerHistoryView {
|
|||||||
|
|
||||||
export interface DockerContainerHistoryPointView {
|
export interface DockerContainerHistoryPointView {
|
||||||
at: number;
|
at: number;
|
||||||
cpuPercent?: number | null;
|
cpuPercent?: number;
|
||||||
memoryUsageBytes?: number | null;
|
memoryUsageBytes?: number;
|
||||||
memoryPercent?: number | null;
|
memoryPercent?: number;
|
||||||
networkRxBytes?: number | null;
|
networkRxBytes?: number;
|
||||||
networkTxBytes?: number | null;
|
networkTxBytes?: number;
|
||||||
blockReadBytes?: number | null;
|
blockReadBytes?: number;
|
||||||
blockWriteBytes?: number | null;
|
blockWriteBytes?: number;
|
||||||
pids?: number | null;
|
pids?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user