From 3465ba4914858ab16f032c8d619ef21cb532bcdd Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 14 Aug 2026 03:52:41 -0700 Subject: [PATCH] refactor(ui): migrate router settings and shared badges off antd and tremor Replaces Ant Design and Tremor in the fallbacks views, the router general settings panel, and the two shared banner and badge components. - Tremor Card, Table and Icon become the ui/card, ui/table and lucide equivalents, reproducing Tremor's icon box so click targets keep their size - antd Alert becomes a composed role="alert" region, since the shadcn CLI's alert pulls in class-variance-authority, which this repo does not have - antd InputNumber becomes a native number input, and Switch onChange becomes onCheckedChange - shadcn TableCell ships whitespace-nowrap where Tremor's did not, so cells holding model names and setting descriptions get whitespace-normal back - adds a DeprecationBanner test covering naming, the link, and dismissal, proven against the antd version first and mutation checked - drops the eslint suppressions these files no longer need --- ui/litellm-dashboard/eslint-suppressions.json | 21 -- .../_components/general_settings.test.tsx | 6 +- .../_components/general_settings.tsx | 246 ++++++++++-------- .../src/components/BetaBadge.tsx | 11 +- .../src/components/DeprecationBanner.test.tsx | 44 ++++ .../src/components/DeprecationBanner.tsx | 61 +++-- .../Fallbacks/EditFallbacks.tsx | 15 +- .../RouterSettings/Fallbacks/Fallbacks.tsx | 115 ++++---- 8 files changed, 306 insertions(+), 213 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..927d0d2b08f 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1405,9 +1405,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 2 } @@ -1761,11 +1758,6 @@ "count": 1 } }, - "src/components/BetaBadge.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": { "no-restricted-imports": { "count": 1 @@ -1789,11 +1781,6 @@ "count": 1 } }, - "src/components/DeprecationBanner.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/EntityUsageExport/ExportSummary.tsx": { "no-restricted-imports": { "count": 1 @@ -1995,11 +1982,6 @@ "count": 1 } }, - "src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx": { "local/no-complex-jsx-arrow": { "count": 1 @@ -2017,9 +1999,6 @@ } }, "src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx": { - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 2 } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx index 9ffcbfc9975..0f3ba6c3471 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx @@ -62,6 +62,8 @@ const settingsRow = async (fieldName: string) => { return row as HTMLElement; }; +const numericValueIn = (row: HTMLElement) => Number((within(row).getByRole("spinbutton") as HTMLInputElement).value); + describe("GeneralSettings General tab", () => { beforeEach(() => { vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]); @@ -87,7 +89,7 @@ describe("GeneralSettings General tab", () => { await user.click(screen.getByText("General")); const row = await settingsRow("max_ui_session_budget"); - expect(within(row).getByRole("spinbutton")).toHaveValue("7.50"); + expect(numericValueIn(row)).toBe(7.5); const actionCell = row.querySelectorAll("td")[3]; const resetIcon = actionCell.querySelector("svg"); @@ -95,7 +97,7 @@ describe("GeneralSettings General tab", () => { await user.click(resetIcon as unknown as Element); expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget"); - expect(within(row).getByRole("spinbutton")).toHaveValue("1.00"); + expect(numericValueIn(row)).toBe(1); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx index ed7b17067d5..2a5b94b2fd7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx @@ -1,22 +1,14 @@ import React, { useState, useEffect } from "react"; -import { - Card, - Table, - TableHead, - TableRow, - TableHeaderCell, - TableCell, - TableBody, - Title, - Text, - Button, - Icon, - Switch, -} from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking"; -import { InputNumber, Select as AntdSelect } from "antd"; -import { TrashIcon } from "@heroicons/react/outline"; +import { Trash2 } from "lucide-react"; import { StatusBadge } from "@/components/shared/table_cells"; import RouterSettings from "@/components/router_settings"; @@ -44,16 +36,22 @@ export interface generalSettingsItem { field_default_value?: any; } +const NUMERIC_INPUT_WIDTH = "w-36"; + +const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw)); + const SettingValueEditor: React.FC<{ setting: generalSettingsItem; onChange: (fieldName: string, newValue: any) => void; }> = ({ setting, onChange }) => { if (setting.field_type === "Integer") { return ( - onChange(setting.field_name, newValue)} + className={NUMERIC_INPUT_WIDTH} + value={setting.field_value ?? ""} + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} /> ); } @@ -61,42 +59,55 @@ const SettingValueEditor: React.FC<{ return ( onChange(setting.field_name, checked)} + onCheckedChange={(checked) => onChange(setting.field_name, checked)} /> ); } if (setting.field_type === "Float") { return ( - onChange(setting.field_name, newValue)} + className={NUMERIC_INPUT_WIDTH} + value={setting.field_value ?? ""} + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} /> ); } if (setting.field_type === "Dollar") { return ( - onChange(setting.field_name, newValue)} - /> + + $ + onChange(setting.field_name, toNumericValue(event.target.value))} + /> + ); } if (setting.field_type === "Select") { return ( - ({ label: option, value: option }))} - onChange={(newValue) => onChange(setting.field_name, newValue ?? "")} - /> + ); } return null; @@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{ return ( - Prompt Caching + + Prompt Caching -
-
- Automatic Anthropic prompt caching -

