From 534f9b515d18969317ab3885d0f9cef483abac1f Mon Sep 17 00:00:00 2001 From: Vivek Raj Date: Tue, 27 Oct 2026 00:00:02 +0530 Subject: [PATCH] fix(ui): copy from the log details drawer over plain HTTP The request log drawer had its own copy handlers for the request id, x-litellm-call-id, trace or session id, input, output, metadata and realtime transcript. Each one called navigator.clipboard directly, so over plain HTTP they did nothing, and the input and output buttons could show a "copied" toast for a copy that never happened Use the shared copyToClipboard helper in each of them, the same way CopyButton now does. The checkmark only shows when the copy succeeded Fixes #48418 --- .../logs/detail/DrawerHeader.test.tsx | 23 ++++++++++++++++++- .../components/logs/detail/DrawerHeader.tsx | 11 ++++----- .../src/components/logs/detail/InputCard.tsx | 8 ++----- .../logs/detail/LogDetailContent.tsx | 12 ++++------ .../logs/detail/LogDetailsDrawer.tsx | 13 ++++------- .../src/components/logs/detail/OutputCard.tsx | 9 ++------ .../logs/detail/RealtimePrettyView.tsx | 3 ++- 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.test.tsx b/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.test.tsx index 564862acccd..a35199c5a6e 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.test.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.test.tsx @@ -1,7 +1,7 @@ import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { ReactNode } from "react"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { render } from "../../../../tests/test-utils"; import type { LogEntry } from "../types"; @@ -118,3 +118,24 @@ describe("DrawerHeader back link", () => { expect(screen.queryByRole("button", { name: /^Back to/ })).not.toBeInTheDocument(); }); }); + +describe("DrawerHeader copy", () => { + afterEach(() => { + vi.restoreAllMocks(); + Reflect.deleteProperty(navigator, "clipboard"); + Reflect.deleteProperty(document, "execCommand"); + }); + + it("copies the request id when the browser has no clipboard API, as on a plain HTTP origin", async () => { + const user = userEvent.setup(); + Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true }); + Object.defineProperty(document, "execCommand", { value: () => true, configurable: true }); + const select = vi.spyOn(HTMLTextAreaElement.prototype, "select"); + renderHeader(logEntry({}), false); + + await user.click(screen.getByRole("button", { name: "Copy Request ID" })); + + expect(select.mock.contexts.map((textArea) => textArea.value)).toEqual(["170d64ea-69f0-431a-be72-332f8f78c18a"]); + expect(await screen.findByRole("button", { name: "Copied!" })).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.tsx index 8ec615d444d..4cb923df4a4 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/DrawerHeader.tsx @@ -9,6 +9,7 @@ import { AutoRouterTag } from "@/components/shared/table_cells"; import { ClassifyTag } from "./ClassifyTag"; import { SidebarToggle } from "./SidebarToggle"; import { getProviderLogoAndName } from "../../provider_info_helpers"; +import { copyToClipboard } from "@/utils/dataUtils"; import { DRAWER_HEADER_PADDING, COLOR_BORDER, @@ -183,13 +184,9 @@ function CopyableId({ const [copied, setCopied] = useState(false); const handleCopy = async () => { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 1200); - } catch { - /* clipboard unavailable in non-secure contexts */ - } + if (!(await copyToClipboard(value))) return; + setCopied(true); + setTimeout(() => setCopied(false), 1200); }; return ( diff --git a/ui/litellm-dashboard/src/components/logs/detail/InputCard.tsx b/ui/litellm-dashboard/src/components/logs/detail/InputCard.tsx index d367c69f263..6667b6b086e 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/InputCard.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/InputCard.tsx @@ -4,7 +4,7 @@ */ import { useState } from "react"; -import { toast } from "@/lib/toast"; +import { copyToClipboard } from "@/utils/dataUtils"; import { ParsedMessage } from "./prettyMessagesTypes"; import { SectionHeader } from "./SectionHeader"; import { CollapsibleMessage } from "./CollapsibleMessage"; @@ -30,11 +30,7 @@ export function InputCard({ messages, promptTokens, inputCost }: InputCardProps) const lastMessage = nonSystemMessages.length > 0 ? nonSystemMessages[nonSystemMessages.length - 1] : null; const historyMessages = nonSystemMessages.slice(0, -1); - const handleCopy = () => { - const content = lastMessage?.content || ""; - navigator.clipboard.writeText(content); - toast.success("Input copied"); - }; + const handleCopy = () => copyToClipboard(lastMessage?.content, "Input copied"); return (
{ - try { - await navigator.clipboard.writeText(getText()); - setCopied(true); - setTimeout(() => setCopied(false), 1200); - } catch { - /* clipboard unavailable in non-secure contexts */ - } + if (!(await copyToClipboard(getText()))) return; + setCopied(true); + setTimeout(() => setCopied(false), 1200); }; return ( diff --git a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx index 48b89ee8ac1..dafb1748dc3 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/LogDetailsDrawer.tsx @@ -13,7 +13,7 @@ import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { LogDetailContent, GuardrailJumpLink } from "./LogDetailContent"; import { sessionSpendLogsCall } from "../../networking"; import { useQuery } from "@tanstack/react-query"; -import { getSpendString } from "@/utils/dataUtils"; +import { copyToClipboard, getSpendString } from "@/utils/dataUtils"; import { normalizeGuardrailEntries, sortSessionLogs, SessionLogSortMode } from "./utils"; import { DRAWER_WIDTH } from "./constants"; import { useLogDetails } from "@/app/(dashboard)/hooks/logDetails/useLogDetails"; @@ -288,14 +288,9 @@ export function LogDetailsDrawer({ const leftPanelDisplayId = leftPanelId.length > 14 ? `${leftPanelId.slice(0, 11)}...` : leftPanelId; const handleCopyLeftPanelId = async () => { - if (!leftPanelId) return; - try { - await navigator.clipboard.writeText(leftPanelId); - setCopiedLeftPanelId(true); - setTimeout(() => setCopiedLeftPanelId(false), 1200); - } catch { - /* clipboard unavailable in non-secure contexts */ - } + if (!(await copyToClipboard(leftPanelId))) return; + setCopiedLeftPanelId(true); + setTimeout(() => setCopiedLeftPanelId(false), 1200); }; if (!currentLog || !enrichedLog) return null; diff --git a/ui/litellm-dashboard/src/components/logs/detail/OutputCard.tsx b/ui/litellm-dashboard/src/components/logs/detail/OutputCard.tsx index 7d0ec62ffe2..1ede243d183 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/OutputCard.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/OutputCard.tsx @@ -4,7 +4,7 @@ */ import { useState } from "react"; -import { toast } from "@/lib/toast"; +import { copyToClipboard } from "@/utils/dataUtils"; import { COLOR_BORDER } from "./constants"; import { ParsedMessage } from "./prettyMessagesTypes"; import { SectionHeader } from "./SectionHeader"; @@ -19,12 +19,7 @@ interface OutputCardProps { export function OutputCard({ message, completionTokens, outputCost }: OutputCardProps) { const [isCollapsed, setIsCollapsed] = useState(false); - const handleCopy = () => { - if (!message) return; - - navigator.clipboard.writeText(message.content || ""); - toast.success("Output copied"); - }; + const handleCopy = () => copyToClipboard(message?.content, "Output copied"); return (
diff --git a/ui/litellm-dashboard/src/components/logs/detail/RealtimePrettyView.tsx b/ui/litellm-dashboard/src/components/logs/detail/RealtimePrettyView.tsx index 82f0d3e7fb2..178875f1d2d 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/RealtimePrettyView.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/RealtimePrettyView.tsx @@ -8,6 +8,7 @@ import { useState } from "react"; import { ChevronDown, ChevronUp, MessageSquare, Mic, Settings, Volume2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { copyToClipboard } from "@/utils/dataUtils"; import { SectionHeader } from "./SectionHeader"; interface RealtimeEvent { @@ -276,7 +277,7 @@ function ConversationCard({ (r.output || []).flatMap((o) => (o.content || []).map((c) => `${o.role}: ${c.transcript || c.text || ""}`)), ) .join("\n"); - navigator.clipboard.writeText(transcripts); + copyToClipboard(transcripts); }; return (