mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate DebugWarningBanner + UIAccessControlForm to shadcn
- DebugWarningBanner: antd Alert (banner mode) \u2192 shadcn Alert with amber palette + lucide AlertTriangle. Banner styling preserved via rounded-none + bordered classes; added to .eslintrc.json overrides for the amber categorical palette. - UIAccessControlForm: antd Form/Input/Select/Button + @tremor TextInput /Text \u2192 shadcn Input/Label/Select/Button + react-hook-form. Watch ui_access_mode_type to conditionally render the restricted_sso_group field (replaces antd Form.Item shouldUpdate render-prop). Gates: TS \u2713 | Vitest 12/12 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
a35fae32b7
commit
4dbe5ba648
3 changed files with 156 additions and 122 deletions
|
|
@ -29,7 +29,8 @@
|
|||
"src/components/GuardrailsMonitor/LogViewer.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailsOverview.tsx",
|
||||
"src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx",
|
||||
"src/components/GuardrailSettingsView.tsx"
|
||||
"src/components/GuardrailSettingsView.tsx",
|
||||
"src/components/DebugWarningBanner.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"litellm-ui/no-banned-ui-imports": "off",
|
||||
|
|
|
|||
|
|
@ -1,32 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Alert } from "antd";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
|
||||
|
||||
export const DebugWarningBanner: React.FC = () => {
|
||||
const { data: healthData } = useHealthReadiness();
|
||||
|
||||
// Only show banner if detailed debug mode is explicitly enabled
|
||||
if (!healthData?.is_detailed_debug) {
|
||||
return null;
|
||||
}
|
||||
if (!healthData?.is_detailed_debug) return null;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
message="Performance Warning: Detailed Debug Mode Active"
|
||||
description={
|
||||
<>
|
||||
Detailed debug logging (<code>LITELLM_LOG=DEBUG</code>) is currently
|
||||
enabled. This mode logs extensive diagnostic information and will
|
||||
significantly degrade performance. It should only be used for
|
||||
troubleshooting and disabled in production environments.
|
||||
</>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
banner
|
||||
style={{ marginBottom: 0, borderRadius: 0 }}
|
||||
/>
|
||||
<Alert variant="default" className="rounded-none border-0 border-b border-amber-300 bg-amber-50 text-amber-800 dark:bg-amber-950/30 dark:text-amber-200 dark:border-amber-800">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Performance Warning: Detailed Debug Mode Active</AlertTitle>
|
||||
<AlertDescription>
|
||||
Detailed debug logging (<code>LITELLM_LOG=DEBUG</code>) is currently
|
||||
enabled. This mode logs extensive diagnostic information and will
|
||||
significantly degrade performance. It should only be used for
|
||||
troubleshooting and disabled in production environments.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Button as Button2, Select } from "antd";
|
||||
import { Text, TextInput } from "@tremor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { getSSOSettings, updateSSOSettings } from "./networking";
|
||||
import NotificationManager from "./molecules/notifications_manager";
|
||||
|
||||
|
|
@ -9,74 +18,81 @@ interface UIAccessControlFormProps {
|
|||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// Separate UI Access Control Form Component
|
||||
const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
interface FormValues {
|
||||
ui_access_mode_type: string;
|
||||
restricted_sso_group: string;
|
||||
sso_group_jwt_field: string;
|
||||
}
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
ui_access_mode_type: "",
|
||||
restricted_sso_group: "",
|
||||
sso_group_jwt_field: "",
|
||||
};
|
||||
|
||||
const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({
|
||||
accessToken,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const form = useForm<FormValues>({ defaultValues, mode: "onSubmit" });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load existing UI access control settings
|
||||
useEffect(() => {
|
||||
const loadUIAccessSettings = async () => {
|
||||
if (accessToken) {
|
||||
try {
|
||||
const ssoData = await getSSOSettings(accessToken);
|
||||
if (ssoData && ssoData.values) {
|
||||
// Handle nested ui_access_mode structure
|
||||
const uiAccessMode = ssoData.values.ui_access_mode;
|
||||
let formValues = {};
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
const ssoData = await getSSOSettings(accessToken);
|
||||
if (ssoData && ssoData.values) {
|
||||
const uiAccessMode = ssoData.values.ui_access_mode;
|
||||
let formValues: FormValues = { ...defaultValues };
|
||||
|
||||
if (uiAccessMode && typeof uiAccessMode === "object") {
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode.type,
|
||||
restricted_sso_group: uiAccessMode.restricted_sso_group,
|
||||
sso_group_jwt_field: uiAccessMode.sso_group_jwt_field,
|
||||
};
|
||||
} else if (typeof uiAccessMode === "string") {
|
||||
// Handle legacy flat structure
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode,
|
||||
restricted_sso_group: ssoData.values.restricted_sso_group,
|
||||
sso_group_jwt_field: ssoData.values.team_ids_jwt_field || ssoData.values.sso_group_jwt_field,
|
||||
};
|
||||
}
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
if (uiAccessMode && typeof uiAccessMode === "object") {
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode.type ?? "",
|
||||
restricted_sso_group: uiAccessMode.restricted_sso_group ?? "",
|
||||
sso_group_jwt_field: uiAccessMode.sso_group_jwt_field ?? "",
|
||||
};
|
||||
} else if (typeof uiAccessMode === "string") {
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode,
|
||||
restricted_sso_group:
|
||||
ssoData.values.restricted_sso_group ?? "",
|
||||
sso_group_jwt_field:
|
||||
ssoData.values.team_ids_jwt_field ||
|
||||
ssoData.values.sso_group_jwt_field ||
|
||||
"",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load UI access settings:", error);
|
||||
form.reset(formValues);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load UI access settings:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadUIAccessSettings();
|
||||
}, [accessToken, form]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [accessToken]);
|
||||
|
||||
const handleUIAccessSubmit = async (formValues: Record<string, any>) => {
|
||||
const handleUIAccessSubmit = form.handleSubmit(async (values) => {
|
||||
if (!accessToken) {
|
||||
NotificationManager.fromBackend("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Transform form data to match API expected structure
|
||||
let apiPayload;
|
||||
|
||||
if (formValues.ui_access_mode_type === "all_authenticated_users") {
|
||||
// Set ui_access_mode to none when all_authenticated_users is selected
|
||||
apiPayload = {
|
||||
ui_access_mode: "none",
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let apiPayload: any;
|
||||
if (values.ui_access_mode_type === "all_authenticated_users") {
|
||||
apiPayload = { ui_access_mode: "none" };
|
||||
} else {
|
||||
apiPayload = {
|
||||
ui_access_mode: {
|
||||
type: formValues.ui_access_mode_type,
|
||||
restricted_sso_group: formValues.restricted_sso_group,
|
||||
sso_group_jwt_field: formValues.sso_group_jwt_field,
|
||||
type: values.ui_access_mode_type,
|
||||
restricted_sso_group: values.restricted_sso_group,
|
||||
sso_group_jwt_field: values.sso_group_jwt_field,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await updateSSOSettings(accessToken, apiPayload);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
|
|
@ -85,66 +101,86 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const accessModeType = form.watch("ui_access_mode_type");
|
||||
|
||||
return (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Text style={{ fontSize: "14px", color: "#6b7280" }}>
|
||||
Configure who can access the UI interface and how group information is extracted from JWT tokens.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Configure who can access the UI interface and how group information is
|
||||
extracted from JWT tokens.
|
||||
</p>
|
||||
|
||||
<Form form={form} onFinish={handleUIAccessSubmit} layout="vertical">
|
||||
<Form.Item label="UI Access Mode" name="ui_access_mode_type" tooltip="Controls who can access the UI interface">
|
||||
<Select placeholder="Select access mode">
|
||||
<Select.Option value="all_authenticated_users">All Authenticated Users</Select.Option>
|
||||
<Select.Option value="restricted_sso_group">Restricted SSO Group</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleUIAccessSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ui_access_mode_type" title="Controls who can access the UI interface">
|
||||
UI Access Mode
|
||||
</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="ui_access_mode_type"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={(v) => field.onChange(v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select access mode" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all_authenticated_users">
|
||||
All Authenticated Users
|
||||
</SelectItem>
|
||||
<SelectItem value="restricted_sso_group">
|
||||
Restricted SSO Group
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.ui_access_mode_type !== currentValues.ui_access_mode_type
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const uiAccessModeType = getFieldValue("ui_access_mode_type");
|
||||
return uiAccessModeType === "restricted_sso_group" ? (
|
||||
<Form.Item
|
||||
label="Restricted SSO Group"
|
||||
name="restricted_sso_group"
|
||||
rules={[{ required: true, message: "Please enter the restricted SSO group" }]}
|
||||
>
|
||||
<TextInput placeholder="ui-access-group" />
|
||||
</Form.Item>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
{accessModeType === "restricted_sso_group" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="restricted_sso_group">
|
||||
Restricted SSO Group{" "}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="restricted_sso_group"
|
||||
placeholder="ui-access-group"
|
||||
{...form.register("restricted_sso_group", {
|
||||
required: "Please enter the restricted SSO group",
|
||||
})}
|
||||
/>
|
||||
{form.formState.errors.restricted_sso_group && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.restricted_sso_group.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label="SSO Group JWT Field"
|
||||
name="sso_group_jwt_field"
|
||||
tooltip="JWT field name that contains team/group information. Use dot notation to access nested fields."
|
||||
>
|
||||
<TextInput placeholder="groups" />
|
||||
</Form.Item>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sso_group_jwt_field" title="JWT field name that contains team/group information. Use dot notation to access nested fields.">
|
||||
SSO Group JWT Field
|
||||
</Label>
|
||||
<Input
|
||||
id="sso_group_jwt_field"
|
||||
placeholder="groups"
|
||||
{...form.register("sso_group_jwt_field")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "16px" }}>
|
||||
<Button2
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{
|
||||
backgroundColor: "#6366f1",
|
||||
borderColor: "#6366f1",
|
||||
}}
|
||||
>
|
||||
Update UI Access Control
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Saving…" : "Update UI Access Control"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue