mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-15 20:20:28 +00:00
v0 - looks decen view
This commit is contained in:
@@ -0,0 +1,422 @@
|
|||||||
|
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tooltip, Tabs, message } from "antd";
|
||||||
|
import { CloseOutlined, CopyOutlined } from "@ant-design/icons";
|
||||||
|
import { Row } from "@tanstack/react-table";
|
||||||
|
import { LogEntry } from "./columns";
|
||||||
|
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||||
|
import { truncateString } from "@/utils/textUtils";
|
||||||
|
import GuardrailViewer from "./GuardrailViewer/GuardrailViewer";
|
||||||
|
import { CostBreakdownViewer } from "./CostBreakdownViewer";
|
||||||
|
import { ConfigInfoMessage } from "./ConfigInfoMessage";
|
||||||
|
import { RequestResponsePanel } from "./RequestResponsePanel";
|
||||||
|
import { VectorStoreViewer } from "./VectorStoreViewer";
|
||||||
|
import { ErrorViewer } from "./ErrorViewer";
|
||||||
|
import { JsonView, defaultStyles } from "react-json-view-lite";
|
||||||
|
import "react-json-view-lite/dist/index.css";
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface LogDetailsDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
logEntry: LogEntry | null;
|
||||||
|
onOpenSettings?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDetailsDrawer({ open, onClose, logEntry, onOpenSettings }: LogDetailsDrawerProps) {
|
||||||
|
if (!logEntry) return null;
|
||||||
|
|
||||||
|
// Helper function to clean metadata by removing specific fields
|
||||||
|
const formatData = (input: any) => {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(input);
|
||||||
|
} catch {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get raw request
|
||||||
|
const getRawRequest = () => {
|
||||||
|
// First check if proxy_server_request exists in metadata
|
||||||
|
if (logEntry?.proxy_server_request) {
|
||||||
|
return formatData(logEntry.proxy_server_request);
|
||||||
|
}
|
||||||
|
// Fall back to messages if proxy_server_request is empty
|
||||||
|
return formatData(logEntry.messages);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract error information from metadata if available
|
||||||
|
const metadata = logEntry.metadata || {};
|
||||||
|
const hasError = metadata.status === "failure";
|
||||||
|
const errorInfo = hasError ? metadata.error_information : null;
|
||||||
|
|
||||||
|
// Check if request/response data is missing
|
||||||
|
const hasMessages =
|
||||||
|
logEntry.messages &&
|
||||||
|
(Array.isArray(logEntry.messages)
|
||||||
|
? logEntry.messages.length > 0
|
||||||
|
: Object.keys(logEntry.messages).length > 0);
|
||||||
|
const hasResponse = logEntry.response && Object.keys(formatData(logEntry.response)).length > 0;
|
||||||
|
const missingData = !hasMessages && !hasResponse;
|
||||||
|
|
||||||
|
// Format the response with error details if present
|
||||||
|
const formattedResponse = () => {
|
||||||
|
if (hasError && errorInfo) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: errorInfo.error_message || "An error occurred",
|
||||||
|
type: errorInfo.error_class || "error",
|
||||||
|
code: errorInfo.error_code || "unknown",
|
||||||
|
param: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return formatData(logEntry.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;
|
||||||
|
|
||||||
|
// Extract guardrail information from metadata if available
|
||||||
|
const guardrailInfo = logEntry.metadata?.guardrail_information;
|
||||||
|
const guardrailEntries = Array.isArray(guardrailInfo) ? guardrailInfo : guardrailInfo ? [guardrailInfo] : [];
|
||||||
|
const hasGuardrailData = guardrailEntries.length > 0;
|
||||||
|
|
||||||
|
// Calculate total masked entities if guardrail data exists
|
||||||
|
const totalMaskedEntities = guardrailEntries.reduce((sum, entry) => {
|
||||||
|
const maskedCounts = entry?.masked_entity_count;
|
||||||
|
if (!maskedCounts) {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
sum +
|
||||||
|
Object.values(maskedCounts).reduce<number>((acc, count) => (typeof count === "number" ? acc + count : acc), 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const primaryGuardrailLabel =
|
||||||
|
guardrailEntries.length === 1
|
||||||
|
? guardrailEntries[0]?.guardrail_name ?? "-"
|
||||||
|
: guardrailEntries.length > 1
|
||||||
|
? `${guardrailEntries.length} guardrails`
|
||||||
|
: "-";
|
||||||
|
|
||||||
|
const handleCopyRequestId = () => {
|
||||||
|
navigator.clipboard.writeText(logEntry.request_id);
|
||||||
|
message.success("Request ID copied to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string, label: string) => {
|
||||||
|
try {
|
||||||
|
// Try modern clipboard API first
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
message.success(`${label} copied to clipboard`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Fallback for non-secure contexts
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = "fixed";
|
||||||
|
textArea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
if (!successful) {
|
||||||
|
throw new Error("execCommand failed");
|
||||||
|
}
|
||||||
|
message.success(`${label} copied to clipboard`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Copy failed:", error);
|
||||||
|
message.error(`Failed to copy ${label}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={null}
|
||||||
|
placement="right"
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
width="60%"
|
||||||
|
closable={false}
|
||||||
|
mask={true}
|
||||||
|
maskClosable={true}
|
||||||
|
styles={{
|
||||||
|
body: { padding: 0, overflow: "hidden" },
|
||||||
|
header: { display: "none" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Custom Header with Request ID prominently displayed */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "16px 24px",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
position: "sticky",
|
||||||
|
top: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Request ID at top - like Langfuse trace ID */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px", flex: 1, minWidth: 0 }}>
|
||||||
|
<Tooltip title={logEntry.request_id}>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: "16px",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logEntry.request_id}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Copy Request ID">
|
||||||
|
<Button type="text" size="small" icon={<CopyOutlined />} onClick={handleCopyRequestId} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status and timestamp row */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||||
|
<Tag color={metadata.status === "failure" ? "error" : "success"}>
|
||||||
|
{metadata.status === "failure" ? "Failure" : "Success"}
|
||||||
|
</Tag>
|
||||||
|
<Text type="secondary" style={{ fontSize: "13px" }}>
|
||||||
|
{logEntry.startTime}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable content area */}
|
||||||
|
<div style={{ height: "calc(100vh - 100px)", overflowY: "auto", padding: "24px" }}>
|
||||||
|
{/* Request Details Section */}
|
||||||
|
<Card title="Request Details" size="small" style={{ marginBottom: "16px" }}>
|
||||||
|
<Descriptions column={2} size="small">
|
||||||
|
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Model ID">{logEntry.model_id}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="API Base">
|
||||||
|
<Tooltip title={logEntry.api_base || "-"}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
maxWidth: "200px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
display: "inline-block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logEntry.api_base || "-"}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Descriptions.Item>
|
||||||
|
{logEntry.requester_ip_address && (
|
||||||
|
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{hasGuardrailData && (
|
||||||
|
<Descriptions.Item label="Guardrail">
|
||||||
|
<span>{primaryGuardrailLabel}</span>
|
||||||
|
{totalMaskedEntities > 0 && (
|
||||||
|
<Tag color="blue" style={{ marginLeft: "8px" }}>
|
||||||
|
{totalMaskedEntities} masked
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Metrics Section */}
|
||||||
|
<Card title="Metrics" size="small" style={{ marginBottom: "16px" }}>
|
||||||
|
<Descriptions column={2} size="small">
|
||||||
|
<Descriptions.Item label="Tokens">
|
||||||
|
{logEntry.total_tokens} ({logEntry.prompt_tokens} prompt + {logEntry.completion_tokens} completion)
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 6)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Duration">{logEntry.duration} s</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Cache Hit">{logEntry.cache_hit}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Cache Read Tokens">
|
||||||
|
{formatNumberWithCommas(metadata?.additional_usage_values?.cache_read_input_tokens || 0)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Cache Creation Tokens">
|
||||||
|
{formatNumberWithCommas(metadata?.additional_usage_values?.cache_creation_input_tokens || 0)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Start Time">{logEntry.startTime}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="End Time">{logEntry.endTime}</Descriptions.Item>
|
||||||
|
{metadata?.litellm_overhead_time_ms !== undefined && (
|
||||||
|
<Descriptions.Item label="LiteLLM Overhead">{metadata.litellm_overhead_time_ms} ms</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Cost Breakdown - Show if cost breakdown data is available */}
|
||||||
|
<CostBreakdownViewer costBreakdown={metadata?.cost_breakdown} totalSpend={logEntry.spend || 0} />
|
||||||
|
|
||||||
|
{/* Configuration Info Message - Show when data is missing */}
|
||||||
|
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
|
||||||
|
|
||||||
|
{/* Request/Response JSON - Using Tabs */}
|
||||||
|
<Card title="Request & Response" size="small" style={{ marginBottom: "16px" }}>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="request"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "request",
|
||||||
|
label: "Request",
|
||||||
|
children: (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
style={{ position: "absolute", top: "8px", right: "8px", zIndex: 1 }}
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(getRawRequest(), null, 2), "Request")}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: "500px",
|
||||||
|
overflowY: "auto",
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
|
||||||
|
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "response",
|
||||||
|
label: "Response",
|
||||||
|
children: (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
style={{ position: "absolute", top: "8px", right: "8px", zIndex: 1 }}
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(formattedResponse(), null, 2), "Response")}
|
||||||
|
disabled={!hasResponse}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: "500px",
|
||||||
|
overflowY: "auto",
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasResponse ? (
|
||||||
|
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
|
||||||
|
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: "center", padding: "20px", color: "#999", fontStyle: "italic" }}>
|
||||||
|
Response data not available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Guardrail Data - Show only if present */}
|
||||||
|
{hasGuardrailData && (
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<GuardrailViewer data={guardrailInfo} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vector Store Request Data - Show only if present */}
|
||||||
|
{hasVectorStoreData && (
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<VectorStoreViewer data={metadata.vector_store_request_metadata} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Card - Only show for failures */}
|
||||||
|
{hasError && errorInfo && (
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<ErrorViewer errorInfo={errorInfo} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tags Card - Only show if there are tags */}
|
||||||
|
{logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && (
|
||||||
|
<Card title="Request Tags" size="small" style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
||||||
|
{Object.entries(logEntry.request_tags).map(([key, value]) => (
|
||||||
|
<Tag key={key}>
|
||||||
|
{key}: {String(value)}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata Card - Only show if there's metadata */}
|
||||||
|
{logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && (
|
||||||
|
<Card
|
||||||
|
title="Metadata"
|
||||||
|
size="small"
|
||||||
|
style={{ marginBottom: "16px" }}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(logEntry.metadata, null, 2), "Metadata")}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
maxHeight: "300px",
|
||||||
|
overflowY: "auto",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(logEntry.metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,46 +49,6 @@ export type LogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const columns: ColumnDef<LogEntry>[] = [
|
export const columns: ColumnDef<LogEntry>[] = [
|
||||||
{
|
|
||||||
id: "expander",
|
|
||||||
header: () => null,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
// Convert the cell function to a React component to properly use hooks
|
|
||||||
const ExpanderCell = () => {
|
|
||||||
const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded());
|
|
||||||
|
|
||||||
// Memoize the toggle handler to prevent unnecessary re-renders
|
|
||||||
const toggleHandler = React.useCallback(() => {
|
|
||||||
setLocalExpanded((prev) => !prev);
|
|
||||||
row.getToggleExpandedHandler()();
|
|
||||||
}, [row]);
|
|
||||||
|
|
||||||
return row.getCanExpand() ? (
|
|
||||||
<button
|
|
||||||
onClick={toggleHandler}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
aria-label={localExpanded ? "Collapse row" : "Expand row"}
|
|
||||||
className="w-6 h-6 flex items-center justify-center focus:outline-none"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className={`w-4 h-4 transform transition-transform duration-75 ${localExpanded ? "rotate-90" : ""}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<span className="w-6 h-6 flex items-center justify-center">●</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return the component
|
|
||||||
return <ExpanderCell />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: "Time",
|
header: "Time",
|
||||||
accessorKey: "startTime",
|
accessorKey: "startTime",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsMo
|
|||||||
import { DataTable } from "./table";
|
import { DataTable } from "./table";
|
||||||
import { VectorStoreViewer } from "./VectorStoreViewer";
|
import { VectorStoreViewer } from "./VectorStoreViewer";
|
||||||
import NewBadge from "../common_components/NewBadge";
|
import NewBadge from "../common_components/NewBadge";
|
||||||
|
import { LogDetailsDrawer } from "./LogDetailsDrawer";
|
||||||
|
|
||||||
interface SpendLogsTableProps {
|
interface SpendLogsTableProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
@@ -89,7 +90,8 @@ export default function SpendLogsTable({
|
|||||||
const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole));
|
const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole));
|
||||||
const [activeTab, setActiveTab] = useState("request logs");
|
const [activeTab, setActiveTab] = useState("request logs");
|
||||||
|
|
||||||
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null);
|
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
|
||||||
const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false);
|
const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false);
|
||||||
|
|
||||||
@@ -317,17 +319,6 @@ export default function SpendLogsTable({
|
|||||||
enabled: !!accessToken && !!selectedSessionId,
|
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) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
return null;
|
return null;
|
||||||
@@ -367,8 +358,14 @@ export default function SpendLogsTable({
|
|||||||
logs.refetch();
|
logs.refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowExpand = (requestId: string | null) => {
|
const handleRowClick = (log: LogEntry) => {
|
||||||
setExpandedRequestId(requestId);
|
setSelectedLog(log);
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDrawer = () => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
// Optionally keep selectedLog for animation purposes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to extract unique error codes from logs
|
// Function to extract unique error codes from logs
|
||||||
@@ -554,9 +551,7 @@ export default function SpendLogsTable({
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={sessionData}
|
data={sessionData}
|
||||||
renderSubComponent={({ row }) => <RequestViewer row={row} onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} />}
|
onRowClick={handleRowClick}
|
||||||
getRowCanExpand={() => true}
|
|
||||||
// Optionally: add session-specific row expansion state
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -753,8 +748,7 @@ export default function SpendLogsTable({
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
renderSubComponent={({ row }) => <RequestViewer row={row} onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} />}
|
onRowClick={handleRowClick}
|
||||||
getRowCanExpand={() => true}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -775,6 +769,14 @@ export default function SpendLogsTable({
|
|||||||
<TabPanel><DeletedTeamsPage /></TabPanel>
|
<TabPanel><DeletedTeamsPage /></TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|
||||||
|
{/* Log Details Drawer */}
|
||||||
|
<LogDetailsDrawer
|
||||||
|
open={isDrawerOpen}
|
||||||
|
onClose={handleCloseDrawer}
|
||||||
|
logEntry={selectedLog}
|
||||||
|
onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } fro
|
|||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
renderSubComponent: (props: { row: Row<TData> }) => React.ReactElement;
|
onRowClick?: (row: TData) => void;
|
||||||
getRowCanExpand: (row: Row<TData>) => boolean;
|
// Legacy props for backward compatibility (audit logs)
|
||||||
|
renderSubComponent?: (props: { row: Row<TData> }) => React.ReactElement;
|
||||||
|
getRowCanExpand?: (row: Row<TData>) => boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
loadingMessage?: string;
|
loadingMessage?: string;
|
||||||
noDataMessage?: string;
|
noDataMessage?: string;
|
||||||
@@ -16,22 +18,26 @@ interface DataTableProps<TData, TValue> {
|
|||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
data = [],
|
data = [],
|
||||||
columns,
|
columns,
|
||||||
getRowCanExpand,
|
onRowClick,
|
||||||
renderSubComponent,
|
renderSubComponent,
|
||||||
|
getRowCanExpand,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
loadingMessage = "🚅 Loading logs...",
|
loadingMessage = "🚅 Loading logs...",
|
||||||
noDataMessage = "No logs found",
|
noDataMessage = "No logs found",
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
// Determine if we're in legacy expansion mode or new drawer mode
|
||||||
|
const isLegacyMode = !!renderSubComponent && !!getRowCanExpand;
|
||||||
|
|
||||||
const table = useReactTable<TData>({
|
const table = useReactTable<TData>({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
getRowCanExpand,
|
...(isLegacyMode && { getRowCanExpand }),
|
||||||
getRowId: (row: TData, index: number) => {
|
getRowId: (row: TData, index: number) => {
|
||||||
const _row: any = row as any;
|
const _row: any = row as any;
|
||||||
return _row?.request_id ?? String(index);
|
return _row?.request_id ?? String(index);
|
||||||
},
|
},
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getExpandedRowModel: getExpandedRowModel(),
|
...(isLegacyMode && { getExpandedRowModel: getExpandedRowModel() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,7 +68,10 @@ export function DataTable<TData, TValue>({
|
|||||||
) : table.getRowModel().rows.length > 0 ? (
|
) : table.getRowModel().rows.length > 0 ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<Fragment key={row.id}>
|
<Fragment key={row.id}>
|
||||||
<TableRow className="h-8">
|
<TableRow
|
||||||
|
className={`h-8 ${!isLegacyMode ? "cursor-pointer hover:bg-gray-50" : ""}`}
|
||||||
|
onClick={() => !isLegacyMode && onRowClick?.(row.original)}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id} className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
<TableCell key={cell.id} className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
@@ -70,7 +79,8 @@ export function DataTable<TData, TValue>({
|
|||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{row.getIsExpanded() && (
|
{/* Legacy expansion mode for audit logs */}
|
||||||
|
{isLegacyMode && row.getIsExpanded() && renderSubComponent && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
||||||
<div className="w-full max-w-full overflow-hidden box-border">{renderSubComponent({ row })}</div>
|
<div className="w-full max-w-full overflow-hidden box-border">{renderSubComponent({ row })}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user