From fc8a6b2a8d8537c3a564fa863a71f001855b5901 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 19 Aug 2026 17:25:09 -0700 Subject: [PATCH] refactor(ui): migrate shared primitives and common components off antd (#37521) * refactor(ui): migrate shared primitives and common components off antd Adds the success variant to the shared Alert plus success, warning and info variants to Badge, introduces UtcDateTimeInput to replace antd's DatePicker, and converts the common components and key/team helpers onto the shadcn primitives. * fix(ui): keep MultiSelect and budget input faithful to their antd behaviour Restore the clear-all control MultiSelect lost, split comma-separated custom entries into one value per token, and stop rounding the budget input on every keystroke so a fractional amount survives typing. * test(ui): drive the access group picker through the migrated MultiSelect AccessGroupSelector no longer renders an antd Select, so the placeholder is an input label rather than a text node and the popup inerts the page until it closes. --- ui/litellm-dashboard/eslint-suppressions.json | 58 +------------- .../components/UsagePageView.test.tsx | 4 +- .../common_components/AccessGroupSelector.tsx | 55 ++++--------- .../RateLimitTypeFormItem.test.tsx | 34 ++------ .../check_openapi_schema.tsx | 50 ++++++++---- .../user_search_modal.test.tsx | 2 +- .../BudgetFallbacksEditor.tsx | 46 ++++------- .../key_team_helpers/BudgetWindowsEditor.tsx | 53 ++++++++---- .../create_key_button.integration.test.tsx | 9 +-- .../src/components/shared/Alert.test.tsx | 80 +++++++++++++++++++ .../src/components/shared/Alert.tsx | 11 ++- .../components/shared/MultiSelect.test.tsx | 25 ++++++ .../src/components/shared/MultiSelect.tsx | 23 +++++- .../shared/PaginatedSearchSelect.tsx | 4 +- .../src/components/shared/SearchSelect.tsx | 7 ++ .../src/components/ui/badge.tsx | 4 +- .../src/contexts/AntdGlobalProvider.tsx | 7 +- 17 files changed, 266 insertions(+), 206 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/shared/Alert.test.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 31e8f81f3b3..d625cca0c60 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -542,11 +542,6 @@ "count": 1 } }, - "src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx": { "no-restricted-imports": { "count": 1 @@ -647,9 +642,6 @@ "src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx": { "no-nested-ternary": { "count": 2 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/mcp-servers/_components/index.tsx": { @@ -661,9 +653,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/static-components": { "count": 4 } @@ -1475,11 +1464,6 @@ "count": 1 } }, - "src/components/Settings/AdminSettings/LoggingSettings/LoggingSettings.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx": { "no-nested-ternary": { "count": 1 @@ -1506,11 +1490,6 @@ "count": 1 } }, - "src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx": { "react-hooks/set-state-in-render": { "count": 1 @@ -1590,9 +1569,6 @@ "src/components/VirtualKeysPage/keyTableColumns.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/activity_metrics.tsx": { @@ -1603,11 +1579,6 @@ "count": 1 } }, - "src/components/add_model/AdaptiveRoutingConfig.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/add_model/AddModelForm.test.tsx": { "no-restricted-imports": { "count": 1 @@ -1620,26 +1591,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - } - }, - "src/components/add_model/ClassificationMethodConfig.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/add_model/ComplexityRouterConfig.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/add_model/EscalationKeywords.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/add_model/KeywordTierRules.tsx": { "no-restricted-imports": { "count": 1 } @@ -1649,11 +1600,6 @@ "count": 1 } }, - "src/components/add_model/SemanticKeywordMatching.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/add_model/add_auto_router_tab.tsx": { "local/filename-pascal-case": { "count": 1 @@ -1672,7 +1618,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 1 } }, "src/components/add_model/auto_router_connection_test.tsx": { @@ -2078,7 +2024,7 @@ }, "src/components/model_add/CredentialModal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/model_add/reuse_credentials.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index a6f46ee8242..118371aa9ac 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -971,8 +971,8 @@ describe("UsagePage", () => { expect(mockUserDailyActivityCall).toHaveBeenCalled(); }); - // Should still render the data from the paginated fallback - expect(screen.getByText("1,500")).toBeInTheDocument(); + // Should still render the data from the paginated fallback, which lands a render after the call + expect(await screen.findByText("1,500")).toBeInTheDocument(); }); it("should stop showing the previous range's paginated pages while a new range is in flight", async () => { diff --git a/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx b/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx index 96f21f30385..e522e7c7e62 100644 --- a/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { Select, Skeleton } from "antd"; import { Users } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MultiSelect, type MultiSelectOption } from "@/components/shared/MultiSelect"; import { useAccessGroups, AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups"; export interface AccessGroupSelectorProps { @@ -12,8 +13,6 @@ export interface AccessGroupSelectorProps { className?: string; showLabel?: boolean; labelText?: string; - /** Allow clearing the selection */ - allowClear?: boolean; } /** @@ -32,7 +31,6 @@ const AccessGroupSelector: React.FC = ({ className, showLabel = false, labelText = "Access Group", - allowClear = true, }) => { const { data: accessGroups, isLoading, isError } = useAccessGroups(); @@ -45,22 +43,16 @@ const AccessGroupSelector: React.FC = ({ {labelText}

)} - + ); } // ── Build options ──────────────────────────────────────────────────────── - const options = (accessGroups ?? []).map((group: AccessGroupResponse) => ({ - label: ( - - {group.access_group_name}{" "} - ({group.access_group_id}) - - ), + const options: MultiSelectOption[] = (accessGroups ?? []).map((group: AccessGroupResponse) => ({ + label: group.access_group_name, value: group.access_group_id, - selectedLabel: group.access_group_name, - searchText: `${group.access_group_name} ${group.access_group_id}`, + description: group.access_group_id, })); // ── Render ─────────────────────────────────────────────────────────────── @@ -71,30 +63,17 @@ const AccessGroupSelector: React.FC = ({ {labelText}

)} - - {property.enum.map((value) => ( - - {value} - - ))} + ); } if (type === "number" || type === "integer") { return ( - control.onChange(toSchemaNumber(event.target.value, type === "integer"))} + className="w-full" /> ); } diff --git a/ui/litellm-dashboard/src/components/common_components/user_search_modal.test.tsx b/ui/litellm-dashboard/src/components/common_components/user_search_modal.test.tsx index cf4461a2b7a..e320a90f01a 100644 --- a/ui/litellm-dashboard/src/components/common_components/user_search_modal.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/user_search_modal.test.tsx @@ -74,7 +74,7 @@ describe("UserSearchModal", () => { expect(notice).toHaveTextContent(/users that already exist/i); expect(notice).toHaveTextContent(/ask a proxy admin to create their account first/i); // info, not warning: a warning here would read as an error state on an empty form - expect(notice.className).toMatch(/text-info/); + expect(notice).toHaveAttribute("data-variant", "info"); }); }); diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetFallbacksEditor.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetFallbacksEditor.tsx index 1d327b87346..4e1c17a4c68 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetFallbacksEditor.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetFallbacksEditor.tsx @@ -1,5 +1,5 @@ -import { SimpleTooltip } from "@/components/ui/tooltip"; -import { Select } from "antd"; +import { MultiSelect } from "@/components/shared/MultiSelect"; +import { SearchSelect } from "@/components/shared/SearchSelect"; import { Button } from "@/components/ui/button"; import { ArrowDown, Plus, X } from "lucide-react"; import React, { useState } from "react"; @@ -94,18 +94,15 @@ export function BudgetFallbacksEditor({ value, onChange, availableModels }: Budg
- updateEntry(entry.id, { fallbackModels: values })} - disabled={!entry.primaryModel} - showSearch - filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())} + ({ label: m, value: m }))} - getPopupContainer={(trigger) => trigger.parentElement || document.body} - maxTagCount="responsive" - maxTagPlaceholder={(omittedValues) => ( - v).join(", ")}> - +{omittedValues.length} more - - )} + value={entry.fallbackModels} + onValueChange={(values) => updateEntry(entry.id, { fallbackModels: values })} + placeholder={entry.primaryModel ? "Select fallback models" : "Select a primary model first"} + emptyText="No models found" + disabled={!entry.primaryModel} + className="w-full" /> {entry.fallbackModels.length > 1 && (
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx index c9a422dcb85..43861d43c0d 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx @@ -1,5 +1,6 @@ -import { InputNumber, Select } from "antd"; import { Button } from "@/components/ui/button"; +import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import React from "react"; export interface BudgetWindowEntry { @@ -41,21 +42,43 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
+ + + $ + + { + const typed = event.target.valueAsNumber; + updateWindow(idx, "max_budget", Number.isNaN(typed) ? null : typed); + }} + onBlur={(event) => { + const typed = event.target.valueAsNumber; + if (!Number.isNaN(typed)) { + updateWindow(idx, "max_budget", Number(typed.toFixed(2))); + } + }} + placeholder="Max spend ($)" + /> +