From beed32eb6050d306b55545d632340dececa347ec Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 27 Aug 2026 15:43:39 -0700 Subject: [PATCH] fix(ui): stamp the tag list with the range it answers Changing the date range left the previous range's tags in state until the new request landed, so the filter either offered tags that range no longer has or, when the old range was empty, stated "no tags" about a range nobody had measured yet. Stamp the fetched list with its range key and select it during render, the same way the request tiles above already guard against a superseded range. Clearing in the effect would be a render too late. --- .../components/UsagePageView.test.tsx | 36 +++++++++++++++++++ .../_components/components/UsagePageView.tsx | 17 ++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index f2dc56af9c5..7d9bee735b4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -610,6 +610,42 @@ describe("UsagePage", () => { expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "[]"); }); + it("should drop the previous range's tags as soon as the range changes", async () => { + mockTagListCall.mockResolvedValue({ "old-range-tag": { name: "old-range-tag" } } as never); + + renderWithProviders(); + + act(() => { + fireEvent.change(screen.getByTestId("usage-view-select"), { target: { value: "tag" } }); + }); + + await waitFor(() => { + expect(screen.getByTestId("entity-usage")).toHaveAttribute( + "data-entity-list", + JSON.stringify([{ label: "old-range-tag", value: "old-range-tag" }]), + ); + }); + + let resolveNewRange: (tags: Record) => void = () => {}; + mockTagListCall.mockReturnValue( + new Promise((resolve) => { + resolveNewRange = resolve; + }) as ReturnType, + ); + + act(() => { + fireEvent.click(screen.getByTestId("pick-a-different-range")); + }); + + expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "null"); + + await act(async () => { + resolveNewRange({}); + }); + + expect(screen.getByTestId("entity-usage")).toHaveAttribute("data-entity-list", "[]"); + }); + it("should show tag usage selector option for internal users", async () => { mockUseAuthorized.mockReturnValue({ isLoading: false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index 0d1e557a833..19d7a752938 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -96,7 +96,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { to: initialToDate, }); - const [allTags, setAllTags] = useState(null); + const [fetchedTags, setFetchedTags] = useState | null>(null); const { data: customers = [] } = useCustomers(); const { data: agentsResponse } = useAgents(); const { data: currentUser } = useCurrentUser(); @@ -138,6 +138,12 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const startTime = useMemo(() => (dateValue.from ? new Date(dateValue.from) : null), [dateValue.from]); const endTime = useMemo(() => (dateValue.to ? new Date(dateValue.to) : null), [dateValue.to]); + // Stamped and selected during render like the request tiles below: the tag + // filter reads "no tags" from an empty list, so a list left over from the + // previous range would state that about a range nobody has measured yet. + const currentTagRangeKey = fetchedRangeKey(startTime, endTime); + const allTags = selectForRange(fetchedTags, currentTagRangeKey); + useEffect(() => { if (!accessToken) return; let cancelled = false; @@ -145,12 +151,13 @@ const UsagePage: React.FC = ({ teams, organizations }) => { try { const tags = await tagListCall(accessToken, startTime, endTime); if (cancelled) return; - setAllTags( - Object.values(tags).map((tag: Tag) => ({ + setFetchedTags({ + rangeKey: currentTagRangeKey, + value: Object.values(tags).map((tag: Tag) => ({ label: tag.name, value: tag.name, })), - ); + }); } catch (e) { if (!cancelled) { console.error("Failed to fetch tag list", e); @@ -160,7 +167,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { return () => { cancelled = true; }; - }, [accessToken, startTime, endTime]); + }, [accessToken, startTime, endTime, currentTagRangeKey]); // Everything the request tiles read is stamped with the range it answers and // selected during render, rather than cleared in an effect. An effect runs