feat(gpu-service): instance-type spec popover with per-card specs

Replace the raw flavor slug in the GPU Instances list with the product name
("<product> x <count>") plus an info-icon popover that breaks the spec down by
category (GPU / CPU / Memory / Disk): per-card VRAM, whole-instance CPU/RAM,
system / ephemeral / persistent disks (persistent size resolved from the
referenced PV). Extract the cell into a shared InstanceTypeCell and centralize
memory formatting in formatMemoryDisplay so the GPU Instances list and the
Usage tab render identical sizes.
This commit is contained in:
michelia
2026-06-04 19:52:52 +08:00
committed by jialin
parent 4c5d42cb13
commit 50601b0aff
5 changed files with 214 additions and 48 deletions
@@ -0,0 +1,89 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { AutoTooltip, IconFont } from '@gpustack/core-ui';
import { Flex, Tooltip } from 'antd';
import React from 'react';
export type InstanceTypeSection = {
icon: string;
name: string;
// [label, value] — a null label renders a single value with no sub-label;
// rows whose value is falsy or "-" are dropped.
rows: [string | null, string | undefined][];
};
/**
* "Instance Type" cell shared by the GPU Instances list and the Usage GPU
* Instances table: a primary product label (e.g. "NVIDIA-GeForce-RTX-5090-D x
* 1") plus a question/info icon whose dark popover breaks the spec down by
* category (GPU / CPU / Memory / Disk), titled with the instance name.
*/
const InstanceTypeCell: React.FC<{
title: string;
name?: string;
sections: InstanceTypeSection[];
}> = ({ title, name, sections }) => {
const specInfo = (
<div style={{ minWidth: 200 }}>
{name && <div style={{ fontWeight: 600, marginBottom: 8 }}>{name}</div>}
{sections.map((sec) => {
const rows = sec.rows.filter(([, v]) => v && v !== '-');
if (!rows.length) return null;
return (
<div
key={sec.name}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 16,
marginBottom: 6
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
minWidth: 88,
lineHeight: '22px'
}}
>
<IconFont type={sec.icon} />
<span>{sec.name}</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{rows.map(([label, value], i) => (
<div
key={i}
style={{ display: 'flex', gap: 16, lineHeight: '22px' }}
>
<span style={{ opacity: 0.65, minWidth: 96 }}>
{label || ''}
</span>
<span>{value}</span>
</div>
))}
</div>
</div>
);
})}
</div>
);
return (
<Flex align="center" style={{ gap: 6 }}>
<AutoTooltip ghost title={<span>{title}</span>}>
<span className="text-primary">{title}</span>
</AutoTooltip>
<Tooltip
title={specInfo}
styles={{ container: { width: 'max-content', maxWidth: 480 } }}
>
<InfoCircleOutlined
style={{ color: 'var(--ant-color-primary)', cursor: 'pointer' }}
/>
</Tooltip>
</Flex>
);
};
export default InstanceTypeCell;
@@ -3,12 +3,9 @@ import { useIntl } from '@umijs/max';
import { Flex } from 'antd';
import styled from 'styled-components';
import { manufactureColorMap } from '../../templates/config';
import { convertKiToGi } from '../config';
import { formatMemoryDisplay } from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
const toDisplayUnit = (value?: string | null) =>
value ? value.replace(/Gi$/, 'GB').replace(/Ti$/, 'TB') : value;
const Title = styled.div`
display: flex;
align-items: center;
@@ -136,8 +133,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
icon="icon-gpu1"
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
value={
toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ??
'-'
formatMemoryDisplay(specData?.memory ?? undefined) ?? '-'
}
/>
<MetaItem