feat(usage): render instance types by actual shape (CPU spec / GPU x cards)
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 "<product> x <cards>", 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.
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
* whole-machine SKU model meters runtime, not decomposed components.
|
* whole-machine SKU model meters runtime, not decomposed components.
|
||||||
*/
|
*/
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
import { instanceTypeSeriesLabel } from '../utils/format-instance-type';
|
||||||
|
|
||||||
export interface ResourceUsageFilters {
|
export interface ResourceUsageFilters {
|
||||||
creator_ids?: number[];
|
creator_ids?: number[];
|
||||||
@@ -73,6 +74,10 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary {
|
|||||||
unit_cpu_milli?: number;
|
unit_cpu_milli?: number;
|
||||||
unit_memory_mib?: number;
|
unit_memory_mib?: number;
|
||||||
vram_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
|
// Per-instance rows also carry the card count + ephemeral disk so the
|
||||||
// Instances table can render "<product> x <count>" + the spec popover.
|
// Instances table can render "<product> x <count>" + the spec popover.
|
||||||
gpu_count?: number;
|
gpu_count?: number;
|
||||||
@@ -202,6 +207,8 @@ interface ServerBreakdownItem {
|
|||||||
unit_cpu_milli?: number | null;
|
unit_cpu_milli?: number | null;
|
||||||
unit_memory_mib?: number | null;
|
unit_memory_mib?: number | null;
|
||||||
vram_mib?: number | null;
|
vram_mib?: number | null;
|
||||||
|
cpu_milli?: number | null;
|
||||||
|
memory_mib?: number | null;
|
||||||
gpu_count?: number | null;
|
gpu_count?: number | null;
|
||||||
ephemeral_mib?: number | null;
|
ephemeral_mib?: number | null;
|
||||||
local_storage_mib?: number | null;
|
local_storage_mib?: number | null;
|
||||||
@@ -313,6 +320,8 @@ function flattenItem(
|
|||||||
if (dims.unit_memory_mib != null)
|
if (dims.unit_memory_mib != null)
|
||||||
flat.unit_memory_mib = dims.unit_memory_mib;
|
flat.unit_memory_mib = dims.unit_memory_mib;
|
||||||
if (dims.vram_mib != null) flat.vram_mib = dims.vram_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.gpu_count != null) flat.gpu_count = dims.gpu_count;
|
||||||
if (dims.ephemeral_mib != null) flat.ephemeral_mib = dims.ephemeral_mib;
|
if (dims.ephemeral_mib != null) flat.ephemeral_mib = dims.ephemeral_mib;
|
||||||
if (dims.local_storage_mib != null)
|
if (dims.local_storage_mib != null)
|
||||||
@@ -321,14 +330,14 @@ function flattenItem(
|
|||||||
if (dims.storage_type) flat.storage_type = dims.storage_type;
|
if (dims.storage_type) flat.storage_type = dims.storage_type;
|
||||||
if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib;
|
if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib;
|
||||||
}
|
}
|
||||||
// For an instance-type grouped trend the series label (``group``) defaults to
|
// Instance-type grouped trend: the series label (``group``) defaults to the
|
||||||
// the raw flavor slug; prefer the pretty product name so the chart legend
|
// raw flavor slug. Instance Types are grouped by actual shape, so label each
|
||||||
// matches the GPU Instances list (#5700). ``groupBy`` here is the unmapped
|
// series by that shape — "<product> x <cards>" / "CPU Only · 3 vCPU · 6 GB" —
|
||||||
// frontend dimension — ``gpu_type`` maps to the backend's ``instance_type``
|
// matching the table and keeping every shape a distinct series (#5700).
|
||||||
// (see GROUP_BY_MAP). Falls back to the slug for legacy rows that predate
|
// ``groupBy`` is the unmapped frontend dimension; the instance-type axis is
|
||||||
// dimension enrichment.
|
// ``gpu_type`` (→ backend ``instance_type`` via GROUP_BY_MAP).
|
||||||
if (groupBy === 'gpu_type' && flat.product) {
|
if (groupBy === 'gpu_type') {
|
||||||
flat.group = flat.product;
|
flat.group = instanceTypeSeriesLabel(flat);
|
||||||
}
|
}
|
||||||
return flat;
|
return flat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ResourceBreakdownItem } from '../../apis/resource';
|
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';
|
import { parseRollup } from '../../utils/time-buckets';
|
||||||
|
|
||||||
type GroupKey = 'gpu_type' | 'instance' | 'user';
|
type GroupKey = 'gpu_type' | 'instance' | 'user';
|
||||||
@@ -44,25 +47,29 @@ const useInstancesColumns = (groupKey: GroupKey) => {
|
|||||||
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
||||||
dataIndex: 'gpu_type',
|
dataIndex: 'gpu_type',
|
||||||
key: 'gpu_type',
|
key: 'gpu_type',
|
||||||
render: (_v: string, row: ResourceBreakdownItem) =>
|
render: (_v: string, row: ResourceBreakdownItem) => {
|
||||||
renderInstanceType(
|
const isCpu = !row.gpu_count && !row.vram_mib;
|
||||||
|
return renderInstanceType(
|
||||||
buildInstanceTypeRecordFromMiB({
|
buildInstanceTypeRecordFromMiB({
|
||||||
name: row.instance_name,
|
name: row.instance_name,
|
||||||
product: row.product || row.gpu_type,
|
product: row.product || row.gpu_type,
|
||||||
gpuCount: row.gpu_count,
|
gpuCount: row.gpu_count,
|
||||||
unitCpuMilli: row.unit_cpu_milli,
|
// CPU instance types show their real total size (cpu/mem totals);
|
||||||
unitMemoryMib: row.unit_memory_mib,
|
// 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
|
vramMib: row.vram_mib
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
intl,
|
intl,
|
||||||
categories: ['cpu', 'ram'],
|
categories: ['cpu', 'ram'],
|
||||||
title:
|
// Each row is one shape: GPU "<product> x <cards>", CPU
|
||||||
!!row.gpu_count || !!row.vram_mib
|
// "CPU Only · <spec>".
|
||||||
? instanceTypeLabel(row)
|
title: instanceTypeSeriesLabel(row)
|
||||||
: 'CPU Only'
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Instances breakdown: render through the canonical GPU Instances list
|
// Instances breakdown: render through the canonical GPU Instances list
|
||||||
// renderer so the label + spec popover are identical. The breakdown row
|
// 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' }),
|
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
||||||
dataIndex: 'gpu_type',
|
dataIndex: 'gpu_type',
|
||||||
key: 'gpu_type',
|
key: 'gpu_type',
|
||||||
render: (_v: string, row: ResourceBreakdownItem) =>
|
render: (_v: string, row: ResourceBreakdownItem) => {
|
||||||
renderInstanceType(
|
const isCpu = !row.gpu_count && !row.vram_mib;
|
||||||
|
return renderInstanceType(
|
||||||
buildInstanceTypeRecordFromMiB({
|
buildInstanceTypeRecordFromMiB({
|
||||||
name: row.instance_name,
|
name: row.instance_name,
|
||||||
product: row.product || row.gpu_type,
|
product: row.product || row.gpu_type,
|
||||||
gpuCount: row.gpu_count,
|
gpuCount: row.gpu_count,
|
||||||
unitCpuMilli: row.unit_cpu_milli,
|
// A per-instance row is one concrete instance, so CPU shows its
|
||||||
unitMemoryMib: row.unit_memory_mib,
|
// 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,
|
vramMib: row.vram_mib,
|
||||||
localStorageMib: row.local_storage_mib,
|
localStorageMib: row.local_storage_mib,
|
||||||
ephemeralMib: row.ephemeral_mib,
|
ephemeralMib: row.ephemeral_mib,
|
||||||
persistentMib: row.persistent_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
|
// 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),
|
// with its offset; parseRollup keeps that wall clock (no browser-tz convert),
|
||||||
|
|||||||
@@ -16,3 +16,39 @@ import { ResourceBreakdownItem } from '../apis/resource';
|
|||||||
export const instanceTypeLabel = (
|
export const instanceTypeLabel = (
|
||||||
row?: Partial<ResourceBreakdownItem>
|
row?: Partial<ResourceBreakdownItem>
|
||||||
): string => row?.product || row?.gpu_type || '-';
|
): 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<ResourceBreakdownItem>): 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 "<product> x <cards>", a CPU shows "CPU Only · <spec>".
|
||||||
|
// 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<ResourceBreakdownItem>
|
||||||
|
): 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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user