diff --git a/packages/web/projects/vgpu/views/card/admin/components/WorkloadSemiProgress.vue b/packages/web/projects/vgpu/views/card/admin/components/WorkloadSemiProgress.vue
new file mode 100644
index 00000000..cb1151af
--- /dev/null
+++ b/packages/web/projects/vgpu/views/card/admin/components/WorkloadSemiProgress.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/packages/web/projects/vgpu/views/card/admin/index.vue b/packages/web/projects/vgpu/views/card/admin/index.vue
index 315bb16e..a8bc6a44 100644
--- a/packages/web/projects/vgpu/views/card/admin/index.vue
+++ b/packages/web/projects/vgpu/views/card/admin/index.vue
@@ -88,8 +88,7 @@ const props = defineProps(['hideTitle', 'filters']);
const router = useRouter();
const route = useRoute();
-const { t, locale } = useI18n();
-const isEnglish = computed(() => String(locale.value || '').startsWith('en'));
+const { t } = useI18n();
const parseTypeFromQuery = (value) => {
if (typeof value === 'string') return value || undefined;
if (Array.isArray(value) && typeof value[0] === 'string') return value[0] || undefined;
@@ -150,15 +149,16 @@ const getCardStatusDisplay = ({ health, isExternal }) => {
return { icon: 'status-unschedulable', text: t('card.abnormal') };
};
-const getRemainingTotalText = ({ total, used, unit = '' }) => {
+const getRemainingTotalText = ({ total, used, unit = '', divisor = 1 }) => {
const totalNum = Number(total || 0);
if (!totalNum) return null;
const usedNum = Number(used || 0);
+ const normalizedDivisor = Number(divisor) > 0 ? Number(divisor) : 1;
const remaining = Math.max(0, totalNum - usedNum);
const unitText = unit ? ` ${unit}` : '';
return {
- remaining: roundToDecimal(remaining, 1),
- total: roundToDecimal(totalNum, 1),
+ remaining: roundToDecimal(remaining / normalizedDivisor, 1),
+ total: roundToDecimal(totalNum / normalizedDivisor, 1),
unitText,
};
};
@@ -190,7 +190,7 @@ const baseColumns = computed(() => [
{
title: t('task.status'),
dataIndex: 'health',
- width: 100,
+ width: 150,
render: ({ health, isExternal }) => {
const { icon, text } = getCardStatusDisplay({ health, isExternal });
return (
@@ -201,44 +201,23 @@ const baseColumns = computed(() => [
);
},
},
- {
- title: t('card.mode'),
- dataIndex: 'mode',
- width: 120,
- render: ({ mode, type }) => (
-
- )
- },
{
title: t('card.node'),
dataIndex: 'nodeName',
- width: 170,
+ width: 200,
hideTooltip: true,
render: ({ nodeName }) => (
),
},
- {
- title: t('card.vgpu'),
- key: 'card-vgpu',
- dataIndex: 'used',
- width: 100,
- render: ({ vgpuTotal, vgpuUsed, isExternal }) => (
-
- ),
- },
{
title: t('card.computeRemainingTotal'),
key: 'card-compute-remaining-total',
dataIndex: 'used',
- width: isEnglish.value ? 220 : 180,
+ width: 220,
render: ({ coreTotal, coreUsed, isExternal }) => {
if (isExternal || !coreTotal) return
;
- const stats = getRemainingTotalText({ total: coreTotal, used: coreUsed });
+ const stats = getRemainingTotalText({ total: coreTotal, used: coreUsed, divisor: 100 });
if (!stats) return
@@ -254,7 +233,7 @@ const baseColumns = computed(() => [
title: t('card.computeAllocTotal'),
key: 'card-compute-allocation',
dataIndex: 'used',
- width: isEnglish.value ? 170 : 140,
+ width: 180,
render: ({ coreTotal, coreUsed, isExternal }) => {
if (isExternal || !coreTotal) return
--;
const percent = Math.max(
@@ -281,7 +260,7 @@ const baseColumns = computed(() => [
title: t('card.memoryRemainingTotal'),
key: 'card-memory-remaining-total',
dataIndex: 'used',
- width: isEnglish.value ? 210 : 180,
+ width: 220,
render: ({ memoryTotal, memoryUsed, isExternal }) => {
if (isExternal || !memoryTotal) return
--;
const stats = getRemainingTotalText({
@@ -304,7 +283,7 @@ const baseColumns = computed(() => [
title: t('card.memoryAllocTotal'),
key: 'card-memory-allocation',
dataIndex: 'w',
- width: isEnglish.value ? 160 : 140,
+ width: 180,
render: ({ memoryTotal, memoryUsed, isExternal }) => {
if (isExternal || !memoryTotal) return
--;
const percent = Math.max(
diff --git a/packages/web/projects/vgpu/views/monitor/overview/Block.vue b/packages/web/projects/vgpu/views/monitor/overview/Block.vue
index 8f237f54..6c2f88e2 100644
--- a/packages/web/projects/vgpu/views/monitor/overview/Block.vue
+++ b/packages/web/projects/vgpu/views/monitor/overview/Block.vue
@@ -16,9 +16,9 @@ defineProps(['title']);
diff --git a/packages/web/projects/vgpu/views/node/admin/getOptions.js b/packages/web/projects/vgpu/views/node/admin/getOptions.js
index 22b04c8b..b55587d9 100644
--- a/packages/web/projects/vgpu/views/node/admin/getOptions.js
+++ b/packages/web/projects/vgpu/views/node/admin/getOptions.js
@@ -1,8 +1,43 @@
import { timeParse } from '@/utils';
+const normalizePoints = (points = []) => {
+ return points
+ .map((point) => {
+ if (Array.isArray(point)) {
+ return { timestamp: Number(point[0]), value: Number(point[1]) };
+ }
+ return {
+ timestamp: Number(point?.timestamp),
+ value: Number(point?.value),
+ };
+ })
+ .filter((item) => Number.isFinite(item.timestamp) && Number.isFinite(item.value));
+};
+
+const buildPercentTooltipFormatter = () => {
+ return (params) => {
+ if (!Array.isArray(params) || params.length === 0) return '';
+ let result = `
${params[0]?.axisValueLabel || params[0]?.name || ''}
`;
+ for (let i = 0; i < params.length; i++) {
+ const item = params[i];
+ const num = Number(item?.value);
+ const value = Number.isFinite(num) ? `${num.toFixed(3)}%` : '-';
+ result += `
+
+ ${item?.seriesName || '-'}:
+ ${value}
+
`;
+ }
+ return result;
+ };
+};
+
export const getRangeOptions = ({ allocation = [], usage = [] }, t = (v) => v) => {
- const xDataSource = allocation?.length ? allocation : usage;
+ const normalizedAllocation = normalizePoints(allocation);
+ const normalizedUsage = normalizePoints(usage);
+ const xDataSource = normalizedAllocation.length ? normalizedAllocation : normalizedUsage;
return {
+ animation: false,
legend: {
bottom: 10,
left: 'center',
@@ -12,20 +47,7 @@ export const getRangeOptions = ({ allocation = [], usage = [] }, t = (v) => v) =
axisPointer: {
type: 'cross',
},
- formatter: function (params) {
- if (!params || params.length === 0) return '';
- var res = params[0].name + '
';
- for (var i = 0; i < params.length; i++) {
- res +=
- params[i].marker +
- params[i].seriesName +
- ' : ' +
- (+params[i].value).toFixed(0) +
- '
';
- }
-
- return res;
- },
+ formatter: buildPercentTooltipFormatter(),
},
grid: {
top: 20, // 上边距
@@ -33,9 +55,16 @@ export const getRangeOptions = ({ allocation = [], usage = [] }, t = (v) => v) =
left: '7%', // 左边距
right: 10, // 右边距
},
+ dataZoom: [
+ {
+ type: 'inside',
+ xAxisIndex: 0,
+ filterMode: 'none',
+ },
+ ],
xAxis: {
type: 'category',
- data: xDataSource.map((item) => timeParse(+item.timestamp)),
+ data: xDataSource.map((item) => timeParse(item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
@@ -53,24 +82,26 @@ export const getRangeOptions = ({ allocation = [], usage = [] }, t = (v) => v) =
series: [
{
name: t('dashboard.allocRateLegend'),
- data: allocation,
+ data: normalizedAllocation.map((item) => item.value),
type: 'line',
itemStyle: {
- color: 'rgb(84, 112, 198)',
+ color: '#5B8FF9',
},
lineStyle: {
- color: 'rgb(84, 112, 198)',
+ width: 3,
+ color: '#5B8FF9',
},
},
{
name: t('dashboard.usageRateLegend'),
- data: usage,
+ data: normalizedUsage.map((item) => item.value),
type: 'line',
itemStyle: {
- color: 'rgb(145, 204, 117)',
+ color: '#42C090',
},
lineStyle: {
- color: 'rgb(145, 204, 117)',
+ width: 3,
+ color: '#42C090',
},
},
],
diff --git a/packages/web/projects/vgpu/views/node/admin/index.vue b/packages/web/projects/vgpu/views/node/admin/index.vue
index 48691a57..ed5615af 100644
--- a/packages/web/projects/vgpu/views/node/admin/index.vue
+++ b/packages/web/projects/vgpu/views/node/admin/index.vue
@@ -140,6 +140,7 @@ const getNodeStatusDisplay = ({ isSchedulable, isExternal }) => {
const baseColumns = computed(() => [
{
title: t('node.name'),
+ minWidth: 200,
dataIndex: 'name',
render: ({ uid, name }) => {
const to = `/admin/vgpu/node/admin/${uid}?nodeName=${name}`;
@@ -157,6 +158,7 @@ const baseColumns = computed(() => [
},
{
title: t('task.status'),
+ minWidth: 150,
dataIndex: 'isSchedulable',
render: ({ isSchedulable, isExternal }) => {
const { icon, text } = getNodeStatusDisplay({ isSchedulable, isExternal });
@@ -170,29 +172,13 @@ const baseColumns = computed(() => [
},
{
title: t('node.ip'),
+ minWidth: 100,
dataIndex: 'ip',
},
- {
- title: t('node.cardModel'),
- dataIndex: 'type',
- },
- {
- title: t('node.cardCount'),
- dataIndex: 'cardCnt',
- },
- {
- title: t('node.vgpu'),
- key: 'node-vgpu',
- dataIndex: 'used',
- render: ({ vgpuTotal, vgpuUsed, isExternal }) => (
-
- {isExternal ? '--' : vgpuUsed}/{isExternal ? '--' : vgpuTotal}
-
- ),
- },
{
title: t('node.computeAllocTotal'),
key: 'node-compute-allocation',
+ minWidth: 280,
dataIndex: 'used',
render: ({ coreTotal, coreUsed, isExternal }) => {
if (isExternal || !coreTotal) return
--;
@@ -218,6 +204,7 @@ const baseColumns = computed(() => [
{
title: t('node.memoryAllocTotal'),
key: 'node-memory-allocation',
+ minWidth: 280,
dataIndex: 'used',
render: ({ memoryTotal, memoryUsed, isExternal }) => {
if (isExternal || !memoryTotal) return
--;
diff --git a/packages/web/projects/vgpu/views/task/admin/Detail.vue b/packages/web/projects/vgpu/views/task/admin/Detail.vue
index f062fadb..4d43f8a9 100644
--- a/packages/web/projects/vgpu/views/task/admin/Detail.vue
+++ b/packages/web/projects/vgpu/views/task/admin/Detail.vue
@@ -1,41 +1,153 @@
-
- {{ $t('task.detail.title') }} : {{ detail.name }}
-
+
+
+
+
+
+
+
+
+ {{ $t('task.checkCloudPlatform') }}
+
+
+
-
+
-
{{ $t('task.detail.detailInfo') }}
-
- -
- {{ label }}:
-
-
-
- {{ detail[value] }}
-
-
- -
-
- {{ label }}
- {{ count }} {{ $t('task.times') }}
+
+
+
{{ detail.namespace || '--' }}
+
{{ $t('task.namespace') }}
+
+
+
+
+
+
+
{{ $t('task.node') }}
+
+
+
+
--
+
+
+
+ {{ item.model }}
+ × {{ item.count }}
+
+
+
+
+
+
{{ $t('task.gpuModel') }}
+
+
+
+
{{ relatedGpuCountText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('task.relatedGpu') }}
+
+
+
+
+ {{ $t('task.image') }}
+
+
+ {{ basicImage }}
+
+ {{ basicImage }}
-
-
+
+
+ {{ $t('task.createTime') }}
+ {{ basicCreateTime }}
+
+
+
-
-
-
-
+
+
+
+
+
+
+
{{ resourceOverviewTexts.gpuCards }}
+
{{ $t('task.gpuCardCount') }}
+
-
-
+
+
+
+
+
{{ resourceOverviewTexts.computeLimit }}
+
{{ $t('task.computePowerLimit') }}
+
+
+
+
+
+
+
+
{{ resourceOverviewTexts.singleCardMemory }}
+
{{ $t('task.singleCardMemory') }}
+
+
+
+
+
+
+
+
{{ resourceOverviewTexts.cpuLimit }}
+
{{ $t('task.cpuLimit') }}
+
+
+
+
+
+
+
+
{{ resourceOverviewTexts.memoryLimit }}
+
{{ $t('task.memoryLimit') }}
+
+
+
+
+
-
+
@@ -57,21 +173,21 @@
diff --git a/packages/web/src/components/BackHeader.vue b/packages/web/src/components/BackHeader.vue
deleted file mode 100644
index 212113a5..00000000
--- a/packages/web/src/components/BackHeader.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/web/src/components/BlockBox.vue b/packages/web/src/components/BlockBox.vue
index 1195b0ad..9ce7f755 100644
--- a/packages/web/src/components/BlockBox.vue
+++ b/packages/web/src/components/BlockBox.vue
@@ -16,7 +16,7 @@ defineProps(['title']);
diff --git a/packages/web/src/components/index.js b/packages/web/src/components/index.js
index 496ff74b..8535cafc 100644
--- a/packages/web/src/components/index.js
+++ b/packages/web/src/components/index.js
@@ -10,7 +10,6 @@ import FormItem from './FormPlus/FormItem.vue';
import FormRender from './FormPlus/FormRender.vue';
import FormGroup from './FormPlus/FormGroup.vue';
import ItemGroup from './ItemGroup.vue';
-import BackHeader from './BackHeader.vue';
import InfoPreview from './InfoPreview.vue';
import ButtonGroup from './ButtonGroup.vue';
import DetailDrawer from './DetailDrawer.vue';
@@ -37,7 +36,6 @@ export default (app) => {
app.component('FormRender', FormRender);
app.component('FormGroup', FormGroup);
app.component('ItemGroup', ItemGroup);
- app.component('BackHeader', BackHeader);
app.component('InfoPreview', InfoPreview);
app.component('ButtonGroup', ButtonGroup);
app.component('DetailDrawer', DetailDrawer);
diff --git a/packages/web/src/icons/svg/cpu-limit.svg b/packages/web/src/icons/svg/cpu-limit.svg
new file mode 100644
index 00000000..19a51eb8
--- /dev/null
+++ b/packages/web/src/icons/svg/cpu-limit.svg
@@ -0,0 +1,5 @@
+
diff --git a/packages/web/src/icons/svg/gpu-ascend.svg b/packages/web/src/icons/svg/gpu-ascend.svg
new file mode 100644
index 00000000..ed594ed8
--- /dev/null
+++ b/packages/web/src/icons/svg/gpu-ascend.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/web/src/icons/svg/gpu-aws.svg b/packages/web/src/icons/svg/gpu-aws.svg
new file mode 100644
index 00000000..7b2b590b
--- /dev/null
+++ b/packages/web/src/icons/svg/gpu-aws.svg
@@ -0,0 +1,7 @@
+
diff --git a/packages/web/src/icons/svg/gpu-metax.svg b/packages/web/src/icons/svg/gpu-metax.svg
new file mode 100644
index 00000000..dd21e707
--- /dev/null
+++ b/packages/web/src/icons/svg/gpu-metax.svg
@@ -0,0 +1,12 @@
+
diff --git a/packages/web/src/icons/svg/gpu-nvidia.svg b/packages/web/src/icons/svg/gpu-nvidia.svg
new file mode 100644
index 00000000..5689ee9a
--- /dev/null
+++ b/packages/web/src/icons/svg/gpu-nvidia.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/web/src/icons/svg/help-circle.svg b/packages/web/src/icons/svg/help-circle.svg
new file mode 100644
index 00000000..8161988f
--- /dev/null
+++ b/packages/web/src/icons/svg/help-circle.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/web/src/icons/svg/node-cpu-total.svg b/packages/web/src/icons/svg/node-cpu-total.svg
new file mode 100644
index 00000000..a7a4470d
--- /dev/null
+++ b/packages/web/src/icons/svg/node-cpu-total.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/web/src/icons/svg/node-memory-total.svg b/packages/web/src/icons/svg/node-memory-total.svg
new file mode 100644
index 00000000..cb41241f
--- /dev/null
+++ b/packages/web/src/icons/svg/node-memory-total.svg
@@ -0,0 +1,16 @@
+
diff --git a/packages/web/src/icons/svg/related-gpu-eye.svg b/packages/web/src/icons/svg/related-gpu-eye.svg
new file mode 100644
index 00000000..53cd1b28
--- /dev/null
+++ b/packages/web/src/icons/svg/related-gpu-eye.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/web/src/icons/svg/vgpu-workload.svg b/packages/web/src/icons/svg/vgpu-workload.svg
new file mode 100644
index 00000000..925b3809
--- /dev/null
+++ b/packages/web/src/icons/svg/vgpu-workload.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/web/src/locales/en.js b/packages/web/src/locales/en.js
index 1f893124..bf579fd2 100644
--- a/packages/web/src/locales/en.js
+++ b/packages/web/src/locales/en.js
@@ -74,10 +74,14 @@ export default {
nodeTotal: 'Total Nodes',
card: 'GPU',
gpuCardCount: 'GPU Cards',
+ workloadCount: 'Workloads',
vgpu: 'vGPU',
compute: 'Compute',
+ cpuTotalCores: 'Total CPU Cores',
+ cpuCoreUnit: 'Core',
+ systemMemoryTotal: 'Total Memory',
memory: 'Memory',
- memoryTotal: 'Total Memory',
+ memoryTotal: 'Total GPU Memory',
schedulable: 'Schedulable',
unschedulable: 'Unschedulable',
allocated: 'Allocated',
@@ -87,10 +91,24 @@ export default {
allocRateLegend: 'Alloc Rate',
gpuComputeAllocUsageTrend: 'GPU Compute Alloc/Usage Trend (%)',
gpuMemAllocUsageTrend: 'GPU Memory Alloc/Usage Trend (%)',
+ cpuAllocUsageTrend: 'CPU Alloc/Usage Trend (%)',
+ memAllocUsageTrend: 'Memory Alloc/Usage Trend (%)',
+ cpuAllocRate: 'CPU Allocation Rate',
+ cpuUsageRate: 'CPU Usage Rate',
+ systemMemAllocRate: 'Memory Allocation Rate',
+ systemMemUsageRate: 'Memory Usage Rate',
nodeResourceUsageTop5: 'Node Usage Top5',
nodeResourceAllocTop5: 'Node Alloc Top5',
nodeComputeTop5: 'Node Compute Top5',
nodeMemoryTop5: 'Node Memory Top5',
+ nodeWorkloadTop5: 'Node Workload Count Top5',
+ nodeWorkloadDistribution: 'Node Workload Distribution',
+ nodeWorkloadDistributionDesc1:
+ 'Show the distribution of all cluster nodes by workload-count ranges.',
+ nodeWorkloadDistributionDesc2:
+ 'X-axis is workload-count range per node, and Y-axis is number of nodes in that range.',
+ workload24hTrendTop5: '24h Workload Usage Trend Top5 (%)',
+ workloadRange: 'Workload Range',
gpuComputeTop5: 'GPU Compute Top5',
gpuMemoryTop5: 'GPU Memory Top5',
resourceAllocTrendTitle: 'Alloc Trend',
@@ -139,6 +157,8 @@ export default {
resourceOverview: 'Resource Overview',
title: 'Node Detail',
detailInfo: 'Details',
+ nodeOverview: 'Node Overview',
+ workloadDistribution: 'Workload Distribution',
nodeSchedule: 'Node Schedule',
resourceAllocTrend: 'Alloc Trend (%)',
resourceUsageTrend: 'Usage Trend (%)',
@@ -150,7 +170,7 @@ export default {
vgpuOvercommit: 'vGPU Overcommit',
computeOvercommit: 'Compute Overcommit',
memoryOvercommit: 'Memory Overcommit',
- nodeIpAddress: 'Node IP Address',
+ nodeIpAddress: 'Node IP',
nodeUuid: 'Node UUID',
osType: 'Operating System Type',
architecture: 'Architecture',
@@ -191,14 +211,14 @@ export default {
unmanaged: 'Unmanaged',
detail: {
title: 'GPU',
- detailInfo: 'Details',
+ detailInfo: 'Basic Info',
resourceOverview: 'Resource Overview',
+ workloadCount: 'Workloads',
+ workloadCountTip:
+ 'Each accelerator card supports sharing by multiple workloads at the same time.\nThe chart shows [Allocated Count / Maximum Supported Count].',
resourceAllocTrend: 'Alloc Trend (%)',
resourceUsageTrend: 'Usage Trend (%)',
- taskList: 'Workloads',
noAllocData: 'No allocation data',
- unmanagedNoTask: 'GPU unmanaged; task data unavailable',
- noTaskData: 'No task data',
uuid: 'GPU UUID',
temperature: 'Temperature',
powerUsage: 'Power Usage',
@@ -227,6 +247,11 @@ export default {
allocatedVgpu: 'vGPU',
allocatedCompute: 'Compute',
allocatedMemory: 'Memory',
+ gpuCardCount: 'GPU Cards',
+ computePowerLimit: 'Compute Power Limit',
+ singleCardMemory: 'Single-card Memory',
+ cpuLimit: 'CPU Limit',
+ memoryLimit: 'Memory Limit',
startTime: 'Start Time',
duration: 'Duration',
namespace: 'Namespace',
@@ -240,17 +265,26 @@ export default {
card: 'GPU',
allCards: 'All GPUs',
cardType: 'GPU Type',
+ gpuModel: 'GPU Model',
+ relatedGpu: 'Related GPU',
+ relatedGpuCards: '{count} GPUs',
+ image: 'Image',
+ cpu: 'CPU',
+ memory: 'Memory',
+ priority: 'Priority',
appName: 'App Name',
createTime: 'Creation Time',
times: 'x',
- computeUsageTrend: 'GPU Compute Usage Trend (%)',
- memUsageTrend: 'GPU Memory Usage Trend (%)',
+ computeUsageTrend: 'GPU Compute Utilization (%)',
+ memUsageTrend: 'GPU Memory Utilization (%)',
+ cpuUsageTrend: 'CPU Usage (%)',
+ memoryUsageTrend: 'Memory Usage (%)',
noMonitorSupport: 'Vendor does not support task-level monitoring',
topCount: 'Workload Count Top5',
topApply: 'Workload Requests Top5',
detail: {
title: 'Workload',
- detailInfo: 'Details',
+ detailInfo: 'Basic Info',
resourceOverview: 'Resource Overview',
containerInfo: 'Container',
podName: 'Pod Name',
diff --git a/packages/web/src/locales/zh.js b/packages/web/src/locales/zh.js
index 35d5452c..83225670 100644
--- a/packages/web/src/locales/zh.js
+++ b/packages/web/src/locales/zh.js
@@ -74,10 +74,14 @@ export default {
nodeTotal: '节点总数',
card: 'GPU',
gpuCardCount: 'GPU 卡数',
+ workloadCount: '工作负载数量',
vgpu: 'vGPU',
compute: '算力',
+ cpuTotalCores: 'CPU 总核',
+ cpuCoreUnit: 'Core',
+ systemMemoryTotal: '内存总量',
memory: '显存',
- memoryTotal: '显存总量',
+ memoryTotal: 'GPU 显存总量',
schedulable: '可调度',
unschedulable: '禁止调度',
allocated: '已分配',
@@ -87,10 +91,22 @@ export default {
allocRateLegend: '分配率',
gpuComputeAllocUsageTrend: 'GPU 算力分配/使用趋势(%)',
gpuMemAllocUsageTrend: 'GPU 显存分配/使用趋势(%)',
+ cpuAllocUsageTrend: 'CPU 分配/使用趋势(%)',
+ memAllocUsageTrend: '内存分配/使用趋势(%)',
+ cpuAllocRate: 'CPU 分配率',
+ cpuUsageRate: 'CPU 使用率',
+ systemMemAllocRate: '内存分配率',
+ systemMemUsageRate: '内存使用率',
nodeResourceUsageTop5: '节点使用率 Top5',
nodeResourceAllocTop5: '节点分配率 Top5',
nodeComputeTop5: '节点算力 Top5',
nodeMemoryTop5: '节点显存 Top5',
+ nodeWorkloadTop5: '节点工作负载数量 Top5',
+ nodeWorkloadDistribution: '节点工作负载分布(台)',
+ nodeWorkloadDistributionDesc1: '展示集群中所有节点的工作负载在不同数量区间的分布情况。',
+ nodeWorkloadDistributionDesc2: '横轴是节点的工作负载数量范围,纵轴表示落入该范围的节点数量。',
+ workload24hTrendTop5: '24 小时工作负载使用趋势 Top5 (%)',
+ workloadRange: '工作负载区间',
gpuComputeTop5: 'GPU 算力 Top5',
gpuMemoryTop5: 'GPU 显存 Top5',
resourceAllocTrendTitle: '分配趋势',
@@ -139,6 +155,8 @@ export default {
resourceOverview: '资源概览',
title: '节点详情',
detailInfo: '详细信息',
+ nodeOverview: '节点概览',
+ workloadDistribution: '工作负载分布',
nodeSchedule: '节点调度',
resourceAllocTrend: '分配趋势(%)',
resourceUsageTrend: '使用趋势(%)',
@@ -150,13 +168,13 @@ export default {
vgpuOvercommit: 'vGPU 超配',
computeOvercommit: '算力超配',
memoryOvercommit: '显存超配',
- nodeIpAddress: '节点 IP 地址',
+ nodeIpAddress: '节点 IP',
nodeUuid: '节点 UUID',
osType: '操作系统类型',
- architecture: '系统架构',
+ architecture: '架构',
kubeletVersion: 'kubelet 版本',
- osVersion: '操作系统版本',
- kernelVersion: '内核版本',
+ osVersion: '操作系统',
+ kernelVersion: 'Kernel 版本',
kubeProxyVersion: 'kube-proxy 版本',
containerRuntime: '容器运行时',
creationTime: '创建时间',
@@ -191,14 +209,13 @@ export default {
unmanaged: '未纳管',
detail: {
title: 'GPU',
- detailInfo: '详细信息',
+ detailInfo: '基本信息',
resourceOverview: '资源概览',
+ workloadCount: '工作负载数',
+ workloadCountTip: '每张算力卡支持被多个工作负载同时共享。\n图中展示的是【已分配数量 / 最大支持数量】。',
resourceAllocTrend: '资源分配趋势(%)',
resourceUsageTrend: '资源使用趋势(%)',
- taskList: '工作负载',
noAllocData: '暂无资源分配数据',
- unmanagedNoTask: '由于 GPU 未纳管,无法获取到任务数据',
- noTaskData: '暂无任务数据',
uuid: 'GPU UUID',
temperature: '温度',
powerUsage: '功耗',
@@ -227,6 +244,11 @@ export default {
allocatedVgpu: 'vGPU',
allocatedCompute: '算力',
allocatedMemory: '显存',
+ gpuCardCount: 'GPU 卡数',
+ computePowerLimit: '算力限制',
+ singleCardMemory: '单卡显存',
+ cpuLimit: 'CPU 限制',
+ memoryLimit: '内存限制',
startTime: '启动时间',
duration: '运行时长',
namespace: '命名空间',
@@ -240,17 +262,26 @@ export default {
card: '所属显卡',
allCards: '全部显卡',
cardType: '显卡类型',
+ gpuModel: 'GPU 型号',
+ relatedGpu: '关联 GPU',
+ relatedGpuCards: '共 {count} 张',
+ image: '镜像',
+ cpu: 'CPU',
+ memory: '内存',
+ priority: '优先级',
appName: '应用名称',
createTime: '创建时间',
times: '倍',
- computeUsageTrend: '算力使用趋势(%)',
- memUsageTrend: '显存使用趋势(%)',
+ computeUsageTrend: 'GPU 算力使用率(%)',
+ memUsageTrend: 'GPU 显存使用率(%)',
+ cpuUsageTrend: 'CPU 使用率(%)',
+ memoryUsageTrend: '内存使用率(%)',
noMonitorSupport: '该设备厂商暂不支持任务维度监控',
topCount: '任务数量分布 Top5',
topApply: '任务资源申请 Top5',
detail: {
title: '任务管理',
- detailInfo: '详细信息',
+ detailInfo: '基础信息',
resourceOverview: '资源概览',
containerInfo: '容器信息',
podName: 'Pod 名称',
diff --git a/packages/web/src/main.js b/packages/web/src/main.js
index c7b72834..e51dfb11 100644
--- a/packages/web/src/main.js
+++ b/packages/web/src/main.js
@@ -4,6 +4,7 @@ import router from './router';
import store from './store';
import installElementPlus from './plugins/element';
import installTDesign from './plugins/tdesign';
+import installEcharts from './plugins/echarts';
import 'normalize.css/normalize.css'; // a modern alternative to CSS resets
import '@/styles/index.scss'; // global css
@@ -20,6 +21,7 @@ const app = createApp(App);
app.use(i18n);
installElementPlus(app);
installTDesign(app);
+installEcharts();
installIcons(app);
app.use(components);
diff --git a/packages/web/src/plugins/echarts.js b/packages/web/src/plugins/echarts.js
new file mode 100644
index 00000000..d74296a8
--- /dev/null
+++ b/packages/web/src/plugins/echarts.js
@@ -0,0 +1,8 @@
+import { LineChart, BarChart } from 'echarts/charts';
+import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from 'echarts/components';
+import { use } from 'echarts/core';
+import { CanvasRenderer } from 'echarts/renderers';
+
+export default function installEcharts() {
+ use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent]);
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f300656e..c8998f70 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -213,6 +213,9 @@ importers:
vue-count-to:
specifier: 1.0.13
version: 1.0.13
+ vue-echarts:
+ specifier: ^6.7.3
+ version: 6.7.3(@vue/runtime-core@3.5.25)(echarts@5.4.3)(vue@3.5.25(typescript@5.9.3))
vue-i18n:
specifier: '9'
version: 9.14.5(vue@3.5.25(typescript@5.9.3))
@@ -5929,6 +5932,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+ resize-detector@0.3.0:
+ resolution: {integrity: sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==}
+
resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}
@@ -6930,6 +6936,17 @@ packages:
vue-count-to@1.0.13:
resolution: {integrity: sha512-6R4OVBVNtQTlcbXu6SJ8ENR35M2/CdWt3Jmv57jOUM+1ojiFmjVGvZPH8DfHpMDSA+ITs+EW5V6qthADxeyYOQ==}
+ vue-demi@0.13.11:
+ resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^3.0.0-0 || ^2.6.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
@@ -6941,6 +6958,19 @@ packages:
'@vue/composition-api':
optional: true
+ vue-echarts@6.7.3:
+ resolution: {integrity: sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==}
+ peerDependencies:
+ '@vue/composition-api': ^1.0.5
+ '@vue/runtime-core': ^3.0.0
+ echarts: ^5.4.1
+ vue: ^2.6.12 || ^3.1.1
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ '@vue/runtime-core':
+ optional: true
+
vue-eslint-parser@8.3.0:
resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -13942,6 +13972,8 @@ snapshots:
requires-port@1.0.0: {}
+ resize-detector@0.3.0: {}
+
resolve-cwd@3.0.0:
dependencies:
resolve-from: 5.0.0
@@ -15091,10 +15123,23 @@ snapshots:
vue-count-to@1.0.13: {}
+ vue-demi@0.13.11(vue@3.5.25(typescript@5.9.3)):
+ dependencies:
+ vue: 3.5.25(typescript@5.9.3)
+
vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)):
dependencies:
vue: 3.5.25(typescript@5.9.3)
+ vue-echarts@6.7.3(@vue/runtime-core@3.5.25)(echarts@5.4.3)(vue@3.5.25(typescript@5.9.3)):
+ dependencies:
+ echarts: 5.4.3
+ resize-detector: 0.3.0
+ vue: 3.5.25(typescript@5.9.3)
+ vue-demi: 0.13.11(vue@3.5.25(typescript@5.9.3))
+ optionalDependencies:
+ '@vue/runtime-core': 3.5.25
+
vue-eslint-parser@8.3.0(eslint@8.56.0):
dependencies:
debug: 4.4.3
diff --git a/server/api/v1/container.proto b/server/api/v1/container.proto
index 8445f188..04d97d73 100644
--- a/server/api/v1/container.proto
+++ b/server/api/v1/container.proto
@@ -66,6 +66,7 @@ message ContainerReply {
string priority = 16;
string namespace = 17;
repeated string device_ids = 18;
+ repeated string images = 19;
}
message ContainersReply {
diff --git a/server/internal/biz/pod.go b/server/internal/biz/pod.go
index 15aa6c52..4162db75 100644
--- a/server/internal/biz/pod.go
+++ b/server/internal/biz/pod.go
@@ -15,6 +15,7 @@ type Container struct {
NodeName string
PodUID string
PodName string
+ Image string
ContainerDevices ContainerDevices
Status string
CreateTime time.Time
diff --git a/server/internal/data/pod.go b/server/internal/data/pod.go
index bfc71e8c..1cb2db78 100644
--- a/server/internal/data/pod.go
+++ b/server/internal/data/pod.go
@@ -140,6 +140,7 @@ func (r *podRepo) fetchContainerInfo(pod *corev1.Pod) []*biz.Container {
NodeName: pod.Spec.NodeName,
PodName: pod.Name,
PodUID: string(pod.UID),
+ Image: ctr.Image,
Status: containerStat[ctr.Name],
NodeUID: r.GetNodeUUID(pod),
Namespace: pod.Namespace,
diff --git a/server/internal/service/container.go b/server/internal/service/container.go
index 4bde7f2a..c108586d 100644
--- a/server/internal/service/container.go
+++ b/server/internal/service/container.go
@@ -27,6 +27,23 @@ func NewContainerService(node *biz.NodeUsecase, pod *biz.PodUseCase) *ContainerS
return &ContainerService{node: node, pod: pod}
}
+func uniqueNonEmpty(values []string) []string {
+ seen := make(map[string]struct{}, len(values))
+ result := make([]string, 0, len(values))
+ for _, value := range values {
+ v := strings.TrimSpace(value)
+ if v == "" {
+ continue
+ }
+ if _, ok := seen[v]; ok {
+ continue
+ }
+ seen[v] = struct{}{}
+ result = append(result, v)
+ }
+ return result
+}
+
func (s *ContainerService) GetAllContainers(ctx context.Context, req *pb.GetAllContainersReq) (*pb.ContainersReply, error) {
filters := req.Filters
containers, err := s.pod.ListAllContainers(ctx)
@@ -58,6 +75,7 @@ func (s *ContainerService) GetAllContainers(ctx context.Context, req *pb.GetAllC
containerReply.Name = container.Name
containerReply.Status = container.Status
containerReply.AppName = container.PodName
+ containerReply.Images = uniqueNonEmpty([]string{container.Image})
containerReply.NodeName = container.NodeName
containerReply.PodUid = container.PodUID
containerReply.NodeUid = container.NodeUID
@@ -109,6 +127,18 @@ func (s *ContainerService) GetContainer(ctx context.Context, req *pb.GetContaine
ctrReply.NodeUid = container.NodeUID
ctrReply.Namespace = container.Namespace
ctrReply.Priority = container.Priority
+ allContainers, err := s.pod.ListAllContainers(ctx)
+ if err == nil {
+ images := make([]string, 0)
+ for _, item := range allContainers {
+ if item.PodUID == container.PodUID {
+ images = append(images, item.Image)
+ }
+ }
+ ctrReply.Images = uniqueNonEmpty(images)
+ } else {
+ ctrReply.Images = uniqueNonEmpty([]string{container.Image})
+ }
for _, containerDevice := range container.ContainerDevices {
if req.DeviceId != "" && req.DeviceId != containerDevice.UUID {
continue