fix: keep uniform for instance type render content

This commit is contained in:
jialin
2026-06-12 22:20:31 +08:00
committed by jialin
parent d5d0749b72
commit 48159171c2
5 changed files with 277 additions and 257 deletions
@@ -112,7 +112,7 @@ function getInstanceDerived(item: InstanceTypeItemModel) {
acceleratable,
isGPU: acceleratable,
manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types
displayName: spec.product || item.name,
displayName: acceleratable ? spec.product || item.name : 'CPU Only',
ramUnit: spec.unitResourcesParsed?.ram?.value,
os: _.capitalize(spec.os) || '',
arch: spec.arch,
@@ -13,15 +13,9 @@ import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import _ from 'lodash';
import { Fragment, useMemo } from 'react';
import { parseJsonSafe } from '../../utils';
import InstanceTypeCell from '../components/instance-type-cell';
import {
formatMemoryDisplay,
InstanceStatusLabelMap,
rowActionList,
status
} from '../config';
import { InstanceTypeSpec, ListItem } from '../config/types';
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
import { ListItem } from '../config/types';
import { renderInstanceType } from '../utils/render-instance-type';
const buildRowActions = (record: ListItem) => {
return rowActionList
@@ -37,71 +31,6 @@ const buildRowActions = (record: ListItem) => {
}));
};
const toGB = (v?: string | number) =>
v ? `${v}`.replace('Gi', ' GB').replace('Mi', ' MB') : '-';
const buildResourcesData = (
instanceType: {
spec: InstanceTypeSpec;
},
options: {
count: number;
}
) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options;
if (acceleratable) {
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: undefined,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: undefined
};
}
return {};
};
const formatResources = (
instanceTypeSpec: { spec: InstanceTypeSpec },
record: ListItem
) => {
const resources = buildResourcesData(instanceTypeSpec, {
count: _.toNumber(record.spec?.resources?.accelerator) || 0
});
if (!record.spec?.resources?.accelerator) {
return {
cpu: record.spec?.resources?.cpu
? `${record.spec?.resources?.cpu} vCPU`
: '-',
ram: record.spec?.resources?.ram
? toGB(record.spec?.resources?.ram)
: '-',
localStorage: record.spec?.resources?.localStorage
? toGB(record.spec?.resources?.localStorage)
: '-'
};
}
// VRAM = per-card GPU memory (a single card's size; not aggregated across
// cards — the model's marquee spec).
const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
return {
cpu: resources.cpu ? `${resources.cpu} vCPU` : '-',
ram: resources.ram ? `${resources.ram} GB` : '-',
vram,
localStorage: record.spec?.resources?.localStorage
? toGB(record.spec?.resources?.localStorage)
: undefined
};
};
type ConnectEntry =
| {
type: 'ssh';
@@ -175,81 +104,6 @@ const useInstancesColumns = ({
const pluginCols = usePluginListColumns('gpuInstances');
const creatorCols = useCreatorColumn<ListItem>('gpuInstances');
const renderInstanceType = (record: ListItem) => {
const description =
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
const resources = formatResources({ spec: description }, record);
const accelerator = record.spec?.resources?.accelerator;
const title = description.acceleratable
? `${description.product} x ${accelerator}`
: 'CPU';
const volume = (record.spec as any)?.volume;
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
// the Deployments instance info icon: dark tooltip, per-category icon,
// instance name as the title. Rows with no value are dropped.
type Section = {
icon: string;
name: string;
rows: [string | null, string | undefined][];
};
const sections: Section[] = [];
if (description.acceleratable) {
sections.push({
icon: 'icon-gpu',
name: 'GPU',
rows: [
[
intl.formatMessage({ id: 'gpuservice.table.count' }),
accelerator ? `${accelerator}` : undefined
],
[
intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
description.product
],
[
intl.formatMessage({ id: 'gpuservice.instance.memory' }),
resources.vram
]
]
});
}
sections.push({
icon: 'icon-cpu',
name: 'CPU',
rows: [[null, resources.cpu]]
});
sections.push({
icon: 'icon-ram-02',
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
rows: [[null, resources.ram]]
});
sections.push({
icon: 'icon-hard-disk',
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
rows: [
[
intl.formatMessage({ id: 'gpuservice.instance.disk.system' }),
resources.localStorage
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.ephemeral' }),
toGB(volume?.ephemeral?.capacity)
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.persistent' }),
toGB(volume?.persistentTemplate?.spec?.capacity) ||
toGB(pvCapacityByName?.[volume?.persistent?.name]) ||
volume?.persistent?.name
]
]
});
return (
<InstanceTypeCell title={title} name={record.name} sections={sections} />
);
};
return useMemo(() => {
const pluginRendered = pluginCols.map((c) => ({
title: intl.formatMessage({ id: c.titleId }),
@@ -389,7 +243,8 @@ const useInstancesColumns = ({
showTitle: false
},
width: 300,
render: (_text: string, record: ListItem) => renderInstanceType(record)
render: (_text: string, record: ListItem) =>
renderInstanceType(record, { intl, pvCapacityByName })
},
...pluginRendered,
{
@@ -0,0 +1,237 @@
/**
* Canonical "Instance Type" cell renderer.
*
* This is the single source of truth for the Instance Type column: the GPU
* Instances list ([use-instances-columns]) and the Usage GPU Instances table
* both render through it, so the primary label + the categorized spec popover
* (GPU / CPU / Memory / Disk) stay byte-for-byte identical.
*
* It operates on the GPU-service ``ListItem`` shape. Callers whose data has a
* different shape (e.g. the Usage breakdown rows) build a minimal ``ListItem``
* with ``buildInstanceTypeRecordFromMiB`` and feed it here.
*/
import _ from 'lodash';
import { parseJsonSafe } from '../../utils';
import InstanceTypeCell from '../components/instance-type-cell';
import { formatMemoryDisplay } from '../config';
import { InstanceTypeSpec, ListItem } from '../config/types';
// Minimal shape of the ``useIntl()`` result we depend on — keeps this module
// free of an intl package import.
type IntlLike = { formatMessage: (descriptor: { id: string }) => string };
const toGB = (v?: string | number) =>
v ? `${v}`.replace('Gi', ' GB').replace('Mi', ' MB') : '-';
const buildResourcesData = (
instanceType: {
spec: InstanceTypeSpec;
},
options: {
count: number;
}
) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options;
if (acceleratable) {
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: undefined,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: undefined
};
}
return {};
};
const formatResources = (
instanceTypeSpec: { spec: InstanceTypeSpec },
record: ListItem
) => {
const resources = buildResourcesData(instanceTypeSpec, {
count: _.toNumber(record.spec?.resources?.accelerator) || 0
});
if (!record.spec?.resources?.accelerator) {
return {
cpu: record.spec?.resources?.cpu
? `${record.spec?.resources?.cpu} vCPU`
: '-',
ram: record.spec?.resources?.ram
? toGB(record.spec?.resources?.ram)
: '-',
localStorage: record.spec?.resources?.localStorage
? toGB(record.spec?.resources?.localStorage)
: '-'
};
}
// VRAM = per-card GPU memory (a single card's size; not aggregated across
// cards — the model's marquee spec).
const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
return {
cpu: resources.cpu ? `${resources.cpu} vCPU` : '-',
ram: resources.ram ? `${resources.ram} GB` : '-',
vram,
localStorage: record.spec?.resources?.localStorage
? toGB(record.spec?.resources?.localStorage)
: undefined
};
};
export const renderInstanceType = (
record: ListItem,
options: {
intl: IntlLike;
// name → capacity (e.g. "20Gi") for referenced persistent volumes, so the
// Disk → Persistent row can show the size instead of just the PV name.
pvCapacityByName?: Record<string, string>;
}
) => {
const { intl, pvCapacityByName } = options;
const description =
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
const resources = formatResources({ spec: description }, record);
const accelerator = record.spec?.resources?.accelerator;
const title = description.acceleratable
? `${description.product} x ${accelerator}`
: 'CPU Only';
const volume = (record.spec as any)?.volume;
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
// the Deployments instance info icon: dark tooltip, per-category icon,
// instance name as the title. Rows with no value are dropped.
type Section = {
icon: string;
name: string;
rows: [string | null, string | undefined][];
};
const sections: Section[] = [];
if (description.acceleratable) {
sections.push({
icon: 'icon-gpu',
name: 'GPU',
rows: [
[
intl.formatMessage({ id: 'gpuservice.table.count' }),
accelerator ? `${accelerator}` : undefined
],
[
intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
description.product
],
[
intl.formatMessage({ id: 'gpuservice.instance.memory' }),
resources.vram
]
]
});
}
sections.push({
icon: 'icon-cpu',
name: 'CPU',
rows: [[null, resources.cpu]]
});
sections.push({
icon: 'icon-ram-02',
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
rows: [[null, resources.ram]]
});
sections.push({
icon: 'icon-hard-disk',
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
rows: [
[
intl.formatMessage({ id: 'gpuservice.instance.disk.system' }),
resources.localStorage
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.ephemeral' }),
toGB(volume?.ephemeral?.capacity)
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.persistent' }),
toGB(volume?.persistentTemplate?.spec?.capacity) ||
toGB(pvCapacityByName?.[volume?.persistent?.name]) ||
volume?.persistent?.name
]
]
});
return (
<InstanceTypeCell title={title} name={record.name} sections={sections} />
);
};
// MiB → k8s "Gi" quantity string, so values that arrive as raw mebibytes (the
// Usage breakdown carries them this way) render through the same toGB path as
// the GPU Instances list — i.e. as "X GB", not "X MB".
const mibToGiQuantity = (mib?: number): string | undefined =>
mib ? `${Math.round(mib / 1024)}Gi` : undefined;
// Per-card / whole-instance metrics carried by the Usage breakdown rows.
export interface InstanceTypeMiB {
name?: string;
product?: string;
// accelerator (GPU card) count; 0/undefined → CPU-only ("CPU Only").
gpuCount?: number;
// Per-card values.
unitCpuMilli?: number;
unitMemoryMib?: number;
vramMib?: number;
// Disk (whole instance).
localStorageMib?: number;
ephemeralMib?: number;
persistentMib?: number;
}
// Adapt the Usage breakdown's flat MiB fields into the ``ListItem`` shape the
// canonical renderer consumes, so both tables render identically. CPU/RAM ride
// on the parsed unit-resources (per card) for accelerated rows and on
// ``spec.resources`` for CPU-only rows, matching how the list derives them.
export const buildInstanceTypeRecordFromMiB = (
data: InstanceTypeMiB
): ListItem => {
const acceleratable = (data.gpuCount ?? 0) > 0;
return {
name: data.name,
description: JSON.stringify({
spec: {
acceleratable,
product: data.product,
memory: data.vramMib,
unitResourcesParsed: {
cpu: data.unitCpuMilli ? { cores: data.unitCpuMilli / 1000 } : null,
ram: data.unitMemoryMib ? { value: data.unitMemoryMib / 1024 } : null
}
}
}),
spec: {
resources: {
accelerator: data.gpuCount ? `${data.gpuCount}` : null,
// Only the CPU-only branch reads cpu/ram off spec.resources.
cpu: acceleratable
? null
: data.unitCpuMilli
? data.unitCpuMilli / 1000
: null,
ram: acceleratable ? null : mibToGiQuantity(data.unitMemoryMib),
localStorage: mibToGiQuantity(data.localStorageMib) ?? null
},
volume: {
ephemeral: { capacity: mibToGiQuantity(data.ephemeralMib) },
persistentTemplate: data.persistentMib
? {
spec: { type: '', capacity: mibToGiQuantity(data.persistentMib)! }
}
: undefined
}
}
} as ListItem;
};