mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): keep projects search and project key table state in the URL
The projects list search lives in ?project_search= and its pagination now goes through useUrlTableState, keeping the page and page_size keys. The key table inside a project reads keys_search, keys_page and keys_page_size, resets to its first page on a new search, and no longer snaps a deep-linked page while the key fetch is failing. Closing a project drops its keys_ params so they do not leak into the next project
This commit is contained in:
parent
417cc5c4fb
commit
df2dd9b7f2
8 changed files with 223 additions and 35 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderWithProviders, screen } from "../../../../../tests/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { OnUrlUpdateFunction } from "nuqs/adapters/testing";
|
||||
import { fireEvent, renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
|
||||
import { ProjectKeysSection } from "./ProjectKeysSection";
|
||||
|
||||
const mockUseKeys = vi.fn();
|
||||
|
|
@ -70,3 +72,108 @@ describe("ProjectKeysSection", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectKeysSection URL state (keys_ prefix)", () => {
|
||||
const fortyTwoKeys = {
|
||||
data: { keys: [], total_count: 42, current_page: 1, total_pages: 9 },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
};
|
||||
const lastSearchParams = (onUrlUpdate: Mock<OnUrlUpdateFunction>) => onUrlUpdate.mock.calls.at(-1)?.[0].searchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseKeys.mockReset();
|
||||
});
|
||||
|
||||
it("should fetch the page, page size and key name filter named by the keys_ params", () => {
|
||||
mockUseKeys.mockReturnValue(fortyTwoKeys);
|
||||
renderWithProviders(<ProjectKeysSection projectId="proj-1" />, {
|
||||
searchParams: "?page=4&keys_page=2&keys_page_size=10&keys_search=prod",
|
||||
});
|
||||
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(
|
||||
2,
|
||||
10,
|
||||
expect.objectContaining({ projectID: "proj-1", selectedKeyAlias: "prod" }),
|
||||
);
|
||||
expect(screen.getByPlaceholderText("Filter by key name...")).toHaveValue("prod");
|
||||
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 2 of 5");
|
||||
});
|
||||
|
||||
it("should write the key name filter to ?keys_search= and return the keys to their first page", async () => {
|
||||
mockUseKeys.mockReturnValue(fortyTwoKeys);
|
||||
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>();
|
||||
renderWithProviders(<ProjectKeysSection projectId="proj-1" />, {
|
||||
searchParams: "?page=4&keys_page=3",
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Filter by key name..."), { target: { value: "prod" } });
|
||||
|
||||
await waitFor(() => expect(lastSearchParams(onUrlUpdate)?.get("keys_search")).toBe("prod"));
|
||||
expect(lastSearchParams(onUrlUpdate)?.has("keys_page")).toBe(false);
|
||||
expect(lastSearchParams(onUrlUpdate)?.get("page")).toBe("4");
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(1, 5, expect.objectContaining({ selectedKeyAlias: "prod" }));
|
||||
});
|
||||
|
||||
it("should remove ?keys_search= when the key filter is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseKeys.mockReturnValue(fortyTwoKeys);
|
||||
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>();
|
||||
renderWithProviders(<ProjectKeysSection projectId="proj-1" />, { searchParams: "?keys_search=prod", onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /clear key filter/i }));
|
||||
|
||||
await waitFor(() => expect(lastSearchParams(onUrlUpdate)?.has("keys_search")).toBe(false));
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(1, 5, expect.objectContaining({ selectedKeyAlias: null }));
|
||||
});
|
||||
|
||||
it("should write key pages to ?keys_page= without touching the projects list's ?page=", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseKeys.mockReturnValue(fortyTwoKeys);
|
||||
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>();
|
||||
renderWithProviders(<ProjectKeysSection projectId="proj-1" />, { searchParams: "?page=4", onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByTestId("pagination-next"));
|
||||
|
||||
await waitFor(() => expect(lastSearchParams(onUrlUpdate)?.get("keys_page")).toBe("2"));
|
||||
expect(lastSearchParams(onUrlUpdate)?.get("page")).toBe("4");
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(2, 5, expect.anything());
|
||||
});
|
||||
|
||||
it("should snap a ?keys_page= past the last page back to the last page once the keys load", async () => {
|
||||
mockUseKeys.mockReturnValue({ data: undefined, isLoading: true, isError: false });
|
||||
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>();
|
||||
const { rerender } = renderWithProviders(<ProjectKeysSection projectId="proj-1" />, {
|
||||
searchParams: "?keys_page=9",
|
||||
onUrlUpdate,
|
||||
});
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(9, 5, expect.anything());
|
||||
|
||||
mockUseKeys.mockReturnValue({
|
||||
data: { keys: [], total_count: 6, current_page: 9, total_pages: 2 },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
rerender(<ProjectKeysSection projectId="proj-1" />);
|
||||
|
||||
await waitFor(() => expect(lastSearchParams(onUrlUpdate)?.get("keys_page")).toBe("2"));
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(2, 5, expect.anything());
|
||||
});
|
||||
|
||||
it("should keep a deep-linked ?keys_page= when the key fetch fails", async () => {
|
||||
mockUseKeys.mockReturnValue({ data: undefined, isLoading: true, isError: false });
|
||||
const onUrlUpdate = vi.fn<OnUrlUpdateFunction>();
|
||||
const { rerender } = renderWithProviders(<ProjectKeysSection projectId="proj-1" />, {
|
||||
searchParams: "?keys_page=3",
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
mockUseKeys.mockReturnValue({ data: undefined, isLoading: false, isError: true });
|
||||
rerender(<ProjectKeysSection projectId="proj-1" />);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(onUrlUpdate).not.toHaveBeenCalled();
|
||||
expect(mockUseKeys).toHaveBeenLastCalledWith(3, 5, expect.anything());
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
|
||||
import { PaginationState } from "@tanstack/react-table";
|
||||
import { KeyIcon, SearchIcon, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { ProjectKeysTable } from "./ProjectKeysTable";
|
||||
import { useProjectKeysTableState } from "./useProjectsUrlState";
|
||||
|
||||
interface ProjectKeysSectionProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export function ProjectKeysSection({ projectId }: ProjectKeysSectionProps) {
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: PAGE_SIZE });
|
||||
const [keyAlias, setKeyAlias] = useState<string>("");
|
||||
const {
|
||||
search: keyAlias,
|
||||
setSearch: setKeyAlias,
|
||||
pagination,
|
||||
onPaginationChange: setPagination,
|
||||
} = useProjectKeysTableState();
|
||||
|
||||
const { data, isLoading } = useKeys(pagination.pageIndex + 1, pagination.pageSize, {
|
||||
const { data, isLoading, isError } = useKeys(pagination.pageIndex + 1, pagination.pageSize, {
|
||||
projectID: projectId,
|
||||
selectedKeyAlias: keyAlias || null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPagination((current) => ({ ...current, pageIndex: 0 }));
|
||||
}, [keyAlias]);
|
||||
|
||||
const keys = data?.keys ?? [];
|
||||
const totalCount = data?.total_count ?? 0;
|
||||
|
||||
|
|
@ -60,6 +57,7 @@ export function ProjectKeysSection({ projectId }: ProjectKeysSectionProps) {
|
|||
keys={keys}
|
||||
totalCount={totalCount}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,16 +8,18 @@ import { KeyResponse } from "@/components/key_team_helpers/key_list";
|
|||
import { DataTable } from "@/components/shared/DataTable";
|
||||
|
||||
import { getProjectKeysTableColumns } from "./ProjectKeysTableColumns";
|
||||
import { PROJECT_KEYS_DEFAULT_PAGE_SIZE } from "./useProjectsUrlState";
|
||||
|
||||
interface ProjectKeysTableProps {
|
||||
keys: KeyResponse[];
|
||||
totalCount: number;
|
||||
isLoading: boolean;
|
||||
isError?: boolean;
|
||||
pagination: PaginationState;
|
||||
onPaginationChange: OnChangeFn<PaginationState>;
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [5, 10, 25];
|
||||
const PAGE_SIZE_OPTIONS = [PROJECT_KEYS_DEFAULT_PAGE_SIZE, 10, 25];
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
|
|
@ -35,6 +37,7 @@ export function ProjectKeysTable({
|
|||
keys,
|
||||
totalCount,
|
||||
isLoading,
|
||||
isError = false,
|
||||
pagination,
|
||||
onPaginationChange,
|
||||
}: ProjectKeysTableProps) {
|
||||
|
|
@ -51,6 +54,7 @@ export function ProjectKeysTable({
|
|||
rowCount={totalCount}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
loadingMessage="Loading keys…"
|
||||
noDataMessage={<EmptyState />}
|
||||
size="compact"
|
||||
|
|
|
|||
|
|
@ -190,22 +190,47 @@ describe("ProjectsPage", () => {
|
|||
|
||||
it("should reset to the first page when the search text changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
const manyProjects = Array.from({ length: 12 }, (_, i) => ({
|
||||
...mockProjects[0],
|
||||
project_id: `proj-${i + 1}`,
|
||||
project_alias: `Project ${String(i + 1).padStart(2, "0")}`,
|
||||
}));
|
||||
mockUseProjects.mockReturnValue({ data: manyProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />);
|
||||
renderWithProviders(<ProjectsPage />, { onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByTestId("pagination-next"));
|
||||
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 2 of 2");
|
||||
await waitFor(() => expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get("page")).toBe("2"));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/search projects/i), { target: { value: "Project 01" } });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Project 01")).toBeInTheDocument();
|
||||
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"));
|
||||
});
|
||||
|
||||
it("should restore the search box and filtered list from a ?project_search= deep link", () => {
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, { searchParams: "?project_search=Beta" });
|
||||
|
||||
expect(screen.getByPlaceholderText(/search projects/i)).toHaveValue("Beta");
|
||||
expect(screen.getByText("Beta Project")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Alpha Project")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should remove ?project_search= when the search is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, { searchParams: "?project_search=Beta", onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /clear search/i }));
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenLastCalledWith(expect.objectContaining({ queryString: "" })));
|
||||
expect(screen.getByPlaceholderText(/search projects/i)).toHaveValue("");
|
||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open the detail view directly from a ?project= deep link", () => {
|
||||
|
|
@ -250,6 +275,23 @@ describe("ProjectsPage", () => {
|
|||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should drop the project's key table state but keep the list's search and page when the detail view is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, {
|
||||
searchParams: "?page=2&project_search=Project&project=proj-1&keys_page=3&keys_page_size=10&keys_search=prod",
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /back to projects/i }));
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalledTimes(1));
|
||||
const [update] = onUrlUpdate.mock.calls[0];
|
||||
expect(update.queryString).toBe("?page=2&project_search=Project");
|
||||
expect(update.options.history).toBe("replace");
|
||||
});
|
||||
|
||||
it("should resolve team alias from the teams list in the Team column", () => {
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [{ team_id: "team-1", team_alias: "Engineering", models: [] }],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "
|
|||
import { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
|
||||
import { ProjectDetail } from "./ProjectDetailsPage";
|
||||
import { ProjectsTable } from "./ProjectsTable";
|
||||
import { useClearProjectKeysTableState, useProjectsTableState } from "./useProjectsUrlState";
|
||||
|
||||
export function ProjectsPage() {
|
||||
const { data: projects, isLoading } = useProjects();
|
||||
|
|
@ -18,8 +19,9 @@ export function ProjectsPage() {
|
|||
"project",
|
||||
parseAsString.withOptions({ history: "push" }),
|
||||
);
|
||||
const clearProjectKeysTableState = useClearProjectKeysTableState();
|
||||
const { search: searchText, setSearch: setSearchText } = useProjectsTableState();
|
||||
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const teamAliasMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
|
|
@ -44,13 +46,13 @@ export function ProjectsPage() {
|
|||
});
|
||||
}, [projects, searchText, teamAliasMap]);
|
||||
|
||||
const closeProject = () => {
|
||||
void setSelectedProjectId(null, { history: "replace" });
|
||||
clearProjectKeysTableState();
|
||||
};
|
||||
|
||||
if (selectedProjectId) {
|
||||
return (
|
||||
<ProjectDetail
|
||||
projectId={selectedProjectId}
|
||||
onBack={() => void setSelectedProjectId(null, { history: "replace" })}
|
||||
/>
|
||||
);
|
||||
return <ProjectDetail projectId={selectedProjectId} onBack={closeProject} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ describe("ProjectsTable pagination URL state", () => {
|
|||
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 11-14 of 14");
|
||||
});
|
||||
|
||||
it("should push ?page=2 onto history when the next page control is clicked", async () => {
|
||||
it("should write ?page=2 to the URL when the next page control is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn();
|
||||
renderTable({ onUrlUpdate });
|
||||
|
|
@ -83,7 +83,7 @@ describe("ProjectsTable pagination URL state", () => {
|
|||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
|
||||
const [update] = onUrlUpdate.mock.calls[0];
|
||||
expect(update.searchParams.get("page")).toBe("2");
|
||||
expect(update.options.history).toBe("push");
|
||||
expect(update.searchParams.has("page_size")).toBe(false);
|
||||
expect(firstDataRow().getByText("Project 11")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
import { SortingState } from "@tanstack/react-table";
|
||||
import { FolderKanban } from "lucide-react";
|
||||
import { parseAsInteger, useQueryStates } from "nuqs";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
|
||||
import { DataTable, DataTablePagination } from "@/components/shared/DataTable";
|
||||
|
||||
import { getProjectsTableColumns } from "./ProjectsTableColumns";
|
||||
import { PROJECTS_DEFAULT_PAGE_SIZE, useProjectsTableState } from "./useProjectsUrlState";
|
||||
|
||||
interface ProjectsTableProps {
|
||||
projects: ProjectResponse[];
|
||||
|
|
@ -19,8 +19,7 @@ interface ProjectsTableProps {
|
|||
isTeamsLoading: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const PAGE_SIZE_OPTIONS = [DEFAULT_PAGE_SIZE, 25, 50];
|
||||
const PAGE_SIZE_OPTIONS = [PROJECTS_DEFAULT_PAGE_SIZE, 25, 50];
|
||||
|
||||
function EmptyState({ isFiltered }: { isFiltered: boolean }) {
|
||||
return (
|
||||
|
|
@ -47,11 +46,8 @@ export function ProjectsTable({
|
|||
isTeamsLoading,
|
||||
}: ProjectsTableProps) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [{ page, page_size }, setPagination] = useQueryStates(
|
||||
{ page: parseAsInteger.withDefault(1), page_size: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE) },
|
||||
{ history: "push" },
|
||||
);
|
||||
const pageSize = PAGE_SIZE_OPTIONS.includes(page_size) ? page_size : DEFAULT_PAGE_SIZE;
|
||||
const { pagination, onPaginationChange } = useProjectsTableState();
|
||||
const pageSize = PAGE_SIZE_OPTIONS.includes(pagination.pageSize) ? pagination.pageSize : PROJECTS_DEFAULT_PAGE_SIZE;
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const deps = { onProjectClick, teamAliasMap, isTeamsLoading };
|
||||
|
|
@ -59,7 +55,7 @@ export function ProjectsTable({
|
|||
}, [onProjectClick, teamAliasMap, isTeamsLoading]);
|
||||
|
||||
const pageCount = Math.max(Math.ceil(projects.length / pageSize), 1);
|
||||
const pageIndex = page >= 1 && page <= pageCount ? page - 1 : 0;
|
||||
const pageIndex = pagination.pageIndex < pageCount ? pagination.pageIndex : 0;
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
|
|
@ -77,8 +73,8 @@ export function ProjectsTable({
|
|||
page={pageIndex}
|
||||
pageSize={pageSize}
|
||||
rowCount={projects.length}
|
||||
onPageChange={(nextPageIndex) => void setPagination({ page: nextPageIndex + 1 })}
|
||||
onPageSizeChange={(nextPageSize) => void setPagination({ page_size: nextPageSize, page: null })}
|
||||
onPageChange={(nextPageIndex) => onPaginationChange({ pageIndex: nextPageIndex, pageSize })}
|
||||
onPageSizeChange={(nextPageSize) => onPaginationChange({ pageIndex: 0, pageSize: nextPageSize })}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { useUrlTableState, type UrlTableState, type UrlTableStateOptions } from "@/components/shared/DataTable";
|
||||
import { parseAsString, useQueryStates } from "nuqs";
|
||||
import { useCallback } 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;
|
||||
|
||||
const PROJECTS_TABLE_STATE_OPTIONS: UrlTableStateOptions<never> = {
|
||||
sortFields: [],
|
||||
defaultSort: { id: "created_at", desc: true },
|
||||
defaultPageSize: PROJECTS_DEFAULT_PAGE_SIZE,
|
||||
filterColumns: [],
|
||||
urlKeys: { search: "project_search" },
|
||||
};
|
||||
|
||||
const PROJECT_KEYS_TABLE_STATE_OPTIONS: UrlTableStateOptions<never> = {
|
||||
sortFields: [],
|
||||
defaultSort: { id: "created_at", desc: true },
|
||||
defaultPageSize: PROJECT_KEYS_DEFAULT_PAGE_SIZE,
|
||||
maxPageSize: 25,
|
||||
filterColumns: [],
|
||||
keyPrefix: PROJECT_KEYS_URL_PREFIX,
|
||||
};
|
||||
|
||||
const PROJECT_KEYS_URL_STATE = Object.fromEntries(
|
||||
TABLE_STATE_URL_KEYS.map((key) => [`${PROJECT_KEYS_URL_PREFIX}${key}`, parseAsString]),
|
||||
);
|
||||
|
||||
export const useProjectsTableState = (): UrlTableState => useUrlTableState(PROJECTS_TABLE_STATE_OPTIONS);
|
||||
|
||||
export const useProjectKeysTableState = (): UrlTableState => useUrlTableState(PROJECT_KEYS_TABLE_STATE_OPTIONS);
|
||||
|
||||
export function useClearProjectKeysTableState(): () => void {
|
||||
const [, setProjectKeysUrlState] = useQueryStates(PROJECT_KEYS_URL_STATE);
|
||||
return useCallback(() => void setProjectKeysUrlState(null), [setProjectKeysUrlState]);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue