fix: keep uniform for instance type render content
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -10,7 +10,10 @@
|
||||
* Talks to the new ``/usage/gpu-instances/{meta,breakdown}`` endpoints.
|
||||
*/
|
||||
import useCoolColors from '@/hooks/use-cool-colors';
|
||||
import InstanceTypeCell from '@/pages/gpu-service/instances/components/instance-type-cell';
|
||||
import {
|
||||
buildInstanceTypeRecordFromMiB,
|
||||
renderInstanceType
|
||||
} from '@/pages/gpu-service/instances/utils/render-instance-type';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
import { SimpleCard } from '@gpustack/core-ui';
|
||||
import { useAccess, useIntl } from '@umijs/max';
|
||||
@@ -24,11 +27,7 @@ import {
|
||||
ResourceBreakdownResponse
|
||||
} from '../apis/resource';
|
||||
import useResourceMeta from '../hooks/use-resource-meta';
|
||||
import {
|
||||
instanceTypeLabel,
|
||||
instanceTypeSections,
|
||||
instanceTypeTitle
|
||||
} from '../utils/format-instance-type';
|
||||
import { instanceTypeLabel } from '../utils/format-instance-type';
|
||||
import {
|
||||
bucketKey,
|
||||
generateBucketRange,
|
||||
@@ -315,22 +314,28 @@ const GpuInstancesTab: React.FC = () => {
|
||||
key: 'gpu_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) => instanceTypeLabel(row)
|
||||
};
|
||||
// Instances breakdown: render exactly like the GPU Instances list —
|
||||
// "<product> x <count>" plus the categorized spec popover behind the icon.
|
||||
// Instances breakdown: render through the canonical GPU Instances list
|
||||
// renderer so the label + spec popover are identical. The breakdown row
|
||||
// carries flat MiB fields, so adapt it into the ListItem shape first.
|
||||
const instanceTypeColInstance = {
|
||||
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
||||
dataIndex: 'gpu_type',
|
||||
key: 'gpu_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) => (
|
||||
<InstanceTypeCell
|
||||
title={instanceTypeTitle(row)}
|
||||
name={row.instance_name}
|
||||
sections={instanceTypeSections(row, {
|
||||
vram: intl.formatMessage({ id: 'gpuservice.instance.memory' }),
|
||||
disk: intl.formatMessage({ id: 'gpuservice.instance.disk' })
|
||||
})}
|
||||
/>
|
||||
)
|
||||
render: (_v: string, row: ResourceBreakdownItem) =>
|
||||
renderInstanceType(
|
||||
buildInstanceTypeRecordFromMiB({
|
||||
name: row.instance_name,
|
||||
product: row.product || row.gpu_type,
|
||||
gpuCount: row.gpu_count,
|
||||
unitCpuMilli: row.unit_cpu_milli,
|
||||
unitMemoryMib: row.unit_memory_mib,
|
||||
vramMib: row.vram_mib,
|
||||
localStorageMib: row.local_storage_mib,
|
||||
ephemeralMib: row.ephemeral_mib,
|
||||
persistentMib: row.persistent_mib
|
||||
}),
|
||||
{ intl }
|
||||
)
|
||||
};
|
||||
// Last Active = the last active day. The backend sends a rollup-tz instant
|
||||
// with its offset; parseRollup keeps that wall clock (no browser-tz convert),
|
||||
@@ -511,6 +516,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
rowKey={(row) =>
|
||||
`${row.gpu_type ?? ''}|${row.instance_id ?? ''}|${row.user_id ?? ''}`
|
||||
}
|
||||
key={t.key}
|
||||
dataSource={tableRows}
|
||||
columns={tableColumns as any}
|
||||
onChange={(_pagination, _filters, sorter: any) => {
|
||||
@@ -532,6 +538,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
pagination={{
|
||||
size: 'middle',
|
||||
current: tablePage,
|
||||
pageSize: tableData?.pagination.perPage ?? 50,
|
||||
total: tableData?.pagination.total ?? 0,
|
||||
|
||||
@@ -1,97 +1,18 @@
|
||||
/**
|
||||
* Instance-type display helpers — render the Usage "Instance Type" the same way
|
||||
* the GPU Instances list does: a pretty product name (e.g.
|
||||
* "NVIDIA-GeForce-RTX-5090-D") plus a per-card spec line, instead of the raw
|
||||
* kueue flavor slug.
|
||||
* Instance-type display helper for the Usage tables.
|
||||
*
|
||||
* The product name + per-card specs ride on the breakdown rows via
|
||||
* ``dimensions`` (instance-type / per-instance groupings only); older rows that
|
||||
* predate the enrichment fall back to the flavor slug (``gpu_type``).
|
||||
* The Instances breakdown renders its Instance Type column through the
|
||||
* canonical GPU Instances renderer (``renderInstanceType`` +
|
||||
* ``buildInstanceTypeRecordFromMiB``), so the label + spec popover stay
|
||||
* identical to the GPU Instances list. Only the Instance Types breakdown — a
|
||||
* plain product label with no popover — still uses the helper below.
|
||||
*
|
||||
* The product name rides on the breakdown rows via ``dimensions``; older rows
|
||||
* that predate the enrichment fall back to the flavor slug (``gpu_type``).
|
||||
*/
|
||||
import { InstanceTypeSection } from '@/pages/gpu-service/instances/components/instance-type-cell';
|
||||
import { formatMemoryDisplay } from '@/pages/gpu-service/instances/config';
|
||||
import { ResourceBreakdownItem } from '../apis/resource';
|
||||
|
||||
// Primary label: GPU product name when known, else the flavor slug.
|
||||
export const instanceTypeLabel = (
|
||||
row?: Partial<ResourceBreakdownItem>
|
||||
): string => row?.product || row?.gpu_type || '-';
|
||||
|
||||
// Round to ≤2 decimals, stripping trailing zeros, so fractional CPU allocations
|
||||
// (e.g. 0.5C / 500m) aren't misrounded up to "1C". Memory uses the shared
|
||||
// formatMemoryDisplay so sizes match the GPU Instances list exactly.
|
||||
const fmt = (n: number): number => parseFloat(n.toFixed(2));
|
||||
|
||||
// Secondary spec line "18C · 54GB RAM · 31GB VRAM" (per card). Storage is
|
||||
// intentionally excluded — it's user-customizable and not part of the type.
|
||||
// Empty string when no specs are known (fall back to label only).
|
||||
export const instanceTypeSpecs = (
|
||||
row?: Partial<ResourceBreakdownItem>
|
||||
): string => {
|
||||
if (!row) return '';
|
||||
const parts: string[] = [];
|
||||
if (row.unit_cpu_milli) {
|
||||
parts.push(`${fmt(row.unit_cpu_milli / 1000)}C`);
|
||||
}
|
||||
if (row.unit_memory_mib) {
|
||||
parts.push(`${formatMemoryDisplay(row.unit_memory_mib)} RAM`);
|
||||
}
|
||||
if (row.vram_mib) {
|
||||
parts.push(`${formatMemoryDisplay(row.vram_mib)} VRAM`);
|
||||
}
|
||||
return parts.join(' · ');
|
||||
};
|
||||
|
||||
// Per-instance title for the Instances table: "<product> x <count>", matching
|
||||
// the GPU Instances list (count carried in dimensions per instance).
|
||||
export const instanceTypeTitle = (
|
||||
row?: Partial<ResourceBreakdownItem>
|
||||
): string => {
|
||||
const label = instanceTypeLabel(row);
|
||||
return row?.gpu_count ? `${label} x ${row.gpu_count}` : label;
|
||||
};
|
||||
|
||||
// Spec-popover sections for the Instances table, fed to the shared
|
||||
// InstanceTypeCell so it renders exactly like the GPU Instances list:
|
||||
// GPU (Count / Instance Type / per-card VRAM), CPU + Memory as whole-instance
|
||||
// totals (count × per-card, as the list shows), and the ephemeral data disk.
|
||||
// Empty rows are dropped by the cell. ``labels`` carries the i18n VRAM / Disk
|
||||
// captions so this util stays intl-free.
|
||||
export const instanceTypeSections = (
|
||||
row: Partial<ResourceBreakdownItem> | undefined,
|
||||
labels: { vram: string; disk: string }
|
||||
): InstanceTypeSection[] => {
|
||||
if (!row) return [];
|
||||
const count = row.gpu_count || 0;
|
||||
const cpu =
|
||||
row.unit_cpu_milli && count
|
||||
? `${fmt((row.unit_cpu_milli / 1000) * count)}C`
|
||||
: undefined;
|
||||
// RAM is the whole-instance total (per-card × count), as the list shows.
|
||||
const ram =
|
||||
row.unit_memory_mib && count
|
||||
? formatMemoryDisplay(row.unit_memory_mib * count)
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
icon: 'icon-gpu',
|
||||
name: 'GPU',
|
||||
rows: [
|
||||
['Count', count ? `${count}` : undefined],
|
||||
['Instance Type', row.product],
|
||||
[labels.vram, formatMemoryDisplay(row.vram_mib)]
|
||||
]
|
||||
},
|
||||
{ icon: 'icon-cpu', name: 'CPU', rows: [[null, cpu]] },
|
||||
{ icon: 'icon-ram-02', name: 'Memory', rows: [[null, ram]] },
|
||||
{
|
||||
icon: 'icon-hard-disk',
|
||||
name: labels.disk,
|
||||
rows: [
|
||||
['System', formatMemoryDisplay(row.local_storage_mib)],
|
||||
['Data', formatMemoryDisplay(row.ephemeral_mib)],
|
||||
['Persistent', formatMemoryDisplay(row.persistent_mib)]
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user