diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.test.tsx new file mode 100644 index 00000000000..8e4a29f03b9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DrawerHeader } from "./DrawerHeader"; +import { LogEntry } from "../columns"; + +const makeLog = (overrides: Partial = {}): LogEntry => + ({ + request_id: "req-123", + api_key: "", + team_id: "", + model: "gpt-4o", + model_id: "", + call_type: "acompletion", + custom_llm_provider: "", + spend: 0, + total_tokens: 0, + prompt_tokens: 0, + completion_tokens: 0, + startTime: "2026-07-08T10:00:00.000Z", + endTime: "2026-07-08T10:00:01.000Z", + cache_hit: "false", + messages: [], + response: {}, + metadata: {}, + ...overrides, + }) as LogEntry; + +const renderHeader = (shareUrl: string) => + render( + {}} + onPrevious={() => {}} + onNext={() => {}} + statusLabel="Success" + statusColor="success" + environment="default" + shareUrl={shareUrl} + />, + ); + +describe("DrawerHeader share link", () => { + const writeText = vi.fn().mockResolvedValue(undefined); + + beforeEach(() => { + writeText.mockClear(); + Object.defineProperty(navigator, "clipboard", { + value: { writeText }, + configurable: true, + }); + }); + + it("copies the share url to the clipboard when the copy-link button is clicked", async () => { + const shareUrl = "https://proxy.example.com/ui/?page=logs&request_id=req-123"; + renderHeader(shareUrl); + + fireEvent.click(screen.getByRole("button", { name: "Copy link to this log" })); + + await waitFor(() => expect(writeText).toHaveBeenCalledWith(shareUrl)); + }); + + it("shows a 'Copied!' tooltip after copying", async () => { + renderHeader("https://proxy.example.com/ui/?request_id=req-123"); + + fireEvent.click(screen.getByRole("button", { name: "Copy link to this log" })); + + await waitFor(() => expect(screen.getByRole("img", { name: "check" })).toBeInTheDocument()); + }); +}); 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 8c0b679a846..53d25c7030c 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,6 @@ import { Button, Space, Tag, Tooltip, Typography } from "antd"; -import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; +import { CheckOutlined, CloseOutlined, DownOutlined, LinkOutlined, UpOutlined } from "@ant-design/icons"; +import { useState } from "react"; import moment from "moment"; import { LogEntry } from "../columns"; import { getProviderLogoAndName } from "../../provider_info_helpers"; @@ -25,6 +26,7 @@ interface DrawerHeaderProps { statusLabel: string; statusColor: "error" | "success"; environment: string; + shareUrl: string; } /** @@ -39,6 +41,7 @@ export function DrawerHeader({ statusLabel, statusColor, environment, + shareUrl, }: DrawerHeaderProps) { const provider = log.custom_llm_provider || ""; const providerInfo = provider ? getProviderLogoAndName(provider) : null; @@ -66,7 +69,7 @@ export function DrawerHeader({ style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: SPACING_MEDIUM }} > - + {/* Row 2: Status + Env + Timestamp */} @@ -148,10 +151,12 @@ function NavigationSection({ onPrevious, onNext, onClose, + shareUrl, }: { onPrevious: () => void; onNext: () => void; onClose: () => void; + shareUrl: string; }) { const keyboardShortcutStyle = { border: "1px solid #d9d9d9", @@ -165,6 +170,7 @@ function NavigationSection({ return ( }> +