From dd7d8e7adf7c67fa45870841f804ce14f6e64a44 Mon Sep 17 00:00:00 2001 From: Kuldeep Joshi Date: Fri, 11 Sep 2026 15:53:36 +0530 Subject: [PATCH] fix: UI/Linting fixes resolved --- .../test_spend_management_endpoints.py | 41 +++++++++---------- .../hooks/uiSettings/useUISettings.ts | 15 ++++++- .../components/EntityUsage/EntityUsage.tsx | 34 +++++++++------ .../projectUsageAggregations.test.ts | 13 +++--- .../ProjectUsage/projectUsageAggregations.ts | 10 ++++- .../_components/components/UsagePageView.tsx | 10 +++-- .../UsageViewSelect/UsageViewSelect.test.tsx | 19 +++++---- .../hooks/usePaginatedDailyActivity.ts | 4 +- .../view_logs/RequestLogsFilters.test.tsx | 4 +- 9 files changed, 91 insertions(+), 59 deletions(-) diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index c5fdc3c6cb8..2adb4470d03 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -4235,7 +4235,7 @@ async def test_ui_view_spend_logs_with_error_message(client): @pytest.mark.asyncio -async def test_ui_view_spend_logs_with_project_id(client): +async def test_ui_view_spend_logs_with_project_id(client, monkeypatch): """Test filtering spend logs by project_id""" mock_spend_logs = [ { @@ -4274,30 +4274,27 @@ async def test_ui_view_spend_logs_with_project_id(client): user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user" ) + monkeypatch.setattr(ps, "prisma_client", make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_project_id)) + try: - with patch.object( - ps, - "prisma_client", - make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_project_id), - ): - start_date, end_date = _default_date_range() + start_date, end_date = _default_date_range() - response = client.get( - "/spend/logs/ui", - params={ - "project_id": "project-1", - "start_date": start_date, - "end_date": end_date, - }, - headers={"Authorization": "Bearer sk-test"}, - ) + response = client.get( + "/spend/logs/ui", + params={ + "project_id": "project-1", + "start_date": start_date, + "end_date": end_date, + }, + headers={"Authorization": "Bearer sk-test"}, + ) - assert response.status_code == 200 - data = response.json() - assert data["total"] == 1 - assert len(data["data"]) == 1 - assert data["data"][0]["id"] == "log1" - assert data["data"][0]["metadata"]["user_api_key_project_id"] == "project-1" + assert response.status_code == 200 + data = response.json() + assert data["total"] == 1 + assert len(data["data"]) == 1 + assert data["data"][0]["id"] == "log1" + assert data["data"][0]["metadata"]["user_api_key_project_id"] == "project-1" finally: app.dependency_overrides.pop(ps.user_api_key_auth, None) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts index eaa1c89eab4..0f6a9b805ba 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiSettings/useUISettings.ts @@ -4,6 +4,16 @@ import { createQueryKeys } from "../common/queryKeysFactory"; const uiSettingsKeys = createQueryKeys("uiSettings"); +export interface UISettingsFieldSchema { + description?: string; + properties?: Record; +} + +export interface UISettingsData { + field_schema: UISettingsFieldSchema; + values: Record; +} + /** * UI settings, cached for an hour by default because they rarely change. * @@ -14,11 +24,12 @@ const uiSettingsKeys = createQueryKeys("uiSettings"); * so a caller that needs to notice a change also has to poll. */ export const useUISettings = (options?: { staleTime?: number; refetchInterval?: number }) => { - return useQuery>({ + const queryOptions = { queryKey: uiSettingsKeys.list({}), queryFn: async () => await getUiSettings(), staleTime: options?.staleTime ?? 60 * 60 * 1000, gcTime: 60 * 60 * 1000, // 1 hour - keep in cache for 1 hour refetchInterval: options?.refetchInterval, - }); + }; + return useQuery(queryOptions); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index f55dc05f5c2..4081acc0d0b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -35,7 +35,7 @@ import { userDailyActivityCall, } from "@/components/networking"; import { Logo } from "@/components/molecules/logo/Logo"; -import { usePaginatedDailyActivity } from "../../hooks/usePaginatedDailyActivity"; +import { usePaginatedDailyActivity, type FetchPageFn } from "../../hooks/usePaginatedDailyActivity"; import { EntityMetricWithMetadata } from "@/components/UsagePage/types"; import { valueFormatterSpend } from "@/components/UsagePage/utils/value_formatters"; import EndpointUsage from "../EndpointUsage/EndpointUsage"; @@ -44,6 +44,15 @@ import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView import TopModelView from "./TopModelView"; import TeamUserSpendCard from "./TeamUserSpendCard"; +/** The entity metadata shape actually probed by getEntityLabel: whichever of these + * fields the backend populated for a given entity type (team, user, ...). */ +interface EntityBreakdownMetadata { + team_alias?: string; + user_email?: string; + user_alias?: string; + [key: string]: unknown; +} + interface EntityMetrics { metrics: { spend: number; @@ -56,7 +65,7 @@ interface EntityMetrics { failed_requests: number; api_requests: number; }; - metadata: Record; + metadata: EntityBreakdownMetadata; } interface EntitySpendData { @@ -88,7 +97,7 @@ interface EntityUsageProps { isOrgAdmin?: boolean; } -const ENTITY_FETCH_FNS: Record Promise> = { +const ENTITY_FETCH_FNS: Record = { tag: tagDailyActivityCall, team: teamDailyActivityCall, organization: organizationDailyActivityCall, @@ -99,7 +108,7 @@ const ENTITY_FETCH_FNS: Record Promise> = { // Single-shot endpoints returning the whole range in one response; entity types // without one fall back to page-draining the paginated endpoint. -const ENTITY_AGGREGATED_FETCH_FNS: Partial Promise>> = { +const ENTITY_AGGREGATED_FETCH_FNS: Partial> = { team: teamDailyActivityAggregatedCall, }; @@ -141,18 +150,19 @@ const EntityUsage: React.FC = ({ const hasRequestWindow = !!accessToken && !!startTime && !!endTime; const enabled = hasRequestWindow && canViewEntity; + const entityPaginatedActivityOptions = { + fetchFn, + args: [accessToken, startTime, endTime, entityFilterArg], + enabled, + aggregatedFetchFn, + }; const { data: spendDataRaw, isFetchingMore, progress, cancelled, cancel, - } = usePaginatedDailyActivity({ - fetchFn, - args: [accessToken, startTime, endTime, entityFilterArg], - enabled, - aggregatedFetchFn, - }); + } = usePaginatedDailyActivity(entityPaginatedActivityOptions); const spendData = spendDataRaw as unknown as EntitySpendData; @@ -181,7 +191,7 @@ const EntityUsage: React.FC = ({ } }; - const getEntityLabel = (entity: string, metadata?: Record): string => { + const getEntityLabel = (entity: string, metadata?: EntityBreakdownMetadata): string => { if (entityList) { const entityItem = entityList.find((item) => item.value === entity); if (entityItem) { @@ -226,7 +236,7 @@ const EntityUsage: React.FC = ({ cache_creation_input_tokens: 0, }, metadata: { - alias: getEntityLabel(entity, data.metadata as any), + alias: getEntityLabel(entity, data.metadata as EntityBreakdownMetadata), id: entity, }, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts index 7ef733f5eac..db6f5ab63c7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts @@ -20,13 +20,15 @@ const row = (overrides: Partial = {}): ProjectDailySpendRo describe("summarizeProjectUsage", () => { it("returns all-zero totals for no rows", () => { - expect(summarizeProjectUsage([])).toEqual({ + const zeroTotals = { total_spend: 0, total_api_requests: 0, total_successful_requests: 0, total_failed_requests: 0, total_tokens: 0, - }); + }; + + expect(summarizeProjectUsage([])).toEqual(zeroTotals); }); it("sums spend, requests, and tokens across every row regardless of project or date", () => { @@ -40,14 +42,15 @@ describe("summarizeProjectUsage", () => { total_tokens: 40, }; const rows = [row({ spend: 1.5 }), row(projectBetaRow)]; - - expect(summarizeProjectUsage(rows)).toEqual({ + const expectedTotals = { total_spend: 3.75, total_api_requests: 7, total_successful_requests: 6, total_failed_requests: 1, total_tokens: 55, - }); + }; + + expect(summarizeProjectUsage(rows)).toEqual(expectedTotals); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts index ed385c56344..5ddd9ee9545 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts @@ -66,6 +66,14 @@ const groupByProjectId = (rows: ProjectDailySpendRow[]): ProjectDailySpendRow[][ return [...groups.values()]; }; +const EMPTY_PROJECT_GROUP_TOTALS = { + spend: 0, + requests: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, +}; + const summarizeProjectGroup = (rows: ProjectDailySpendRow[]): ProjectSpendRow => { const [{ project_id, project_alias }] = rows; const totals = rows.reduce( @@ -76,7 +84,7 @@ const summarizeProjectGroup = (rows: ProjectDailySpendRow[]): ProjectSpendRow => failed_requests: acc.failed_requests + row.failed_requests, tokens: acc.tokens + row.total_tokens, }), - { spend: 0, requests: 0, successful_requests: 0, failed_requests: 0, tokens: 0 }, + EMPTY_PROJECT_GROUP_TOTALS, ); return { project_id, project_alias: project_alias || project_id, ...totals }; }; 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 b841d3f0a3b..85c5f4faf92 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 @@ -124,9 +124,9 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const [isGlobalExportModalOpen, setIsGlobalExportModalOpen] = useState(false); const [isAiChatOpen, setIsAiChatOpen] = useState(false); const [selectedUsageView, setUsageView] = useState("global"); - const stillHasAccessToSelectedView = - (selectedUsageView !== "organization" || canViewOrganizationUsage) && - (selectedUsageView !== "project" || canViewProjectUsage); + const hasOrganizationAccessIfSelected = selectedUsageView !== "organization" || canViewOrganizationUsage; + const hasProjectAccessIfSelected = selectedUsageView !== "project" || canViewProjectUsage; + const stillHasAccessToSelectedView = hasOrganizationAccessIfSelected && hasProjectAccessIfSelected; const usageView: UsageOption = stillHasAccessToSelectedView ? selectedUsageView : "global"; const [showCredentialBanner, setShowCredentialBanner] = useState(true); @@ -238,10 +238,12 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const aggregatedFailed = selectForRange(aggregatedFailure, currentAggregatedRangeKey) === true; // Paginated fallback — only enabled when aggregated endpoint fails + const hasRequestWindow = !!accessToken && !!startTime && !!endTime; + const hasPaginatedFallbackRequestWindow = aggregatedFailed && hasRequestWindow; const paginatedResult = usePaginatedDailyActivity({ fetchFn: userDailyActivityCall, args: [accessToken, startTime, endTime, effectiveUserId], - enabled: aggregatedFailed && !!accessToken && !!startTime && !!endTime, + enabled: hasPaginatedFallbackRequestWindow, }); // Derive userSpendData from whichever source is active diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx index f3327ba036b..be1951abe4f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.test.tsx @@ -71,15 +71,18 @@ describe("UsageViewSelect", () => { }, ); - it.each(["Organization Usage", "Agent Usage (A2A)", "Project Usage"])("should hide %s from an internal user", async (optionName) => { - const user = userEvent.setup(); - const { container } = render( - , - ); + it.each(["Organization Usage", "Agent Usage (A2A)", "Project Usage"])( + "should hide %s from an internal user", + async (optionName) => { + const user = userEvent.setup(); + const { container } = render( + , + ); - await openMenu(user); - expect(offers(container, optionName)).toBe(false); - }); + await openMenu(user); + expect(offers(container, optionName)).toBe(false); + }, + ); // An org admin's session role is "Internal User" — org-admin-ness lives in the // membership table — so the two rows above cannot tell them apart from a plain diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts index e023feda2e3..49f83c8c9d3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts @@ -32,12 +32,12 @@ const SUMMABLE_METADATA_KEYS = [ "total_flat_cost", ] as const; -interface DailyActivityResponse { +export interface DailyActivityResponse { results: DailyData[]; metadata: Record; } -type FetchPageFn = (...args: any[]) => Promise; +export type FetchPageFn = (...args: any[]) => Promise; interface UsePaginatedDailyActivityParams { /** The API call function (e.g., userDailyActivityCall). */ diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx index 56c5b612707..eb1ee087d85 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.test.tsx @@ -87,9 +87,7 @@ describe("RequestLogsFilters", () => { vi.mocked(useInfiniteSpendLogEndUsers).mockReturnValue( emptyInfiniteQuery as unknown as ReturnType, ); - vi.mocked(useProjects).mockReturnValue({ data: [], isLoading: false } as unknown as ReturnType< - typeof useProjects - >); + vi.mocked(useProjects).mockReturnValue({ data: [], isLoading: false } as unknown as ReturnType); vi.mocked(useUISettings).mockReturnValue({ data: { values: { enable_projects_ui: true } }, } as unknown as ReturnType);