From 298489075a26a9dede2d051e5e7137f20359019e Mon Sep 17 00:00:00 2001 From: cursor Date: Fri, 24 Apr 2026 11:09:07 +0000 Subject: [PATCH] feat(ui): migrate SearchTools subtree to shadcn Migrates the SearchTools CRUD surface: - SearchToolColumn.tsx: antd ColumnsType -> tanstack-table ColumnDef[] (matches the DataTable wrapper already used elsewhere; header/accessorKey/cell shape). - SearchTools.tsx: antd Table -> shadcn DataTable (reusing view_logs/table.tsx which is already tremor-inside-shadcn-chrome). antd Form.useForm (edit modal) -> react-hook-form (FormProvider + register + Controller). antd Input/Select/TextArea -> shadcn Input/ Select/Textarea with Label. Dialog wrapper unchanged. - CreateSearchTools.tsx: antd Modal -> shadcn Dialog. antd Form.useForm + onValuesChange -> react-hook-form (watch() replaces the manual formValues state). antd Select + Option (with logo-prefixed label) -> shadcn Select + SelectItem rendering SearchProviderLabel directly. antd Tooltip + Typography.Link -> shadcn Tooltip + plain anchor. antd Input/Input.Password/Input.TextArea -> shadcn Input (type=password for API key) + Textarea. Form.Item info-icon helper tooltips preserved via shadcn Tooltip. - SearchToolTester.tsx: antd Button/Input/Spin/Typography + tremor Card/Title -> shadcn Button/Input/Card + plain h3/p. Loader2 for the Spin replacement. Categorical palette (blue accent, green success) kept raw with eslint override. SearchToolTester.tsx gets added to .eslintrc.json's no-raw-tailwind-colors override list (blue accent / green success palette on result cards). Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- .../SearchTools/CreateSearchTools.tsx | 462 ++++++++++-------- .../SearchTools/SearchToolColumn.tsx | 215 ++++---- .../SearchTools/SearchToolTester.tsx | 210 ++++---- .../components/SearchTools/SearchTools.tsx | 284 ++++++----- ui/litellm-dashboard/tsconfig.tsbuildinfo | 2 +- 6 files changed, 620 insertions(+), 556 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index d4b772f0be7..567170665dc 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -161,7 +161,8 @@ "src/components/UsagePage/components/EntityUsage/EntityUsage.tsx", "src/components/UsagePage/components/UsageAIChatPanel.tsx", "src/components/UsagePage/components/UsagePageView.tsx", - "src/components/prompts/prompt_info.tsx" + "src/components/prompts/prompt_info.tsx", + "src/components/SearchTools/SearchToolTester.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx b/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx index dcd6f732083..043a1d42568 100644 --- a/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx +++ b/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx @@ -1,18 +1,38 @@ import { isAdminRole } from "@/utils/roles"; import { Info as InfoCircleOutlined } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports -import { Button, TextInput } from "@tremor/react"; -import { Form, Input, Modal, Select, Tooltip, Typography } from "antd"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + 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 Image from "next/image"; import React, { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; import NotificationsManager from "../molecules/notifications_manager"; import { createSearchTool, fetchAvailableSearchProviders } from "../networking"; import SearchConnectionTest from "./SearchConnectionTest"; import { AvailableSearchProvider, SearchTool } from "./types"; -const { TextArea } = Input; - // Search provider logos folder path (matches existing provider logo pattern) const searchProviderLogosFolder = "../ui/assets/logos/"; @@ -21,7 +41,6 @@ const getSearchProviderLogo = (providerName: string): string => { return `${searchProviderLogosFolder}${providerName}.png`; }; -// Component to display search provider logo and name interface SearchProviderLabelProps { providerName: string; displayName: string; @@ -54,6 +73,16 @@ interface CreateSearchToolProps { setModalVisible: (visible: boolean) => void; } +interface CreateFormValues { + search_tool_name: string; + search_provider: string; + api_key?: string; + api_base?: string; + description?: string; +} + +const SEARCH_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; + const CreateSearchTool: React.FC = ({ userRole, accessToken, @@ -61,14 +90,20 @@ const CreateSearchTool: React.FC = ({ isModalVisible, setModalVisible, }) => { - const [form] = Form.useForm(); + const form = useForm({ + defaultValues: { + search_tool_name: "", + search_provider: "", + api_key: "", + api_base: "", + description: "", + }, + }); const [isLoading, setIsLoading] = useState(false); - const [formValues, setFormValues] = useState>({}); const [isTestModalVisible, setIsTestModalVisible] = useState(false); const [isTestingConnection, setIsTestingConnection] = useState(false); const [connectionTestId, setConnectionTestId] = useState(""); - // Fetch available search providers const { data: providersResponse, isLoading: isLoadingProviders, @@ -83,23 +118,20 @@ const CreateSearchTool: React.FC = ({ const availableProviders = providersResponse?.providers || []; - const handleCreate = async (formValues: Record) => { + const onSubmit = form.handleSubmit(async (formValues) => { setIsLoading(true); try { - // Prepare the payload const payload = { search_tool_name: formValues.search_tool_name, litellm_params: { search_provider: formValues.search_provider, api_key: formValues.api_key, api_base: formValues.api_base, - timeout: formValues.timeout ? parseFloat(formValues.timeout) : undefined, - max_retries: formValues.max_retries ? parseInt(formValues.max_retries) : undefined, }, search_tool_info: formValues.description ? { - description: formValues.description, - } + description: formValues.description, + } : undefined, }; @@ -107,10 +139,8 @@ const CreateSearchTool: React.FC = ({ if (accessToken != null) { const response = await createSearchTool(accessToken, payload); - NotificationsManager.success("Search tool created successfully"); - form.resetFields(); - setFormValues({}); + form.reset(); setModalVisible(false); onCreateSuccess(response); } @@ -119,219 +149,237 @@ const CreateSearchTool: React.FC = ({ } finally { setIsLoading(false); } - }; + }); const handleCancel = () => { - form.resetFields(); - setFormValues({}); + form.reset(); setModalVisible(false); }; const handleTestConnection = async () => { - try { - // Validate required fields for testing - await form.validateFields(["search_provider", "api_key"]); - - setIsTestingConnection(true); - // Generate a new test ID (using timestamp for uniqueness) - setConnectionTestId(`test-${Date.now()}`); - // Show the modal with the fresh test - setIsTestModalVisible(true); - } catch (error) { + const valid = await form.trigger(["search_provider", "api_key"]); + if (!valid) { NotificationsManager.error("Please fill in Search Provider and API Key before testing"); + return; } + + setIsTestingConnection(true); + setConnectionTestId(`test-${Date.now()}`); + setIsTestModalVisible(true); }; - // Clear formValues when modal closes to reset + // Clear form when modal closes to reset React.useEffect(() => { if (!isModalVisible) { - setFormValues({}); + form.reset(); } - }, [isModalVisible]); + }, [isModalVisible, form]); + + const formValues = form.watch(); if (!isAdminRole(userRole)) { return null; } return ( - - 🔍 -

Add New Search Tool

- - } - open={isModalVisible} - width={800} - onCancel={handleCancel} - footer={null} - className="top-8" - styles={{ - body: { padding: "24px" }, - header: { padding: "24px 24px 0 24px", border: "none" }, - }} - > -
-
setFormValues(allValues)} - layout="vertical" - className="space-y-6" - > -
- - Search Tool Name - - - - - } - name="search_tool_name" - rules={[ - { required: true, message: "Please enter a search tool name" }, - { - pattern: /^[a-zA-Z0-9_-]+$/, - message: "Name can only contain letters, numbers, hyphens, and underscores", - }, - ]} - > - - - - - Search Provider - - - - - } - name="search_provider" - rules={[{ required: true, message: "Please select a search provider" }]} - > - - - - - API Key - - - - - } - name="api_key" - rules={[{ required: false, message: "Please enter an API key" }]} - > - - - - Description (Optional)} - name="description" - > -