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:
Cursor Agent 2026-04-23 08:54:18 +00:00
parent 4dbe5ba648
commit 9894ce40d7
No known key found for this signature in database
2 changed files with 182 additions and 134 deletions

View file

@ -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",

View file

@ -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>
);
};