From 609e1e97638698fe8a7948e4199443f30076ceda Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Mon, 1 Jun 2026 18:43:09 -0700 Subject: [PATCH] fix(ui): render caller-supplied filter options in caller order (LIT-3151) (#29462) FilterComponent iterated a hardcoded orderedFilters whitelist instead of the options prop, so any consumer whose filter names were not on that list rendered nothing. The Tool Policies page passes "Input Policy", "Output Policy", "Team Name" and "Key Name", none of which were whitelisted, so its Filters panel opened to an empty area. Drop the whitelist and render the options the caller passes, in the order they pass them, so each page owns its own filter set and ordering. The Logs page array is reordered to match its prior on-screen order; VirtualKeys and TeamVirtualKeys already matched the old whitelist order and are unaffected. --- .../src/components/molecules/filter.test.tsx | 81 ++++++++----------- .../src/components/molecules/filter.tsx | 20 +---- .../components/view_logs/filter_options.ts | 24 +++--- 3 files changed, 48 insertions(+), 77 deletions(-) diff --git a/ui/litellm-dashboard/src/components/molecules/filter.test.tsx b/ui/litellm-dashboard/src/components/molecules/filter.test.tsx index 1a90c4a069..3a15c5c84f 100644 --- a/ui/litellm-dashboard/src/components/molecules/filter.test.tsx +++ b/ui/litellm-dashboard/src/components/molecules/filter.test.tsx @@ -103,7 +103,7 @@ describe("FilterComponent", () => { }); }); - it("should render filters in correct order", async () => { + it("renders filters in the caller-supplied order", async () => { const user = userEvent.setup({ delay: null }); const options: FilterOption[] = [ { name: "model", label: "Model" }, @@ -113,11 +113,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -125,8 +121,7 @@ describe("FilterComponent", () => { await waitFor(() => { const labels = screen.getAllByText(/^(Team ID|Status|User ID|Model)$/); - expect(labels[0]).toHaveTextContent("Team ID"); - expect(labels[1]).toHaveTextContent("Status"); + expect(labels.map((l) => l.textContent)).toEqual(["Model", "Team ID", "Status", "User ID"]); }); }); @@ -218,11 +213,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -245,9 +236,7 @@ describe("FilterComponent", () => { it("should debounce search input for searchable filters", async () => { const user = userEvent.setup({ delay: null }); - const mockSearchFn = vi.fn().mockResolvedValue([ - { label: "Result", value: "result" }, - ]); + const mockSearchFn = vi.fn().mockResolvedValue([{ label: "Result", value: "result" }]); const options: FilterOption[] = [ { @@ -259,11 +248,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -311,11 +296,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -360,11 +341,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -393,9 +370,7 @@ describe("FilterComponent", () => { it("should load initial options when dropdown opens for searchable filter", async () => { const user = userEvent.setup({ delay: null }); - const mockSearchFn = vi.fn().mockResolvedValue([ - { label: "Initial Result", value: "initial" }, - ]); + const mockSearchFn = vi.fn().mockResolvedValue([{ label: "Initial Result", value: "initial" }]); const options: FilterOption[] = [ { @@ -407,11 +382,7 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); @@ -433,7 +404,7 @@ describe("FilterComponent", () => { }); }); - it("should not render filters that are not in orderedFilters list", async () => { + it("renders caller-supplied options that match no predefined filter name (LIT-3151)", async () => { const user = userEvent.setup({ delay: null }); const options: FilterOption[] = [ { @@ -443,18 +414,36 @@ describe("FilterComponent", () => { ]; renderWithProviders( - , + , ); const filterButton = screen.getByRole("button", { name: "Filters" }); await user.click(filterButton); await waitFor(() => { - expect(screen.queryByText("Unknown Filter")).not.toBeInTheDocument(); + expect(screen.getByText("Unknown Filter")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Enter Unknown Filter...")).toBeInTheDocument(); + }); + }); + + it("renders every Tool Policies filter when none match a predefined name (LIT-3151)", async () => { + const user = userEvent.setup({ delay: null }); + const options: FilterOption[] = [ + { name: "Input Policy", label: "Input Policy", options: [{ label: "Trusted", value: "trusted" }] }, + { name: "Output Policy", label: "Output Policy", options: [{ label: "Blocked", value: "blocked" }] }, + { name: "Team Name", label: "Team Name", options: [] }, + { name: "Key Name", label: "Key Name", options: [] }, + ]; + + renderWithProviders( + , + ); + + await user.click(screen.getByRole("button", { name: "Filters" })); + + await waitFor(() => { + const labels = screen.getAllByText(/^(Input Policy|Output Policy|Team Name|Key Name)$/); + expect(labels.map((l) => l.textContent)).toEqual(["Input Policy", "Output Policy", "Team Name", "Key Name"]); }); }); diff --git a/ui/litellm-dashboard/src/components/molecules/filter.tsx b/ui/litellm-dashboard/src/components/molecules/filter.tsx index 29a55c1f03..7086892c32 100644 --- a/ui/litellm-dashboard/src/components/molecules/filter.tsx +++ b/ui/litellm-dashboard/src/components/molecules/filter.tsx @@ -129,21 +129,6 @@ const FilterComponent: React.FC = ({ } }; - // Define the order of filters - const orderedFilters = [ - "Team ID", - "Status", - "Organization ID", - "Key Alias", - "User ID", - "End User", - "Error Code", - "Error Message", - "Key Hash", - "Model", - "Public model / search tool", - ]; - return (
@@ -159,10 +144,7 @@ const FilterComponent: React.FC = ({ {showFilters && (
- {orderedFilters.map((filterName) => { - const option = options.find((opt) => opt.label === filterName || opt.name === filterName); - if (!option) return null; - + {options.map((option) => { return (
diff --git a/ui/litellm-dashboard/src/components/view_logs/filter_options.ts b/ui/litellm-dashboard/src/components/view_logs/filter_options.ts index 59ac58b674..52632ea586 100644 --- a/ui/litellm-dashboard/src/components/view_logs/filter_options.ts +++ b/ui/litellm-dashboard/src/components/view_logs/filter_options.ts @@ -22,16 +22,6 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] { { label: "Failure", value: "failure" }, ], }, - { - name: "Model", - label: "Model", - customComponent: PaginatedModelSelect, - }, - { - name: FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL, - label: "Public model / search tool", - isSearchable: false, - }, { name: "Key Alias", label: "Key Alias", @@ -63,14 +53,24 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] { return filtered; }, }, + { + name: "Error Message", + label: "Error Message", + isSearchable: false, + }, { name: "Key Hash", label: "Key Hash", isSearchable: false, }, { - name: "Error Message", - label: "Error Message", + name: "Model", + label: "Model", + customComponent: PaginatedModelSelect, + }, + { + name: FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL, + label: "Public model / search tool", isSearchable: false, }, ];