diff --git a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx index 7a02b89891b..affe28e0b25 100644 --- a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx @@ -59,7 +59,7 @@ export const CostBreakdownViewer: React.FC = ({ } return ( -
+
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx deleted file mode 100644 index 6aa6410b302..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { Button, Tag, Tooltip, Typography } from "antd"; -import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; -import moment from "moment"; -import { LogEntry } from "../columns"; -import { getProviderLogoAndName } from "../../provider_info_helpers"; -import { - DRAWER_HEADER_PADDING, - COLOR_BORDER, - COLOR_BACKGROUND, - SPACING_MEDIUM, - SPACING_LARGE, - FONT_SIZE_HEADER, - FONT_SIZE_MEDIUM, - FONT_FAMILY_MONO, - SPACING_SMALL, -} from "./constants"; - -const { Text } = Typography; - -interface DrawerHeaderProps { - log: LogEntry; - onClose: () => void; - onCopyRequestId: () => void; - onPrevious: () => void; - onNext: () => void; - statusLabel: string; - statusColor: "error" | "success"; - environment: string; -} - -/** - * Header component for the log details drawer. - * Displays model/provider, request ID, navigation controls, status, environment, and timestamp. - */ -export function DrawerHeader({ - log, - onClose, - onCopyRequestId, - onPrevious, - onNext, - statusLabel, - statusColor, - environment, -}: DrawerHeaderProps) { - const provider = log.custom_llm_provider || ""; - const providerInfo = provider ? getProviderLogoAndName(provider) : null; - - return ( -
- {/* Row 0: Model + Provider with Logo */} - - - {/* Row 1: Request ID + Actions */} -
- - -
- - {/* Row 2: Status + Env + Timestamp */} - -
- ); -} - -/** - * Model and Provider display with logo - */ -function ModelProviderSection({ - model, - providerLogo, - providerName, -}: { - model: string; - providerLogo?: string; - providerName?: string; -}) { - return ( -
- {providerLogo && ( - {providerName { - const target = e.target as HTMLImageElement; - target.style.display = "none"; - }} - /> - )} -
- - {model} - - {providerName && ( - - {providerName} - - )} -
-
- ); -} - -/** - * Request ID display with copy button - */ -function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () => void }) { - return ( -
- - - {requestId} - - - -
- ); -} - -/** - * Navigation controls (previous, next, close) - * Shows keyboard shortcuts styled as buttons for visibility - */ -function NavigationSection({ - onPrevious, - onNext, - onClose, -}: { - onPrevious: () => void; - onNext: () => void; - onClose: () => void; -}) { - const keyboardShortcutStyle: React.CSSProperties = { - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - minWidth: "20px", - height: "20px", - padding: "0 6px", - fontSize: 11, - fontWeight: 600, - fontFamily: "monospace", - marginLeft: 4, - background: "#fff", - border: "1px solid #d9d9d9", - borderRadius: 4, - boxShadow: "0 1px 2px rgba(0,0,0,0.05)", - }; - - return ( -
- - - -
- - -
- ); -} - -/** - * Status bar with tags and timestamp - */ -function StatusBar({ - log, - statusLabel, - statusColor, - environment, -}: { - log: LogEntry; - statusLabel: string; - statusColor: "error" | "success"; - environment: string; -}) { - return ( -
- {statusLabel} - Env: {environment} - - {moment(log.startTime).format("MMM D, YYYY h:mm:ss A")} - ({moment(log.startTime).fromNow()}) - -
- ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx deleted file mode 100644 index 6463abf89d4..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Typography } from "antd"; -import { JsonView, defaultStyles } from "react-json-view-lite"; -import "react-json-view-lite/dist/index.css"; -import { JSON_MAX_HEIGHT, COLOR_BG_LIGHT, SPACING_LARGE } from "./constants"; - -const { Text } = Typography; - -interface JsonViewerProps { - data: any; - mode: "formatted"; -} - -/** - * Displays JSON data in formatted tree view. - * Uses an interactive tree component for easy navigation. - */ -export function JsonViewer({ data }: JsonViewerProps) { - if (!data) return No data; - - return ( -
-
- -
-
- ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx deleted file mode 100644 index 733cccdf1c9..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import { useState } from "react"; -import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message } from "antd"; -import { CopyOutlined } from "@ant-design/icons"; -import { Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; -import moment from "moment"; -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 { DrawerHeader } from "./DrawerHeader"; -import { copyToClipboard } from "./clipboardUtils"; -import { useKeyboardNavigation } from "./useKeyboardNavigation"; -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, - MESSAGE_REQUEST_ID_COPIED, -} from "./constants"; - -const { Text } = Typography; - -export interface LogDetailsDrawerProps { - open: boolean; - onClose: () => void; - logEntry: LogEntry | null; - onOpenSettings?: () => void; - allLogs?: LogEntry[]; - onSelectLog?: (log: LogEntry) => void; -} - -/** - * Right-side drawer panel for displaying detailed log information. - * Features: - * - Request ID prominently displayed with copy functionality - * - Keyboard navigation (J/K for next/prev, Escape to close) - * - Formatted and JSON view toggle for request/response - * - Smart display of cache fields (hidden when zero) - * - Error alerts for failed requests - * - Collapsible sections for guardrails, vector store, metadata - */ -export function LogDetailsDrawer({ - open, - onClose, - logEntry, - onOpenSettings, - allLogs = [], - onSelectLog, -}: LogDetailsDrawerProps) { - const [activeTab, setActiveTab] = useState(TAB_REQUEST); - - // Keyboard navigation - const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({ - isOpen: open, - currentLog: logEntry, - allLogs, - onClose, - onSelectLog, - }); - - 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; - - // 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); - - // 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 handleCopyRequestId = () => { - navigator.clipboard.writeText(logEntry.request_id); - message.success(MESSAGE_REQUEST_ID_COPIED); - }; - - const getRawRequest = () => { - return formatData(logEntry.proxy_server_request || logEntry.messages); - }; - - 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); - }; - - return ( - - - -
- {/* 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 */} - - - {/* Configuration Info Message - Show when data is missing */} - {missingData && ( -
- -
- )} - - {/* Request/Response JSON - Collapsible */} - copyToClipboard(JSON.stringify(data, null, 2), label)} - getRawRequest={getRawRequest} - getFormattedResponse={getFormattedResponse} - /> - - {/* 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 && ( - copyToClipboard(data, "Metadata")} /> - )} - - {/* 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; - onCopy: (data: any, label: string) => void; - getRawRequest: () => any; - getFormattedResponse: () => any; -} - -function RequestResponseSection({ - hasResponse, - onCopy, - getRawRequest, - getFormattedResponse, -}: RequestResponseSectionProps) { - const [activeTab, setActiveTab] = useState(TAB_REQUEST); - - const handleCopy = () => { - const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); - const label = activeTab === TAB_REQUEST ? "Request" : "Response"; - onCopy(data, label); - }; - - return ( -
- - -

Request & Response

-
- -
- setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ - - } - items={[ - { - key: TAB_REQUEST, - label: "Request", - children: ( -
- -
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse ? ( - - ) : ( -
- Response data not available -
- )} -
- ), - }, - ]} - /> -
-
-
-
- ); -} - -function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { - return ( -
- } - onClick={() => onCopy(JSON.stringify(metadata, null, 2))} - > - Copy - - } - > -
-          {JSON.stringify(metadata, null, 2)}
-        
-
-
- ); -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -function formatData(input: any) { - if (typeof input === "string") { - try { - return JSON.parse(input); - } catch { - return input; - } - } - return input; -} - -function checkHasMessages(messages: any): boolean { - if (!messages) return false; - if (Array.isArray(messages)) return messages.length > 0; - if (typeof messages === "object") return Object.keys(messages).length > 0; - return false; -} - -function checkHasResponse(response: any): boolean { - if (!response) return false; - return Object.keys(formatData(response)).length > 0; -} - -function normalizeGuardrailEntries(guardrailInfo: any): any[] { - if (Array.isArray(guardrailInfo)) return guardrailInfo; - if (guardrailInfo) return [guardrailInfo]; - return []; -} - -function calculateTotalMaskedEntities(entries: any[]): number { - return entries.reduce((sum, entry) => { - const maskedCounts = entry?.masked_entity_count; - if (!maskedCounts) return sum; - return ( - sum + - Object.values(maskedCounts).reduce((acc, count) => (typeof count === "number" ? acc + count : acc), 0) - ); - }, 0); -} - -function getGuardrailLabel(entries: any[]): string { - if (entries.length === 0) return "-"; - if (entries.length === 1) return entries[0]?.guardrail_name ?? "-"; - return `${entries.length} guardrails`; -} - -function checkHasVectorStoreData(metadata: Record): boolean { - return ( - metadata.vector_store_request_metadata && - Array.isArray(metadata.vector_store_request_metadata) && - metadata.vector_store_request_metadata.length > 0 - ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx deleted file mode 100644 index 5eec3c0a5cb..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Typography } from "antd"; - -const { Text } = Typography; - -interface TokenFlowProps { - prompt?: number; - completion?: number; - total?: number; -} - -/** - * Displays token usage in LiteLLM format: "12 (9 prompt tokens + 3 completion tokens)" - * Shows total with breakdown of prompt and completion tokens. - */ -export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) { - return ( - - {total.toLocaleString()} ({prompt.toLocaleString()} prompt tokens + {completion.toLocaleString()} completion - tokens) - - ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx deleted file mode 100644 index b03895808d6..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Typography, Tooltip } from "antd"; -import { DEFAULT_MAX_WIDTH, FONT_FAMILY_MONO, FONT_SIZE_SMALL } from "./constants"; - -const { Text } = Typography; - -interface TruncatedValueProps { - value?: string; - maxWidth?: number; -} - -/** - * Displays a truncated value with tooltip and copy functionality. - * Useful for displaying long IDs, URLs, or other text that may overflow. - */ -export function TruncatedValue({ value, maxWidth = DEFAULT_MAX_WIDTH }: TruncatedValueProps) { - if (!value) return -; - - return ( - - - {value} - - - ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts deleted file mode 100644 index 6aae95bb72c..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { message } from "antd"; -import { MESSAGE_COPY_SUCCESS } from "./constants"; - -/** - * Copies text to clipboard with fallback for non-secure contexts. - * Shows success/error message to user. - * - * @param text - Text to copy to clipboard - * @param label - Label for the copied content (e.g., "Request", "Metadata") - * @returns Promise - true if copy succeeded, false otherwise - */ -export async function copyToClipboard(text: string, label: string): Promise { - try { - // Try modern clipboard API first - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - message.success(`${label} ${MESSAGE_COPY_SUCCESS}`); - return true; - } else { - // Fallback for non-secure contexts (like 0.0.0.0) - 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} ${MESSAGE_COPY_SUCCESS}`); - return true; - } - } catch (error) { - console.error("Copy failed:", error); - message.error(`Failed to copy ${label}`); - return false; - } -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts deleted file mode 100644 index 91f5ff8f118..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Drawer configuration constants -export const DRAWER_WIDTH = "60%"; -export const DRAWER_HEADER_PADDING = "16px 24px"; -export const DRAWER_CONTENT_PADDING = "24px"; - -// Truncation and display limits -export const DEFAULT_MAX_WIDTH = 180; -export const API_BASE_MAX_WIDTH = 200; -export const JSON_MAX_HEIGHT = 400; -export const METADATA_MAX_HEIGHT = 300; - -// Tab keys (kept for backwards compatibility if needed) -export const TAB_REQUEST = "request" as const; -export const TAB_RESPONSE = "response" as const; - -// Keyboard shortcuts -export const KEY_ESCAPE = "Escape"; -export const KEY_J_LOWER = "j"; -export const KEY_J_UPPER = "J"; -export const KEY_K_LOWER = "k"; -export const KEY_K_UPPER = "K"; - -// Typography -export const FONT_FAMILY_MONO = "monospace"; -export const FONT_SIZE_SMALL = 12; -export const FONT_SIZE_MEDIUM = 13; -export const FONT_SIZE_HEADER = 16; - -// Colors -export const COLOR_BORDER = "#f0f0f0"; -export const COLOR_BACKGROUND = "#fff"; -export const COLOR_SECONDARY = "#8c8c8c"; -export const COLOR_BG_LIGHT = "#fafafa"; - -// Spacing -export const SPACING_SMALL = 4; -export const SPACING_MEDIUM = 8; -export const SPACING_LARGE = 12; -export const SPACING_XLARGE = 16; -export const SPACING_XXLARGE = 24; - -// Messages -export const MESSAGE_COPY_SUCCESS = "copied to clipboard"; -export const MESSAGE_REQUEST_ID_COPIED = "Request ID copied to clipboard"; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts deleted file mode 100644 index e1fdd9d2d60..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LogDetailsDrawer } from "./LogDetailsDrawer"; -export type { LogDetailsDrawerProps } from "./LogDetailsDrawer"; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts deleted file mode 100644 index e4fa9bc6185..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect } from "react"; -import { LogEntry } from "../columns"; -import { KEY_ESCAPE, KEY_J_LOWER, KEY_J_UPPER, KEY_K_LOWER, KEY_K_UPPER } from "./constants"; - -interface UseKeyboardNavigationProps { - isOpen: boolean; - currentLog: LogEntry | null; - allLogs: LogEntry[]; - onClose: () => void; - onSelectLog?: (log: LogEntry) => void; -} - -/** - * Custom hook for keyboard navigation in the log details drawer. - * Handles J/K for next/previous and Escape for close. - * - * Keyboard shortcuts: - * - J: Navigate to next log - * - K: Navigate to previous log - * - Escape: Close drawer - */ -export function useKeyboardNavigation({ - isOpen, - currentLog, - allLogs, - onClose, - onSelectLog, -}: UseKeyboardNavigationProps) { - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't trigger if user is typing in an input - if (isUserTyping(e.target)) { - return; - } - - if (!isOpen) return; - - switch (e.key) { - case KEY_ESCAPE: - onClose(); - break; - case KEY_J_LOWER: - case KEY_J_UPPER: - selectNextLog(); - break; - case KEY_K_LOWER: - case KEY_K_UPPER: - selectPreviousLog(); - break; - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [isOpen, currentLog, allLogs]); - - const selectNextLog = () => { - if (!currentLog || !allLogs.length || !onSelectLog) return; - - const currentIndex = allLogs.findIndex((l) => l.request_id === currentLog.request_id); - if (currentIndex < allLogs.length - 1) { - onSelectLog(allLogs[currentIndex + 1]); - } - }; - - const selectPreviousLog = () => { - if (!currentLog || !allLogs.length || !onSelectLog) return; - - const currentIndex = allLogs.findIndex((l) => l.request_id === currentLog.request_id); - if (currentIndex > 0) { - onSelectLog(allLogs[currentIndex - 1]); - } - }; - - return { - selectNextLog, - selectPreviousLog, - }; -} - -/** - * Checks if the user is currently typing in an input field. - * Used to prevent keyboard shortcuts from interfering with text input. - */ -function isUserTyping(target: EventTarget | null): boolean { - return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; -} diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 3e72c8e13b8..2da1e83747b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -49,6 +49,46 @@ export type LogEntry = { }; export const columns: ColumnDef[] = [ + { + 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() ? ( + + ) : ( + + ); + }; + + // Return the component + return ; + }, + }, { header: "Time", accessorKey: "startTime", diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 3859a5e51fb..826fc7ccc02 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -31,7 +31,6 @@ 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; @@ -90,8 +89,7 @@ export default function SpendLogsTable({ const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole)); const [activeTab, setActiveTab] = useState("request logs"); - const [selectedLog, setSelectedLog] = useState(null); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [expandedRequestId, setExpandedRequestId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false); @@ -319,6 +317,17 @@ 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; @@ -358,18 +367,8 @@ export default function SpendLogsTable({ logs.refetch(); }; - const handleRowClick = (log: LogEntry) => { - setSelectedLog(log); - setIsDrawerOpen(true); - }; - - const handleCloseDrawer = () => { - setIsDrawerOpen(false); - // Optionally keep selectedLog for animation purposes - }; - - const handleSelectLog = (log: LogEntry) => { - setSelectedLog(log); + const handleRowExpand = (requestId: string | null) => { + setExpandedRequestId(requestId); }; // Function to extract unique error codes from logs @@ -555,7 +554,9 @@ export default function SpendLogsTable({ setIsSpendLogsSettingsModalVisible(true)} />} + getRowCanExpand={() => true} + // Optionally: add session-specific row expansion state />
) : ( @@ -752,7 +753,8 @@ export default function SpendLogsTable({ setIsSpendLogsSettingsModalVisible(true)} />} + getRowCanExpand={() => true} />
@@ -773,16 +775,6 @@ export default function SpendLogsTable({ - - {/* Log Details Drawer */} - setIsSpendLogsSettingsModalVisible(true)} - allLogs={filteredData} - onSelectLog={handleSelectLog} - />
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index fb7706cba19..605341cb2ed 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -6,10 +6,8 @@ import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } fro interface DataTableProps { data: TData[]; columns: ColumnDef[]; - onRowClick?: (row: TData) => void; - // Legacy props for backward compatibility (audit logs) - renderSubComponent?: (props: { row: Row }) => React.ReactElement; - getRowCanExpand?: (row: Row) => boolean; + renderSubComponent: (props: { row: Row }) => React.ReactElement; + getRowCanExpand: (row: Row) => boolean; isLoading?: boolean; loadingMessage?: string; noDataMessage?: string; @@ -18,26 +16,22 @@ interface DataTableProps { export function DataTable({ data = [], columns, - onRowClick, - renderSubComponent, getRowCanExpand, + renderSubComponent, isLoading = false, loadingMessage = "🚅 Loading logs...", noDataMessage = "No logs found", }: DataTableProps) { - // Determine if we're in legacy expansion mode or new drawer mode - const isLegacyMode = !!renderSubComponent && !!getRowCanExpand; - const table = useReactTable({ data, columns, - ...(isLegacyMode && { getRowCanExpand }), + getRowCanExpand, getRowId: (row: TData, index: number) => { const _row: any = row as any; return _row?.request_id ?? String(index); }, getCoreRowModel: getCoreRowModel(), - ...(isLegacyMode && { getExpandedRowModel: getExpandedRowModel() }), + getExpandedRowModel: getExpandedRowModel(), }); return ( @@ -68,10 +62,7 @@ export function DataTable({ ) : table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( - !isLegacyMode && onRowClick?.(row.original)} - > + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -79,8 +70,7 @@ export function DataTable({ ))} - {/* Legacy expansion mode for audit logs */} - {isLegacyMode && row.getIsExpanded() && renderSubComponent && ( + {row.getIsExpanded() && (
{renderSubComponent({ row })}