"use client"; import { ArrowDownIcon, ArrowUpIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../shadcnui"; import { cn } from "../../../utils"; import type { TokenUsageAdminBreakdownInterface } from "../data/tokenusage-admin-breakdown.interface"; import type { Metric } from "../data/tokenusage-admin.types"; import { useUsageFormatters } from "../lib/formatters"; import { cacheHitPercentage, metricValue } from "../lib/metrics"; type Props = { rows: TokenUsageAdminBreakdownInterface[]; metric: Metric; }; type SortKey = "label" | "sublabel" | "activeUsers" | "calls" | "tokensIn" | "tokensOut" | "cacheHit" | "cost" | "credits" | "share"; type SortState = { key: SortKey; direction: "asc" | "desc" }; /** * The numeric detail behind the ranked bars: every row, every metric column. * * The bars answer "who is biggest"; this answers "why". Sorting is client-side * because the whole ranked set — top-N plus the "other" rollup — is already in * memory, so a round trip would buy nothing. */ export function TokenUsageBreakdownTable({ rows, metric }: Props) { const t = useTranslations(); const { decimal, metricValue: formatValue, percent, compare } = useUsageFormatters(); const [sort, setSort] = useState(undefined); // The company dimension is the only one that reports active users. Rather than // render an empty column on user/operation breakdowns, the column disappears. const showActiveUsers = rows.some((row) => row.activeUsers !== undefined); const total = useMemo(() => rows.reduce((sum, row) => sum + metricValue(row, metric), 0), [rows, metric]); const sortValue = (row: TokenUsageAdminBreakdownInterface, key: SortKey): number | string => { switch (key) { case "label": return row.label ?? ""; case "sublabel": return row.sublabel ?? ""; case "activeUsers": return row.activeUsers ?? 0; case "cacheHit": return cacheHitPercentage(row.cached, row.tokensIn); case "share": return metricValue(row, metric); default: return row[key]; } }; const sortedRows = useMemo(() => { if (!sort) return rows; const factor = sort.direction === "desc" ? -1 : 1; return [...rows].sort((a, b) => { const left = sortValue(a, sort.key); const right = sortValue(b, sort.key); if (typeof left === "string" || typeof right === "string") { return compare(String(left), String(right)) * factor; } return (left - right) * factor; }); }, [rows, sort, metric, compare]); // First click on a column sorts descending: on a spend table the interesting // end of every numeric column is the top one. const toggleSort = (key: SortKey) => setSort((current) => current?.key === key ? { key, direction: current.direction === "desc" ? "asc" : "desc" } : { key, direction: "desc" }, ); const columns: { key: SortKey; label: string; numeric: boolean }[] = [ { key: "label", label: t("token_usage.admin.columns.label"), numeric: false }, { key: "sublabel", label: t("token_usage.admin.columns.sublabel"), numeric: false }, ...(showActiveUsers ? [{ key: "activeUsers" as const, label: t("token_usage.admin.columns.active_users"), numeric: true }] : []), { key: "calls", label: t("token_usage.admin.columns.calls"), numeric: true }, { key: "tokensIn", label: t("token_usage.admin.columns.tokens_in"), numeric: true }, { key: "tokensOut", label: t("token_usage.admin.columns.tokens_out"), numeric: true }, // Reuses the tile's key rather than minting a column-specific one — same // metric, same words. { key: "cacheHit", label: t("token_usage.admin.cache_hit"), numeric: true }, { key: "cost", label: t("token_usage.admin.columns.cost"), numeric: true }, { key: "credits", label: t("token_usage.admin.columns.credits"), numeric: true }, { key: "share", label: t("token_usage.admin.columns.share"), numeric: true }, ]; return ( // The package's wraps itself in an `overflow-x-clip` container, which // would swallow the overflow before this scroller ever saw it — hence the // child override. Wide metric tables must scroll, never widen the page.
{columns.map((column) => ( ))} {sortedRows.map((row) => { const value = metricValue(row, metric); const share = total > 0 ? (value / total) * 100 : 0; return ( {row.id === "other" ? t("token_usage.admin.other") : row.label} {row.sublabel ?? ""} {showActiveUsers && ( {row.activeUsers === undefined ? "" : decimal(row.activeUsers, 0)} )} {decimal(row.calls, 0)} {decimal(row.tokensIn, 0)} {decimal(row.tokensOut, 0)} {percent(cacheHitPercentage(row.cached, row.tokensIn))} {formatValue(row.cost, "cost")} {formatValue(row.credits, "credits")} {percent(share)} ); })}
); }