- token & resource filter bars render a plugin slot (UsageFilterBar / ResourceUsageFilterBar) for the organization / user-group filters - breakdown tabs read plugin-provided extra sub-tabs (breakdownExtraTabs / resourceBreakdownExtraTabs) so the enterprise plugin can inject the Organization sub-tab - thread the org / user-group filter state through the token filter hook and the resource tabs / breakdown tables / summary / export dialog - standardize the organization label on the principal name; resource export date formatted to date-only
680 lines
22 KiB
TypeScript
680 lines
22 KiB
TypeScript
/**
|
|
* Resource Usage API client — adapter over the unified ``metered_usage``
|
|
* read API (``/usage/{resource,gpu-instances,storage,summary,events}``).
|
|
*
|
|
* The server returns a generic ``{ key, id, metrics:{...} }`` breakdown shape
|
|
* (one engine for every tab). This module flattens it into the per-tab item
|
|
* shape the components consume, maps the frontend ``group_by`` vocabulary onto
|
|
* the backend's (``gpu_type`` → ``instance_type``/sku), and derives the few
|
|
* convenience fields (``gpu_minutes``). Metrics the backend doesn't track
|
|
* (cpu/memory/ephemeral hours, dangling volumes) are left at 0 — the
|
|
* whole-machine SKU model meters runtime, not decomposed components.
|
|
*/
|
|
import { getIntl, request } from '@umijs/max';
|
|
import { withDeletedMark } from '../utils/deleted-label';
|
|
import { instanceTypeSeriesLabel } from '../utils/format-instance-type';
|
|
|
|
export interface ResourceUsageFilters {
|
|
creator_ids?: number[];
|
|
cluster_ids?: number[];
|
|
instance_ids?: number[];
|
|
gpu_types?: string[];
|
|
volume_ids?: number[];
|
|
// Platform-wide "All" view only (backend-gated): consumer-Org ids and
|
|
// user-group ids (expanded server-side to the groups' direct members).
|
|
organization_ids?: number[];
|
|
user_group_ids?: number[];
|
|
}
|
|
|
|
export interface ResourceBreakdownRequest {
|
|
start_date: string;
|
|
end_date: string;
|
|
scope?: 'self' | 'all';
|
|
filters?: ResourceUsageFilters;
|
|
// One or more grouping dimensions, combined left-to-right (mirrors the token
|
|
// usage API). A trend uses ['date', '<dim>']; a table uses ['<dim>'].
|
|
group_by?: string[];
|
|
granularity?: 'hour' | 'day' | 'week' | 'month';
|
|
// Server-side sort: a metric key (e.g. gpu_hours / instance_hours) +
|
|
// direction. Defaults on the server when omitted.
|
|
order_by?: string;
|
|
descending?: boolean;
|
|
page?: number;
|
|
perPage?: number;
|
|
}
|
|
|
|
export interface ResourceBreakdownSummary {
|
|
gpu_hours: number;
|
|
gpu_minutes: number;
|
|
instance_hours: number;
|
|
cpu_hours: number;
|
|
memory_gb_hours: number;
|
|
ephemeral_gb_hours: number;
|
|
active_instances: number;
|
|
gpu_types_used: number;
|
|
active_users: number;
|
|
storage_gb_days: number;
|
|
storage_gb_hours: number;
|
|
active_volumes: number;
|
|
dangling_volumes: number;
|
|
}
|
|
|
|
export interface ResourceBreakdownItem extends ResourceBreakdownSummary {
|
|
date?: string;
|
|
resource_type?: string;
|
|
gpu_type?: string;
|
|
instance_id?: number;
|
|
instance_name?: string;
|
|
volume_id?: number;
|
|
volume_name?: string;
|
|
user_id?: number;
|
|
user_name?: string;
|
|
// Organization grouping (platform-wide "All" view). ``organization_name``
|
|
// is resolved live server-side; a gone Org sets ``deleted``.
|
|
organization_id?: number;
|
|
organization_name?: string;
|
|
// The grouped entity (instance / volume / user) no longer exists. The name
|
|
// fields keep the clean (stale) name; the tables show a DeletedTag off this
|
|
// flag plus the id, matching the Tokens tab.
|
|
deleted?: boolean;
|
|
// Owner user of a per-instance / per-volume row (compound date+dim grouping),
|
|
// with its own deletion state — independent of the row's ``deleted`` (which
|
|
// refers to the grouped instance/volume). Lets the export mark the User
|
|
// column separately, matching the Tokens tab.
|
|
user_deleted?: boolean;
|
|
// Grouped-trend rows carry the sub-group label (sku / instance / user / …)
|
|
// alongside ``date`` so the chart can pivot one series per group.
|
|
group?: string;
|
|
last_active?: string;
|
|
// Instance-type rows carry the flavor's display fields (pretty product name +
|
|
// per-card specs) so the UI matches the GPU Instances list.
|
|
product?: string;
|
|
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 "<product> x <count>" + the spec popover.
|
|
gpu_count?: number;
|
|
ephemeral_mib?: number;
|
|
local_storage_mib?: number;
|
|
persistent_mib?: number;
|
|
// Storage volume rows: provisioned capacity + storage type.
|
|
storage_type?: string;
|
|
capacity_mib?: number;
|
|
}
|
|
|
|
export interface ResourceBreakdownResponse {
|
|
summary: ResourceBreakdownSummary;
|
|
group_by?: string;
|
|
granularity?: string;
|
|
pagination: {
|
|
page: number;
|
|
perPage: number;
|
|
total: number;
|
|
totalPage: number;
|
|
};
|
|
items: ResourceBreakdownItem[];
|
|
}
|
|
|
|
export interface UsageOption {
|
|
key: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface ResourceUsageFilterOption {
|
|
id: number;
|
|
label: string;
|
|
}
|
|
|
|
export interface ResourceUsageMetaResponse {
|
|
metrics: UsageOption[];
|
|
granularities: UsageOption[];
|
|
group_bys: UsageOption[];
|
|
filters: {
|
|
creators?: ResourceUsageFilterOption[];
|
|
clusters?: ResourceUsageFilterOption[];
|
|
instances?: ResourceUsageFilterOption[];
|
|
gpu_types?: UsageOption[];
|
|
volumes?: ResourceUsageFilterOption[];
|
|
};
|
|
}
|
|
|
|
export interface ResourceEventItem {
|
|
id: number;
|
|
occurred_at: string;
|
|
creator_id?: number;
|
|
creator_name?: string;
|
|
cluster_id?: number;
|
|
cluster_name?: string;
|
|
resource_type: string;
|
|
resource_id?: number;
|
|
resource_name: string;
|
|
event_type: string;
|
|
event_message?: string;
|
|
phase?: string;
|
|
// status.phaseMessage at event time — the detail behind a failure phase.
|
|
phase_message?: string;
|
|
}
|
|
|
|
export interface ResourceEventsResponse {
|
|
pagination: {
|
|
page: number;
|
|
perPage: number;
|
|
total: number;
|
|
totalPage: number;
|
|
};
|
|
items: ResourceEventItem[];
|
|
}
|
|
|
|
export interface SummaryResourceDistributionItem {
|
|
label: string;
|
|
value: number;
|
|
percentage: number;
|
|
}
|
|
|
|
export interface UsageSummaryResponse {
|
|
total_tokens: number;
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
token_active_users: number;
|
|
gpu_hours: number;
|
|
instance_hours: number;
|
|
active_instances: number;
|
|
storage_gb_days: number;
|
|
active_users: number;
|
|
distribution: SummaryResourceDistributionItem[];
|
|
}
|
|
|
|
// --- endpoints -----------------------------------------------------------
|
|
|
|
const URL = {
|
|
RESOURCE_BREAKDOWN: '/usage/resource/breakdown',
|
|
GPU_BREAKDOWN: '/usage/gpu-instances/breakdown',
|
|
STORAGE_BREAKDOWN: '/usage/storage/breakdown',
|
|
EVENTS: '/usage/resource-events',
|
|
SUMMARY: '/usage/summary',
|
|
RESOURCE_META: '/usage/resource/meta'
|
|
};
|
|
|
|
// --- server (generic) shapes ---------------------------------------------
|
|
|
|
interface ServerMetrics {
|
|
instance_hours?: number;
|
|
gpu_hours?: number;
|
|
gb_days?: number;
|
|
gb_hours?: number;
|
|
resources?: number;
|
|
active_users?: number;
|
|
last_active?: string;
|
|
}
|
|
|
|
// gpu_type / type both mean the sku (Type) on the server.
|
|
|
|
interface ServerBreakdownItem {
|
|
date?: string | null;
|
|
// Grouped entity: ``key`` is its display name, ``id`` its id, ``deleted`` its
|
|
// own lifecycle state (instance / volume / user / sku, per group_by).
|
|
key?: string | null;
|
|
id?: number | null;
|
|
sku?: string | null;
|
|
deleted?: boolean | null;
|
|
// Owner (creator) of the instance/volume row (compound date+dim grouping),
|
|
// at the item root alongside the grouped entity, with its OWN deletion state
|
|
// — independent of ``deleted`` — so the export can show a User column that
|
|
// marks a deleted owner separately from a deleted instance/volume.
|
|
creator_id?: number | null;
|
|
creator_name?: string | null;
|
|
creator_deleted?: boolean | null;
|
|
dimensions?: {
|
|
product?: string | null;
|
|
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;
|
|
persistent_mib?: number | null;
|
|
storage_type?: string | null;
|
|
capacity_mib?: number | null;
|
|
} | null;
|
|
metrics: ServerMetrics;
|
|
}
|
|
|
|
interface ServerBreakdownResponse {
|
|
summary: ServerMetrics;
|
|
group_by?: string;
|
|
pagination: {
|
|
page: number;
|
|
perPage: number;
|
|
total: number;
|
|
totalPage: number;
|
|
};
|
|
items: ServerBreakdownItem[];
|
|
}
|
|
|
|
// --- transforms ----------------------------------------------------------
|
|
|
|
// Frontend group_by vocabulary → backend. "gpu_type" / "type" both mean the
|
|
// sku (Type / flavor) on the server.
|
|
const GROUP_BY_MAP: Record<string, string> = {
|
|
resource_type: 'resource_type',
|
|
gpu_type: 'instance_type',
|
|
type: 'type',
|
|
instance: 'instance',
|
|
volume: 'volume',
|
|
user: 'user',
|
|
organization: 'organization',
|
|
date: 'date'
|
|
};
|
|
|
|
const num = (v?: number) => Number(v ?? 0);
|
|
|
|
function flattenMetrics(m: ServerMetrics): ResourceBreakdownSummary {
|
|
const gpuHours = num(m.gpu_hours);
|
|
return {
|
|
gpu_hours: gpuHours,
|
|
gpu_minutes: gpuHours * 60,
|
|
instance_hours: num(m.instance_hours),
|
|
// not metered under the whole-machine SKU model → 0
|
|
cpu_hours: 0,
|
|
memory_gb_hours: 0,
|
|
ephemeral_gb_hours: 0,
|
|
active_instances: num(m.resources),
|
|
gpu_types_used: 0,
|
|
active_users: num(m.active_users),
|
|
storage_gb_days: num(m.gb_days),
|
|
storage_gb_hours: num(m.gb_hours),
|
|
active_volumes: num(m.resources),
|
|
dangling_volumes: 0
|
|
};
|
|
}
|
|
|
|
function flattenItem(
|
|
groupBy: string | null | undefined,
|
|
it: ServerBreakdownItem
|
|
): ResourceBreakdownItem {
|
|
const flat: ResourceBreakdownItem = {
|
|
...flattenMetrics(it.metrics || {}),
|
|
last_active: it.metrics?.last_active ?? undefined
|
|
};
|
|
if (it.date) flat.date = it.date;
|
|
const id = it.id ?? undefined;
|
|
const deleted = !!it.deleted;
|
|
const rawKey = it.key ?? undefined;
|
|
// The chart series legend can't render a tag, so it carries the deleted
|
|
// marker as text ("<name> [Deleted.<id>]"); the tables render a DeletedTag off
|
|
// ``flat.deleted`` + the id and so keep the clean name.
|
|
const key =
|
|
rawKey != null
|
|
? withDeletedMark(
|
|
rawKey,
|
|
deleted,
|
|
deleted ? getIntl().formatMessage({ id: 'usage.table.deleted' }) : '',
|
|
id
|
|
)
|
|
: rawKey;
|
|
// Generic group label — for a compound (date + dim) trend row the key is the
|
|
// sub-group value (the switch below targets single-dimension table rows).
|
|
if (rawKey != null) flat.group = key;
|
|
flat.deleted = deleted;
|
|
switch (groupBy) {
|
|
case 'resource_type':
|
|
flat.resource_type = key;
|
|
break;
|
|
case 'gpu_type':
|
|
case 'type':
|
|
flat.gpu_type = key;
|
|
break;
|
|
case 'instance':
|
|
flat.instance_name = rawKey;
|
|
flat.instance_id = id;
|
|
break;
|
|
case 'volume':
|
|
flat.volume_name = rawKey;
|
|
flat.volume_id = id;
|
|
break;
|
|
case 'user':
|
|
flat.user_name = rawKey;
|
|
flat.user_id = id;
|
|
break;
|
|
case 'organization':
|
|
flat.organization_name = rawKey;
|
|
flat.organization_id = id;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Per-resource rows (instance / volume) carry their sku → surface it as the
|
|
// Instance Type / Type column when not already the group key.
|
|
if (!flat.gpu_type && it.sku) {
|
|
flat.gpu_type = it.sku ?? undefined;
|
|
}
|
|
// Instance-type rows carry flavor display fields (pretty product + per-card
|
|
// specs) so the UI can render them like the GPU Instances list.
|
|
const dims = it.dimensions;
|
|
if (dims) {
|
|
if (dims.product) flat.product = dims.product;
|
|
if (dims.unit_cpu_milli != null) flat.unit_cpu_milli = dims.unit_cpu_milli;
|
|
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)
|
|
flat.local_storage_mib = dims.local_storage_mib;
|
|
if (dims.persistent_mib != null) flat.persistent_mib = dims.persistent_mib;
|
|
if (dims.storage_type) flat.storage_type = dims.storage_type;
|
|
if (dims.capacity_mib != null) flat.capacity_mib = dims.capacity_mib;
|
|
}
|
|
// Owner (creator) of a per-instance / per-volume row — the grouped entity is
|
|
// the instance/volume (``key``/``deleted``), so the owner sits at the item
|
|
// root with its own deleted flag for the export's User column.
|
|
if (it.creator_name != null) flat.user_name = it.creator_name;
|
|
if (it.creator_id != null) flat.user_id = it.creator_id;
|
|
if (it.creator_deleted != null) flat.user_deleted = !!it.creator_deleted;
|
|
// 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 — "<product> x <cards>" / "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;
|
|
}
|
|
|
|
function flattenResponse(
|
|
groupBy: string | null | undefined,
|
|
res: ServerBreakdownResponse
|
|
): ResourceBreakdownResponse {
|
|
return {
|
|
summary: flattenMetrics(res.summary || {}),
|
|
group_by: res.group_by,
|
|
pagination: res.pagination,
|
|
items: (res.items || []).map((it) => flattenItem(groupBy, it))
|
|
};
|
|
}
|
|
|
|
function toServerRequest(data: ResourceBreakdownRequest) {
|
|
const groupByList = data.group_by?.length ? data.group_by : ['resource_type'];
|
|
const {
|
|
creator_ids,
|
|
instance_ids,
|
|
volume_ids,
|
|
organization_ids,
|
|
user_group_ids
|
|
} = data.filters ?? {};
|
|
// The non-date dimension drives response flattening into the right field.
|
|
const dim = groupByList.find((g) => g !== 'date');
|
|
return {
|
|
body: {
|
|
start_date: data.start_date,
|
|
end_date: data.end_date,
|
|
scope: data.scope ?? 'all',
|
|
group_by: groupByList.map((g) => GROUP_BY_MAP[g] ?? g),
|
|
granularity: data.granularity ?? 'day',
|
|
// POST endpoints take proper id arrays. "filter by user" + "filter by
|
|
// resource" (instance ids on the GPU tab / volume ids on Storage).
|
|
...(creator_ids?.length ? { creator_ids } : {}),
|
|
...(instance_ids?.length ? { instance_ids } : {}),
|
|
...(volume_ids?.length ? { volume_ids } : {}),
|
|
...(organization_ids?.length ? { organization_ids } : {}),
|
|
...(user_group_ids?.length ? { user_group_ids } : {}),
|
|
...(data.order_by ? { order_by: data.order_by } : {}),
|
|
...(data.descending !== undefined ? { descending: data.descending } : {}),
|
|
page: data.page ?? 1,
|
|
perPage: data.perPage ?? 20
|
|
},
|
|
groupBy: dim
|
|
};
|
|
}
|
|
|
|
// --- request helpers -----------------------------------------------------
|
|
|
|
async function _breakdown(
|
|
url: string,
|
|
data: ResourceBreakdownRequest,
|
|
options?: {
|
|
token?: any;
|
|
}
|
|
): Promise<ResourceBreakdownResponse> {
|
|
const { body, groupBy } = toServerRequest(data);
|
|
const res = await request<ServerBreakdownResponse>(url, {
|
|
data: body,
|
|
method: 'POST',
|
|
cancelToken: options?.token
|
|
});
|
|
return flattenResponse(groupBy, res);
|
|
}
|
|
|
|
export async function queryResourceBreakdown(
|
|
data: ResourceBreakdownRequest,
|
|
options?: {
|
|
token?: any;
|
|
}
|
|
): Promise<ResourceBreakdownResponse> {
|
|
return _breakdown(URL.RESOURCE_BREAKDOWN, data, options);
|
|
}
|
|
|
|
export async function queryGpuInstancesBreakdown(
|
|
data: ResourceBreakdownRequest,
|
|
options?: {
|
|
token?: any;
|
|
}
|
|
): Promise<ResourceBreakdownResponse> {
|
|
return _breakdown(URL.GPU_BREAKDOWN, data, options);
|
|
}
|
|
|
|
export async function queryStorageBreakdown(
|
|
data: ResourceBreakdownRequest,
|
|
options?: {
|
|
token?: any;
|
|
}
|
|
): Promise<ResourceBreakdownResponse> {
|
|
return _breakdown(URL.STORAGE_BREAKDOWN, data, options);
|
|
}
|
|
|
|
export async function queryResourceEvents(
|
|
data: {
|
|
start_date?: string;
|
|
end_date?: string;
|
|
scope?: 'self' | 'all';
|
|
filters?: ResourceUsageFilters;
|
|
resource_types?: string[];
|
|
resource_name?: string;
|
|
event_types?: string[];
|
|
page?: number;
|
|
perPage?: number;
|
|
},
|
|
options?: { skipErrorHandler?: boolean; token?: any }
|
|
): Promise<ResourceEventsResponse> {
|
|
const creatorIds = data.filters?.creator_ids;
|
|
const organizationIds = data.filters?.organization_ids;
|
|
const userGroupIds = data.filters?.user_group_ids;
|
|
return request<ResourceEventsResponse>(URL.EVENTS, {
|
|
params: {
|
|
start_date: data.start_date,
|
|
end_date: data.end_date,
|
|
scope: data.scope ?? 'all',
|
|
resource_type: data.resource_types?.[0],
|
|
// GET endpoints take list params as CSV strings (avoids axios array
|
|
// serialization quirks); the server splits them back into lists.
|
|
...(creatorIds?.length ? { creator_ids: creatorIds.join(',') } : {}),
|
|
...(organizationIds?.length
|
|
? { organization_ids: organizationIds.join(',') }
|
|
: {}),
|
|
...(userGroupIds?.length
|
|
? { user_group_ids: userGroupIds.join(',') }
|
|
: {}),
|
|
...(data.event_types?.length
|
|
? { event_types: data.event_types.join(',') }
|
|
: {}),
|
|
...(data.resource_name ? { resource_name: data.resource_name } : {}),
|
|
page: data.page ?? 1,
|
|
perPage: data.perPage ?? 50
|
|
},
|
|
method: 'GET',
|
|
skipErrorHandler: options?.skipErrorHandler,
|
|
cancelToken: options?.token
|
|
});
|
|
}
|
|
|
|
export interface ResourceFilterOption {
|
|
id: number;
|
|
label: string;
|
|
deleted?: boolean;
|
|
// ``org`` / ``user`` / ``group`` — only set on organization options so the
|
|
// filter dropdown can tag a personal (USER) consumer.
|
|
kind?: string;
|
|
}
|
|
|
|
export interface ResourceFilterMeta {
|
|
creators: ResourceFilterOption[];
|
|
instances: ResourceFilterOption[];
|
|
volumes: ResourceFilterOption[];
|
|
// Platform-wide "All" view only (backend returns them empty otherwise).
|
|
organizations: ResourceFilterOption[];
|
|
user_groups: ResourceFilterOption[];
|
|
}
|
|
|
|
export async function queryResourceFilterMeta(
|
|
scope: 'self' | 'all' = 'all'
|
|
): Promise<ResourceFilterMeta> {
|
|
const res = await request<Partial<ResourceFilterMeta>>(URL.RESOURCE_META, {
|
|
params: { scope },
|
|
method: 'GET'
|
|
});
|
|
return {
|
|
creators: res.creators || [],
|
|
instances: res.instances || [],
|
|
volumes: res.volumes || [],
|
|
organizations: res.organizations || [],
|
|
user_groups: res.user_groups || []
|
|
};
|
|
}
|
|
|
|
export async function queryUsageSummary(
|
|
params: {
|
|
start_date: string;
|
|
end_date: string;
|
|
scope?: 'self' | 'all';
|
|
creator_ids?: number[];
|
|
organization_ids?: number[];
|
|
user_group_ids?: number[];
|
|
},
|
|
options?: { token?: any }
|
|
): Promise<UsageSummaryResponse> {
|
|
const { creator_ids, organization_ids, user_group_ids, ...rest } = params;
|
|
const res = await request<{
|
|
total_tokens: number;
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
token_active_users: number;
|
|
gpu_hours: number;
|
|
instance_hours: number;
|
|
storage_gb_days: number;
|
|
active_users: number;
|
|
}>(URL.SUMMARY, {
|
|
params: {
|
|
...rest,
|
|
scope: params.scope ?? 'all',
|
|
...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {}),
|
|
...(organization_ids?.length
|
|
? { organization_ids: organization_ids.join(',') }
|
|
: {}),
|
|
...(user_group_ids?.length
|
|
? { user_group_ids: user_group_ids.join(',') }
|
|
: {})
|
|
},
|
|
method: 'GET',
|
|
cancelToken: options?.token
|
|
});
|
|
|
|
// Resource Distribution donut — by GPU type, using GPU-Hours (a single,
|
|
// well-defined unit). Built from the GPU-instances breakdown grouped by
|
|
// instance type. (A true cross-resource split needs a common unit.)
|
|
let distribution: SummaryResourceDistributionItem[] = [];
|
|
try {
|
|
const byType = await queryGpuInstancesBreakdown(
|
|
{
|
|
start_date: params.start_date,
|
|
end_date: params.end_date,
|
|
scope: params.scope ?? 'all',
|
|
group_by: ['gpu_type'],
|
|
...(creator_ids?.length ||
|
|
organization_ids?.length ||
|
|
user_group_ids?.length
|
|
? {
|
|
filters: {
|
|
...(creator_ids?.length ? { creator_ids } : {}),
|
|
...(organization_ids?.length ? { organization_ids } : {}),
|
|
...(user_group_ids?.length ? { user_group_ids } : {})
|
|
}
|
|
}
|
|
: {}),
|
|
page: 1,
|
|
perPage: 100
|
|
},
|
|
{ token: options?.token }
|
|
);
|
|
const total = byType.items.reduce((s, i) => s + (i.gpu_hours || 0), 0);
|
|
distribution = byType.items
|
|
.filter((i) => (i.gpu_hours || 0) > 0)
|
|
.map((i) => ({
|
|
// Pretty product name (e.g. "NVIDIA-GeForce-RTX-5090-D") when known,
|
|
// else the raw flavor slug — matches the GPU Instances list.
|
|
label: i.product || i.gpu_type || 'unknown',
|
|
value: i.gpu_hours,
|
|
percentage: total > 0 ? (i.gpu_hours / total) * 100 : 0
|
|
}));
|
|
} catch {
|
|
distribution = [];
|
|
}
|
|
|
|
return {
|
|
total_tokens: num(res.total_tokens),
|
|
input_tokens: num(res.input_tokens),
|
|
output_tokens: num(res.output_tokens),
|
|
token_active_users: num(res.token_active_users),
|
|
gpu_hours: num(res.gpu_hours),
|
|
instance_hours: num(res.instance_hours),
|
|
active_instances: 0,
|
|
storage_gb_days: num(res.storage_gb_days),
|
|
active_users: num(res.active_users),
|
|
distribution
|
|
};
|
|
}
|
|
|
|
// Meta is synthesized client-side — the components hardcode their metric /
|
|
// group_by options and don't call these, but keep them for any external
|
|
// importers. Filter dropdowns are empty until a meta endpoint lands.
|
|
const STATIC_META: ResourceUsageMetaResponse = {
|
|
metrics: [],
|
|
granularities: [
|
|
{ key: 'day', label: 'Day' },
|
|
{ key: 'week', label: 'Week' },
|
|
{ key: 'month', label: 'Month' }
|
|
],
|
|
group_bys: [],
|
|
filters: {}
|
|
};
|
|
|
|
export async function queryResourceMeta(): Promise<ResourceUsageMetaResponse> {
|
|
return STATIC_META;
|
|
}
|
|
export async function queryGpuInstancesMeta(): Promise<ResourceUsageMetaResponse> {
|
|
return STATIC_META;
|
|
}
|
|
export async function queryStorageMeta(): Promise<ResourceUsageMetaResponse> {
|
|
return STATIC_META;
|
|
}
|