mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): migrate settings panels to shadcn
Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
9028e69e76
commit
511049458d
4 changed files with 576 additions and 612 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 'Internal Users' 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 'Internal Users' 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue