Merge pull request #36966 from BerriAI/litellm_shadcn_smallfiles_0814

refactor(ui): migrate ten small dashboard files off antd and tremor
This commit is contained in:
yuneng-jiang 2026-08-14 11:50:56 -07:00 committed by GitHub
commit f03df1bb42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 218 additions and 97 deletions

View file

@ -1653,21 +1653,11 @@
"count": 1
}
},
"src/app/onboarding/OnboardingErrorView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/onboarding/OnboardingFormBody.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/onboarding/OnboardingLoadingView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/AIHub/ModelHubTable.test.tsx": {
"max-params": {
"count": 1
@ -1847,9 +1837,6 @@
}
},
"src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx": {
"no-restricted-imports": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 1
}
@ -2388,11 +2375,6 @@
"count": 1
}
},
"src/components/key_team_helpers/TagRateLimitEditor.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/key_team_helpers/fetch_available_models_team_key.tsx": {
"local/filename-pascal-case": {
"count": 1
@ -2715,17 +2697,9 @@
"count": 1
}
},
"src/components/router_settings/LatencyBasedConfiguration.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/router_settings/ReliabilityRetriesSection.tsx": {
"no-nested-ternary": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/router_settings/RoutingStrategySelector.tsx": {
@ -2733,18 +2707,10 @@
"count": 1
}
},
"src/components/router_settings/TagFilteringToggle.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/router_settings/index.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 1
},
"prefer-const": {
"count": 2
}
@ -2781,11 +2747,6 @@
"count": 4
}
},
"src/components/shared/CreatedKeyDisplay.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/shared/advanced_date_picker.tsx": {
"local/filename-pascal-case": {
"count": 1
@ -2850,9 +2811,6 @@
"src/components/shared/numerical_input.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/shared/table_cells/cell_tooltip.tsx": {

View file

@ -9,6 +9,11 @@ describe("OnboardingErrorView", () => {
expect(screen.getByText("Failed to load invitation")).toBeInTheDocument();
});
it("should expose the failure as an alert to assistive technology", () => {
render(<OnboardingErrorView />);
expect(screen.getByRole("alert")).toHaveTextContent("Failed to load invitation");
});
it("should show the expiry description", () => {
render(<OnboardingErrorView />);
expect(screen.getByText("The invitation link may be invalid or expired.")).toBeInTheDocument();
@ -16,7 +21,6 @@ describe("OnboardingErrorView", () => {
it("should render a Back to Login link pointing to /ui/login/", () => {
render(<OnboardingErrorView />);
// antd Button with href renders as an <a> element
const link = screen.getByRole("link", { name: "Back to Login" });
expect(link).toHaveAttribute("href", "/ui/login/");
});

View file

@ -1,18 +1,21 @@
import React from "react";
import { Alert, Button } from "antd";
import { CircleAlert } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { buttonVariants } from "@/components/ui/button";
import { getLoginUrl } from "@/utils/returnUrlUtils";
export function OnboardingErrorView() {
return (
<div className="mx-auto w-full max-w-md mt-10">
<Alert
type="error"
message="Failed to load invitation"
description="The invitation link may be invalid or expired."
showIcon
/>
<Alert variant="error">
<CircleAlert />
<AlertTitle>Failed to load invitation</AlertTitle>
<AlertDescription>The invitation link may be invalid or expired.</AlertDescription>
</Alert>
<div className="mt-4">
<Button href={getLoginUrl()}>Back to Login</Button>
<a href={getLoginUrl()} className={buttonVariants({ variant: "outline" })}>
Back to Login
</a>
</div>
</div>
);

View file

@ -1,12 +1,12 @@
import React from "react";
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { OnboardingLoadingView } from "./OnboardingLoadingView";
describe("OnboardingLoadingView", () => {
it("should render a spinner container", () => {
const { container } = render(<OnboardingLoadingView />);
expect(container.firstChild).toBeInTheDocument();
it("should expose the loading state to assistive technology", () => {
render(<OnboardingLoadingView />);
expect(screen.getByRole("status", { name: "Loading invitation" })).toBeInTheDocument();
});
it("should apply centering layout classes", () => {

View file

@ -1,11 +1,10 @@
import React from "react";
import { Spin } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
export function OnboardingLoadingView() {
return (
<div className="mx-auto w-full max-w-md mt-10 flex justify-center">
<Spin indicator={<LoadingOutlined spin />} size="large" />
<UiLoadingSpinner role="status" aria-label="Loading invitation" className="size-8 text-muted-foreground" />
</div>
);
}

View file

@ -4,9 +4,9 @@
* Works with forms - reads from and writes to router_settings.fallbacks
*/
import { Button as TremorButton } from "@tremor/react";
import { Button } from "antd";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import MessageManager from "@/components/molecules/message_manager";
import NotificationManager from "../../../molecules/notifications_manager";
import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
@ -119,13 +119,10 @@ export default function AddFallbacks({ accessToken, value = [], onChange }: AddF
return (
<div>
<TremorButton
className="mx-auto"
onClick={() => setIsModalVisible(true)}
icon={() => <span className="mr-1">+</span>}
>
<Button className="mx-auto" onClick={() => setIsModalVisible(true)}>
<span>+</span>
Add Fallbacks
</TremorButton>
</Button>
<AddFallbacksModal open={isModalVisible} onCancel={handleCancel}>
<FallbackSelectionForm
key={modalKey}
@ -138,15 +135,11 @@ export default function AddFallbacks({ accessToken, value = [], onChange }: AddF
{/* Footer with Cancel and Save buttons */}
{groups.length > 0 && (
<div className="flex items-center justify-end space-x-3 pt-6 mt-6 border-t border-gray-100">
<Button type="default" onClick={handleCancel} disabled={isSaving}>
<Button variant="outline" onClick={handleCancel} disabled={isSaving}>
Cancel
</Button>
<Button
type="default"
onClick={handleSaveAll}
disabled={groups.length === 0 || isSaving}
loading={isSaving}
>
<Button variant="outline" onClick={handleSaveAll} disabled={groups.length === 0 || isSaving}>
{isSaving && <UiLoadingSpinner className="size-4" />}
{isSaving ? "Saving Configuration..." : "Save All Configurations"}
</Button>
</div>

View file

@ -0,0 +1,129 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { TagRateLimitEditor, TagRateLimitEntry, tagLimitsToRows, tagRowsToLimits } from "./TagRateLimitEditor";
function Harness({ initial = [] as TagRateLimitEntry[], onValue }: { initial?: TagRateLimitEntry[]; onValue?: any }) {
const [rows, setRows] = useState<TagRateLimitEntry[]>(initial);
return (
<TagRateLimitEditor
value={rows}
onChange={(next) => {
setRows(next);
onValue?.(next);
}}
/>
);
}
const rowsWith = (tag: string, rpm: number | null): TagRateLimitEntry[] => [{ id: "r1", tag, rpm_limit: rpm }];
describe("TagRateLimitEditor", () => {
it("should render one tag and one RPM field per row", () => {
render(<Harness initial={rowsWith("cell-1", 100)} />);
expect(screen.getByRole("textbox", { name: "Tag" })).toHaveValue("cell-1");
expect(screen.getByRole("spinbutton", { name: "RPM limit" })).toHaveValue(100);
});
it("should add a row when Add Tag Limit is clicked", async () => {
const user = userEvent.setup();
render(<Harness />);
expect(screen.queryAllByRole("textbox", { name: "Tag" })).toHaveLength(0);
await user.click(screen.getByRole("button", { name: /add tag limit/i }));
expect(screen.getAllByRole("textbox", { name: "Tag" })).toHaveLength(1);
});
it("should let the user type a tag name", async () => {
const user = userEvent.setup();
render(<Harness initial={rowsWith("", null)} />);
await user.type(screen.getByRole("textbox", { name: "Tag" }), "cell-2");
expect(screen.getByRole("textbox", { name: "Tag" })).toHaveValue("cell-2");
});
it("should record the typed RPM limit as a number, not a string", async () => {
const user = userEvent.setup();
const seen: TagRateLimitEntry[][] = [];
render(<Harness initial={rowsWith("cell-1", null)} onValue={(v: TagRateLimitEntry[]) => seen.push(v)} />);
await user.type(screen.getByRole("spinbutton", { name: "RPM limit" }), "60");
const latest = seen[seen.length - 1][0];
expect(latest.rpm_limit).toBe(60);
expect(typeof latest.rpm_limit).toBe("number");
});
it("should reset the RPM limit to null when the field is cleared", async () => {
const user = userEvent.setup();
const seen: TagRateLimitEntry[][] = [];
render(<Harness initial={rowsWith("cell-1", 60)} onValue={(v: TagRateLimitEntry[]) => seen.push(v)} />);
await user.clear(screen.getByRole("spinbutton", { name: "RPM limit" }));
expect(seen[seen.length - 1][0].rpm_limit).toBeNull();
});
it("should remove only the clicked row", async () => {
const user = userEvent.setup();
const initial: TagRateLimitEntry[] = [
{ id: "r1", tag: "keep-me", rpm_limit: 10 },
{ id: "r2", tag: "delete-me", rpm_limit: 20 },
];
render(<Harness initial={initial} />);
await user.click(screen.getAllByRole("button", { name: "Remove tag limit" })[1]);
const tags = screen.getAllByRole("textbox", { name: "Tag" });
expect(tags).toHaveLength(1);
expect(tags[0]).toHaveValue("keep-me");
});
it("should not submit the surrounding form when a row is removed", async () => {
const user = userEvent.setup();
let submitted = false;
render(
<form
onSubmit={() => {
submitted = true;
}}
>
<Harness initial={rowsWith("cell-1", 10)} />
</form>,
);
await user.click(screen.getByRole("button", { name: "Remove tag limit" }));
expect(submitted).toBe(false);
expect(screen.queryAllByRole("textbox", { name: "Tag" })).toHaveLength(0);
});
});
describe("tagRowsToLimits", () => {
it("should map named rows with numeric limits into the rpm map", () => {
expect(tagRowsToLimits([{ id: "a", tag: "cell-1", rpm_limit: 60 }])).toEqual({ tag_rpm_limit: { "cell-1": 60 } });
});
it("should drop rows with a blank tag or a null limit", () => {
const rows: TagRateLimitEntry[] = [
{ id: "a", tag: " ", rpm_limit: 60 },
{ id: "b", tag: "cell-2", rpm_limit: null },
];
expect(tagRowsToLimits(rows)).toEqual({ tag_rpm_limit: {} });
});
});
describe("tagLimitsToRows", () => {
it("should rebuild rows from a stored rpm map", () => {
const rows = tagLimitsToRows({ "cell-1": 60 });
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ tag: "cell-1", rpm_limit: 60 });
});
it("should ignore non-numeric entries", () => {
expect(tagLimitsToRows({ "cell-1": "sixty" })).toEqual([]);
});
});

View file

@ -1,5 +1,6 @@
import { Button, Input, InputNumber } from "antd";
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export interface TagRateLimitEntry {
// Stable identity for React list keys so deleting a middle row doesn't shift
@ -72,25 +73,29 @@ export function TagRateLimitEditor({ value, onChange }: TagRateLimitEditorProps)
{value.map((row, idx) => (
<div key={row.id} style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
<Input
aria-label="Tag"
value={row.tag}
onChange={(e) => updateRow(idx, "tag", e.target.value)}
placeholder="Tag (e.g. cell-1)"
style={{ width: 180 }}
/>
<InputNumber
<Input
aria-label="RPM limit"
type="number"
min={0}
value={row.rpm_limit ?? undefined}
onChange={(v) => updateRow(idx, "rpm_limit", v ?? null)}
value={row.rpm_limit ?? ""}
onChange={(e) => updateRow(idx, "rpm_limit", e.target.value === "" ? null : Number(e.target.value))}
placeholder="RPM"
style={{ width: 120 }}
/>
<Button type="text" danger size="small" onClick={() => removeRow(idx)} style={{ padding: "0 4px" }}>
<Button variant="destructive" size="sm" aria-label="Remove tag limit" onClick={() => removeRow(idx)}>
</Button>
</div>
))}
<Button
size="small"
variant="outline"
size="sm"
onClick={(e) => {
e.preventDefault();
addRow();

View file

@ -1,5 +1,5 @@
import React from "react";
import { Input } from "antd";
import { Input } from "@/components/ui/input";
interface routingStrategyArgs {
ttl?: number;

View file

@ -1,5 +1,5 @@
import React from "react";
import { Input } from "antd";
import { Input } from "@/components/ui/input";
interface ReliabilityRetriesSectionProps {
routerSettings: { [key: string]: any };

View file

@ -22,6 +22,11 @@ describe("TagFilteringToggle", () => {
expect(screen.getByText("Enable Tag Filtering")).toBeInTheDocument();
});
it("should name the switch with the metadata label so it is reachable by accessible name", () => {
render(<TagFilteringToggle enabled={false} routerFieldsMetadata={baseMetadata} onToggle={vi.fn()} />);
expect(screen.getByRole("switch", { name: "Tag Filtering" })).toBeInTheDocument();
});
it("should display the label from metadata when provided", () => {
render(<TagFilteringToggle enabled={false} routerFieldsMetadata={baseMetadata} onToggle={vi.fn()} />);
expect(screen.getByText("Tag Filtering")).toBeInTheDocument();

View file

@ -1,5 +1,5 @@
import React from "react";
import { Switch } from "antd";
import React, { useId } from "react";
import { Switch } from "@/components/ui/switch";
interface TagFilteringToggleProps {
enabled: boolean;
@ -8,11 +8,13 @@ interface TagFilteringToggleProps {
}
const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({ enabled, routerFieldsMetadata, onToggle }) => {
const toggleId = useId();
return (
<div className="space-y-3 max-w-3xl">
<div className="flex items-start justify-between">
<div className="flex-1">
<label className="text-xs font-medium text-gray-700 uppercase tracking-wide">
<label htmlFor={toggleId} className="text-xs font-medium text-gray-700 uppercase tracking-wide">
{routerFieldsMetadata["enable_tag_filtering"]?.ui_field_name || "Enable Tag Filtering"}
</label>
<p className="text-xs text-gray-500 mt-0.5">
@ -32,7 +34,7 @@ const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({ enabled, router
)}
</p>
</div>
<Switch checked={enabled} onChange={onToggle} className="ml-4" />
<Switch id={toggleId} checked={enabled} onCheckedChange={onToggle} className="ml-4" />
</div>
</div>
);

View file

@ -134,6 +134,30 @@ describe("RouterSettings", () => {
);
});
it("should send the edited input value, not the loaded one, on Save Changes", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterSettings {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
});
const numRetries = await screen.findByRole("textbox", { name: /num_retries/i });
await user.clear(numRetries);
await user.type(numRetries, "42");
await user.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() =>
expect(setCallbacksCall).toHaveBeenCalledWith(
"test-token",
expect.objectContaining({
router_settings: expect.objectContaining({ num_retries: 42 }),
}),
),
);
});
it("should show a success notification after saving", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterSettings {...defaultProps} />);

View file

@ -1,5 +1,5 @@
import { Button } from "antd";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import NotificationsManager from "../molecules/notifications_manager";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
@ -190,10 +190,10 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
{/* Actions - Sticky at bottom */}
<div className="border-t border-gray-200 pt-6 flex justify-end gap-3">
<Button onClick={() => window.location.reload()}>Reset</Button>
<Button type="primary" onClick={handleSaveChanges}>
Save Changes
<Button variant="outline" onClick={() => window.location.reload()}>
Reset
</Button>
<Button onClick={handleSaveChanges}>Save Changes</Button>
</div>
</div>
);

View file

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Button } from "antd";
import { Button } from "@/components/ui/button";
import MessageManager from "@/components/molecules/message_manager";
interface CreatedKeyDisplayProps {
@ -41,9 +41,7 @@ const CreatedKeyDisplay: React.FC<CreatedKeyDisplayProps> = ({ apiKey }) => {
</div>
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
<Button type="primary" style={{ marginTop: 12 }}>
{copied ? "Copied!" : "Copy Virtual Key"}
</Button>
<Button className="mt-3">{copied ? "Copied!" : "Copy Virtual Key"}</Button>
</CopyToClipboard>
</div>
);

View file

@ -1,5 +1,5 @@
import React from "react";
import { NumberInput } from "@tremor/react";
import { Input } from "@/components/ui/input";
interface NumericalInputProps {
step?: number;
@ -7,7 +7,7 @@ interface NumericalInputProps {
placeholder?: string;
min?: number;
max?: number;
onChange?: any; // Using any to avoid type conflicts with Tremor's NumberInput
onChange?: any;
[key: string]: any;
}
@ -20,7 +20,7 @@ interface NumericalInputProps {
* @param {number} [props.min] - Minimum value
* @param {number} [props.max] - Maximum value
* @param {Function} [props.onChange] - On change handler
* @param {any} props.rest - Additional props passed to NumberInput
* @param {any} props.rest - Additional props passed to Input
*/
const NumericalInput: React.FC<NumericalInputProps> = ({
step = 0.01,
@ -32,7 +32,8 @@ const NumericalInput: React.FC<NumericalInputProps> = ({
...rest
}) => {
return (
<NumberInput
<Input
type="number"
onWheel={(event) => event.currentTarget.blur()}
step={step}
style={style}