refactor(ui): migrate transform-request to shadcn (#34303)

* test(ui): characterise transform-request panel behaviour before migration

* refactor(ui): migrate transform-request to shadcn

* fix(ui): keep transform-request panels within the fixed-height content fold

* fix(ui): let transform-request flow naturally so the shell scrolls instead of clipping

* test(ui): select the copy button by its accessible name
This commit is contained in:
yuneng-jiang 2026-07-22 17:01:41 -07:00 • committed by GitHub
parent 692e6d48e9
commit 169ba0e287
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 226 additions and 144 deletions

View file

@ -1098,11 +1098,6 @@
"count": 1
}
},
"src/app/(dashboard)/transform-request/TransformRequestPanel.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/ui-theme/UIThemeSettings.tsx": {
"no-restricted-imports": {
"count": 1

View file

@ -0,0 +1,160 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import TransformRequestPanel from "./TransformRequestPanel";
import { transformRequestCall } from "@/components/networking";
import NotificationsManager from "@/components/molecules/notifications_manager";
vi.mock("@/components/networking", () => ({
transformRequestCall: vi.fn(),
}));
vi.mock("@/components/molecules/notifications_manager", () => ({
default: {
success: vi.fn(),
info: vi.fn(),
fromBackend: vi.fn(),
},
}));
const transformRequestCallMock = vi.mocked(transformRequestCall);
const notify = vi.mocked(NotificationsManager);
const ACCESS_TOKEN = "sk-test-token";
const getRequestTextarea = () => screen.getByPlaceholderText(/press cmd\/ctrl \+ enter to transform/i);
const getTransformButton = () => screen.getByRole("button", { name: /transform/i });
const getCopyButton = () => screen.getByRole("button", { name: /copy to clipboard/i });
describe("TransformRequestPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("renders both panels, the prefilled request and the placeholder curl", () => {
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
expect(screen.getByText("Original Request")).toBeInTheDocument();
expect(screen.getByText("Transformed Request")).toBeInTheDocument();
expect(screen.getByText(/sensitive headers are not shown/i)).toBeInTheDocument();
expect((getRequestTextarea() as HTMLTextAreaElement).value).toContain('"model": "openai/gpt-4o"');
expect(screen.getByText(/https:\/\/api\.openai\.com\/v1\/chat\/completions/)).toBeInTheDocument();
expect(screen.getByRole("link", { name: /here/i })).toHaveAttribute(
"href",
"https://github.com/BerriAI/litellm/issues",
);
});
it("sends the edited request body as a completion call and renders the returned curl", async () => {
const user = userEvent.setup();
transformRequestCallMock.mockResolvedValue({
raw_request_api_base: "https://api.anthropic.com/v1/messages",
raw_request_body: { model: "claude-opus-4-8", max_tokens: 42 },
raw_request_headers: { "x-api-key": "redacted" },
});
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
const textarea = getRequestTextarea();
await user.clear(textarea);
await user.type(textarea, '{{"model": "claude-opus-4-8"}');
await user.click(getTransformButton());
await waitFor(() => expect(transformRequestCallMock).toHaveBeenCalledTimes(1));
expect(transformRequestCallMock).toHaveBeenCalledWith(ACCESS_TOKEN, {
call_type: "completion",
request_body: { model: "claude-opus-4-8" },
});
const output = await screen.findByText(/api\.anthropic\.com\/v1\/messages/);
expect(output.textContent).toContain("curl -X POST");
expect(output.textContent).toContain("-H 'x-api-key: redacted'");
expect(output.textContent).toContain('"model": "claude-opus-4-8"');
expect(output.textContent).toContain('"max_tokens": 42');
expect(notify.success).toHaveBeenCalledWith("Request transformed successfully");
});
it("transforms on Cmd/Ctrl + Enter without clicking the button", async () => {
const user = userEvent.setup();
transformRequestCallMock.mockResolvedValue({
raw_request_api_base: "https://api.openai.com/v1/chat/completions",
raw_request_body: { model: "gpt-4o" },
raw_request_headers: {},
});
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
getRequestTextarea().focus();
await user.keyboard("{Meta>}{Enter}{/Meta}");
await waitFor(() => expect(transformRequestCallMock).toHaveBeenCalledTimes(1));
});
it("rejects invalid JSON without calling the backend", async () => {
const user = userEvent.setup();
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
const textarea = getRequestTextarea();
await user.clear(textarea);
await user.type(textarea, "not json");
await user.click(getTransformButton());
await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("Invalid JSON in request body"));
expect(transformRequestCallMock).not.toHaveBeenCalled();
});
it("does not call the backend when there is no access token", async () => {
const user = userEvent.setup();
render(<TransformRequestPanel accessToken={null} />);
await user.click(getTransformButton());
await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("No access token found"));
expect(transformRequestCallMock).not.toHaveBeenCalled();
});
it("reports a failed transform and leaves the placeholder curl in place", async () => {
const user = userEvent.setup();
vi.spyOn(console, "error").mockImplementation(() => {});
transformRequestCallMock.mockRejectedValue(new Error("boom"));
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
await user.click(getTransformButton());
await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("Failed to transform request"));
expect(screen.getByText(/https:\/\/api\.openai\.com\/v1\/chat\/completions/)).toBeInTheDocument();
});
it("copies the transformed request to the clipboard", async () => {
const user = userEvent.setup();
const writeText = vi.spyOn(navigator.clipboard, "writeText");
transformRequestCallMock.mockResolvedValue({
raw_request_api_base: "https://api.anthropic.com/v1/messages",
raw_request_body: { model: "claude-opus-4-8" },
raw_request_headers: {},
});
render(<TransformRequestPanel accessToken={ACCESS_TOKEN} />);
await user.click(getTransformButton());
await screen.findByText(/api\.anthropic\.com\/v1\/messages/);
await user.click(getCopyButton());
expect(writeText).toHaveBeenCalledTimes(1);
expect(writeText.mock.calls[0]?.[0]).toContain("https://api.anthropic.com/v1/messages");
expect(notify.success).toHaveBeenCalledWith("Copied to clipboard");
});
});

