From ce1c604be2350ab88161b95c3a700d0bf11f0b18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Apr 2026 08:48:53 +0000 Subject: [PATCH] feat(ui): migrate guardrails forms + info to shadcn Completed migrations in this batch: - src/components/guardrails.tsx: antd Tabs -> shadcn Tabs+TabsList+TabsTrigger+TabsContent. - src/components/guardrails/TeamGuardrailsTab.tsx: antd Modal + Form.useForm submission dialog -> shadcn Dialog + react-hook-form + shadcn Select/Textarea; existing categorical Tailwind palette preserved behind an eslint override. Deferred to follow-up (entries added to docs/BLOCKERS.md): - add_guardrail_form.tsx (1202 LoC, multi-step wizard w/ antd Form.useForm) - edit_guardrail_form.tsx (antd Form mirror of add form) - guardrail_info.tsx (888 LoC, Tremor tabs + antd Form settings panel) - guardrail_provider_fields.tsx, guardrail_optional_params.tsx (render antd Form.Item keyed to the parent antd Form context; cannot be migrated independently of the above parents). Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + ui/litellm-dashboard/docs/BLOCKERS.md | 29 ++ .../src/components/guardrails.tsx | 240 +++++----- .../guardrails/TeamGuardrailsTab.tsx | 411 ++++++++++++------ 4 files changed, 435 insertions(+), 246 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index efd7f8d8607..5f0a43bf194 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -106,6 +106,7 @@ "src/components/guardrails/content_filter/CategoryTable.tsx", "src/components/guardrails/content_filter/ContentCategoryConfiguration.tsx", "src/components/guardrails/custom_code/CustomCodeModal.tsx", + "src/components/guardrails/TeamGuardrailsTab.tsx", "src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx", "src/components/prompts/variable_textarea.tsx", "src/components/chat/MCPCredentialsTab.tsx", diff --git a/ui/litellm-dashboard/docs/BLOCKERS.md b/ui/litellm-dashboard/docs/BLOCKERS.md index 67f8eba5354..692941a9f18 100644 --- a/ui/litellm-dashboard/docs/BLOCKERS.md +++ b/ui/litellm-dashboard/docs/BLOCKERS.md @@ -40,3 +40,32 @@ stay on antd for phase 1 and will be addressed in a targeted follow-up. exceeds the two-attempt budget for this batch. Defer until Tremor `Accordion` has a shadcn replacement adopted and the antd Form surface can be broken into sub-forms. +- `src/components/guardrails/add_guardrail_form.tsx` (1202 LoC): multi-step + wizard driven by antd `Form.useForm` with provider-specific dynamic + fields (Presidio PII, PromptGuard, content filter, custom code, tool + permission, Azure Text Moderation). Uses `form.validateFields` per step, + `form.setFieldsValue` to apply presets, `form.resetFields`, antd + `Select` with children `Option` rendering provider logos, `Tag`, + `Typography.Title/Text/Link`. The step navigation and provider-param + validation are tightly coupled to antd's validation API. +- `src/components/guardrails/edit_guardrail_form.tsx` (451 LoC): mirrors + add_guardrail_form's provider-switching logic for edit flow. Defer with + it. +- `src/components/guardrails/guardrail_info.tsx` (888 LoC): guardrail + detail/edit page with Tremor `TabGroup`/`TabList`/`Card`/`Grid` (still + required by overview), a large antd `Form` settings panel + (`Form.useForm` + `Form.Item` rules, `Input.TextArea`, antd `Select`, + `Divider orientation="left"` section headers) that reuses + `GuardrailProviderFields` and `GuardrailOptionalParams`. Settings-panel + migration requires migrating both child components in tandem. +- `src/components/guardrails/guardrail_provider_fields.tsx` (240 LoC): + renders antd `Form.Item` elements keyed to the parent's antd Form + context (including nested `optional_params` + antd `Slider` with marks + for percentage fields). Cannot be migrated independently of its two + callers (`add_guardrail_form.tsx` + `guardrail_info.tsx`) without + breaking the form wiring. Defer with them. +- `src/components/guardrails/guardrail_optional_params.tsx` (256 LoC): + renders antd `Form.Item` for provider-specific optional params including + a dict-field builder with dynamic antd `Select.Option` lists. Same + dependency on the parent antd Form context as the above. Defer with the + parent forms. diff --git a/ui/litellm-dashboard/src/components/guardrails.tsx b/ui/litellm-dashboard/src/components/guardrails.tsx index 8e4f2567696..7fabdc28986 100644 --- a/ui/litellm-dashboard/src/components/guardrails.tsx +++ b/ui/litellm-dashboard/src/components/guardrails.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useState, useEffect } from "react"; -import { Tabs } from "antd"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -139,128 +139,130 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole return (
- - ), - }, - { - key: "guardrails", - label: "Guardrails", - children: ( - <> -
- - - - - - - - Add Provider Guardrail - - - - Create Custom Code Guardrail - - - -
+ + + {isAdmin && ( + <> + Guardrail Garden + Guardrails + + Test Playground + + + )} + Submitted Guardrails + - {selectedGuardrailId ? ( - setSelectedGuardrailId(null)} - accessToken={accessToken} - isAdmin={isAdmin} - /> - ) : ( - setSelectedGuardrailId(id)} - /> - )} + {isAdmin && ( + <> + + + + +
+ + + + + + + + Add Provider Guardrail + + + + Create Custom Code Guardrail + + + +
- + {selectedGuardrailId ? ( + setSelectedGuardrailId(null)} + accessToken={accessToken} + isAdmin={isAdmin} + /> + ) : ( + setSelectedGuardrailId(id)} + /> + )} - + - - - ), - }, - { - key: "playground", - label: "Test Playground", - disabled: !accessToken, - children: ( - {}} - /> - ), - }, - ] - : []), - { - key: "submitted", - label: "Submitted Guardrails", - children: , - }, - ]} - /> + + + +
+ + {}} + /> + + + )} + + + + +
); }; diff --git a/ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx b/ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx index b03ac92ba2b..d61c3141f98 100644 --- a/ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/TeamGuardrailsTab.tsx @@ -14,7 +14,25 @@ import { AlertCircleIcon, InfoIcon, } from "lucide-react"; -import { Modal, Form, Input, Select } from "antd"; +import { Controller, FormProvider, useForm, useFormContext } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; import { listGuardrailSubmissions, approveGuardrailSubmission, @@ -824,7 +842,6 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) { const [error, setError] = useState(null); const [searchDebounced, setSearchDebounced] = useState(""); const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false); - const [submitForm] = Form.useForm(); const registerGuardrail = useRegisterGuardrail(); useEffect(() => { @@ -1084,133 +1101,273 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) { /> )} - { - setIsSubmitModalOpen(false); - submitForm.resetFields(); + onOpenChange={setIsSubmitModalOpen} + onSubmit={async (values) => { + const litellm_params: Record = { + ...(values.extra_litellm_params + ? JSON.parse(values.extra_litellm_params) + : {}), + guardrail: "generic_guardrail_api", + mode: values.mode, + api_base: values.api_base, + }; + try { + await registerGuardrail.mutateAsync({ + team_id: values.team_id, + guardrail_name: values.guardrail_name, + litellm_params, + guardrail_info: values.guardrail_info + ? JSON.parse(values.guardrail_info) + : undefined, + }); + NotificationsManager.success("Guardrail submitted for review"); + setIsSubmitModalOpen(false); + fetchSubmissions(); + } catch { + // error already handled by networking layer + } }} - onOk={() => submitForm.submit()} - okText="Submit for Review" - > -
- Your guardrail will be sent for admin review before it becomes active. -
-
{ - const litellm_params: Record = { - ...(values.extra_litellm_params ? JSON.parse(values.extra_litellm_params) : {}), - guardrail: "generic_guardrail_api", - mode: values.mode, - api_base: values.api_base, - }; - try { - await registerGuardrail.mutateAsync({ - team_id: values.team_id, - guardrail_name: values.guardrail_name, - litellm_params, - guardrail_info: values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined, - }); - NotificationsManager.success("Guardrail submitted for review"); - setIsSubmitModalOpen(false); - submitForm.resetFields(); - fetchSubmissions(); - } catch { - // error already handled by networking layer - } - }} - > - - - - - - - - - - - - - { - if (!value) return Promise.resolve(); - try { - const parsed = JSON.parse(value); - if (typeof parsed !== "object" || Array.isArray(parsed)) { - return Promise.reject("Must be a JSON object"); - } - return Promise.resolve(); - } catch { - return Promise.reject("Invalid JSON"); - } - }, - }, - ]} - > - - - { - if (!value) return Promise.resolve(); - try { - JSON.parse(value); - return Promise.resolve(); - } catch { - return Promise.reject("Invalid JSON"); - } - }, - }, - ]} - > - - - -
+ /> ); } + +interface SubmitGuardrailFormValues { + team_id: string; + guardrail_name: string; + mode: string; + api_base: string; + extra_litellm_params?: string; + guardrail_info?: string; +} + +function SubmitGuardrailDialog({ + open, + onOpenChange, + onSubmit, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (values: SubmitGuardrailFormValues) => Promise | void; +}) { + const form = useForm({ + defaultValues: { + team_id: "", + guardrail_name: "", + mode: "pre_call", + api_base: "", + extra_litellm_params: "", + guardrail_info: "", + }, + }); + + const handleClose = (nextOpen: boolean) => { + if (!nextOpen) { + form.reset(); + } + onOpenChange(nextOpen); + }; + + const handleSubmit = form.handleSubmit(async (values) => { + await onSubmit(values); + form.reset(); + }); + + return ( + + + + Submit Guardrail for Review + + {/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */} +
+ Your guardrail will be sent for admin review before it becomes + active. +
+ +
+ + + + + + +
+
+
+ ); +} + +function SubmitGuardrailFields() { + const { register, control, formState } = + useFormContext(); + + const validateJsonObject = (value?: string) => { + if (!value) return true; + try { + const parsed = JSON.parse(value); + if (typeof parsed !== "object" || Array.isArray(parsed)) { + return "Must be a JSON object"; + } + return true; + } catch { + return "Invalid JSON"; + } + }; + + const validateJson = (value?: string) => { + if (!value) return true; + try { + JSON.parse(value); + return true; + } catch { + return "Invalid JSON"; + } + }; + + const validateUrl = (value: string) => { + if (!value) return "Enter the API base URL"; + try { + new URL(value); + return true; + } catch { + return "Must be a valid URL"; + } + }; + + return ( + <> +
+ + ( + + )} + /> + {formState.errors.team_id && ( +

+ {formState.errors.team_id.message as string} +

+ )} +
+ +
+ + + {formState.errors.guardrail_name && ( +

+ {formState.errors.guardrail_name.message as string} +

+ )} +
+ +
+ + ( + + )} + /> + {formState.errors.mode && ( +

+ {formState.errors.mode.message as string} +

+ )} +
+ +
+ + + {formState.errors.api_base && ( +

+ {formState.errors.api_base.message as string} +

+ )} +
+ +
+ +