@@ -52,7 +50,7 @@ export function ProjectKeysTable({
pagination={pagination}
onPaginationChange={onPaginationChange}
rowCount={totalCount}
- pageSizeOptions={PAGE_SIZE_OPTIONS}
+ pageSizeOptions={PROJECT_KEYS_PAGE_SIZE_OPTIONS}
isLoading={isLoading}
isError={isError}
loadingMessage="Loading keys…"
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
index 2a8f7d11d8f..309da01b295 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx
@@ -209,6 +209,7 @@ describe("ProjectsPage", () => {
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 1");
});
await waitFor(() => expect(onUrlUpdate.mock.calls.at(-1)?.[0].queryString).toBe("?project_search=Project+01"));
+ expect(onUrlUpdate).toHaveBeenCalledTimes(2);
});
it("should restore the search box and filtered list from a ?project_search= deep link", () => {
@@ -280,7 +281,8 @@ describe("ProjectsPage", () => {
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
renderWithProviders(
, {
- searchParams: "?page=2&project_search=Project&project=proj-1&keys_page=3&keys_page_size=10&keys_search=prod",
+ searchParams:
+ "?page=2&project_search=Project&project=proj-1&keys_page=3&keys_page_size=10&keys_search=prod&keys_sort_by=spend&keys_sort_order=asc",
onUrlUpdate,
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx
index 523932007fa..aaecf98d4ab 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsTable.test.tsx
@@ -73,7 +73,7 @@ describe("ProjectsTable pagination URL state", () => {
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 11-14 of 14");
});
- it("should write ?page=2 to the URL when the next page control is clicked", async () => {
+ it("should push ?page=2 onto history when the next page control is clicked", async () => {
const user = userEvent.setup();
const onUrlUpdate = vi.fn();
renderTable({ onUrlUpdate });
@@ -84,6 +84,7 @@ describe("ProjectsTable pagination URL state", () => {
const [update] = onUrlUpdate.mock.calls[0];
expect(update.searchParams.get("page")).toBe("2");
expect(update.searchParams.has("page_size")).toBe(false);
+ expect(update.options.history).toBe("push");
expect(firstDataRow().getByText("Project 11")).toBeInTheDocument();
});
@@ -147,6 +148,7 @@ describe("ProjectsTable pagination URL state", () => {
const lastUpdate = onUrlUpdate.mock.calls.at(-1)?.[0];
expect(lastUpdate.searchParams.get("page")).toBeNull();
expect(lastUpdate.searchParams.get("page_size")).toBe("25");
+ expect(lastUpdate.options.history).toBe("push");
});
it("should apply both params from a ?page=2&page_size=25 deep link so the restored view matches", () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/useProjectsUrlState.ts b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/useProjectsUrlState.ts
index db88ad7c3a9..57c1a8fd167 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/useProjectsUrlState.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/useProjectsUrlState.ts
@@ -1,12 +1,11 @@
+import { functionalUpdate, type OnChangeFn, type PaginationState } from "@tanstack/react-table";
import { useUrlTableState, type UrlTableState, type UrlTableStateOptions } from "@/components/shared/DataTable";
-import { parseAsString, useQueryStates } from "nuqs";
-import { useCallback } from "react";
+import { parseAsInteger, useQueryStates } from "nuqs";
+import { useCallback, useMemo } from "react";
export const PROJECTS_DEFAULT_PAGE_SIZE = 10;
export const PROJECT_KEYS_DEFAULT_PAGE_SIZE = 5;
-
-const PROJECT_KEYS_URL_PREFIX = "keys_";
-const TABLE_STATE_URL_KEYS = ["search", "sort_by", "sort_order", "page", "page_size"] as const;
+export const PROJECT_KEYS_PAGE_SIZE_OPTIONS = [PROJECT_KEYS_DEFAULT_PAGE_SIZE, 10, 25];
const PROJECTS_TABLE_STATE_OPTIONS: UrlTableStateOptions
= {
sortFields: [],
@@ -16,24 +15,65 @@ const PROJECTS_TABLE_STATE_OPTIONS: UrlTableStateOptions = {
urlKeys: { search: "project_search" },
};
+const PROJECTS_PAGE_PARAMS = {
+ page: parseAsInteger.withDefault(1),
+ page_size: parseAsInteger.withDefault(PROJECTS_DEFAULT_PAGE_SIZE),
+};
+
const PROJECT_KEYS_TABLE_STATE_OPTIONS: UrlTableStateOptions = {
sortFields: [],
defaultSort: { id: "created_at", desc: true },
defaultPageSize: PROJECT_KEYS_DEFAULT_PAGE_SIZE,
- maxPageSize: 25,
+ maxPageSize: Math.max(...PROJECT_KEYS_PAGE_SIZE_OPTIONS),
filterColumns: [],
- keyPrefix: PROJECT_KEYS_URL_PREFIX,
+ keyPrefix: "keys_",
};
-const PROJECT_KEYS_URL_STATE = Object.fromEntries(
- TABLE_STATE_URL_KEYS.map((key) => [`${PROJECT_KEYS_URL_PREFIX}${key}`, parseAsString]),
-);
+export function useProjectsTableState(): UrlTableState {
+ const tableState = useUrlTableState(PROJECTS_TABLE_STATE_OPTIONS);
+ const [, setPageParams] = useQueryStates(PROJECTS_PAGE_PARAMS, { history: "push" });
+ const { pagination } = tableState;
-export const useProjectsTableState = (): UrlTableState => useUrlTableState(PROJECTS_TABLE_STATE_OPTIONS);
+ const onPaginationChange = useCallback>(
+ (updaterOrValue) => {
+ const next = functionalUpdate(updaterOrValue, pagination);
+ void setPageParams({ page: next.pageIndex + 1, page_size: next.pageSize });
+ },
+ [pagination, setPageParams],
+ );
-export const useProjectKeysTableState = (): UrlTableState => useUrlTableState(PROJECT_KEYS_TABLE_STATE_OPTIONS);
+ return useMemo(() => ({ ...tableState, onPaginationChange }), [tableState, onPaginationChange]);
+}
+
+export function useProjectKeysTableState(): UrlTableState {
+ const tableState = useUrlTableState(PROJECT_KEYS_TABLE_STATE_OPTIONS);
+ const { pagination: urlPagination, onPaginationChange: writePagination } = tableState;
+ const pageSize = PROJECT_KEYS_PAGE_SIZE_OPTIONS.includes(urlPagination.pageSize)
+ ? urlPagination.pageSize
+ : PROJECT_KEYS_DEFAULT_PAGE_SIZE;
+
+ const pagination = useMemo(
+ () => ({ pageIndex: urlPagination.pageIndex, pageSize }),
+ [urlPagination.pageIndex, pageSize],
+ );
+
+ const onPaginationChange = useCallback>(
+ (updaterOrValue) => writePagination(functionalUpdate(updaterOrValue, pagination)),
+ [pagination, writePagination],
+ );
+
+ return useMemo(
+ () => ({ ...tableState, pagination, onPaginationChange }),
+ [tableState, pagination, onPaginationChange],
+ );
+}
export function useClearProjectKeysTableState(): () => void {
- const [, setProjectKeysUrlState] = useQueryStates(PROJECT_KEYS_URL_STATE);
- return useCallback(() => void setProjectKeysUrlState(null), [setProjectKeysUrlState]);
+ const { setSearch, onSortingChange, onColumnFiltersChange, onPaginationChange } = useProjectKeysTableState();
+ return useCallback(() => {
+ setSearch("");
+ onSortingChange([]);
+ onColumnFiltersChange([]);
+ onPaginationChange({ pageIndex: 0, pageSize: PROJECT_KEYS_DEFAULT_PAGE_SIZE });
+ }, [setSearch, onSortingChange, onColumnFiltersChange, onPaginationChange]);
}