From 591c2c035af5b856c0f461ad8595a447525a3c58 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 10:40:24 +0000 Subject: [PATCH] feat(ui): common_components \u2014 AccessGroupSelector + DeleteResourceModal + PassThroughSecuritySection - AccessGroupSelector: antd Select mode='multiple' with ant Skeleton loading state \u2192 shadcn Popover + chip-list multi-select (same pattern established in MCPServerSelector / VectorStoreSelector). Skeleton from shadcn. - PassThroughSecuritySection: @tremor Card/Title/Subtitle/Text + antd Form.Item/Switch \u2192 shadcn Card/Switch + semantic Tailwind. Amber upgrade-prompt callout added to .eslintrc.json overrides. - DeleteResourceModal: antd Modal/Alert/Card/Descriptions/Input/ Typography/theme.useToken \u2192 shadcn AlertDialog/Card/Input + lucide AlertCircle. Resource information rendered as a two-column grid inside a destructive-tinted Card (replaces antd Descriptions). Confirmation input preserved; Delete action is disabled until the confirmation string matches. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 4 +- .../common_components/AccessGroupSelector.tsx | 190 ++++++++++++------ .../common_components/DeleteResourceModal.tsx | 182 ++++++++++------- .../PassThroughSecuritySection.tsx | 54 ++--- 4 files changed, 266 insertions(+), 164 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 52f6d6d108b..87440dd7232 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -44,7 +44,9 @@ "src/components/shared/usage_date_picker.tsx", "src/components/shared/advanced_date_picker.tsx", "src/components/activity_metrics.tsx", - "src/components/skill_hub_table_columns.tsx" + "src/components/skill_hub_table_columns.tsx", + "src/components/common_components/PassThroughSecuritySection.tsx", + "src/components/common_components/DeleteResourceModal.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx b/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx index 392587d910a..a586aacf0ec 100644 --- a/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/AccessGroupSelector.tsx @@ -1,7 +1,14 @@ -import React from "react"; -import { Select, Skeleton } from "antd"; -import { TeamOutlined } from "@ant-design/icons"; -import { Text } from "@tremor/react"; +import React, { useMemo, useState } from "react"; +import { Users, X } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; import { useAccessGroups, AccessGroupResponse, @@ -16,17 +23,16 @@ export interface AccessGroupSelectorProps { className?: string; showLabel?: boolean; labelText?: string; - /** Allow clearing the selection */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars allowClear?: boolean; } /** - * Reusable multi-select selector for access groups. + * Reusable multi-select for access groups (shadcn Popover + chip list). * - * - Displays the **access_group_name** in the dropdown. - * - Returns an array of **access_group_id** values. - * - Always multi-select since users can assign multiple access groups. - * - Integrates with Ant Design `` out of the box via `value` / `onChange`. + * - Displays the `access_group_name` in chips and options. + * - Returns an array of `access_group_id` values. + * - Drop-in replacement for the old antd Select mode='multiple'. */ const AccessGroupSelector: React.FC = ({ value, @@ -37,74 +43,134 @@ const AccessGroupSelector: React.FC = ({ className, showLabel = false, labelText = "Access Group", - allowClear = true, }) => { const { data: accessGroups, isLoading, isError } = useAccessGroups(); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const selected = value ?? []; + + const allOptions = useMemo( + () => + (accessGroups ?? []).map((group: AccessGroupResponse) => ({ + id: group.access_group_id, + name: group.access_group_name, + searchText: `${group.access_group_name} ${group.access_group_id}`, + })), + [accessGroups], + ); + + const filteredOptions = useMemo( + () => + allOptions + .filter((o) => !selected.includes(o.id)) + .filter((o) => + query + ? o.searchText.toLowerCase().includes(query.toLowerCase()) + : true, + ), + [allOptions, selected, query], + ); + + const labelFor = (id: string) => + allOptions.find((o) => o.id === id)?.name ?? id; - // ── Loading skeleton ───────────────────────────────────────────────────── if (isLoading) { return (
{showLabel && ( - - {labelText} - +
+ {labelText} +
)} - +
); } - // ── Build options ──────────────────────────────────────────────────────── - const options = (accessGroups ?? []).map((group: AccessGroupResponse) => ({ - label: ( - - {group.access_group_name}{" "} - ({group.access_group_id}) - - ), - value: group.access_group_id, - selectedLabel: group.access_group_name, - searchText: `${group.access_group_name} ${group.access_group_id}`, - })); - - // ── Render ─────────────────────────────────────────────────────────────── return (
{showLabel && ( - - {labelText} - +
+ {labelText} +
)} - setQuery(e.target.value)} + className="h-8 mb-2" + /> +
+ {isError ? ( +
+ Failed to load access groups +
+ ) : filteredOptions.length === 0 ? ( +
+ No access groups found +
+ ) : ( + filteredOptions.map((opt) => ( + + )) + )} +
+ +
); }; diff --git a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx index 26419585a4c..3ac7a4b46fb 100644 --- a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx +++ b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx @@ -1,5 +1,16 @@ -import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd"; -import { ExclamationCircleOutlined } from "@ant-design/icons"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { AlertCircle } from "lucide-react"; import React, { useState, useEffect } from "react"; interface DeleteResourceModalProps { @@ -8,12 +19,16 @@ interface DeleteResourceModalProps { alertMessage?: string; message: string; resourceInformationTitle?: string; - resourceInformation?: Array< - { - label: string; - value: string | number | undefined | null; - } & Omit, "children"> - >; + resourceInformation?: Array<{ + label: string; + value: string | number | undefined | null; + // Kept for back-compat with callers that used to spread antd + // Typography.Text props (e.g. `type="danger"`, `strong`). Now only + // `className` is applied. + className?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + }>; onCancel: () => void; onOk: () => void; confirmLoading: boolean; @@ -32,80 +47,97 @@ export default function DeleteResourceModal({ confirmLoading, requiredConfirmation, }: DeleteResourceModalProps) { - const { Title, Text } = Typography; - const { token } = theme.useToken(); - const [requiredConfirmationInput, setRequiredConfirmationInput] = useState(""); + const [requiredConfirmationInput, setRequiredConfirmationInput] = + useState(""); useEffect(() => { - if (isOpen) { - setRequiredConfirmationInput(""); - } + if (isOpen) setRequiredConfirmationInput(""); }, [isOpen]); + const confirmationBlocked = + !!requiredConfirmation && requiredConfirmationInput !== requiredConfirmation; + return ( - (!o ? onCancel() : undefined)} > -
- {alertMessage && } - - - {resourceInformation && - resourceInformation.map(({ label, value, ...textProps }) => ( - {label}}> - {value ?? "-"} - - ))} - - -
- {message} + + + {title} + {/* Hoist alert + resource info up so the AlertDialogDescription + stays a plain string/paragraph (it must be a DIV-less element + inside

to satisfy Radix/a11y). */} + + +

