From f307ad90bfe8c438b15d99a0fef7cf4273d38e01 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Thu, 16 Apr 2026 09:22:37 -0700 Subject: [PATCH] Collapse dual-path filtering into single React Query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 10 filter keys now go through the useQuery — the imperative performSearch / debouncedSearch / backendFilteredLogs path is deleted. Filter values are debounced via useDebouncedValue(300ms) before hitting the query key so text inputs don't fire per-keystroke. Removed: performSearch, debouncedSearch, backendFilteredLogs, lastSearchTimestamp, hasBackendFilters, clientDerivedFilteredLogs, the sort/page/time refetch useEffect, and the filteredLogs chooser memo. --- .../components/view_logs/LogsTableToolbar.tsx | 4 +- .../src/components/view_logs/index.tsx | 2 - .../components/view_logs/log_filter_logic.tsx | 248 +++--------------- 3 files changed, 35 insertions(+), 219 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx index 1706bd0c23a..72133a2ff9b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx @@ -26,7 +26,6 @@ interface LogsTableToolbarProps { isButtonLoading: boolean; onRefetch: () => void; filteredLogs: PaginatedResponse; - hasBackendFilters: boolean; } export function LogsTableToolbar({ @@ -49,7 +48,6 @@ export function LogsTableToolbar({ isButtonLoading, onRefetch, filteredLogs, - hasBackendFilters, }: LogsTableToolbarProps) { const [quickSelectOpen, setQuickSelectOpen] = useState(false); const quickSelectRef = useRef(null); @@ -227,7 +225,7 @@ export function LogsTableToolbar({ - {isLiveTail && currentPage === 1 && !hasBackendFilters && ( + {isLiveTail && currentPage === 1 && (
Auto-refreshing every 15 seconds diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 00ecc729a89..1777827a154 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -108,7 +108,6 @@ export default function SpendLogsTable({ const { logsQuery, filteredLogs, - hasBackendFilters, allTeams, handleFilterChange, handleFilterReset: handleFilterResetFromHook, @@ -291,7 +290,6 @@ export default function SpendLogsTable({ isButtonLoading={isButtonLoading} onRefetch={() => logsQuery.refetch()} filteredLogs={filteredLogs} - hasBackendFilters={hasBackendFilters} /> (value: T, delayMs: number): T { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delayMs); + return () => clearTimeout(timer); + }, [value, delayMs]); + return debounced; +} + export const FILTER_KEYS = { TEAM_ID: "Team ID", KEY_HASH: "Key Hash", @@ -75,88 +83,10 @@ export function useLogFilterLogic({ sortOrder?: "asc" | "desc"; currentPage?: number; }) { - const [backendFilteredLogs, setBackendFilteredLogs] = useState(null); - const lastSearchTimestamp = useRef(0); - const performSearch = useCallback( - async (filters: LogFilterState, page = 1) => { - if (!accessToken) return; - - console.log("Filters being sent to API:", filters); - const currentTimestamp = Date.now(); - lastSearchTimestamp.current = currentTimestamp; - - const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss"); - const formattedEndTime = isCustomDate - ? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss") - : moment().utc().format("YYYY-MM-DD HH:mm:ss"); - - try { - const response = await uiSpendLogsCall({ - accessToken, - start_date: formattedStartTime, - end_date: formattedEndTime, - page, - page_size: pageSize, - params: { - api_key: filters[FILTER_KEYS.KEY_HASH] || undefined, - team_id: filters[FILTER_KEYS.TEAM_ID] || undefined, - request_id: filters[FILTER_KEYS.REQUEST_ID] || undefined, - user_id: filters[FILTER_KEYS.USER_ID] || undefined, - end_user: filters[FILTER_KEYS.END_USER] || undefined, - status_filter: filters[FILTER_KEYS.STATUS] || undefined, - model_id: filters[FILTER_KEYS.MODEL] || undefined, - key_alias: filters[FILTER_KEYS.KEY_ALIAS] || undefined, - error_code: filters[FILTER_KEYS.ERROR_CODE] || undefined, - error_message: filters[FILTER_KEYS.ERROR_MESSAGE] || undefined, - sort_by: sortBy, - sort_order: sortOrder, - }, - }); - - if (currentTimestamp === lastSearchTimestamp.current) { - setBackendFilteredLogs({ - ...response, - data: response.data ?? [], - }); - } - } catch (error) { - console.error("Error searching users:", error); - setBackendFilteredLogs({ - data: [], - total: 0, - page: 1, - page_size: pageSize, - total_pages: 0, - }); - } - }, - [accessToken, startTime, endTime, isCustomDate, pageSize, sortBy, sortOrder], - ); - - const debouncedSearch = useMemo( - () => debounce((filters: LogFilterState, page: number) => performSearch(filters, page), 300), - [performSearch], - ); - - useEffect(() => { - return () => debouncedSearch.cancel(); - }, [debouncedSearch]); - - // Determine when backend filters are active (server-side filtering) - const hasBackendFilters = useMemo( - () => - !!( - filters[FILTER_KEYS.KEY_ALIAS] || - filters[FILTER_KEYS.KEY_HASH] || - filters[FILTER_KEYS.REQUEST_ID] || - filters[FILTER_KEYS.USER_ID] || - filters[FILTER_KEYS.END_USER] || - filters[FILTER_KEYS.ERROR_CODE] || - filters[FILTER_KEYS.ERROR_MESSAGE] || - filters[FILTER_KEYS.MODEL] - ), - [filters], - ); + // Debounce filters so text inputs (Key Hash, Error Message) don't fire a + // request per keystroke. Dropdown selects get a 300ms delay too, which is + // imperceptible since the user just clicked an option. + const debouncedFilters = useDebouncedValue(filters, 300); const logsQuery = useQuery({ queryKey: [ @@ -166,11 +96,8 @@ export function useLogFilterLogic({ pageSize, startTime, endTime, - filters[FILTER_KEYS.TEAM_ID], - filters[FILTER_KEYS.KEY_HASH], + debouncedFilters, filterByCurrentUser ? userID : null, - filters[FILTER_KEYS.STATUS], - filters[FILTER_KEYS.MODEL], sortBy, sortOrder, ], @@ -197,12 +124,16 @@ export function useLogFilterLogic({ page: currentPage, page_size: pageSize, params: { - api_key: filters[FILTER_KEYS.KEY_HASH] || undefined, - team_id: filters[FILTER_KEYS.TEAM_ID] || undefined, - user_id: filterByCurrentUser ? userID ?? undefined : undefined, - end_user: filters[FILTER_KEYS.END_USER] || undefined, - status_filter: filters[FILTER_KEYS.STATUS] || undefined, - model_id: filters[FILTER_KEYS.MODEL] || undefined, + api_key: debouncedFilters[FILTER_KEYS.KEY_HASH] || undefined, + team_id: debouncedFilters[FILTER_KEYS.TEAM_ID] || undefined, + request_id: debouncedFilters[FILTER_KEYS.REQUEST_ID] || undefined, + user_id: debouncedFilters[FILTER_KEYS.USER_ID] || (filterByCurrentUser ? userID ?? undefined : undefined), + end_user: debouncedFilters[FILTER_KEYS.END_USER] || undefined, + status_filter: debouncedFilters[FILTER_KEYS.STATUS] || undefined, + model_id: debouncedFilters[FILTER_KEYS.MODEL] || undefined, + key_alias: debouncedFilters[FILTER_KEYS.KEY_ALIAS] || undefined, + error_code: debouncedFilters[FILTER_KEYS.ERROR_CODE] || undefined, + error_message: debouncedFilters[FILTER_KEYS.ERROR_MESSAGE] || undefined, sort_by: sortBy, sort_order: sortOrder, }, @@ -210,164 +141,53 @@ export function useLogFilterLogic({ return response; }, - enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs" && !hasBackendFilters, + enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", refetchInterval: isLiveTail && currentPage === 1 ? 15000 : false, placeholderData: keepPreviousData, refetchIntervalInBackground: true, }); - // 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); - } - // 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]); + const filteredLogs: PaginatedResponse = logsQuery.data ?? { + data: [], + total: 0, + page: 1, + page_size: pageSize, + total_pages: 0, + }; - // Compute client-side filtered logs directly from query data and filters - const spendLogsData = logsQuery.data; - const clientDerivedFilteredLogs: PaginatedResponse = useMemo(() => { - if (!spendLogsData) { - return { - data: [], - total: 0, - page: 1, - page_size: pageSize, - total_pages: 0, - }; - } - - // If backend filters are on, don't perform client-side filtering here - if (hasBackendFilters) { - return spendLogsData; - } - - let filteredData = [...spendLogsData.data]; - - if (filters[FILTER_KEYS.TEAM_ID]) { - filteredData = filteredData.filter((log) => log.team_id === filters[FILTER_KEYS.TEAM_ID]); - } - - if (filters[FILTER_KEYS.STATUS]) { - filteredData = filteredData.filter((log) => { - if (filters[FILTER_KEYS.STATUS] === "success") { - return !log.status || log.status === "success"; - } - return log.status === filters[FILTER_KEYS.STATUS]; - }); - } - - if (filters[FILTER_KEYS.MODEL]) { - filteredData = filteredData.filter((log) => log.model_id === filters[FILTER_KEYS.MODEL]); - } - - if (filters[FILTER_KEYS.KEY_HASH]) { - filteredData = filteredData.filter((log) => log.api_key === filters[FILTER_KEYS.KEY_HASH]); - } - - if (filters[FILTER_KEYS.END_USER]) { - filteredData = filteredData.filter((log) => log.end_user === filters[FILTER_KEYS.END_USER]); - } - - if (filters[FILTER_KEYS.ERROR_CODE]) { - filteredData = filteredData.filter((log) => { - const metadata = log.metadata || {}; - const errorInfo = metadata.error_information; - return errorInfo && errorInfo.error_code === filters[FILTER_KEYS.ERROR_CODE]; - }); - } - - return { - data: filteredData, - total: spendLogsData.total, - page: spendLogsData.page, - page_size: spendLogsData.page_size, - total_pages: spendLogsData.total_pages, - }; - }, [spendLogsData, filters, hasBackendFilters]); - - // Choose which filtered logs to expose: backend result when active, otherwise client-derived - const filteredLogs: PaginatedResponse = useMemo(() => { - if (hasBackendFilters) { - // When backend filters are active, only show backend results. - // If search hasn't completed yet (null), show empty state rather than - // falling back to unfiltered logs — that caused filtered views to - // display mismatched data when the filter matched zero rows. - if (backendFilteredLogs !== null) { - return backendFilteredLogs; - } - return { - data: [], - total: 0, - page: 1, - page_size: pageSize, - total_pages: 0, - }; - } - return clientDerivedFilteredLogs; - }, [hasBackendFilters, backendFilteredLogs, clientDerivedFilteredLogs]); - - // Fetch all teams and users for potential filter dropdowns (optional, can be adapted) const { data: allTeams } = useQuery({ queryKey: ["allTeamsForLogFilters", accessToken], queryFn: async () => { if (!accessToken) return []; - // Use fetchAllTeams helper function for consistency and abstraction - // Assuming fetchAllTeams returns Team[] directly const teamsData = await fetchAllTeams(accessToken); - return teamsData || []; // Ensure it returns an array + return teamsData || []; }, enabled: !!accessToken, }); - // Update filters state const handleFilterChange = (newFilters: Partial) => { setFilters((prev) => { const updatedFilters = { ...prev, ...newFilters }; - - // Ensure all keys in LogFilterState are present, defaulting to '' if not in newFilters for (const key of Object.keys(defaultFilters) as Array) { if (!(key in updatedFilters)) { updatedFilters[key] = defaultFilters[key]; } } - - // Only call debouncedSearch if filters have actually changed if (JSON.stringify(updatedFilters) !== JSON.stringify(prev)) { setCurrentPage(1); - setBackendFilteredLogs(null); - debouncedSearch(updatedFilters, 1); } - return updatedFilters as LogFilterState; }); }; const handleFilterReset = () => { - // Reset filters state setFilters(defaultFilters); - - // Clear backend filtered logs to ensure fresh render - setBackendFilteredLogs(null); - - // Cancel any in-flight debounced search - debouncedSearch.cancel(); - - // Reset to first page so the unfiltered view starts at page 1 setCurrentPage(1); }; return { logsQuery, filteredLogs, - hasBackendFilters, allTeams, handleFilterChange, handleFilterReset,