diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx index 6aa6410b302..ee1dedba49f 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -1,5 +1,5 @@ -import { Button, Tag, Tooltip, Typography } from "antd"; -import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; +import { Button, Space, Tag, Tooltip, Typography } from "antd"; +import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; import moment from "moment"; import { LogEntry } from "../columns"; import { getProviderLogoAndName } from "../../provider_info_helpers"; @@ -20,7 +20,6 @@ const { Text } = Typography; interface DrawerHeaderProps { log: LogEntry; onClose: () => void; - onCopyRequestId: () => void; onPrevious: () => void; onNext: () => void; statusLabel: string; @@ -35,7 +34,6 @@ interface DrawerHeaderProps { export function DrawerHeader({ log, onClose, - onCopyRequestId, onPrevious, onNext, statusLabel, @@ -61,7 +59,7 @@ export function DrawerHeader({ {/* Row 1: Request ID + Actions */}
- +
@@ -84,7 +82,7 @@ function ModelProviderSection({ providerName?: string; }) { return ( -
+ {providerLogo && ( )} -
+ {model} {providerName && ( - + {providerName} )} -
-
+ + ); } /** - * Request ID display with copy button + * Request ID display with copy functionality */ -function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () => void }) { +function RequestIdSection({ requestId }: { requestId: string }) { return ( -
+
{requestId} - -
); } /** * Navigation controls (previous, next, close) - * Shows keyboard shortcuts styled as buttons for visibility + * Shows keyboard shortcuts with bounding boxes for visibility */ function NavigationSection({ onPrevious, @@ -168,7 +165,7 @@ function NavigationSection({ }; return ( -
+ }> - -
-
+
); } @@ -202,13 +196,17 @@ function StatusBar({ environment: string; }) { return ( -
+ {statusLabel} Env: {environment} - - {moment(log.startTime).format("MMM D, YYYY h:mm:ss A")} - ({moment(log.startTime).fromNow()}) - -
+ + + {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/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index cbb237dc017..afc7aa62ebc 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd"; -import { CopyOutlined } from "@ant-design/icons"; +import { Drawer, Typography, Space, Descriptions, Card, Tag, Tabs, Alert, Collapse } from "antd"; import moment from "moment"; import { LogEntry } from "../columns"; import { formatNumberWithCommas } from "@/utils/dataUtils"; @@ -12,8 +11,16 @@ 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 { + formatData, + checkHasMessages, + checkHasResponse, + normalizeGuardrailEntries, + calculateTotalMaskedEntities, + getGuardrailLabel, + checkHasVectorStoreData, +} from "./utils"; import { DRAWER_WIDTH, DRAWER_CONTENT_PADDING, @@ -24,7 +31,7 @@ import { FONT_SIZE_SMALL, FONT_FAMILY_MONO, SPACING_XLARGE, - MESSAGE_REQUEST_ID_COPIED, + SPACING_MEDIUM, } from "./constants"; import { ToolsSection } from "../ToolsSection"; @@ -94,11 +101,6 @@ export function LogDetailsDrawer({ 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); }; @@ -135,7 +137,6 @@ export function LogDetailsDrawer({ copyToClipboard(JSON.stringify(data, null, 2), label)} getRawRequest={getRawRequest} getFormattedResponse={getFormattedResponse} /> @@ -217,7 +217,7 @@ export function LogDetailsDrawer({ {/* Metadata Card - Only show if there's metadata */} {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( - copyToClipboard(data, "Metadata")} /> + )} {/* Bottom spacing for scroll area */} @@ -254,27 +254,27 @@ function TagsSection({ tags }: { tags: Record }) { Tags -
+ {Object.entries(tags).map(([key, value]) => ( {key}: {String(value)} ))} -
+
); } function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) { return ( - <> + {label} {maskedCount > 0 && ( - + {maskedCount} masked )} - + ); } @@ -337,23 +337,20 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: 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 getCopyText = () => { const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); - const label = activeTab === TAB_REQUEST ? "Request" : "Response"; - onCopy(data, label); + return JSON.stringify(data, null, 2); }; return ( @@ -371,15 +368,13 @@ function RequestResponseSection({ activeKey={activeTab} onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} tabBarExtraContent={ - + /> } items={[ { @@ -417,7 +412,7 @@ function RequestResponseSection({ ); } -function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { +function MetadataSection({ metadata }: { metadata: Record }) { return (
; children: (
- +
;
   );
 }
 
-// ============================================================================
-// 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/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
index 91f5ff8f118..3186cc27b42 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts
@@ -39,6 +39,4 @@ 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";
+// Messages (kept for backwards compatibility if needed elsewhere)
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/utils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/utils.ts
new file mode 100644
index 00000000000..61301cf5b54
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/utils.ts
@@ -0,0 +1,93 @@
+/**
+ * Utility functions for LogDetailsDrawer component.
+ * These functions handle data formatting, validation, and guardrail calculations.
+ */
+
+/**
+ * Formats data for display. If input is a string, attempts to parse as JSON.
+ * @param input - Data to format (string or object)
+ * @returns Parsed JSON object or original input
+ */
+export function formatData(input: any) {
+  if (typeof input === "string") {
+    try {
+      return JSON.parse(input);
+    } catch {
+      return input;
+    }
+  }
+  return input;
+}
+
+/**
+ * Checks if messages array/object contains data.
+ * @param messages - Messages to check
+ * @returns True if messages exist and have content
+ */
+export 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;
+}
+
+/**
+ * Checks if response object contains data.
+ * @param response - Response to check
+ * @returns True if response exists and has content
+ */
+export function checkHasResponse(response: any): boolean {
+  if (!response) return false;
+  return Object.keys(formatData(response)).length > 0;
+}
+
+/**
+ * Normalizes guardrail information into an array.
+ * @param guardrailInfo - Guardrail data (may be array, object, or null)
+ * @returns Array of guardrail entries
+ */
+export function normalizeGuardrailEntries(guardrailInfo: any): any[] {
+  if (Array.isArray(guardrailInfo)) return guardrailInfo;
+  if (guardrailInfo) return [guardrailInfo];
+  return [];
+}
+
+/**
+ * Calculates total number of masked entities across all guardrail entries.
+ * @param entries - Array of guardrail entries
+ * @returns Total count of masked entities
+ */
+export 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);
+}
+
+/**
+ * Gets a display label for guardrail(s).
+ * @param entries - Array of guardrail entries
+ * @returns Display string for guardrail label
+ */
+export function getGuardrailLabel(entries: any[]): string {
+  if (entries.length === 0) return "-";
+  if (entries.length === 1) return entries[0]?.guardrail_name ?? "-";
+  return `${entries.length} guardrails`;
+}
+
+/**
+ * Checks if vector store data exists in metadata.
+ * @param metadata - Metadata object to check
+ * @returns True if vector store data exists and is non-empty
+ */
+export 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
+  );
+}