+ {alertMessage && ( +
+

+ {alertMessage} +

+
+ )} + {resourceInformationTitle || resourceInformation?.length ? ( + + {resourceInformationTitle && ( +
+ + {resourceInformationTitle} + +
+ )} +
+ {resourceInformation?.map(({ label, value, className }) => ( + + {label} + {value ?? "-"} + + ))} +
+
+ ) : null} + {message} + {requiredConfirmation && ( +
+
+ Type{" "} + + {requiredConfirmation} + {" "} + to confirm deletion: +
+
+ + + setRequiredConfirmationInput(e.target.value) + } + placeholder={requiredConfirmation} + className="pl-7" + autoFocus + /> +
+
+ )}
- {requiredConfirmation && ( -
- - Type - - {requiredConfirmation} - - to confirm deletion: - - setRequiredConfirmationInput(e.target.value)} - placeholder={requiredConfirmation} - className="rounded-md" - prefix={} - autoFocus - /> -
- )} -
- + + + Cancel + + { + e.preventDefault(); + if (!confirmationBlocked) onOk(); + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {confirmLoading ? "Deleting..." : "Delete"} + + + + ); } diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx index c63770d3c85..b5b8fc2fbfc 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Card, Title, Subtitle, Text } from "@tremor/react"; -import { Form, Switch } from "antd"; +import { Card } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; export interface PassThroughSecuritySectionProps { premiumUser: boolean; @@ -8,10 +8,6 @@ export interface PassThroughSecuritySectionProps { onAuthChange: (checked: boolean) => void; } -/** - * Reusable Security section for pass-through endpoints - * Shows authentication toggle for premium users or upgrade message for free users - */ const PassThroughSecuritySection: React.FC = ({ premiumUser, authEnabled, @@ -19,33 +15,39 @@ const PassThroughSecuritySection: React.FC = ({ }) => { return ( - Security - - When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key - +

Security

+

+ When enabled, requests to this endpoint will require a valid LiteLLM + Virtual Key +

{premiumUser ? ( - - { - onAuthChange(checked); - }} - /> - + onAuthChange(checked)} + /> ) : (
-
- - Authentication (Premium) +
+ + + Authentication (Premium) +
-
- - Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "} - +
)}