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}
+
)}
-
);
};
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 && (
+
+ )}
+ {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)
+
-
)}