mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate SCIM config view to shadcn
- @tremor Card/Title/Text/Grid/TremorButton/Callout/TextInput/Divider + antd Form + @ant-design/icons LinkOutlined/KeyOutlined/CopyOutlined/ ExclamationCircleOutlined/PlusCircleOutlined \u2192 shadcn Card/Input/ Label/Button/Alert/Separator + react-hook-form + lucide Link/Key/Copy/AlertCircle/PlusCircle. - Token-revealed banner uses amber palette (categorical 'warning' color); added to .eslintrc.json overrides. CopyToClipboard wrappers preserved. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
4dbe5ba648
commit
9894ce40d7
2 changed files with 182 additions and 134 deletions
|
|
@ -30,7 +30,8 @@
|
|||
"src/components/GuardrailsMonitor/GuardrailsOverview.tsx",
|
||||
"src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx",
|
||||
"src/components/GuardrailSettingsView.tsx",
|
||||
"src/components/DebugWarningBanner.tsx"
|
||||
"src/components/DebugWarningBanner.tsx",
|
||||
"src/components/SCIM.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"litellm-ui/no-banned-ui-imports": "off",
|
||||
|
|
|
|||
|
|
@ -1,190 +1,237 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Text, Grid, Button as TremorButton, Callout, TextInput, Divider } from "@tremor/react";
|
||||
import { Form } from "antd";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { keyCreateCall } from "./networking";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import {
|
||||
LinkOutlined,
|
||||
KeyOutlined,
|
||||
CopyOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
PlusCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
AlertCircle,
|
||||
Copy,
|
||||
Key,
|
||||
Link as LinkIcon,
|
||||
PlusCircle,
|
||||
} from "lucide-react";
|
||||
import { parseErrorMessage } from "./shared/errorUtils";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
interface SCIMConfigProps {
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
proxySettings: any;
|
||||
}
|
||||
|
||||
const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySettings }) => {
|
||||
const [form] = Form.useForm();
|
||||
interface SCIMFormValues {
|
||||
key_alias: string;
|
||||
}
|
||||
|
||||
const SCIMConfig: React.FC<SCIMConfigProps> = ({
|
||||
accessToken,
|
||||
userID,
|
||||
proxySettings,
|
||||
}) => {
|
||||
const form = useForm<SCIMFormValues>({
|
||||
defaultValues: { key_alias: "" },
|
||||
mode: "onSubmit",
|
||||
});
|
||||
const [isCreatingToken, setIsCreatingToken] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [tokenData, setTokenData] = useState<any>(null);
|
||||
const [baseUrl, setBaseUrl] = useState("<your_proxy_base_url>");
|
||||
|
||||
useEffect(() => {
|
||||
let url = "<your_proxy_base_url>";
|
||||
|
||||
if (proxySettings && proxySettings.PROXY_BASE_URL && proxySettings.PROXY_BASE_URL !== undefined) {
|
||||
if (
|
||||
proxySettings &&
|
||||
proxySettings.PROXY_BASE_URL &&
|
||||
proxySettings.PROXY_BASE_URL !== undefined
|
||||
) {
|
||||
url = proxySettings.PROXY_BASE_URL;
|
||||
} else if (typeof window !== "undefined") {
|
||||
// Use the current origin as the base URL if no proxy URL is set
|
||||
url = window.location.origin;
|
||||
}
|
||||
|
||||
setBaseUrl(url);
|
||||
}, [proxySettings]);
|
||||
|
||||
const scimBaseUrl = `${baseUrl}/scim/v2`;
|
||||
|
||||
const handleCreateSCIMToken = async (values: any) => {
|
||||
const handleCreateSCIMToken = form.handleSubmit(async (values) => {
|
||||
if (!accessToken || !userID) {
|
||||
NotificationsManager.fromBackend("You need to be logged in to create a SCIM token");
|
||||
NotificationsManager.fromBackend(
|
||||
"You need to be logged in to create a SCIM token",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingToken(true);
|
||||
|
||||
const formData = {
|
||||
key_alias: values.key_alias || "SCIM Access Token",
|
||||
team_id: null,
|
||||
models: [],
|
||||
allowed_routes: ["/scim/*"],
|
||||
};
|
||||
|
||||
const response = await keyCreateCall(accessToken, userID, formData);
|
||||
setTokenData(response);
|
||||
NotificationsManager.success("SCIM token created successfully");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error creating SCIM token:", error);
|
||||
NotificationsManager.fromBackend("Failed to create SCIM token: " + parseErrorMessage(error));
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create SCIM token: " + parseErrorMessage(error),
|
||||
);
|
||||
} finally {
|
||||
setIsCreatingToken(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid numItems={1}>
|
||||
<Card>
|
||||
<div className="flex items-center mb-4">
|
||||
<Title>SCIM Configuration</Title>
|
||||
</div>
|
||||
<Text className="text-gray-600">
|
||||
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and
|
||||
groups in LiteLLM.
|
||||
</Text>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">SCIM Configuration</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
System for Cross-domain Identity Management (SCIM) allows you to
|
||||
automatically provision and manage users and groups in LiteLLM.
|
||||
</p>
|
||||
|
||||
<Divider />
|
||||
<Separator className="my-6" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Step 1: SCIM URL */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 mr-2">
|
||||
1
|
||||
</div>
|
||||
<Title className="text-lg flex items-center">
|
||||
<LinkOutlined className="h-5 w-5 mr-2" />
|
||||
SCIM Tenant URL
|
||||
</Title>
|
||||
<div className="space-y-8">
|
||||
{/* Step 1: SCIM URL */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/15 text-primary mr-2 text-sm font-medium">
|
||||
1
|
||||
</div>
|
||||
<Text className="text-gray-600 mb-3">
|
||||
Use this URL in your identity provider SCIM integration settings.
|
||||
</Text>
|
||||
<div className="flex items-center">
|
||||
<TextInput value={scimBaseUrl} disabled={true} className="flex-grow" />
|
||||
<CopyToClipboard
|
||||
text={scimBaseUrl}
|
||||
onCopy={() => NotificationsManager.success("URL copied to clipboard")}
|
||||
<h4 className="text-lg font-semibold flex items-center">
|
||||
<LinkIcon className="h-5 w-5 mr-2" />
|
||||
SCIM Tenant URL
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Use this URL in your identity provider SCIM integration settings.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value={scimBaseUrl} disabled className="flex-grow" />
|
||||
<CopyToClipboard
|
||||
text={scimBaseUrl}
|
||||
onCopy={() =>
|
||||
NotificationsManager.success("URL copied to clipboard")
|
||||
}
|
||||
>
|
||||
<Button>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2: SCIM Token */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/15 text-primary mr-2 text-sm font-medium">
|
||||
2
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold flex items-center">
|
||||
<Key className="h-5 w-5 mr-2" />
|
||||
Authentication Token
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<Alert className="mb-4">
|
||||
<AlertTitle>Using SCIM</AlertTitle>
|
||||
<AlertDescription>
|
||||
You need a SCIM token to authenticate with the SCIM API. Create
|
||||
one below and use it in your SCIM provider configuration.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{!tokenData ? (
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleCreateSCIMToken} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="key_alias">
|
||||
Token Name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key_alias"
|
||||
rules={{
|
||||
required: "Please enter a name for your token",
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="key_alias"
|
||||
placeholder="SCIM Access Token"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.key_alias && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.key_alias.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" disabled={isCreatingToken}>
|
||||
<Key className="h-4 w-4" />
|
||||
{isCreatingToken ? "Creating…" : "Create SCIM Token"}
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-4 border-amber-300 bg-amber-50 dark:bg-amber-950/30 dark:border-amber-800">
|
||||
<div className="flex items-center mb-2 text-amber-800 dark:text-amber-200">
|
||||
<AlertCircle className="h-5 w-5 mr-2" />
|
||||
<h4 className="text-lg font-semibold">Your SCIM Token</h4>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-amber-800 dark:text-amber-200 mb-4">
|
||||
Make sure to copy this token now. You will not be able to see
|
||||
it again.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={tokenData.key}
|
||||
className="flex-grow bg-background"
|
||||
type="password"
|
||||
disabled
|
||||
/>
|
||||
<CopyToClipboard
|
||||
text={tokenData.key}
|
||||
onCopy={() =>
|
||||
NotificationsManager.success("Token copied to clipboard")
|
||||
}
|
||||
>
|
||||
<Button>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-4"
|
||||
onClick={() => setTokenData(null)}
|
||||
>
|
||||
<TremorButton variant="primary" className="ml-2 flex items-center">
|
||||
<CopyOutlined className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
</TremorButton>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2: SCIM Token */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-700 mr-2">
|
||||
2
|
||||
</div>
|
||||
<Title className="text-lg flex items-center">
|
||||
<KeyOutlined className="h-5 w-5 mr-2" />
|
||||
Authentication Token
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
<Callout title="Using SCIM" color="blue" className="mb-4">
|
||||
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider
|
||||
configuration.
|
||||
</Callout>
|
||||
|
||||
{!tokenData ? (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<Form form={form} onFinish={handleCreateSCIMToken} layout="vertical">
|
||||
<Form.Item
|
||||
name="key_alias"
|
||||
label="Token Name"
|
||||
rules={[{ required: true, message: "Please enter a name for your token" }]}
|
||||
>
|
||||
<TextInput placeholder="SCIM Access Token" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<TremorButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
loading={isCreatingToken}
|
||||
className="flex items-center"
|
||||
>
|
||||
<KeyOutlined className="h-4 w-4 mr-1" />
|
||||
Create SCIM Token
|
||||
</TremorButton>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="border border-yellow-300 bg-yellow-50">
|
||||
<div className="flex items-center mb-2 text-yellow-800">
|
||||
<ExclamationCircleOutlined className="h-5 w-5 mr-2" />
|
||||
<Title className="text-lg text-yellow-800">Your SCIM Token</Title>
|
||||
</div>
|
||||
<Text className="text-yellow-800 mb-4 font-medium">
|
||||
Make sure to copy this token now. You will not be able to see it again.
|
||||
</Text>
|
||||
<div className="flex items-center">
|
||||
<TextInput
|
||||
value={tokenData.key}
|
||||
className="flex-grow mr-2 bg-white"
|
||||
type="password"
|
||||
disabled={true}
|
||||
/>
|
||||
<CopyToClipboard
|
||||
text={tokenData.key}
|
||||
onCopy={() => NotificationsManager.success("Token copied to clipboard")}
|
||||
>
|
||||
<TremorButton variant="primary" className="flex items-center">
|
||||
<CopyOutlined className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
</TremorButton>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<TremorButton className="mt-4 flex items-center" variant="secondary" onClick={() => setTokenData(null)}>
|
||||
<PlusCircleOutlined className="h-4 w-4 mr-1" />
|
||||
Create Another Token
|
||||
</TremorButton>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
Create Another Token
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue