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 (