Merge pull request #37376 from BerriAI/litellm_/funny-cerf-33d2bd

refactor(ui): move the model alias manager onto design tokens and shadcn controls
This commit is contained in:
yuneng-jiang 2026-08-18 16:11:42 -07:00 • committed by GitHub
commit 673f036680
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 190 additions and 54 deletions

View file

@ -55,6 +55,14 @@ describe("AddAgentForm logos", () => {
expect(selectionLogo).toHaveAttribute("src", expect.stringContaining("assets/logos/a2a_agent.png"));
});
it("labels the agent type picker so the label points at the control", async () => {
renderForm();
await screen.findByAltText("A2A Agent logo");
expect(screen.getByLabelText("Agent Type")).toBe(screen.getByRole("combobox"));
});
it("renders the option logo when the agent type dropdown is opened", async () => {
renderForm();

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Form, Modal, Select, Steps, Tag } from "antd";
import { Modal, Select, Steps, Tag } from "antd";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { toast } from "@/lib/toast";
import { Logo } from "@/components/molecules/logo/Logo";
@ -679,12 +679,12 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
const renderConfigureStep = () => (
<>
<Form.Item
label={<span className="text-sm font-medium text-foreground">Agent Type</span>}
required
tooltip="Select the type of agent you want to create"
>
<Field className="gap-1">
<FieldLabel htmlFor="agent-type">
{labelWithHint("Agent Type", "Select the type of agent you want to create")}
</FieldLabel>
<Select
id="agent-type"
value={agentType}
onChange={handleAgentTypeChange}
size="large"
@ -744,7 +744,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
</Select.Option>
))}
</Select>
</Form.Item>
</Field>
<div className="mt-4">
{agentType === CUSTOM_AGENT_TYPE ? (

View file

@ -22,7 +22,6 @@ export interface AccessGroupSelectorProps {
* - Displays the **access_group_name** in the dropdown.
* - Returns an array of **access_group_id** values.
* - Always multi-select since users can assign multiple access groups.
* - Integrates with Ant Design `<Form.Item>` out of the box via `value` / `onChange`.
*/
const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
value,
@ -42,7 +41,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
return (
<div>
{showLabel && (
<p className="text-sm font-medium block mb-2 text-gray-700 flex items-center">
<p className="mb-2 flex items-center text-sm font-medium text-foreground">
<TeamOutlined className="mr-2" /> {labelText}
</p>
)}
@ -56,7 +55,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
label: (
<span>
<span className="font-medium">{group.access_group_name}</span>{" "}
<span className="text-gray-400 text-xs">({group.access_group_id})</span>
<span className="text-xs text-muted-foreground">({group.access_group_id})</span>
</span>
),
value: group.access_group_id,
@ -68,7 +67,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
return (
<div>
{showLabel && (
<p className="text-sm font-medium block mb-2 text-gray-700 flex items-center">
<p className="mb-2 flex items-center text-sm font-medium text-foreground">
<TeamOutlined className="mr-2" /> {labelText}
</p>
)}
@ -83,7 +82,7 @@ const AccessGroupSelector: React.FC<AccessGroupSelectorProps> = ({
style={{ width: "100%", ...style }}
className={`rounded-md ${className ?? ""}`}
notFoundContent={
isError ? <span className="text-red-500">Failed to load access groups</span> : "No access groups found"
isError ? <span className="text-destructive">Failed to load access groups</span> : "No access groups found"
}
filterOption={(input, option) => {
const searchText = options.find((opt) => opt.value === option?.value)?.searchText ?? "";

View file

@ -0,0 +1,129 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import ModelAliasManager from "./ModelAliasManager";
vi.mock("./ModelSelector", () => ({
default: ({ value, onChange }: { value?: string; onChange?: (v: string) => void }) => (
<input aria-label="target model" value={value ?? ""} onChange={(event) => onChange?.(event.target.value)} />
),
}));
vi.mock("@/lib/toast", () => ({
toast: { success: vi.fn(), fromError: vi.fn(), info: vi.fn() },
}));
const SOURCE_PATH = resolve(process.cwd(), "src/components/common_components/ModelAliasManager.tsx");
const HARDCODED_PALETTE =
/\b(?:text|bg|border|hover:bg|hover:text|dark:bg|dark:text)-(?:gray|slate|zinc|neutral|stone|red|blue|green|yellow|amber|orange)-\d+\b/g;
const SEMANTIC_TOKEN =
/\b(?:text|bg|border)-(?:foreground|muted-foreground|muted|background|card|primary|secondary|destructive|border|input|accent)\b/g;
const NO_ALIASES: Record<string, string> = {};
const renderInForm = (onSubmit = vi.fn(), onAliasUpdate = vi.fn()) => {
render(
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit();
}}
>
<ModelAliasManager
accessToken="tok"
initialModelAliases={NO_ALIASES}
onAliasUpdate={onAliasUpdate}
showExampleConfig={false}
/>
<button type="submit">Save team</button>
</form>,
);
return { onSubmit, onAliasUpdate, user: userEvent.setup() };
};
const addAlias = async (user: ReturnType<typeof userEvent.setup>, name: string, model: string) => {
await user.type(screen.getByLabelText("Alias Name"), name);
await user.type(screen.getByLabelText("target model"), model);
await user.click(screen.getByRole("button", { name: /add alias/i }));
};
describe("ModelAliasManager dark-mode tokens", () => {
const source = readFileSync(SOURCE_PATH, "utf8");
it("reads the component source it is asserting about", () => {
expect(source.length).toBeGreaterThan(0);
expect(source).toContain("Manage Existing Aliases");
});
it("uses semantic colour tokens and no hardcoded palette classes", () => {
expect(source.match(SEMANTIC_TOKEN) ?? []).not.toHaveLength(0);
expect(source.match(HARDCODED_PALETTE) ?? []).toHaveLength(0);
});
});
describe("ModelAliasManager", () => {
it("reports a typed alias and target model to the parent", async () => {
const { onAliasUpdate, user } = renderInForm();
await addAlias(user, "fast-model", "gpt-4o");
expect(onAliasUpdate).toHaveBeenCalledWith({ "fast-model": "gpt-4o" });
expect(screen.getByText("fast-model")).toBeInTheDocument();
});
it("keeps Add Alias disabled until both fields have a value", async () => {
const { onAliasUpdate, user } = renderInForm();
const addButton = screen.getByRole("button", { name: /add alias/i });
expect(addButton).toBeDisabled();
await user.type(screen.getByLabelText("Alias Name"), "fast-model");
expect(addButton).toBeDisabled();
await user.type(screen.getByLabelText("target model"), "gpt-4o");
expect(addButton).toBeEnabled();
await user.click(addButton);
expect(onAliasUpdate).toHaveBeenCalledWith({ "fast-model": "gpt-4o" });
});
it("renames an alias through the row editor", async () => {
const { onAliasUpdate, user } = renderInForm();
await addAlias(user, "fast-model", "gpt-4o");
await user.click(screen.getByRole("button", { name: "Edit fast-model" }));
const rowInput = screen.getByLabelText("Edit alias name");
await user.clear(rowInput);
await user.type(rowInput, "renamed-model");
await user.click(screen.getByRole("button", { name: "Save" }));
expect(onAliasUpdate).toHaveBeenLastCalledWith({ "renamed-model": "gpt-4o" });
});
it("drops an alias through the row delete action", async () => {
const { onAliasUpdate, user } = renderInForm();
await addAlias(user, "fast-model", "gpt-4o");
await user.click(screen.getByRole("button", { name: "Delete fast-model" }));
expect(onAliasUpdate).toHaveBeenLastCalledWith({});
expect(screen.getByText("No aliases added yet. Add a new alias above.")).toBeInTheDocument();
});
it("does not submit the surrounding form when its own controls are used", async () => {
const { onSubmit, user } = renderInForm();
await user.click(screen.getByRole("button", { name: "Save team" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
await addAlias(user, "fast-model", "gpt-4o");
const row = screen.getByText("fast-model").closest("tr") as HTMLElement;
await user.click(within(row).getByRole("button", { name: "Edit fast-model" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
await user.click(screen.getByRole("button", { name: "Delete fast-model" }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useId } from "react";
import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline";
import { Button } from "@/components/ui/button";
import { Card, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table";
import ModelSelector from "./ModelSelector";
import { toast } from "@/lib/toast";
@ -27,6 +29,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
const [aliases, setAliases] = useState<AliasItem[]>([]);
const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" });
const [editingAlias, setEditingAlias] = useState<AliasItem | null>(null);
const aliasNameId = useId();
useEffect(() => {
// Convert object to array for display
@ -142,11 +145,14 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
return (
<div className="mt-4">
<div className="mb-6">
<p className="text-sm font-medium text-gray-700 mb-2">Add New Alias</p>
<p className="mb-2 text-sm font-medium text-foreground">Add New Alias</p>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">Alias Name</label>
<input
<label htmlFor={aliasNameId} className="mb-1 block text-xs text-muted-foreground">
Alias Name
</label>
<Input
id={aliasNameId}
type="text"
value={newAlias.aliasName}
onChange={(e) =>
@ -156,11 +162,10 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
})
}
placeholder="e.g., gpt-4o"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Target Model</label>
<label className="mb-1 block text-xs text-muted-foreground">Target Model</label>
<ModelSelector
accessToken={accessToken}
value={newAlias.targetModel}
@ -175,20 +180,16 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
/>
</div>
<div className="flex items-end">
<button
onClick={handleAddAlias}
disabled={!newAlias.aliasName || !newAlias.targetModel}
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700"}`}
>
<PlusCircleIcon className="w-4 h-4 mr-1" />
<Button onClick={handleAddAlias} disabled={!newAlias.aliasName || !newAlias.targetModel}>
<PlusCircleIcon className="mr-1 h-4 w-4" />
Add Alias
</button>
</Button>
</div>
</div>
</div>
<p className="text-sm font-medium text-gray-700 mb-2">Manage Existing Aliases</p>
<div className="rounded-lg custom-border relative mb-6">
<p className="mb-2 text-sm font-medium text-foreground">Manage Existing Aliases</p>
<div className="relative mb-6 rounded-lg border">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHeader>
@ -204,8 +205,9 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{editingAlias && editingAlias.id === alias.id ? (
<>
<TableCell className="py-0.5">
<input
<Input
type="text"
aria-label="Edit alias name"
value={editingAlias.aliasName}
onChange={(e) =>
setEditingAlias({
@ -213,7 +215,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
aliasName: e.target.value,
})
}
className="w-full px-2 py-1 border border-gray-300 rounded-md text-sm"
className="h-8"
/>
</TableCell>
<TableCell className="py-0.5">
@ -232,39 +234,37 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
onClick={handleUpdateAlias}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded-sm hover:bg-blue-100"
>
<Button variant="secondary" size="xs" onClick={handleUpdateAlias}>
Save
</button>
<button
onClick={handleCancelEdit}
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded-sm hover:bg-gray-100"
>
</Button>
<Button variant="outline" size="xs" onClick={handleCancelEdit}>
Cancel
</button>
</Button>
</div>
</TableCell>
</>
) : (
<>
<TableCell className="py-0.5 text-sm text-gray-900">{alias.aliasName}</TableCell>
<TableCell className="py-0.5 text-sm text-gray-500">{alias.targetModel}</TableCell>
<TableCell className="py-0.5 text-sm text-foreground">{alias.aliasName}</TableCell>
<TableCell className="py-0.5 text-sm text-muted-foreground">{alias.targetModel}</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
<Button
variant="secondary"
size="icon-xs"
aria-label={`Edit ${alias.aliasName}`}
onClick={() => handleEditAlias(alias)}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded-sm hover:bg-blue-100"
>
<PencilIcon className="w-3 h-3" />
</button>
<button
<PencilIcon className="h-3 w-3" />
</Button>
<Button
variant="destructive"
size="icon-xs"
aria-label={`Delete ${alias.aliasName}`}
onClick={() => deleteAlias(alias.id)}
className="text-xs bg-red-50 text-red-600 px-2 py-1 rounded-sm hover:bg-red-100"
>
<TrashIcon className="w-3 h-3" />
</button>
<TrashIcon className="h-3 w-3" />
</Button>
</div>
</TableCell>
</>
@ -273,7 +273,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
))}
{aliases.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
<TableCell colSpan={3} className="py-0.5 text-center text-sm text-muted-foreground">
No aliases added yet. Add a new alias above.
</TableCell>
</TableRow>
@ -287,12 +287,12 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{showExampleConfig && (
<Card className="px-6">
<CardTitle className="mb-4">Configuration Example</CardTitle>
<p className="text-gray-600 mb-4">Here&apos;s how your current aliases would look in the config:</p>
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-700">
<p className="mb-4 text-muted-foreground">Here&apos;s how your current aliases would look in the config:</p>
<div className="rounded-lg bg-muted p-4 font-mono text-sm">
<div className="text-foreground">
model_aliases:
{Object.keys(aliasObject).length === 0 ? (
<span className="text-gray-500">
<span className="text-muted-foreground">
<br />
&nbsp;&nbsp;# No aliases configured yet
</span>