Raju2024's picture
Upload 1072 files
e3278e4 verified
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);
// New state variables for Start and End Time
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")
);
// 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 [selectedFilter, setSelectedFilter] = useState("Team ID");
// 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)
) {
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,
};
}
// Convert times to UTC before formatting
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));
// No need for additional filtering since we're now handling this in the API call
return matchesSearch;
}) || [];
// Add this function to handle manual refresh
const handleRefresh = () => {
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')}`;
}
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>
);
}