mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
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
This commit is contained in:
parent
ba746967c8
commit
534f9b515d
7 changed files with 40 additions and 39 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { LogEntry } from "../types";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { copyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
|
||||
import GuardrailViewer from "./sections/GuardrailViewer/GuardrailViewer";
|
||||
import EvalViewer from "./sections/EvalViewer/EvalViewer";
|
||||
|
|
@ -304,13 +304,9 @@ function CopyButton({
|
|||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="overflow-hidden rounded-md" style={{ border: `1px solid ${COLOR_BORDER}` }}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue