From fab8c8ec8ddbac14c2ed518bd610ca87e189c771 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 11:11:05 +0000 Subject: [PATCH] feat(ui): PolicySelector antd Select mode='multiple' \u2192 shadcn Popover chip-list - Reuses the established multi-select-with-search popover pattern. - Preserves policyVersionRef helper and getPolicyOptionEntries export (used by other policy files). - Disabled placeholder ('premium feature') and loading placeholder ('Loading policies\u2026') both surface inside the trigger. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- .../components/policies/PolicySelector.tsx | 136 ++++++++++++++---- 1 file changed, 107 insertions(+), 29 deletions(-) diff --git a/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx b/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx index 3a4c45f419f..354ebe767cc 100644 --- a/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx +++ b/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx @@ -1,18 +1,26 @@ -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 { Policy } from "./types"; import { getPoliciesList } from "../networking"; /** Prefix for policy version IDs in request body; must match backend POLICY_VERSION_ID_PREFIX. */ export const POLICY_VERSION_ID_PREFIX = "policy_"; -/** Build the value sent in the request body: policy_ so backend executes this exact version. */ export function policyVersionRef(policyId: string): string { return `${POLICY_VERSION_ID_PREFIX}${policyId}`; } -/** Build select options from policies (filter non-draft, label with name/version/status). */ -export function getPolicyOptionEntries(policies: Policy[]): { value: string; label: string }[] { +export function getPolicyOptionEntries( + policies: Policy[], +): { value: string; label: string }[] { return policies .filter((policy) => (policy.version_status ?? "draft") !== "draft") .map((policy) => { @@ -39,7 +47,6 @@ interface PolicySelectorProps { className?: string; accessToken: string; disabled?: boolean; - /** Called after policies are loaded; use to build value→label map for display elsewhere. */ onPoliciesLoaded?: (policies: Policy[]) => void; } @@ -53,6 +60,8 @@ const PolicySelector: React.FC = ({ }) => { const [policies, setPolicies] = useState([]); const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); useEffect(() => { const fetchPolicies = async () => { @@ -75,31 +84,100 @@ const PolicySelector: React.FC = ({ fetchPolicies(); }, [accessToken, onPoliciesLoaded]); - const handlePolicyChange = (selectedValues: string[]) => { - onChange(selectedValues); - }; + const options = useMemo( + () => getPolicyOptionEntries(policies), + [policies], + ); + + const selected = value ?? []; + const filteredOptions = useMemo( + () => + options + .filter((o) => !selected.includes(o.value)) + .filter((o) => + query ? o.label.toLowerCase().includes(query.toLowerCase()) : true, + ), + [options, selected, query], + ); + + const labelFor = (v: string) => + options.find((o) => o.value === v)?.label ?? v; + + const placeholder = disabled + ? "Setting policies is a premium feature." + : loading + ? "Loading policies…" + : "Select policies (production or published versions)"; return ( -
- setQuery(e.target.value)} + className="h-8 mb-2" + /> +
+ {filteredOptions.length === 0 ? ( +
+ No matches +
+ ) : ( + filteredOptions.map((opt) => ( + + )) + )} +
+ + ); };