From eef41c9987909504cca90607bd5fc7ad0aea7327 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 18 Aug 2026 15:36:35 -0700 Subject: [PATCH] refactor(ui): move the admin, SSO, SCIM, alerting and fallback forms off tremor (#37315) * refactor(ui): move the admin, SSO, SCIM, alerting and fallback forms off tremor Swaps the tremor Button, Card, Callout, Grid, Divider, Text, Title, TextInput, Table parts, Badge, Icon and Switch in these nine files for the shadcn layer and lucide icons, keeping antd in place. The SCIM create-token button keeps an explicit type="submit"; the two SCIM copy buttons sit outside the antd form so they stay plain buttons, and the alerting form's Enterprise Feature upsell button is a link wrapper rather than a save action, so it deliberately stays type="button" while the form keeps its own Update Settings submit. The teal login callout on the admin panel maps to the info Alert variant since no teal variant exists. Prunes the nine tremor no-restricted-imports suppressions these files no longer need. * fix(ui): keep the alerting settings name column left aligned tremor's TableCell hardcoded text-left, so the align="center" attribute never took effect. The shadcn cell has no text-align of its own, so translating that attribute into text-center would have centered the field name and its description for the first time. * fix(ui): restore the CloudZero key reveal toggle and the SCIM divider gap tremor's TextInput drew its own show/hide button whenever the type was password and the field was not disabled, so the straight pass-through to the plain shadcn Input silently deleted that affordance from the CloudZero API key field. Rebuilds it with the InputGroup reveal pattern that email_settings.tsx already uses, behind a small local control so the antd Form.Item keeps its id, value and onChange wiring and the field still matches its shadcn sibling in the same form. Also puts the SCIM separator back on my-6: tremor's Divider was "w-full mx-auto my-6", and the conversion shipped my-4, tightening that gap by 8px on each side. The disabled SCIM token field stays a bare Input because tremor suppressed its toggle when disabled too. --- ui/litellm-dashboard/eslint-suppressions.json | 23 +- .../admin-panel/_components/AdminPanel.tsx | 51 ++--- ui/litellm-dashboard/src/components/SCIM.tsx | 215 +++++++++--------- .../src/components/SSOModals.tsx | 9 +- .../Fallbacks/FallbackSelectionForm.tsx | 5 +- .../dynamic_form.integration.test.tsx | 4 +- .../src/components/alerting/dynamic_form.tsx | 39 +++- .../src/components/cloudzero_export_modal.tsx | 59 +++-- .../src/components/onboarding_link.tsx | 11 +- 9 files changed, 205 insertions(+), 211 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index e467ddc5ec4..bc23c2433d3 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -16,7 +16,7 @@ }, "src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 @@ -1647,9 +1647,6 @@ } }, "src/components/SCIM.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -1661,7 +1658,7 @@ }, "src/components/SSOModals.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx": { @@ -1705,11 +1702,6 @@ "count": 1 } }, - "src/components/Settings/AdminSettings/SSOSettings/Modals/BaseSSOSettingsForm.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "src/components/Settings/AdminSettings/SSOSettings/Modals/EditSSOSettingsModal.test.tsx": { "max-nested-callbacks": { "count": 1 @@ -1760,7 +1752,7 @@ }, "src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "react-hooks/set-state-in-effect": { "count": 1 @@ -2024,10 +2016,7 @@ "count": 1 }, "no-nested-ternary": { - "count": 4 - }, - "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/bulk_create_users_button.tsx": { @@ -2091,7 +2080,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "no-restricted-syntax": { "count": 3 @@ -2461,7 +2450,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/organisms/RegenerateKeyModal.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx index 3f3f7fb5db7..48eaa62e37b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/admin-panel/_components/AdminPanel.tsx @@ -3,18 +3,12 @@ * Use this to avoid sharing master key with others */ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { - Button, - Callout, - Card, - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, -} from "@tremor/react"; -import { Alert, Modal, Space, Tabs, Typography } from "antd"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Alert as AntdAlert, Modal, Space, Tabs, Typography } from "antd"; +import { Info } from "lucide-react"; import React, { useEffect, useState } from "react"; import NewBadge from "@/components/common_components/NewBadge"; import { useBaseUrl } from "@/components/constants"; @@ -37,7 +31,6 @@ import UIAccessControlForm from "@/components/UIAccessControlForm"; import { z } from "zod/v4"; import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; -import { Button as ShadcnButton } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useZodForm } from "@/lib/forms/useZodForm"; @@ -59,7 +52,7 @@ const AddAllowedIPForm = ({ onSubmit }: { onSubmit: (values: AllowedIPFormValues {({ ref, ...field }) => }
- Add IP Address +
@@ -228,9 +221,9 @@ const AdminPanel: React.FC = ({ proxySettings }) => { label: "Security Settings", children: ( <> - + ✨ Security Settings - = ({ proxySettings }) => { ]} > - + - IP Address - Action + IP Address + Action - + {allowedIPs.map((ip, index) => ( {ip} {ip !== all_ip_address_allowed && ( - )} @@ -365,12 +358,16 @@ const AdminPanel: React.FC = ({ proxySettings }) => { /> - - If you need to login without sso, you can access{" "} - - {nonSssoUrl}{" "} - - + + + Login without SSO + + If you need to login without sso, you can access{" "} + + {nonSssoUrl}{" "} + + + ), }, diff --git a/ui/litellm-dashboard/src/components/SCIM.tsx b/ui/litellm-dashboard/src/components/SCIM.tsx index 4110db4daba..6663ab0aed0 100644 --- a/ui/litellm-dashboard/src/components/SCIM.tsx +++ b/ui/litellm-dashboard/src/components/SCIM.tsx @@ -1,21 +1,17 @@ import React, { useState, useEffect } from "react"; -import { Card, Title, Text, Grid, Callout, Divider } from "@tremor/react"; import { z } from "zod/v4"; import { keyCreateCall } from "./networking"; import { CopyToClipboard } from "react-copy-to-clipboard"; -import { - LinkOutlined, - KeyOutlined, - CopyOutlined, - ExclamationCircleOutlined, - PlusCircleOutlined, -} from "@ant-design/icons"; +import { CircleAlert, CirclePlus, Copy, Info, KeyRound, Link } from "lucide-react"; import { parseErrorMessage } from "./shared/errorUtils"; import { toast } from "@/lib/toast"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { useZodForm } from "@/lib/forms/useZodForm"; @@ -80,114 +76,121 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti }; return ( - +
-
- SCIM Configuration -
- - System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and - groups in LiteLLM. - - - - -
- {/* Step 1: SCIM URL */} -
-
-
- 1 -
- - <LinkOutlined className="h-5 w-5 mr-2" /> - SCIM Tenant URL - -
- - Use this URL in your identity provider SCIM integration settings. - -
- - toast.success("URL copied to clipboard")}> - - -
+ +
+ SCIM Configuration
+

+ System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users + and groups in LiteLLM. +

- {/* Step 2: SCIM Token */} -
-
-
- 2 + + +
+ {/* Step 1: SCIM URL */} +
+
+
+ 1 +
+

+ + SCIM Tenant URL +

+
+

+ Use this URL in your identity provider SCIM integration settings. +

+
+ + toast.success("URL copied to clipboard")}> + +
- - <KeyOutlined className="h-5 w-5 mr-2" /> - Authentication Token -
- - You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider - configuration. - + {/* Step 2: SCIM Token */} +
+
+
+ 2 +
+

+ + Authentication Token +

+
- {!tokenData ? ( -
-
- - - {({ ref, ...field }) => } - -
- +
+
+ +
+ ) : ( + +
+ +

Your SCIM Token

+
+

+ Make sure to copy this token now. You will not be able to see it again. +

+
+ + toast.success("Token copied to clipboard")}> + -
- - -
- ) : ( - -
- - Your SCIM Token -
- - Make sure to copy this token now. You will not be able to see it again. - -
- - toast.success("Token copied to clipboard")}> - - -
- -
- )} + +
+ + + )} +
-
+
- +
); }; diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 4a156c3b30a..2bcc7849449 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { FormProvider, useWatch, type UseFormReturn } from "react-hook-form"; import { Modal } from "antd"; -import { Text } from "@tremor/react"; import { getSSOSettings, updateSSOSettings } from "./networking"; import { toast } from "@/lib/toast"; import { parseErrorMessage } from "./shared/errorUtils"; @@ -317,10 +316,10 @@ const SSOModals: React.FC = ({ onCancel={handleInstructionsCancel} >

Follow these steps to complete the SSO setup:

- 1. DO NOT Exit this TAB - 2. Open a new tab, visit your proxy base url - 3. Confirm your SSO is configured correctly and you can login on the new Tab - 4. If Step 3 is successful, you can close this tab +

1. DO NOT Exit this TAB

+

2. Open a new tab, visit your proxy base url

+

3. Confirm your SSO is configured correctly and you can login on the new Tab

+

4. If Step 3 is successful, you can close this tab

diff --git a/ui/litellm-dashboard/src/components/alerting/dynamic_form.integration.test.tsx b/ui/litellm-dashboard/src/components/alerting/dynamic_form.integration.test.tsx index 89eee83374a..f9b9765f59e 100644 --- a/ui/litellm-dashboard/src/components/alerting/dynamic_form.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/alerting/dynamic_form.integration.test.tsx @@ -174,9 +174,7 @@ describe("DynamicForm change notifications", () => { const user = userEvent.setup(); const { handleResetField } = renderForm(); - const row = screen.getByText("region_name").closest("tr"); - const reset = row?.querySelector(".tremor-Icon-root"); - await user.click(reset as Element); + await user.click(screen.getByRole("button", { name: "Reset region_name" })); expect(handleResetField).toHaveBeenCalledWith("region_name", 1); }); diff --git a/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx b/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx index 1ee3833c1b9..0b7924d5599 100644 --- a/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx +++ b/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx @@ -1,8 +1,11 @@ import React from "react"; import { useForm } from "react-hook-form"; -import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline"; -import { Button, Badge, Icon, Text, TableRow, TableCell, Switch } from "@tremor/react"; +import { CircleCheck, Trash2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { TableCell, TableRow } from "@/components/ui/table"; interface AlertingSetting { field_name: string; @@ -71,7 +74,13 @@ const DynamicForm: React.FC = ({ ); } if (setting.field_type === "Boolean") { - return handleToggle(setting, checked)} />; + return ( + handleToggle(setting, checked)} + /> + ); } return handleTextChange(setting, event)} />; }; @@ -80,8 +89,8 @@ const DynamicForm: React.FC = ({
{alertingSettings.map((value, index) => ( - - {value.field_name} + +

{value.field_name}

{value.field_description}

{value.premium_field && !premiumUser ? ( @@ -97,19 +106,27 @@ const DynamicForm: React.FC = ({ )} {value.stored_in_db == true ? ( - + + In DB ) : value.stored_in_db == false ? ( - In Config + In Config ) : ( - Not Set + Not Set )} - handleResetField(value.field_name, index)}> - Reset - +
))} diff --git a/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx b/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx index 7a937d32b3e..24aa0d9d1b8 100644 --- a/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx +++ b/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx @@ -1,13 +1,16 @@ import React, { useState, useEffect } from "react"; -import { Text, Button, Callout } from "@tremor/react"; import { Modal, Spin, Select } from "antd"; +import { CircleCheck, FileDown } from "lucide-react"; import { z } from "zod/v4"; import { getGlobalLitellmHeaderName } from "@/components/networking"; import { toast } from "@/lib/toast"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { PasswordInput } from "@/components/shared/PasswordInput"; import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { useZodForm } from "@/lib/forms/useZodForm"; const cloudZeroSettingsSchema = z.object({ @@ -242,7 +245,7 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC
{/* Export Type Selection */}
- Export Destination +

Export Destination