diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.test.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.test.tsx index 23ae3ab83df..1117d59e669 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.test.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.test.tsx @@ -47,8 +47,8 @@ function renderFilters(filters: Record = {}) { return { set }; } -function StatefulFilters() { - const [filters, setFilters] = useState>({}); +function StatefulFilters({ initialFilters = {} }: { initialFilters?: Record }) { + const [filters, setFilters] = useState>(initialFilters); return ( filters[id]} @@ -213,6 +213,22 @@ describe("RequestLogsFilters", () => { expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, "noisy"); }); + it("keeps a restored is not operator when clearing the alias to choose another", async () => { + vi.mocked(useInfiniteKeyAliases).mockReturnValue({ + ...emptyInfiniteQuery, + data: { pages: [{ aliases: ["noisy", "quiet"] }], pageParams: [1] }, + } as unknown as ReturnType); + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByRole("button", { name: "Clear", hidden: true })); + + expect(screen.getByRole("combobox", { name: "Key Alias operator" })).toHaveTextContent("is not"); + await chooseSelectOption(user, screen.getByPlaceholderText("Search a key alias"), "quiet"); + expect(screen.getByRole("combobox", { name: "Key Alias operator" })).toHaveTextContent("is not"); + expect(screen.getByPlaceholderText("Search a key alias")).toHaveValue("quiet"); + }); + it("does not leak the team scope into the Model lookup", async () => { renderFilters({ [LOG_FILTER_IDS.TEAM_ID]: "team-42" }); diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx index 5822a8d0b5f..9b970b6277b 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx @@ -108,7 +108,7 @@ function KeyAliasFilterField({ teamId: string; }) { const [search, setSearch] = useState(""); - const [operator, setOperator] = useState("is"); + const [operator, setOperator] = useState(excludeValue !== "" ? "is not" : "is"); let activeOperator: KeyAliasOperator = operator; if (excludeValue !== "") { activeOperator = "is not"; diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx index 6c97bf91b26..cf5c780f943 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx @@ -566,6 +566,21 @@ describe("RequestLogsPanel", () => { await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBeUndefined()); }); + it.each([" noisy ", " "])( + "preserves the exact excluded alias %j when reapplying bookmarked filters", + async (alias) => { + const user = userEvent.setup(); + renderPanel(new URLSearchParams({ exclude_key_alias: alias }).toString()); + + await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBe(alias)); + await user.click(screen.getByTestId("datatable-filters-trigger")); + await user.click(screen.getByTestId("filter-drawer-apply")); + + await waitFor(() => expect(urlParams().get("exclude_key_alias")).toBe(alias)); + expect(lastCall()?.params?.exclude_key_alias).toBe(alias); + }, + ); + it("treats conflicting Key Alias URL parameters as one mutually exclusive filter", async () => { renderPanel("key_alias=quiet&exclude_key_alias=noisy"); diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx index 70988223027..7f79b6f601f 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx @@ -18,6 +18,7 @@ import type { LogEntry } from "../types"; import { DEFAULT_LOGS_SORTING, formatLogsWindow, + getExactFilterValue, getFilterValue, getLogsWindowEndBound, LOG_FILTER_IDS, @@ -224,7 +225,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, (updaterOrValue) => { const next = typeof updaterOrValue === "function" ? updaterOrValue(columnFiltersRef.current) : updaterOrValue; const nextKeyAlias = getFilterValue(next, LOG_FILTER_IDS.KEY_ALIAS); - const nextExcludeKeyAlias = getFilterValue(next, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS); + const nextExcludeKeyAlias = getExactFilterValue(next, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS); const normalizedNext = next.filter( (filter) => filter.id !== LOG_FILTER_IDS.KEY_ALIAS && filter.id !== LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, ); diff --git a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.test.tsx b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.test.tsx index b79a8b2904e..6fe9c9db193 100644 --- a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.test.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.test.tsx @@ -92,7 +92,9 @@ describe("useLogFilterLogic", () => { { id: LOG_FILTER_IDS.MODEL_ID, value: "model-uuid-1", param: "model_id" }, { id: LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL, value: "gpt-4o", param: "model" }, { id: LOG_FILTER_IDS.KEY_ALIAS, value: "alias-1", param: "key_alias" }, - { id: "exclude_key_alias", value: "alias-1", param: "exclude_key_alias" }, + { id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: "alias-1", param: "exclude_key_alias" }, + { id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: " alias-1 ", param: "exclude_key_alias" }, + { id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: " ", param: "exclude_key_alias" }, { id: LOG_FILTER_IDS.ERROR_CODE, value: "429", param: "error_code" }, { id: LOG_FILTER_IDS.ERROR_MESSAGE, value: "rate limited", param: "error_message" }, { id: LOG_FILTER_IDS.USER_ID, value: "user-9", param: "user_id" }, @@ -111,6 +113,7 @@ describe("useLogFilterLogic", () => { columnFilters: [ { id: LOG_FILTER_IDS.TEAM_ID, value: " " }, { id: LOG_FILTER_IDS.KEY_HASH, value: "" }, + { id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: "" }, ], }); @@ -118,6 +121,7 @@ describe("useLogFilterLogic", () => { const params = lastCallParams()?.params; expect(params?.team_id).toBeUndefined(); expect(params?.api_key).toBeUndefined(); + expect(params?.exclude_key_alias).toBeUndefined(); expect(params?.error_code).toBeUndefined(); }); }); diff --git a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts index 19016d461a4..fa2d9c432c2 100644 --- a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts +++ b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts @@ -98,10 +98,13 @@ export const DEFAULT_LOGS_SORTING: SortingState = [{ id: "startTime", desc: true const isSortField = (id: string): id is LogsSortField => Object.hasOwn(LOGS_SORT_FIELD_MAP, id); -export const getFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => { +export const getExactFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => { const entry = columnFilters.find((filter) => filter.id === columnId); - if (typeof entry?.value !== "string") return undefined; - const trimmed = entry.value.trim(); + return typeof entry?.value === "string" && entry.value !== "" ? entry.value : undefined; +}; + +export const getFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => { + const trimmed = getExactFilterValue(columnFilters, columnId)?.trim(); return trimmed === "" ? undefined : trimmed; }; @@ -194,7 +197,7 @@ export function useLogFilterLogic({ model_id: getFilterValue(columnFilters, LOG_FILTER_IDS.MODEL_ID), model: getFilterValue(columnFilters, LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL), key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.KEY_ALIAS), - exclude_key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS), + exclude_key_alias: getExactFilterValue(columnFilters, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS), error_code: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_CODE), error_message: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_MESSAGE), sort_by: sortBy,