From 457d2f2f72b30e24f95ff518c136f3a724a4a0e3 Mon Sep 17 00:00:00 2001 From: michelia Date: Tue, 30 Jun 2026 11:18:33 +0800 Subject: [PATCH] feat(usage): render instance types by actual shape (CPU spec / GPU x cards) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instance Types are grouped by actual shape now, so each row/series is one concrete type. Label them accordingly via a shared instanceTypeSeriesLabel, used by both the table column and the trend legend so they read identically: GPU shows " x ", CPU shows "CPU Only · N vCPU · M GB" (the instance's real size, not the bare "CPU Only" + hover tooltip). The Instances tab also uses the per-instance cpu/mem totals, so a 3c6g instance of a 1c2g flavor reads 3c6g instead of 1c2g. flatten carries cpu_milli/memory_mib/ gpu_count from the breakdown dimensions. --- src/pages/usage/apis/resource.ts | 25 +++++++---- .../tables/use-instances-columns.tsx | 44 ++++++++++++------- src/pages/usage/utils/format-instance-type.ts | 36 +++++++++++++++ 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/pages/usage/apis/resource.ts b/src/pages/usage/apis/resource.ts index 24cc251a..f35a3e0d 100644 --- a/src/pages/usage/apis/resource.ts +++ b/src/pages/usage/apis/resource.ts @@ -11,6 +11,7 @@ * whole-machine SKU model meters runtime, not decomposed components. */ import { request } from '@umijs/max'; +import { instanceTypeSeriesLabel } from '../utils/format-instance-type'; export interface ResourceUsageFilters { creator_ids?: number[]; @@ -73,6 +74,10 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary { unit_cpu_milli?: number; unit_memory_mib?: number; vram_mib?: number; + // Instance totals (requested cpu/ram) — the real size, so CPU instance types + // show "CPU Only · 2 vCPU · 4 GB" instead of just the per-unit spec. + cpu_milli?: number; + memory_mib?: number; // Per-instance rows also carry the card count + ephemeral disk so the // Instances table can render " x " + the spec popover. gpu_count?: number; @@ -202,6 +207,8 @@ interface ServerBreakdownItem { unit_cpu_milli?: number | null; unit_memory_mib?: number | null; vram_mib?: number | null; + cpu_milli?: number | null; + memory_mib?: number | null; gpu_count?: number | null; ephemeral_mib?: number | null; local_storage_mib?: number | null; @@ -313,6 +320,8 @@ function flattenItem( if (dims.unit_memory_mib != null) flat.unit_memory_mib = dims.unit_memory_mib; if (dims.vram_mib != null) flat.vram_mib = dims.vram_mib; + if (dims.cpu_milli != null) flat.cpu_milli = dims.cpu_milli; + if (dims.memory_mib != null) flat.memory_mib = dims.memory_mib; if (dims.gpu_count != null) flat.gpu_count = dims.gpu_count; if (dims.ephemeral_mib != null) flat.ephemeral_mib = dims.ephemeral_mib; if (dims.local_storage_mib != null) @@ -321,14 +330,14 @@ function flattenItem( if (dims.storage_type) flat.storage_type = dims.storage_type; if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib; } - // For an instance-type grouped trend the series label (``group``) defaults to - // the raw flavor slug; prefer the pretty product name so the chart legend - // matches the GPU Instances list (#5700). ``groupBy`` here is the unmapped - // frontend dimension — ``gpu_type`` maps to the backend's ``instance_type`` - // (see GROUP_BY_MAP). Falls back to the slug for legacy rows that predate - // dimension enrichment. - if (groupBy === 'gpu_type' && flat.product) { - flat.group = flat.product; + // Instance-type grouped trend: the series label (``group``) defaults to the + // raw flavor slug. Instance Types are grouped by actual shape, so label each + // series by that shape — " x " / "CPU Only · 3 vCPU · 6 GB" — + // matching the table and keeping every shape a distinct series (#5700). + // ``groupBy`` is the unmapped frontend dimension; the instance-type axis is + // ``gpu_type`` (→ backend ``instance_type`` via GROUP_BY_MAP). + if (groupBy === 'gpu_type') { + flat.group = instanceTypeSeriesLabel(flat); } return flat; } diff --git a/src/pages/usage/instances-tab/tables/use-instances-columns.tsx b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx index af22b104..3eeea14c 100644 --- a/src/pages/usage/instances-tab/tables/use-instances-columns.tsx +++ b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx @@ -5,7 +5,10 @@ import { import { useIntl } from '@umijs/max'; import { useMemo } from 'react'; import { ResourceBreakdownItem } from '../../apis/resource'; -import { instanceTypeLabel } from '../../utils/format-instance-type'; +import { + cpuOnlyLabel, + instanceTypeSeriesLabel +} from '../../utils/format-instance-type'; import { parseRollup } from '../../utils/time-buckets'; type GroupKey = 'gpu_type' | 'instance' | 'user'; @@ -44,25 +47,29 @@ const useInstancesColumns = (groupKey: GroupKey) => { title: intl.formatMessage({ id: 'usage.table.instanceType' }), dataIndex: 'gpu_type', key: 'gpu_type', - render: (_v: string, row: ResourceBreakdownItem) => - renderInstanceType( + render: (_v: string, row: ResourceBreakdownItem) => { + const isCpu = !row.gpu_count && !row.vram_mib; + return 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, + // CPU instance types show their real total size (cpu/mem totals); + // GPU keeps per-card specs since the renderer multiplies by the + // card count. + unitCpuMilli: isCpu ? row.cpu_milli : row.unit_cpu_milli, + unitMemoryMib: isCpu ? row.memory_mib : row.unit_memory_mib, vramMib: row.vram_mib }), { intl, categories: ['cpu', 'ram'], - title: - !!row.gpu_count || !!row.vram_mib - ? instanceTypeLabel(row) - : 'CPU Only' + // Each row is one shape: GPU " x ", CPU + // "CPU Only · ". + title: instanceTypeSeriesLabel(row) } - ) + ); + } }; // Instances breakdown: render through the canonical GPU Instances list // renderer so the label + spec popover are identical. The breakdown row @@ -71,21 +78,26 @@ const useInstancesColumns = (groupKey: GroupKey) => { title: intl.formatMessage({ id: 'usage.table.instanceType' }), dataIndex: 'gpu_type', key: 'gpu_type', - render: (_v: string, row: ResourceBreakdownItem) => - renderInstanceType( + render: (_v: string, row: ResourceBreakdownItem) => { + const isCpu = !row.gpu_count && !row.vram_mib; + return 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, + // A per-instance row is one concrete instance, so CPU shows its + // real requested size (cpu/mem totals), not the per-unit flavor + // spec — e.g. a 3c6g instance of a 1c2g flavor reads "3 vCPU · 6 GB". + unitCpuMilli: isCpu ? row.cpu_milli : row.unit_cpu_milli, + unitMemoryMib: isCpu ? row.memory_mib : row.unit_memory_mib, vramMib: row.vram_mib, localStorageMib: row.local_storage_mib, ephemeralMib: row.ephemeral_mib, persistentMib: row.persistent_mib }), - { intl } - ) + { intl, title: isCpu ? cpuOnlyLabel(row) : undefined } + ); + } }; // 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), diff --git a/src/pages/usage/utils/format-instance-type.ts b/src/pages/usage/utils/format-instance-type.ts index 42523f92..6a55d99f 100644 --- a/src/pages/usage/utils/format-instance-type.ts +++ b/src/pages/usage/utils/format-instance-type.ts @@ -16,3 +16,39 @@ import { ResourceBreakdownItem } from '../apis/resource'; export const instanceTypeLabel = ( row?: Partial ): string => row?.product || row?.gpu_type || '-'; + +const _trim = (n: number): string => + Number.isInteger(n) ? `${n}` : n.toFixed(1); + +// Compact CPU/RAM spec, e.g. "2 vCPU · 4 GB", from the instance totals +// (millicores / MiB). Empty string when neither is known. +export const formatCpuSpec = ( + cpuMilli?: number | null, + memMib?: number | null +): string => { + const parts: string[] = []; + if (cpuMilli) parts.push(`${_trim(cpuMilli / 1000)} vCPU`); + if (memMib) parts.push(`${_trim(memMib / 1024)} GB`); + return parts.join(' · '); +}; + +// CPU instance-type label: "CPU Only" plus its real size when known, e.g. +// "CPU Only · 2 vCPU · 4 GB". Used by both the table column and the trend +// legend so they read identically. +export const cpuOnlyLabel = (row?: Partial): string => { + const spec = formatCpuSpec(row?.cpu_milli, row?.memory_mib); + return spec ? `CPU Only · ${spec}` : 'CPU Only'; +}; + +// Instance Types are grouped by actual shape, so each row is one concrete +// type: a GPU shows " x ", a CPU shows "CPU Only · ". +// One label for the table column and the trend legend so they read the same +// and each shape is a distinct series. (" x " matches the GPU Instances list.) +export const instanceTypeSeriesLabel = ( + row?: Partial +): string => { + const isCpu = !row?.gpu_count && !row?.vram_mib; + if (isCpu) return cpuOnlyLabel(row); + const product = row?.product || row?.gpu_type || '-'; + return row?.gpu_count ? `${product} x ${row.gpu_count}` : product; +};