mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 07:05:00 +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>[] = [
|
||||
{
|
||||
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",
|
||||
accessorKey: "startTime",
|
||||
|
||||
@@ -31,6 +31,7 @@ import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsMo
|
||||
import { DataTable } from "./table";
|
||||
import { VectorStoreViewer } from "./VectorStoreViewer";
|
||||
import NewBadge from "../common_components/NewBadge";
|
||||
import { LogDetailsDrawer } from "./LogDetailsDrawer";
|
||||
|
||||
interface SpendLogsTableProps {
|
||||
accessToken: string | null;
|
||||
@@ -89,7 +90,8 @@ export default function SpendLogsTable({
|
||||
const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole));
|
||||
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 [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false);
|
||||
|
||||
@@ -317,17 +319,6 @@ export default function SpendLogsTable({
|
||||
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) {
|
||||
return null;
|
||||
@@ -367,8 +358,14 @@ export default function SpendLogsTable({
|
||||
logs.refetch();
|
||||
};
|
||||
|
||||
const handleRowExpand = (requestId: string | null) => {
|
||||
setExpandedRequestId(requestId);
|
||||
const handleRowClick = (log: LogEntry) => {
|
||||
setSelectedLog(log);
|
||||
setIsDrawerOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDrawer = () => {
|
||||
setIsDrawerOpen(false);
|
||||
// Optionally keep selectedLog for animation purposes
|
||||
};
|
||||
|
||||
// Function to extract unique error codes from logs
|
||||
@@ -554,9 +551,7 @@ export default function SpendLogsTable({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={sessionData}
|
||||
renderSubComponent={({ row }) => <RequestViewer row={row} onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} />}
|
||||
getRowCanExpand={() => true}
|
||||
// Optionally: add session-specific row expansion state
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -753,8 +748,7 @@ export default function SpendLogsTable({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
renderSubComponent={({ row }) => <RequestViewer row={row} onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} />}
|
||||
getRowCanExpand={() => true}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -775,6 +769,14 @@ export default function SpendLogsTable({
|
||||
<TabPanel><DeletedTeamsPage /></TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
{/* Log Details Drawer */}
|
||||
<LogDetailsDrawer
|
||||
open={isDrawerOpen}
|
||||
onClose={handleCloseDrawer}
|
||||
logEntry={selectedLog}
|
||||
onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } fro
|
||||
interface DataTableProps<TData, TValue> {
|
||||
data: TData[];
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
renderSubComponent: (props: { row: Row<TData> }) => React.ReactElement;
|
||||
getRowCanExpand: (row: Row<TData>) => boolean;
|
||||
onRowClick?: (row: TData) => void;
|
||||
// Legacy props for backward compatibility (audit logs)
|
||||
renderSubComponent?: (props: { row: Row<TData> }) => React.ReactElement;
|
||||
getRowCanExpand?: (row: Row<TData>) => boolean;
|
||||
isLoading?: boolean;
|
||||
loadingMessage?: string;
|
||||
noDataMessage?: string;
|
||||
@@ -16,22 +18,26 @@ interface DataTableProps<TData, TValue> {
|
||||
export function DataTable<TData, TValue>({
|
||||
data = [],
|
||||
columns,
|
||||
getRowCanExpand,
|
||||
onRowClick,
|
||||
renderSubComponent,
|
||||
getRowCanExpand,
|
||||
isLoading = false,
|
||||
loadingMessage = "🚅 Loading logs...",
|
||||
noDataMessage = "No logs found",
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
// Determine if we're in legacy expansion mode or new drawer mode
|
||||
const isLegacyMode = !!renderSubComponent && !!getRowCanExpand;
|
||||
|
||||
const table = useReactTable<TData>({
|
||||
data,
|
||||
columns,
|
||||
getRowCanExpand,
|
||||
...(isLegacyMode && { getRowCanExpand }),
|
||||
getRowId: (row: TData, index: number) => {
|
||||
const _row: any = row as any;
|
||||
return _row?.request_id ?? String(index);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
...(isLegacyMode && { getExpandedRowModel: getExpandedRowModel() }),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -62,7 +68,10 @@ export function DataTable<TData, TValue>({
|
||||
) : table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<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) => (
|
||||
<TableCell key={cell.id} className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
@@ -70,7 +79,8 @@ export function DataTable<TData, TValue>({
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{row.getIsExpanded() && (
|
||||
{/* Legacy expansion mode for audit logs */}
|
||||
{isLegacyMode && row.getIsExpanded() && renderSubComponent && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
||||
<div className="w-full max-w-full overflow-hidden box-border">{renderSubComponent({ row })}</div>
|
||||
|
||||
Reference in New Issue
Block a user