feat(ui): configure prompt caching request rows per page (#42842)

* feat(ui): configure prompt caching request rows per page

* style(ui): match prompt caching pagination arrows
This commit is contained in:
tin-berri 2026-09-23 18:25:50 -07:00 • committed by GitHub
parent 2340dcc30c
commit eef80535ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 143 additions and 36 deletions

View file

@ -1,5 +1,15 @@
import { Profiler } from "react";
import { act, fireEvent, renderWithProviders, screen, testQueryClient, waitFor, within } from "@/../tests/test-utils";
import userEvent from "@testing-library/user-event";
import {
act,
chooseSelectOption,
fireEvent,
renderWithProviders,
screen,
testQueryClient,
waitFor,
within,
} from "@/../tests/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { components } from "@/lib/http/schema";
@ -23,8 +33,13 @@ const request = (overrides: Partial<CacheRequest> = {}): CacheRequest => ({
net_savings: -0.0075,
...overrides,
});
const response = (requests: CacheRequest[], nextCursor: RequestsResponse["next_cursor"] = null) => {
const body: RequestsResponse = { requests, has_more: nextCursor !== null, next_cursor: nextCursor, page_size: 10 };
const response = (requests: CacheRequest[], nextCursor: RequestsResponse["next_cursor"] = null, pageSize = 10) => {
const body: RequestsResponse = {
requests,
has_more: nextCursor !== null,
next_cursor: nextCursor,
page_size: pageSize,
};
return Response.json(body);
};
const lastQuery = () => new URL(String(fetchMock.mock.calls.at(-1)?.[0]), "http://localhost").searchParams;
@ -100,18 +115,76 @@ describe("PromptCachingRequestsTable", () => {
const table = await screen.findByRole("table", { name: "Prompt caching requests" });
expect(within(table).getAllByRole("link")).toHaveLength(10);
expect(within(table).queryByRole("link", { name: "request-11" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "request-11" });
expect(within(screen.getByRole("table", { name: "Prompt caching requests" })).getAllByRole("link")).toHaveLength(1);
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Previous" }));
expect(screen.getByRole("button", { name: "Go to next page" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Go to previous page" }));
await screen.findByRole("link", { name: "request-1" });
expect(within(screen.getByRole("table", { name: "Prompt caching requests" })).getAllByRole("link")).toHaveLength(
10,
);
expect(screen.getByRole("button", { name: "Previous" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to previous page" })).toBeDisabled();
});
it.each([25, 50, 100])(
"restarts at page one with %i rows and retains the size across navigation and filters",
async (pageSize) => {
const user = userEvent.setup();
const rows = Array.from({ length: 101 }, (_, index) => request({ request_id: `request-${index + 1}` }));
fetchMock.mockImplementation(async (input) => {
const query = new URL(String(input), "http://localhost").searchParams;
const start = rows.findIndex((row) => row.request_id === query.get("cursor_request_id")) + 1;
const size = Number(query.get("page_size"));
const end = start + size;
const page = rows.slice(start, end);
const last = page.at(-1);
return response(
page,
end < rows.length && last ? { start_time: last.start_time, request_id: last.request_id } : null,
size,
);
});
renderWithProviders(<PromptCachingRequestsTable accessToken="token-a" dateValue={dates} />);
await screen.findByRole("link", { name: "request-1" });
expect(screen.getByRole("combobox", { name: "Rows per page" })).toHaveTextContent("10");
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "request-11" });
await chooseSelectOption(user, screen.getByRole("combobox", { name: "Rows per page" }), String(pageSize));
await screen.findByRole("link", { name: "request-1" });
expect(within(screen.getByRole("table", { name: "Prompt caching requests" })).getAllByRole("link")).toHaveLength(
pageSize,
);
expect(lastQuery().get("page_size")).toBe(String(pageSize));
expect(lastQuery().has("cursor_request_id")).toBe(false);
expect(lastQuery().has("cursor_start_time")).toBe(false);
expect(screen.getByText("Page 1")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Go to previous page" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: `request-${pageSize + 1}` });
expect(lastQuery().get("page_size")).toBe(String(pageSize));
expect(lastQuery().get("cursor_request_id")).toBe(`request-${pageSize}`);
expect(screen.getByText("Page 2")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Go to previous page" }));
await screen.findByRole("link", { name: "request-1" });
expect(within(screen.getByRole("table", { name: "Prompt caching requests" })).getAllByRole("link")).toHaveLength(
pageSize,
);
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: `request-${pageSize + 1}` });
fireEvent.click(screen.getByRole("tab", { name: "Cache hits" }));
await screen.findByRole("link", { name: "request-1" });
expect(lastQuery().get("filter")).toBe("hits");
expect(lastQuery().get("page_size")).toBe(String(pageSize));
expect(lastQuery().has("cursor_request_id")).toBe(false);
expect(screen.getByText("Page 1")).toBeInTheDocument();
expect(screen.getByRole("combobox", { name: "Rows per page" })).toHaveTextContent(String(pageSize));
},
);
it("forwards complete server cursors, goes back to prior cursors, and clears them for each caching filter", async () => {
fetchMock.mockImplementation(async (input) => {
const query = new URL(String(input), "http://localhost").searchParams;
@ -130,33 +203,33 @@ describe("PromptCachingRequestsTable", () => {
});
renderWithProviders(<PromptCachingRequestsTable accessToken="token-a" dateValue={dates} />);
await screen.findByRole("link", { name: "all-1" });
expect(screen.getByRole("button", { name: "Previous" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to previous page" })).toBeDisabled();
expect(lastQuery().has("page")).toBe(false);
expect(lastQuery().has("cursor_request_id")).toBe(false);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "all-2" });
expect(screen.getByText("Page 2")).toBeInTheDocument();
expect(lastQuery().get("cursor_start_time")).toBe(firstCursor.start_time);
expect(lastQuery().get("cursor_request_id")).toBe(firstCursor.request_id);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "all-3" });
expect(screen.getByText("Page 3")).toBeInTheDocument();
expect(lastQuery().get("cursor_start_time")).toBe(secondCursor.start_time);
expect(lastQuery().get("cursor_request_id")).toBe(secondCursor.request_id);
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to next page" })).toBeDisabled();
await testQueryClient.invalidateQueries({ refetchType: "none" });
fireEvent.click(screen.getByRole("button", { name: "Previous" }));
fireEvent.click(screen.getByRole("button", { name: "Go to previous page" }));
await screen.findByRole("link", { name: "all-2" });
await waitFor(() => expect(lastQuery().get("cursor_request_id")).toBe(firstCursor.request_id));
expect(lastQuery().get("cursor_start_time")).toBe(firstCursor.start_time);
expect(screen.getByText("Page 2")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Previous" }));
fireEvent.click(screen.getByRole("button", { name: "Go to previous page" }));
await screen.findByRole("link", { name: "all-1" });
await waitFor(() => expect(lastQuery().has("cursor_request_id")).toBe(false));
expect(lastQuery().has("cursor_start_time")).toBe(false);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "all-2" });
fireEvent.click(screen.getByRole("tab", { name: "LiteLLM injected" }));
@ -166,7 +239,7 @@ describe("PromptCachingRequestsTable", () => {
expect(lastQuery().has("cursor_request_id")).toBe(false);
expect(lastQuery().has("cursor_start_time")).toBe(false);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "injected-2" });
fireEvent.click(screen.getByRole("tab", { name: "Cache hits" }));
await screen.findByRole("link", { name: "hits-1" });
@ -203,7 +276,7 @@ describe("PromptCachingRequestsTable", () => {
);
const { rerender } = renderWithProviders(tree("token-a", dates));
await screen.findByRole("link", { name: "old-first" });
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Go to next page" }));
await screen.findByRole("link", { name: "old-second" });
const pending = Promise.withResolvers<Response>();
@ -253,7 +326,7 @@ describe("PromptCachingRequestsTable", () => {
expect(screen.getByRole("link", { name: "current-hit" })).toBeInTheDocument();
expect(screen.queryByRole("link", { name: "stale-all" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to next page" })).toBeDisabled();
});
it("offers retry after a failed read and shows the empty state after it succeeds", async () => {
@ -265,7 +338,7 @@ describe("PromptCachingRequestsTable", () => {
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(await screen.findByText("No matching prompt caching requests in this range")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to next page" })).toBeDisabled();
expect(fetchMock).toHaveBeenCalledTimes(2);
});

View file

@ -1,12 +1,14 @@
"use client";
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { ChevronLeft, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { apiClient } from "@/components/networking";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { LOG_ID_QUERY_PARAM } from "@/components/view_logs/logDetailRouting";
@ -18,6 +20,7 @@ import { benchmarksWindow as activityWindow } from "./useAutoRouterBenchmarks";
import type { DateRange } from "./useDailyActivityRange";
const REQUESTS_PATH = "/cost_optimization/prompt_caching/requests";
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
type RequestsEndpoint = paths[typeof REQUESTS_PATH]["get"];
type RequestsResponse = RequestsEndpoint["responses"][200]["content"]["application/json"];
type RequestsQuery = NonNullable<RequestsEndpoint["parameters"]["query"]>;
@ -31,10 +34,11 @@ interface PromptCachingRequestsTableProps {
export default function PromptCachingRequestsTable({ accessToken, dateValue }: PromptCachingRequestsTableProps) {
const [filter, setFilter] = useState<RequestFilter>("all");
const [pageSize, setPageSize] = useState(10);
const window = activityWindow(dateValue, new Date());
const startDate = window.start_date ? `${window.start_date}T00:00:00.000Z` : "";
const endDate = window.end_date ? `${window.end_date}T23:59:59.999Z` : "";
const scope = JSON.stringify([accessToken, startDate, endDate, filter]);
const scope = JSON.stringify([accessToken, startDate, endDate, filter, pageSize]);
const [pagination, setPagination] = useState<{ scope: string; cursors: readonly RequestCursor[] }>({
scope,
cursors: [null],
@ -52,7 +56,7 @@ export default function PromptCachingRequestsTable({ accessToken, dateValue }: P
start_date: startDate,
end_date: endDate,
filter,
page_size: 10,
page_size: pageSize,
cursor_start_time: cursor?.start_time,
cursor_request_id: cursor?.request_id,
};
@ -161,22 +165,52 @@ export default function PromptCachingRequestsTable({ accessToken, dateValue }: P
</TableBody>
</Table>
)}
<div className="mt-4 flex items-center justify-end gap-3">
<Button
variant="outline"
disabled={page === 1}
onClick={() => setPagination({ scope, cursors: cursors.slice(0, -1) })}
>
Previous
</Button>
<span className="text-sm text-muted-foreground">Page {page}</span>
<Button
variant="outline"
disabled={!requests.data.has_more || !nextCursor}
onClick={() => nextCursor && setPagination({ scope, cursors: [...cursors, nextCursor] })}
>
Next
</Button>
<div className="mt-4 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Rows per page</span>
<Select
value={String(pageSize)}
onValueChange={(value) => {
if (typeof value === "string") {
setPageSize(Number(value));
}
}}
>
<SelectTrigger size="sm" aria-label="Rows per page" className="w-[4.5rem]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((option) => (
<SelectItem key={option} value={String(option)}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground tabular-nums">Page {page}</span>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon-sm"
aria-label="Go to previous page"
disabled={page === 1}
onClick={() => setPagination({ scope, cursors: cursors.slice(0, -1) })}
>
<ChevronLeft />
</Button>
<Button
variant="outline"
size="icon-sm"
aria-label="Go to next page"
disabled={!requests.data.has_more || !nextCursor}
onClick={() => nextCursor && setPagination({ scope, cursors: [...cursors, nextCursor] })}
>
<ChevronRight />
</Button>
</div>
</div>
</div>
</>
)}