View file

@ -1,9 +1,12 @@
import React, { useState } from "react";
import { Button } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { Title } from "@tremor/react";
import { ArrowRight, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { transformRequestCall } from "@/components/networking";
import NotificationsManager from "@/components/molecules/notifications_manager";
interface TransformRequestPanelProps {
accessToken: string | null;
}
@ -128,130 +131,50 @@ ${formattedBody}
};
return (
<div className="w-full m-2" style={{ overflow: "hidden" }}>
<Title>Playground</Title>
<p className="text-sm text-gray-500">See how LiteLLM transforms your request for the specified provider.</p>
<div
style={{
display: "flex",
gap: "16px",
width: "100%",
minWidth: 0,
overflow: "hidden",
}}
className="mt-4"
>
<div className="p-2">
<h1 className="text-lg font-medium text-foreground">Playground</h1>
<p className="text-sm text-muted-foreground">
See how LiteLLM transforms your request for the specified provider.
</p>
<div className="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* Original Request Panel */}
<div
style={{
flex: "1 1 50%",
display: "flex",
flexDirection: "column",
border: "1px solid #e8e8e8",
borderRadius: "8px",
padding: "24px",
overflow: "hidden",
maxHeight: "600px",
minWidth: 0,
}}
>
<div style={{ marginBottom: "24px" }}>
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>Original Request</h2>
<p style={{ color: "#666", margin: 0 }}>
The request you would send to LiteLLM /chat/completions endpoint.
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-2xl font-bold">Original Request</CardTitle>
<CardDescription>The request you would send to LiteLLM /chat/completions endpoint.</CardDescription>
</CardHeader>
<textarea
style={{
flex: "1 1 auto",
width: "100%",
minHeight: "240px",
padding: "16px",
border: "1px solid #e8e8e8",
borderRadius: "6px",
fontFamily: "monospace",
fontSize: "14px",
resize: "none",
marginBottom: "24px",
overflow: "auto",
}}
value={originalRequestJSON}
onChange={(e) => setOriginalRequestJSON(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Press Cmd/Ctrl + Enter to transform"
/>
<CardContent>
<Textarea
className="h-72 resize-none p-4 font-mono text-sm field-sizing-fixed"
value={originalRequestJSON}
onChange={(e) => setOriginalRequestJSON(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Press Cmd/Ctrl + Enter to transform"
/>
</CardContent>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "auto",
}}
>
<Button
type="primary"
style={{
backgroundColor: "#000",
display: "flex",
alignItems: "center",
gap: "8px",
}}
onClick={handleTransform}
loading={isLoading}
>
<CardFooter className="justify-end">
<Button onClick={handleTransform} disabled={isLoading}>
<span>Transform</span>
<span>→</span>
{isLoading ? <UiLoadingSpinner className="size-4" /> : <ArrowRight />}
</Button>
</div>
</div>
</CardFooter>
</Card>
{/* Transformed Request Panel */}
<div
style={{
flex: "1 1 50%",
display: "flex",
flexDirection: "column",
border: "1px solid #e8e8e8",
borderRadius: "8px",
padding: "24px",
overflow: "hidden",
maxHeight: "800px",
minWidth: 0,
}}
>
<div style={{ marginBottom: "24px" }}>
<h2 style={{ fontSize: "24px", fontWeight: "bold", margin: "0 0 4px 0" }}>Transformed Request</h2>
<p style={{ color: "#666", margin: 0 }}>How LiteLLM transforms your request for the specified provider.</p>
<br />
<p style={{ color: "#666", margin: 0 }} className="text-xs">
Note: Sensitive headers are not shown.
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-2xl font-bold">Transformed Request</CardTitle>
<CardDescription>How LiteLLM transforms your request for the specified provider.</CardDescription>
<p className="mt-2 text-xs text-muted-foreground">Note: Sensitive headers are not shown.</p>
</CardHeader>
<div
style={{
position: "relative",
backgroundColor: "#f5f5f5",
borderRadius: "6px",
flex: "1 1 auto",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<pre
style={{
padding: "16px",
fontFamily: "monospace",
fontSize: "14px",
margin: 0,
overflow: "auto",
flex: "1 1 auto",
}}
>
{transformedResponse ||
`curl -X POST \\
<CardContent>
<div className="relative rounded-md bg-muted">
<pre className="h-72 overflow-auto p-4 font-mono text-sm">
{transformedResponse ||
`curl -X POST \\
https://api.openai.com/v1/chat/completions \\
-H 'Authorization: Bearer sk-xxx' \\
-H 'Content-Type: application/json' \\
@ -265,29 +188,33 @@ ${formattedBody}
],
"temperature": 0.7
}'`}
</pre>
</pre>
<Button
type="text"
icon={<CopyOutlined />}
style={{
position: "absolute",
right: "8px",
top: "8px",
}}
size="small"
onClick={() => {
navigator.clipboard.writeText(transformedResponse || "");
NotificationsManager.success("Copied to clipboard");
}}
/>
</div>
</div>
<Button
variant="ghost"
size="icon-sm"
aria-label="Copy to clipboard"
className="absolute top-2 right-2"
onClick={() => {
navigator.clipboard.writeText(transformedResponse || "");
NotificationsManager.success("Copied to clipboard");
}}
>
<Copy />
</Button>
</div>
</CardContent>
</Card>
</div>
<div className="mt-4 text-right w-full">
<p className="text-sm text-gray-500">
<div className="mt-4 text-right">
<p className="text-sm text-muted-foreground">
Found an error? File an issue{" "}
<a href="https://github.com/BerriAI/litellm/issues" target="_blank" rel="noopener noreferrer">
<a
className="underline underline-offset-4"
href="https://github.com/BerriAI/litellm/issues"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.