diff --git a/src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx b/src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx new file mode 100644 index 0000000000..d9cf7f1720 --- /dev/null +++ b/src/app/(dashboard)/dashboard/costs/quota-share/components/StackedAllocationBar.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { PoolAllocation } from "@/lib/quota/dimensions"; +import type { PoolUsageSnapshot } from "@/lib/quota/types"; + +export interface StackedAllocationBarProps { + allocations: PoolAllocation[]; + usage: PoolUsageSnapshot | null; + keyLabels: Record; + /** When usage has multiple dimensions, which one to display in this bar. + * Default: the first dimension. */ + dimensionIndex?: number; +} + +const PALETTE = [ + "#a78bfa", + "#60a5fa", + "#34d399", + "#fbbf24", + "#f87171", + "#22d3ee", + "#f472b6", + "#94a3b8", +]; + +export default function StackedAllocationBar({ + allocations, + usage, + keyLabels, + dimensionIndex = 0, +}: StackedAllocationBarProps): JSX.Element | null { + const t = useTranslations("quotaShare"); + + if (allocations.length === 0) { + return null; + } + + // Build a map of apiKeyId → { consumed, fairShare } from the relevant dimension + const perKeyMap: Record = {}; + if (usage) { + const dim = usage.dimensions[dimensionIndex]; + if (dim) { + for (const entry of dim.perKey) { + perKeyMap[entry.apiKeyId] = { consumed: entry.consumed, fairShare: entry.fairShare }; + } + } + } + + return ( +
+

+ {t("stackedBarTitle")} +

+ + {/* Stacked bar */} +
+ {allocations.map((alloc, i) => { + const color = PALETTE[i % PALETTE.length]; + const keyUsage = perKeyMap[alloc.apiKeyId]; + let consumedPercent: number | null = null; + if (keyUsage && keyUsage.fairShare > 0) { + consumedPercent = Math.round((keyUsage.consumed / keyUsage.fairShare) * 100); + } + const label = keyLabels[alloc.apiKeyId] ?? alloc.apiKeyId; + const tooltipText = + consumedPercent !== null + ? `${label}: ${alloc.weight}% (${t("usedSuffix", { percent: consumedPercent })})` + : `${label}: ${alloc.weight}%`; + return ( +
+ ); + })} +
+ + {/* Labels */} +
+ {allocations.map((alloc, i) => { + const color = PALETTE[i % PALETTE.length]; + const keyUsage = perKeyMap[alloc.apiKeyId]; + let consumedPercent: number | null = null; + if (keyUsage && keyUsage.fairShare > 0) { + consumedPercent = Math.round((keyUsage.consumed / keyUsage.fairShare) * 100); + } + const label = keyLabels[alloc.apiKeyId] ?? alloc.apiKeyId; + return ( + + + + {label} {alloc.weight}% + {consumedPercent !== null && ( + + {" "} + ({t("usedSuffix", { percent: consumedPercent })}) + + )} + + + ); + })} +
+
+ ); +}