From aa47c626ba6b25ed99a5bf51335edeb540d5b9e2 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 17 Jul 2026 23:32:42 +0800 Subject: [PATCH] feat: add GpuUsageCell component Semicircle dual-arc gauge (used/allocated) with hover Popover for GPU memory usage in table cells. --- .../_components/gpu-usage-cell/index.tsx | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/pages/_components/gpu-usage-cell/index.tsx diff --git a/src/pages/_components/gpu-usage-cell/index.tsx b/src/pages/_components/gpu-usage-cell/index.tsx new file mode 100644 index 00000000..a0dc634e --- /dev/null +++ b/src/pages/_components/gpu-usage-cell/index.tsx @@ -0,0 +1,204 @@ +import { convertFileSize } from '@/utils'; +import { useIntl } from '@umijs/max'; +import { Flex, Popover, theme } from 'antd'; +import { createStyles } from 'antd-style'; +import type { GlobalToken } from 'antd/es/theme'; +import React from 'react'; + +export interface GpuMemory { + total: number; // bytes + used: number; // bytes — Used + allocated: number; // bytes — Allocated +} + +export interface GpuUsageItem { + index: number; + label?: string; + memory: GpuMemory; +} + +interface GpuUsageCellProps { + data: GpuUsageItem[]; +} + +// guage external radius: 20, internal radius: 16, stroke width: 2 +const CX = 25; +const CY = 24; +const R_OUT = 20; +const R_IN = 16; +const SW = 2; +const L_OUT = Math.PI * R_OUT; +const L_IN = Math.PI * R_IN; +const arcPath = (r: number) => + `M ${CX - r} ${CY} A ${r} ${r} 0 0 1 ${CX + r} ${CY}`; +const P_OUT = arcPath(R_OUT); +const P_IN = arcPath(R_IN); + +const clamp01 = (n: number) => Math.min(Math.max(n, 0), 1); + +// Used color: green < 75%, yellow < 90%, red >= 90% +const usedColor = (ratio: number, token: GlobalToken) => { + if (ratio >= 0.9) return token.colorError; + if (ratio >= 0.75) return token.colorWarning; + return token.colorSuccess; +}; + +const useStyles = createStyles(({ css, token }) => ({ + row: css` + width: fit-content; + padding: 3px 6px; + border-radius: ${token.borderRadius}px; + cursor: pointer; + transition: background-color 0.15s; + &:hover { + background-color: ${token.colorFillTertiary}; + } + `, + index: css` + font-size: 12px; + font-weight: 600; + color: ${token.colorTextSecondary}; + `, + popTitle: css` + font-size: 12px; + font-weight: 600; + color: ${token.colorText}; + margin-bottom: 6px; + `, + popList: css` + font-size: 12px; + font-variant-numeric: tabular-nums; + `, + metric: css` + white-space: nowrap; + `, + dot: css` + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + `, + value: css` + min-width: 64px; + text-align: right; + font-weight: 500; + color: ${token.colorText}; + `, + label: css` + color: ${token.colorTextTertiary}; + ` +})); + +const GpuUsageCell: React.FC = ({ data }) => { + const intl = useIntl(); + const { styles } = useStyles(); + const { token } = theme.useToken(); + + const trackColor = token.colorFillSecondary; + const allocColor = token.colorInfo; + + return ( + + {data.map((item) => { + const { total = 0, used = 0, allocated = 0 } = item.memory || {}; + const usedR = total > 0 ? clamp01(used / total) : 0; + const allocR = total > 0 ? clamp01(allocated / total) : 0; + const pct = Math.round(usedR * 100); + const uc = usedColor(usedR, token); + + const content = ( +
+
+ [{item.index}]{item.label ? ` ${item.label}` : ''} ·{' '} + {convertFileSize(total)} +
+ + + + {convertFileSize(used)} + + {intl.formatMessage({ id: 'resources.table.used' })} + + + + + + {convertFileSize(allocated)} + + + {intl.formatMessage({ id: 'resources.table.allocated' })} + + + +
+ ); + + return ( + + + + + + {usedR > 0 && ( + + )} + {allocR > 0 && ( + + )} + + {pct}% + + + [{item.index}] + + + ); + })} +
+ ); +}; + +export default GpuUsageCell;