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