mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-01 12:22:11 +00:00
Merge pull request #17233 from BerriAI/litellm_ui_logs_json
[Fix] Request and Response Panel JSONViewer
This commit is contained in:
@@ -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(
|
||||
<RequestResponsePanel
|
||||
row={{ original: baseLogEntry }}
|
||||
hasMessages={true}
|
||||
hasResponse={true}
|
||||
hasError={false}
|
||||
errorInfo={null}
|
||||
getRawRequest={mockGetRawRequest}
|
||||
formattedResponse={mockFormattedResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<RequestResponsePanel
|
||||
row={{ original: baseLogEntry }}
|
||||
hasMessages={true}
|
||||
hasResponse={true}
|
||||
hasError={false}
|
||||
errorInfo={null}
|
||||
getRawRequest={mockGetRawRequest}
|
||||
formattedResponse={mockFormattedResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<RequestResponsePanel
|
||||
row={{ original: baseLogEntry }}
|
||||
hasMessages={true}
|
||||
hasResponse={true}
|
||||
hasError={false}
|
||||
errorInfo={null}
|
||||
getRawRequest={mockGetRawRequest}
|
||||
formattedResponse={mockFormattedResponse}
|
||||
/>,
|
||||
);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap break-all w-full max-w-full overflow-hidden break-words">
|
||||
{/* {JSON.stringify(getRawRequest(), null, 2)} */}
|
||||
<JsonView
|
||||
data={getRawRequest()}
|
||||
shouldExpandNode={allExpanded}
|
||||
style={defaultStyles}
|
||||
clickToExpandNode={true}
|
||||
/>
|
||||
</pre>
|
||||
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,17 +129,9 @@ export function RequestResponsePanel({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-auto max-h-96 bg-gray-50 w-full max-w-full box-border">
|
||||
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
|
||||
{hasResponse ? (
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap break-all w-full max-w-full overflow-hidden break-words">
|
||||
{/* {JSON.stringify(formattedResponse(), null, 2)} */}
|
||||
<JsonView
|
||||
data={formattedResponse()}
|
||||
shouldExpandNode={allExpanded}
|
||||
style={defaultStyles}
|
||||
clickToExpandNode={true}
|
||||
/>
|
||||
</pre>
|
||||
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode={true} />
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user