mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate Settings/RouterSettings/Fallbacks/Fallbacks.tsx to shadcn
- @tremor Icon/Table*/+ antd Tooltip/Typography + @heroicons ArrowRightIcon/PlayIcon/TrashIcon \u2192 shadcn Table/Tooltip + lucide ArrowRight/Play/Trash2. - Action buttons (Test fallback, Delete fallback) are now real <button>s with semantic aria-labels and shadcn Tooltip wrappers. - Empty-state uses semantic muted/border tokens. Test updates: - Fallbacks.test.tsx: `name: /delete/i` \u2192 `name: 'Delete'` (exact match) so the modal's confirm button doesn't collide with the row's 'Delete fallback' aria-label. - FallbackSelectionForm.test.tsx: assert against the new '+ Add group' Button + the inline X button per tab (aria-label='Remove <label>') instead of antd's editable-card 'add tab' / 'remove' tab roles. Gates: TS \u2713 | Vitest 42/42 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
f474bde283
commit
6451e87872
3 changed files with 101 additions and 64 deletions
|
|
@ -109,7 +109,7 @@ describe("FallbackSelectionForm", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
const addTabButton = screen.getByRole("button", { name: /add tab/i });
|
||||
const addTabButton = screen.getByRole("button", { name: /add group/i });
|
||||
await user.click(addTabButton);
|
||||
|
||||
expect(mockOnGroupsChange).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -138,7 +138,10 @@ describe("FallbackSelectionForm", () => {
|
|||
maxGroups={5}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: /add tab/i })).not.toBeInTheDocument();
|
||||
// Post phase-1: 'add tab' is now an explicit '+ Add group' Button.
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /add group/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show add tab button when below maxGroups with custom maxGroups", () => {
|
||||
|
|
@ -153,12 +156,13 @@ describe("FallbackSelectionForm", () => {
|
|||
maxGroups={3}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add tab/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /add group/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onGroupsChange when a group is removed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const antd = await import("antd");
|
||||
const groups: FallbackGroup[] = [
|
||||
{ id: "1", primaryModel: "gpt-4", fallbackModels: [] },
|
||||
{ id: "2", primaryModel: "gpt-3.5-turbo", fallbackModels: [] },
|
||||
|
|
@ -171,14 +175,15 @@ describe("FallbackSelectionForm", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
const removeButtons = screen.getAllByRole("tab", { name: "remove" });
|
||||
await user.click(removeButtons[0]);
|
||||
// Post phase-1: each TabsTrigger has an inline X button with
|
||||
// aria-label="Remove <label>".
|
||||
const removeFirst = screen.getByLabelText("Remove gpt-4");
|
||||
await user.click(removeFirst);
|
||||
|
||||
expect(mockOnGroupsChange).toHaveBeenCalledTimes(1);
|
||||
const [newGroups] = mockOnGroupsChange.mock.calls[0];
|
||||
expect(newGroups).toHaveLength(1);
|
||||
expect(newGroups[0].id).toBe("2");
|
||||
expect(antd.message.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render FallbackGroupConfig for each group", () => {
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ describe("Fallbacks", () => {
|
|||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -256,7 +256,7 @@ describe("Fallbacks", () => {
|
|||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -283,7 +283,7 @@ describe("Fallbacks", () => {
|
|||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
|
||||
import { ArrowRightIcon, PlayIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
|
||||
import { Tooltip, Typography } from "antd";
|
||||
import { ArrowRight, Play, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import openai from "openai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DeleteResourceModal from "../../../common_components/DeleteResourceModal";
|
||||
|
|
@ -14,7 +26,7 @@ type FallbackEntry = { [modelName: string]: string[] };
|
|||
type Fallbacks = FallbackEntry[];
|
||||
|
||||
const modelCardClass =
|
||||
"inline-flex items-center gap-2 px-2.5 py-1 rounded-md border border-gray-200 bg-gray-50 text-sm font-medium text-gray-800 shrink-0";
|
||||
"inline-flex items-center gap-2 px-2.5 py-1 rounded-md border border-border bg-muted text-sm font-medium text-foreground shrink-0";
|
||||
|
||||
function renderModelNameCell(
|
||||
modelName: string,
|
||||
|
|
@ -49,16 +61,16 @@ function renderFallbacksChain(
|
|||
return (
|
||||
<span className="grid grid-cols-[auto_1fr] items-start gap-x-2 w-full min-w-0">
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-8 h-8 shrink-0 self-start text-blue-600"
|
||||
className="inline-flex items-center justify-center w-8 h-8 shrink-0 self-start text-primary"
|
||||
aria-hidden
|
||||
>
|
||||
<ArrowRightIcon className="w-5 h-5 stroke-[2.5]" />
|
||||
<ArrowRight className="w-5 h-5 stroke-[2.5]" />
|
||||
</span>
|
||||
<span className="flex flex-wrap items-start gap-1 min-w-0">
|
||||
{list.map((model, i) => (
|
||||
<React.Fragment key={model}>
|
||||
{i > 0 && (
|
||||
<Icon icon={ArrowRightIcon} size="xs" className="shrink-0 text-gray-400" />
|
||||
<ArrowRight className="w-3 h-3 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<ChainCard modelName={model} />
|
||||
</React.Fragment>
|
||||
|
|
@ -253,60 +265,80 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
onChange={handleFallbacksChange}
|
||||
/>
|
||||
{!hasFallbacks ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center">
|
||||
<Typography.Text type="secondary">
|
||||
No fallbacks configured. Add fallbacks to automatically try another model when the primary
|
||||
fails.
|
||||
</Typography.Text>
|
||||
<div className="rounded-lg border border-border bg-muted px-4 py-6 text-center">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
No fallbacks configured. Add fallbacks to automatically try another
|
||||
model when the primary fails.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model Name</TableHeaderCell>
|
||||
<TableHeaderCell>Fallbacks</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
<TableHead>Model Name</TableHead>
|
||||
<TableHead>Fallbacks</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{routerSettings["fallbacks"].map((item: FallbackEntry, index: number) =>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<TableRow key={index.toString() + key}>
|
||||
<TableCell className="align-top">
|
||||
{renderModelNameCell(key, getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
{renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<Tooltip title="Test fallback">
|
||||
<Icon
|
||||
icon={PlayIcon}
|
||||
size="sm"
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete fallback">
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
|
||||
className="cursor-pointer inline-flex"
|
||||
>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
className="hover:text-red-600"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
{routerSettings["fallbacks"].map(
|
||||
(item: FallbackEntry, index: number) =>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<TableRow key={index.toString() + key}>
|
||||
<TableCell className="align-top">
|
||||
{renderModelNameCell(key, getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
{renderFallbacksChain(
|
||||
key,
|
||||
Array.isArray(value) ? value : [],
|
||||
getProviderFromModel,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
testFallbackModelResponse(
|
||||
Object.keys(item)[0],
|
||||
accessToken || "",
|
||||
)
|
||||
}
|
||||
className="cursor-pointer hover:text-primary inline-flex p-1"
|
||||
aria-label="Test fallback"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Test fallback</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="delete-fallback-button"
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" && handleDeleteClick(item)
|
||||
}
|
||||
className="cursor-pointer inline-flex p-1 hover:text-destructive"
|
||||
aria-label="Delete fallback"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete fallback</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue