feat(usage): group-by on the GPU Instances & Storage trend charts

Mirror the Tokens trend: add a clearable "Group by" select to the
MetricChartCard so the GPU Instances chart can split by instance type /
instance / user, and Storage by storage / user. When grouped, the chart
fetches group_by=["date", "<dim>"] (the same list style as the token usage
API) and pivots into one stacked series per group (shared buildTrendSeries
util), with a legend; ungrouped stays a single series. group_by is now a
list across the resource breakdown client; group-by options reuse the
bottom-table dimensions (Users only when org-wide).
This commit is contained in:
michelia
2026-06-09 11:54:28 +08:00
committed by michela feng
parent feccc81c5f
commit 68a7e3f66a
6 changed files with 236 additions and 68 deletions
+66
View File
@@ -0,0 +1,66 @@
/**
* Build bar-chart series for the resource trend charts (GPU Instances /
* Storage), shared so both tabs behave identically.
*
* Without a group dimension it's a single series of the chosen metric. With
* one (the backend returns ``group_by=["date", "<dim>"]`` rows, each carrying
* ``date`` + ``group``), it pivots into one stacked series per group value
* over the date axis — mirroring the Tokens tab's grouped trend.
*/
import type { BarSeriesItem } from '@/pages/_components/bar-chart';
import { ResourceBreakdownItem } from '../apis/resource';
import { bucketKey, Granularity } from './time-buckets';
const valueOf = (item: ResourceBreakdownItem, metric: string): number =>
Number((item as Record<string, any>)[metric] ?? 0);
export const buildTrendSeries = (opts: {
items?: ResourceBreakdownItem[];
metric: string;
granularity: Granularity;
xAxis: string[];
groupBy: string | null;
// Palette factory (e.g. useCoolColors()); called with the series count.
palette: (n: number) => string[];
singleName: string;
}): BarSeriesItem[] => {
const { items, metric, granularity, xAxis, groupBy, palette, singleName } =
opts;
if (!groupBy) {
const byDate = new Map<string, number>();
(items || []).forEach((i) => {
if (!i.date) return;
byDate.set(bucketKey(i.date, granularity), valueOf(i, metric));
});
return [
{
name: singleName,
data: xAxis.map((d) => byDate.get(d) ?? 0),
color: palette(1)[0]
}
];
}
// Group label → (date bucket → value). ``order`` preserves first-seen order
// so colors stay stable as the date range scrolls.
const byGroup = new Map<string, Map<string, number>>();
const order: string[] = [];
(items || []).forEach((i) => {
if (!i.date) return;
const label = i.group || 'unknown';
if (!byGroup.has(label)) {
byGroup.set(label, new Map());
order.push(label);
}
byGroup.get(label)!.set(bucketKey(i.date, granularity), valueOf(i, metric));
});
const colors = palette(Math.max(order.length, 1));
return order.map((label, idx) => ({
name: label,
data: xAxis.map((d) => byGroup.get(label)?.get(d) ?? 0),
color: colors[idx % colors.length],
stack: 'total'
}));
};