|
import moment from "moment"; |
|
import { useQuery } from "@tanstack/react-query"; |
|
import { useState, useRef, useEffect } from "react"; |
|
|
|
import { uiSpendLogsCall } from "../networking"; |
|
import { DataTable } from "./table"; |
|
import { columns, LogEntry } from "./columns"; |
|
import { Row } from "@tanstack/react-table"; |
|
|
|
interface SpendLogsTableProps { |
|
accessToken: string | null; |
|
token: string | null; |
|
userRole: string | null; |
|
userID: string | null; |
|
} |
|
|
|
interface PaginatedResponse { |
|
data: LogEntry[]; |
|
total: number; |
|
page: number; |
|
page_size: number; |
|
total_pages: number; |
|
} |
|
|
|
export default function SpendLogsTable({ |
|
accessToken, |
|
token, |
|
userRole, |
|
userID, |
|
}: 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<HTMLDivElement>(null); |
|
const filtersRef = useRef<HTMLDivElement>(null); |
|
const quickSelectRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
const [startTime, setStartTime] = useState<string>( |
|
moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") |
|
); |
|
const [endTime, setEndTime] = useState<string>( |
|
moment().format("YYYY-MM-DDTHH:mm") |
|
); |
|
|
|
|
|
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 [selectedFilter, setSelectedFilter] = useState("Team ID"); |
|
|
|
|
|
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) |
|
) { |
|
setQuickSelectOpen(false); |
|
} |
|
} |
|
|
|
document.addEventListener("mousedown", handleClickOutside); |
|
return () => |
|
document.removeEventListener("mousedown", handleClickOutside); |
|
}, []); |
|
|
|
const logs = useQuery<PaginatedResponse>({ |
|
queryKey: [ |
|
"logs", |
|
"table", |
|
currentPage, |
|
pageSize, |
|
startTime, |
|
endTime, |
|
selectedTeamId, |
|
selectedKeyHash, |
|
], |
|
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"); |
|
|
|
return await uiSpendLogsCall( |
|
accessToken, |
|
selectedKeyHash || undefined, |
|
selectedTeamId || undefined, |
|
undefined, |
|
formattedStartTime, |
|
formattedEndTime, |
|
currentPage, |
|
pageSize |
|
); |
|
}, |
|
enabled: !!accessToken && !!token && !!userRole && !!userID, |
|
refetchInterval: 5000, |
|
refetchIntervalInBackground: true, |
|
}); |
|
|
|
if (!accessToken || !token || !userRole || !userID) { |
|
console.log( |
|
"got None values for one of accessToken, token, userRole, userID", |
|
); |
|
return null; |
|
} |
|
|
|
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)); |
|
|
|
|
|
return matchesSearch; |
|
}) || []; |
|
|
|
|
|
const handleRefresh = () => { |
|
logs.refetch(); |
|
}; |
|
|
|
|
|
const getTimeRangeDisplay = () => { |
|
if (isCustomDate) { |
|
return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; |
|
} |
|
|
|
const now = moment(); |
|
const start = moment(startTime); |
|
const diffMinutes = now.diff(start, 'minutes'); |
|
|
|
if (diffMinutes <= 15) return 'Last 15 Minutes'; |
|
if (diffMinutes <= 60) return 'Last Hour'; |
|
|
|
const diffHours = now.diff(start, 'hours'); |
|
if (diffHours <= 4) return 'Last 4 Hours'; |
|
if (diffHours <= 24) return 'Last 24 Hours'; |
|
if (diffHours <= 168) return 'Last 7 Days'; |
|
return `${start.format('MMM D')} - ${now.format('MMM D')}`; |
|
}; |
|
|
|
return ( |
|
<div className="w-full p-6"> |
|
<div className="flex items-center justify-between mb-4"> |
|
<h1 className="text-xl font-semibold">Request Logs</h1> |
|
</div> |
|
|
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="border-b px-6 py-4"> |
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0"> |
|
<div className="flex flex-wrap items-center gap-3"> |
|
<div className="relative w-64"> |
|
<input |
|
type="text" |
|
placeholder="Search by Request ID" |
|
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
value={searchTerm} |
|
onChange={(e) => setSearchTerm(e.target.value)} |
|
/> |
|
<svg |
|
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" |
|
fill="none" |
|
stroke="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" |
|
/> |
|
</svg> |
|
</div> |
|
<div className="relative" ref={filtersRef}> |
|
<button |
|
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2" |
|
onClick={() => setShowFilters(!showFilters)} |
|
> |
|
<svg |
|
className="w-4 h-4" |
|
fill="none" |
|
stroke="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" |
|
/> |
|
</svg> |
|
Filter |
|
</button> |
|
|
|
{showFilters && ( |
|
<div className="absolute left-0 mt-2 w-[500px] bg-white rounded-lg shadow-lg border p-4 z-50"> |
|
<div className="flex flex-col gap-4"> |
|
<div className="flex items-center gap-2"> |
|
<span className="text-sm font-medium">Where</span> |
|
<div className="relative"> |
|
<button |
|
onClick={() => setShowColumnDropdown(!showColumnDropdown)} |
|
className="px-3 py-1.5 border rounded-md bg-white text-sm min-w-[160px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-left flex justify-between items-center" |
|
> |
|
{selectedFilter} |
|
<svg |
|
className="h-4 w-4 text-gray-500" |
|
fill="none" |
|
stroke="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M19 9l-7 7-7-7" |
|
/> |
|
</svg> |
|
</button> |
|
{showColumnDropdown && ( |
|
<div className="absolute left-0 mt-1 w-[160px] bg-white border rounded-md shadow-lg z-50"> |
|
{["Team ID", "Key Hash"].map((option) => ( |
|
<button |
|
key={option} |
|
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2 ${ |
|
selectedFilter === option |
|
? "bg-blue-50 text-blue-600" |
|
: "" |
|
}`} |
|
onClick={() => { |
|
setSelectedFilter(option); |
|
setShowColumnDropdown(false); |
|
if (option === "Team ID") { |
|
setTempKeyHash(""); |
|
} else { |
|
setTempTeamId(""); |
|
} |
|
}} |
|
> |
|
{selectedFilter === option && ( |
|
<svg |
|
className="h-4 w-4 text-blue-600" |
|
fill="none" |
|
viewBox="0 0 24 24" |
|
stroke="currentColor" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M5 13l4 4L19 7" |
|
/> |
|
</svg> |
|
)} |
|
{option} |
|
</button> |
|
))} |
|
</div> |
|
)} |
|
</div> |
|
<input |
|
type="text" |
|
placeholder="Enter value..." |
|
className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" |
|
value={selectedFilter === "Team ID" ? tempTeamId : tempKeyHash} |
|
onChange={(e) => { |
|
if (selectedFilter === "Team ID") { |
|
setTempTeamId(e.target.value); |
|
} else { |
|
setTempKeyHash(e.target.value); |
|
} |
|
}} |
|
/> |
|
<button |
|
className="p-1 hover:bg-gray-100 rounded-md" |
|
onClick={() => { |
|
setTempTeamId(""); |
|
setTempKeyHash(""); |
|
}} |
|
> |
|
<span className="text-gray-500">×</span> |
|
</button> |
|
</div> |
|
|
|
<div className="flex justify-end gap-2"> |
|
<button |
|
className="px-3 py-1.5 text-sm border rounded-md hover:bg-gray-50" |
|
onClick={() => { |
|
setTempTeamId(""); |
|
setTempKeyHash(""); |
|
setShowFilters(false); |
|
}} |
|
> |
|
Cancel |
|
</button> |
|
<button |
|
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700" |
|
onClick={() => { |
|
setSelectedTeamId(tempTeamId); |
|
setSelectedKeyHash(tempKeyHash); |
|
setCurrentPage(1); // Reset to first page when applying new filters |
|
setShowFilters(false); |
|
}} |
|
> |
|
Apply Filters |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
|
|
<div className="flex items-center gap-2"> |
|
<div className="relative" ref={quickSelectRef}> |
|
<button |
|
onClick={() => setQuickSelectOpen(!quickSelectOpen)} |
|
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2" |
|
> |
|
<svg |
|
className="w-4 h-4" |
|
fill="none" |
|
stroke="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" |
|
/> |
|
</svg> |
|
{getTimeRangeDisplay()} |
|
</button> |
|
|
|
{quickSelectOpen && ( |
|
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border p-2 z-50"> |
|
<div className="space-y-1"> |
|
{[ |
|
{ 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) => ( |
|
<button |
|
key={option.label} |
|
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${ |
|
getTimeRangeDisplay() === option.label ? 'bg-blue-50 text-blue-600' : '' |
|
}`} |
|
onClick={() => { |
|
setEndTime(moment().format("YYYY-MM-DDTHH:mm")); |
|
setStartTime( |
|
moment() |
|
.subtract(option.value, option.unit as any) |
|
.format("YYYY-MM-DDTHH:mm") |
|
); |
|
setQuickSelectOpen(false); |
|
setIsCustomDate(false); |
|
}} |
|
> |
|
{option.label} |
|
</button> |
|
))} |
|
<div className="border-t my-2" /> |
|
<button |
|
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${ |
|
isCustomDate ? 'bg-blue-50 text-blue-600' : '' |
|
}`} |
|
onClick={() => setIsCustomDate(!isCustomDate)} |
|
> |
|
Custom Range |
|
</button> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
|
|
<button |
|
onClick={handleRefresh} |
|
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2" |
|
title="Refresh data" |
|
> |
|
<svg |
|
className={`w-4 h-4 ${logs.isFetching ? 'animate-spin' : ''}`} |
|
fill="none" |
|
stroke="currentColor" |
|
viewBox="0 0 24 24" |
|
> |
|
<path |
|
strokeLinecap="round" |
|
strokeLinejoin="round" |
|
strokeWidth={2} |
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" |
|
/> |
|
</svg> |
|
<span>Refresh</span> |
|
</button> |
|
</div> |
|
|
|
{isCustomDate && ( |
|
<div className="flex items-center gap-2"> |
|
<div> |
|
<input |
|
type="datetime-local" |
|
value={startTime} |
|
onChange={(e) => { |
|
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" |
|
/> |
|
</div> |
|
<span className="text-gray-500">to</span> |
|
<div> |
|
<input |
|
type="datetime-local" |
|
value={endTime} |
|
onChange={(e) => { |
|
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" |
|
/> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
|
|
<div className="flex items-center space-x-4"> |
|
<span className="text-sm text-gray-700"> |
|
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 |
|
</span> |
|
<div className="flex items-center space-x-2"> |
|
<span className="text-sm text-gray-700"> |
|
Page {logs.isLoading ? "..." : currentPage} of{" "} |
|
{logs.isLoading |
|
? "..." |
|
: logs.data |
|
? logs.data.total_pages |
|
: 1} |
|
</span> |
|
<button |
|
onClick={() => |
|
setCurrentPage((p) => Math.max(1, p - 1)) |
|
} |
|
disabled={logs.isLoading || currentPage === 1} |
|
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" |
|
> |
|
Previous |
|
</button> |
|
<button |
|
onClick={() => |
|
setCurrentPage((p) => |
|
Math.min( |
|
logs.data?.total_pages || 1, |
|
p + 1, |
|
), |
|
) |
|
} |
|
disabled={ |
|
logs.isLoading || |
|
currentPage === (logs.data?.total_pages || 1) |
|
} |
|
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" |
|
> |
|
Next |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<DataTable |
|
columns={columns} |
|
data={filteredData} |
|
renderSubComponent={RequestViewer} |
|
getRowCanExpand={() => true} |
|
/> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|
|
function RequestViewer({ row }: { row: Row<LogEntry> }) { |
|
const formatData = (input: any) => { |
|
if (typeof input === "string") { |
|
try { |
|
return JSON.parse(input); |
|
} catch { |
|
return input; |
|
} |
|
} |
|
return input; |
|
}; |
|
|
|
return ( |
|
<div className="p-6 bg-gray-50 space-y-6"> |
|
{/* Combined Info Card */} |
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="p-4 border-b"> |
|
<h3 className="text-lg font-medium ">Request Details</h3> |
|
</div> |
|
<div className="space-y-2 p-4 "> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Request ID:</span> |
|
<span>{row.original.request_id}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Api Key:</span> |
|
<span>{row.original.api_key}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Team ID:</span> |
|
<span>{row.original.team_id}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Model:</span> |
|
<span>{row.original.model}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Api Base:</span> |
|
<span>{row.original.api_base}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Call Type:</span> |
|
<span>{row.original.call_type}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Spend:</span> |
|
<span>{row.original.spend}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Total Tokens:</span> |
|
<span>{row.original.total_tokens}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Prompt Tokens:</span> |
|
<span>{row.original.prompt_tokens}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Completion Tokens:</span> |
|
<span>{row.original.completion_tokens}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Start Time:</span> |
|
<span>{row.original.startTime}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">End Time:</span> |
|
<span>{row.original.endTime}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Cache Hit:</span> |
|
<span>{row.original.cache_hit}</span> |
|
</div> |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Cache Key:</span> |
|
<span>{row.original.cache_key}</span> |
|
</div> |
|
{row?.original?.requester_ip_address && ( |
|
<div className="flex"> |
|
<span className="font-medium w-1/3">Request IP Address:</span> |
|
<span>{row?.original?.requester_ip_address}</span> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
|
|
{/* Request Card */} |
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="flex justify-between items-center p-4 border-b"> |
|
<h3 className="text-lg font-medium">Request Tags</h3> |
|
</div> |
|
<pre className="p-4 text-wrap overflow-auto text-sm"> |
|
{JSON.stringify(formatData(row.original.request_tags), null, 2)} |
|
</pre> |
|
</div> |
|
|
|
{/* Request Card */} |
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="flex justify-between items-center p-4 border-b"> |
|
<h3 className="text-lg font-medium">Request</h3> |
|
{/* <div> |
|
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50"> |
|
Expand |
|
</button> |
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50"> |
|
JSON |
|
</button> |
|
</div> */} |
|
</div> |
|
<pre className="p-4 text-wrap overflow-auto text-sm"> |
|
{JSON.stringify(formatData(row.original.messages), null, 2)} |
|
</pre> |
|
</div> |
|
|
|
{/* Response Card */} |
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="flex justify-between items-center p-4 border-b"> |
|
<h3 className="text-lg font-medium">Response</h3> |
|
<div> |
|
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50"> |
|
Expand |
|
</button> |
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50"> |
|
JSON |
|
</button> */} |
|
</div> |
|
</div> |
|
<pre className="p-4 text-wrap overflow-auto text-sm"> |
|
{JSON.stringify(formatData(row.original.response), null, 2)} |
|
</pre> |
|
</div> |
|
|
|
{/* Metadata Card */} |
|
{row.original.metadata && |
|
Object.keys(row.original.metadata).length > 0 && ( |
|
<div className="bg-white rounded-lg shadow"> |
|
<div className="flex justify-between items-center p-4 border-b"> |
|
<h3 className="text-lg font-medium">Metadata</h3> |
|
{/* <div> |
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50"> |
|
JSON |
|
</button> |
|
</div> */} |
|
</div> |
|
<pre className="p-4 text-wrap overflow-auto text-sm "> |
|
{JSON.stringify(row.original.metadata, null, 2)} |
|
</pre> |
|
</div> |
|
)} |
|
</div> |
|
); |
|
} |
|
|