mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-30 08:05:43 +00:00
feat(dashboard): add system and GPU status views (#14251)
This commit is contained in:
@@ -72,8 +72,14 @@
|
||||
</template>
|
||||
</CardWithHeader>
|
||||
<CardWithHeader :header="$t('commons.table.status')" class="card-interval">
|
||||
<template #header-r>
|
||||
<el-radio-group v-if="hasGPU" v-model="statusView">
|
||||
<el-radio-button value="system">{{ $t('menu.system') }}</el-radio-button>
|
||||
<el-radio-button value="gpu">GPU</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template #body>
|
||||
<SystemStatus ref="statusRef" style="margin-bottom: 33px" />
|
||||
<SystemStatus ref="statusRef" :view="statusView" style="margin-bottom: 33px" />
|
||||
</template>
|
||||
</CardWithHeader>
|
||||
<CardWithHeader
|
||||
@@ -544,6 +550,7 @@ const welcomeOpen = ref();
|
||||
const shadowContainer = ref();
|
||||
|
||||
const chartOption = ref('network');
|
||||
const statusOption = ref(localStorage.getItem('dashboard_status_view') === 'gpu' ? 'gpu' : 'system');
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
let initialSampleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let isInit = ref<boolean>(true);
|
||||
@@ -678,6 +685,16 @@ const currentInfo = ref<Dashboard.CurrentInfo>({
|
||||
|
||||
shotTime: new Date(),
|
||||
});
|
||||
const hasGPU = computed(() =>
|
||||
[currentInfo.value.gpuData, currentInfo.value.npuData, currentInfo.value.xpuData].some((items) => items?.length),
|
||||
);
|
||||
const statusView = computed({
|
||||
get: () => (hasGPU.value && statusOption.value === 'gpu' ? 'gpu' : 'system'),
|
||||
set: (value: 'system' | 'gpu') => {
|
||||
statusOption.value = value;
|
||||
localStorage.setItem('dashboard_status_view', value);
|
||||
},
|
||||
});
|
||||
const currentChartInfo = reactive({
|
||||
ioReadBytes: 0,
|
||||
ioWriteBytes: 0,
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div class="gpu-status">
|
||||
<div class="gpu-summary">
|
||||
<div class="gpu-summary-header">
|
||||
<span class="metric-label">{{ $t('aiTools.gpu.deviceInfo') }}</span>
|
||||
<el-button link type="primary" size="small" @click="$emit('open')">
|
||||
{{ $t('aiTools.gpu.gpu') }}
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="summary-devices">
|
||||
<div class="device-counts">
|
||||
<strong v-for="group in deviceGroups" :key="group.type">
|
||||
{{ group.type }} × {{ group.count }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="model-list">
|
||||
<span v-for="model in modelGroups" :key="model.name">{{ model.name }} × {{ model.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-memory">
|
||||
<span class="metric-label">{{ $t('aiTools.gpu.memoryTotal') }}</span>
|
||||
<strong>{{ formatMemory(totalMemory) }}</strong>
|
||||
<span class="metric-label">{{ $t('home.used') }} {{ formatMemory(usedMemory) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gpu-grid">
|
||||
<article v-for="device in visibleDevices" :key="device.key" class="gpu-card">
|
||||
<div class="device-header">
|
||||
<span class="device-id">{{ device.id }}</span>
|
||||
<el-popover :width="360" trigger="click" popper-class="dashboard-gpu-popover">
|
||||
<el-descriptions :title="device.name" :column="1" size="small">
|
||||
<el-descriptions-item
|
||||
v-for="detail in device.details.filter((item) => isAvailable(item[1]))"
|
||||
:key="detail[0]"
|
||||
:label="detail[0]"
|
||||
>
|
||||
{{ detail[1] }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #reference>
|
||||
<el-button link type="primary" size="small">{{ $t('commons.button.view') }}</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="device-identity">
|
||||
<h3>{{ device.name }}</h3>
|
||||
<span class="memory-capacity">{{ formatMemory(device.memoryTotal) }}</span>
|
||||
</div>
|
||||
<div class="device-metric">
|
||||
<div class="metric-heading">
|
||||
<span class="metric-label">
|
||||
{{ device.type === 'NPU' ? $t('aiTools.gpu.aiCore') : $t('aiTools.gpu.gpuUtil') }}
|
||||
</span>
|
||||
<strong>{{ device.utilization === null ? '—' : `${device.utilization}%` }}</strong>
|
||||
</div>
|
||||
<el-progress :percentage="device.utilization ?? 0" :show-text="false" :stroke-width="6" />
|
||||
</div>
|
||||
<div class="device-metric">
|
||||
<div class="metric-heading">
|
||||
<span class="metric-label">{{ $t('aiTools.gpu.memoryUsed') }}</span>
|
||||
<strong>
|
||||
{{ formatMemory(device.memoryUsed) }}
|
||||
<span class="metric-total">/ {{ formatMemory(device.memoryTotal) }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="device.memoryPercent ?? 0"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="var(--el-color-success)"
|
||||
/>
|
||||
</div>
|
||||
<div class="device-footer">
|
||||
<div>
|
||||
<span class="metric-label">{{ $t('aiTools.gpu.temperature') }}</span>
|
||||
<strong>{{ device.temperature }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">{{ $t('aiTools.gpu.powerUsage') }}</span>
|
||||
<strong>{{ device.power }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-if="devices.length > 4" class="gpu-expand">
|
||||
<el-button link type="primary" @click="showAll = !showAll">
|
||||
{{ $t(showAll ? 'tabs.hide' : 'tabs.more') }}
|
||||
<el-icon>
|
||||
<ArrowUp v-if="showAll" />
|
||||
<ArrowDown v-else />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { ArrowDown, ArrowRight, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
import { computeSizeForDocker } from '@/utils/size';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const props = defineProps<{ current: Dashboard.CurrentInfo }>();
|
||||
defineEmits<{ open: [] }>();
|
||||
const showAll = ref(false);
|
||||
const t = i18n.global.t;
|
||||
|
||||
const isAvailable = (value?: string) =>
|
||||
Boolean(value?.trim() && !/^(N\/A|NA|NOT SUPPORTED|\[NOT SUPPORTED\])$/i.test(value.trim()));
|
||||
|
||||
const memoryBytes = (value?: string): number | null => {
|
||||
const match = value?.trim().match(/^(\d+(?:\.\d+)?)\s*(B|bytes?|[KMGT]i?B)?$/i);
|
||||
if (!match) return null;
|
||||
const units: Record<string, number> = {
|
||||
'': 1024 ** 2,
|
||||
b: 1,
|
||||
byte: 1,
|
||||
bytes: 1,
|
||||
kb: 1000,
|
||||
kib: 1024,
|
||||
mb: 1000 ** 2,
|
||||
mib: 1024 ** 2,
|
||||
gb: 1000 ** 3,
|
||||
gib: 1024 ** 3,
|
||||
tb: 1000 ** 4,
|
||||
tib: 1024 ** 4,
|
||||
};
|
||||
const bytes = Number(match[1]) * units[(match[2] || '').toLowerCase()];
|
||||
return Number.isFinite(bytes) ? bytes : null;
|
||||
};
|
||||
|
||||
const formatMemory = (value: number | null) => (value === null ? '—' : computeSizeForDocker(value));
|
||||
|
||||
const devices = computed(() =>
|
||||
[
|
||||
...(props.current.gpuData || []).map((item) => ({
|
||||
key: `gpu-${item.type}-${item.busID || item.index}`,
|
||||
type: 'GPU',
|
||||
card: `${item.type}-${item.busID || item.index}`,
|
||||
id: `GPU ${item.index}`,
|
||||
name: item.productName || 'GPU',
|
||||
util: item.gpuUtil,
|
||||
used: item.memUsed || item.memoryUsage?.split('/')[0],
|
||||
total: item.memTotal || item.memoryUsage?.split('/')[1],
|
||||
temperature: item.temperature,
|
||||
power: item.powerUsage || item.powerDraw,
|
||||
details: [
|
||||
[t('aiTools.gpu.busID'), item.busID],
|
||||
[t('aiTools.gpu.fanSpeed'), item.fanSpeed],
|
||||
[t('aiTools.gpu.performanceState'), item.performanceState],
|
||||
[t('aiTools.gpu.powerUsage'), item.powerUsage || item.powerDraw],
|
||||
],
|
||||
})),
|
||||
...(props.current.npuData || []).map((item) => ({
|
||||
key: `npu-${item.npuIndex}-${item.chipIndex}`,
|
||||
type: 'NPU',
|
||||
card: item.npuIndex,
|
||||
id: `NPU ${item.npuIndex} · Chip ${item.chipIndex}`,
|
||||
name: item.productName || 'NPU',
|
||||
util: item.aiCore,
|
||||
used: item.memUsed,
|
||||
total: item.memTotal,
|
||||
temperature: item.temperature,
|
||||
power: item.powerDraw,
|
||||
details: [
|
||||
[t('aiTools.gpu.busID'), item.busID],
|
||||
[t('commons.table.status'), item.health],
|
||||
[t('aiTools.gpu.ddrUsage'), `${item.memoryUsed || '—'} / ${item.memoryTotal || '—'}`],
|
||||
[t('aiTools.gpu.hbmUsage'), `${item.hbmUsed || '—'} / ${item.hbmTotal || '—'}`],
|
||||
[t('aiTools.gpu.hugepagesUsage'), `${item.hugepagesUsed || '—'} / ${item.hugepagesTotal || '—'}`],
|
||||
],
|
||||
})),
|
||||
...(props.current.xpuData || []).map((item) => ({
|
||||
key: `xpu-${item.deviceID}`,
|
||||
type: 'XPU',
|
||||
card: item.deviceID,
|
||||
id: `XPU ${item.deviceID}`,
|
||||
name: item.deviceName || 'XPU',
|
||||
util: item.gpuUtil,
|
||||
used: item.memoryUsed,
|
||||
total: item.memory,
|
||||
temperature: item.temperature,
|
||||
power: item.power,
|
||||
details: [[t('aiTools.gpu.busID'), item.pciBdfAddress]],
|
||||
})),
|
||||
].map((item) => {
|
||||
const memoryUsed = memoryBytes(item.used);
|
||||
const memoryTotal = memoryBytes(item.total);
|
||||
const utilization = item.util?.trim().match(/^(\d+(?:\.\d+)?)\s*%?$/);
|
||||
return {
|
||||
...item,
|
||||
memoryUsed,
|
||||
memoryTotal,
|
||||
utilization: utilization ? Math.min(100, Number(utilization[1])) : null,
|
||||
memoryPercent:
|
||||
memoryUsed !== null && memoryTotal !== null && memoryTotal > 0
|
||||
? Math.min(100, Number(((memoryUsed / memoryTotal) * 100).toFixed(1)))
|
||||
: null,
|
||||
temperature: isAvailable(item.temperature) ? item.temperature.replace(/\s*°?C\b/, ' °C') : '—',
|
||||
power: isAvailable(item.power) ? item.power : '—',
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const visibleDevices = computed(() => (showAll.value ? devices.value : devices.value.slice(0, 4)));
|
||||
const deviceGroups = computed(() =>
|
||||
['GPU', 'NPU', 'XPU']
|
||||
.map((type) => ({
|
||||
type,
|
||||
count: new Set(devices.value.filter((item) => item.type === type).map((item) => item.card)).size,
|
||||
}))
|
||||
.filter((item) => item.count > 0),
|
||||
);
|
||||
const modelGroups = computed(() => {
|
||||
const models = new Map<string, Set<string>>();
|
||||
for (const device of devices.value) {
|
||||
if (!models.has(device.name)) models.set(device.name, new Set());
|
||||
models.get(device.name)!.add(`${device.type}-${device.card}`);
|
||||
}
|
||||
return Array.from(models, ([name, cards]) => ({ name, count: cards.size }));
|
||||
});
|
||||
const totalMemory = computed(() =>
|
||||
devices.value.some((item) => item.memoryTotal === null)
|
||||
? null
|
||||
: devices.value.reduce((sum, item) => sum + item.memoryTotal!, 0),
|
||||
);
|
||||
const usedMemory = computed(() =>
|
||||
devices.value.some((item) => item.memoryUsed === null)
|
||||
? null
|
||||
: devices.value.reduce((sum, item) => sum + item.memoryUsed!, 0),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gpu-status {
|
||||
container-type: inline-size;
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.gpu-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 12px 24px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.gpu-summary-header {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.metric-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.summary-devices {
|
||||
flex: 1 1 200px;
|
||||
min-width: 0;
|
||||
}
|
||||
.device-counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 20px;
|
||||
margin: 8px 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
.model-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.summary-memory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.summary-memory > strong {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.gpu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.gpu-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
min-width: 0;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.device-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.device-id {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.device-identity {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
margin: 10px 0 20px;
|
||||
}
|
||||
.device-identity h3 {
|
||||
flex: 1 1 150px;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.memory-capacity {
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.device-metric + .device-metric {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.metric-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.metric-heading strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.metric-total {
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.device-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 20px;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
.device-footer > div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.device-footer strong {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gpu-expand {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
@container (max-width: 560px) {
|
||||
.gpu-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.gpu-summary {
|
||||
padding: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.summary-memory {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.dashboard-gpu-popover.el-popover {
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: calc(100dvh - 24px);
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="custom-row">
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center">
|
||||
<el-col v-if="view === 'system'" :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
@@ -52,7 +52,7 @@
|
||||
</el-popover>
|
||||
<span class="input-help">{{ loadStatus(currentInfo.loadUsagePercent) }}</span>
|
||||
</el-col>
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3">
|
||||
<el-col v-if="view === 'system'" :xs="6" :sm="6" :md="3" :lg="3" :xl="3">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
@@ -160,7 +160,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center">
|
||||
<el-col v-if="view === 'system'" :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
@@ -254,7 +254,15 @@
|
||||
</span>
|
||||
</el-col>
|
||||
<template v-for="(item, index) of currentInfo.diskData" :key="index">
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('disk', index)">
|
||||
<el-col
|
||||
:xs="6"
|
||||
:sm="6"
|
||||
:md="3"
|
||||
:lg="3"
|
||||
:xl="3"
|
||||
align="center"
|
||||
v-if="view === 'system' && (showMore || index < (currentInfo.diskData.length > 5 ? 4 : 5))"
|
||||
>
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
@@ -310,157 +318,16 @@
|
||||
<span class="input-help">{{ computeSize(item.used) }} / {{ computeSize(item.total) }}</span>
|
||||
</el-col>
|
||||
</template>
|
||||
<template v-for="(item, index) of currentInfo.gpuData" :key="index">
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('gpu', index)">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
:width="450"
|
||||
popper-class="dashboard-status-popover"
|
||||
v-if="chartsOption[`gpu${index}`]"
|
||||
>
|
||||
<el-descriptions :title="item.productName" direction="vertical" :column="3" size="small">
|
||||
<el-descriptions-item :label="$t('aiTools.gpu.gpuUtil')">
|
||||
{{ item.gpuUtil }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('aiTools.gpu.temperature')">
|
||||
{{ formatDashboardTemperature(item.temperature) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('aiTools.gpu.memoryUsage')">
|
||||
{{ item.memoryUsage }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.busID)" :label="$t('aiTools.gpu.busID')">
|
||||
{{ item.busID }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.fanSpeed)" :label="$t('aiTools.gpu.fanSpeed')">
|
||||
{{ item.fanSpeed }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
v-if="hasField(item.performanceState)"
|
||||
:label="$t('aiTools.gpu.performanceState')"
|
||||
>
|
||||
{{ item.performanceState }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.powerDraw)" :label="$t('aiTools.gpu.powerUsage')">
|
||||
{{ item.powerUsage }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #reference>
|
||||
<v-charts
|
||||
@click="goGPU()"
|
||||
height="160px"
|
||||
:id="`gpu${index}`"
|
||||
type="pie"
|
||||
:option="chartsOption[`gpu${index}`]"
|
||||
v-if="chartsOption[`gpu${index}`]"
|
||||
/>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-tooltip :content="item.productName" v-if="item.productName.length > 25">
|
||||
<span class="input-help">{{ item.productName.substring(0, 22) }}...</span>
|
||||
</el-tooltip>
|
||||
<span class="input-help" v-else>{{ item.productName }}</span>
|
||||
</el-col>
|
||||
</template>
|
||||
<template v-for="(item, index) of currentInfo.npuData" :key="index">
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('npu', index)">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
:width="450"
|
||||
popper-class="dashboard-status-popover"
|
||||
v-if="chartsOption[`npu${index}`]"
|
||||
>
|
||||
<el-descriptions :title="item.productName" direction="vertical" :column="3" size="small">
|
||||
<el-descriptions-item v-if="hasField(item.aiCore)" label="AICore(%)">
|
||||
{{ item.aiCore }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.temperature)" :label="$t('aiTools.gpu.temperature')">
|
||||
{{ formatDashboardTemperature(item.temperature) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
v-if="hasMetricPair(item.memUsed, item.memTotal)"
|
||||
:label="$t('aiTools.gpu.memoryUsage')"
|
||||
>
|
||||
{{ formatMetricPair(item.memUsed, item.memTotal) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.powerDraw)" :label="$t('aiTools.gpu.powerUsage')">
|
||||
{{ item.powerDraw }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item
|
||||
v-if="hasMetricPair(item.hugepagesUsed, item.hugepagesTotal)"
|
||||
label="Hugepages-Usage(page)"
|
||||
>
|
||||
{{ formatMetricPair(item.hugepagesUsed, item.hugepagesTotal) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.busID)" :label="$t('aiTools.gpu.busID')">
|
||||
{{ item.busID }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasMetricPair(item.hbmUsed, item.hbmTotal)" label="HBM-Usage">
|
||||
{{ formatMetricPair(item.hbmUsed, item.hbmTotal) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #reference>
|
||||
<v-charts
|
||||
@click="goGPU()"
|
||||
height="160px"
|
||||
:id="`npu${index}`"
|
||||
type="pie"
|
||||
:option="chartsOption[`npu${index}`]"
|
||||
v-if="chartsOption[`npu${index}`]"
|
||||
/>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-tooltip :content="item.productName" v-if="item.productName.length > 25">
|
||||
<span class="input-help">{{ item.productName.substring(0, 22) }}...</span>
|
||||
</el-tooltip>
|
||||
<span class="input-help" v-else>{{ item.productName }}</span>
|
||||
</el-col>
|
||||
</template>
|
||||
<template v-for="(item, index) of currentInfo.xpuData" :key="index">
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="isShow('xpu', index)">
|
||||
<el-popover
|
||||
:hide-after="20"
|
||||
:teleported="false"
|
||||
:width="400"
|
||||
popper-class="dashboard-status-popover"
|
||||
v-if="chartsOption[`xpu${index}`]"
|
||||
>
|
||||
<el-descriptions :title="item.deviceName" direction="vertical" :column="3" size="small">
|
||||
<el-descriptions-item v-if="hasField(item.gpuUtil)" :label="$t('aiTools.gpu.gpuUtil')">
|
||||
{{ item.gpuUtil }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.temperature)" :label="$t('aiTools.gpu.temperature')">
|
||||
{{ item.temperature }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('aiTools.gpu.memoryUsage')">
|
||||
{{ item.memoryUsed }}/{{ item.memory }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.pciBdfAddress)" :label="$t('aiTools.gpu.busID')">
|
||||
{{ item.pciBdfAddress }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="hasField(item.power)" :label="$t('aiTools.gpu.powerUsage')">
|
||||
{{ item.power }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #reference>
|
||||
<v-charts
|
||||
@click="goGPU()"
|
||||
height="160px"
|
||||
:id="`xpu${index}`"
|
||||
type="pie"
|
||||
:option="chartsOption[`xpu${index}`]"
|
||||
v-if="chartsOption[`xpu${index}`]"
|
||||
/>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-tooltip :content="item.deviceName" v-if="item.deviceName.length > 25">
|
||||
<span class="input-help">{{ item.deviceName.substring(0, 22) }}...</span>
|
||||
</el-tooltip>
|
||||
<span class="input-help" v-else>{{ item.deviceName }}</span>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col :xs="6" :sm="6" :md="3" :lg="3" :xl="3" align="center" v-if="totalCount > 5">
|
||||
<GPUStatus v-if="view === 'gpu'" :current="currentInfo" @open="routerToName('GPU')" />
|
||||
<el-col
|
||||
:xs="6"
|
||||
:sm="6"
|
||||
:md="3"
|
||||
:lg="3"
|
||||
:xl="3"
|
||||
align="center"
|
||||
v-if="view === 'system' && currentInfo.diskData.length > 5"
|
||||
>
|
||||
<el-button v-if="!showMore" link type="primary" @click="changeShowMore(true)" class="buttonClass">
|
||||
{{ $t('tabs.more') }}
|
||||
<el-icon><Bottom /></el-icon>
|
||||
@@ -478,7 +345,8 @@
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
import { computeSize } from '@/utils/size';
|
||||
import i18n from '@/lang';
|
||||
import { nextTick, onBeforeUnmount, ref } from 'vue';
|
||||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import GPUStatus from './gpu.vue';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { routerToFileWithPath, routerToName } from '@/utils/router';
|
||||
import { stopProcess } from '@/api/modules/process';
|
||||
@@ -486,9 +354,8 @@ import { loadTopCPU, loadTopMem } from '@/api/modules/dashboard';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const hasFinePointer = useMediaQuery('(hover: hover) and (pointer: fine)');
|
||||
const props = defineProps<{ view: 'system' | 'gpu' }>();
|
||||
const showMore = ref(false);
|
||||
const totalCount = ref();
|
||||
|
||||
let cpuPopoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let memPopoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let cpuLoading = false;
|
||||
@@ -606,57 +473,10 @@ const acceptParams = (current: Dashboard.CurrentInfo, base: Dashboard.BaseInfo):
|
||||
data: formatNumber(currentInfo.value.diskData[i].usedPercent),
|
||||
};
|
||||
}
|
||||
currentInfo.value.gpuData = currentInfo.value.gpuData || [];
|
||||
for (let i = 0; i < currentInfo.value.gpuData.length; i++) {
|
||||
chartsOption.value['gpu' + i] = {
|
||||
title: 'GPU-' + currentInfo.value.gpuData[i].index,
|
||||
data: metricPercentage(currentInfo.value.gpuData[i].gpuUtil),
|
||||
};
|
||||
}
|
||||
currentInfo.value.npuData = currentInfo.value.npuData || [];
|
||||
for (let i = 0; i < currentInfo.value.npuData.length; i++) {
|
||||
chartsOption.value['npu' + i] = {
|
||||
title: 'NPU-' + currentInfo.value.npuData[i].npuIndex + '/' + currentInfo.value.npuData[i].chipIndex,
|
||||
data: metricPercentage(currentInfo.value.npuData[i].aiCore),
|
||||
};
|
||||
}
|
||||
currentInfo.value.xpuData = currentInfo.value.xpuData || [];
|
||||
for (let i = 0; i < currentInfo.value.xpuData.length; i++) {
|
||||
chartsOption.value['xpu' + i] = {
|
||||
title: 'XPU-' + currentInfo.value.xpuData[i].deviceID,
|
||||
data: metricPercentage(currentInfo.value.xpuData[i].gpuUtil || currentInfo.value.xpuData[i].memoryUtil),
|
||||
};
|
||||
}
|
||||
totalCount.value =
|
||||
currentInfo.value.diskData.length +
|
||||
currentInfo.value.gpuData.length +
|
||||
currentInfo.value.npuData.length +
|
||||
currentInfo.value.xpuData.length;
|
||||
showMore.value = localStorage.getItem('dashboard_show') === 'more';
|
||||
});
|
||||
};
|
||||
|
||||
const isShow = (val: string, index: number) => {
|
||||
let showCount = totalCount.value < 6 ? 5 : 4;
|
||||
switch (val) {
|
||||
case 'disk':
|
||||
return showMore.value || index < showCount;
|
||||
case 'gpu':
|
||||
let gpuCount = showCount - currentInfo.value.diskData.length;
|
||||
return showMore.value || index < gpuCount;
|
||||
case 'npu':
|
||||
let npuCount = showCount - currentInfo.value.diskData.length - currentInfo.value.gpuData.length;
|
||||
return showMore.value || index < npuCount;
|
||||
case 'xpu':
|
||||
let xpuCount =
|
||||
showCount -
|
||||
currentInfo.value.diskData.length -
|
||||
currentInfo.value.gpuData.length -
|
||||
currentInfo.value.npuData.length;
|
||||
return showMore.value || index < xpuCount;
|
||||
}
|
||||
};
|
||||
|
||||
const changeShowMore = (show: boolean) => {
|
||||
showMore.value = show;
|
||||
localStorage.setItem('dashboard_show', show ? 'more' : 'hide');
|
||||
@@ -690,35 +510,10 @@ function loadStatus(val: number) {
|
||||
return i18n.global.t('home.runJam');
|
||||
}
|
||||
|
||||
const goGPU = () => {
|
||||
routerToName('GPU');
|
||||
};
|
||||
|
||||
function formatNumber(val: number) {
|
||||
return Number(val.toFixed(2));
|
||||
}
|
||||
|
||||
const hasField = (value?: string) => {
|
||||
return typeof value === 'string' && value.trim() !== '';
|
||||
};
|
||||
|
||||
const hasMetricPair = (used?: string, total?: string) => {
|
||||
return hasField(used) || hasField(total);
|
||||
};
|
||||
|
||||
const formatMetricPair = (used?: string, total?: string) => {
|
||||
return `${used || 'N/A'} / ${total || 'N/A'}`;
|
||||
};
|
||||
|
||||
const formatDashboardTemperature = (value: string) => {
|
||||
return value.replace(/\s*°?C\b/, ' °C');
|
||||
};
|
||||
|
||||
const metricPercentage = (value?: string) => {
|
||||
const matched = value?.match(/[0-9]+(?:\.[0-9]+)?/);
|
||||
return matched ? formatNumber(Number.parseFloat(matched[0])) : 0;
|
||||
};
|
||||
|
||||
const normalizeDashboardAccelerators = (current: Dashboard.CurrentInfo) => {
|
||||
const legacyNPUs = (current.gpuData || [])
|
||||
.filter((item) => item.type === 'ascend')
|
||||
@@ -820,6 +615,14 @@ const loadTopMemData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.view,
|
||||
() => {
|
||||
onCpuPopoverHide();
|
||||
onMemPopoverHide();
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cpuPopoverTimer) {
|
||||
clearInterval(Number(cpuPopoverTimer));
|
||||
|
||||
Reference in New Issue
Block a user