mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 03:34:10 +00:00
(UI Logs) - add pagination + filtering by key name/team name (#7860)
* fix remove emoji on logs page * fix title of page * ui - get countryIP * ui lookup * ui - get country from ip address * show team and key alias on root * working team / key filter * working filters * ui filtering by key / team alias * simple search * fix add pagination on view logs page * add start / end time filters * add custom time filter
This commit is contained in:
parent
f60756c815
commit
f6a0bc8bdb
5 changed files with 453 additions and 42 deletions
|
@ -11,7 +11,11 @@ model_list:
|
||||||
api_key: os.environ/ANTHROPIC_API_KEY
|
api_key: os.environ/ANTHROPIC_API_KEY
|
||||||
model_info:
|
model_info:
|
||||||
health_check_model: anthropic/claude-3-5-sonnet-20240620
|
health_check_model: anthropic/claude-3-5-sonnet-20240620
|
||||||
|
- model_name: fake-openai-endpoint
|
||||||
|
litellm_params:
|
||||||
|
model: openai/fake
|
||||||
|
api_key: fake-key
|
||||||
|
api_base: https://exampleopenaiendpoint-production.up.railway.app/
|
||||||
|
|
||||||
general_settings:
|
general_settings:
|
||||||
store_prompts_in_spend_logs: true
|
store_prompts_in_spend_logs: true
|
||||||
|
|
||||||
|
|
|
@ -1638,10 +1638,25 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
default=None,
|
default=None,
|
||||||
description="Time till which to view key spend",
|
description="Time till which to view key spend",
|
||||||
),
|
),
|
||||||
|
page: int = fastapi.Query(
|
||||||
|
default=1, description="Page number for pagination", ge=1
|
||||||
|
),
|
||||||
|
page_size: int = fastapi.Query(
|
||||||
|
default=50, description="Number of items per page", ge=1, le=100
|
||||||
|
),
|
||||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
View spend logs for UI
|
View spend logs for UI with pagination support
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"data": List[LiteLLM_SpendLogs], # Paginated spend logs
|
||||||
|
"total": int, # Total number of records
|
||||||
|
"page": int, # Current page number
|
||||||
|
"page_size": int, # Number of items per page
|
||||||
|
"total_pages": int # Total number of pages
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
from litellm.proxy.proxy_server import prisma_client
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
|
@ -1656,7 +1671,13 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
verbose_proxy_logger.debug(
|
verbose_proxy_logger.debug(
|
||||||
"Prompts and responses are not stored in spend logs, returning empty list"
|
"Prompts and responses are not stored in spend logs, returning empty list"
|
||||||
)
|
)
|
||||||
return []
|
return {
|
||||||
|
"data": [],
|
||||||
|
"total": 0,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total_pages": 0,
|
||||||
|
}
|
||||||
if start_date is None or end_date is None:
|
if start_date is None or end_date is None:
|
||||||
raise ProxyException(
|
raise ProxyException(
|
||||||
message="Start date and end date are required",
|
message="Start date and end date are required",
|
||||||
|
@ -1672,15 +1693,39 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
start_date_iso = start_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
start_date_iso = start_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
||||||
end_date_iso = end_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
end_date_iso = end_date_obj.isoformat() + "Z" # Add Z to indicate UTC
|
||||||
|
|
||||||
return await prisma_client.db.litellm_spendlogs.find_many(
|
# 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={
|
||||||
|
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get paginated data
|
||||||
|
data = await prisma_client.db.litellm_spendlogs.find_many(
|
||||||
where={
|
where={
|
||||||
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
||||||
},
|
},
|
||||||
order={
|
order={
|
||||||
"startTime": "desc",
|
"startTime": "desc",
|
||||||
},
|
},
|
||||||
|
skip=skip,
|
||||||
|
take=page_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Calculate total pages
|
||||||
|
total_pages = (total_records + page_size - 1) // page_size
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": data,
|
||||||
|
"total": total_records,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/spend/logs",
|
"/spend/logs",
|
||||||
|
|
|
@ -1513,6 +1513,7 @@ export const userSpendLogsCall = async (
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uiSpendLogsCall = async (
|
export const uiSpendLogsCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
api_key?: string,
|
api_key?: string,
|
||||||
|
@ -1520,6 +1521,8 @@ export const uiSpendLogsCall = async (
|
||||||
request_id?: string,
|
request_id?: string,
|
||||||
start_date?: string,
|
start_date?: string,
|
||||||
end_date?: string,
|
end_date?: string,
|
||||||
|
page?: number,
|
||||||
|
page_size?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Construct base URL
|
// Construct base URL
|
||||||
|
@ -1532,6 +1535,9 @@ export const uiSpendLogsCall = async (
|
||||||
if (request_id) queryParams.append('request_id', request_id);
|
if (request_id) queryParams.append('request_id', request_id);
|
||||||
if (start_date) queryParams.append('start_date', start_date);
|
if (start_date) queryParams.append('start_date', start_date);
|
||||||
if (end_date) queryParams.append('end_date', end_date);
|
if (end_date) queryParams.append('end_date', end_date);
|
||||||
|
if (page) queryParams.append('page', page.toString());
|
||||||
|
if (page_size) queryParams.append('page_size', page_size.toString());
|
||||||
|
|
||||||
// Append query parameters to URL if any exist
|
// Append query parameters to URL if any exist
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
|
|
|
@ -56,13 +56,25 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||||
header: "Request ID",
|
header: "Request ID",
|
||||||
accessorKey: "request_id",
|
accessorKey: "request_id",
|
||||||
cell: (info: any) => (
|
cell: (info: any) => (
|
||||||
<span className="font-mono text-xs">{String(info.getValue() || "")}</span>
|
<span className="font-mono text-xs max-w-[100px] truncate block">
|
||||||
|
{String(info.getValue() || "")}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Type",
|
header: "Country",
|
||||||
accessorKey: "call_type",
|
accessorKey: "requester_ip_address",
|
||||||
cell: (info: any) => <span>{String(info.getValue() || "")}</span>,
|
cell: (info: any) => <CountryCell ipAddress={info.getValue()} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Team",
|
||||||
|
accessorKey: "metadata.user_api_key_team_alias",
|
||||||
|
cell: (info: any) => <span>{String(info.getValue() || "-")}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Key Name",
|
||||||
|
accessorKey: "metadata.user_api_key_alias",
|
||||||
|
cell: (info: any) => <span>{String(info.getValue() || "-")}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Request",
|
header: "Request",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
import { uiSpendLogsCall } from "../networking";
|
import { uiSpendLogsCall } from "../networking";
|
||||||
import { DataTable } from "./table";
|
import { DataTable } from "./table";
|
||||||
|
@ -13,39 +14,92 @@ interface SpendLogsTableProps {
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PaginatedResponse {
|
||||||
|
data: LogEntry[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SpendLogsTable({
|
export default function SpendLogsTable({
|
||||||
accessToken,
|
accessToken,
|
||||||
token,
|
token,
|
||||||
userRole,
|
userRole,
|
||||||
userID,
|
userID,
|
||||||
}: SpendLogsTableProps) {
|
}: SpendLogsTableProps) {
|
||||||
const logs = useQuery({
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
queryKey: ["logs", "table"],
|
const [keyNameFilter, setKeyNameFilter] = useState("");
|
||||||
|
const [teamNameFilter, setTeamNameFilter] = useState("");
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
|
||||||
|
const [selectedColumn, setSelectedColumn] = useState("Key Name");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize] = useState(50);
|
||||||
|
const dropdownRef = 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);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowColumnDropdown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logs = useQuery<PaginatedResponse>({
|
||||||
|
queryKey: ["logs", "table", currentPage, pageSize, startTime, endTime],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
console.log(
|
console.log(
|
||||||
"got None values for one of accessToken, token, userRole, userID",
|
"got None values for one of accessToken, token, userRole, userID",
|
||||||
);
|
);
|
||||||
return;
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
page_size: pageSize,
|
||||||
|
total_pages: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get logs for last 24 hours using ISO string and proper date formatting
|
const formattedStartTime = moment(startTime).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const endTime = moment().format("YYYY-MM-DD HH:mm:ss");
|
const formattedEndTime = moment(endTime).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const startTime = moment()
|
|
||||||
.subtract(24, "hours")
|
|
||||||
.format("YYYY-MM-DD HH:mm:ss");
|
|
||||||
|
|
||||||
const data = await uiSpendLogsCall(
|
const data = await uiSpendLogsCall(
|
||||||
accessToken,
|
accessToken,
|
||||||
token,
|
token,
|
||||||
userRole,
|
userRole,
|
||||||
userID,
|
userID,
|
||||||
startTime,
|
formattedStartTime,
|
||||||
endTime,
|
formattedEndTime,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
// Refetch when startTime or endTime changes
|
||||||
|
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
|
@ -55,36 +109,326 @@ export default function SpendLogsTable({
|
||||||
return null;
|
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));
|
||||||
|
const matchesKeyName =
|
||||||
|
!keyNameFilter ||
|
||||||
|
(log.metadata?.user_api_key_alias &&
|
||||||
|
log.metadata.user_api_key_alias
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyNameFilter.toLowerCase()));
|
||||||
|
const matchesTeamName =
|
||||||
|
!teamNameFilter ||
|
||||||
|
(log.metadata?.user_api_key_team_alias &&
|
||||||
|
log.metadata.user_api_key_team_alias
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(teamNameFilter.toLowerCase()));
|
||||||
|
return matchesSearch && matchesKeyName && matchesTeamName;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 md:px-8 py-8 w-full">
|
<div className="w-full">
|
||||||
<h1 className="text-xl font-semibold mb-4">Traces</h1>
|
<h1 className="text-xl font-semibold mb-4">Traces</h1>
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
<div className="p-4 border-b flex justify-between items-center">
|
<div className="border-b px-6 py-4">
|
||||||
<div className="flex space-x-4 items-center">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-4 md:space-y-0">
|
||||||
<input
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
type="text"
|
<div className="relative w-64">
|
||||||
placeholder="Search by request ID, model, or user..."
|
<input
|
||||||
className="px-4 py-2 border rounded-lg w-80"
|
type="text"
|
||||||
/>
|
placeholder="Search by Request ID"
|
||||||
<button className="px-4 py-2 border rounded-lg flex items-center gap-2">
|
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"
|
||||||
<span>Filters</span>
|
value={searchTerm}
|
||||||
<span className="text-xs bg-gray-100 px-2 py-1 rounded">0</span>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
</button>
|
/>
|
||||||
<select className="px-4 py-2 border rounded-lg">
|
<svg
|
||||||
<option>Last 24 hours</option>
|
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
|
||||||
<option>Last 7 days</option>
|
fill="none"
|
||||||
<option>Last 30 days</option>
|
stroke="currentColor"
|
||||||
</select>
|
viewBox="0 0 24 24"
|
||||||
</div>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<path
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
strokeLinecap="round"
|
||||||
Export
|
strokeLinejoin="round"
|
||||||
</button>
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<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 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"
|
||||||
|
>
|
||||||
|
{selectedColumn}
|
||||||
|
<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">
|
||||||
|
{["Key Name", "Team Name"].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 ${
|
||||||
|
selectedColumn === option
|
||||||
|
? "bg-blue-50 text-blue-600"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedColumn(option);
|
||||||
|
setShowColumnDropdown(false);
|
||||||
|
if (option === "Key Name") {
|
||||||
|
setTeamNameFilter("");
|
||||||
|
} else {
|
||||||
|
setKeyNameFilter("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedColumn === 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={keyNameFilter || teamNameFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (selectedColumn === "Key Name") {
|
||||||
|
setKeyNameFilter(e.target.value);
|
||||||
|
} else {
|
||||||
|
setTeamNameFilter(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-gray-100 rounded-md"
|
||||||
|
onClick={() => {
|
||||||
|
setKeyNameFilter("");
|
||||||
|
setTeamNameFilter("");
|
||||||
|
setShowFilters(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-gray-500">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<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>
|
||||||
|
Time Range
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{quickSelectOpen && (
|
||||||
|
<div className="absolute left-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"
|
||||||
|
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"
|
||||||
|
onClick={() => setIsCustomDate(!isCustomDate)}
|
||||||
|
>
|
||||||
|
Custom Range
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={logs.data}
|
data={filteredData}
|
||||||
renderSubComponent={RequestViewer}
|
renderSubComponent={RequestViewer}
|
||||||
getRowCanExpand={() => true}
|
getRowCanExpand={() => true}
|
||||||
/>
|
/>
|
||||||
|
@ -121,7 +465,7 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-4 overflow-auto text-sm">
|
<pre className="p-4 overflow-auto text-sm">
|
||||||
{JSON.stringify(formatData(row.original.request_id), null, 2)}
|
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue