From 5f4378e8cc136dc16e959f4ed5414a1e7fb1fb2f Mon Sep 17 00:00:00 2001 From: Mrinal Chanshetty Date: Sat, 24 Oct 2026 21:17:55 -0500 Subject: [PATCH] fix(ui): make rows per page work on the Spend Per Tag card The card showed a rows-per-page selector with one option and ignored changes. It now offers 25, 50 and 100 (the tags route's maximum), refetches the first page at the chosen size, and keeps the size when the team or date range changes. --- .../EntityUsage/EntityUsage.test.tsx | 17 ++++++++ .../EntityUsage/TeamTagSpendCard.tsx | 43 ++++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx index 36ee54fb490..1d068f659e9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx @@ -680,6 +680,23 @@ describe("EntityUsage", () => { expect(await within(card).findByText("Showing 51-60 of 60")).toBeInTheDocument(); }); + it("refetches the first page at the chosen size when rows per page changes", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole("button", { name: "Team Multi Select" })); + const card = await screen.findByTestId("team-tag-spend-card"); + await user.click(within(card).getByRole("button", { name: "Go to next page" })); + expect(await within(card).findByText("Showing 51-60 of 60")).toBeInTheDocument(); + + await user.click(within(card).getByTestId("pagination-page-size")); + await user.click(await screen.findByRole("option", { name: "100" })); + + expect(await within(card).findByText("Showing 1-60 of 60")).toBeInTheDocument(); + expect(mockDailyActivityTagPageCall).toHaveBeenLastCalledWith( + expect.objectContaining({ teamId: "team-1", offset: 0, limit: 100 }), + ); + }); + it("asks for tag spend only for exactly one team in the team view", async () => { render(); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx index 12f10da4d2a..06bf6ec0170 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx @@ -2,7 +2,7 @@ import type { ColumnDef } from "@tanstack/react-table"; import { Tags } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; -import { DataTable, DataTablePagination } from "@/components/shared/DataTable"; +import { DataTable, DataTablePagination, DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import type { DailyActivityTagPageResponse, DailyActivityTagSpendRow } from "@/components/UsagePage/dailyActivityApi"; @@ -19,22 +19,24 @@ interface TeamTagSpendCardProps { interface TagPageState { scope: object; - page: number; + pageKey: string; response: DailyActivityTagPageResponse | null; failed: boolean; } interface TeamTagSpendPage { page: number; + pageSize: number; loading: boolean; failed: boolean; rows: DailyActivityTagSpendRow[]; totalTags: number; onPageChange: (nextPage: number) => void; + onPageSizeChange: (nextPageSize: number) => void; } const QUIET_HEADER = { headerClassName: "font-normal" }; -const PAGE_SIZE = 50; +const DEFAULT_PAGE_SIZE = 50; const columns: ColumnDef[] = [ { @@ -82,11 +84,17 @@ const useTeamTagSpendPage = ({ accessToken, startTime, endTime, teamId }: TeamTa () => ({ hasAccessToken: accessToken !== null, endTimestamp, startTimestamp, teamId }), [accessToken, endTimestamp, startTimestamp, teamId], ); - const [pageState, setPageState] = useState<{ scope: object; page: number }>({ scope: {}, page: 0 }); + const [pageState, setPageState] = useState<{ scope: object; pageSize: number; page: number }>({ + scope: {}, + pageSize: DEFAULT_PAGE_SIZE, + page: 0, + }); const [tagPageState, setTagPageState] = useState(null); + const pageSize = pageState.pageSize; const page = pageState.scope === requestScope ? pageState.page : 0; + const pageKey = `${page}:${pageSize}`; const currentScopeState = tagPageState?.scope === requestScope ? tagPageState : null; - const currentPageState = currentScopeState?.page === page ? currentScopeState : null; + const currentPageState = currentScopeState?.pageKey === pageKey ? currentScopeState : null; const enabled = Boolean(accessToken && startTimestamp !== undefined && endTimestamp !== undefined); useEffect(() => { @@ -98,15 +106,15 @@ const useTeamTagSpendPage = ({ accessToken, startTime, endTime, teamId }: TeamTa startTime: new Date(startTimestamp), endTime: new Date(endTimestamp), teamId, - offset: page * PAGE_SIZE, - limit: PAGE_SIZE, + offset: page * pageSize, + limit: pageSize, signal: controller.signal, }; void ENTITY_API.team .tagPage(tagPageRequest) .then((response) => { if (controller.signal.aborted) return; - const nextPageState: TagPageState = { scope: requestScope, page, response, failed: false }; + const nextPageState: TagPageState = { scope: requestScope, pageKey, response, failed: false }; setTagPageState(nextPageState); }) .catch((error: unknown) => { @@ -114,25 +122,28 @@ const useTeamTagSpendPage = ({ accessToken, startTime, endTime, teamId }: TeamTa console.error("Team tag spend page request failed:", error); setTagPageState((current) => ({ scope: requestScope, - page, + pageKey, response: current?.scope === requestScope ? current.response : null, failed: true, })); }); return () => controller.abort(); - }, [accessToken, endTimestamp, page, requestScope, startTimestamp, teamId]); + }, [accessToken, endTimestamp, page, pageKey, pageSize, requestScope, startTimestamp, teamId]); const loading = enabled && currentPageState === null; const failed = currentPageState?.failed ?? false; const rows = failed ? [] : currentPageState?.response?.tags ?? []; const totalTags = currentScopeState?.response?.total_tags ?? 0; - const onPageChange = (nextPage: number) => setPageState({ scope: requestScope, page: nextPage }); + const onPageChange = (nextPage: number) => setPageState({ scope: requestScope, pageSize, page: nextPage }); + const onPageSizeChange = (nextPageSize: number) => + setPageState({ scope: requestScope, pageSize: nextPageSize, page: 0 }); - return { page, loading, failed, rows, totalTags, onPageChange }; + return { page, pageSize, loading, failed, rows, totalTags, onPageChange, onPageSizeChange }; }; const TeamTagSpendCard: React.FC = (props) => { - const { page, loading, failed, rows, totalTags, onPageChange } = useTeamTagSpendPage(props); + const { page, pageSize, loading, failed, rows, totalTags, onPageChange, onPageSizeChange } = + useTeamTagSpendPage(props); return (

@@ -152,11 +163,11 @@ const TeamTagSpendCard: React.FC = (props) => { {totalTags > 0 && ( undefined} - pageSizeOptions={[PAGE_SIZE]} + onPageSizeChange={onPageSizeChange} + pageSizeOptions={DEFAULT_PAGE_SIZE_OPTIONS} isLoading={loading} /> )}