mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Add search functionality to MCP Servers page
- Added search input field that filters servers by name, alias, URL, server ID, and transport - Search bar placed prominently in the filter section alongside Team and Access Group filters - Updated filtering logic to support text search in addition to existing filters - Added comprehensive unit test for search functionality - Fixed bug in create_mcp_server.tsx where setTools was called instead of clearTools The search feature allows users to quickly find MCP servers by typing in any relevant text, filtering the displayed servers in real-time. Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
parent
cf331c4fc5
commit
66d77cfa22
4 changed files with 148 additions and 25 deletions
15
ui/litellm-dashboard/package-lock.json
generated
15
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -13279,21 +13279,6 @@
|
|||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
|
||||
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setCostConfig({});
|
||||
setTools([]);
|
||||
clearTools();
|
||||
setAllowedTools([]);
|
||||
setAliasManuallyEdited(false);
|
||||
setModalVisible(false);
|
||||
|
|
|
|||
|
|
@ -348,4 +348,110 @@ describe("MCPServers", () => {
|
|||
// Team B server should not be visible
|
||||
expect(screen.queryByText("Team B Server")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter servers by search term", async () => {
|
||||
const mockServers = [
|
||||
{
|
||||
server_id: "server-1",
|
||||
server_name: "GitHub Server",
|
||||
alias: "github",
|
||||
url: "https://api.github.com/mcp",
|
||||
transport: "http",
|
||||
auth_type: "oauth2",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
teams: [],
|
||||
mcp_access_groups: [],
|
||||
},
|
||||
{
|
||||
server_id: "server-2",
|
||||
server_name: "Notion Server",
|
||||
alias: "notion",
|
||||
url: "https://api.notion.com/mcp",
|
||||
transport: "sse",
|
||||
auth_type: "api_key",
|
||||
created_at: "2024-01-02T00:00:00Z",
|
||||
created_by: "user-2",
|
||||
updated_at: "2024-01-02T00:00:00Z",
|
||||
updated_by: "user-2",
|
||||
teams: [],
|
||||
mcp_access_groups: [],
|
||||
},
|
||||
{
|
||||
server_id: "server-3",
|
||||
server_name: "Slack Server",
|
||||
alias: "slack",
|
||||
url: "https://slack.com/api/mcp",
|
||||
transport: "http",
|
||||
auth_type: "oauth2",
|
||||
created_at: "2024-01-03T00:00:00Z",
|
||||
created_by: "user-3",
|
||||
updated_at: "2024-01-03T00:00:00Z",
|
||||
updated_by: "user-3",
|
||||
teams: [],
|
||||
mcp_access_groups: [],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers);
|
||||
vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
|
||||
|
||||
const queryClient = createQueryClient();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MCPServers {...defaultProps} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("MCP Servers")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GitHub Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("GitHub Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Notion Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Slack Server")).toBeInTheDocument();
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search servers by name, alias, URL...");
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(searchInput, { target: { value: "github" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GitHub Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("GitHub Server")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Notion Server")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Slack Server")).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(searchInput, { target: { value: "notion.com" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Notion Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText("GitHub Server")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Notion Server")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Slack Server")).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(searchInput, { target: { value: "" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GitHub Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Notion Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Slack Server")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { isAdminRole } from "@/utils/roles";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { QuestionCircleOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { Button, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
|
||||
import NewBadge from "../common_components/NewBadge";
|
||||
import { Descriptions, Modal, Select, Tooltip, Typography } from "antd";
|
||||
import { Descriptions, Input, Modal, Select, Tooltip, Typography } from "antd";
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers";
|
||||
import { useMCPServerHealth } from "../../app/(dashboard)/hooks/mcpServers/useMCPServerHealth";
|
||||
|
|
@ -57,6 +57,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
const [editServer, setEditServer] = useState(false);
|
||||
const [selectedTeam, setSelectedTeam] = useState<string>("all");
|
||||
const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState<string>("all");
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [filteredServers, setFilteredServers] = useState<MCPServer[]>([]);
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
const [isDiscoveryVisible, setDiscoveryVisible] = useState(false);
|
||||
|
|
@ -113,8 +114,8 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
);
|
||||
}, [serversWithHealth]);
|
||||
|
||||
// Filtering logic for both team and access group
|
||||
const filterServers = useCallback((teamId: string, group: string) => {
|
||||
// Filtering logic for team, access group, and search
|
||||
const filterServers = useCallback((teamId: string, group: string, search: string) => {
|
||||
if (!serversWithHealth) return setFilteredServers([]);
|
||||
let filtered = serversWithHealth;
|
||||
if (teamId === "personal") {
|
||||
|
|
@ -129,25 +130,44 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)),
|
||||
);
|
||||
}
|
||||
if (search.trim()) {
|
||||
const searchLower = search.toLowerCase();
|
||||
filtered = filtered.filter((server) => {
|
||||
return (
|
||||
(server.server_name && server.server_name.toLowerCase().includes(searchLower)) ||
|
||||
(server.alias && server.alias.toLowerCase().includes(searchLower)) ||
|
||||
(server.url && server.url.toLowerCase().includes(searchLower)) ||
|
||||
(server.server_id && server.server_id.toLowerCase().includes(searchLower)) ||
|
||||
(server.transport && server.transport.toLowerCase().includes(searchLower))
|
||||
);
|
||||
});
|
||||
}
|
||||
setFilteredServers(filtered);
|
||||
}, [serversWithHealth]);
|
||||
|
||||
// Handle team filter change
|
||||
const handleTeamChange = (teamId: string) => {
|
||||
setSelectedTeam(teamId);
|
||||
filterServers(teamId, selectedMcpAccessGroup);
|
||||
filterServers(teamId, selectedMcpAccessGroup, searchTerm);
|
||||
};
|
||||
|
||||
// Handle MCP access group filter change
|
||||
const handleMcpAccessGroupChange = (group: string) => {
|
||||
setSelectedMcpAccessGroup(group);
|
||||
filterServers(selectedTeam, group);
|
||||
filterServers(selectedTeam, group, searchTerm);
|
||||
};
|
||||
|
||||
// Handle search term change
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const term = e.target.value;
|
||||
setSearchTerm(term);
|
||||
filterServers(selectedTeam, selectedMcpAccessGroup, term);
|
||||
};
|
||||
|
||||
// Initial and effect-based filtering (trigger on query data updates and health data updates)
|
||||
useEffect(() => {
|
||||
filterServers(selectedTeam, selectedMcpAccessGroup);
|
||||
}, [serversWithHealth, selectedTeam, selectedMcpAccessGroup, filterServers]);
|
||||
filterServers(selectedTeam, selectedMcpAccessGroup, searchTerm);
|
||||
}, [serversWithHealth, selectedTeam, selectedMcpAccessGroup, searchTerm, filterServers]);
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
|
|
@ -364,7 +384,19 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
<div className="w-full h-full">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200">
|
||||
<div className="flex items-center gap-4 bg-white rounded-lg px-4 py-3 border border-gray-200">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Input
|
||||
placeholder="Search servers by name, alias, URL..."
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
allowClear
|
||||
className="rounded-lg"
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-6 w-px bg-gray-200"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text className="text-sm font-medium text-gray-600 whitespace-nowrap">Team</Text>
|
||||
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 220 }} size="middle">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue