From cb65102669742b43d3ce559ee19752abdc3990db Mon Sep 17 00:00:00 2001 From: Bytechoreographer Date: Wed, 6 May 2026 19:36:05 +0800 Subject: [PATCH] fix(ui): reset virtual keys sort on filter reset and wire key hash filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups on top of the filter-persistence fix: - Reset Filters now also resets the column sort. The sort state lives in the table's local `sorting`, separate from `filters` — clicking Reset cleared the chips but left the column header arrow and the useKeys sort_by/sort_order untouched. Wrap onResetFilters to setSorting and call onSortChange with the defaults (created_at desc). - Wire the "Key Hash" filter end-to-end. The chip was rendered but handleFilterChange dropped the value and useKeys never received it, so typing into Key Hash did nothing. Add it to FilterState/ DEFAULT_FILTERS/handleFilterChange and forward debouncedFilters ["Key Hash"] into useKeys. - Drop the now-dead "Sort By"/"Sort Order" entries from FilterState — the sort handler no longer writes them and Reset's old behavior of resetting them was a no-op. - Tests: forward Key Hash assertion in VirtualKeysTable.test.tsx, Key Hash handleFilterChange test in filter_logic.test.tsx, and update mock filter shapes accordingly. Co-Authored-By: Claude Opus 4 (1M context) --- .../VirtualKeysPage/VirtualKeysTable.test.tsx | 56 +++++++++++-------- .../VirtualKeysPage/VirtualKeysTable.tsx | 8 ++- .../key_team_helpers/filter_logic.test.tsx | 13 ++++- .../key_team_helpers/filter_logic.tsx | 9 +-- 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index 65de44bb309..5347f202757 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -203,8 +203,7 @@ beforeEach(() => { "User ID": "user-1", "User Email": "user@example.com", "User Role": "user", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -322,8 +321,7 @@ it("should show 'No keys found' message when filteredKeys is empty", () => { "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -370,8 +368,7 @@ it("should handle models with more than 3 entries to trigger expansion UI", () = "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -518,8 +515,7 @@ it("should display 'Default Proxy Admin' for user_id when value is 'default_user "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -568,8 +564,7 @@ it("should display 'Default Proxy Admin' for created_by when value is 'default_u "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -626,8 +621,7 @@ it("should display created_by_user email in 'Created By' column when available", "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -681,8 +675,7 @@ it("should display created_by_user alias over email when both available", async "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -731,8 +724,7 @@ it("should render table without crashing when models is null", async () => { "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -782,8 +774,7 @@ it("should render table without crashing when models is undefined", async () => "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -868,8 +859,7 @@ it("should display 'Unknown' for last_active when value is null", async () => { "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }, allTeams: [mockTeam], allOrganizations: [mockOrganization], @@ -916,7 +906,7 @@ describe("pagination display – total count and page count", () => { } as any); mockUseFilterLogic.mockReturnValue({ - filters: { "Team ID": "", "Organization ID": "", "Key Alias": "", "User ID": "", "Sort By": "created_at", "Sort Order": "desc" }, + filters: { "Team ID": "", "Organization ID": "", "Key Alias": "", "User ID": "", "Key Hash": "" }, allTeams: [mockTeam], allOrganizations: [mockOrganization], handleFilterChange: vi.fn(), @@ -947,7 +937,7 @@ describe("pagination display – total count and page count", () => { } as any); mockUseFilterLogic.mockReturnValue({ - filters: { "Team ID": "", "Organization ID": "", "Key Alias": "aaaaa", "User ID": "", "Sort By": "created_at", "Sort Order": "desc" }, + filters: { "Team ID": "", "Organization ID": "", "Key Alias": "aaaaa", "User ID": "", "Key Hash": "" }, allTeams: [mockTeam], allOrganizations: [mockOrganization], handleFilterChange: vi.fn(), @@ -964,7 +954,7 @@ describe("pagination display – total count and page count", () => { 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" }, + filters: { "Team ID": "", "Organization ID": "", "Key Alias": "aaaaa", "User ID": "", "Key Hash": "" }, allTeams: [mockTeam], allOrganizations: [mockOrganization], handleFilterChange: vi.fn(), @@ -983,6 +973,26 @@ describe("pagination display – total count and page count", () => { ); }); }); + + it("should forward the Key Hash filter into useKeys", async () => { + mockUseFilterLogic.mockReturnValue({ + filters: { "Team ID": "", "Organization ID": "", "Key Alias": "", "User ID": "", "Key Hash": "hash-xyz" }, + allTeams: [mockTeam], + allOrganizations: [mockOrganization], + handleFilterChange: vi.fn(), + handleFilterReset: vi.fn(), + }); + + renderWithProviders(); + + await waitFor(() => { + expect(mockUseKeys).toHaveBeenCalledWith( + expect.any(Number), + expect.any(Number), + expect.objectContaining({ keyHash: "hash-xyz" }), + ); + }); + }); }); describe("refetch button", () => { diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx index 2dfc6f45f29..059fc462866 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx @@ -105,6 +105,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo debouncedFilters["Organization ID"], debouncedFilters["Key Alias"], debouncedFilters["User ID"], + debouncedFilters["Key Hash"], ]); const { @@ -121,6 +122,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo organizationID: debouncedFilters["Organization ID"] || undefined, selectedKeyAlias: debouncedFilters["Key Alias"] || undefined, userID: debouncedFilters["User ID"] || undefined, + keyHash: debouncedFilters["Key Hash"] || undefined, }); const [expandedAccordions, setExpandedAccordions] = useState>({}); @@ -700,7 +702,11 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo options={filterOptions} onApplyFilters={handleFilterChange} initialValues={filters} - onResetFilters={handleFilterReset} + onResetFilters={() => { + handleFilterReset(); + setSorting([{ id: "created_at", desc: true }]); + onSortChange?.("created_at", "desc"); + }} /> 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 0158f159cba..1105052265c 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 @@ -17,8 +17,7 @@ const DEFAULT_FILTERS = { "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }; describe("useFilterLogic – filter state management", () => { @@ -42,6 +41,16 @@ describe("useFilterLogic – filter state management", () => { expect(result.current.filters["Key Alias"]).toBe("my-alias"); }); + it("records the Key Hash filter when handleFilterChange is called", () => { + const { result } = renderHook(() => useFilterLogic(defaultProps)); + + act(() => { + result.current.handleFilterChange({ "Key Hash": "abc123" }); + }); + + expect(result.current.filters["Key Hash"]).toBe("abc123"); + }); + it("preserves filter state across re-renders (regression: bug where results reset on remount)", () => { const { result, rerender } = renderHook(() => useFilterLogic(defaultProps)); 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 3f762d26b7c..52630bf708f 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 @@ -10,8 +10,7 @@ export interface FilterState { "Key Alias": string; [key: string]: string; "User ID": string; - "Sort By": string; - "Sort Order": string; + "Key Hash": string; } const DEFAULT_FILTERS: FilterState = { @@ -19,8 +18,7 @@ const DEFAULT_FILTERS: FilterState = { "Organization ID": "", "Key Alias": "", "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", + "Key Hash": "", }; export function useFilterLogic({ @@ -71,8 +69,7 @@ export function useFilterLogic({ "Organization ID": newFilters["Organization ID"] || "", "Key Alias": newFilters["Key Alias"] || "", "User ID": newFilters["User ID"] || "", - "Sort By": newFilters["Sort By"] || "created_at", - "Sort Order": newFilters["Sort Order"] || "desc", + "Key Hash": newFilters["Key Hash"] || "", }); };