diff --git a/ui/litellm-dashboard/docs/BLOCKERS.md b/ui/litellm-dashboard/docs/BLOCKERS.md index bf418564c7e..c098fa20c91 100644 --- a/ui/litellm-dashboard/docs/BLOCKERS.md +++ b/ui/litellm-dashboard/docs/BLOCKERS.md @@ -31,6 +31,15 @@ stay on antd for phase 1 and will be addressed in a targeted follow-up. with Switch/InputNumber/Select controls. Defer until shared section unblocks. - `src/components/add_pass_through.tsx`: same dependency chain. +- `src/components/settings.tsx` (840 LoC): logging-callbacks settings page. + Two separate antd `Form`s (`addForm`, `editForm`) with `Form.useForm`, + `validateFields`, `setFieldsValue`, `resetFields`, plus Tremor + `TabGroup`/`TabList`/`Tab`/`TabPanels`/`TabPanel` and Tremor `Table` + containers, dynamic provider-specific field rendering via `DynamicParamsFields` + (which imports `antd/es/form/FormItem` directly and registers under the + parent antd Form). A faithful migration needs both forms rewritten to RHF, + the tabs flipped to shadcn, and `DynamicParamsFields` ported in lockstep + with its two parents — exceeds the 2-attempt budget. - `src/components/OldTeams.tsx` (1577 LoC): legacy teams page that mirrors `team/TeamInfo.tsx` in structure — deeply coupled antd `Form` with model / guardrail multi-selects, `Form.List`-style panels, antd `Pagination` + `Table` diff --git a/ui/litellm-dashboard/src/components/AdminPanel.tsx b/ui/litellm-dashboard/src/components/AdminPanel.tsx index ee2ded4fa0f..2eda0332c25 100644 --- a/ui/litellm-dashboard/src/components/AdminPanel.tsx +++ b/ui/litellm-dashboard/src/components/AdminPanel.tsx @@ -3,20 +3,33 @@ * Use this to avoid sharing master key with others */ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports +import React, { useEffect, useRef, useState } from "react"; +import { AlertTriangle, LogIn } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; import { - Button, - Callout, - Card, Table, TableBody, TableCell, TableHead, - TableHeaderCell, + TableHeader, TableRow, -} from "@tremor/react"; -import { Alert, Button as Button2, Form, Input, Modal, Space, Tabs, Typography } from "antd"; -import React, { useEffect, useState } from "react"; +} from "@/components/ui/table"; import NewBadge from "./common_components/NewBadge"; import { useBaseUrl } from "./constants"; import NotificationsManager from "./molecules/notifications_manager"; @@ -28,15 +41,20 @@ import HashicorpVault from "./Settings/AdminSettings/HashicorpVault/HashicorpVau import SSOModals from "./SSOModals"; import UIAccessControlForm from "./UIAccessControlForm"; -const { Title, Paragraph, Text } = Typography; - interface AdminPanelProps { proxySettings?: any; } const AdminPanel: React.FC = ({ proxySettings }) => { const { premiumUser, accessToken, userId: userID } = useAuthorized(); - const [form] = Form.useForm(); + // Legacy antd `Form` handle — retained as a no-op bridge so the + // SSOModals component (which still accepts a `form` prop for back-compat) + // has something to call `resetFields` on. + const ssoFormHandle = useRef({ + resetFields: () => {}, + setFieldsValue: () => {}, + getFieldsValue: () => ({}), + }); const [isAddSSOModalVisible, setIsAddSSOModalVisible] = useState(false); const [isInstructionsModalVisible, setIsInstructionsModalVisible] = useState(false); const [isAllowedIPModalVisible, setIsAllowedIPModalVisible] = useState(false); @@ -46,6 +64,7 @@ const AdminPanel: React.FC = ({ proxySettings }) => { const [allowedIPs, setAllowedIPs] = useState([]); const [ipToDelete, setIPToDelete] = useState(null); const [ssoConfigured, setSsoConfigured] = useState(false); + const [newIP, setNewIP] = useState(""); const baseUrl = useBaseUrl(); const all_ip_address_allowed = "All IP Addresses Allowed"; @@ -99,14 +118,19 @@ const AdminPanel: React.FC = ({ proxySettings }) => { } }; - const handleAddIP = async (values: { ip: string }) => { + const handleAddIP = async (e?: React.FormEvent) => { + e?.preventDefault(); + if (!newIP.trim()) { + NotificationsManager.fromBackend("Please enter an IP address"); + return; + } try { if (accessToken) { - await addAllowedIP(accessToken, values.ip); - // Fetch the updated list of IPs + await addAllowedIP(accessToken, newIP.trim()); const updatedIPs = await getAllowedIPs(accessToken); setAllowedIPs(updatedIPs); NotificationsManager.success("IP address added successfully"); + setNewIP(""); } } catch (error) { console.error("Error adding IP:", error); @@ -125,7 +149,6 @@ const AdminPanel: React.FC = ({ proxySettings }) => { if (ipToDelete && accessToken) { try { await deleteAllowedIP(accessToken, ipToDelete); - // Fetch the updated list of IPs const updatedIPs = await getAllowedIPs(accessToken); setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : [all_ip_address_allowed]); NotificationsManager.success("IP address deleted successfully"); @@ -141,7 +164,7 @@ const AdminPanel: React.FC = ({ proxySettings }) => { const handleAddSSOOk = () => { setIsAddSSOModalVisible(false); - form.resetFields(); + ssoFormHandle.current.resetFields(); if (accessToken && premiumUser) { checkSSOConfiguration(); } @@ -149,10 +172,10 @@ const AdminPanel: React.FC = ({ proxySettings }) => { const handleAddSSOCancel = () => { setIsAddSSOModalVisible(false); - form.resetFields(); + ssoFormHandle.current.resetFields(); }; - const handleShowInstructions = (formValues: Record) => { + const handleShowInstructions = (_formValues: Record) => { setIsAddSSOModalVisible(false); setIsInstructionsModalVisible(true); }; @@ -173,7 +196,8 @@ const AdminPanel: React.FC = ({ proxySettings }) => { useEffect(() => { checkSSOConfiguration(); - }, [accessToken, premiumUser, checkSSOConfiguration]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accessToken, premiumUser]); const handleUIAccessControlOk = () => { setIsUIAccessControlModalVisible(false); @@ -183,47 +207,55 @@ const AdminPanel: React.FC = ({ proxySettings }) => { setIsUIAccessControlModalVisible(false); }; - const tabItems = [ - { - key: "sso-settings", - label: "SSO Settings", - children: , - }, - { - key: "security-settings", - label: "Security Settings", - children: ( - <> - - ✨ Security Settings - -
+ return ( +
+

Admin Access

+

+ Go to 'Internal Users' page to add other admins. +

+ + + SSO Settings + Security Settings + SCIM + + + UI Settings + + + + Hashicorp Vault + + + + + + + + +

✨ Security Settings

+ + + SSO Configuration Deprecated + + Editing SSO Settings on this page is deprecated and will be removed in a future + version. Please use the SSO Settings tab for SSO configuration. + + +
-
-
, - , - ]} + onOpenChange={(o) => { + if (!o) setIsAllowedIPModalVisible(false); + }} > - - - - IP Address - Action - - - - {allowedIPs.map((ip, index) => ( - - {ip} - - {ip !== all_ip_address_allowed && ( - - )} - + + + Manage Allowed IP Addresses + +
+ + + IP Address + Action - ))} - -
- + + + {allowedIPs.map((ip, index) => ( + + {ip} + + {ip !== all_ip_address_allowed && ( + + )} + + + ))} + + + + + + + + - setIsAddIPModalVisible(false)} - footer={null} + onOpenChange={(o) => { + if (!o) setIsAddIPModalVisible(false); + }} > -
- - - - - Add IP Address - -
-
+ + + Add Allowed IP Address + +
+ setNewIP(e.target.value)} + /> + + + +
+
+ - setIsDeleteIPModalVisible(false)} - onOk={confirmDeleteIP} - footer={[ - , - , - ]} + onOpenChange={(o) => { + if (!o) setIsDeleteIPModalVisible(false); + }} > - Are you sure you want to delete the IP address: {ipToDelete}? - + + + Confirm Delete + +

