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}
)}
-