mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
commit
673f036680
5 changed files with 190 additions and 54 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 ?? "";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'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'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 />
|
||||
# No aliases configured yet
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue