Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fireEvent, render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import type { DailyData, KeyMetricWithMetadata, SpendMetrics } from "@/components/UsagePage/types";
import type { DailyActivityRange } from "./useDailyActivityRange";

vi.mock("@/components/shared/advanced_date_picker", () => ({
__esModule: true,
Expand Down Expand Up @@ -59,7 +60,7 @@ const dayWithModels = (date: string, models: Record<string, Partial<SpendMetrics
},
});

const renderWith = (results: DailyData[]) =>
const renderWith = (results: DailyData[], overrides: Partial<DailyActivityRange> = {}) =>
render(
<CacheLeakageCard
activity={{
Expand All @@ -68,6 +69,10 @@ const renderWith = (results: DailyData[]) =>
results,
loading: false,
isFetchingMore: false,
progress: { currentPage: 1, totalPages: 1 },
cancelled: false,
cancel: vi.fn(),
...overrides,
}}
/>,
);
Expand Down Expand Up @@ -138,4 +143,38 @@ describe("CacheLeakageCard", () => {
expect(getByText("No key usage in this range.")).toBeInTheDocument();
expect(queryByRole("table")).not.toBeInTheDocument();
});

it("tells the user the table is still filling in while fallback pages stream", () => {
const day = dayWithKeys("2026-07-12", {
"hash-leaky": key("leaky-key", { prompt_tokens: 10000, cache_read_input_tokens: 0 }),
});
const { getByText, getByRole } = renderWith([day], { isFetchingMore: true });

expect(getByRole("table")).toBeInTheDocument();
expect(
getByText("Data is still loading; rows and totals will update as the rest of the range arrives."),
).toBeInTheDocument();
});

it("keeps the streaming note off while a fresh range loads over the previous range's rows", () => {
const day = dayWithKeys("2026-07-12", {
"hash-leaky": key("leaky-key", { prompt_tokens: 10000, cache_read_input_tokens: 0 }),
});
const { queryByText } = renderWith([day], { loading: true });

expect(
queryByText("Data is still loading; rows and totals will update as the rest of the range arrives."),
).not.toBeInTheDocument();
});

it("drops the streaming note once the range has settled", () => {
const day = dayWithKeys("2026-07-12", {
"hash-leaky": key("leaky-key", { prompt_tokens: 10000, cache_read_input_tokens: 0 }),
});
const { queryByText } = renderWith([day]);

expect(
queryByText("Data is still loading; rows and totals will update as the rest of the range arrives."),
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ const CacheLeakageCard: React.FC<CacheLeakageCardProps> = ({ activity }) => {
</Tabs>
</CardHeader>
<CardContent>
{rows.length > 0 && isFetchingMore && (
<p className="mb-2 text-sm text-muted-foreground">
Data is still loading; rows and totals will update as the rest of the range arrives.
</p>
)}
Comment thread
cursor[bot] marked this conversation as resolved.
{rows.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{loading || isFetchingMore ? "Loading..." : `No ${emptyNoun} usage in this range.`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("CostOptimizationView daily activity", () => {
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "proxy_admin" });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });

const { getByRole, getByTestId, findByTestId } = render(
const { getByRole, getByTestId, findByTestId, queryByText } = render(
<QueryClientProvider client={queryClient}>
<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />
</QueryClientProvider>,
Expand All @@ -67,5 +67,28 @@ describe("CostOptimizationView daily activity", () => {

expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(1);
expect(mockUserDailyActivityCall).not.toHaveBeenCalled();
expect(queryByText(/Currently fetching spend data/)).not.toBeInTheDocument();
});

it("shows the fetch-progress banner while the paginated fallback streams pages in", async () => {
mockUserDailyActivityAggregatedCall.mockReset();
mockUserDailyActivityCall.mockReset();
mockUserDailyActivityAggregatedCall.mockRejectedValue(new Error("aggregated unavailable"));
mockUserDailyActivityCall.mockImplementation((...args: unknown[]) =>
args[3] === 1
? Promise.resolve({ results: [], metadata: { total_pages: 3, has_more: true, page: 1 } })
: new Promise(() => {}),
);
useAuthorizedMock.mockReturnValue({ accessToken: "test-token", userId: "u1", userRole: "proxy_admin" });
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });

const { findByText, getByRole } = render(
<QueryClientProvider client={queryClient}>
<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />
</QueryClientProvider>,
);

expect(await findByText(/Currently fetching spend data: fetched 1 \/ 3 pages/)).toBeInTheDocument();
expect(getByRole("button", { name: "Stop" })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from "react";
import { Info, PiggyBank } from "lucide-react";

import useCan from "@/app/(dashboard)/hooks/useCan";
import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import UsageTab from "./UsageTab";
import PromptCompressionTab from "./PromptCompressionTab";
Expand Down Expand Up @@ -62,6 +63,12 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
</p>
</div>

<PaginationStatusAlerts
isFetchingMore={activity.isFetchingMore}
cancelled={activity.cancelled}
progress={activity.progress}
cancel={activity.cancel}
/>
<Tabs defaultValue="usage" onValueChange={handleTabChange}>
<TabsList variant="line" className="h-auto w-full justify-start rounded-none p-0">
<TabsTrigger value="usage" className="flex-none rounded-none px-4 py-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ describe("PromptCachingTab", () => {
results: [],
loading: false,
isFetchingMore: false,
progress: { currentPage: 1, totalPages: 1 },
cancelled: false,
cancel: vi.fn(),
};
const { getByTestId } = render(<PromptCachingTab accessToken="test-token" activity={activity} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ const renderWith = (results: DailyData[], options: RenderOptions = {}) => {
results,
loading: false,
isFetchingMore: false,
progress: { currentPage: 1, totalPages: 1 },
cancelled: false,
cancel: vi.fn(),
}}
/>,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import { describe, expect, it, vi } from "vitest";

const mockUsePaginatedDailyActivity = vi.fn();

const mockCancel = vi.fn();

vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", () => ({
usePaginatedDailyActivity: (args: unknown) => {
mockUsePaginatedDailyActivity(args);
return { data: { results: [] }, loading: false, isFetchingMore: false };
return {
data: { results: [] },
loading: false,
isFetchingMore: false,
progress: { currentPage: 4, totalPages: 9 },
cancelled: false,
cancel: mockCancel,
};
},
}));

Expand Down Expand Up @@ -41,6 +50,14 @@ describe("useDailyActivityRange", () => {
);
});

it("forwards the pagination progress and cancel affordances instead of dropping them", () => {
const { result } = renderHook(() => useDailyActivityRange("test-token", "u1", "proxy_admin"));

expect(result.current.progress).toEqual({ currentPage: 4, totalPages: 9 });
expect(result.current.cancelled).toBe(false);
expect(result.current.cancel).toBe(mockCancel);
});

it("stays disabled until an access token is available", () => {
renderHook(() => useDailyActivityRange(null, "u1", "proxy_admin"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
results: DailyData[];
loading: boolean;
isFetchingMore: boolean;
progress: { currentPage: number; totalPages: number };
cancelled: boolean;
cancel: () => void;
}

export const useDailyActivityRange = (
Expand All @@ -33,7 +36,7 @@
const endTime = dateValue.to ?? null;
const effectiveUserId = all_admin_roles.includes(userRole) ? null : userId;

const { data, loading, isFetchingMore } = usePaginatedDailyActivity({
const { data, loading, isFetchingMore, progress, cancelled, cancel } = usePaginatedDailyActivity({

Check warning on line 39 in ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 4 properties passed inline as an argument; assign it to a named variable first
fetchFn: userDailyActivityCall,
aggregatedFetchFn: userDailyActivityAggregatedCall,
args: [accessToken, startTime, endTime, effectiveUserId, true],
Expand All @@ -46,5 +49,8 @@
results: data.results as DailyData[],
loading,
isFetchingMore,
progress,
cancelled,
cancel,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
import { hasCapability, type Capability } from "@/utils/capabilities";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
import { ChevronDown, ChevronRight, ExternalLink, Info, Loader2 } from "lucide-react";
import { ChevronDown, ChevronRight, Info } from "lucide-react";
import type { ColumnDef } from "@tanstack/react-table";
import { Alert, AlertDescription } from "@/components/shared/Alert";
import { Button } from "@/components/ui/button";
import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import React, { type ReactNode, useMemo, useState } from "react";
Expand Down Expand Up @@ -57,7 +56,7 @@
failed_requests: number;
api_requests: number;
};
metadata: Record<string, any>;

Check warning on line 59 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
}

interface EntitySpendData {
Expand Down Expand Up @@ -89,7 +88,7 @@
isOrgAdmin?: boolean;
}

const ENTITY_FETCH_FNS: Record<EntityType, (...args: any[]) => Promise<any>> = {

Check warning on line 91 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 91 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
tag: tagDailyActivityCall,
team: teamDailyActivityCall,
organization: organizationDailyActivityCall,
Expand All @@ -100,7 +99,7 @@

// 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<Record<EntityType, (...args: any[]) => Promise<any>>> = {

Check warning on line 102 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 102 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
team: teamDailyActivityAggregatedCall,
};

Expand All @@ -117,7 +116,7 @@
userRole,
dateValue,
isOrgAdmin = false,
}) => {

Check warning on line 119 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Arrow function has a complexity of 30. Maximum allowed is 20
const { teams } = useTeams();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [modelViewType, setModelViewType] = useState<ModelViewType>("groups");
Expand Down Expand Up @@ -148,7 +147,7 @@
progress,
cancelled,
cancel,
} = usePaginatedDailyActivity({

Check warning on line 150 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 4 properties passed inline as an argument; assign it to a named variable first
fetchFn,
args: [accessToken, startTime, endTime, entityFilterArg],
enabled,
Expand Down Expand Up @@ -182,7 +181,7 @@
}
};

const getEntityLabel = (entity: string, metadata?: Record<string, any>): string => {

Check warning on line 184 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
if (entityList) {
const entityItem = entityList.find((item) => item.value === entity);
if (entityItem) {
Expand Down Expand Up @@ -227,7 +226,7 @@
cache_creation_input_tokens: 0,
},
metadata: {
alias: getEntityLabel(entity, data.metadata as any),

Check warning on line 229 in ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
id: entity,
},
};
Expand Down Expand Up @@ -643,57 +642,20 @@

return (
<div style={{ width: "100%" }} className="relative">
{isFetchingMore && (
<Alert variant="warning" className="mb-2">
<AlertDescription className="flex items-center justify-between text-inherit">
<span>
<Loader2 className="mr-2 inline size-4 animate-spin align-text-bottom" />
Currently fetching spend data: fetched {progress.currentPage} / {progress.totalPages} pages. Charts will
update periodically as data loads. Moving off of this page will stop and reset this. To continue using the
UI in the meantime,{" "}
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExternalLink className="inline size-3.5 align-text-bottom" />
</a>
.
</span>
<Button variant="destructive" onClick={cancel}>
Stop
</Button>
</AlertDescription>
</Alert>
)}
{cancelled && (
<Alert variant="info" className="mb-2">
<AlertDescription className="text-inherit">
Showing partial data ({progress.currentPage}/{progress.totalPages} pages loaded)
</AlertDescription>
</Alert>
)}
{agentIsFetchingMore && showAgentBreakdown && (
<Alert variant="warning" className="mb-2">
<AlertDescription className="flex items-center justify-between text-inherit">
<span>
<Loader2 className="mr-2 inline size-4 animate-spin align-text-bottom" />
Currently fetching agent data: fetched {agentProgress.currentPage} / {agentProgress.totalPages} pages.
Charts will update periodically as data loads. Moving off of this page will stop and reset this. To
continue using the UI in the meantime,{" "}
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExternalLink className="inline size-3.5 align-text-bottom" />
</a>
.
</span>
<Button variant="destructive" onClick={agentCancel}>
Stop
</Button>
</AlertDescription>
</Alert>
)}
{agentCancelled && showAgentBreakdown && (
<Alert variant="info" className="mb-2">
<AlertDescription className="text-inherit">
Showing partial agent data ({agentProgress.currentPage}/{agentProgress.totalPages} pages loaded)
</AlertDescription>
</Alert>
<PaginationStatusAlerts
isFetchingMore={isFetchingMore}
cancelled={cancelled}
progress={progress}
cancel={cancel}
/>
{showAgentBreakdown && (
<PaginationStatusAlerts
isFetchingMore={agentIsFetchingMore}
cancelled={agentCancelled}
progress={agentProgress}
cancel={agentCancel}
subject="agent data"
/>
)}
<UsageExportHeader
dateValue={dateValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* Works at 1m+ spend logs, by querying an aggregate table instead.
*/

import { ChevronDown, ChevronRight, Download, ExternalLink, Info, Loader2, Sparkles, X } from "lucide-react";
import { ChevronDown, ChevronRight, Download, Info, Sparkles, X } from "lucide-react";
import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { BarChart } from "@/components/shared/charts";
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import PaginationStatusAlerts from "@/components/shared/PaginationStatusAlerts";
import { Button } from "@/components/ui/button";
import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Expand Down Expand Up @@ -473,33 +474,12 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
/>
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
</div>
{paginatedResult.isFetchingMore && (
<Alert variant="warning" className="mb-2">
<AlertDescription className="flex items-center justify-between text-inherit">
<span>
<Loader2 className="mr-2 inline size-4 animate-spin align-text-bottom" />
Currently fetching spend data: fetched {paginatedResult.progress.currentPage} /{" "}
{paginatedResult.progress.totalPages} pages. Charts will update periodically as data loads. Moving off
of this page will stop and reset this. To continue using the UI in the meantime,{" "}
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExternalLink className="inline size-3.5 align-text-bottom" />
</a>
.
</span>
<Button variant="destructive" onClick={paginatedResult.cancel}>
Stop
</Button>
</AlertDescription>
</Alert>
)}
{paginatedResult.cancelled && (
<Alert variant="info" className="mb-2">
<AlertDescription className="text-inherit">
Showing partial data ({paginatedResult.progress.currentPage}/{paginatedResult.progress.totalPages} pages
loaded)
</AlertDescription>
</Alert>
)}
<PaginationStatusAlerts
isFetchingMore={paginatedResult.isFetchingMore}
cancelled={paginatedResult.cancelled}
progress={paginatedResult.progress}
cancel={paginatedResult.cancel}
/>
{/* Your Usage / Global Usage Panel */}
{(usageView === "global" || usageView === "my-usage") && (
<>
Expand Down
Loading
Loading