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 58aef24dddc..366512bc040 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,6 +249,34 @@ describe("UsageTab", () => { expect(readSeries(bars)[0]).toMatchObject({ "Auto-router": -0.05 }); }); + it("keeps the legend and the accumulation toggle in one place across both tabs", async () => { + // The subtitle is longer on Cumulative ("Running total saved") than on Per day + // ("Saved per day"). With a wrapping header the extra width pushed the legend and + // the toggle onto a second row, so they jumped whenever the tab changed. + const { getByRole, getByTestId, container } = renderWith(twoDays()); + + const controlsOn = () => { + const legend = getByTestId("chart-legend"); + const controls = legend.parentElement as HTMLElement; + // the toggle lives in the same box as the legend, so neither can move alone + expect(controls.contains(getByRole("tablist"))).toBe(true); + return controls; + }; + + const cumulative = controlsOn(); + expect(cumulative.className).toContain("shrink-0"); + + const header = cumulative.parentElement as HTMLElement; + expect(header.className).not.toContain("flex-wrap"); + + await userEvent.click(getByRole("tab", { name: "Per day" })); + + // same container, same classes, after the subtitle changed length + expect(controlsOn()).toBe(cumulative); + expect((cumulative.parentElement as HTMLElement).className).not.toContain("flex-wrap"); + expect(container.textContent).toContain("Saved per day"); + }); + it("subtracts a losing auto-router route from the total and keeps it out of the donut", () => { // Switching models leaves the new one with a cold cache, so a route can cost more // than the baseline would have. A negative slice is meaningless in a donut, but the 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 5a6ffdf6c99..fd452c33e49 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 @@ -210,21 +210,22 @@ const UsageTab: React.FC = ({ accessToken, activity }) => {
- -
-
- Savings -

{savingsSubtitle}

-
-
- - setAccumulation(value as SavingsAccumulation)}> - - Cumulative - {intervalLabel} - - -
+ {/* CardHeader is the row itself, matching SummaryCard: an inner wrapper that + wrapped on overflow moved the legend and the tab control onto a second line + whenever the subtitle grew, so they jumped between the two tabs */} + +
+ Savings +

{savingsSubtitle}

+
+
+ + setAccumulation(value as SavingsAccumulation)}> + + Cumulative + {intervalLabel} + +