mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-06 04:23:40 +00:00
Merge pull request #21708 from BerriAI/litellm_logs_table_filter_bug
[Fix] UI - Logs: Fix table not updating and pagination issues
This commit is contained in:
@@ -91,6 +91,11 @@ export default function SpendLogsTable({
|
||||
const [sortBy, setSortBy] = useState<LogsSortField>("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<boolean>(() => {
|
||||
@@ -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,
|
||||
@@ -254,25 +260,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, currentPage, 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, currentPage, pageSize],
|
||||
);
|
||||
|
||||
const handleFilterReset = useCallback(() => {
|
||||
handleFilterResetFromHook();
|
||||
// Reset custom time range to default (last 24 hours)
|
||||
@@ -283,7 +270,13 @@ export default function SpendLogsTable({
|
||||
setCurrentPage(1);
|
||||
}, [handleFilterResetFromHook]);
|
||||
|
||||
// Add this effect to update selected filters when filter changes
|
||||
// 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;
|
||||
|
||||
@@ -296,14 +289,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 +582,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()
|
||||
@@ -694,7 +685,7 @@ export default function SpendLogsTable({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLiveTail && currentPage === 1 && (
|
||||
{isLiveTail && currentPage === 1 && isMainQueryEnabled && (
|
||||
<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>
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -158,12 +158,20 @@ 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]);
|
||||
// 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
|
||||
const clientDerivedFilteredLogs: PaginatedResponse = useMemo(() => {
|
||||
@@ -301,6 +309,7 @@ export function useLogFilterLogic({
|
||||
return {
|
||||
filters,
|
||||
filteredLogs,
|
||||
hasBackendFilters,
|
||||
allKeyAliases,
|
||||
allTeams,
|
||||
handleFilterChange,
|
||||
|
||||
Reference in New Issue
Block a user