feat(ui): migrate general_settings + GuardrailSettingsView to shadcn

- general_settings: @tremor Card/Table/Tab*/Badge/Switch/Button/Icon +
  antd InputNumber + @heroicons TrashIcon/CheckCircleIcon \u2192 shadcn
  Card/Table/Tabs/Badge/Switch/Button/Input + lucide CheckCircle/Trash2.
- GuardrailSettingsView: antd Tag + @ant-design/icons GlobalOutlined \u2192
  shadcn Badge + lucide Globe. The killSwitchOn 'gold' (amber) badge is
  mapped to a categorical bg-amber-500 Badge \u2014 added to the raw-color
  override list since this signals 'bypassed' rather than a theme state.

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:46:36 +00:00
parent 42713bde4f
commit a35fae32b7
No known key found for this signature in database
3 changed files with 159 additions and 118 deletions

View file

@ -28,7 +28,8 @@
"src/components/GuardrailsMonitor/GuardrailDetail.tsx",
"src/components/GuardrailsMonitor/LogViewer.tsx",
"src/components/GuardrailsMonitor/GuardrailsOverview.tsx",
"src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx"
"src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx",
"src/components/GuardrailSettingsView.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -1,6 +1,7 @@
import React from "react";
import { Tag } from "antd";
import { GlobalOutlined } from "@ant-design/icons";
import { Badge } from "@/components/ui/badge";
import { Globe } from "lucide-react";
import { cn } from "@/lib/utils";
interface GuardrailSettingsViewProps {
globalGuardrailNames: Set<string>;
@ -28,43 +29,56 @@ export function GuardrailSettingsView({
);
const isEmpty =
!killSwitchOn && globalsRunning.length === 0 && nonGlobalOptIns.length === 0;
!killSwitchOn &&
globalsRunning.length === 0 &&
nonGlobalOptIns.length === 0;
const content = isEmpty ? (
<span className="block text-gray-500">No guardrails configured</span>
<span className="block text-muted-foreground">No guardrails configured</span>
) : (
<div className="flex flex-col gap-4">
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">
<GlobalOutlined style={{ marginInlineEnd: 4 }} aria-label="Global guardrail" />
<span className="block text-sm font-medium text-foreground mb-2">
<Globe
className="inline-block h-4 w-4 mr-1"
aria-label="Global guardrail"
/>
Global
</span>
{killSwitchOn ? (
<Tag color="gold">Bypassed for this team</Tag>
<Badge className="bg-amber-500 text-white hover:bg-amber-500">
Bypassed for this team
</Badge>
) : globalsRunning.length > 0 ? (
<div className="flex flex-wrap gap-2">
{globalsRunning.map((name) => (
<Tag key={name} color="blue">
<Badge key={name} variant="default">
{name}
</Tag>
</Badge>
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
<span className="block text-sm text-muted-foreground">
None configured
</span>
)}
</div>
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">Team-specific</span>
<span className="block text-sm font-medium text-foreground mb-2">
Team-specific
</span>
{nonGlobalOptIns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{nonGlobalOptIns.map((name) => (
<Tag key={name} color="blue">
<Badge key={name} variant="default">
{name}
</Tag>
</Badge>
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
<span className="block text-sm text-muted-foreground">
None configured
</span>
)}
</div>
</div>
@ -72,11 +86,18 @@ export function GuardrailSettingsView({
if (variant === "card") {
return (
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div
className={cn(
"bg-background border border-border rounded-lg p-6",
className,
)}
>
<div className="flex items-center gap-2 mb-6">
<div>
<span className="block font-semibold text-gray-900">Guardrails Settings</span>
<span className="block text-xs text-gray-500">
<span className="block font-semibold text-foreground">
Guardrails Settings
</span>
<span className="block text-xs text-muted-foreground">
Global and team-specific guardrails applied to this team
</span>
</div>
@ -87,8 +108,10 @@ export function GuardrailSettingsView({
}
return (
<div className={`${className}`}>
<span className="block font-medium text-gray-900 mb-3">Guardrails Settings</span>
<div className={className}>
<span className="block font-medium text-foreground mb-3">
Guardrails Settings
</span>
{content}
</div>
);

View file

@ -1,203 +1,220 @@
import React, { useState, useEffect } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Card,
Table,
TableHead,
TableRow,
Badge,
TableHeaderCell,
TableCell,
TableBody,
Text,
Button,
Icon,
Switch,
} from "@tremor/react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CheckCircle, Trash2 } from "lucide-react";
import {
getGeneralSettingsCall,
updateConfigFieldSetting,
deleteConfigFieldSetting,
} from "./networking";
import { InputNumber } from "antd";
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
import RouterSettings from "./router_settings";
import Fallbacks from "./Settings/RouterSettings/Fallbacks/Fallbacks";
interface GeneralSettingsPageProps {
accessToken: string | null;
userRole: string | null;
userID: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
modelData: any;
}
interface generalSettingsItem {
field_name: string;
field_type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
field_value: any;
field_description: string;
stored_in_db: boolean | null;
}
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID, modelData }) => {
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({
accessToken,
userRole,
userID,
modelData,
}) => {
const [generalSettings, setGeneralSettings] = useState<
generalSettingsItem[]
>([]);
useEffect(() => {
if (!accessToken) {
return;
}
if (!accessToken) return;
getGeneralSettingsCall(accessToken).then((data) => {
let general_settings = data;
setGeneralSettings(general_settings);
setGeneralSettings(data);
});
}, [accessToken]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleInputChange = (fieldName: string, newValue: any) => {
// Update the value in the state
const updatedSettings = generalSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting,
setting.field_name === fieldName
? { ...setting, field_value: newValue }
: setting,
);
setGeneralSettings(updatedSettings);
};
const handleUpdateField = (fieldName: string, idx: number) => {
if (!accessToken) {
return;
}
let fieldValue = generalSettings[idx].field_value;
if (fieldValue == null || fieldValue == undefined) {
return;
}
if (!accessToken) return;
const fieldValue = generalSettings[idx].field_value;
if (fieldValue == null || fieldValue == undefined) return;
try {
updateConfigFieldSetting(accessToken, fieldName, fieldValue);
// update value in state
const updatedSettings = generalSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, stored_in_db: true } : setting,
setting.field_name === fieldName
? { ...setting, stored_in_db: true }
: setting,
);
setGeneralSettings(updatedSettings);
} catch (error) {
// do something
} catch {
// ignore
}
};
const handleResetField = (fieldName: string, idx: number) => {
if (!accessToken) {
return;
}
const handleResetField = (fieldName: string) => {
if (!accessToken) return;
try {
deleteConfigFieldSetting(accessToken, fieldName);
// update value in state
const updatedSettings = generalSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, stored_in_db: null, field_value: null } : setting,
setting.field_name === fieldName
? { ...setting, stored_in_db: null, field_value: null }
: setting,
);
setGeneralSettings(updatedSettings);
} catch (error) {
// do something
} catch {
// ignore
}
};
if (!accessToken) {
return null;
}
if (!accessToken) return null;
return (
<div className="w-full">
<TabGroup className="h-[75vh] w-full">
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Fallbacks</Tab>
<Tab value="3">General</Tab>
</TabList>
<TabPanels className="px-8 py-6">
<TabPanel>
<Tabs defaultValue="loadbalancing" className="h-[75vh] w-full">
<TabsList className="px-8 pt-4 mx-8">
<TabsTrigger value="loadbalancing">Loadbalancing</TabsTrigger>
<TabsTrigger value="fallbacks">Fallbacks</TabsTrigger>
<TabsTrigger value="general">General</TabsTrigger>
</TabsList>
<div className="px-8 py-6">
<TabsContent value="loadbalancing">
<RouterSettings
accessToken={accessToken}
userRole={userRole}
userID={userID}
modelData={modelData}
/>
</TabPanel>
<TabPanel>
</TabsContent>
<TabsContent value="fallbacks">
<Fallbacks
accessToken={accessToken}
userRole={userRole}
userID={userID}
modelData={modelData}
/>
</TabPanel>
<TabPanel>
<Card>
</TabsContent>
<TabsContent value="general">
<Card className="p-6">
<Table>
<TableHead>
<TableHeader>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
<TableHead>Setting</TableHead>
<TableHead>Value</TableHead>
<TableHead>Status</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHead>
</TableHeader>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary")
.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.field_name}</Text>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1"
>
<p>{value.field_name}</p>
<p className="text-[0.65rem] text-muted-foreground italic mt-1">
{value.field_description}
</p>
</TableCell>
<TableCell>
{value.field_type == "Integer" ? (
<InputNumber
{value.field_type === "Integer" ? (
<Input
type="number"
step={1}
value={value.field_value}
onChange={(newValue) => handleInputChange(value.field_name, newValue)}
value={value.field_value ?? ""}
onChange={(e) =>
handleInputChange(
value.field_name,
e.target.value === ""
? null
: Number(e.target.value),
)
}
/>
) : value.field_type == "Boolean" ? (
) : value.field_type === "Boolean" ? (
<Switch
checked={value.field_value === true || value.field_value === "true"}
onChange={(checked) => handleInputChange(value.field_name, checked)}
checked={
value.field_value === true ||
value.field_value === "true"
}
onCheckedChange={(checked) =>
handleInputChange(value.field_name, checked)
}
/>
) : null}
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<Badge icon={CheckCircleIcon} className="text-white">
{value.stored_in_db === true ? (
<Badge className="gap-1">
<CheckCircle size={12} />
In DB
</Badge>
) : value.stored_in_db == false ? (
<Badge className="text-gray bg-white outline">In Config</Badge>
) : value.stored_in_db === false ? (
<Badge variant="outline">In Config</Badge>
) : (
<Badge className="text-gray bg-white outline">Not Set</Badge>
<Badge variant="outline">Not Set</Badge>
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name, index)}>Update</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
Reset
</Icon>
<div className="flex items-center gap-2">
<Button
size="sm"
onClick={() =>
handleUpdateField(value.field_name, index)
}
>
Update
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
onClick={() => handleResetField(value.field_name)}
aria-label="Reset"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</TabsContent>
</div>
</Tabs>
</div>
);
};