From 669fd84aee99f8d9e705bf13504c3084fb4daae6 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 31 Jul 2026 23:05:03 -0700 Subject: [PATCH] fix(ui): keep the savings header one shape across both tabs Title, legend, toggle and subtitle all shared a row. The subtitle is longer on Cumulative than on Per day, so it wrapped on one tab and not the other, growing the header by a line and shifting the legend, the toggle and the chart with it. The title and the controls now hold a fixed row and the subtitle sits on its own line beneath, so nothing above the chart depends on how long that text is. The test that guards this selected the subtitle by class and matched a summary card's hint instead, which made it pass with the subtitle moved back into the row. It now finds the element by its text. --- .../_components/UsageTab.test.tsx | 39 ++++++++++++------- .../_components/UsageTab.tsx | 30 +++++++------- 2 files changed, 39 insertions(+), 30 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 366512bc040..a754f97d9f3 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,31 +249,40 @@ 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. + it("keeps the header the same shape on both tabs so the chart cannot shift", 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. const { getByRole, getByTestId, container } = renderWith(twoDays()); - const controlsOn = () => { + const layout = () => { 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 + // the toggle travels with the legend, so neither can move independently expect(controls.contains(getByRole("tablist"))).toBe(true); - return controls; + 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 cumulative = controlsOn(); - expect(cumulative.className).toContain("shrink-0"); - - const header = cumulative.parentElement as HTMLElement; - expect(header.className).not.toContain("flex-wrap"); + 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"); 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"); + 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"); }); 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 fd452c33e49..e9618d8f7a6 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,23 +210,23 @@ const UsageTab: React.FC = ({ accessToken, activity }) => {

- {/* 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 */} - -
+ {/* 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 -

{savingsSubtitle}

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

{savingsSubtitle}

{accumulation === "cumulative" ? (