From f6eea31739dc46a4c40e4cd6ab23336f4b53b448 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 15:21:38 -0800 Subject: [PATCH 1/3] [Fix] UI - Logs: Fix table not updating with custom time range and pagination issues Fix two bugs in the logs table with backend filters (e.g., Key Alias): 1. Bug 1 - Table doesn't update with custom time range: When Key Alias filter was active and user selected a custom time range, the main query would refetch (network request visible) but backendFilteredLogs would stay stale because the performSearch effect only watched [sortBy, sortOrder, currentPage]. Added startTime, endTime, isCustomDate to the effect deps. 2. Bug 2 - Pagination shows wrong results: fetchKeyHashForAlias incorrectly had currentPage (log page) in its deps, causing it to search the wrong page of the key list and trigger unnecessary effect re-runs. Removed currentPage from deps and always pass page 1 for key alias lookup. Also added debouncedSearch.cancel() in the effect to prevent race conditions when pagination happens within 300ms of filter application. Added tests verifying that time range changes trigger refetch when backend filters are active. Co-Authored-By: Claude Haiku 4.5 --- .../src/components/view_logs/index.tsx | 4 +- .../view_logs/log_filter_logic.test.tsx | 57 +++++++++++++++++++ .../components/view_logs/log_filter_logic.tsx | 7 ++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index a14a263a3f..e632b8da33 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -259,7 +259,7 @@ export default function SpendLogsTable({ if (!accessToken) return; try { - const response = await keyListCall(accessToken, null, null, keyAlias, null, null, currentPage, pageSize); + const response = await keyListCall(accessToken, null, null, keyAlias, null, null, 1, pageSize); const selectedKey = response.keys.find((key: any) => key.key_alias === keyAlias); @@ -270,7 +270,7 @@ export default function SpendLogsTable({ console.error("Error fetching key hash for alias:", error); } }, - [accessToken, currentPage, pageSize], + [accessToken, pageSize], ); const handleFilterReset = useCallback(() => { diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx index da4822d018..0b9cd59b9a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx @@ -570,6 +570,63 @@ describe("useLogFilterLogic", () => { ); }); + it("should refetch when startTime changes and backend filters are active", async () => { + vi.mocked(uiSpendLogsCall).mockResolvedValue( + createPaginatedResponse([createLogEntry()]), + ); + const logs = createPaginatedResponse([createLogEntry()]); + const { result, rerender } = renderHook( + (props: { startTime?: string }) => + useLogFilterLogic({ ...defaultProps, logs, ...props }), + { wrapper, initialProps: { startTime: "2025-01-01T00:00:00Z" } }, + ); + + act(() => { + result.current.handleFilterChange({ "Key Alias": "alias-1" }); + }); + + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalledTimes(1), { + timeout: 500, + }); + + rerender({ startTime: "2025-01-02T00:00:00Z" }); + + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalledTimes(2), { + timeout: 500, + }); + expect(uiSpendLogsCall).toHaveBeenLastCalledWith( + expect.objectContaining({ + start_date: "2025-01-02 00:00:00", + }), + ); + }); + + it("should refetch when isCustomDate changes and backend filters are active", async () => { + vi.mocked(uiSpendLogsCall).mockResolvedValue( + createPaginatedResponse([createLogEntry()]), + ); + const logs = createPaginatedResponse([createLogEntry()]); + const { result, rerender } = renderHook( + (props: { isCustomDate?: boolean }) => + useLogFilterLogic({ ...defaultProps, logs, ...props }), + { wrapper, initialProps: { isCustomDate: false } }, + ); + + act(() => { + result.current.handleFilterChange({ "Key Alias": "alias-1" }); + }); + + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalledTimes(1), { + timeout: 500, + }); + + rerender({ isCustomDate: true }); + + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalledTimes(2), { + timeout: 500, + }); + }); + it("should not call setCurrentPage when handleFilterChange receives identical filters", async () => { const setCurrentPage = vi.fn(); const logs = createPaginatedResponse([createLogEntry()]); diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index d9323b03af..58e86cb005 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -158,12 +158,15 @@ export function useLogFilterLogic({ [filters], ); - // Refetch when sort or page changes (backend filters use their own fetch, not the main query) + // Refetch when sort, page, or time range changes (backend filters use their own fetch, not the main query) useEffect(() => { if (hasBackendFilters && accessToken) { + // Cancel any pending debounced search to prevent it from overwriting this page's results + debouncedSearch.cancel(); performSearch(filters, currentPage); } - }, [sortBy, sortOrder, currentPage]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortBy, sortOrder, currentPage, startTime, endTime, isCustomDate]); // Compute client-side filtered logs directly from incoming logs and filters const clientDerivedFilteredLogs: PaginatedResponse = useMemo(() => { From 251526f52a60527a1d52c88bc453cb3b7641476a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 15:34:48 -0800 Subject: [PATCH 2/3] Fix double request and page reset issues in Logs table filters - Remove fetchKeyHashForAlias: Key Alias filtering is handled server-side by performSearch via key_alias; translating the alias to api_key hash caused a duplicate main-query request alongside performSearch's request. The effect now sets selectedKeyHash = filters["Key Hash"] || "" directly. - Add setCurrentPage(1) to quick select time range handler so the page resets to 1 when the user picks a preset time window (was keeping the previous page number, e.g. page=4, in the API request). - Add comments explaining the intentionally omitted react-hooks/exhaustive-deps in the performSearch effect per Greptile review feedback. Co-Authored-By: Claude Haiku 4.5 --- .../src/components/view_logs/index.tsx | 35 ++++--------------- .../components/view_logs/log_filter_logic.tsx | 7 +++- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index e632b8da33..83e889d2d6 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -254,25 +254,6 @@ export default function SpendLogsTable({ currentPage, }); - const fetchKeyHashForAlias = useCallback( - async (keyAlias: string) => { - if (!accessToken) return; - - try { - const response = await keyListCall(accessToken, null, null, keyAlias, null, null, 1, pageSize); - - const selectedKey = response.keys.find((key: any) => key.key_alias === keyAlias); - - if (selectedKey) { - setSelectedKeyHash(selectedKey.token); - } - } catch (error) { - console.error("Error fetching key hash for alias:", error); - } - }, - [accessToken, pageSize], - ); - const handleFilterReset = useCallback(() => { handleFilterResetFromHook(); // Reset custom time range to default (last 24 hours) @@ -283,7 +264,7 @@ export default function SpendLogsTable({ setCurrentPage(1); }, [handleFilterResetFromHook]); - // Add this effect to update selected filters when filter changes + // Sync filter state into the individual selectedX state variables used by the main query useEffect(() => { if (!accessToken) return; @@ -296,14 +277,11 @@ export default function SpendLogsTable({ setSelectedModelId(filters["Model"] || ""); setSelectedEndUser(filters["End User"] || ""); - if (filters["Key Hash"]) { - setSelectedKeyHash(filters["Key Hash"]); - } else if (filters["Key Alias"]) { - fetchKeyHashForAlias(filters["Key Alias"]); - } else { - setSelectedKeyHash(""); - } - }, [filters, accessToken, fetchKeyHashForAlias]); + // Key Alias filtering is handled server-side by performSearch via the key_alias param. + // We intentionally do not translate the alias to a hash here to avoid firing a + // redundant main-query request (api_key=hash) alongside performSearch's key_alias request. + setSelectedKeyHash(filters["Key Hash"] || ""); + }, [filters, accessToken]); if (!accessToken || !token || !userRole || !userID) { return null; @@ -592,6 +570,7 @@ export default function SpendLogsTable({ className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${displayLabel === option.label ? "bg-blue-50 text-blue-600" : "" }`} onClick={() => { + setCurrentPage(1); setEndTime(moment().format("YYYY-MM-DDTHH:mm")); setStartTime( moment() diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 58e86cb005..0701d38af4 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -165,7 +165,12 @@ export function useLogFilterLogic({ debouncedSearch.cancel(); performSearch(filters, currentPage); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // Intentionally omitted from deps: + // - `filters` / `debouncedSearch` / `performSearch`: filter changes are handled by + // handleFilterChange → debouncedSearch; adding them here would double-fetch on filter apply. + // - `hasBackendFilters` / `accessToken`: stable across sort/page/time changes; including them + // would cause spurious re-runs when the filter state first becomes active. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortBy, sortOrder, currentPage, startTime, endTime, isCustomDate]); // Compute client-side filtered logs directly from incoming logs and filters From f5caa34ebe7a99c59bd8805cc565fd39b7a2747c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 16:03:48 -0800 Subject: [PATCH 3/3] [Fix] UI - Logs: disable main query while backend filters are active When backend filters (Key Alias, Key Hash, etc.) were active, the main logs query still refetched whenever startTime/endTime/sort/page changed, firing a redundant unfiltered server request whose result was discarded. Expose hasBackendFilters from useLogFilterLogic and use it to gate the main query's enabled condition. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/view_logs/index.tsx | 16 ++++++++++++++-- .../components/view_logs/log_filter_logic.tsx | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 83e889d2d6..0cdca512e9 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -91,6 +91,11 @@ export default function SpendLogsTable({ const [sortBy, setSortBy] = useState("startTime"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + // Tracks whether any filter that uses performSearch (backend) is active. + // Used to disable the main query so it doesn't fire redundant unfiltered requests + // when time range / sort / page changes while a backend filter is in effect. + const [isMainQueryEnabled, setIsMainQueryEnabled] = useState(true); + const queryClient = useQueryClient(); const [isLiveTail, setIsLiveTail] = useState(() => { @@ -212,7 +217,7 @@ export default function SpendLogsTable({ return response; }, - enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", + enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs" && isMainQueryEnabled, refetchInterval: isLiveTail && currentPage === 1 ? 15000 : false, placeholderData: keepPreviousData, refetchIntervalInBackground: true, @@ -235,6 +240,7 @@ export default function SpendLogsTable({ const { filters, filteredLogs, + hasBackendFilters, allTeams: hookAllTeams, allKeyAliases, handleFilterChange, @@ -264,6 +270,12 @@ export default function SpendLogsTable({ setCurrentPage(1); }, [handleFilterResetFromHook]); + // Disable the main query whenever backend filters are active so it doesn't fire + // redundant unfiltered requests when time range / sort / page changes. + useEffect(() => { + setIsMainQueryEnabled(!hasBackendFilters); + }, [hasBackendFilters]); + // Sync filter state into the individual selectedX state variables used by the main query useEffect(() => { if (!accessToken) return; @@ -673,7 +685,7 @@ export default function SpendLogsTable({ - {isLiveTail && currentPage === 1 && ( + {isLiveTail && currentPage === 1 && isMainQueryEnabled && (
Auto-refreshing every 15 seconds diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 0701d38af4..097519d2f3 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -309,6 +309,7 @@ export function useLogFilterLogic({ return { filters, filteredLogs, + hasBackendFilters, allKeyAliases, allTeams, handleFilterChange,