diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
index e6d144df65a..65de44bb309 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx
@@ -206,8 +206,6 @@ beforeEach(() => {
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [mockKey],
- filteredTotalCount: null,
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -306,6 +304,18 @@ it("should show loading message only on initial load (isPending)", () => {
it("should show 'No keys found' message when filteredKeys is empty", () => {
// Mock empty filteredKeys
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [],
+ total_count: 0,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -315,7 +325,6 @@ it("should show 'No keys found' message when filteredKeys is empty", () => {
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -343,6 +352,18 @@ it("should handle models with more than 3 entries to trigger expansion UI", () =
models: ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "claude-3", "claude-3-5-sonnet"],
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithManyModels],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -352,7 +373,6 @@ it("should handle models with more than 3 entries to trigger expansion UI", () =
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithManyModels],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -480,6 +500,18 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user
user: { user_id: "default_user_id", user_email: "", user_alias: null },
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithDefaultUserId],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -489,7 +521,6 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithDefaultUserId],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -519,6 +550,18 @@ it("should display 'Default Proxy Admin' for created_by when value is 'default_u
created_by: "default_user_id",
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithDefaultCreatedBy],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -528,7 +571,6 @@ it("should display 'Default Proxy Admin' for created_by when value is 'default_u
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithDefaultCreatedBy],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -566,6 +608,18 @@ it("should display created_by_user email in 'Created By' column when available",
},
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithCreatedByUser],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -575,7 +629,6 @@ it("should display created_by_user email in 'Created By' column when available",
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithCreatedByUser],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -610,6 +663,18 @@ it("should display created_by_user alias over email when both available", async
},
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithCreatedByUser],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -619,7 +684,6 @@ it("should display created_by_user alias over email when both available", async
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithCreatedByUser],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -649,6 +713,18 @@ it("should render table without crashing when models is null", async () => {
models: null as unknown as string[],
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithNullModels],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -658,7 +734,6 @@ it("should render table without crashing when models is null", async () => {
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithNullModels],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -689,6 +764,18 @@ it("should render table without crashing when models is undefined", async () =>
models: undefined as unknown as string[],
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithUndefinedModels],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -698,7 +785,6 @@ it("should render table without crashing when models is undefined", async () =>
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithUndefinedModels],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -764,6 +850,18 @@ it("should display 'Unknown' for last_active when value is null", async () => {
last_active: null,
};
+ mockUseKeys.mockReturnValue({
+ data: {
+ keys: [keyWithNullLastActive],
+ total_count: 1,
+ current_page: 1,
+ total_pages: 1,
+ } as KeysResponse,
+ isPending: false,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
mockUseFilterLogic.mockReturnValue({
filters: {
"Team ID": "",
@@ -773,7 +871,6 @@ it("should display 'Unknown' for last_active when value is null", async () => {
"Sort By": "created_at",
"Sort Order": "desc",
},
- filteredKeys: [keyWithNullLastActive],
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -820,8 +917,6 @@ describe("pagination display – total count and page count", () => {
mockUseFilterLogic.mockReturnValue({
filters: { "Team ID": "", "Organization ID": "", "Key Alias": "", "User ID": "", "Sort By": "created_at", "Sort Order": "desc" },
- filteredKeys: [mockKey],
- filteredTotalCount: null,
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -836,13 +931,15 @@ describe("pagination display – total count and page count", () => {
});
});
- it("should show filteredTotalCount in pagination text when a filter search returns results", async () => {
+ it("should show the server-returned filtered total when a filter is active", async () => {
+ // With the filter wired into useKeys, the server returns the filtered
+ // total_count directly — the component has no separate client-side total.
mockUseKeys.mockReturnValue({
data: {
keys: [mockKey],
- total_count: 509,
+ total_count: 1,
current_page: 1,
- total_pages: 11,
+ total_pages: 1,
} as KeysResponse,
isPending: false,
isFetching: false,
@@ -851,8 +948,6 @@ describe("pagination display – total count and page count", () => {
mockUseFilterLogic.mockReturnValue({
filters: { "Team ID": "", "Organization ID": "", "Key Alias": "aaaaa", "User ID": "", "Sort By": "created_at", "Sort Order": "desc" },
- filteredKeys: [mockKey],
- filteredTotalCount: 1,
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -867,23 +962,9 @@ describe("pagination display – total count and page count", () => {
});
});
- it("should not show stale unfiltered totals when filteredTotalCount is set", async () => {
- mockUseKeys.mockReturnValue({
- data: {
- keys: [mockKey],
- total_count: 509,
- current_page: 1,
- total_pages: 11,
- } as KeysResponse,
- isPending: false,
- isFetching: false,
- refetch: vi.fn(),
- } as any);
-
+ it("should pass current filter values into useKeys so remount/refetch keeps the filter applied", async () => {
mockUseFilterLogic.mockReturnValue({
filters: { "Team ID": "", "Organization ID": "", "Key Alias": "aaaaa", "User ID": "", "Sort By": "created_at", "Sort Order": "desc" },
- filteredKeys: [mockKey],
- filteredTotalCount: 1,
allTeams: [mockTeam],
allOrganizations: [mockOrganization],
handleFilterChange: vi.fn(),
@@ -892,9 +973,14 @@ describe("pagination display – total count and page count", () => {
renderWithProviders();
+ // useKeys must be invoked with the selected alias so that a later refetch
+ // (e.g. after navigating away and back) still fetches the filtered set.
await waitFor(() => {
- expect(screen.queryByText(/509 results/)).not.toBeInTheDocument();
- expect(screen.queryByText(/of 11/)).not.toBeInTheDocument();
+ expect(mockUseKeys).toHaveBeenCalledWith(
+ expect.any(Number),
+ expect.any(Number),
+ expect.objectContaining({ selectedKeyAlias: "aaaaa" }),
+ );
});
});
});
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
index b30d4b6ce5b..2dfc6f45f29 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
@@ -81,6 +81,32 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
const sortBy = sorting.length > 0 ? sorting[0].id : null;
const sortOrder = sorting.length > 0 ? (sorting[0].desc ? "desc" : "asc") : null;
+ // Use the filter logic hook
+ const { filters, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
+ useFilterLogic({
+ teams,
+ organizations,
+ });
+
+ // Debounce filter values before feeding them into useKeys so that each
+ // keystroke in a text filter (e.g. Key Alias) doesn't fire a request.
+ const [debouncedFilters, setDebouncedFilters] = useState(filters);
+ useEffect(() => {
+ const timer = setTimeout(() => setDebouncedFilters(filters), 300);
+ return () => clearTimeout(timer);
+ }, [filters]);
+
+ // When active filters change, jump back to the first page — otherwise we
+ // may land on an out-of-range page of the filtered result set.
+ useEffect(() => {
+ setTablePagination((prev) => (prev.pageIndex === 0 ? prev : { ...prev, pageIndex: 0 }));
+ }, [
+ debouncedFilters["Team ID"],
+ debouncedFilters["Organization ID"],
+ debouncedFilters["Key Alias"],
+ debouncedFilters["User ID"],
+ ]);
+
const {
data: keys,
isPending: isLoading,
@@ -91,18 +117,13 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
sortBy: sortBy || undefined,
sortOrder: sortOrder || undefined,
expand: "user",
+ teamID: debouncedFilters["Team ID"] || undefined,
+ organizationID: debouncedFilters["Organization ID"] || undefined,
+ selectedKeyAlias: debouncedFilters["Key Alias"] || undefined,
+ userID: debouncedFilters["User ID"] || undefined,
});
const [expandedAccordions, setExpandedAccordions] = useState>({});
- // Use the filter logic hook
-
- const { filters, filteredKeys, filteredTotalCount, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
- useFilterLogic({
- keys: keys?.keys || [],
- teams,
- organizations,
- });
-
// Defer the transition so the button stays in loading state until the table
// has rendered with the new data (mirrors the spend-logs pattern)
const isFetchingDeferred = useDeferredValue(isFetching);
@@ -112,7 +133,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
refetch();
};
- const totalCount = filteredTotalCount ?? keys?.total_count ?? 0;
+ const totalCount = keys?.total_count ?? 0;
// Add a useEffect to call refresh when a key is created
useEffect(() => {
@@ -618,7 +639,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
];
const table = useReactTable({
- data: filteredKeys,
+ data: keys?.keys ?? [],
columns: columns.filter((col) => col.id !== "expander"),
columnResizeMode: "onChange",
columnResizeDirection: "ltr",
@@ -633,16 +654,6 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
const sortState = newSorting[0];
const sortBy = sortState.id;
const sortOrder = sortState.desc ? "desc" : "asc";
- // Update filters state without triggering debouncedSearch
- // The useKeys hook will automatically refetch with the new sort parameters
- handleFilterChange(
- {
- ...filters,
- "Sort By": sortBy,
- "Sort Order": sortOrder,
- },
- true, // skipDebounce - let useKeys handle the API call with correct page size
- );
onSortChange?.(sortBy, sortOrder);
}
},
@@ -833,7 +844,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
- ) : filteredKeys.length > 0 ? (
+ ) : (keys?.keys?.length ?? 0) > 0 ? (
table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => (
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx
index 4e3cda123ca..0158f159cba 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx
@@ -1,149 +1,90 @@
-import { act, renderHook, waitFor } from "@testing-library/react";
+import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useFilterLogic } from "./filter_logic";
-import { keyListCall } from "../networking";
-
-vi.mock("../networking", () => ({
- keyListCall: vi.fn(),
-}));
vi.mock("./filter_helpers", () => ({
fetchAllTeams: vi.fn().mockResolvedValue([]),
fetchAllOrganizations: vi.fn().mockResolvedValue([]),
}));
-const mockKey = {
- token: "abc123",
- key_alias: "aaaaa",
- team_id: null,
- organization_id: null,
-};
-
const defaultProps = {
- keys: [mockKey] as any[],
teams: [],
organizations: [],
};
-const makeApiResponse = (overrides: { keys?: any[]; total_count?: number; total_pages?: number } = {}) => ({
- keys: overrides.keys ?? [mockKey],
- total_count: overrides.total_count ?? 1,
- current_page: 1,
- total_pages: overrides.total_pages ?? 1,
-});
+const DEFAULT_FILTERS = {
+ "Team ID": "",
+ "Organization ID": "",
+ "Key Alias": "",
+ "User ID": "",
+ "Sort By": "created_at",
+ "Sort Order": "desc",
+};
-describe("useFilterLogic – filteredTotalCount", () => {
+describe("useFilterLogic – filter state management", () => {
beforeEach(() => {
vi.clearAllMocks();
- vi.mocked(keyListCall).mockResolvedValue(makeApiResponse({ total_count: 509, total_pages: 11 }));
});
- it("should expose filteredTotalCount as null before any filter search runs", () => {
+ it("starts with default (empty) filter values", () => {
const { result } = renderHook(() => useFilterLogic(defaultProps));
- expect(result.current.filteredTotalCount).toBeNull();
+ expect(result.current.filters).toEqual(DEFAULT_FILTERS);
});
- it("should set filteredTotalCount to the API total_count after a Key Alias filter is applied", async () => {
- vi.mocked(keyListCall).mockResolvedValue(makeApiResponse({ keys: [mockKey], total_count: 1, total_pages: 1 }));
-
- const { result } = renderHook(() => useFilterLogic(defaultProps));
-
- act(() => {
- result.current.handleFilterChange({ "Key Alias": "aaaaa" });
- });
-
- await waitFor(() => {
- expect(result.current.filteredTotalCount).toBe(1);
- }, { timeout: 500 });
- });
-
- it("should reflect the filtered total_count even when it differs from the full key count", async () => {
- vi.mocked(keyListCall).mockResolvedValue(makeApiResponse({ total_count: 7, total_pages: 1 }));
-
- const { result } = renderHook(() => useFilterLogic(defaultProps));
-
- act(() => {
- result.current.handleFilterChange({ "Team ID": "team-x" });
- });
-
- await waitFor(() => {
- expect(result.current.filteredTotalCount).toBe(7);
- }, { timeout: 500 });
- });
-
- it("should reset filteredTotalCount to null when handleFilterReset is called", async () => {
- vi.mocked(keyListCall).mockResolvedValue(makeApiResponse({ total_count: 1 }));
-
- const { result } = renderHook(() => useFilterLogic(defaultProps));
-
- act(() => {
- result.current.handleFilterChange({ "Key Alias": "aaaaa" });
- });
-
- await waitFor(() => {
- expect(result.current.filteredTotalCount).toBe(1);
- }, { timeout: 500 });
-
- act(() => {
- result.current.handleFilterReset();
- });
-
- // filteredTotalCount resets synchronously before the debounced reset search completes
- expect(result.current.filteredTotalCount).toBeNull();
- });
-
- it("should pass the Key Alias value to keyListCall", async () => {
- vi.mocked(keyListCall).mockResolvedValue(makeApiResponse({ total_count: 2 }));
-
+ it("updates the Key Alias filter when handleFilterChange is called", () => {
const { result } = renderHook(() => useFilterLogic(defaultProps));
act(() => {
result.current.handleFilterChange({ "Key Alias": "my-alias" });
});
- await waitFor(() => {
- expect(keyListCall).toHaveBeenCalledWith(
- expect.any(String), // accessToken
- null, // organizationID (empty → null)
- null, // teamID (empty → null)
- "my-alias", // selectedKeyAlias ← the filter value
- null, // userID
- null, // keyHash
- 1, // page (resets to 1 on filter change)
- expect.any(Number),// pageSize (defaultPageSize)
- expect.anything(), // sortBy
- expect.anything(), // sortOrder
- );
- }, { timeout: 500 });
+ expect(result.current.filters["Key Alias"]).toBe("my-alias");
});
- it("should not update filteredTotalCount when keyListCall throws", async () => {
- vi.mocked(keyListCall).mockRejectedValue(new Error("Network error"));
+ it("preserves filter state across re-renders (regression: bug where results reset on remount)", () => {
+ const { result, rerender } = renderHook(() => useFilterLogic(defaultProps));
+ act(() => {
+ result.current.handleFilterChange({ "Key Alias": "keep-me" });
+ });
+ expect(result.current.filters["Key Alias"]).toBe("keep-me");
+
+ // Re-render with the same props; filter must remain applied so downstream
+ // useKeys is still invoked with the alias instead of falling back to "all".
+ rerender();
+ expect(result.current.filters["Key Alias"]).toBe("keep-me");
+ });
+
+ it("resets filters to defaults when handleFilterReset is called", () => {
const { result } = renderHook(() => useFilterLogic(defaultProps));
act(() => {
- result.current.handleFilterChange({ "Key Alias": "bad-alias" });
+ result.current.handleFilterChange({ "Team ID": "team-x", "Key Alias": "abc" });
});
+ expect(result.current.filters["Team ID"]).toBe("team-x");
+ expect(result.current.filters["Key Alias"]).toBe("abc");
- await waitFor(() => {
- expect(keyListCall).toHaveBeenCalled();
- }, { timeout: 500 });
-
- expect(result.current.filteredTotalCount).toBeNull();
+ act(() => {
+ result.current.handleFilterReset();
+ });
+ expect(result.current.filters).toEqual(DEFAULT_FILTERS);
});
- it("should not trigger a debounced search when skipDebounce is true", async () => {
+ it("handleFilterChange replaces the entire filter set (missing keys become empty)", () => {
const { result } = renderHook(() => useFilterLogic(defaultProps));
act(() => {
- result.current.handleFilterChange({ "Sort By": "spend", "Sort Order": "asc" }, true);
+ result.current.handleFilterChange({ "Team ID": "team-1", "Key Alias": "alpha" });
});
- await new Promise((resolve) => setTimeout(resolve, 350));
+ act(() => {
+ result.current.handleFilterChange({ "User ID": "user-1" });
+ });
- expect(keyListCall).not.toHaveBeenCalled();
- expect(result.current.filteredTotalCount).toBeNull();
+ expect(result.current.filters).toEqual({
+ ...DEFAULT_FILTERS,
+ "User ID": "user-1",
+ });
});
});
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx
index cd55477208c..3f762d26b7c 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx
@@ -1,10 +1,7 @@
-import { useCallback, useEffect, useState, useRef } from "react";
-import { KeyResponse } from "../key_team_helpers/key_list";
-import { keyListCall, Organization } from "../networking";
+import { useEffect, useState } from "react";
import { Team } from "../key_team_helpers/key_list";
+import { Organization } from "../networking";
import { fetchAllOrganizations, fetchAllTeams } from "./filter_helpers";
-import { debounce } from "lodash";
-import { defaultPageSize } from "../constants";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export interface FilterState {
@@ -17,133 +14,58 @@ export interface FilterState {
"Sort Order": string;
}
+const DEFAULT_FILTERS: FilterState = {
+ "Team ID": "",
+ "Organization ID": "",
+ "Key Alias": "",
+ "User ID": "",
+ "Sort By": "created_at",
+ "Sort Order": "desc",
+};
+
export function useFilterLogic({
- keys,
teams,
organizations,
}: {
- keys: KeyResponse[];
teams: Team[] | null;
organizations: Organization[] | null;
}) {
- const defaultFilters: FilterState = {
- "Team ID": "",
- "Organization ID": "",
- "Key Alias": "",
- "User ID": "",
- "Sort By": "created_at",
- "Sort Order": "desc",
- };
const { accessToken } = useAuthorized();
- const [filters, setFilters] = useState(defaultFilters);
+ const [filters, setFilters] = useState(DEFAULT_FILTERS);
const [allTeams, setAllTeams] = useState(teams || []);
const [allOrganizations, setAllOrganizations] = useState(organizations || []);
- const [filteredKeys, setFilteredKeys] = useState(keys);
- const [filteredTotalCount, setFilteredTotalCount] = useState(null);
- const lastSearchTimestamp = useRef(0);
- const debouncedSearch = useCallback(
- debounce(async (filters: FilterState) => {
- if (!accessToken) {
- return;
- }
- const currentTimestamp = Date.now();
- lastSearchTimestamp.current = currentTimestamp;
-
- try {
- // Make the API call using userListCall with all filter parameters
- const data = await keyListCall(
- accessToken,
- filters["Organization ID"] || null,
- filters["Team ID"] || null,
- filters["Key Alias"] || null,
- filters["User ID"] || null,
- filters["Key Hash"] || null,
- 1, // Reset to first page when searching
- defaultPageSize,
- filters["Sort By"] || null,
- filters["Sort Order"] || null,
- );
-
- // Only update state if this is the most recent search
- if (currentTimestamp === lastSearchTimestamp.current) {
- if (data) {
- setFilteredKeys(data.keys);
- setFilteredTotalCount(data.total_count ?? null);
- console.log("called from debouncedSearch filters:", JSON.stringify(filters));
- console.log("called from debouncedSearch data:", JSON.stringify(data));
- }
- }
- } catch (error) {
- console.error("Error searching users:", error);
- }
- }, 300),
- [accessToken],
- );
- // Apply filters to keys whenever keys or filters change
useEffect(() => {
- if (!keys) {
- setFilteredKeys([]);
- return;
- }
+ if (!accessToken) return;
- let result = [...keys];
-
- // Apply Team ID filter
- if (filters["Team ID"]) {
- result = result.filter((key) => key.team_id === filters["Team ID"]);
- }
-
- // Apply Organization ID filter
- if (filters["Organization ID"]) {
- result = result.filter((key) => (key.organization_id ?? key.org_id) === filters["Organization ID"]);
- }
-
- setFilteredKeys(result);
- }, [keys, filters]);
-
- // Fetch all data for filters when component mounts
- useEffect(() => {
const loadAllFilterData = async () => {
- // Load all teams - no organization filter needed here
const teamsData = await fetchAllTeams(accessToken);
if (teamsData.length > 0) {
setAllTeams(teamsData);
}
- // Load all organizations
const orgsData = await fetchAllOrganizations(accessToken);
if (orgsData.length > 0) {
setAllOrganizations(orgsData);
}
};
- if (accessToken) {
- loadAllFilterData();
- }
+ loadAllFilterData();
}, [accessToken]);
- // Update teams and organizations when props change
useEffect(() => {
if (teams && teams.length > 0) {
- setAllTeams((prevTeams) => {
- // Only update if we don't already have a larger set of teams
- return prevTeams.length < teams.length ? teams : prevTeams;
- });
+ setAllTeams((prevTeams) => (prevTeams.length < teams.length ? teams : prevTeams));
}
}, [teams]);
useEffect(() => {
if (organizations && organizations.length > 0) {
- setAllOrganizations((prevOrgs) => {
- // Only update if we don't already have a larger set of organizations
- return prevOrgs.length < organizations.length ? organizations : prevOrgs;
- });
+ setAllOrganizations((prevOrgs) => (prevOrgs.length < organizations.length ? organizations : prevOrgs));
}
}, [organizations]);
- const handleFilterChange = (newFilters: Record, skipDebounce: boolean = false) => {
- // Update filters state
+ const handleFilterChange = (newFilters: Record) => {
setFilters({
"Team ID": newFilters["Team ID"] || "",
"Organization ID": newFilters["Organization ID"] || "",
@@ -152,32 +74,14 @@ export function useFilterLogic({
"Sort By": newFilters["Sort By"] || "created_at",
"Sort Order": newFilters["Sort Order"] || "desc",
});
-
- // Only trigger debouncedSearch if skipDebounce is false
- // This allows sorting to be handled by the parent component's useKeys hook
- if (!skipDebounce) {
- // Fetch keys based on new filters
- const updatedFilters = {
- ...filters,
- ...newFilters,
- };
- debouncedSearch(updatedFilters);
- }
};
const handleFilterReset = () => {
- // Reset filters state
- setFilters(defaultFilters);
- setFilteredTotalCount(null);
-
- // Reset selections
- debouncedSearch(defaultFilters);
+ setFilters(DEFAULT_FILTERS);
};
return {
filters,
- filteredKeys,
- filteredTotalCount,
allTeams,
allOrganizations,
handleFilterChange,