From e9b4f956c0b01878fa148fc63dde706b24f293f6 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 31 Jul 2026 22:53:26 -0700 Subject: [PATCH] fix(ui): stop the savings legend and tab control moving between tabs The card header was a wrapping flex row, and the subtitle is longer on Cumulative ("Running total saved") than on Per day ("Saved per day"). The extra width pushed the legend and the accumulation toggle onto a second row, so both jumped whenever the tab changed. `CardHeader` is now the row itself, the same structure `SummaryCard` in this file already uses, with the controls held in a shrink-0 box so they keep their place whatever the subtitle says. The other two headers in the file are title-only and correctly stay plain. --- .../_components/UsageTab.test.tsx | 28 +++++++++++++++++ .../_components/UsageTab.tsx | 31 ++++++++++--------- 2 files changed, 44 insertions(+), 15 deletions(-) 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} + +