feat(ui): migrate SearchTools subtree to shadcn

Migrates the SearchTools CRUD surface:

- SearchToolColumn.tsx: antd ColumnsType -> tanstack-table
  ColumnDef<SearchTool>[] (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 <yuneng-berri@users.noreply.github.com>
This commit is contained in:
cursor 2026-04-24 11:09:07 +00:00
parent 68bbcae478
commit 298489075a
No known key found for this signature in database
6 changed files with 620 additions and 556 deletions

View file

@ -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",

View file

@ -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<CreateSearchToolProps> = ({
userRole,
accessToken,
@ -61,14 +90,20 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
isModalVisible,
setModalVisible,
}) => {
const [form] = Form.useForm();
const form = useForm<CreateFormValues>({
defaultValues: {
search_tool_name: "",
search_provider: "",
api_key: "",
api_base: "",
description: "",
},
});
const [isLoading, setIsLoading] = useState(false);
const [formValues, setFormValues] = useState<Record<string, any>>({});
const [isTestModalVisible, setIsTestModalVisible] = useState(false);
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [connectionTestId, setConnectionTestId] = useState<string>("");
// Fetch available search providers
const {
data: providersResponse,
isLoading: isLoadingProviders,
@ -83,23 +118,20 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
const availableProviders = providersResponse?.providers || [];
const handleCreate = async (formValues: Record<string, any>) => {
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<CreateSearchToolProps> = ({
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<CreateSearchToolProps> = ({
} 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 (
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<span className="text-2xl">🔍</span>
<h2 className="text-xl font-semibold text-gray-900">Add New Search Tool</h2>
</div>
}
open={isModalVisible}
width={800}
onCancel={handleCancel}
footer={null}
className="top-8"
styles={{
body: { padding: "24px" },
header: { padding: "24px 24px 0 24px", border: "none" },
}}
>
<div className="mt-6">
<Form
form={form}
onFinish={handleCreate}
onValuesChange={(_, allValues) => setFormValues(allValues)}
layout="vertical"
className="space-y-6"
>
<div className="grid grid-cols-1 gap-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Search Tool Name
<Tooltip title="A unique name to identify this search tool configuration (e.g., 'perplexity-search', 'tavily-news-search').">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
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",
},
]}
>
<TextInput
placeholder="e.g., perplexity-search, my-tavily-tool"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Search Provider
<Tooltip title="Select the search provider you want to use. Each provider has different capabilities and pricing.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="search_provider"
rules={[{ required: true, message: "Please select a search provider" }]}
>
<Select
placeholder="Select a search provider"
className="rounded-lg"
size="large"
loading={isLoadingProviders}
showSearch
optionFilterProp="children"
optionLabelProp="label"
>
{availableProviders.map((provider) => (
<Select.Option
key={provider.provider_name}
value={provider.provider_name}
label={
<SearchProviderLabel
providerName={provider.provider_name}
displayName={provider.ui_friendly_name}
/>
}
>
<SearchProviderLabel
providerName={provider.provider_name}
displayName={provider.ui_friendly_name}
/>
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
API Key
<Tooltip title="The API key for authenticating with the search provider. This will be securely stored.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="api_key"
rules={[{ required: false, message: "Please enter an API key" }]}
>
<TextInput
type="password"
placeholder="Enter your API key"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Description (Optional)</span>}
name="description"
>
<TextArea
rows={3}
placeholder="Brief description of this search tool's purpose"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
</div>
<div className="flex justify-between items-center pt-6 border-t border-gray-100">
<Tooltip title="Get help on our github">
<Typography.Link href="https://github.com/BerriAI/litellm/issues" target="_blank">
Need Help?
</Typography.Link>
</Tooltip>
<div className="space-x-2">
<Button onClick={handleTestConnection} loading={isTestingConnection}>
Test Connection
</Button>
<Button loading={isLoading} type="submit">
Add Search Tool
</Button>
<Dialog open={isModalVisible} onOpenChange={(o) => (!o ? handleCancel() : undefined)}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle asChild>
<div className="flex items-center space-x-3 pb-4 border-b border-border">
<span className="text-2xl">🔍</span>
<h2 className="text-xl font-semibold text-foreground">Add New Search Tool</h2>
</div>
</div>
</Form>
</div>
</DialogTitle>
</DialogHeader>
{/* Test Connection Results Modal */}
<Modal
title="Connection Test Results"
open={isTestModalVisible}
onCancel={() => {
setIsTestModalVisible(false);
setIsTestingConnection(false);
}}
footer={[
<Button
key="close"
onClick={() => {
<FormProvider {...form}>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-6">
<div className="space-y-2">
<Label htmlFor="create-search-tool-name" className="flex items-center">
Search Tool Name <span className="text-destructive ml-1">*</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
A unique name to identify this search tool configuration
(e.g., &quot;perplexity-search&quot;, &quot;tavily-news-search&quot;).
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="create-search-tool-name"
placeholder="e.g., perplexity-search, my-tavily-tool"
{...form.register("search_tool_name", {
required: "Please enter a search tool name",
pattern: {
value: SEARCH_TOOL_NAME_PATTERN,
message: "Name can only contain letters, numbers, hyphens, and underscores",
},
})}
/>
{form.formState.errors.search_tool_name && (
<p className="text-sm text-destructive">
{form.formState.errors.search_tool_name.message as string}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="create-search-provider" className="flex items-center">
Search Provider <span className="text-destructive ml-1">*</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
Select the search provider you want to use. Each provider has different capabilities and pricing.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Controller
control={form.control}
name="search_provider"
rules={{ required: "Please select a search provider" }}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} disabled={isLoadingProviders}>
<SelectTrigger id="create-search-provider">
<SelectValue placeholder="Select a search provider" />
</SelectTrigger>
<SelectContent>
{availableProviders.map((provider) => (
<SelectItem key={provider.provider_name} value={provider.provider_name}>
<SearchProviderLabel
providerName={provider.provider_name}
displayName={provider.ui_friendly_name}
/>
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{form.formState.errors.search_provider && (
<p className="text-sm text-destructive">
{form.formState.errors.search_provider.message as string}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="create-api-key" className="flex items-center">
API Key
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
The API key for authenticating with the search provider. This will be securely stored.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="create-api-key"
type="password"
placeholder="Enter your API key"
{...form.register("api_key")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="create-description">Description (Optional)</Label>
<Textarea
id="create-description"
rows={3}
placeholder="Brief description of this search tool's purpose"
{...form.register("description")}
/>
</div>
</div>
<DialogFooter className="pt-6 border-t border-border flex !justify-between">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://github.com/BerriAI/litellm/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm"
>
Need Help?
</a>
</TooltipTrigger>
<TooltipContent>Get help on our github</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="space-x-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={isTestingConnection}
>
{isTestingConnection ? "Testing..." : "Test Connection"}
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Adding..." : "Add Search Tool"}
</Button>
</div>
</DialogFooter>
</form>
</FormProvider>
{/* Test Connection Results Dialog */}
<Dialog
open={isTestModalVisible}
onOpenChange={(o) => {
if (!o) {
setIsTestModalVisible(false);
setIsTestingConnection(false);
}}
>
Close
</Button>,
]}
width={700}
>
{/* Only render the SearchConnectionTest when modal is visible and we have a test ID */}
{isTestModalVisible && accessToken && (
<SearchConnectionTest
key={connectionTestId}
litellmParams={{
search_provider: formValues.search_provider,
api_key: formValues.api_key,
api_base: formValues.api_base,
}}
accessToken={accessToken}
onTestComplete={() => setIsTestingConnection(false)}
/>
)}
</Modal>
</Modal>
}
}}
>
<DialogContent className="max-w-[700px]">
<DialogHeader>
<DialogTitle>Connection Test Results</DialogTitle>
</DialogHeader>
{isTestModalVisible && accessToken && (
<SearchConnectionTest
key={connectionTestId}
litellmParams={{
search_provider: formValues.search_provider,
api_key: formValues.api_key,
api_base: formValues.api_base,
}}
accessToken={accessToken}
onTestComplete={() => setIsTestingConnection(false)}
/>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsTestModalVisible(false);
setIsTestingConnection(false);
}}
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);
};
export default CreateSearchTool;

View file

@ -1,6 +1,6 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ColumnsType } from "antd/es/table";
import { ColumnDef } from "@tanstack/react-table";
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { SearchTool } from "./types";
@ -9,115 +9,116 @@ export const searchToolColumns = (
onEdit: (searchToolId: string) => void,
onDelete: (searchToolId: string) => void,
availableProviders: Array<{ provider_name: string; ui_friendly_name: string }>,
): ColumnsType<SearchTool> => [
{
title: "Search Tool ID",
dataIndex: "search_tool_id",
key: "search_tool_id",
render: (_, tool) => {
const isFromConfig = tool.is_from_config;
): ColumnDef<SearchTool>[] => [
{
header: "Search Tool ID",
accessorKey: "search_tool_id",
cell: ({ row }) => {
const tool = row.original;
const isFromConfig = tool.is_from_config;
if (isFromConfig) {
return <span className="text-xs">-</span>;
}
if (isFromConfig) {
return <span className="text-xs">-</span>;
}
return (
<button
type="button"
onClick={() => onView(tool.search_tool_id!)}
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40 rounded"
>
<span className="truncate block">{tool.search_tool_id}</span>
</button>
);
},
return (
<button
type="button"
onClick={() => onView(tool.search_tool_id!)}
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40 rounded"
>
<span className="truncate block">{tool.search_tool_id}</span>
</button>
);
},
{
title: "Name",
dataIndex: "search_tool_name",
key: "search_tool_name",
render: (name: string) => <span className="font-medium">{name}</span>,
},
{
header: "Name",
accessorKey: "search_tool_name",
cell: (info) => <span className="font-medium">{info.getValue() as string}</span>,
},
{
header: "Provider",
id: "provider",
cell: ({ row }) => {
const tool = row.original;
const provider = tool.litellm_params.search_provider;
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
const displayName = providerInfo?.ui_friendly_name || provider;
return <span className="text-sm">{displayName}</span>;
},
{
title: "Provider",
key: "provider",
render: (_, tool) => {
const provider = tool.litellm_params.search_provider;
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
const displayName = providerInfo?.ui_friendly_name || provider;
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => (
<span className="text-xs">
{row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"}
</span>
),
},
{
header: "Updated At",
accessorKey: "updated_at",
cell: ({ row }) => (
<span className="text-xs">
{row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"}
</span>
),
},
{
header: "Source",
id: "source",
cell: ({ row }) => {
const isFromConfig = row.original.is_from_config ?? false;
return (
<Badge
className={cn(
"text-xs",
isFromConfig
? "bg-muted text-muted-foreground"
: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
)}
>
{isFromConfig ? "Config" : "DB"}
</Badge>
);
},
},
{
header: "Actions",
id: "actions",
cell: ({ row }) => {
const tool = row.original;
const toolId = tool.search_tool_id;
const isFromConfig = tool.is_from_config ?? false;
return <span className="text-sm">{displayName}</span>;
},
return (
<div className="flex items-center gap-2">
<TableIconActionButton
variant="Edit"
tooltipText="Edit search tool"
disabled={isFromConfig}
disabledTooltipText="Config search tool cannot be edited on the dashboard. Please edit it from the config file."
onClick={() => {
if (toolId && !isFromConfig) {
onEdit(toolId);
}
}}
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete search tool"
disabled={isFromConfig}
disabledTooltipText="Config search tool cannot be deleted on the dashboard. Please delete it from the config file."
onClick={() => {
if (toolId && !isFromConfig) {
onDelete(toolId);
}
}}
/>
</div>
);
},
{
title: "Created At",
dataIndex: "created_at",
key: "created_at",
render: (_, tool) => {
return <span className="text-xs">{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}</span>;
},
},
{
title: "Updated At",
dataIndex: "updated_at",
key: "updated_at",
render: (_, tool) => {
return <span className="text-xs">{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}</span>;
},
},
{
title: "Source",
key: "source",
render: (_, tool) => {
const isFromConfig = tool.is_from_config ?? false;
return (
<Badge
className={cn(
"text-xs",
isFromConfig
? "bg-muted text-muted-foreground"
: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
)}
>
{isFromConfig ? "Config" : "DB"}
</Badge>
);
},
},
{
title: "Actions",
key: "actions",
render: (_, tool) => {
const toolId = tool.search_tool_id;
const isFromConfig = tool.is_from_config ?? false;
return (
<div className="flex items-center gap-2">
<TableIconActionButton
variant="Edit"
tooltipText="Edit search tool"
disabled={isFromConfig}
disabledTooltipText="Config search tool cannot be edited on the dashboard. Please edit it from the config file."
onClick={() => {
if (toolId && !isFromConfig) {
onEdit(toolId);
}
}}
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete search tool"
disabled={isFromConfig}
disabledTooltipText="Config search tool cannot be deleted on the dashboard. Please delete it from the config file."
onClick={() => {
if (toolId && !isFromConfig) {
onDelete(toolId);
}
}}
/>
</div>
);
},
},
];
},
];

View file

@ -1,13 +1,11 @@
import React, { useState } from "react";
import { Button, Input, Typography, Spin } from "antd";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import MessageManager from "@/components/molecules/message_manager";
import { Search as SearchOutlined, Loader2 as LoadingOutlined } from "lucide-react";
import { Loader2, Search as SearchOutlined } from "lucide-react";
import { searchToolQueryCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Card, Title as TremorTitle } from "@tremor/react";
const { Text } = Typography;
interface SearchResult {
title: string;
@ -61,7 +59,6 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
};
setSearchHistory((prev) => [historyEntry, ...prev]);
// Don't clear query after search so user can modify it
} catch (error) {
console.error("Error querying search tool:", error);
NotificationsManager.fromBackend("Failed to query search tool");
@ -88,64 +85,58 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
}));
};
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} className="animate-spin" />;
const latestResults = searchHistory.length > 0 ? searchHistory[0] : null;
return (
<Card className="mt-6">
<Card className={`mt-6 p-6 ${className}`}>
<div className="mb-6">
<TremorTitle>Test Search Tool</TremorTitle>
<h3 className="text-lg font-semibold">Test Search Tool</h3>
</div>
<div className="flex flex-col" style={{ minHeight: "600px" }}>
{/* Search Bar at Top */}
<div className="mb-6">
<div className="flex items-stretch gap-3">
<div
className="flex items-center flex-1 bg-white rounded-lg px-4 transition-all duration-200"
style={{
<div
className="flex items-center flex-1 bg-background rounded-lg px-4 transition-all duration-200"
style={{
border: isInputFocused ? "2px solid #3b82f6" : "2px solid #e5e7eb",
boxShadow: isInputFocused ? "0 0 0 3px rgba(59, 130, 246, 0.1)" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
height: "48px"
height: "48px",
}}
>
<SearchOutlined className="text-gray-400 mr-3" style={{ fontSize: "18px" }} />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSearch();
}
}}
placeholder="Enter your search query..."
disabled={isLoading}
bordered={false}
style={{ fontSize: "15px", padding: 0, height: "100%", boxShadow: "none" }}
/>
<SearchOutlined className="text-muted-foreground mr-3 h-[18px] w-[18px]" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSearch();
}
}}
placeholder="Enter your search query..."
disabled={isLoading}
className="border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-[15px] h-full p-0"
/>
</div>
<Button
type="primary"
onClick={handleSearch}
disabled={isLoading || !query.trim()}
icon={<SearchOutlined />}
loading={isLoading}
style={{
height: "48px",
paddingLeft: "24px",
paddingRight: "24px",
borderRadius: "8px",
fontWeight: 500,
fontSize: "15px",
backgroundColor: isLoading || !query.trim() ? undefined : "#1890ff",
borderColor: isLoading || !query.trim() ? undefined : "#1890ff",
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
}}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SearchOutlined className="h-4 w-4" />
)}
Search
</Button>
</div>
@ -155,42 +146,46 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<div className="flex-1">
{!latestResults && !isLoading ? (
<div className="h-full flex flex-col items-center justify-center p-8">
<div className="flex items-center justify-center w-24 h-24 rounded-full bg-gray-100 mb-6">
<SearchOutlined style={{ fontSize: "48px", color: "#9ca3af" }} />
<div className="flex items-center justify-center w-24 h-24 rounded-full bg-muted mb-6">
<SearchOutlined className="h-12 w-12 text-muted-foreground" />
</div>
<Text className="text-lg text-gray-600 font-medium">Test your search tool</Text>
<Text className="text-sm text-gray-500 mt-2">Enter a query above to see search results</Text>
<p className="text-lg text-foreground font-medium">Test your search tool</p>
<p className="text-sm text-muted-foreground mt-2">Enter a query above to see search results</p>
</div>
) : (
<div>
{isLoading && (
<div className="flex flex-col justify-center items-center py-16">
<Spin indicator={antIcon} />
<Text className="mt-4 text-gray-600 font-medium">Searching...</Text>
<Loader2 className="h-6 w-6 animate-spin" />
<p className="mt-4 text-muted-foreground font-medium">Searching...</p>
</div>
)}
{latestResults && !isLoading && (
<>
{/* Query Info Bar */}
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg" style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}>
<div
className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-950/30 dark:border-blue-900"
style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<Text className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Search Query</Text>
<div className="text-base font-semibold text-gray-900 mt-1.5">{latestResults.query}</div>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Search Query
</p>
<div className="text-base font-semibold text-foreground mt-1.5">{latestResults.query}</div>
</div>
<div className="text-right ml-4">
<Text className="text-xs text-gray-500">{formatTimestamp(latestResults.timestamp)}</Text>
<p className="text-xs text-muted-foreground">{formatTimestamp(latestResults.timestamp)}</p>
<div className="flex items-center gap-3 mt-1">
<div className="text-sm font-semibold text-blue-600">
{latestResults.response?.results?.length || 0} {latestResults.response?.results?.length === 1 ? 'result' : 'results'}
{latestResults.response?.results?.length || 0}{" "}
{latestResults.response?.results?.length === 1 ? "result" : "results"}
</div>
{latestResults.latency !== undefined && (
<>
<span className="text-gray-400">•</span>
<div className="text-sm font-semibold text-green-600">
{latestResults.latency}ms
</div>
<span className="text-muted-foreground">•</span>
<div className="text-sm font-semibold text-green-600">{latestResults.latency}ms</div>
</>
)}
</div>
@ -205,17 +200,16 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
const isResultExpanded = expandedResults[`0-${resultIndex}`] || false;
return (
<div
key={resultIndex}
className="bg-white border border-gray-200 rounded-lg overflow-hidden transition-all duration-200"
<div
key={resultIndex}
className="bg-background border border-border rounded-lg overflow-hidden transition-all duration-200"
style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}
onMouseEnter={(e) => {
e.currentTarget.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
e.currentTarget.style.borderColor = "#e0e7ff";
e.currentTarget.style.boxShadow =
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)";
e.currentTarget.style.borderColor = "#e5e7eb";
}}
>
<div className="p-5">
@ -225,47 +219,45 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-lg font-semibold text-blue-600 hover:text-blue-700 flex-1 leading-snug"
style={{ textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}
onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}
className="text-lg font-semibold text-blue-600 hover:text-blue-700 hover:underline flex-1 leading-snug"
>
{result.title}
</a>
<Button
type="text"
size="small"
className="flex-shrink-0"
icon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
}
variant="ghost"
size="sm"
className="flex-shrink-0 text-muted-foreground"
onClick={() => window.open(result.url, "_blank")}
style={{ color: "#6b7280" }}
/>
aria-label="Open in new tab"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</Button>
</div>
{/* URL */}
<div className="text-sm text-green-700 mb-3 truncate font-medium">{result.url}</div>
{/* Snippet Preview */}
<div className="text-sm text-gray-700 leading-relaxed">
{isResultExpanded ? result.snippet : `${result.snippet.substring(0, 200)}${result.snippet.length > 200 ? '...' : ''}`}
<div className="text-sm text-foreground leading-relaxed">
{isResultExpanded
? result.snippet
: `${result.snippet.substring(0, 200)}${result.snippet.length > 200 ? "..." : ""}`}
</div>
{/* Expand/Collapse */}
{result.snippet.length > 200 && (
<Button
type="link"
size="small"
className="mt-3 p-0 h-auto"
variant="link"
size="sm"
className="mt-3 p-0 h-auto text-blue-600"
onClick={() => toggleResultExpansion(0, resultIndex)}
style={{
fontSize: "13px",
fontWeight: 500,
color: "#3b82f6"
}}
>
{isResultExpanded ? "Show less" : "Show more"}
</Button>
@ -276,31 +268,23 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
})}
</div>
) : (
<div className="text-center py-12 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 mx-auto mb-4">
<SearchOutlined style={{ fontSize: "24px", color: "#9ca3af" }} />
<div className="text-center py-12 bg-muted border border-border rounded-lg">
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted mx-auto mb-4">
<SearchOutlined className="h-6 w-6 text-muted-foreground" />
</div>
<Text className="text-gray-600 font-medium">No results found</Text>
<Text className="text-sm text-gray-500 mt-1">Try a different search query</Text>
<p className="text-muted-foreground font-medium">No results found</p>
<p className="text-sm text-muted-foreground mt-1">Try a different search query</p>
</div>
)}
</>
)}
{/* Search History Sidebar */}
{/* Search History */}
{searchHistory.length > 1 && (
<div className="mt-8 pt-6 border-t border-gray-200">
<div className="mt-8 pt-6 border-t border-border">
<div className="flex items-center justify-between mb-4">
<Text className="text-sm font-semibold text-gray-700">Previous Searches</Text>
<Button
onClick={clearHistory}
size="small"
type="link"
style={{
fontSize: "13px",
fontWeight: 500,
}}
>
<p className="text-sm font-semibold text-foreground">Previous Searches</p>
<Button onClick={clearHistory} variant="link" size="sm" className="text-sm">
Clear All
</Button>
</div>
@ -308,22 +292,21 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
{searchHistory.slice(1, 6).map((entry, index) => (
<div
key={index + 1}
className="p-3 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer transition-all duration-200 hover:bg-gray-100 hover:border-gray-300"
className="p-3 bg-muted border border-border rounded-lg cursor-pointer transition-all duration-200 hover:bg-muted/80"
onClick={() => {
setQuery(entry.query);
}}
>
<div className="text-sm font-medium text-gray-800 truncate">{entry.query}</div>
<div className="text-xs text-gray-500 mt-1.5 flex items-center gap-2">
<div className="text-sm font-medium text-foreground truncate">{entry.query}</div>
<div className="text-xs text-muted-foreground mt-1.5 flex items-center gap-2">
<span className="font-medium text-blue-600">
{entry.response?.results?.length || 0} {entry.response?.results?.length === 1 ? 'result' : 'results'}
{entry.response?.results?.length || 0}{" "}
{entry.response?.results?.length === 1 ? "result" : "results"}
</span>
{entry.latency !== undefined && (
<>
<span>•</span>
<span className="font-medium text-green-600">
{entry.latency}ms
</span>
<span className="font-medium text-green-600">{entry.latency}ms</span>
</>
)}
<span>•</span>
@ -343,4 +326,3 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
};
export default SearchToolTester;

View file

@ -8,14 +8,19 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Form,
Input as AntInput,
Select,
Table,
} from "antd";
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Loader2 } from "lucide-react";
import React, { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import NotificationsManager from "../molecules/notifications_manager";
import {
@ -24,6 +29,7 @@ import {
fetchSearchTools,
updateSearchTool,
} from "../networking";
import { DataTable } from "../view_logs/table";
import CreateSearchTool from "./CreateSearchTools";
import { searchToolColumns } from "./SearchToolColumn";
import { SearchToolView } from "./SearchToolView";
@ -35,6 +41,15 @@ interface SearchToolsProps {
userID: string | null;
}
interface EditFormValues {
search_tool_name: string;
search_provider: string;
api_key?: string;
api_base?: string;
timeout?: string;
max_retries?: string;
description?: string;
}
const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID }) => {
const {
@ -50,10 +65,7 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
enabled: !!accessToken,
}) as { data: SearchTool[]; isLoading: boolean; refetch: () => void };
const {
data: providersResponse,
isLoading: isLoadingProviders,
} = useQuery({
const { data: providersResponse, isLoading: isLoadingProviders } = useQuery({
queryKey: ["searchProviders"],
queryFn: () => {
if (!accessToken) throw new Error("Access Token required");
@ -72,7 +84,23 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
const [editTool, setEditTool] = useState(false);
const [isCreateModalVisible, setCreateModalVisible] = useState(false);
const [isEditModalVisible, setEditModalVisible] = useState(false);
const [form] = Form.useForm();
const editForm = useForm<EditFormValues>({
defaultValues: {
search_tool_name: "",
search_provider: "",
api_key: "",
api_base: "",
timeout: "",
max_retries: "",
description: "",
},
});
function handleDelete(toolId: string) {
setToolToDelete(toolId);
setIsDeleteModalOpen(true);
}
const columns = React.useMemo(
() =>
@ -84,14 +112,15 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
(toolId: string) => {
const tool = searchTools?.find((t) => t.search_tool_id === toolId);
if (tool) {
form.setFieldsValue({
editForm.reset({
search_tool_name: tool.search_tool_name,
search_provider: tool.litellm_params.search_provider,
api_key: tool.litellm_params.api_key,
api_base: tool.litellm_params.api_base,
timeout: tool.litellm_params.timeout,
max_retries: tool.litellm_params.max_retries,
description: tool.search_tool_info?.description,
api_key: tool.litellm_params.api_key ?? "",
api_base: tool.litellm_params.api_base ?? "",
timeout: tool.litellm_params.timeout !== undefined ? String(tool.litellm_params.timeout) : "",
max_retries:
tool.litellm_params.max_retries !== undefined ? String(tool.litellm_params.max_retries) : "",
description: tool.search_tool_info?.description ?? "",
});
setSelectedToolId(toolId);
setEditModalVisible(true);
@ -100,14 +129,9 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
handleDelete,
availableProviders,
),
[availableProviders, searchTools, form],
[availableProviders, searchTools, editForm],
);
function handleDelete(toolId: string) {
setToolToDelete(toolId);
setIsDeleteModalOpen(true);
}
const confirmDelete = async () => {
if (toolIdToDelete == null || accessToken == null) {
return;
@ -137,16 +161,21 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
? availableProviders.find((p) => p.provider_name === toolToDelete.litellm_params.search_provider)
: null;
const handleCreateSuccess = (newSearchTool: SearchTool) => {
const handleCreateSuccess = (_newSearchTool: SearchTool) => {
setCreateModalVisible(false);
refetch();
};
const handleEditSubmit = async () => {
const handleEditCancel = () => {
setEditModalVisible(false);
editForm.reset();
setSelectedToolId(null);
};
const handleEditSubmit = editForm.handleSubmit(async (values) => {
if (!accessToken || !selectedToolId) return;
try {
const values = await form.validateFields();
const searchToolData = {
search_tool_name: values.search_tool_name,
litellm_params: {
@ -156,69 +185,26 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
timeout: values.timeout ? parseFloat(values.timeout) : undefined,
max_retries: values.max_retries ? parseInt(values.max_retries) : undefined,
},
search_tool_info: values.description ? {
description: values.description,
} : undefined,
search_tool_info: values.description
? {
description: values.description,
}
: undefined,
};
await updateSearchTool(accessToken, selectedToolId, searchToolData);
NotificationsManager.success("Search tool updated successfully");
setEditModalVisible(false);
form.resetFields();
setSelectedToolId(null);
handleEditCancel();
refetch();
} catch (error) {
console.error("Failed to update search tool:", error);
NotificationsManager.error("Failed to update search tool");
}
};
const renderEditForm = () => (
<Form form={form} layout="vertical">
<Form.Item
name="search_tool_name"
label="Search Tool Name"
rules={[{ required: true, message: "Please enter a search tool name" }]}
>
<AntInput placeholder="e.g., my-perplexity-search" />
</Form.Item>
<Form.Item
name="search_provider"
label="Search Provider"
rules={[{ required: true, message: "Please select a search provider" }]}
>
<Select placeholder="Select a search provider" loading={isLoadingProviders}>
{availableProviders.map((provider) => (
<Select.Option key={provider.provider_name} value={provider.provider_name}>
{provider.ui_friendly_name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="api_key"
label="API Key"
extra="API key for the search provider"
>
<AntInput.Password placeholder="Enter API key" />
</Form.Item>
<Form.Item name="description" label="Description">
<AntInput.TextArea
rows={3}
placeholder="Description of this search tool"
/>
</Form.Item>
</Form>
);
});
if (!accessToken || !userRole || !userID) {
return (
<div className="p-6 text-center text-muted-foreground">
Missing required authentication parameters.
</div>
<div className="p-6 text-center text-muted-foreground">Missing required authentication parameters.</div>
);
}
@ -250,18 +236,13 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)}
<Table
bordered
dataSource={searchTools || []}
<DataTable
columns={columns}
rowKey={(record) =>
record.search_tool_id || record.search_tool_name
}
pagination={false}
locale={{
emptyText: "No search tools configured",
}}
size="small"
data={searchTools || []}
renderSubComponent={() => <></>}
getRowCanExpand={() => false}
isLoading={false}
noDataMessage="No search tools configured"
/>
</div>
);
@ -276,14 +257,14 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
resourceInformation={
toolToDelete
? [
{ label: "Name", value: toolToDelete.search_tool_name },
{ label: "ID", value: toolToDelete.search_tool_id, code: true },
{
label: "Provider",
value: providerInfo?.ui_friendly_name || toolToDelete.litellm_params.search_provider,
},
{ label: "Description", value: toolToDelete.search_tool_info?.description || "-" },
]
{ label: "Name", value: toolToDelete.search_tool_name },
{ label: "ID", value: toolToDelete.search_tool_id, code: true },
{
label: "Provider",
value: providerInfo?.ui_friendly_name || toolToDelete.litellm_params.search_provider,
},
{ label: "Description", value: toolToDelete.search_tool_info?.description || "-" },
]
: []
}
onCancel={cancelDelete}
@ -300,46 +281,97 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
/>
{/* Edit Modal */}
<Dialog
open={isEditModalVisible}
onOpenChange={(o) => {
if (!o) {
setEditModalVisible(false);
form.resetFields();
setSelectedToolId(null);
}
}}
>
<Dialog open={isEditModalVisible} onOpenChange={(o) => (!o ? handleEditCancel() : undefined)}>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle>Edit Search Tool</DialogTitle>
</DialogHeader>
{renderEditForm()}
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setEditModalVisible(false);
form.resetFields();
setSelectedToolId(null);
}}
>
Cancel
</Button>
<Button onClick={handleEditSubmit}>OK</Button>
</DialogFooter>
<FormProvider {...editForm}>
<form onSubmit={handleEditSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-search-tool-name">
Search Tool Name <span className="text-destructive">*</span>
</Label>
<Input
id="edit-search-tool-name"
placeholder="e.g., my-perplexity-search"
{...editForm.register("search_tool_name", {
required: "Please enter a search tool name",
})}
/>
{editForm.formState.errors.search_tool_name && (
<p className="text-sm text-destructive">
{editForm.formState.errors.search_tool_name.message as string}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="edit-search-provider">
Search Provider <span className="text-destructive">*</span>
</Label>
<Controller
control={editForm.control}
name="search_provider"
rules={{ required: "Please select a search provider" }}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange} disabled={isLoadingProviders}>
<SelectTrigger id="edit-search-provider">
<SelectValue placeholder="Select a search provider" />
</SelectTrigger>
<SelectContent>
{availableProviders.map((provider) => (
<SelectItem key={provider.provider_name} value={provider.provider_name}>
{provider.ui_friendly_name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{editForm.formState.errors.search_provider && (
<p className="text-sm text-destructive">
{editForm.formState.errors.search_provider.message as string}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="edit-api-key">API Key</Label>
<Input
id="edit-api-key"
type="password"
placeholder="Enter API key"
{...editForm.register("api_key")}
/>
<p className="text-xs text-muted-foreground">API key for the search provider</p>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description">Description</Label>
<Textarea
id="edit-description"
rows={3}
placeholder="Description of this search tool"
{...editForm.register("description")}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={handleEditCancel}>
Cancel
</Button>
<Button type="submit">OK</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
<h1 className="text-2xl font-semibold">Search Tools</h1>
<p className="text-muted-foreground mt-2">
Configure and manage your search providers
</p>
<p className="text-muted-foreground mt-2">Configure and manage your search providers</p>
{isAdminRole(userRole) && (
<Button
className="mt-4 mb-4"
onClick={() => setCreateModalVisible(true)}
>
<Button className="mt-4 mb-4" onClick={() => setCreateModalVisible(true)}>
+ Add New Search Tool
</Button>
)}

File diff suppressed because one or more lines are too long