diff --git a/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx b/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx index 0e0628cd18f..3ed2faada76 100644 --- a/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx @@ -1,5 +1,13 @@ -import React, { useEffect, useState } from "react"; -import { Select } from "antd"; +import React, { useEffect, useMemo, useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; import { Guardrail } from "./types"; import { getGuardrailsList } from "../networking"; @@ -11,60 +19,123 @@ interface GuardrailSelectorProps { disabled?: boolean; } -const GuardrailSelector: React.FC = ({ onChange, value, className, accessToken, disabled }) => { +/** + * Multi-select for guardrails. Mirrors the VectorStoreSelector pattern + * (chip-style trigger + popover suggestion list with search) since shadcn + * lacks a native multi-select primitive. + */ +const GuardrailSelector: React.FC = ({ + onChange, + value, + className, + accessToken, + disabled, +}) => { const [guardrails, setGuardrails] = useState([]); - const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const selected = useMemo(() => value ?? [], [value]); useEffect(() => { const fetchGuardrails = async () => { if (!accessToken) return; - - setLoading(true); try { const response = await getGuardrailsList(accessToken); - console.log("Guardrails response:", response); - if (response.guardrails) { - console.log("Guardrails data:", response.guardrails); - setGuardrails(response.guardrails); - } + if (response.guardrails) setGuardrails(response.guardrails); } catch (error) { console.error("Error fetching guardrails:", error); - } finally { - setLoading(false); } }; - fetchGuardrails(); }, [accessToken]); - const handleGuardrailChange = (selectedValues: string[]) => { - console.log("Selected guardrails:", selectedValues); - onChange(selectedValues); - }; + const remaining = useMemo( + () => + guardrails + .filter((g) => g.guardrail_name && !selected.includes(g.guardrail_name)) + .filter((g) => + query + ? (g.guardrail_name ?? "") + .toLowerCase() + .includes(query.toLowerCase()) + : true, + ), + [guardrails, selected, query], + ); return ( -
- setQuery(e.target.value)} + className="h-8 mb-2" + /> +
+ {remaining.length === 0 ? ( +
+ No matches +
+ ) : ( + remaining.map((g) => { + const name = g.guardrail_name as string; + return ( + + ); + }) + )} +
+ + ); }; diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_garden_card.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_garden_card.tsx index 1b9e1638041..f86786103e0 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_garden_card.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_garden_card.tsx @@ -1,27 +1,17 @@ import React, { useState } from "react"; -import { CheckCircleFilled } from "@ant-design/icons"; +import { CheckCircle2 } from "lucide-react"; import { GuardrailCardInfo } from "./guardrail_garden_data"; +import { cn } from "@/lib/utils"; -const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }) => { +const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ + src, + name, +}) => { const [hasError, setHasError] = useState(false); if (hasError || !src) { return ( -
+
{name?.charAt(0) || "?"}
); @@ -31,52 +21,41 @@ const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name } setHasError(true)} /> ); }; -const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }> = ({ card, onClick }) => { - const [hovered, setHovered] = useState(false); - +const GuardrailCard: React.FC<{ + card: GuardrailCardInfo; + onClick: () => void; +}> = ({ card, onClick }) => { return (
setHovered(true)} - onMouseLeave={() => setHovered(false)} - style={{ - borderRadius: 12, - border: hovered ? "1px solid #93c5fd" : "1px solid #e5e7eb", - backgroundColor: "#ffffff", - padding: "20px 20px 16px 20px", - cursor: "pointer", - transition: "border-color 0.15s, box-shadow 0.15s", - display: "flex", - flexDirection: "column", - minHeight: 170, - boxShadow: hovered ? "0 1px 6px rgba(59,130,246,0.08)" : "none", - }} + className={cn( + "rounded-xl border border-border bg-background px-5 pt-5 pb-4", + "cursor-pointer transition-colors transition-shadow", + "hover:border-primary/50 hover:shadow-sm", + "flex flex-col min-h-[170px]", + )} > - {/* Icon + Name row */} -
+
- {card.name} + + {card.name} +
- {/* Description */} -

+

{card.description}

- {/* Eval badge */} {card.eval && ( -
- - +
+ + F1: {card.eval.f1}% · {card.eval.testCases} test cases