{enableSetting.field_description}

-
- persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} /> -
- - {ttlSetting && (
-
- Cache lifetime (TTL) -

{ttlSetting.field_description}

+
+

Automatic Anthropic prompt caching

+

{enableSetting.field_description}

- ({ label: option, value: option }))} - onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")} - /> + persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
- )} + + {ttlSetting && ( +
+
+

Cache lifetime (TTL)

+

{ttlSetting.field_description}

+
+ +
+ )} + ); }; @@ -254,55 +275,60 @@ const GeneralSettings: React.FC = ({ accessToken, user - - - - Setting - Value - Status - Action - - - - {generalSettings - .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) - .map((value, index) => ( - - - {value.field_name} -

- {value.field_description} -

-
- - - - - {value.stored_in_db == true ? ( - - ) : value.stored_in_db == false ? ( - - ) : ( - - )} - - - - handleResetField(value.field_name)}> - Reset - - -
- ))} -
-
+ + + + + Setting + Value + Status + Action + + + + {generalSettings + .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) + .map((value, index) => ( + + +

{value.field_name}

+

+ {value.field_description} +

+
+ + + + + {value.stored_in_db == true ? ( + + ) : value.stored_in_db == false ? ( + + ) : ( + + )} + + + + handleResetField(value.field_name)} + className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500" + > + + + +
+ ))} +
+
+
diff --git a/ui/litellm-dashboard/src/components/BetaBadge.tsx b/ui/litellm-dashboard/src/components/BetaBadge.tsx index 7c4ef04417e..4e2195d1c36 100644 --- a/ui/litellm-dashboard/src/components/BetaBadge.tsx +++ b/ui/litellm-dashboard/src/components/BetaBadge.tsx @@ -1,4 +1,4 @@ -import { Badge } from "antd"; +import { Badge } from "@/components/ui/badge"; import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) { @@ -8,11 +8,14 @@ export default function BetaBadge({ children, dot = false }: { children?: React. return children ? <>{children} : null; } + const badge = dot ? : Beta; + return children ? ( - + {children} - + {badge} + ) : ( - + badge ); } diff --git a/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx b/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx new file mode 100644 index 00000000000..596ad2a1626 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx @@ -0,0 +1,44 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { DeprecationBanner } from "./DeprecationBanner"; + +describe("DeprecationBanner", () => { + it("names the deprecated feature in the heading and the body", () => { + render(); + + expect(screen.getByText("Memory is on a draft deprecation list")).toBeInTheDocument(); + expect(screen.getByText(/Memory is one of several experimental features/)).toBeInTheDocument(); + }); + + it("states the target removal date and that the list is not final", () => { + render(); + + expect(screen.getByText(/as early as September 1, 2026/)).toBeInTheDocument(); + expect(screen.getByText(/This list is a draft and is not final/)).toBeInTheDocument(); + }); + + it("links to the deprecation discussion in a new tab without leaking the opener", () => { + render(); + + const link = screen.getByRole("link", { name: "deprecation discussion" }); + expect(link).toHaveAttribute("href", "https://github.com/BerriAI/litellm/discussions/32090"); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noopener noreferrer"); + }); + + it("exposes a named close control", () => { + render(); + + expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); + }); + + it("hides the banner once the close control is used", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: /close/i })); + + expect(screen.queryByText("Memory is on a draft deprecation list")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/DeprecationBanner.tsx b/ui/litellm-dashboard/src/components/DeprecationBanner.tsx index 33c75ec22e3..9df34636f82 100644 --- a/ui/litellm-dashboard/src/components/DeprecationBanner.tsx +++ b/ui/litellm-dashboard/src/components/DeprecationBanner.tsx @@ -1,8 +1,8 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; -import { Alert } from "antd"; +import { Info, X } from "lucide-react"; const DEPRECATION_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/32090"; const DEPRECATION_TARGET_DATE = "September 1, 2026"; @@ -11,21 +11,42 @@ interface DeprecationBannerProps { featureName: string; } -export const DeprecationBanner: React.FC = ({ featureName }) => ( - - {`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `} - - deprecation discussion - - . - - } - type="info" - showIcon - closable - style={{ marginBottom: 16 }} - /> -); +export const DeprecationBanner: React.FC = ({ featureName }) => { + const [isClosed, setIsClosed] = useState(false); + + if (isClosed) { + return null; + } + + return ( +
+ +
+

{`${featureName} is on a draft deprecation list`}

+

+ {`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `} + + deprecation discussion + + . +

+
+ +
+ ); +}; diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx index 938e1104301..3efdcfd6b51 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx @@ -4,9 +4,9 @@ * Reuses FallbackGroupConfig with the primary model locked */ -import { Button } from "antd"; +import { Button } from "@/components/ui/button"; import { useQuery } from "@tanstack/react-query"; -import { Pencil } from "lucide-react"; +import { LoaderCircle, Pencil } from "lucide-react"; import React, { useMemo, useState } from "react"; import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; import NotificationManager from "../../../molecules/notifications_manager"; @@ -88,16 +88,11 @@ export default function EditFallbacks({ disablePrimaryModel />
- -
diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx index 4aa9fb15705..f82780f0c73 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx @@ -1,7 +1,7 @@ import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"; -import { ArrowRightIcon, PencilAltIcon, PlayIcon, TrashIcon } from "@heroicons/react/outline"; -import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react"; -import { Tooltip, Typography } from "antd"; +import { ArrowRight, Pencil, 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"; @@ -18,12 +18,14 @@ 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"; +const iconWrapperClass = "inline-flex shrink-0 items-center justify-center px-1.5 py-1.5"; + function renderModelNameCell(modelName: string, getProviderFromModel?: (modelName: string) => string): React.ReactNode { const provider = getProviderFromModel?.(modelName) ?? modelName; return ( - {modelName} + {modelName} ); } @@ -41,19 +43,23 @@ function renderFallbacksChain( return ( - {modelName} + {modelName} ); }; return ( - + {list.map((model, i) => ( - {i > 0 && } + {i > 0 && ( + + + + )} ))} @@ -248,7 +254,7 @@ const Fallbacks: React.FC = ({ accessToken, userRole, userID }) const canModify = isProxyAdminRole(userRole ?? ""); return ( - <> + {canModify && ( = ({ accessToken, userRole, userID }) )} {!hasFallbacks ? (
- + No fallbacks configured. Add fallbacks to automatically try another model when the primary fails. - +
) : ( - + - Model Name - Fallbacks - Actions + Model Name + Fallbacks + Actions - + {routerSettings["fallbacks"].map((item: FallbackEntry, index: number) => Object.entries(item).map(([key, value]) => ( - {renderModelNameCell(key, getProviderFromModel)} - + + {renderModelNameCell(key, getProviderFromModel)} + + {renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)} {canModify && ( <> - - testFallbackModelResponse(Object.keys(item)[0], accessToken || "")} - className="cursor-pointer hover:text-blue-600" - /> - - - handleEditClick(item)} - onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)} - className="cursor-pointer inline-flex" + + testFallbackModelResponse(Object.keys(item)[0], accessToken || "")} + className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`} + /> + } > - - + + + Test fallback - - handleDeleteClick(item)} - onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)} - className="cursor-pointer inline-flex" + + handleEditClick(item)} + onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)} + className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`} + /> + } > - - + + + Edit fallback + + + handleDeleteClick(item)} + onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)} + className={`${iconWrapperClass} cursor-pointer hover:text-red-600`} + /> + } + > + + + Delete fallback )} @@ -350,7 +373,7 @@ const Fallbacks: React.FC = ({ accessToken, userRole, userID }) onOk={handleDeleteConfirm} confirmLoading={isDeleting} /> - + ); };