From f6eea31739dc46a4c40e4cd6ab23336f4b53b448 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 15:21:38 -0800 Subject: [PATCH] [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 a14a263a3fe..e632b8da339 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 da4822d0189..0b9cd59b9aa 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 d9323b03afb..58e86cb005d 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(() => {