fix(ui): surface the paginated fallback on Cost Optimization (#37659)

* fix(ui): surface the paginated fallback on Cost Optimization

The page streamed its fallback silently: useDailyActivityRange dropped
the hook's progress and cancel fields and CacheLeakageCard only showed
a loading state while empty. Extract the Usage page's fetch banner into
a shared PaginationStatusAlerts component, render it above the tabs,
and note on the cache leakage tables when pages are still arriving.

* fix(ui): gate the cache leakage streaming note on isFetchingMore only

loading also covers a fresh aggregated request over the previous
range's rows, where pagination copy mislabels stale data. Drop the
redundant component comment flagged against the repo comment policy.
This commit is contained in:
tin-berri 2026-08-20 14:55:00 -07:00 committed by GitHub
parent d556fac56b
commit 60e03bedcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 244 additions and 86 deletions

View file

@ -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,
@ -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={{
@ -68,6 +69,10 @@ const renderWith = (results: DailyData[]) =>
results,
loading: false,
isFetchingMore: false,
progress: { currentPage: 1, totalPages: 1 },
cancelled: false,
cancel: vi.fn(),
...overrides,
}}
/>,
);
@ -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();
});
});

View file

@ -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>
)}
{rows.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{loading || isFetchingMore ? "Loading..." : `No ${emptyNoun} usage in this range.`}

View file

@ -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>,
@ -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();
});
});

View file

@ -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";
@ -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">

View file

@ -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} />);

View file

@ -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(),
}}
/>,
);

View file

@ -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,
};
},
}));
@ -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"));

View file

@ -18,6 +18,9 @@ export interface DailyActivityRange {
results: DailyData[];
loading: boolean;
isFetchingMore: boolean;
progress: { currentPage: number; totalPages: number };
cancelled: boolean;
cancel: () => void;
}
export const useDailyActivityRange = (
@ -33,7 +36,7 @@ export const useDailyActivityRange = (
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({
fetchFn: userDailyActivityCall,
aggregatedFetchFn: userDailyActivityAggregatedCall,
args: [accessToken, startTime, endTime, effectiveUserId, true],
@ -46,5 +49,8 @@ export const useDailyActivityRange = (
results: data.results as DailyData[],
loading,
isFetchingMore,
progress,
cancelled,
cancel,
};
};

View file

@ -15,10 +15,9 @@ import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/compon
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";
@ -643,57 +642,20 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
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}

View file

@ -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";
@ -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") && (
<>

View file

@ -0,0 +1,62 @@
import { fireEvent, render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import PaginationStatusAlerts from "./PaginationStatusAlerts";
describe("PaginationStatusAlerts", () => {
it("shows page progress and wires the Stop button while fetching", () => {
const cancel = vi.fn();
const { getByRole, getByText } = render(
<PaginationStatusAlerts
isFetchingMore={true}
cancelled={false}
progress={{ currentPage: 7, totalPages: 42 }}
cancel={cancel}
/>,
);
expect(getByText(/Currently fetching spend data: fetched 7 \/ 42 pages/)).toBeInTheDocument();
fireEvent.click(getByRole("button", { name: "Stop" }));
expect(cancel).toHaveBeenCalledTimes(1);
});
it("shows the partial-data notice after a cancel, frozen at the last fetched page", () => {
const { getByText } = render(
<PaginationStatusAlerts
isFetchingMore={false}
cancelled={true}
progress={{ currentPage: 7, totalPages: 42 }}
cancel={vi.fn()}
/>,
);
expect(getByText("Showing partial spend data (7/42 pages loaded)")).toBeInTheDocument();
});
it("names the subject it is fetching", () => {
const { getByText } = render(
<PaginationStatusAlerts
isFetchingMore={true}
cancelled={false}
progress={{ currentPage: 1, totalPages: 3 }}
cancel={vi.fn()}
subject="agent data"
/>,
);
expect(getByText(/Currently fetching agent data: fetched 1 \/ 3 pages/)).toBeInTheDocument();
});
it("renders nothing when idle", () => {
const { container } = render(
<PaginationStatusAlerts
isFetchingMore={false}
cancelled={false}
progress={{ currentPage: 1, totalPages: 1 }}
cancel={vi.fn()}
/>,
);
expect(container).toBeEmptyDOMElement();
});
});

View file

@ -0,0 +1,51 @@
import { ExternalLink, Loader2 } from "lucide-react";
import { Alert, AlertDescription } from "@/components/shared/Alert";
import { Button } from "@/components/ui/button";
interface PaginationStatusAlertsProps {
isFetchingMore: boolean;
cancelled: boolean;
progress: { currentPage: number; totalPages: number };
cancel: () => void;
subject?: string;
}
const PaginationStatusAlerts = ({
isFetchingMore,
cancelled,
progress,
cancel,
subject = "spend data",
}: PaginationStatusAlertsProps) => (
<>
{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 {subject}: 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 {subject} ({progress.currentPage}/{progress.totalPages} pages loaded)
</AlertDescription>
</Alert>
)}
</>
);
export default PaginationStatusAlerts;