mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
d556fac56b
commit
60e03bedcf
12 changed files with 244 additions and 86 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.`}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} />);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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") && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Reference in a new issue