From 90f0a4274310bcc10c40cc61288269275d3a4fe2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 19:51:47 +0000 Subject: [PATCH] feat(ui): policies/add_attachment_form to shadcn - antd Modal/Radio/Divider/Typography + @tremor Button \u2192 shadcn Dialog/Button/RadioGroup + native section divider. - antd Form/Form.Item/Select preserved (mode='tags' + tokenSeparators + filterOption). loadTeamsKeysAndModels refactored into useCallback. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- .../policies/add_attachment_form.tsx | 213 +++++++++++------- 1 file changed, 134 insertions(+), 79 deletions(-) diff --git a/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx index ccdaf01a558..f9bc5accc35 100644 --- a/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx +++ b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx @@ -1,21 +1,32 @@ -import React, { useState, useEffect } from "react"; -import { Modal, Form, Select, Radio, Divider, Typography } from "antd"; -import { Button } from "@tremor/react"; +import React, { useCallback, useState, useEffect } from "react"; +import { Form, Select } from "antd"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Policy } from "./types"; -import { teamListCall, keyListCall, modelAvailableCall, estimateAttachmentImpactCall } from "../networking"; +import { + teamListCall, + keyListCall, + modelAvailableCall, + estimateAttachmentImpactCall, +} from "../networking"; import NotificationsManager from "../molecules/notifications_manager"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { buildAttachmentData } from "./build_attachment_data"; import ImpactPreviewAlert from "./impact_preview_alert"; -const { Text } = Typography; - interface AddAttachmentFormProps { visible: boolean; onClose: () => void; onSuccess: () => void; accessToken: string | null; policies: Policy[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any createAttachment: (accessToken: string, attachmentData: any) => Promise; } @@ -37,25 +48,21 @@ const AddAttachmentForm: React.FC = ({ const [isLoadingKeys, setIsLoadingKeys] = useState(false); const [isLoadingModels, setIsLoadingModels] = useState(false); const [isEstimating, setIsEstimating] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [impactResult, setImpactResult] = useState(null); const { userId, userRole } = useAuthorized(); - useEffect(() => { - if (visible && accessToken) { - loadTeamsKeysAndModels(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible, accessToken]); - - const loadTeamsKeysAndModels = async () => { + const loadTeamsKeysAndModels = useCallback(async () => { if (!accessToken) return; - // Load teams — teamListCall returns a plain array of team objects setIsLoadingTeams(true); try { const teamsResponse = await teamListCall(accessToken, null, userId); - const teamsArray = Array.isArray(teamsResponse) ? teamsResponse : (teamsResponse?.data || []); + const teamsArray = Array.isArray(teamsResponse) + ? teamsResponse + : teamsResponse?.data || []; const teamAliases = teamsArray + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((t: any) => t.team_alias) .filter(Boolean); setAvailableTeams(teamAliases); @@ -65,12 +72,21 @@ const AddAttachmentForm: React.FC = ({ setIsLoadingTeams(false); } - // Load keys — keyListCall returns {keys: [...], total_count, ...} setIsLoadingKeys(true); try { - const keysResponse = await keyListCall(accessToken, null, null, null, null, null, 1, 100); + const keysResponse = await keyListCall( + accessToken, + null, + null, + null, + null, + null, + 1, + 100, + ); const keysArray = keysResponse?.keys || keysResponse?.data || []; const keyAliases = keysArray + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((k: any) => k.key_alias) .filter(Boolean); setAvailableKeys(keyAliases); @@ -80,12 +96,18 @@ const AddAttachmentForm: React.FC = ({ setIsLoadingKeys(false); } - // Load models setIsLoadingModels(true); try { - const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || ""); - const modelsArray = modelsResponse?.data || (Array.isArray(modelsResponse) ? modelsResponse : []); + const modelsResponse = await modelAvailableCall( + accessToken, + userId || "", + userRole || "", + ); + const modelsArray = + modelsResponse?.data || + (Array.isArray(modelsResponse) ? modelsResponse : []); const modelIds = modelsArray + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((m: any) => m.id || m.model_name) .filter(Boolean); setAvailableModels(modelIds); @@ -94,7 +116,13 @@ const AddAttachmentForm: React.FC = ({ } finally { setIsLoadingModels(false); } - }; + }, [accessToken, userId, userRole]); + + useEffect(() => { + if (visible && accessToken) { + loadTeamsKeysAndModels(); + } + }, [visible, accessToken, loadTeamsKeysAndModels]); const resetForm = () => { form.resetFields(); @@ -200,50 +228,68 @@ const AddAttachmentForm: React.FC = ({ })); return ( - (!o ? handleClose() : undefined)} > -
- + + Create Policy Attachment + + - + (option?.label ?? "") + .toLowerCase() + .includes(input.toLowerCase()) + } + style={{ width: "100%" }} + /> + + +
+ Scope +
+
+ + + + setScopeType(v as "global" | "specific") + } + className="flex flex-col gap-2" + > + + + + {scopeType === "specific" && ( <> @@ -318,9 +364,13 @@ const AddAttachmentForm: React.FC = ({ label="Tags" tooltip="Match against tags set in key or team metadata. Use exact values (e.g., healthcare) or wildcard patterns (e.g., health-*) where * matches any suffix." extra={ - - Matches tags from key/team metadata.tags or tags passed dynamically in the request body. Use * as a suffix wildcard (e.g., prod-* matches prod-us, prod-eu). - + + Matches tags from key/team metadata.tags or + tags passed dynamically in the request body. Use{" "} + * as a suffix wildcard (e.g.,{" "} + prod-* matches prod-us,{" "} + prod-eu). + } >