Are you sure you want to delete the IP address: {ipToDelete}?

+ + + + +
+ {/* UI Access Control Modal */} - { + if (!o) handleUIAccessControlCancel(); + }} > - { - handleUIAccessControlOk(); - NotificationsManager.success("UI Access Control settings updated successfully"); - }} - /> - + + + UI Access Control Settings + + { + handleUIAccessControlOk(); + NotificationsManager.success("UI Access Control settings updated successfully"); + }} + /> + +
- - If you need to login without sso, you can access{" "} - - {nonSssoUrl}{" "} - - - - ), - }, - { - key: "scim", - label: "SCIM", - children: , - }, - { - key: "ui-settings", - label: ( - - - UI Settings - - - ), - children: , - }, - { - key: "hashicorp-vault", - label: "Hashicorp Vault", - children: , - }, - ]; - return ( -
- Admin Access - Go to 'Internal Users' page to add other admins. - + {/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */} +
+ +
+

Login without SSO

+

+ If you need to login without sso, you can access{" "} + + {nonSssoUrl} + +

+
+
+ + + + + + + + + + + + + +
); }; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index c1042e3cd35..fa134d2d0cc 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -5,7 +5,47 @@ import { useUpdateUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUpdat import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import NotificationManager from "@/components/molecules/notifications_manager"; import PageVisibilitySettings from "./PageVisibilitySettings"; -import { Alert, Card, Divider, Skeleton, Space, Switch, Typography } from "antd"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; + +function SettingRow({ + checked, + disabled, + onCheckedChange, + ariaLabel, + title, + description, + titleMuted, + indent, +}: { + checked: boolean; + disabled: boolean; + onCheckedChange: (checked: boolean) => void; + ariaLabel?: string; + title: string; + description?: string; + titleMuted?: boolean; + indent?: boolean; +}) { + return ( +
+ +
+ {title} + {description && {description}} +
+
+ ); +} export default function UISettings() { const { accessToken } = useAuthorized(); @@ -33,425 +73,215 @@ export default function UISettings() { const isAgentsDisabled = Boolean(values.disable_agents_for_internal_users); const isVectorStoresDisabled = Boolean(values.disable_vector_stores_for_internal_users); - const handleToggle = (checked: boolean) => { - updateSettings( - { disable_model_add_for_internal_users: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); + const toggleField = ( + key: string, + successMsg = "UI settings updated successfully", + reload = false, + ) => + (checked: boolean) => { + updateSettings( + { [key]: checked }, + { + onSuccess: () => { + NotificationManager.success( + reload ? `${successMsg}. Refreshing page...` : successMsg, + ); + if (reload) setTimeout(() => window.location.reload(), 1000); + }, + onError: (err) => { + NotificationManager.fromBackend(err); + }, }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleTeamAdminDelete = (checked: boolean) => { - updateSettings( - { disable_team_admin_delete_team_user: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; + ); + }; const handleUpdatePageVisibility = (settings: { enabled_ui_pages_internal_users: string[] | null }) => { updateSettings(settings, { onSuccess: () => { NotificationManager.success("Page visibility settings updated successfully"); }, - onError: (error) => { - NotificationManager.fromBackend(error); + onError: (err) => { + NotificationManager.fromBackend(err); }, }); }; - const handleToggleForwardClientHeaders = (checked: boolean) => { - updateSettings( - { forward_client_headers_to_llm_api: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleForwardLLMProviderAuthHeaders = (checked: boolean) => { - updateSettings( - { forward_llm_provider_auth_headers: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleEnableProjectsUI = (checked: boolean) => { - updateSettings( - { enable_projects_ui: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully. Refreshing page..."); - setTimeout(() => window.location.reload(), 1000); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleRequireAuthForPublicAIHub = (checked: boolean) => { - updateSettings( - { require_auth_for_public_ai_hub: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleDisableAgents = (checked: boolean) => { - updateSettings( - { disable_agents_for_internal_users: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleAllowAgentsTeamAdmins = (checked: boolean) => { - updateSettings( - { allow_agents_for_team_admins: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleDisableVectorStores = (checked: boolean) => { - updateSettings( - { disable_vector_stores_for_internal_users: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleAllowVectorStoresTeamAdmins = (checked: boolean) => { - updateSettings( - { allow_vector_stores_for_team_admins: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleScopeUserSearch = (checked: boolean) => { - updateSettings( - { scope_user_search_to_org: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - - const handleToggleDisableCustomApiKeys = (checked: boolean) => { - updateSettings( - { disable_custom_api_keys: checked }, - { - onSuccess: () => { - NotificationManager.success("UI settings updated successfully"); - }, - onError: (error) => { - NotificationManager.fromBackend(error); - }, - }, - ); - }; - return ( - + +

UI Settings

{isLoading ? ( - +
+ + + +
) : isError ? ( - + + Could not load UI settings + {error instanceof Error ? ( + {error.message} + ) : null} + ) : ( - +
{schema?.description && ( - {schema.description} +

{schema.description}

)} {updateError && ( - + + Could not update UI settings + {updateError instanceof Error ? ( + {updateError.message} + ) : null} + )} - - - - Disable model add for internal users - {property?.description && {property.description}} - - + - - - - Disable team admin delete team user - {disableTeamAdminDeleteProperty?.description && ( - {disableTeamAdminDeleteProperty.description} - )} - - + - - - - Require authentication for public AI Hub - {requireAuthForPublicAIHubProperty?.description && ( - {requireAuthForPublicAIHubProperty.description} - )} - - + - - - - Forward client headers to LLM API - - {forwardClientHeadersProperty?.description ?? - "Forwards client headers (Authorization, anthropic-beta, and x-* custom headers) to the upstream LLM. Enable for Claude Code with a Max subscription (forwards the OAuth token) or to pass custom/tracing headers through to the provider. Independent of the BYOK toggle — enable only the one(s) you need."} - - - + - - - - Forward LLM provider auth headers - - {forwardLLMProviderAuthHeadersProperty?.description ?? - "Forwards provider auth headers (x-api-key, x-goog-api-key, api-key, ocp-apim-subscription-key) to the upstream LLM, overriding any deployment-configured key for that request. Enable for Claude Code BYOK (clients bring their own API key). Independent of the client-headers toggle — enable only the one(s) you need."} - - - + - - - - [BETA] Enable Projects (page will refresh) - - {enableProjectsUIProperty?.description ?? - "If enabled, shows the Projects feature in the UI sidebar and the project field in key management."} - - - + - + {/* Agents access control */} - - - - Disable agents for internal users - {disableAgentsProperty?.description && ( - {disableAgentsProperty.description} - )} - - + - - - - - Allow agents for team admins - - {allowAgentsTeamAdminsProperty?.description && ( - {allowAgentsTeamAdminsProperty.description} - )} - - + - + {/* Vector Stores access control */} - - - - Disable vector stores for internal users - {disableVectorStoresProperty?.description && ( - {disableVectorStoresProperty.description} - )} - - + - - - - - Allow vector stores for team admins - - {allowVectorStoresTeamAdminsProperty?.description && ( - {allowVectorStoresTeamAdminsProperty.description} - )} - - + - + {/* Scope user search to organization */} - - - - Scope user search to organization - - {scopeUserSearchProperty?.description ?? - "If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."} - - - + - + {/* Disable custom Virtual key values */} - - - - Disable custom Virtual key values - - {disableCustomApiKeysProperty?.description ?? - "If true, users cannot specify custom key values. All keys must be auto-generated."} - - - + - + {/* Page Visibility for Internal Users */} - +
)}
); diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx index 7f991ef78f4..e21bca6c1b8 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx @@ -3,15 +3,22 @@ * Handles primary model selection and fallback chain configuration */ -import { Select } from "antd"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { AlertCircle, ArrowDown, X } from "lucide-react"; -import React from "react"; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Badge } from "@/components/ui/badge"; +import { AlertCircle, ArrowDown, Check, X } from "lucide-react"; +import React, { useMemo, useState } from "react"; export interface FallbackGroup { id: string; @@ -26,20 +33,129 @@ interface FallbackGroupConfigProps { maxFallbacks: number; } +function SearchableMultiSelect({ + options, + values, + onChange, + placeholder, + disabled, + maxSelected, +}: { + options: string[]; + values: string[]; + onChange: (next: string[]) => void; + placeholder: string; + disabled?: boolean; + maxSelected: number; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const filtered = useMemo( + () => + options.filter((o) => + query ? o.toLowerCase().includes(query.toLowerCase()) : true, + ), + [options, query], + ); + + const toggle = (value: string) => { + if (values.includes(value)) { + onChange(values.filter((v) => v !== value)); + } else if (values.length < maxSelected) { + onChange([...values, value]); + } + }; + + return ( + + + + + + setQuery(e.target.value)} + className="h-8 mb-2" + /> +
+ {filtered.length === 0 ? ( +
No models found
+ ) : ( + filtered.map((opt) => { + const isSelected = values.includes(opt); + const orderIndex = isSelected ? values.indexOf(opt) + 1 : null; + return ( + + ); + }) + )} +
+
+
+ ); +} + export function FallbackGroupConfig({ group, onChange, availableModels, maxFallbacks, }: FallbackGroupConfigProps) { - // Filter available options for fallbacks (exclude primary only, allow already selected to be shown for deselection) const availableFallbackOptions = availableModels.filter( (m) => m !== group.primaryModel, ); const handlePrimaryChange = (value: string) => { let newFallbacks = [...group.fallbackModels]; - // Remove from fallbacks if it was there if (newFallbacks.includes(value)) { newFallbacks = newFallbacks.filter((m) => m !== value); } @@ -51,9 +167,7 @@ export function FallbackGroupConfig({ }; const handleFallbackSelect = (values: string[]) => { - // Limit to maxFallbacks const limitedValues = values.slice(0, maxFallbacks); - onChange({ ...group, fallbackModels: limitedValues, @@ -78,19 +192,22 @@ export function FallbackGroupConfig({ Primary Model * {!group.primaryModel && ( + /* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
Select a model to begin configuring fallbacks @@ -100,6 +217,7 @@ export function FallbackGroupConfig({ {/* Visual Connection */}
+ {/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
IF FAILS, TRY... @@ -120,58 +238,17 @@ export function FallbackGroupConfig({
{/* Add Fallback Input */}
-