From 43524c1f62edb17823d31efd27ac935372ea430a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 07:42:28 +0000 Subject: [PATCH] feat(ui): migrate tag-management section to shadcn - TagSelector: rewritten from antd Select mode=tags as a custom chip-input using shadcn Popover (suggestions) + Input (free-form text) + Badge (chips). Supports type-to-add-via-Enter, comma separator, Backspace to remove last tag, and click-to-add from server-provided suggestion list. - TagTable: @heroicons + @tremor + antd Tooltip \u2192 shadcn Table + TanStack-react-table + lucide Pencil/Trash2/Chevron* + shadcn Tooltip / Badge / Button. Action buttons are now real + + ))} + { + setQuery(e.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + onKeyDown={handleKeyDown} + className="border-0 shadow-none focus-visible:ring-0 h-7 px-1 flex-1 min-w-[120px]" + /> + + + e.preventDefault()} + > + {suggestions.map((t) => ( + + ))} + + ); }; diff --git a/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx b/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx index a56721787d5..f0cdcb5be84 100644 --- a/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx @@ -87,15 +87,18 @@ describe("TagTable", () => { it("should disable edit icon for dynamic spend tags", () => { render(); - const editIcon = screen.getByLabelText("Edit tag (disabled)"); - expect(editIcon).toBeInTheDocument(); - expect(editIcon).toHaveClass("cursor-not-allowed"); + // Post phase-1 migration the action buttons are + + + + + + + {isDynamicSpendTag + ? "You cannot view the information of a dynamically generated spend tag" + : tag.name} + - + ); }, }, @@ -70,9 +89,16 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag cell: ({ row }) => { const tag = row.original; return ( - - {tag.description || "-"} - + + + + {tag.description || "-"} + + {tag.description && ( + {tag.description} + )} + + ); }, }, @@ -82,18 +108,21 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag cell: ({ row }) => { const tag = row.original; return ( -
+
{tag?.models?.length === 0 ? ( - - All Models - + All Models ) : ( tag?.models?.map((modelId) => ( - - - {tag.model_info?.[modelId] || modelId} + + + + + {tag.model_info?.[modelId] || modelId} + + + {`ID: ${modelId}`} - + )) )}
@@ -104,10 +133,11 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag header: "Created", accessorKey: "created_at", sortingFn: "datetime", - cell: ({ row }) => { - const tag = row.original; - return {new Date(tag.created_at).toLocaleDateString()}; - }, + cell: ({ row }) => ( + + {new Date(row.original.created_at).toLocaleDateString()} + + ), }, { id: "actions", @@ -116,45 +146,57 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag const tag = row.original; const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION; return ( -
- {isDynamicSpendTag ? ( - - +
+ + + + + + + {isDynamicSpendTag + ? "Dynamically generated spend tags cannot be edited" + : "Edit tag"} + - ) : ( - - onEdit(tag)} - className="cursor-pointer hover:text-blue-500" - /> + + + + + + + + {isDynamicSpendTag + ? "Dynamically generated spend tags cannot be deleted" + : "Delete tag"} + - )} - {isDynamicSpendTag ? ( - - - - ) : ( - - onDelete(tag.name)} - className="cursor-pointer hover:text-red-500" - /> - - )} +
); }, @@ -164,9 +206,7 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag const table = useReactTable({ data, columns, - state: { - sorting, - }, + state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -177,39 +217,50 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( -
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )}
{header.id !== "actions" && (
{header.column.getIsSorted() ? ( { - asc: , - desc: , + asc: ( + + ), + desc: ( + + ), }[header.column.getIsSorted() as string] ) : ( - + )}
)}
-
+
))} ))} - + {table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( @@ -219,7 +270,7 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag key={cell.id} className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${ cell.column.id === "actions" - ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" + ? "sticky right-0 bg-background shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : "" }`} > @@ -230,8 +281,11 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag )) ) : ( - -
+ +

No tags found

diff --git a/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.test.tsx b/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.test.tsx index 997faf4a004..509af21e10d 100644 --- a/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.test.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.test.tsx @@ -40,15 +40,18 @@ describe("CreateTagModal", () => { const user = userEvent.setup(); render(); - const tagNameInput = screen.getByLabelText("Tag Name"); + // The label includes a required indicator; match by partial text via regex. + const tagNameInput = screen.getByLabelText(/Tag Name/); await user.type(tagNameInput, "test-tag"); const submitButton = screen.getByRole("button", { name: /Create Tag/i }); await user.click(submitButton); - expect(mockOnSubmit).toHaveBeenCalledWith({ - tag_name: "test-tag", - }); + // Post phase-1: rhf submits the full default values bag, not just the + // touched fields. Assert the tag_name made it through. + expect(mockOnSubmit).toHaveBeenCalledWith( + expect.objectContaining({ tag_name: "test-tag" }), + ); }); it("should not submit form when tag name is missing", async () => { diff --git a/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.tsx b/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.tsx index 4b1f7d34853..2fe5e71e86e 100644 --- a/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/components/CreateTagModal.tsx @@ -1,130 +1,308 @@ -import { InfoCircleOutlined } from "@ant-design/icons"; -import { Accordion, AccordionBody, AccordionHeader, Button, TextInput, Title } from "@tremor/react"; -import { Form, Input, Modal, Select as Select2, Tooltip } from "antd"; import React from "react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Info, X } from "lucide-react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; import BudgetDurationDropdown from "../../common_components/budget_duration_dropdown"; import NumericalInput from "../../shared/numerical_input"; interface ModelInfo { model_name: string; - litellm_params: { - model: string; - }; - model_info: { - id: string; - }; + litellm_params: { model: string }; + model_info: { id: string }; } +interface CreateTagFormValues { + tag_name: string; + description: string; + allowed_llms: string[]; + max_budget: number | null; + budget_duration: string | null; +} + +const defaultValues: CreateTagFormValues = { + tag_name: "", + description: "", + allowed_llms: [], + max_budget: null, + budget_duration: null, +}; + interface CreateTagModalProps { visible: boolean; onCancel: () => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any onSubmit: (values: any) => void; availableModels: ModelInfo[]; } -const CreateTagModal: React.FC = ({ visible, onCancel, onSubmit, availableModels }) => { - const [form] = Form.useForm(); +function InfoTip({ children }: { children: React.ReactNode }) { + return ( + + + + + + {children} + + + ); +} - const handleFinish = (values: any) => { +const CreateTagModal: React.FC = ({ + visible, + onCancel, + onSubmit, + availableModels, +}) => { + const form = useForm({ + defaultValues, + mode: "onSubmit", + }); + + const handleFinish = form.handleSubmit((values) => { onSubmit(values); - form.resetFields(); - }; + form.reset(defaultValues); + }); const handleCancel = () => { - form.resetFields(); + form.reset(defaultValues); onCancel(); }; + // Multi-select scaffolding (chips + add via Select). + const allowedLlms = form.watch("allowed_llms"); + const remainingModels = availableModels.filter( + (m) => !allowedLlms.includes(m.model_info.id), + ); + return ( - -
- - - - - - - - - - Allowed Models - - - - - } - name="allowed_llms" - > - - {availableModels.map((model) => ( - -
- {model.model_name} - ({model.model_info.id}) -
-
- ))} -
-
- - - - Budget & Rate Limits (Optional) - - - - Max Budget (USD){" "} - - - - - } - name="max_budget" - > - - - - Reset Budget{" "} - - - - - } - name="budget_duration" - > - form.setFieldValue("budget_duration", value)} /> - - -
-

- TPM/RPM limits for tags are not currently supported. If you need this feature, please{" "} - - create a GitHub issue - - . -

+ (!o ? handleCancel() : undefined)} + > + + + Create New Tag + + Define a new tag with allowed models and optional budget / rate + limits. + + + + +
+ + + {form.formState.errors.tag_name && ( +

+ {form.formState.errors.tag_name.message as string} +

+ )}
- - -
- -
- - +
+ +