From e171adbf3ce7cd4d2a9582d9604177230f6e7edd Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 11 Feb 2026 19:23:45 -0800 Subject: [PATCH] ui fixes --- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 729 +++++++----------- 1 file changed, 287 insertions(+), 442 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index a3f948296eb..4c64e52b55e 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -1,52 +1,92 @@ -import { useState } from "react"; -import { Drawer, Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space } from "antd"; -import moment from "moment"; +import { useEffect, useMemo, useState } from "react"; +import { Button, Drawer } from "antd"; +import { + CheckOutlined, + CopyOutlined, + LeftOutlined, + RightOutlined, +} from "@ant-design/icons"; +import { Sparkles, Wrench } from "lucide-react"; import { LogEntry } from "../columns"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import GuardrailViewer from "../GuardrailViewer/GuardrailViewer"; -import { CostBreakdownViewer } from "../CostBreakdownViewer"; -import { ConfigInfoMessage } from "../ConfigInfoMessage"; -import { VectorStoreViewer } from "../VectorStoreViewer"; -import { TruncatedValue } from "./TruncatedValue"; -import { TokenFlow } from "./TokenFlow"; -import { JsonViewer } from "./JsonViewer"; +import { MCP_CALL_TYPES } from "../constants"; +import { getEventDisplayName } from "../utils"; import { DrawerHeader } from "./DrawerHeader"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; -import { - formatData, - checkHasMessages, - checkHasResponse, - normalizeGuardrailEntries, - calculateTotalMaskedEntities, - getGuardrailLabel, - checkHasVectorStoreData, -} from "./utils"; -import { - DRAWER_WIDTH, - DRAWER_CONTENT_PADDING, - API_BASE_MAX_WIDTH, - METADATA_MAX_HEIGHT, - TAB_REQUEST, - TAB_RESPONSE, - FONT_SIZE_SMALL, - FONT_FAMILY_MONO, - SPACING_XLARGE, - SPACING_MEDIUM, -} from "./constants"; -import { ToolsSection } from "../ToolsSection"; -import { PrettyMessagesView } from "./PrettyMessagesView"; - -const { Text } = Typography; +import { LogDetailContent } from "./LogDetailContent"; +import { sessionSpendLogsCall } from "../../networking"; +import { useQuery } from "@tanstack/react-query"; +import { getSpendString } from "@/utils/dataUtils"; +import { DRAWER_WIDTH } from "./constants"; export interface LogDetailsDrawerProps { open: boolean; onClose: () => void; logEntry: LogEntry | null; + sessionId?: string | null; + accessToken?: string | null; onOpenSettings?: () => void; allLogs?: LogEntry[]; onSelectLog?: (log: LogEntry) => void; } +const SIDEBAR_WIDTH_PX = 224; + +/* ------------------------------------------------------------------ */ +/* TraceEventRow — compact event row used in both session & non- */ +/* session sidebar lists. Extracted to avoid JSX duplication. */ +/* ------------------------------------------------------------------ */ +interface TraceEventRowProps { + row: LogEntry; + isSelected: boolean; + onClick: () => void; +} + +function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) { + const isMcp = MCP_CALL_TYPES.includes(row.call_type); + const durationValue = + row.duration != null + ? row.duration.toFixed(3) + : row.startTime && row.endTime + ? ((Date.parse(row.endTime) - Date.parse(row.startTime)) / 1000).toFixed(3) + : "-"; + + return ( + + ); +} + /** * Right-side drawer panel for displaying detailed log information. * Features: @@ -61,64 +101,112 @@ export function LogDetailsDrawer({ open, onClose, logEntry, + sessionId, + accessToken, onOpenSettings, allLogs = [], onSelectLog, }: LogDetailsDrawerProps) { - const [activeTab, setActiveTab] = useState(TAB_REQUEST); + const isSessionMode = Boolean(sessionId); + const [selectedSessionRequestId, setSelectedSessionRequestId] = useState(null); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const [copiedLeftPanelId, setCopiedLeftPanelId] = useState(false); + + const { data: sessionLogs = [] } = useQuery({ + queryKey: ["sessionLogs", sessionId], + queryFn: async () => { + if (!sessionId || !accessToken) return []; + const response = await sessionSpendLogsCall(accessToken, sessionId); + const allSessionLogs: LogEntry[] = response.data || response || []; + return allSessionLogs + .map((row) => ({ + ...row, + duration: (Date.parse(row.endTime) - Date.parse(row.startTime)) / 1000, + })) + .sort((a, b) => { + const aIsMcp = MCP_CALL_TYPES.includes(a.call_type) ? 1 : 0; + const bIsMcp = MCP_CALL_TYPES.includes(b.call_type) ? 1 : 0; + if (aIsMcp !== bIsMcp) return aIsMcp - bIsMcp; + return new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + }); + }, + enabled: Boolean(open && isSessionMode && sessionId && accessToken), + }); + + const currentLog = useMemo(() => { + if (!isSessionMode) return logEntry; + if (!sessionLogs.length) return null; + if (selectedSessionRequestId) { + return sessionLogs.find((row) => row.request_id === selectedSessionRequestId) || sessionLogs[0]; + } + if (logEntry?.request_id) { + const clickedLog = sessionLogs.find((row) => row.request_id === logEntry.request_id); + return clickedLog || sessionLogs[0]; + } + return sessionLogs[0]; + }, [isSessionMode, logEntry, selectedSessionRequestId, sessionLogs]); + + useEffect(() => { + if (!isSessionMode || !sessionLogs.length) return; + if (!selectedSessionRequestId || !sessionLogs.some((row) => row.request_id === selectedSessionRequestId)) { + const fallbackRequestId = logEntry?.request_id && sessionLogs.some((row) => row.request_id === logEntry.request_id) + ? logEntry.request_id + : sessionLogs[0].request_id; + setSelectedSessionRequestId(fallbackRequestId); + } + }, [isSessionMode, logEntry, selectedSessionRequestId, sessionLogs]); + + // Reset transient UI state when the drawer opens or closes. + useEffect(() => { + if (open) { + setIsSidebarCollapsed(false); + } else { + if (isSessionMode) setSelectedSessionRequestId(null); + setCopiedLeftPanelId(false); + } + }, [open, isSessionMode]); // Keyboard navigation const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({ isOpen: open, - currentLog: logEntry, - allLogs, + currentLog, + allLogs: isSessionMode ? sessionLogs : allLogs, onClose, - onSelectLog, + onSelectLog: (selected) => { + if (isSessionMode) { + setSelectedSessionRequestId(selected.request_id); + } + onSelectLog?.(selected); + }, }); - if (!logEntry) return null; - - const metadata = logEntry.metadata || {}; - const hasError = metadata.status === "failure"; - const errorInfo = hasError ? metadata.error_information : null; - - // Check if request/response data is present - const hasMessages = checkHasMessages(logEntry.messages); - const hasResponse = checkHasResponse(logEntry.response); - const missingData = !hasMessages && !hasResponse && !hasError; - - // Guardrail data - const guardrailInfo = metadata?.guardrail_information; - const guardrailEntries = normalizeGuardrailEntries(guardrailInfo); - const hasGuardrailData = guardrailEntries.length > 0; - const totalMaskedEntities = calculateTotalMaskedEntities(guardrailEntries); - const primaryGuardrailLabel = getGuardrailLabel(guardrailEntries); - - // Vector store data - const hasVectorStoreData = checkHasVectorStoreData(metadata); + const metadata = currentLog?.metadata || {}; // Status display values const statusLabel = metadata.status === "failure" ? "Failure" : "Success"; const statusColor = metadata.status === "failure" ? ("error" as const) : ("success" as const); const environment = metadata?.user_api_key_team_alias || "default"; - const getRawRequest = () => { - return formatData(logEntry.proxy_server_request || logEntry.messages); + const totalSessionCost = sessionLogs.reduce((sum, row) => sum + (row.spend || 0), 0); + const sessionStart = sessionLogs.length > 0 ? new Date(sessionLogs[0].startTime) : null; + const sessionEnd = sessionLogs.length > 0 ? new Date(sessionLogs[sessionLogs.length - 1].endTime) : null; + const sessionDurationSeconds = + sessionStart && sessionEnd ? ((sessionEnd.getTime() - sessionStart.getTime()) / 1000).toFixed(2) : "0.00"; + const llmCount = sessionLogs.filter((row) => !MCP_CALL_TYPES.includes(row.call_type)).length; + const mcpCount = sessionLogs.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length; + const logsForList = isSessionMode ? sessionLogs : currentLog ? [currentLog] : []; + const leftPanelId = isSessionMode ? sessionId || "" : currentLog?.request_id || ""; + const leftPanelDisplayId = + leftPanelId.length > 14 ? `${leftPanelId.slice(0, 11)}...` : leftPanelId; + + const handleCopyLeftPanelId = async () => { + if (!leftPanelId) return; + await navigator.clipboard.writeText(leftPanelId); + setCopiedLeftPanelId(true); + setTimeout(() => setCopiedLeftPanelId(false), 1200); }; - const getFormattedResponse = () => { - 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); - }; + if (!currentLog) return null; return ( - +
+ {!isSidebarCollapsed ? ( + +
+ + +
+ {logsForList.length} req + · + {isSessionMode + ? `${llmCount} LLM` + : `${logsForList.filter((row) => !MCP_CALL_TYPES.includes(row.call_type)).length} LLM`} + · + {isSessionMode + ? `${mcpCount} MCP` + : `${logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length} MCP`} + · + {isSessionMode + ? getSpendString(totalSessionCost) + : getSpendString(currentLog.spend || 0)} + {isSessionMode && ( + <> + · + {sessionDurationSeconds}s + + )} +
+ -
- {/* Error Alert - Show prominently at top for failures */} - {hasError && errorInfo && ( - } - className="mb-6" - /> - )} - - {/* Tags - Only show if present */} - {logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && ( - - )} - - {/* Request Details Section */} -
- - - {logEntry.model} - {logEntry.custom_llm_provider || "-"} - {logEntry.call_type} - - - - - - - {logEntry.requester_ip_address && ( - {logEntry.requester_ip_address} - )} - {hasGuardrailData && ( - - - - )} - - -
- - {/* Metrics Section */} - - - {/* Cost Breakdown - Show if cost breakdown data is available */} - - - {/* Tools Section - Show if tools are present in request */} - - - {/* Configuration Info Message - Show when data is missing */} - {missingData && ( -
- +
+ {isSessionMode ? ( +
+ {/* Child events — vertical tree line with horizontal connectors */} +
+
+ {logsForList.map((row, idx) => { + const isLast = idx === logsForList.length - 1; + return ( +
+
+ {isLast &&
} + { + setSelectedSessionRequestId(row.request_id); + onSelectLog?.(row); + }} + /> +
+ ); + })} +
+
+ ) : ( +
+ {logsForList.map((row) => ( + onSelectLog?.(row)} + /> + ))} +
+ )} +
- )} + )} - {/* Request/Response JSON - Collapsible */} - - - {/* Guardrail Data - Show only if present */} - {hasGuardrailData && } - - {/* Vector Store Request Data - Show only if present */} - {hasVectorStoreData && } - - {/* Metadata Card - Only show if there's metadata */} - {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( - - )} - - {/* Bottom spacing for scroll area */} -
-
+
+ +
+ +
+
+
); } - -// ============================================================================ -// Helper Components -// ============================================================================ - -function ErrorDescription({ errorInfo }: { errorInfo: any }) { - return ( -
- {errorInfo.error_code && ( -
- Error Code: {errorInfo.error_code} -
- )} - {errorInfo.error_message && ( -
- Message: {errorInfo.error_message} -
- )} -
- ); -} - -function TagsSection({ tags }: { tags: Record }) { - return ( -
- - Tags - - - {Object.entries(tags).map(([key, value]) => ( - - {key}: {String(value)} - - ))} - -
- ); -} - -function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) { - return ( - - {label} - {maskedCount > 0 && ( - - {maskedCount} masked - - )} - - ); -} - -function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record }) { - const hasCacheActivity = - logEntry.cache_hit || - (metadata?.additional_usage_values?.cache_read_input_tokens && - metadata.additional_usage_values.cache_read_input_tokens > 0); - - return ( -
- - - - - - ${formatNumberWithCommas(logEntry.spend || 0, 8)} - {logEntry.duration?.toFixed(3)} s - - {/* Only show cache fields if there's cache activity */} - {hasCacheActivity && ( - <> - - {logEntry.cache_hit || "None"} - - {metadata?.additional_usage_values?.cache_read_input_tokens > 0 && ( - - {formatNumberWithCommas(metadata.additional_usage_values.cache_read_input_tokens)} - - )} - {metadata?.additional_usage_values?.cache_creation_input_tokens > 0 && ( - - {formatNumberWithCommas(metadata.additional_usage_values.cache_creation_input_tokens)} - - )} - - )} - - {metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && ( - - {metadata.litellm_overhead_time_ms.toFixed(2)} ms - - )} - - - {moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} - - - {moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} - - - -
- ); -} - -interface RequestResponseSectionProps { - hasResponse: boolean; - hasError: boolean; - getRawRequest: () => any; - getFormattedResponse: () => any; - logEntry: LogEntry; -} - -function RequestResponseSection({ - hasResponse, - hasError, - getRawRequest, - getFormattedResponse, - logEntry, -}: RequestResponseSectionProps) { - const [activeTab, setActiveTab] = useState(TAB_REQUEST); - const [viewMode, setViewMode] = useState<'pretty' | 'json'>('pretty'); - - const getCopyText = () => { - const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); - return JSON.stringify(data, null, 2); - }; - - // Calculate input and output costs - // Assume average cost if not explicitly provided - const totalSpend = logEntry.spend || 0; - const promptTokens = logEntry.prompt_tokens || 0; - const completionTokens = logEntry.completion_tokens || 0; - const totalTokens = promptTokens + completionTokens; - - // Estimate input/output costs proportionally if not available - const inputCost = totalTokens > 0 ? (totalSpend * promptTokens) / totalTokens : 0; - const outputCost = totalTokens > 0 ? (totalSpend * completionTokens) / totalTokens : 0; - - return ( -
- { - // Only prevent if clicking on the Radio.Group area - const target = e.target as HTMLElement; - if (target.closest('.ant-radio-group')) { - e.stopPropagation(); - } - }} - > -

Request & Response

- {/* View Mode Toggle - In the header */} - setViewMode(e.target.value)} - > - Pretty - JSON - -
- ), - children: ( -
- {viewMode === 'pretty' ? ( - - ) : ( - setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ - - } - items={[ - { - key: TAB_REQUEST, - label: "Request", - children: ( -
- -
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse || hasError ? ( - - ) : ( -
- Response data not available -
- )} -
- ), - }, - ]} - /> - )} -
- ), - }, - ]} - /> -
- ); -} - -function MetadataSection({ metadata }: { metadata: Record }) { - return ( -
- Metadata, - children: ( -
-
- -
-
-                  {JSON.stringify(metadata, null, 2)}
-                
-
- ), - }, - ]} - /> -
- ); -} -