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,