feat(gpu-service): refine instance type card layout in create-instance drawer

- Regroup meta into aligned rows (CPU/RAM/Memory, Arch/Max/Sliced) and drop the OS row
- Use aggregated endpoint for instance types
- Share formatManufacturer helper for consistent vendor casing
This commit is contained in:
jialin
2026-07-22 15:52:17 +08:00
parent 9386f0872b
commit 15f457a0e1
3 changed files with 88 additions and 104 deletions
@@ -12,7 +12,7 @@ import {
export const GPU_SERVICE_INSTANCES_API = '/gpu-instances'; export const GPU_SERVICE_INSTANCES_API = '/gpu-instances';
export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types'; export const GPU_SERVICE_INSTANCES_TYPE_API = '/gpu-instance-types/aggregated';
// View logs / events still go through the K8s proxy until the /v2 // View logs / events still go through the K8s proxy until the /v2
// /gpu-instances API exposes equivalents. clusterID and namespace come // /gpu-instances API exposes equivalents. clusterID and namespace come
@@ -5,11 +5,10 @@ import { Flex, Tag } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import styled from 'styled-components'; import styled from 'styled-components';
import { manufactureColorMap } from '../../templates/config'; import { manufactureColorMap } from '../../templates/config';
import { formatManufacturer } from '../../utils';
import { formatMemoryDisplay } from '../config'; import { formatMemoryDisplay } from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types'; import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
const Vendors = ['intel'] as const;
const Title = styled.div` const Title = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -120,122 +119,95 @@ function getInstanceDerived(item: InstanceTypeItemModel) {
ramUnit: spec.unitResourcesParsed?.ram?.value, ramUnit: spec.unitResourcesParsed?.ram?.value,
os: _.capitalize(spec.os) || '', os: _.capitalize(spec.os) || '',
arch: spec.arch, arch: spec.arch,
cpuManufacturer: Vendors.includes(cpuManufacturer as any) cpuManufacturer: formatManufacturer(cpuManufacturer),
? _.capitalize(cpuManufacturer)
: _.toUpper(cpuManufacturer),
cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores
}; };
} }
type MetaEntry = { icon: string; label?: string; value: React.ReactNode };
// All rows share a single grid so columns — and therefore icons — line up
// vertically. Each item is 3 cells (icon/label/value); every item past the
// first adds a leading dot cell, so a row of k items spans 4k-1 cells. A short
// row is padded with a spanning spacer so the next row restarts at column 1.
const renderMetaRow = (items: MetaEntry[], columns: number, rowKey: string) => {
const cells = items.map((item, index) => (
<MetaItem
key={`${rowKey}-${item.icon}`}
showDot={index > 0}
icon={item.icon}
label={item.label}
value={item.value}
/>
));
const remaining = columns - (4 * items.length - 1);
if (remaining > 0) {
cells.push(
<span
key={`${rowKey}-spacer`}
style={{ gridColumn: `span ${remaining}` }}
/>
);
}
return cells;
};
export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({ export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({
spec, spec,
slicedMaxPercentage slicedMaxPercentage
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { ramUnit, cpuUnitCores, isGPU, os, arch } = getInstanceDerived({ const { ramUnit, cpuUnitCores, isGPU, arch } = getInstanceDerived({
spec spec
} as InstanceTypeItemModel); } as InstanceTypeItemModel);
// Sliceable types get a dedicated "Sliceable {n}%" row (on its own grid row) // Sliceable types append a "Sliceable {n}%" cell to the second row.
// rather than crowding it into the Max cell.
const showSliceable = !!spec.sliceable && (slicedMaxPercentage ?? 0) > 0; const showSliceable = !!spec.sliceable && (slicedMaxPercentage ?? 0) > 0;
return ( const cpuItem: MetaEntry = {
<Meta $columns={isGPU ? 11 : 7}> icon: 'icon-cpu',
{isGPU && ( label: 'CPU',
<> value: cpuUnitCores || '-'
{/* row 1: Memory | Max | RAM */} };
<MetaItem const ramItem: MetaEntry = {
show={isGPU} icon: 'icon-ram-02',
showDot={false} label: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
icon="icon-gpu1" value: ramUnit ? `${ramUnit} GB` : '-'
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })} };
value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'} const memoryItem: MetaEntry = {
/> icon: 'icon-gpu1',
<MetaItem label: intl.formatMessage({ id: 'gpuservice.instance.memory' }),
showDot={true} value: formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'
icon="icon-ram-02" };
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })} const archItem: MetaEntry = {
value={ramUnit ? `${ramUnit} GB` : '-'} icon: 'icon-cube',
/> label: intl.formatMessage({ id: 'gpuservice.instance.arch' }),
<MetaItem value: _.toUpper(arch) || '-'
icon="icon-database" };
label={intl.formatMessage( const maxItem: MetaEntry = {
{ icon: 'icon-database',
id: 'common.max' label: intl.formatMessage({ id: 'common.max' }, { count: '' }),
}, value: `${spec.maxComputeUnitCount || 0}`
{ count: '' } };
)} const slicedItem: MetaEntry = {
value={`${spec.maxComputeUnitCount || 0}`} icon: 'icon-sliced',
/> label: intl.formatMessage({ id: 'gpuservice.instance.sliceable' }),
{/* row 2: OS | Arch | CPU */} value: `${slicedMaxPercentage}%`
<MetaItem };
showDot={false}
icon="icon-server02"
label={intl.formatMessage({ id: 'gpuservice.instance.os' })}
value={os || '-'}
/>
<MetaItem
icon="icon-cube"
label={intl.formatMessage({ id: 'gpuservice.instance.arch' })}
value={_.toUpper(arch) || '-'}
/>
<MetaItem
show={isGPU}
showDot={true}
icon="icon-cpu"
label="CPU"
value={
<Flex gap={4} align="center">
<span>{cpuUnitCores || '-'}</span>
</Flex>
}
/>
{/* row 3 (sliceable types only): Sliceable {n}% on its own row */}
<MetaItem
show={showSliceable}
showDot={false}
icon="icon-sliced"
label={intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
value={`${slicedMaxPercentage}%`}
/>
</>
)}
{!isGPU && ( // GPU: 3 items/row → 11 cols. CPU: 2 items/row → 7 cols.
<> const columns = isGPU ? 11 : 7;
{/* row 1: RAM | Max */} const rows: MetaEntry[][] = isGPU
<MetaItem ? [
showDot={false} [ramItem, memoryItem, cpuItem],
icon="icon-ram-02" showSliceable ? [archItem, maxItem, slicedItem] : [archItem, maxItem]
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })} ]
value={ramUnit ? `${ramUnit} GB` : '-'} : [[ramItem], [archItem, maxItem]];
/>
<MetaItem return (
icon="icon-database" <Meta $columns={columns}>
label={intl.formatMessage( {rows.map((row, index) => renderMetaRow(row, columns, `row-${index}`))}
{
id: 'common.max'
},
{ count: '' }
)}
value={`${spec.maxComputeUnitCount || 0}`}
/>
{/* row 2: OS | Arch */}
<MetaItem
showDot={false}
icon="icon-server02"
label={intl.formatMessage({ id: 'gpuservice.instance.os' })}
value={os || '-'}
/>
<MetaItem
icon="icon-cube"
label={intl.formatMessage({ id: 'gpuservice.instance.arch' })}
value={_.toUpper(arch) || '-'}
/>
</>
)}
</Meta> </Meta>
); );
}; };
@@ -276,7 +248,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
disabled={false} disabled={false}
style={{ fontWeight: 400 }} style={{ fontWeight: 400 }}
> >
{manufacturer?.toUpperCase()} {formatManufacturer(manufacturer)}
</ThemeTag> </ThemeTag>
)} )}
{showCpuManufacturerTag && ( {showCpuManufacturerTag && (
+12
View File
@@ -1,5 +1,17 @@
import _ from 'lodash'; import _ from 'lodash';
// Manufacturer display: most vendors read best all-caps (NVIDIA, AMD), but a
// few read better capitalized (Intel). Shared by the instance-type list and the
// create-instance drawer so both render the vendor identically.
const CapitalizedVendors = ['intel'];
export const formatManufacturer = (manufacturer?: string | null): string => {
if (!manufacturer) return '';
return CapitalizedVendors.includes(manufacturer.toLowerCase())
? _.capitalize(manufacturer)
: _.toUpper(manufacturer);
};
export const omitPathParams = <T extends Record<string, any>>( export const omitPathParams = <T extends Record<string, any>>(
params: T params: T
): Omit<T, 'namespace' | 'clusterID'> => { ): Omit<T, 'namespace' | 'clusterID'> => {