diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx index a754f97d9f3..26b24950006 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.test.tsx @@ -249,41 +249,38 @@ describe("UsageTab", () => { expect(readSeries(bars)[0]).toMatchObject({ "Auto-router": -0.05 }); }); - it("keeps the header the same shape on both tabs so the chart cannot shift", async () => { + it("lays the savings header out with the card's own slots so nothing shifts between tabs", async () => { // The subtitle differs in length between the tabs ("Running total saved" vs "Saved - // per day"). Sharing a row with the title and controls made the header grow a line - // when it wrapped, moving the legend, the toggle and the chart below it. + // per day"). Hand-rolled rows made it compete with the legend and the toggle for + // width, so the header grew a line on one tab and the chart moved with it. CardHeader + // sizes the action column to its content and gives the rest to the title column. const { getByRole, getByTestId, container } = renderWith(twoDays()); - const layout = () => { + const header = () => { const legend = getByTestId("chart-legend"); - const controls = legend.parentElement as HTMLElement; - // the toggle travels with the legend, so neither can move independently - expect(controls.contains(getByRole("tablist"))).toBe(true); - const titleRow = controls.parentElement as HTMLElement; - // by text, not by class: several cards render a muted

, and grabbing the first - // one silently asserts against a summary-card hint instead of this subtitle - const subtitle = Array.from(container.querySelectorAll("p")).find((el) => - /Running total saved|Saved per day/.test(el.textContent ?? ""), - ); - expect(subtitle, "savings subtitle should be rendered").toBeTruthy(); - return { controls, titleRow, subtitle: subtitle as HTMLElement }; + const action = legend.closest('[data-slot="card-action"]') as HTMLElement; + const cardHeader = action.parentElement as HTMLElement; + const description = cardHeader.querySelector('[data-slot="card-description"]') as HTMLElement; + return { action, cardHeader, description }; }; - const before = layout(); - // the subtitle is a sibling BELOW the title row, never inside it, so its length - // cannot change that row's height - expect(before.titleRow.contains(before.subtitle)).toBe(false); - expect(before.titleRow.className).not.toContain("flex-wrap"); - expect(before.controls.className).toContain("shrink-0"); + const before = header(); + expect(before.action).toBeTruthy(); + expect(before.description).toBeTruthy(); + // the toggle rides in the same action slot as the legend, so neither moves alone + expect(before.action.contains(getByRole("tablist"))).toBe(true); + // the subtitle lives outside that slot, so its length cannot reposition the controls + expect(before.action.contains(before.description)).toBe(false); + expect(before.description.textContent).toContain("Running total saved"); await userEvent.click(getByRole("tab", { name: "Per day" })); - const after = layout(); - expect(after.controls).toBe(before.controls); - expect(after.titleRow).toBe(before.titleRow); - expect(after.titleRow.contains(after.subtitle)).toBe(false); - expect(container.textContent).toContain("Saved per day"); + const after = header(); + expect(after.action).toBe(before.action); + expect(after.cardHeader).toBe(before.cardHeader); + expect(after.action.contains(after.description)).toBe(false); + expect(after.description.textContent).toContain("Saved per day"); + expect(container.textContent).toContain("Savings"); }); it("subtracts a losing auto-router route from the total and keeps it out of the donut", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.tsx index e9618d8f7a6..1997182f506 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/UsageTab.tsx @@ -5,7 +5,7 @@ import { Info } from "lucide-react"; import { AreaChart, BarChart, CustomLegend, DonutChart, SEQUENTIAL_COLOR_RAMP } from "@/components/shared/charts"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getToolSpend, ToolSpendResponse } from "@/components/networking"; @@ -210,23 +210,22 @@ const UsageTab: React.FC = ({ accessToken, activity }) => {

- {/* Title and controls share a fixed row; the subtitle gets its own line below. - Competing for one row made the header taller whenever the subtitle wrapped, - which differs between the two tabs, so the chart shifted down on one of them */} - -
- Savings -
- - setAccumulation(value as SavingsAccumulation)}> - - Cumulative - {intervalLabel} - - -
-
-

{savingsSubtitle}

+ {/* CardHeader's own slots rather than hand-rolled rows: the action column is + sized to its content and the title column takes the rest, so the subtitle + never competes with the controls for width and neither moves when it grows. + The controls wrap within their column instead of pushing past the card */} + + Savings + {savingsSubtitle} + + + setAccumulation(value as SavingsAccumulation)}> + + Cumulative + {intervalLabel} + + + {accumulation === "cumulative" ? (