mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Collapse dual-path filtering into single React Query
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.
This commit is contained in:
parent
b5455d12c0
commit
f307ad90bf
3 changed files with 35 additions and 219 deletions
|
|
@ -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<HTMLDivElement>(null);
|
||||
|
|
@ -227,7 +225,7 @@ export function LogsTableToolbar({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLiveTail && currentPage === 1 && !hasBackendFilters && (
|
||||
{isLiveTail && currentPage === 1 && (
|
||||
<div className="mb-4 px-4 py-2 bg-green-50 border border-greem-200 rounded-md flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-green-700">Auto-refreshing every 15 seconds</span>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
<DataTable
|
||||
columns={createColumns({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
import moment from "moment";
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { uiSpendLogsCall } from "../networking";
|
||||
import { Team } from "../key_team_helpers/key_list";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { fetchAllTeams } from "../../components/key_team_helpers/filter_helpers";
|
||||
import { debounce } from "lodash";
|
||||
import { defaultPageSize } from "../constants";
|
||||
import { PaginatedResponse } from ".";
|
||||
import type { LogsSortField } from "./columns";
|
||||
|
||||
function useDebouncedValue<T>(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<PaginatedResponse | null>(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<PaginatedResponse>({
|
||||
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<Team[], Error>({
|
||||
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<LogFilterState>) => {
|
||||
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<keyof LogFilterState>) {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue