diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 7d8eadcd03f..64fcfb08f95 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -1,28 +1,22 @@ import moment from "moment"; -import React, { useState, useRef, useEffect, useMemo } from "react"; -import { useQueryClient, useQuery } from "@tanstack/react-query"; -import { ColumnDef, Row } from "@tanstack/react-table"; +import { useQuery } from "@tanstack/react-query"; +import { useState, useRef, useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { uiSpendLogsCall, keyInfoV1Call, sessionSpendLogsCall } from "../networking"; import { DataTable } from "./table"; -import { LogEntry } from "./columns"; -import { getCountryFromIP } from "./ip_lookup"; -import { CountryCell } from "./country_cell"; -import { TimeCell } from "./time_cell"; +import { columns, LogEntry } from "./columns"; +import { Row } from "@tanstack/react-table"; import { prefetchLogDetails } from "./prefetch"; import { RequestResponsePanel } from "./columns"; import { ErrorViewer } from './ErrorViewer'; import { internalUserRoles } from "../../utils/roles"; import { ConfigInfoMessage } from './ConfigInfoMessage'; -import { Tooltip, Button as AntButton } from "antd"; -import { Button } from "@tremor/react"; +import { Tooltip } from "antd"; import { KeyResponse, Team } from "../key_team_helpers/key_list"; import KeyInfoView from "../key_info_view"; import { SessionView } from './SessionView'; import { VectorStoreViewer } from './VectorStoreViewer'; -import FilterComponent from "../common_components/filter"; -import { FilterOption } from "../common_components/filter"; -import { useLogFilterLogic } from "./log_filter_logic"; interface SpendLogsTableProps { accessToken: string | null; @@ -32,6 +26,14 @@ interface SpendLogsTableProps { allTeams: Team[]; } +interface PaginatedResponse { + data: LogEntry[]; + total: number; + page: number; + page_size: number; + total_pages: number; +} + interface PrefetchedLog { messages: any[]; response: any; @@ -45,7 +47,12 @@ export default function SpendLogsTable({ allTeams, }: SpendLogsTableProps) { const [searchTerm, setSearchTerm] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [showColumnDropdown, setShowColumnDropdown] = useState(false); + const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(50); + const dropdownRef = useRef(null); + const filtersRef = useRef(null); const quickSelectRef = useRef(null); // New state variables for Start and End Time @@ -59,54 +66,22 @@ export default function SpendLogsTable({ // Add these new state variables at the top with other useState declarations const [isCustomDate, setIsCustomDate] = useState(false); const [quickSelectOpen, setQuickSelectOpen] = useState(false); + const [tempTeamId, setTempTeamId] = useState(""); + const [tempKeyHash, setTempKeyHash] = useState(""); + const [selectedTeamId, setSelectedTeamId] = useState(""); + const [selectedKeyHash, setSelectedKeyHash] = useState(""); const [selectedKeyInfo, setSelectedKeyInfo] = useState(null); const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null); - + const [selectedFilter, setSelectedFilter] = useState("Team ID"); + const [filterByCurrentUser, setFilterByCurrentUser] = useState( + userRole && internalUserRoles.includes(userRole) + ); + const [expandedRequestId, setExpandedRequestId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); const queryClient = useQueryClient(); - const { - filters, - filteredLogs, - allTeams: hookAllTeams, - allOrganizations, - allUsers, - allKeyAliases, - handleFilterChange, - handleFilterReset, - isLoading, - pagination, - setCurrentPage, - } = useLogFilterLogic({ - accessToken, - startTime, - endTime, - pageSize, - }); - - const sessionLogs = useQuery<{data: LogEntry[]}>({ - queryKey: ["sessionLogs", selectedSessionId], - queryFn: async () => { - if (!accessToken || !selectedSessionId) return { data: [] }; - const response = await sessionSpendLogsCall(accessToken, selectedSessionId); - return { - data: response.data || response || [], - }; - }, - enabled: !!accessToken && !!selectedSessionId, - }); - - useEffect(() => { - if (filteredLogs && expandedRequestId) { - const stillExists = filteredLogs.some((log: LogEntry) => log.request_id === expandedRequestId); - if (!stillExists) { - setExpandedRequestId(null); - } - } - }, [filteredLogs, expandedRequestId]); - useEffect(() => { const fetchKeyInfo = async () => { if (selectedKeyIdInfoView && accessToken) { @@ -127,6 +102,18 @@ export default function SpendLogsTable({ // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setShowColumnDropdown(false); + } + if ( + filtersRef.current && + !filtersRef.current.contains(event.target as Node) + ) { + setShowFilters(false); + } if ( quickSelectRef.current && !quickSelectRef.current.contains(event.target as Node) @@ -140,39 +127,154 @@ export default function SpendLogsTable({ document.removeEventListener("mousedown", handleClickOutside); }, []); - if (!accessToken || !userID) { + + useEffect(() => { + if (userRole && internalUserRoles.includes(userRole)) { + setFilterByCurrentUser(true); + } + }, [userRole]); + + const logs = useQuery({ + queryKey: [ + "logs", + "table", + currentPage, + pageSize, + startTime, + endTime, + selectedTeamId, + selectedKeyHash, + filterByCurrentUser ? userID : null, + ], + queryFn: async () => { + if (!accessToken || !token || !userRole || !userID) { + console.log("Missing required auth parameters"); + return { + data: [], + total: 0, + page: 1, + page_size: pageSize, + total_pages: 0, + }; + } + + 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"); + + // Get base response from API + const response = await uiSpendLogsCall( + accessToken, + selectedKeyHash || undefined, + selectedTeamId || undefined, + undefined, + formattedStartTime, + formattedEndTime, + currentPage, + pageSize, + filterByCurrentUser ? userID : undefined + ); + + // Trigger prefetch for all logs + await prefetchLogDetails( + response.data, + formattedStartTime, + accessToken, + queryClient + ); + + // Update logs with prefetched data if available + response.data = response.data.map((log: LogEntry) => { + const prefetchedData = queryClient.getQueryData( + ["logDetails", log.request_id, formattedStartTime] + ); + + if (prefetchedData?.messages && prefetchedData?.response) { + log.messages = prefetchedData.messages; + log.response = prefetchedData.response; + return log; + } + return log; + }); + + return response; + }, + enabled: !!accessToken && !!token && !!userRole && !!userID, + refetchInterval: 5000, + refetchIntervalInBackground: true, + }); + + // Fetch logs for a session if selected + const sessionLogs = useQuery({ + queryKey: ["sessionLogs", selectedSessionId], + queryFn: async () => { + if (!accessToken || !selectedSessionId) return { data: [], total: 0, page: 1, page_size: 50, total_pages: 1 }; + const response = await sessionSpendLogsCall(accessToken, selectedSessionId); + // If the API returns an array, wrap it in the same shape as PaginatedResponse + return { + data: response.data || response || [], + total: (response.data || response || []).length, + page: 1, + page_size: 1000, + total_pages: 1, + }; + }, + enabled: !!accessToken && !!selectedSessionId, + }); + + // Add this effect to preserve expanded state when data refreshes + useEffect(() => { + if (logs.data?.data && expandedRequestId) { + // Check if the expanded request ID still exists in the new data + const stillExists = logs.data.data.some(log => log.request_id === expandedRequestId); + if (!stillExists) { + // If the request ID no longer exists in the data, clear the expanded state + setExpandedRequestId(null); + } + } + }, [logs.data?.data, expandedRequestId]); + + if (!accessToken || !token || !userRole || !userID) { console.log( - "Missing accessToken or userID", + "got None values for one of accessToken, token, userRole, userID", ); return null; } - const processedLogs = filteredLogs.filter((log: LogEntry) => { - const matchesSearch = - !searchTerm || - log.request_id.includes(searchTerm) || - log.model.includes(searchTerm) || - (log.user && log.user.includes(searchTerm)); - return matchesSearch; - }).map((log: LogEntry) => ({ - ...log, - onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), - onSessionClick: (sessionId: string) => { - if (sessionId) setSelectedSessionId(sessionId); - }, - })); + const filteredData = + logs.data?.data?.filter((log) => { + const matchesSearch = + !searchTerm || + log.request_id.includes(searchTerm) || + log.model.includes(searchTerm) || + (log.user && log.user.includes(searchTerm)); + + // No need for additional filtering since we're now handling this in the API call + return matchesSearch; + + }).map(log => ({ + ...log, + onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), + onSessionClick: (sessionId: string) => { + if (sessionId) setSelectedSessionId(sessionId); + }, + })) || []; + // For session logs, add onKeyHashClick/onSessionClick as well const sessionData = - sessionLogs.data?.data?.map((log: LogEntry) => ({ + sessionLogs.data?.data?.map(log => ({ ...log, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), onSessionClick: (sessionId: string) => {}, })) || []; + // Add this function to handle manual refresh const handleRefresh = () => { - queryClient.invalidateQueries({ queryKey: ['logs', 'table', accessToken, startTime, endTime, filters] }); + logs.refetch(); }; + // Add this function to format the time range display const getTimeRangeDisplay = () => { if (isCustomDate) { return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; @@ -196,298 +298,7 @@ export default function SpendLogsTable({ setExpandedRequestId(requestId); }; - const logFilterOptions: FilterOption[] = [ - { - name: 'Team ID', - label: 'Team ID', - isSearchable: true, - searchFn: async (searchText: string) => { - if (!hookAllTeams || hookAllTeams.length === 0) return []; - const filtered = hookAllTeams.filter((team: Team) =>{ - console.log("team", searchText) - return team.team_id.toLowerCase().includes(searchText.toLowerCase()) || - (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) - }); - return filtered.map((team: Team) => ({ - label: `${team.team_alias || team.team_id} (${team.team_id})`, - value: team.team_id - })); - } - }, - // { - // name: 'Key Alias', - // label: 'Key Alias', - // isSearchable: true, - // searchFn: async (searchText: string) => { - // const filteredKeyAliases = allKeyAliases.filter(key => { - // console.log("key", searchText); - // return key.toLowerCase().includes(searchText.toLowerCase()) - // }); - - // return filteredKeyAliases.map((key) => { - // return { - // label: key, - // value: key - // } - // }); - // } - // }, - // { - // name: 'Key Hash', - // label: 'Key Hash', - // isSearchable: false, - // }, - // { - // name: 'Request ID', - // label: 'Request ID', - // isSearchable: false, - // }, - // { - // name: 'Model', - // label: 'Model', - // isSearchable: false, - // }, - // { - // name: 'User', - // label: 'User', - // isSearchable: true, - // searchFn: async (searchText: string) => { - // if (!allUsers || allUsers.length === 0) return []; - // const filtered = allUsers.filter((user: UserInfo) => - // (user.user_id && user.user_id.toLowerCase().includes(searchText.toLowerCase())) || - // (user.user_email && user.user_email.toLowerCase().includes(searchText.toLowerCase())) - // ); - // return filtered.map((user: UserInfo) => ({ - // label: `${user.user_email || user.user_id} (${user.user_id})`, - // value: user.user_id - // })); - // } - // }, - ]; - - if (selectedSessionId && sessionLogs.data) { - return ( -
- setSelectedSessionId(null)} - /> -
- ); - } - - const columns: ColumnDef[] = [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - const ExpanderCell = () => { - const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded()); - const toggleHandler = React.useCallback(() => { - setLocalExpanded((prev) => !prev); - row.getToggleExpandedHandler()(); - }, [row]); - - return row.getCanExpand() ? ( - - ) : ( - ● - ); - }; - return ; - }, - }, - { - header: "Time", - accessorKey: "startTime", - cell: (info: any) => , - }, - { - header: "Status", - accessorFn: (row) => row.metadata?.status || "Success", - cell: (info: any) => { - const status = info.getValue() as string; - const isSuccess = status.toLowerCase() !== "failure"; - return ( - - {isSuccess ? "Success" : "Failure"} - - ); - }, - }, - { - header: "Session ID", - accessorKey: "session_id", - cell: (info: any) => { - const value = String(info.getValue() || ""); - const onSessionClick = info.row.original.onSessionClick; - return value ? ( - - - - ) : "-"; - }, - }, - { - header: "Request ID", - accessorKey: "request_id", - cell: (info: any) => ( - - - {String(info.getValue() || "")} - - - ), - }, - { - header: "Cost", - accessorKey: "spend", - cell: (info: any) => ( - ${Number(info.getValue() || 0).toFixed(6)} - ), - }, - { - header: "Country", - accessorKey: "requester_ip_address", - cell: (info: any) => , - }, - { - header: "Team Name", - accessorKey: "team_id", - cell: ({ row }) => { - const teamId = row.original.team_id; - if (!teamId) return "-"; - const team = hookAllTeams.find(t => t.team_id === teamId); - const teamDisplayName = team?.team_alias || teamId; - const truncatedName = teamDisplayName.length > 20 - ? `${teamDisplayName.slice(0, 20)}...` - : teamDisplayName; - - return ( - - {truncatedName} - - ); - }, - }, - { - header: "Key Hash", - accessorFn: (row) => row.metadata?.user_api_key || "-", - cell: (info: any) => { - const value = String(info.getValue()); - const onKeyHashClick = info.row.original.onKeyHashClick; - return value !== "-" ? ( - - onKeyHashClick?.(value)} - > - {value} - - - ) : "-"; - }, - }, - { - header: "Key Name", - accessorFn: (row) => row.metadata?.user_api_key_alias || "-", - cell: (info: any) => ( - - {String(info.getValue())} - - ), - }, - { - header: "User", - accessorKey: "user", - cell: (info: any) => ( - - {String(info.getValue() || "-")} - - ), - }, - { - header: "End User", - accessorKey: "end_user", - cell: (info: any) => ( - - {String(info.getValue() || "-")} - - ), - }, - { - header: "Model", - accessorKey: "model", - cell: (info: any) => ( - - {String(info.getValue() || "-")} - - ), - }, - { - header: "Provider", - accessorKey: "custom_llm_provider", - cell: (info: any) => ( - - {String(info.getValue() || "-")} - - ), - }, - { - header: "Cache", - accessorKey: "cache_hit", - cell: (info: any) => info.getValue() || "-", - }, - { - header: "Tokens", - accessorKey: "total_tokens", - cell: (info: any) => { - const total = info.row.original.total_tokens || 0; - const prompt = info.row.original.prompt_tokens || 0; - const completion = info.row.original.completion_tokens || 0; - return ( - - {total} - - ); - }, - }, - ]; - + // When a session is selected, render the SessionView component if (selectedSessionId && sessionLogs.data) { return (
@@ -504,14 +315,35 @@ export default function SpendLogsTable({

- Request Logs + {selectedSessionId ? ( + <> + Session: {selectedSessionId} + + + ) : ( + "Request Logs" + )}

{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( - setSelectedKeyIdInfoView(null)} /> + setSelectedKeyIdInfoView(null)} /> + ) : selectedSessionId ? ( +
+ true} + // Optionally: add session-specific row expansion state + /> +
) : ( <> -
@@ -519,7 +351,7 @@ export default function SpendLogsTable({
setSearchTerm(e.target.value)} @@ -538,13 +370,10 @@ export default function SpendLogsTable({ />
-
- -
-
+
- {quickSelectOpen && ( -
-
- {[ - { label: "Last 15 Minutes", value: 15, unit: "minutes" }, - { label: "Last Hour", value: 1, unit: "hours" }, - { label: "Last 4 Hours", value: 4, unit: "hours" }, - { label: "Last 24 Hours", value: 24, unit: "hours" }, - { label: "Last 7 Days", value: 7, unit: "days" }, - ].map((option) => ( + {showFilters && ( +
+
+
+ Where +
+ + {showColumnDropdown && ( +
+ {["Team ID", "Key Hash"].map((option) => ( + + ))} +
+ )} +
+ { + if (selectedFilter === "Team ID") { + setTempTeamId(e.target.value); + } else { + setTempKeyHash(e.target.value); + } + }} + /> - ))} -
- +
+ +
+ + +
)}
- - + + {quickSelectOpen && ( +
+
+ {[ + { label: "Last 15 Minutes", value: 15, unit: "minutes" }, + { label: "Last Hour", value: 1, unit: "hours" }, + { label: "Last 4 Hours", value: 4, unit: "hours" }, + { label: "Last 24 Hours", value: 24, unit: "hours" }, + { label: "Last 7 Days", value: 7, unit: "days" }, + ].map((option) => ( + + ))} +
+ +
+
+ )} +
+ + + + + + Refresh + +
+ + {isCustomDate && ( +
+
+ { + setStartTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ to +
+ { + setEndTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ )}
- {isCustomDate && ( -
-
- { - setStartTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- to -
- { - setEndTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
+
+ + Showing{" "} + {logs.isLoading + ? "..." + : logs.data + ? (currentPage - 1) * pageSize + 1 + : 0}{" "} + -{" "} + {logs.isLoading + ? "..." + : logs.data + ? Math.min(currentPage * pageSize, logs.data.total) + : 0}{" "} + of{" "} + {logs.isLoading + ? "..." + : logs.data + ? logs.data.total + : 0}{" "} + results + +
+ + Page {logs.isLoading ? "..." : currentPage} of{" "} + {logs.isLoading + ? "..." + : logs.data + ? logs.data.total_pages + : 1} + + +
- )} +
true} - isLoading={isLoading} />
- )} + )}
); } export function RequestViewer({ row }: { row: Row }) { + // Helper function to clean metadata by removing specific fields const formatData = (input: any) => { if (typeof input === "string") { try { @@ -682,22 +712,28 @@ export function RequestViewer({ row }: { row: Row }) { return input; }; + // New helper function to get raw request const getRawRequest = () => { + // First check if proxy_server_request exists in metadata if (row.original?.proxy_server_request) { return formatData(row.original.proxy_server_request); } + // Fall back to messages if proxy_server_request is empty return formatData(row.original.messages); }; + // Extract error information from metadata if available const metadata = row.original.metadata || {}; const hasError = metadata.status === "failure"; const errorInfo = hasError ? metadata.error_information : null; + // Check if request/response data is missing const hasMessages = row.original.messages && (Array.isArray(row.original.messages) ? row.original.messages.length > 0 : Object.keys(row.original.messages).length > 0); const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0; const missingData = !hasMessages && !hasResponse; + // Format the response with error details if present const formattedResponse = () => { if (hasError && errorInfo) { return { @@ -712,12 +748,14 @@ export function RequestViewer({ row }: { row: Row }) { return formatData(row.original.response); }; + // Extract vector store request metadata if available const hasVectorStoreData = metadata.vector_store_request_metadata && Array.isArray(metadata.vector_store_request_metadata) && metadata.vector_store_request_metadata.length > 0; return (
+ {/* Combined Info Card */}

Request Details

@@ -774,7 +812,7 @@ export function RequestViewer({ row }: { row: Row }) {
Status: - }) {
+ {/* Configuration Info Message - Show when data is missing */} + {/* Request/Response Panel */}
+ {/* Request Side */}

Request

@@ -818,6 +859,7 @@ export function RequestViewer({ row }: { row: Row }) {
+ {/* Response Side */}

@@ -850,12 +892,15 @@ export function RequestViewer({ row }: { row: Row }) {

- {hasVectorStoreData && ( + {/* Vector Store Request Data - Show only if present */} + {hasVectorStoreData && ( )} + {/* Error Card - Only show for failures */} {hasError && errorInfo && } + {/* Tags Card - Only show if there are tags */} {row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (
@@ -873,6 +918,7 @@ export function RequestViewer({ row }: { row: Row }) {
)} + {/* Metadata Card - Only show if there's metadata */} {row.original.metadata && Object.keys(row.original.metadata).length > 0 && (
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 deleted file mode 100644 index 0a04e161ea2..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useCallback, useEffect, useState, useRef, useMemo } from "react"; -import { LogEntry } from "./columns"; -import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall } from "../networking"; -import { KeyResponse } from "../key_team_helpers/key_list"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Setter } from "@/types"; -import { debounce } from "lodash"; -import { defaultPageSize } from "../constants"; -import moment from "moment"; -import { fetchAllKeyAliases, fetchAllTeams } from "../key_team_helpers/filter_helpers"; // Import fetchAllTeams - -// Define the shape of the filter state for logs -export interface LogFilterState { - 'Team ID': string; - 'Organization ID': string; // Added for potential future use - 'Key Alias': string; - 'Key Hash': string; - 'Request ID': string; - 'Model': string; - 'User': string; - 'Cache Hit': 'true' | 'false' | ''; // Example for boolean filter - 'Status': 'success' | 'failure' | ''; // Example for status filter - [key: string]: string | ''; // Allow for other string-based filters -} - -// Define the shape of the pagination state -export interface PaginationState { - currentPage: number; - totalPages: number; - totalCount: number; - pageSize: number; -} - -export function useLogFilterLogic({ - accessToken, - startTime, // Receive from SpendLogsTable - endTime, // Receive from SpendLogsTable - pageSize = defaultPageSize, - initialPage = 1, - initialFilters = {} -}: { - accessToken: string | null; - startTime: string; - endTime: string; - pageSize?: number; - initialPage?: number; - initialFilters?: Partial; -}) { - const defaultFilters: LogFilterState = { - 'Team ID': '', - 'Organization ID': '', - 'Key Alias': '', - 'Key Hash': '', - 'Request ID': '', - 'Model': '', - 'User': '', - 'Cache Hit': '', - 'Status': '', - ...initialFilters - }; - - const [filters, setFilters] = useState(defaultFilters); - const [currentPage, setCurrentPage] = useState(initialPage); - const queryClient = useQueryClient(); - - // States for manually managed data, loading, and error - const [logEntries, setLogEntries] = useState([]); - const [paginationDetails, setPaginationDetails] = useState({ - currentPage: initialPage, - totalPages: 0, - totalCount: 0, - pageSize - }); - const [isLoadingLogs, setIsLoadingLogs] = useState(false); - const [logsError, setLogsError] = useState(null); - const lastSearchTimestamp = useRef(0); - - const queryAllKeysQuery = useQuery({ - queryKey: ['allKeysForLogFilters', accessToken], // Ensure unique queryKey - queryFn: async () => { - if (!accessToken) throw new Error('Access token required'); - return await fetchAllKeyAliases(accessToken); - }, - enabled: !!accessToken - }); - const allKeyAliases = queryAllKeysQuery.data || [] - - - // 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 - }, - enabled: !!accessToken, - }); - - const { data: allUsers } = useQuery({ - 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 - return response.users || []; - }, - enabled: !!accessToken, - }); - - // 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 currentTimestamp = Date.now(); - lastSearchTimestamp.current = currentTimestamp; - setIsLoadingLogs(true); - setLogsError(null); - - 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 = currentFilters['Key Hash'] || undefined; - const teamIdParam = currentFilters['Team ID'] || undefined; - const requestIdParam = currentFilters['Request ID'] || undefined; - const userIdParam = currentFilters['User'] || undefined; - // const modelParam = currentFilters['Model'] || undefined; // Prepared if uiSpendLogsCall is updated - - const response = await uiSpendLogsCall( - accessToken, - apiKeyParam, - teamIdParam, - requestIdParam, - formattedStartTime, - formattedEndTime, - pageToFetch, - pageSize, - userIdParam - // If uiSpendLogsCall is updated to accept more params (e.g., model), pass them here: - // 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); - } - // Cleanup function for debounce - return () => { - debouncedSearch.cancel(); - }; - }, [accessToken, filters, currentPage, startTime, endTime, pageSize, debouncedSearch]); - - - const handleFilterChange = (newFilters: Partial) => { - setFilters(prev => { - const updatedFilters = { ...prev, ...newFilters }; // Simpler update - // 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]; - } - } - return updatedFilters as LogFilterState; - }); - setCurrentPage(1); // Reset to first page when filters change - // debouncedSearch will be called by the useEffect hook - }; - - const handleFilterReset = () => { - setFilters(defaultFilters); - setCurrentPage(1); - // debouncedSearch will be called by the useEffect hook - }; - - const pagination: PaginationState = { - currentPage: paginationDetails.currentPage, - totalPages: paginationDetails.totalPages, - totalCount: paginationDetails.totalCount, - pageSize: paginationDetails.pageSize, - }; - - return { - filters, - filteredLogs: logEntries, - allKeyAliases, - allTeams: allTeams || [], - allUsers: allUsers || [], - allOrganizations: [], // Placeholder for now, can be fetched if needed - handleFilterChange, - handleFilterReset, - isLoading: isLoadingLogs, - pagination, - setCurrentPage, - error: logsError - }; -} \ No newline at end of file