From 5410752f43c0adbb58ccf209087b25df3d724120 Mon Sep 17 00:00:00 2001 From: ishaan-berri <155045088+ishaan-berri@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:18:42 -0700 Subject: [PATCH] feat(ui): redesign usage page with stacked model and agent charts (#45221) * feat(ui): add shared StackedUsageChart from the model leaderboard chart * test(ui): cover StackedUsageChart palette and tooltip rows * feat(ui): export StackedUsageChart from shared charts * refactor(ui): render model leaderboard chart with StackedUsageChart * feat(ui): add usage overview series, leaderboard and totals helpers * feat(ui): add Panel, Stat, Segmented and Sparkline usage primitives * feat(ui): map model names to provider logos * feat(ui): add model leaderboard rows with provider logo tiles * feat(ui): redesign the usage overview tab * feat(ui): group user-agent tags into named agents with logos * test(ui): cover agent grouping against prod-shaped user-agent tags * feat(ui): add top agents chart and table to usage overview * feat(ui): add useTagSummary hook for tag spend and tokens * feat(ui): wire redesigned overview, top agents and provider spend into usage * test(ui): update usage page tests for the redesigned overview * feat(ui): compact usage view selector with a dropdown that fits its content * test(ui): update usage view selector tests * feat(ui): show provider spend as a share bar over the provider table * test(ui): assert provider share segments instead of the donut * feat(ui): restyle team, org, tag and customer usage to match the overview * test(ui): update entity usage tests for the restyled views * style(ui): restyle team user spend card * style(ui): restyle top model view * test(ui): update top model view tests * style(ui): restyle endpoint activity tab * style(ui): label endpoint bars Successful and Failed * test(ui): update endpoint bar chart tests * style(ui): restyle endpoint line chart * test(ui): update endpoint line chart tests * style(ui): quiet endpoint table header * style(ui): align usage AI chat panel typography * style(ui): restyle key activity tab with the overview stat strip * style(ui): replace top key toggle buttons with segmented controls * test(ui): update top key view tests * style(ui): restyle model and MCP activity tabs * test(ui): update activity metrics tests * feat(ui): restyle user agent activity and accept an initial agent filter * test(ui): update user agent activity tests * style(ui): outline export button in usage headers --- .../(dashboard)/hooks/tags/useTagSummary.ts | 29 + .../_components/ModelInsightsView.tsx | 60 +- .../EndpointUsage/EndpointUsage.tsx | 8 +- .../components/EndpointUsageBarChart.test.tsx | 10 +- .../components/EndpointUsageBarChart.tsx | 80 +- .../EndpointUsageLineChart.test.tsx | 7 +- .../components/EndpointUsageLineChart.tsx | 55 +- .../components/EndpointUsageTable.tsx | 60 +- .../EntityUsage/EntityUsage.test.tsx | 37 +- .../components/EntityUsage/EntityUsage.tsx | 658 ++++++++--------- .../EntityUsage/SpendByProvider.test.tsx | 34 +- .../EntityUsage/SpendByProvider.tsx | 181 +++-- .../EntityUsage/TeamUserSpendCard.tsx | 86 ++- .../EntityUsage/TopModelView.test.tsx | 4 +- .../components/EntityUsage/TopModelView.tsx | 84 ++- .../components/UsageAIChatPanel.tsx | 43 +- .../components/UsagePageView.test.tsx | 115 +-- .../_components/components/UsagePageView.tsx | 687 ++++------------- .../UsageViewSelect/UsageViewSelect.test.tsx | 5 +- .../UsageViewSelect/UsageViewSelect.tsx | 84 +-- .../components/overview/BreakdownChart.tsx | 178 +++++ .../components/overview/Primitives.tsx | 175 +++++ .../components/overview/TopAgents.tsx | 233 ++++++ .../components/overview/UsageOverview.tsx | 193 +++++ .../components/overview/agentCatalog.test.ts | 173 +++++ .../components/overview/agentCatalog.ts | 325 ++++++++ .../components/overview/modelProvider.ts | 32 + .../components/overview/overviewData.ts | 243 ++++++ .../EntityUsageExport/UsageExportHeader.tsx | 6 +- .../EntityUsage/TopKeyView.test.tsx | 28 +- .../components/EntityUsage/TopKeyView.tsx | 88 ++- .../UsagePage/components/KeyActivityPanel.tsx | 237 ++++-- .../src/components/activity_metrics.test.tsx | 11 +- .../src/components/activity_metrics.tsx | 693 +++++++++--------- .../shared/charts/StackedUsageChart.test.tsx | 57 ++ .../shared/charts/StackedUsageChart.tsx | 116 +++ .../src/components/shared/charts/index.ts | 7 + .../components/user_agent_activity.test.tsx | 6 +- .../src/components/user_agent_activity.tsx | 498 +++++++------ 39 files changed, 3549 insertions(+), 2077 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTagSummary.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/BreakdownChart.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/Primitives.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/TopAgents.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/modelProvider.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/overviewData.ts create mode 100644 ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.test.tsx create mode 100644 ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTagSummary.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTagSummary.ts new file mode 100644 index 00000000000..d20187d1287 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTagSummary.ts @@ -0,0 +1,29 @@ +import { $api } from "@/lib/http/api"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const isoDay = (date: Date) => { + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${date.getFullYear()}-${month}-${day}`; +}; + +/** Per-tag spend, tokens and requests for a date range, including the `User-Agent:` tags agents are read from. */ +export const useTagSummary = (startTime: Date | null, endTime: Date | null) => { + const { accessToken } = useAuthorized(); + return $api.useQuery( + "get", + "/tag/summary", + { + params: { + query: { + start_date: startTime ? isoDay(startTime) : "", + end_date: endTime ? isoDay(endTime) : "", + }, + }, + }, + { + enabled: Boolean(accessToken && startTime && endTime), + select: (data) => data.results, + }, + ); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx index 21a707b8c42..ec3b5066d2f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx @@ -2,8 +2,8 @@ import { Page } from "@/components/shared/Page"; import React from "react"; -import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { ArrowDownRight, ArrowUpRight, BarChart3, Minus } from "lucide-react"; +import { StackedUsageChart } from "@/components/shared/charts"; import { apiClient } from "@/components/networking"; import { extractErrorMessage } from "@/utils/errorUtils"; @@ -11,7 +11,6 @@ import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { PageHeader, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -26,18 +25,6 @@ import { RankedModel, } from "./modelInsightsData"; -const PALETTE = [ - "#ec4899", - "#a855f7", - "#f59e0b", - "#3b82f6", - "#10b981", - "#ef4444", - "#14b8a6", - "#84cc16", - "#6366f1", - "#f97316", -]; const SCALES = ["linear", "log"] as const; const GRANULARITIES = ["day", "week"] as const; const GRANULARITY_LABELS: Record = { day: "Daily", week: "Weekly" }; @@ -143,10 +130,6 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string ); } - const chartConfig = Object.fromEntries( - models.map((model, index) => [model, { label: model, color: PALETTE[index % PALETTE.length] }]), - ) satisfies ChartConfig; - return ( @@ -198,38 +181,15 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string - - - - - formatMetric(Number(value), shown)} - /> - - `${label} · Gateway total ${formatMetric(bucketTotals.get(String(label)) ?? 0, shown)}` - } - /> - } - /> - {models.map((model, index) => ( - - ))} - - + formatMetric(value, shown)} + totalLabel="Gateway total" + totalFor={(label) => bucketTotals.get(label) ?? 0} + /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx index 51e451ca770..dcf248cee65 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx @@ -56,10 +56,12 @@ const EndpointUsage: React.FC = ({ userSpendData }) => { }, [userSpendData]); return ( -
+
+
+ + +
- -
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx index a9e65b21f4b..0c9407d6ea1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx @@ -30,18 +30,20 @@ describe("EndpointUsageBarChart", () => { renderWithProviders(); expect(screen.getByText("Success vs Failed Requests by Endpoint")).toBeInTheDocument(); - expect(screen.getByText("Successful Requests")).toBeInTheDocument(); - expect(screen.getByText("Failed Requests")).toBeInTheDocument(); + expect(screen.getByText("Successful")).toBeInTheDocument(); + expect(screen.getByText("Failed")).toBeInTheDocument(); + expect(screen.getByText("160")).toBeInTheDocument(); + expect(screen.getByText("7")).toBeInTheDocument(); }); - it("renders stacked green and red bars per endpoint", () => { + it("renders stacked brand-blue and red bars per endpoint", () => { const { container } = renderWithProviders(); expect(container.querySelectorAll(".recharts-bar")).toHaveLength(2); const rectangles = Array.from(container.querySelectorAll("path.recharts-rectangle")); expect(rectangles).toHaveLength(4); const fills = new Set(rectangles.map((rect) => rect.getAttribute("fill"))); - expect(fills).toEqual(new Set(["var(--color-green-500, #22c55e)", "var(--color-red-500, #ef4444)"])); + expect(fills).toEqual(new Set(["#2b3fd6", "#ef4444"])); const xPositions = rectangles.map((rect) => rect.getAttribute("d")?.split(",")[0]); expect(new Set(xPositions).size).toBe(2); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx index bf9868d77cf..b55ea0513c7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx @@ -1,53 +1,69 @@ import React from "react"; -import { BarChart, CustomLegend, CustomTooltip } from "@/components/shared/charts"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ChartColumnStacked } from "lucide-react"; +import { BarChart, CustomTooltip } from "@/components/shared/charts"; import { MetricWithMetadata } from "@/components/UsagePage/types"; +import { Panel } from "../../overview/Primitives"; interface EndpointUsageBarChartProps { endpointData?: Record; } +const SUCCESS_COLOR = "#2b3fd6"; +const FAILED_COLOR = "#ef4444"; +const CATEGORIES = ["Successful", "Failed"] as const; +const COLORS = [SUCCESS_COLOR, FAILED_COLOR] as const; + +const valueFormatter = (value: number) => value.toLocaleString(); + const EndpointUsageBarChart: React.FC = ({ endpointData }) => { // Transform endpoint data into chart format const chartData = React.useMemo(() => { return Object.entries(endpointData || {}).map(([endpoint, data]) => ({ endpoint, - "metrics.successful_requests": data.metrics.successful_requests, - "metrics.failed_requests": data.metrics.failed_requests, - metrics: { - successful_requests: data.metrics.successful_requests, - failed_requests: data.metrics.failed_requests, - }, + Successful: data.metrics.successful_requests, + Failed: data.metrics.failed_requests, })); }, [endpointData]); - const valueFormatter = (value: number) => value.toLocaleString(); + const totals = React.useMemo( + () => + chartData.reduce( + (acc, row) => ({ Successful: acc.Successful + row.Successful, Failed: acc.Failed + row.Failed }), + { Successful: 0, Failed: 0 }, + ), + [chartData], + ); return ( - - -
- Success vs Failed Requests by Endpoint - + + {CATEGORIES.map((category, i) => ( + + + ))}
-
- - - -
+ } + > + + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx index 81ca90d8cb9..8e26e63d9d9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx @@ -2,6 +2,7 @@ import { screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { renderWithProviders } from "@/../tests/test-utils"; import { DailyData, MetricWithMetadata, SpendMetrics } from "@/components/UsagePage/types"; +import { STACKED_USAGE_PALETTE } from "@/components/shared/charts"; import EndpointUsageLineChart from "./EndpointUsageLineChart"; const spendMetrics = (apiRequests: number): SpendMetrics => ({ @@ -53,13 +54,13 @@ describe("EndpointUsageLineChart", () => { expect(screen.getByText("Endpoint Usage Trends")).toBeInTheDocument(); }); - it("renders one line per endpoint with the tremor palette strokes", () => { + it("renders one line per endpoint with the stacked usage palette strokes", () => { const { container } = renderWithProviders(); const curves = Array.from(container.querySelectorAll("path.recharts-line-curve")); expect(curves).toHaveLength(2); expect(new Set(curves.map((curve) => curve.getAttribute("stroke")))).toEqual( - new Set(["var(--color-blue-500, #3b82f6)", "var(--color-cyan-500, #06b6d4)"]), + new Set([STACKED_USAGE_PALETTE[0], STACKED_USAGE_PALETTE[1]]), ); }); @@ -87,7 +88,7 @@ describe("EndpointUsageLineChart", () => { expect(screen.getAllByText(/^\d,\d{3}$/).length).toBeGreaterThan(0); }); - it("draws smooth natural curves", () => { + it("draws smooth curves that never overshoot below zero (monotone)", () => { const { container } = renderWithProviders(); const path = container.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx index 483a30b1639..ee2a50b5505 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx @@ -1,7 +1,8 @@ import { useMemo } from "react"; -import { LineChart, type ChartColor } from "@/components/shared/charts"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ChartLine } from "lucide-react"; +import { LineChart, stackedUsageColor } from "@/components/shared/charts"; import { DailyData } from "@/components/UsagePage/types"; +import { Panel } from "../../overview/Primitives"; interface EndpointUsageLineChartProps { dailyData?: { results: DailyData[] }; @@ -58,41 +59,25 @@ export function EndpointUsageLineChart({ dailyData }: EndpointUsageLineChartProp return keys; }, [chartData]); - // Tremor color palette for multiple lines - const colors: readonly ChartColor[] = [ - "blue", - "cyan", - "indigo", - "violet", - "purple", - "fuchsia", - "pink", - "rose", - "red", - "orange", - ]; + // Same palette as the Overview / Model Leaderboard stacked chart + const colors = useMemo(() => categories.map((_, i) => stackedUsageColor(i)), [categories]); return ( - - - Endpoint Usage Trends - - - value.toLocaleString()} - showLegend={true} - showGridLines={true} - yAxisWidth={60} - connectNulls={true} - curveType="natural" - /> - - + + value.toLocaleString()} + showLegend={true} + showGridLines={true} + yAxisWidth={56} + connectNulls={true} + curveType="monotone" + /> + ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx index 3d19d825c0c..2cccdfb47db 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx @@ -1,5 +1,8 @@ import React from "react"; import type { ColumnDef } from "@tanstack/react-table"; +import { Route } from "lucide-react"; +import { cn } from "@/lib/cva.config"; +import { Panel } from "../../overview/Primitives"; import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; @@ -41,7 +44,7 @@ const EndpointUsageTable: React.FC = ({ endpointData }) { header: "Endpoint", accessorKey: "endpoint", - cell: ({ row }) => {row.original.endpoint}, + cell: ({ row }) => {row.original.endpoint}, }, { header: "Successful / Failed", @@ -54,18 +57,20 @@ const EndpointUsageTable: React.FC = ({ endpointData }) const totalPercentage = successPercentage + failurePercentage; return ( -
-
+
+
- 0 ? "bg-destructive" : undefined}> - + 0 ? "h-1 bg-destructive/70" : "h-1"}> +
-
- {record.successful_requests.toLocaleString()} +
+ {record.successful_requests.toLocaleString()} / - {record.failed_requests.toLocaleString()} + 0 ? "text-destructive" : "text-muted-foreground"}> + {record.failed_requests.toLocaleString()} +
); @@ -75,7 +80,7 @@ const EndpointUsageTable: React.FC = ({ endpointData }) header: "Total Request", accessorKey: "api_requests", meta: { numeric: true }, - cell: ({ row }) => row.original.api_requests.toLocaleString(), + cell: ({ row }) => {row.original.api_requests.toLocaleString()}, }, { header: "Success Rate", @@ -86,13 +91,10 @@ const EndpointUsageTable: React.FC = ({ endpointData }) const successRateStr = value.toFixed(2); return ( = 95 - ? "text-success font-medium" - : value >= 80 - ? "text-warning font-medium" - : "text-destructive font-medium" - } + className={cn( + "tabular-nums", + value >= 95 ? "text-foreground" : value >= 80 ? "text-warning" : "text-destructive", + )} > {successRateStr}% @@ -103,7 +105,7 @@ const EndpointUsageTable: React.FC = ({ endpointData }) header: "Total Tokens", accessorKey: "total_tokens", meta: { numeric: true }, - cell: ({ row }) => row.original.total_tokens.toLocaleString(), + cell: ({ row }) => {row.original.total_tokens.toLocaleString()}, }, { header: "Spend", @@ -114,13 +116,23 @@ const EndpointUsageTable: React.FC = ({ endpointData }) ]; return ( - row.key} - noDataMessage="No endpoint usage data" - size="compact" - /> + + {dataSource.length.toLocaleString()} {dataSource.length === 1 ? "endpoint" : "endpoints"} + + } + > + row.key} + noDataMessage="No endpoint usage data" + size="compact" + /> + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx index 23bd772729b..b546b2c0558 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx @@ -563,7 +563,8 @@ describe("EntityUsage", () => { expect(spendElements.length).toBeGreaterThan(0); }); - expect(screen.getByText("1,000")).toBeInTheDocument(); // Total Requests + // Scoped to the active Cost tab: the keep-mounted Key Activity tab shows the same totals. + expect(within(screen.getByRole("tabpanel")).getByText("1,000")).toBeInTheDocument(); // Total Requests }); it("should render with team entity type and call team API", async () => { @@ -683,7 +684,7 @@ describe("EntityUsage", () => { fireEvent.click(keyActivityTab); }); - expect(screen.getAllByText("Activity Metrics")[1]).toBeInTheDocument(); + expect(within(screen.getByRole("tabpanel")).getByRole("heading", { name: "Overall Usage" })).toBeInTheDocument(); }); it("loads key pages separately from the aggregate using the current entity scope", async () => { @@ -806,9 +807,10 @@ describe("EntityUsage", () => { }); expect(await screen.findByText("Tag Spend Overview")).toBeInTheDocument(); - expect(await screen.findByText("$0.00")).toBeInTheDocument(); - expect(screen.getByText("Total Spend")).toBeInTheDocument(); - expect(screen.getAllByText("0")[0]).toBeInTheDocument(); + const costTab = screen.getByRole("tabpanel"); + expect(await within(costTab).findByText("$0.00")).toBeInTheDocument(); + expect(within(costTab).getByText("Total Spend")).toBeInTheDocument(); + expect(within(costTab).getAllByText("0")[0]).toBeInTheDocument(); }); it("should display Model Activity tab for non-agent entity types", async () => { @@ -1093,31 +1095,26 @@ describe("EntityUsage", () => { }); }); - it("renders daily spend bars, per-entity bars, and the provider donut with cyan fills and a $ center total", async () => { + it("renders the stacked daily spend chart, the per-entity table, and the provider share bar with a $ total", async () => { const { container } = render(); await waitFor(() => { expect(mockTagDailyActivityCall).toHaveBeenCalled(); }); + // The fixture carries no model breakdown, so the day's spend stacks as a single "Other" segment. await waitFor(() => { - expect(container.querySelectorAll("path.recharts-rectangle")).toHaveLength(2); + expect(container.querySelectorAll("path.recharts-rectangle")).toHaveLength(1); }); + expect(container.querySelector("path.recharts-rectangle")).toHaveAttribute("fill", "#94a3b8"); - const barFills = new Set( - Array.from(container.querySelectorAll("path.recharts-rectangle")).map((rect) => rect.getAttribute("fill")), - ); - expect(barFills).toEqual(new Set(["var(--color-cyan-500, #06b6d4)"])); + expect(screen.getAllByText("Jan 1").length).toBeGreaterThan(0); + expect(screen.getAllByText("Tag 1").length).toBeGreaterThan(0); - expect(screen.getAllByText("2025-01-01").length).toBeGreaterThan(0); - expect(screen.getAllByText("Tag 1").length).toBeGreaterThan(1); - - const sectors = container.querySelectorAll(".recharts-pie-sector path"); - expect(sectors).toHaveLength(1); - expect(sectors[0]).toHaveAttribute("fill", "var(--color-cyan-500, #06b6d4)"); - - const centerLabels = Array.from(container.querySelectorAll("text.fill-foreground")).map((text) => text.textContent); - expect(centerLabels).toContain("$100.50"); + const segments = screen.getAllByTestId("provider-share-segment"); + expect(segments).toHaveLength(1); + expect(segments[0]).toHaveStyle({ backgroundColor: "rgb(236, 72, 153)" }); + expect(screen.getByTestId("provider-spend-total")).toHaveTextContent("$100.50"); }); it("should label the chart with user_email metadata instead of the raw UUID (LIT-3889)", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index 3f239250967..165fdfef703 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -1,32 +1,24 @@ import useTeams from "@/app/(dashboard)/hooks/useTeams"; -import { BarChart, DonutChart } from "@/components/shared/charts"; +import { StackedUsageChart, type StackedUsageScale } from "@/components/shared/charts"; import { DataTable } from "@/components/shared/DataTable"; -import { - getProviderSpend, - getTopAgents, - getTopAPIKeys, - getTopModels, - type ProviderSpendRow, -} from "./entityUsageAggregations"; +import { getProviderSpend, getTopAgents, getTopAPIKeys, getTopModels } from "./entityUsageAggregations"; import { buildCostBreakdownTiles, buildSummaryTiles, hasFlatCost, type SummaryTile } from "./entityUsageSummary"; import { MoneyCell } from "@/components/shared/table_cells"; -import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { hasCapability, type Capability } from "@/utils/capabilities"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; import type { DateRangePickerValue } from "@/components/shared/date_picker_types"; -import { ChevronDown, ChevronRight, Info } from "lucide-react"; +import { Bot, Boxes, ChevronDown, ChevronRight, ExternalLink, Info, KeyRound, Layers, Server } from "lucide-react"; import type { ColumnDef } from "@tanstack/react-table"; import { Alert, AlertDescription } from "@/components/shared/Alert"; import { ChartLoader } from "@/components/shared/chart_loader"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/cva.config"; import React, { type ReactNode, useCallback, useMemo, useState } from "react"; import TeamMultiSelect from "@/components/common_components/team_multi_select"; import UserDropdown from "@/components/common_components/UserDropdown"; import { ActivityMetrics, processActivityData } from "@/components/activity_metrics"; import { UsageExportHeader } from "@/components/EntityUsageExport"; import type { EntityType } from "@/components/EntityUsageExport/types"; -import { Logo } from "@/components/molecules/logo/Logo"; import { useAggregatedDailyActivity } from "../../hooks/useAggregatedDailyActivity"; import { ENTITY_API } from "./entityFetchFns"; import { @@ -35,28 +27,76 @@ import { type DailyActivityRequest, } from "@/components/UsagePage/dailyActivityApi"; import { keyDetailFromResponse, overallUsageMetrics } from "@/components/UsagePage/keyActivityData"; -import { EntityMetricWithMetadata } from "@/components/UsagePage/types"; -import { valueFormatterSpend } from "@/components/UsagePage/utils/value_formatters"; +import type { DailyData, EntityMetricWithMetadata } from "@/components/UsagePage/types"; import EndpointUsage from "../EndpointUsage/EndpointUsage"; import ModelViewToggle, { ModelViewType } from "../ModelViewToggle"; import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView"; import KeyActivityPanel from "@/components/UsagePage/components/KeyActivityPanel"; +import { BreakdownControls, Leaderboard, useBreakdown, type BreakdownState } from "../overview/BreakdownChart"; +import { + bucketSeries, + bucketTotals, + dailyTotals, + formatMetricValue, + type Granularity, + type Series, + type UsageMetric, +} from "../overview/overviewData"; +import { Panel, Segmented, Sparkline, Stat } from "../overview/Primitives"; import TopModelView from "./TopModelView"; import TeamUserSpendCard from "./TeamUserSpendCard"; +import { ProviderSpendBreakdown } from "./SpendByProvider"; -interface EntityMetrics { - metrics: { - spend: number; - prompt_tokens: number; - completion_tokens: number; - cache_read_input_tokens: number; - cache_creation_input_tokens: number; - total_tokens: number; - successful_requests: number; - failed_requests: number; - api_requests: number; +const BRAND = "#2b3fd6"; +const FLAT_COST_SERIES = "Flat cost"; +const FLAT_COST_COLOR = "#8b5cf6"; +const METRIC_TITLE: Record = { spend: "Spend", tokens: "Tokens", requests: "Requests" }; +/** Summary tiles that carry a sparkline, keyed by tile title, valued by the dailyTotals field. */ +const TILE_TRENDS: Readonly> = { + "Total Spend": "spend", + "Total Cost": "spend", + "Total Requests": "requests", + "Total Tokens": "tokens", +}; +const GRANULARITY_OPTIONS = [ + { value: "day", label: "Daily" }, + { value: "week", label: "Weekly" }, +] as const satisfies readonly { value: Granularity; label: string }[]; +const SCALE_OPTIONS = [ + { value: "linear", label: "Linear" }, + { value: "log", label: "Log" }, +] as const satisfies readonly { value: StackedUsageScale; label: string }[]; +/** Quiet headers: muted, regular weight, so the rows carry the emphasis. */ +const QUIET_HEADER = { headerClassName: "font-normal" }; + +/** Stacks reserved-capacity flat cost on top of the per-model spend, so each bar is the day's full cost. */ +const withFlatCost = (series: Series, results: readonly DailyData[]): Series => { + const flatByDate = new Map(results.map((day) => [day.date, day.metrics.flat_cost ?? 0])); + return { + data: series.data.map((day) => ({ ...day, [FLAT_COST_SERIES]: flatByDate.get(day.date) ?? 0 })), + keys: [...series.keys, FLAT_COST_SERIES], + colors: [...series.colors, FLAT_COST_COLOR], }; - metadata: Record; +}; + +function ShareBar({ value, max }: { value: number; max: number }) { + return ( +