feat(ui): migrate settings panels to shadcn

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 12:32:31 +00:00
parent 9028e69e76
commit 511049458d
No known key found for this signature in database
4 changed files with 576 additions and 612 deletions

View file

@ -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`

View file

@ -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<AdminPanelProps> = ({ 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<AdminPanelProps> = ({ proxySettings }) => {
const [allowedIPs, setAllowedIPs] = useState<string[]>([]);
const [ipToDelete, setIPToDelete] = useState<string | null>(null);
const [ssoConfigured, setSsoConfigured] = useState<boolean>(false);
const [newIP, setNewIP] = useState<string>("");
const baseUrl = useBaseUrl();
const all_ip_address_allowed = "All IP Addresses Allowed";
@ -99,14 +118,19 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ 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<AdminPanelProps> = ({ 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<AdminPanelProps> = ({ proxySettings }) => {
const handleAddSSOOk = () => {
setIsAddSSOModalVisible(false);
form.resetFields();
ssoFormHandle.current.resetFields();
if (accessToken && premiumUser) {
checkSSOConfiguration();
}
@ -149,10 +172,10 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
const handleAddSSOCancel = () => {
setIsAddSSOModalVisible(false);
form.resetFields();
ssoFormHandle.current.resetFields();
};
const handleShowInstructions = (formValues: Record<string, any>) => {
const handleShowInstructions = (_formValues: Record<string, any>) => {
setIsAddSSOModalVisible(false);
setIsInstructionsModalVisible(true);
};
@ -173,7 +196,8 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ 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<AdminPanelProps> = ({ proxySettings }) => {
setIsUIAccessControlModalVisible(false);
};
const tabItems = [
{
key: "sso-settings",
label: "SSO Settings",
children: <SSOSettings />,
},
{
key: "security-settings",
label: "Security Settings",
children: (
<>
<Card>
<Title level={4}> ✨ Security Settings</Title>
<Alert
message="SSO Configuration Deprecated"
description="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."
type="warning"
showIcon
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
marginTop: "1rem",
marginLeft: "0.5rem",
}}
>
return (
<div className="w-full m-2 mt-2 p-8">
<h1 className="text-xl font-semibold">Admin Access </h1>
<p className="text-sm text-muted-foreground mb-4">
Go to &apos;Internal Users&apos; page to add other admins.
</p>
<Tabs defaultValue="sso-settings">
<TabsList>
<TabsTrigger value="sso-settings">SSO Settings</TabsTrigger>
<TabsTrigger value="security-settings">Security Settings</TabsTrigger>
<TabsTrigger value="scim">SCIM</TabsTrigger>
<TabsTrigger value="ui-settings">
<span className="inline-flex items-center gap-2">
UI Settings
<NewBadge />
</span>
</TabsTrigger>
<TabsTrigger value="hashicorp-vault">Hashicorp Vault</TabsTrigger>
</TabsList>
<TabsContent value="sso-settings">
<SSOSettings />
</TabsContent>
<TabsContent value="security-settings">
<Card className="p-6">
<h3 className="text-lg font-semibold mb-3">✨ Security Settings</h3>
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertTitle>SSO Configuration Deprecated</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
<div className="flex flex-col gap-4 mt-4 ml-2">
<div>
<Button style={{ width: "150px" }} onClick={() => setIsAddSSOModalVisible(true)}>
<Button className="w-[150px]" onClick={() => setIsAddSSOModalVisible(true)}>
{ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
</Button>
</div>
<div>
<Button style={{ width: "150px" }} onClick={handleShowAllowedIPs}>
<Button className="w-[150px]" onClick={handleShowAllowedIPs}>
Allowed IPs
</Button>
</div>
<div>
<Button
style={{ width: "150px" }}
className="w-[150px]"
onClick={() =>
premiumUser === true
? setIsUIAccessControlModalVisible(true)
@ -245,136 +277,150 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
handleShowInstructions={handleShowInstructions}
handleInstructionsOk={handleInstructionsOk}
handleInstructionsCancel={handleInstructionsCancel}
form={form}
form={ssoFormHandle.current}
accessToken={accessToken}
ssoConfigured={ssoConfigured}
/>
<Modal
title="Manage Allowed IP Addresses"
width={800}
<Dialog
open={isAllowedIPModalVisible}
onCancel={() => setIsAllowedIPModalVisible(false)}
footer={[
<Button className="mx-1" key="add" onClick={() => setIsAddIPModalVisible(true)}>
Add IP Address
</Button>,
<Button key="close" onClick={() => setIsAllowedIPModalVisible(false)}>
Close
</Button>,
]}
onOpenChange={(o) => {
if (!o) setIsAllowedIPModalVisible(false);
}}
>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>IP Address</TableHeaderCell>
<TableHeaderCell className="text-right">Action</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{allowedIPs.map((ip, index) => (
<TableRow key={index}>
<TableCell>{ip}</TableCell>
<TableCell className="text-right">
{ip !== all_ip_address_allowed && (
<Button onClick={() => handleDeleteIP(ip)} color="red" size="xs">
Delete
</Button>
)}
</TableCell>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Manage Allowed IP Addresses</DialogTitle>
</DialogHeader>
<Table>
<TableHeader>
<TableRow>
<TableHead>IP Address</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</Modal>
</TableHeader>
<TableBody>
{allowedIPs.map((ip, index) => (
<TableRow key={index}>
<TableCell>{ip}</TableCell>
<TableCell className="text-right">
{ip !== all_ip_address_allowed && (
<Button
onClick={() => handleDeleteIP(ip)}
variant="destructive"
size="sm"
>
Delete
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<DialogFooter>
<Button onClick={() => setIsAddIPModalVisible(true)}>Add IP Address</Button>
<Button variant="outline" onClick={() => setIsAllowedIPModalVisible(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Modal
title="Add Allowed IP Address"
<Dialog
open={isAddIPModalVisible}
onCancel={() => setIsAddIPModalVisible(false)}
footer={null}
onOpenChange={(o) => {
if (!o) setIsAddIPModalVisible(false);
}}
>
<Form onFinish={handleAddIP}>
<Form.Item name="ip" rules={[{ required: true, message: "Please enter an IP address" }]}>
<Input placeholder="Enter IP address" />
</Form.Item>
<Form.Item>
<Button2 htmlType="submit">Add IP Address</Button2>
</Form.Item>
</Form>
</Modal>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Allowed IP Address</DialogTitle>
</DialogHeader>
<form onSubmit={handleAddIP} className="space-y-4">
<Input
placeholder="Enter IP address"
value={newIP}
onChange={(e) => setNewIP(e.target.value)}
/>
<DialogFooter>
<Button type="submit">Add IP Address</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Modal
title="Confirm Delete"
<Dialog
open={isDeleteIPModalVisible}
onCancel={() => setIsDeleteIPModalVisible(false)}
onOk={confirmDeleteIP}
footer={[
<Button className="mx-1" key="delete" onClick={() => confirmDeleteIP()}>
Yes
</Button>,
<Button key="close" onClick={() => setIsDeleteIPModalVisible(false)}>
Close
</Button>,
]}
onOpenChange={(o) => {
if (!o) setIsDeleteIPModalVisible(false);
}}
>
<Text>Are you sure you want to delete the IP address: {ipToDelete}?</Text>
</Modal>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Delete</DialogTitle>
</DialogHeader>
<p>Are you sure you want to delete the IP address: {ipToDelete}?</p>
<DialogFooter>
<Button variant="destructive" onClick={() => confirmDeleteIP()}>
Yes
</Button>
<Button variant="outline" onClick={() => setIsDeleteIPModalVisible(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* UI Access Control Modal */}
<Modal
title="UI Access Control Settings"
<Dialog
open={isUIAccessControlModalVisible}
width={600}
footer={null}
onOk={handleUIAccessControlOk}
onCancel={handleUIAccessControlCancel}
onOpenChange={(o) => {
if (!o) handleUIAccessControlCancel();
}}
>
<UIAccessControlForm
accessToken={accessToken}
onSuccess={() => {
handleUIAccessControlOk();
NotificationsManager.success("UI Access Control settings updated successfully");
}}
/>
</Modal>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle>UI Access Control Settings</DialogTitle>
</DialogHeader>
<UIAccessControlForm
accessToken={accessToken}
onSuccess={() => {
handleUIAccessControlOk();
NotificationsManager.success("UI Access Control settings updated successfully");
}}
/>
</DialogContent>
</Dialog>
</div>
<Callout title="Login without SSO" color="teal">
If you need to login without sso, you can access{" "}
<a href={nonSssoUrl} target="_blank" rel="noopener noreferrer">
<b>{nonSssoUrl}</b>{" "}
</a>
</Callout>
</>
),
},
{
key: "scim",
label: "SCIM",
children: <SCIMConfig accessToken={accessToken} userID={userID} proxySettings={proxySettings} />,
},
{
key: "ui-settings",
label: (
<Space>
<Text>
UI Settings <NewBadge />
</Text>
</Space>
),
children: <UISettings />,
},
{
key: "hashicorp-vault",
label: "Hashicorp Vault",
children: <HashicorpVault />,
},
];
return (
<div className="w-full m-2 mt-2 p-8">
<Title level={4}>Admin Access </Title>
<Paragraph>Go to &apos;Internal Users&apos; page to add other admins.</Paragraph>
<Tabs items={tabItems} />
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<div className="rounded-md border border-teal-200 bg-teal-50 text-teal-900 dark:bg-teal-950/30 dark:border-teal-900 dark:text-teal-200 p-4 flex gap-3">
<LogIn className="h-5 w-5 mt-0.5 shrink-0" />
<div>
<p className="font-medium">Login without SSO</p>
<p className="text-sm">
If you need to login without sso, you can access{" "}
<a href={nonSssoUrl} target="_blank" rel="noopener noreferrer">
<b>{nonSssoUrl}</b>
</a>
</p>
</div>
</div>
</TabsContent>
<TabsContent value="scim">
<SCIMConfig accessToken={accessToken} userID={userID} proxySettings={proxySettings} />
</TabsContent>
<TabsContent value="ui-settings">
<UISettings />
</TabsContent>
<TabsContent value="hashicorp-vault">
<HashicorpVault />
</TabsContent>
</Tabs>
</div>
);
};

View file

@ -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 (
<div className={`flex items-start gap-4 ${indent ? "ml-8" : ""}`}>
<Switch
checked={checked}
disabled={disabled}
onCheckedChange={onCheckedChange}
aria-label={ariaLabel}
className="mt-1"
/>
<div className="flex flex-col gap-1">
<span className={`font-semibold ${titleMuted ? "text-muted-foreground" : ""}`}>{title}</span>
{description && <span className="text-sm text-muted-foreground">{description}</span>}
</div>
</div>
);
}
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 (
<Card title="UI Settings">
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">UI Settings</h3>
{isLoading ? (
<Skeleton active />
<div className="space-y-2">
<Skeleton className="h-6 w-full" />
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-6 w-1/2" />
</div>
) : isError ? (
<Alert
type="error"
message="Could not load UI settings"
description={error instanceof Error ? error.message : undefined}
/>
<Alert variant="destructive">
<AlertTitle>Could not load UI settings</AlertTitle>
{error instanceof Error ? (
<AlertDescription>{error.message}</AlertDescription>
) : null}
</Alert>
) : (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<div className="flex flex-col gap-6 w-full">
{schema?.description && (
<Typography.Paragraph style={{ marginBottom: 0 }}>{schema.description}</Typography.Paragraph>
<p className="mb-0">{schema.description}</p>
)}
{updateError && (
<Alert
type="error"
message="Could not update UI settings"
description={updateError instanceof Error ? updateError.message : undefined}
/>
<Alert variant="destructive">
<AlertTitle>Could not update UI settings</AlertTitle>
{updateError instanceof Error ? (
<AlertDescription>{updateError.message}</AlertDescription>
) : null}
</Alert>
)}
<Space align="start" size="middle">
<Switch
checked={isDisabledForInternalUsers}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggle}
aria-label={property?.description ?? "Disable model add for internal users"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Disable model add for internal users</Typography.Text>
{property?.description && <Typography.Text type="secondary">{property.description}</Typography.Text>}
</Space>
</Space>
<SettingRow
checked={isDisabledForInternalUsers}
disabled={isUpdating}
onCheckedChange={toggleField("disable_model_add_for_internal_users")}
ariaLabel={property?.description ?? "Disable model add for internal users"}
title="Disable model add for internal users"
description={property?.description}
/>
<Space align="start" size="middle">
<Switch
checked={isDisabledTeamAdminDeleteTeamUser}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleTeamAdminDelete}
aria-label={disableTeamAdminDeleteProperty?.description ?? "Disable team admin delete team user"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Disable team admin delete team user</Typography.Text>
{disableTeamAdminDeleteProperty?.description && (
<Typography.Text type="secondary">{disableTeamAdminDeleteProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={isDisabledTeamAdminDeleteTeamUser}
disabled={isUpdating}
onCheckedChange={toggleField("disable_team_admin_delete_team_user")}
ariaLabel={disableTeamAdminDeleteProperty?.description ?? "Disable team admin delete team user"}
title="Disable team admin delete team user"
description={disableTeamAdminDeleteProperty?.description}
/>
<Space align="start" size="middle">
<Switch
checked={values.require_auth_for_public_ai_hub}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleRequireAuthForPublicAIHub}
aria-label={requireAuthForPublicAIHubProperty?.description ?? "Require authentication for public AI Hub"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Require authentication for public AI Hub</Typography.Text>
{requireAuthForPublicAIHubProperty?.description && (
<Typography.Text type="secondary">{requireAuthForPublicAIHubProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={Boolean(values.require_auth_for_public_ai_hub)}
disabled={isUpdating}
onCheckedChange={toggleField("require_auth_for_public_ai_hub")}
ariaLabel={
requireAuthForPublicAIHubProperty?.description ??
"Require authentication for public AI Hub"
}
title="Require authentication for public AI Hub"
description={requireAuthForPublicAIHubProperty?.description}
/>
<Space align="start" size="middle">
<Switch
checked={Boolean(values.forward_client_headers_to_llm_api)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleForwardClientHeaders}
aria-label={forwardClientHeadersProperty?.description ?? "Forward client headers to LLM API"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Forward client headers to LLM API</Typography.Text>
<Typography.Text type="secondary">
{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."}
</Typography.Text>
</Space>
</Space>
<SettingRow
checked={Boolean(values.forward_client_headers_to_llm_api)}
disabled={isUpdating}
onCheckedChange={toggleField("forward_client_headers_to_llm_api")}
ariaLabel={forwardClientHeadersProperty?.description ?? "Forward client headers to LLM API"}
title="Forward client headers to LLM API"
description={
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."
}
/>
<Space align="start" size="middle">
<Switch
checked={Boolean(values.forward_llm_provider_auth_headers)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleForwardLLMProviderAuthHeaders}
aria-label={
forwardLLMProviderAuthHeadersProperty?.description ??
"Forward LLM provider auth headers"
}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Forward LLM provider auth headers</Typography.Text>
<Typography.Text type="secondary">
{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."}
</Typography.Text>
</Space>
</Space>
<SettingRow
checked={Boolean(values.forward_llm_provider_auth_headers)}
disabled={isUpdating}
onCheckedChange={toggleField("forward_llm_provider_auth_headers")}
ariaLabel={
forwardLLMProviderAuthHeadersProperty?.description ?? "Forward LLM provider auth headers"
}
title="Forward LLM provider auth headers"
description={
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."
}
/>
<Space align="start" size="middle">
<Switch
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleEnableProjectsUI}
aria-label={enableProjectsUIProperty?.description ?? "Enable Projects UI"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>[BETA] Enable Projects (page will refresh)</Typography.Text>
<Typography.Text type="secondary">
{enableProjectsUIProperty?.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."}
</Typography.Text>
</Space>
</Space>
<SettingRow
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
onCheckedChange={toggleField("enable_projects_ui", "UI settings updated successfully", true)}
ariaLabel={enableProjectsUIProperty?.description ?? "Enable Projects UI"}
title="[BETA] Enable Projects (page will refresh)"
description={
enableProjectsUIProperty?.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."
}
/>
<Divider />
<Separator />
{/* Agents access control */}
<Space align="start" size="middle">
<Switch
checked={isAgentsDisabled}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleDisableAgents}
aria-label={disableAgentsProperty?.description ?? "Disable agents for internal users"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Disable agents for internal users</Typography.Text>
{disableAgentsProperty?.description && (
<Typography.Text type="secondary">{disableAgentsProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={isAgentsDisabled}
disabled={isUpdating}
onCheckedChange={toggleField("disable_agents_for_internal_users")}
ariaLabel={disableAgentsProperty?.description ?? "Disable agents for internal users"}
title="Disable agents for internal users"
description={disableAgentsProperty?.description}
/>
<Space align="start" size="middle" style={{ marginLeft: 32 }}>
<Switch
checked={Boolean(values.allow_agents_for_team_admins)}
disabled={isUpdating || !isAgentsDisabled}
loading={isUpdating}
onChange={handleToggleAllowAgentsTeamAdmins}
aria-label={allowAgentsTeamAdminsProperty?.description ?? "Allow agents for team admins"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong type={!isAgentsDisabled ? "secondary" : undefined}>
Allow agents for team admins
</Typography.Text>
{allowAgentsTeamAdminsProperty?.description && (
<Typography.Text type="secondary">{allowAgentsTeamAdminsProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={Boolean(values.allow_agents_for_team_admins)}
disabled={isUpdating || !isAgentsDisabled}
onCheckedChange={toggleField("allow_agents_for_team_admins")}
ariaLabel={allowAgentsTeamAdminsProperty?.description ?? "Allow agents for team admins"}
title="Allow agents for team admins"
description={allowAgentsTeamAdminsProperty?.description}
titleMuted={!isAgentsDisabled}
indent
/>
<Divider />
<Separator />
{/* Vector Stores access control */}
<Space align="start" size="middle">
<Switch
checked={isVectorStoresDisabled}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleDisableVectorStores}
aria-label={disableVectorStoresProperty?.description ?? "Disable vector stores for internal users"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Disable vector stores for internal users</Typography.Text>
{disableVectorStoresProperty?.description && (
<Typography.Text type="secondary">{disableVectorStoresProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={isVectorStoresDisabled}
disabled={isUpdating}
onCheckedChange={toggleField("disable_vector_stores_for_internal_users")}
ariaLabel={disableVectorStoresProperty?.description ?? "Disable vector stores for internal users"}
title="Disable vector stores for internal users"
description={disableVectorStoresProperty?.description}
/>
<Space align="start" size="middle" style={{ marginLeft: 32 }}>
<Switch
checked={Boolean(values.allow_vector_stores_for_team_admins)}
disabled={isUpdating || !isVectorStoresDisabled}
loading={isUpdating}
onChange={handleToggleAllowVectorStoresTeamAdmins}
aria-label={allowVectorStoresTeamAdminsProperty?.description ?? "Allow vector stores for team admins"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong type={!isVectorStoresDisabled ? "secondary" : undefined}>
Allow vector stores for team admins
</Typography.Text>
{allowVectorStoresTeamAdminsProperty?.description && (
<Typography.Text type="secondary">{allowVectorStoresTeamAdminsProperty.description}</Typography.Text>
)}
</Space>
</Space>
<SettingRow
checked={Boolean(values.allow_vector_stores_for_team_admins)}
disabled={isUpdating || !isVectorStoresDisabled}
onCheckedChange={toggleField("allow_vector_stores_for_team_admins")}
ariaLabel={allowVectorStoresTeamAdminsProperty?.description ?? "Allow vector stores for team admins"}
title="Allow vector stores for team admins"
description={allowVectorStoresTeamAdminsProperty?.description}
titleMuted={!isVectorStoresDisabled}
indent
/>
<Divider />
<Separator />
{/* Scope user search to organization */}
<Space align="start" size="middle">
<Switch
checked={Boolean(values.scope_user_search_to_org)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleScopeUserSearch}
aria-label={scopeUserSearchProperty?.description ?? "Scope user search to organization"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Scope user search to organization</Typography.Text>
<Typography.Text type="secondary">
{scopeUserSearchProperty?.description ??
"If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."}
</Typography.Text>
</Space>
</Space>
<SettingRow
checked={Boolean(values.scope_user_search_to_org)}
disabled={isUpdating}
onCheckedChange={toggleField("scope_user_search_to_org")}
ariaLabel={scopeUserSearchProperty?.description ?? "Scope user search to organization"}
title="Scope user search to organization"
description={
scopeUserSearchProperty?.description ??
"If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."
}
/>
<Divider />
<Separator />
{/* Disable custom Virtual key values */}
<Space align="start" size="middle">
<Switch
checked={Boolean(values.disable_custom_api_keys)}
disabled={isUpdating}
loading={isUpdating}
onChange={handleToggleDisableCustomApiKeys}
aria-label={disableCustomApiKeysProperty?.description ?? "Disable custom Virtual key values"}
/>
<Space direction="vertical" size={4}>
<Typography.Text strong>Disable custom Virtual key values</Typography.Text>
<Typography.Text type="secondary">
{disableCustomApiKeysProperty?.description ??
"If true, users cannot specify custom key values. All keys must be auto-generated."}
</Typography.Text>
</Space>
</Space>
<SettingRow
checked={Boolean(values.disable_custom_api_keys)}
disabled={isUpdating}
onCheckedChange={toggleField("disable_custom_api_keys")}
ariaLabel={disableCustomApiKeysProperty?.description ?? "Disable custom Virtual key values"}
title="Disable custom Virtual key values"
description={
disableCustomApiKeysProperty?.description ??
"If true, users cannot specify custom key values. All keys must be auto-generated."
}
/>
<Divider />
<Separator />
{/* Page Visibility for Internal Users */}
<PageVisibilitySettings
@ -460,7 +290,7 @@ export default function UISettings() {
isUpdating={isUpdating}
onUpdate={handleUpdatePageVisibility}
/>
</Space>
</div>
)}
</Card>
);

View file

@ -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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
className="min-h-10 w-full flex flex-wrap items-center gap-1 rounded-md border border-input bg-background px-2 py-1.5 text-sm text-left disabled:opacity-50"
>
{values.length === 0 ? (
<span className="text-muted-foreground px-1">{placeholder}</span>
) : (
<>
{values.map((v, i) => (
<Badge key={v} variant="secondary" className="inline-flex items-center gap-1">
<span className="text-xs">{i + 1}.</span>
{v}
<span
role="button"
tabIndex={0}
aria-label={`Remove ${v}`}
onClick={(e) => {
e.stopPropagation();
onChange(values.filter((x) => x !== v));
}}
className="inline-flex items-center"
>
<X size={12} />
</span>
</Badge>
))}
</>
)}
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[var(--radix-popover-trigger-width)] p-2">
<Input
autoFocus
placeholder="Search models…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="h-8 mb-2"
/>
<div className="max-h-60 overflow-y-auto">
{filtered.length === 0 ? (
<div className="py-2 px-3 text-sm text-muted-foreground">No models found</div>
) : (
filtered.map((opt) => {
const isSelected = values.includes(opt);
const orderIndex = isSelected ? values.indexOf(opt) + 1 : null;
return (
<button
key={opt}
type="button"
onClick={() => toggle(opt)}
className="w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent flex items-center gap-2"
>
{isSelected && orderIndex !== null ? (
/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
<span className="flex items-center justify-center w-5 h-5 rounded bg-indigo-100 text-indigo-600 dark:bg-indigo-950 dark:text-indigo-300 text-xs font-bold">
{orderIndex}
</span>
) : (
<span className="w-5 h-5 inline-flex items-center justify-center">
{isSelected ? <Check size={14} /> : null}
</span>
)}
<span>{opt}</span>
</button>
);
})
)}
</div>
</PopoverContent>
</Popover>
);
}
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 <span className="text-destructive">*</span>
</label>
<Select
className="w-full h-12"
size="large"
placeholder="Select primary model"
value={group.primaryModel}
onChange={handlePrimaryChange}
showSearch
getPopupContainer={(trigger) => trigger.parentElement || document.body}
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={availableModels.map((m) => ({ label: m, value: m }))}
/>
value={group.primaryModel ?? ""}
onValueChange={handlePrimaryChange}
>
<SelectTrigger className="w-full h-12">
<SelectValue placeholder="Select primary model" />
</SelectTrigger>
<SelectContent>
{availableModels.map((m) => (
<SelectItem key={m} value={m}>
{m}
</SelectItem>
))}
</SelectContent>
</Select>
{!group.primaryModel && (
/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
<div className="mt-2 flex items-center gap-2 text-amber-600 dark:text-amber-400 text-xs bg-amber-50 dark:bg-amber-950/30 p-2 rounded">
<AlertCircle className="w-4 h-4" />
<span>Select a model to begin configuring fallbacks</span>
@ -100,6 +217,7 @@ export function FallbackGroupConfig({
{/* Visual Connection */}
<div className="flex items-center justify-center -my-4 z-10">
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<div className="bg-indigo-50 dark:bg-indigo-950/30 text-indigo-500 dark:text-indigo-400 px-4 py-1 rounded-full text-xs font-bold border border-indigo-100 dark:border-indigo-900 flex items-center gap-2 shadow-sm">
<ArrowDown className="w-4 h-4" />
IF FAILS, TRY...
@ -120,58 +238,17 @@ export function FallbackGroupConfig({
<div className="bg-muted rounded-xl p-4 border border-border">
{/* Add Fallback Input */}
<div className="mb-4">
<Select
mode="multiple"
className="w-full"
size="large"
<SearchableMultiSelect
options={availableFallbackOptions}
values={group.fallbackModels}
onChange={handleFallbackSelect}
placeholder={
canAddMoreFallbacks
? "Select fallback models to add..."
: `Maximum ${maxFallbacks} fallbacks reached`
}
value={group.fallbackModels}
onChange={handleFallbackSelect}
disabled={!group.primaryModel}
getPopupContainer={(trigger) => trigger.parentElement || document.body}
options={availableFallbackOptions.map((m) => ({
label: m,
value: m,
}))}
optionRender={(option) => {
const isSelected = group.fallbackModels.includes(
option.value as string,
);
const orderIndex = isSelected
? group.fallbackModels.indexOf(option.value as string) + 1
: null;
return (
<div className="flex items-center gap-2">
{isSelected && orderIndex !== null && (
<span className="flex items-center justify-center w-5 h-5 rounded bg-indigo-100 text-indigo-600 dark:bg-indigo-950 dark:text-indigo-300 text-xs font-bold">
{orderIndex}
</span>
)}
<span>{option.label}</span>
</div>
);
}}
maxTagCount="responsive"
maxTagPlaceholder={(omittedValues) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>+{omittedValues.length} more</span>
</TooltipTrigger>
<TooltipContent>
{omittedValues.map(({ value }) => value).join(", ")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
showSearch
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
maxSelected={maxFallbacks}
/>
<p className="text-xs text-muted-foreground mt-1 ml-1">
{canAddMoreFallbacks
@ -193,9 +270,11 @@ export function FallbackGroupConfig({
group.fallbackModels.map((modelValue, index) => (
<div
key={`${modelValue}-${index}`}
/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
className="group flex items-center justify-between p-3 bg-background rounded-lg border border-border hover:border-indigo-300 dark:hover:border-indigo-700 hover:shadow-sm transition-all"
>
<div className="flex items-center gap-3">
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<div className="flex items-center justify-center w-6 h-6 rounded bg-muted text-muted-foreground group-hover:text-indigo-500 group-hover:bg-indigo-50 dark:group-hover:bg-indigo-950/30">
<span className="text-xs font-bold">{index + 1}</span>
</div>