From 92ba34823fd5a724c311bb40957039e7a572aa9c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 27 Nov 2025 14:46:05 -0800 Subject: [PATCH] Fix Request and Response Panel JSONViewer --- .../view_logs/RequestResponsePanel.test.tsx | 164 ++++++++++++++++++ .../view_logs/RequestResponsePanel.tsx | 25 +-- 2 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx new file mode 100644 index 0000000000..d163be057b --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.test.tsx @@ -0,0 +1,164 @@ +import { render, screen, act } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { RequestResponsePanel } from "./RequestResponsePanel"; +import type { LogEntry } from "./columns"; +import NotificationsManager from "../molecules/notifications_manager"; + +const mockNotificationsManager = vi.mocked(NotificationsManager); + +const baseLogEntry: LogEntry = { + request_id: "chatcmpl-test-id", + api_key: "api-key", + team_id: "team-id", + model: "gpt-4", + model_id: "gpt-4", + call_type: "chat", + spend: 0, + total_tokens: 0, + prompt_tokens: 0, + completion_tokens: 0, + startTime: "2025-11-14T00:00:00Z", + endTime: "2025-11-14T00:00:00Z", + cache_hit: "miss", + duration: 1, + messages: [{ role: "user", content: "hello" }], + response: { status: "ok" }, + metadata: { + status: "success", + additional_usage_values: { + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + }, + request_tags: {}, + custom_llm_provider: "openai", + api_base: "https://api.example.com", +}; + +describe("RequestResponsePanel", () => { + beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + writable: true, + configurable: true, + }); + Object.defineProperty(window, "isSecureContext", { + value: true, + writable: true, + configurable: true, + }); + }); + + it("should render the component with request and response panels", () => { + const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" }); + const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" }); + + render( + , + ); + + expect(screen.getByText("Request")).toBeInTheDocument(); + expect(screen.getByText("Response")).toBeInTheDocument(); + }); + + it("should copy request to clipboard when copy button is clicked", async () => { + const user = userEvent.setup(); + const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request data" }); + const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" }); + const mockWriteText = vi.fn().mockResolvedValue(undefined); + + if (navigator.clipboard) { + vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText); + } else { + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: mockWriteText, + }, + writable: true, + configurable: true, + }); + } + + render( + , + ); + + const copyButtons = screen.getAllByRole("button"); + const copyRequestButton = copyButtons.find((button) => button.getAttribute("title") === "Copy request"); + + expect(copyRequestButton).toBeInTheDocument(); + + await act(async () => { + await user.click(copyRequestButton!); + }); + + expect(mockGetRawRequest).toHaveBeenCalled(); + expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "request data" }, null, 2)); + expect(mockNotificationsManager.success).toHaveBeenCalledWith("Request copied to clipboard"); + }); + + it("should copy response to clipboard when copy button is clicked", async () => { + const user = userEvent.setup(); + const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" }); + const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response data" }); + const mockWriteText = vi.fn().mockResolvedValue(undefined); + + if (navigator.clipboard) { + vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText); + } else { + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: mockWriteText, + }, + writable: true, + configurable: true, + }); + } + + render( + , + ); + + const copyButtons = screen.getAllByRole("button"); + const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response"); + + expect(copyResponseButton).toBeInTheDocument(); + expect(copyResponseButton).not.toBeDisabled(); + + await act(async () => { + await user.click(copyResponseButton!); + }); + + expect(mockFormattedResponse).toHaveBeenCalled(); + expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "response data" }, null, 2)); + expect(mockNotificationsManager.success).toHaveBeenCalledWith("Response copied to clipboard"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx index 407d99fdd7..97a9272f1e 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestResponsePanel.tsx @@ -1,6 +1,7 @@ import { LogEntry } from "./columns"; import NotificationsManager from "../molecules/notifications_manager"; -import { JsonView, allExpanded, defaultStyles } from 'react-json-view-lite'; +import { JsonView, defaultStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; interface RequestResponsePanelProps { row: { @@ -95,15 +96,7 @@ export function RequestResponsePanel({
-
-            {/* {JSON.stringify(getRawRequest(), null, 2)} */}
-            
-          
+
@@ -136,17 +129,9 @@ export function RequestResponsePanel({ -
+
{hasResponse ? ( -
-              {/* {JSON.stringify(formattedResponse(), null, 2)} */}
-              
-            
+ ) : (
Response data not available
)}