Fix log table bugs (after filtering logic was added) (#10712)

* fix status filter

* fix refresh button

* minor

* fix refresh and auto refresh
This commit is contained in:
tanjiro
2025-05-10 15:46:05 +09:00
committed by GitHub
parent a7550c0b3d
commit 3fafe37eb9
3 changed files with 114 additions and 137 deletions
@@ -1727,11 +1727,6 @@ async def ui_view_spend_logs( # noqa: PLR0915
# Calculate skip value for pagination
skip = (page - 1) * page_size
# Get total count of records
total_records = await prisma_client.db.litellm_spendlogs.count(
where=where_conditions,
)
# Get paginated data
data = await prisma_client.db.litellm_spendlogs.find_many(
where=where_conditions,
@@ -1742,6 +1737,24 @@ async def ui_view_spend_logs( # noqa: PLR0915
take=page_size,
)
if status_filter:
status_lower = status_filter.strip().lower()
def status_filter_fn(row):
metadata = getattr(row, "metadata", {}) or {}
status_val = metadata.get("status") if isinstance(metadata, dict) else None
if status_lower == "failure":
return status_val == "failure"
elif status_lower == "success":
return status_val != "failure"
return True
data = list(filter(status_filter_fn, data))
# Get total count of records
total_records = await prisma_client.db.litellm_spendlogs.count(
where=where_conditions,
)
# Calculate total pages
total_pages = (total_records + page_size - 1) // page_size
@@ -88,6 +88,7 @@ export default function SpendLogsTable({
pageSize,
userID,
userRole,
autoRefreshInterval: 5000,
});
const sessionLogs = useQuery<{data: LogEntry[]}>({
@@ -2,9 +2,9 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { LogEntry } from "./columns";
import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall, modelAvailableCall } from "../networking";
import { KeyResponse } from "../key_team_helpers/key_list";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient, QueryKey } from "@tanstack/react-query";
import { Setter } from "@/types";
import { debounce } from "lodash";
// import { debounce } from "lodash"; // No longer needed
import { defaultPageSize } from "../constants";
import moment from "moment";
import { fetchAllKeyAliases, fetchAllTeams } from "../key_team_helpers/filter_helpers"; // Import fetchAllTeams
@@ -31,6 +31,16 @@ export interface PaginationState {
pageSize: number;
}
// Define the expected response structure from uiSpendLogsCall for type safety
interface SpendLogsResponse {
data: LogEntry[];
page: number;
total_pages: number;
total: number;
page_size: number;
}
export function useLogFilterLogic({
accessToken,
startTime, // Receive from SpendLogsTable
@@ -39,7 +49,8 @@ export function useLogFilterLogic({
initialPage = 1,
initialFilters = {},
userID,
userRole
userRole,
autoRefreshInterval // Added for auto-refresh
}: {
accessToken: string | null;
startTime: string;
@@ -49,6 +60,7 @@ export function useLogFilterLogic({
initialFilters?: Partial<LogFilterState>;
userID: string | null;
userRole: string | null;
autoRefreshInterval?: number; // Added for auto-refresh
}) {
const defaultFilters: LogFilterState = {
'Team ID': '',
@@ -67,38 +79,22 @@ export function useLogFilterLogic({
const [currentPage, setCurrentPage] = useState<number>(initialPage);
const queryClient = useQueryClient();
// States for manually managed data, loading, and error
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [paginationDetails, setPaginationDetails] = useState<PaginationState>({
currentPage: initialPage,
totalPages: 0,
totalCount: 0,
pageSize
});
const [isLoadingLogs, setIsLoadingLogs] = useState<boolean>(false);
const [logsError, setLogsError] = useState<Error | null>(null);
const lastSearchTimestamp = useRef(0);
const queryAllKeysQuery = useQuery({
queryKey: ['allKeysForLogFilters', accessToken], // Ensure unique queryKey
const queryAllKeysQuery = useQuery<string[], Error>({
queryKey: ['allKeysForLogFilters', accessToken],
queryFn: async () => {
if (!accessToken) throw new Error('Access token required');
return await fetchAllKeyAliases(accessToken);
},
enabled: !!accessToken
});
const allKeyAliases = queryAllKeysQuery.data || []
const allKeyAliases = queryAllKeysQuery.data || [];
// 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,
});
@@ -107,8 +103,7 @@ export function useLogFilterLogic({
queryKey: ["allUsersForLogFilters", accessToken],
queryFn: async () => {
if (!accessToken) return [];
// Assuming userListCall fetches all users. Adapt if pagination or specific params are needed.
const response = await userListCall(accessToken, null, 1, 1000); // Fetch a large number for dropdown
const response = await userListCall(accessToken, null, 1, 100);
return response.users || [];
},
enabled: !!accessToken,
@@ -118,113 +113,67 @@ export function useLogFilterLogic({
queryKey: ['allModels', accessToken, userID, userRole],
queryFn: async () => {
if (!accessToken || !userID || !userRole) return [];
const response = await modelAvailableCall(
accessToken,
userID,
userRole,
false, // return_wildcard_routes
null // teamID
false,
null
);
return response.data.map((model: { id: string }) => model.id);
},
enabled: !!accessToken && !!userID && !!userRole,
});
// Debounced API call
const debouncedSearch = useCallback(
debounce(async (currentFilters: LogFilterState, pageToFetch: number) => {
if (!accessToken) {
setLogEntries([]);
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
setIsLoadingLogs(false);
setLogsError(null);
return;
}
const logsQueryKey: QueryKey = ['spendLogs', accessToken, filters, currentPage, startTime, endTime, pageSize, userID, userRole];
const currentTimestamp = Date.now();
lastSearchTimestamp.current = currentTimestamp;
setIsLoadingLogs(true);
setLogsError(null);
const logsQueryFn = async (): Promise<SpendLogsResponse> => {
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
const formattedEndTime = moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss");
try {
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
const formattedEndTime = moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss");
const apiKeyParam = filters['Key Hash'] || undefined;
const teamIdParam = filters['Team ID'] || undefined;
const requestIdParam = filters['Request ID'] || undefined;
const userIdParamFilter = filters['User'] || undefined;
const statusParam = filters['Status'] || undefined;
const modelParam = filters['Model'] || undefined;
const apiKeyParam = currentFilters['Key Hash'] || undefined;
const teamIdParam = currentFilters['Team ID'] || undefined;
const requestIdParam = currentFilters['Request ID'] || undefined;
const userIdParam = currentFilters['User'] || undefined;
const statusParam = currentFilters['Status'] || undefined;
const modelParam = currentFilters['Model'] || undefined;
const response = await uiSpendLogsCall(
accessToken,
apiKeyParam,
teamIdParam,
requestIdParam,
formattedStartTime,
formattedEndTime,
pageToFetch,
pageSize,
userIdParam,
statusParam,
modelParam,
);
if (currentTimestamp === lastSearchTimestamp.current) {
if (response && response.data) {
setLogEntries(response.data);
setPaginationDetails({
currentPage: response.page,
totalPages: response.total_pages,
totalCount: response.total,
pageSize: response.page_size || pageSize,
});
} else {
setLogEntries([]);
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
}
}
} catch (error: any) {
if (currentTimestamp === lastSearchTimestamp.current) {
console.error("Error fetching logs:", error);
setLogsError(error);
setLogEntries([]);
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
}
} finally {
if (currentTimestamp === lastSearchTimestamp.current) {
setIsLoadingLogs(false);
}
}
}, 300),
[accessToken, startTime, endTime, pageSize, setLogEntries, setPaginationDetails, setIsLoadingLogs, setLogsError]
);
// useEffect to trigger debouncedSearch when dependencies change
useEffect(() => {
if (accessToken) {
debouncedSearch(filters, currentPage);
} else {
// Clear data if accessToken is lost or not present
setLogEntries([]);
setPaginationDetails(prev => ({ ...prev, currentPage, totalPages: 0, totalCount: 0 }));
setIsLoadingLogs(false);
setLogsError(null);
if (!accessToken) {
// This case should ideally be prevented by the `enabled` option
// Returning a structure that matches SpendLogsResponse to satisfy Promise type
return { data: [], page: 1, total_pages: 0, total: 0, page_size: pageSize };
}
// Cleanup function for debounce
return () => {
debouncedSearch.cancel();
};
}, [accessToken, filters, currentPage, startTime, endTime, pageSize, debouncedSearch]);
const response = await uiSpendLogsCall(
accessToken,
apiKeyParam,
teamIdParam,
requestIdParam,
formattedStartTime,
formattedEndTime,
currentPage,
pageSize,
userIdParamFilter,
statusParam,
modelParam,
);
// Assuming uiSpendLogsCall returns a type compatible with SpendLogsResponse
return response as SpendLogsResponse;
};
const logsQuery = useQuery<SpendLogsResponse, Error, SpendLogsResponse, QueryKey>({
queryKey: logsQueryKey,
queryFn: logsQueryFn,
enabled: !!accessToken && !!userID && !!userRole,
refetchInterval: autoRefreshInterval && autoRefreshInterval > 0 ? autoRefreshInterval : undefined,
refetchIntervalInBackground: true,
placeholderData: (previousData) => previousData,
notifyOnChangeProps: ['data', 'error'],
});
const handleFilterChange = (newFilters: Partial<LogFilterState>) => {
setFilters(prev => {
const updatedFilters = { ...prev, ...newFilters }; // Simpler update
// Ensure all keys in LogFilterState are present, defaulting to '' if not in newFilters
const updatedFilters = { ...prev, ...newFilters };
for (const key of Object.keys(defaultFilters) as Array<keyof LogFilterState>) {
if (!(key in updatedFilters)) {
updatedFilters[key] = defaultFilters[key];
@@ -232,44 +181,58 @@ export function useLogFilterLogic({
}
return updatedFilters as LogFilterState;
});
setCurrentPage(1); // Reset to first page when filters change
// debouncedSearch will be called by the useEffect hook
setCurrentPage(1);
};
const handleFilterReset = () => {
setFilters(defaultFilters);
setCurrentPage(1);
// debouncedSearch will be called by the useEffect hook
};
const pagination: PaginationState = useMemo(() => {
const data: SpendLogsResponse | undefined = logsQuery.data;
return {
currentPage: data?.page ?? currentPage,
totalPages: data?.total_pages ?? 0,
totalCount: data?.total ?? 0,
pageSize: data?.page_size ?? pageSize,
};
}, [logsQuery.data, currentPage, pageSize]);
const pagination: PaginationState = {
currentPage: paginationDetails.currentPage,
totalPages: paginationDetails.totalPages,
totalCount: paginationDetails.totalCount,
pageSize: paginationDetails.pageSize,
};
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
const handleRefresh = () => {
// Reset to first page
setCurrentPage(1);
// The useEffect in useLogFilterLogic will automatically trigger a refetch
// when currentPage changes
setIsManualRefreshing(true); // <-- ADD THIS LINE
// First invalidate all the filter queries
queryClient.invalidateQueries({ queryKey: ['allKeysForLogFilters'] });
queryClient.invalidateQueries({ queryKey: ['allTeamsForLogFilters'] });
queryClient.invalidateQueries({ queryKey: ['allUsersForLogFilters'] });
queryClient.invalidateQueries({ queryKey: ['allModels'] });
// Force an immediate refetch of the logs query
queryClient.refetchQueries({
queryKey: logsQueryKey,
exact: true,
type: 'active'
}).then(() => {
setIsManualRefreshing(false); // Reset loading state when refresh completes
});
};
return {
filters,
filteredLogs: logEntries,
filteredLogs: logsQuery.data?.data || [],
allKeyAliases,
allTeams: allTeams || [],
allUsers: allUsers || [],
allOrganizations: [], // Placeholder for now, can be fetched if needed
allOrganizations: [],
allModels: allModels || [],
handleFilterChange,
handleFilterReset,
isLoading: isLoadingLogs,
isLoading: logsQuery.isFetching || isManualRefreshing,
pagination,
setCurrentPage,
error: logsError,
setCurrentPage,
error: logsQuery.error,
handleRefresh
};
}