mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
refactor(mcp): clear server list lint warnings
This commit is contained in:
parent
68074da1d1
commit
64078689d3
2 changed files with 232 additions and 91 deletions
|
|
@ -167,6 +167,130 @@ describe("MCPServers", () => {
|
|||
const myConnections = await screen.findByRole("link", { name: "My Connections" });
|
||||
expect(myConnections).toBeVisible();
|
||||
expect(myConnections).toHaveAttribute("href", "/ui/connect");
|
||||
for (const name of ["Semantic Filter", "Tool Search", "Network Settings", "Submitted MCPs"]) {
|
||||
const tab = screen.queryByRole("tab", { name });
|
||||
if (userRole === "Admin") {
|
||||
expect(tab).toBeVisible();
|
||||
} else {
|
||||
expect(tab).not.toBeInTheDocument();
|
||||
}
|
||||
}
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: userRole === "Admin" ? "+ Add New MCP Server" : "+ Submit MCP Server",
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it.each(["cancel", "success", "failure", "unnamed"])("preserves delete confirmation on %s", async (outcome) => {
|
||||
const server: MCPServer = {
|
||||
server_id: "delete-server",
|
||||
server_name: outcome === "unnamed" ? null : "Delete fixture",
|
||||
alias: "delete-alias",
|
||||
url: outcome === "unnamed" ? null : "https://example.com/mcp",
|
||||
created_by: "user",
|
||||
updated_by: "user",
|
||||
};
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([server]);
|
||||
vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
|
||||
let finishDelete: () => void = () => {};
|
||||
vi.mocked(networking.deleteMCPServer).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
finishDelete = () => (outcome === "failure" ? reject(new Error("Delete failed")) : resolve(undefined));
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<MCPServers {...defaultProps} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Server actions" }));
|
||||
await userEvent.click(await screen.findByRole("menuitem", { name: "Delete" }));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Delete MCP Server?" });
|
||||
expect(within(dialog).getByText("delete-server")).toBeVisible();
|
||||
if (outcome === "unnamed") {
|
||||
expect(within(dialog).queryByText("Name")).not.toBeInTheDocument();
|
||||
expect(within(dialog).queryByText("URL")).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(within(dialog).getByText("Delete fixture")).toBeVisible();
|
||||
expect(within(dialog).getByText("https://example.com/mcp")).toBeVisible();
|
||||
}
|
||||
if (outcome === "cancel") {
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
expect(networking.deleteMCPServer).not.toHaveBeenCalled();
|
||||
} else {
|
||||
await userEvent.click(within(dialog).getByRole("button", { name: "Delete", exact: true }));
|
||||
expect(within(dialog).getByRole("button", { name: "Deleting..." })).toBeDisabled();
|
||||
expect(within(dialog).getByRole("button", { name: "Cancel" })).toBeDisabled();
|
||||
expect(networking.deleteMCPServer).toHaveBeenCalledWith("123", "delete-server");
|
||||
await act(async () => finishDelete());
|
||||
}
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("filters servers by access group", async () => {
|
||||
const server = { created_by: "user", updated_by: "user" };
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([
|
||||
{
|
||||
...server,
|
||||
server_id: "string-group",
|
||||
server_name: "String group",
|
||||
alias: "string-alias",
|
||||
mcp_access_groups: ["shared"],
|
||||
},
|
||||
{
|
||||
...server,
|
||||
server_id: "legacy-group",
|
||||
server_name: "Legacy group",
|
||||
alias: "legacy-alias",
|
||||
mcp_access_groups: ["shared"],
|
||||
},
|
||||
{
|
||||
...server,
|
||||
server_id: "other-group",
|
||||
server_name: "Other group",
|
||||
alias: "other-alias",
|
||||
mcp_access_groups: ["different"],
|
||||
},
|
||||
]);
|
||||
vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
|
||||
render(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<MCPServers {...defaultProps} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await screen.findByText("String group");
|
||||
await userEvent.click(screen.getByRole("combobox", { name: "Access Group" }));
|
||||
await userEvent.click(await screen.findByRole("option", { name: "shared", exact: true }));
|
||||
expect(screen.getByText("String group")).toBeVisible();
|
||||
expect(screen.getByText("Legacy group")).toBeVisible();
|
||||
expect(screen.queryByText("Other group")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(["server_name", "alias", "url", "server_id"] as const)("searches by %s case-insensitively", async (field) => {
|
||||
const server: MCPServer = {
|
||||
server_id: "search-server",
|
||||
server_name: "Search fixture",
|
||||
created_by: "user",
|
||||
updated_by: "user",
|
||||
[field]: "Needle",
|
||||
};
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([server]);
|
||||
vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);
|
||||
render(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<MCPServers {...defaultProps} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await screen.findByTestId("mcp-servers-grid");
|
||||
const search = screen.getByPlaceholderText("Search by name, alias, URL, or ID");
|
||||
await userEvent.type(search, " NEEDLE ");
|
||||
expect(screen.getByTestId("mcp-servers-grid")).toBeVisible();
|
||||
await userEvent.clear(search);
|
||||
await userEvent.type(search, "no-match");
|
||||
expect(screen.queryByTestId("mcp-servers-grid")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("No servers match the current filters or search.")).toBeVisible();
|
||||
});
|
||||
|
||||
it("should render mocked MCP servers data in the table", async () => {
|
||||
|
|
@ -409,9 +533,7 @@ describe("MCPServers", () => {
|
|||
expect(screen.getByText("Team B Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team A Server 2")).toBeInTheDocument();
|
||||
|
||||
// Find the team select by its "Team" label, then the combobox it labels
|
||||
const teamLabel = screen.getByText("Team");
|
||||
const teamSelect = within(teamLabel.parentElement!).getByRole("combobox");
|
||||
const teamSelect = screen.getByRole("combobox", { name: "Team" });
|
||||
|
||||
await userEvent.click(teamSelect);
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,62 @@ const readToolsOAuthServerId = (): string | null => {
|
|||
}
|
||||
};
|
||||
|
||||
function DeleteServerDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
server,
|
||||
isDeleting,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
server: MCPServer | undefined;
|
||||
isDeleting: boolean;
|
||||
onConfirm: () => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete MCP Server?</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This action is permanent and cannot be undone. All associated configurations will be removed.
|
||||
</p>
|
||||
|
||||
{server && (
|
||||
<dl className="mt-3 space-y-1 rounded-lg border border-border bg-muted p-4">
|
||||
{server.server_name && (
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">Name</dt>
|
||||
<dd className="text-sm font-semibold">{server.server_name}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">ID</dt>
|
||||
<dd className="font-mono text-xs">{server.server_id}</dd>
|
||||
</div>
|
||||
{server.url && (
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">URL</dt>
|
||||
<dd className="font-mono text-xs break-all">{server.url}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<Button variant="destructive" disabled={isDeleting} onClick={onConfirm}>
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, isViewOnly = false }) => {
|
||||
const { data: mcpServers, isLoading: isLoadingServers, refetch } = useMCPServers();
|
||||
|
||||
|
|
@ -299,7 +355,9 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
}
|
||||
if (group !== "all") {
|
||||
filtered = filtered.filter((server) =>
|
||||
server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)),
|
||||
server.mcp_access_groups?.some((g: string | { name?: string } | null) =>
|
||||
typeof g === "string" ? g === group : g?.name === group,
|
||||
),
|
||||
);
|
||||
}
|
||||
setFilteredServers(filtered);
|
||||
|
|
@ -333,7 +391,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
const alias = (s.alias || "").toLowerCase();
|
||||
const url = (s.url || "").toLowerCase();
|
||||
const id = s.server_id.toLowerCase();
|
||||
return name.includes(q) || alias.includes(q) || url.includes(q) || id.includes(q);
|
||||
return [name, alias, url, id].some((value) => value.includes(q));
|
||||
})
|
||||
: filteredServers;
|
||||
return [...matches].sort((a, b) => compareServers(a, b, sortKey));
|
||||
|
|
@ -376,9 +434,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
};
|
||||
|
||||
// Find the server to delete from the servers list
|
||||
const serverToDelete = serverIdToDelete
|
||||
? (mcpServers || []).find((server) => server.server_id === serverIdToDelete)
|
||||
: null;
|
||||
const serverToDelete = mcpServers?.find((server) => server.server_id === serverIdToDelete);
|
||||
|
||||
const handleCreateSuccess = (newMcpServer: MCPServer) => {
|
||||
setFilteredServers((prev) => [...prev, newMcpServer]);
|
||||
|
|
@ -420,45 +476,13 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
return (
|
||||
<TooltipProvider>
|
||||
<div className="h-full w-full p-6">
|
||||
<AlertDialog open={isDeleteModalOpen} onOpenChange={(open) => !open && cancelDelete()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete MCP Server?</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This action is permanent and cannot be undone. All associated configurations will be removed.
|
||||
</p>
|
||||
|
||||
{serverToDelete && (
|
||||
<dl className="mt-3 space-y-1 rounded-lg border border-border bg-muted p-4">
|
||||
{serverToDelete.server_name && (
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">Name</dt>
|
||||
<dd className="text-sm font-semibold">{serverToDelete.server_name}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">ID</dt>
|
||||
<dd className="font-mono text-xs">{serverToDelete.server_id}</dd>
|
||||
</div>
|
||||
{serverToDelete.url && (
|
||||
<div className="flex gap-2">
|
||||
<dt className="text-sm text-muted-foreground">URL</dt>
|
||||
<dd className="font-mono text-xs break-all">{serverToDelete.url}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeletingServer}>Cancel</AlertDialogCancel>
|
||||
<Button variant="destructive" disabled={isDeletingServer} onClick={confirmDelete}>
|
||||
{isDeletingServer ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteServerDialog
|
||||
open={isDeleteModalOpen}
|
||||
onOpenChange={(open) => !open && cancelDelete()}
|
||||
server={serverToDelete}
|
||||
isDeleting={isDeletingServer}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
<CreateMCPServer
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
|
|
@ -487,7 +511,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
<Plug />
|
||||
My Connections
|
||||
</Link>
|
||||
{isAdminRole(userRole) && (
|
||||
{isAdminRole(userRole) ? (
|
||||
<>
|
||||
<Button className="shrink-0" variant="secondary" onClick={() => setImportVisible(true)}>
|
||||
Import from JSON
|
||||
|
|
@ -496,8 +520,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
+ Add New MCP Server
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isAdminRole(userRole) && (
|
||||
) : (
|
||||
<Button
|
||||
className="shrink-0"
|
||||
onClick={() => {
|
||||
|
|
@ -544,24 +567,23 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
Connect
|
||||
</TabsTrigger>
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsTrigger value="semantic-filter" className="flex-none rounded-none px-4 py-2">
|
||||
Semantic Filter
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsTrigger value="tool-search" className="flex-none rounded-none px-4 py-2">
|
||||
Tool Search
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsTrigger value="network-settings" className="flex-none rounded-none px-4 py-2">
|
||||
Network Settings
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsTrigger value="submitted" className="flex-none rounded-none px-4 py-2">
|
||||
Submitted MCPs
|
||||
</TabsTrigger>
|
||||
<>
|
||||
<TabsTrigger value="semantic-filter" className="flex-none rounded-none px-4 py-2">
|
||||
Semantic Filter
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="tool-search" className="flex-none rounded-none px-4 py-2">
|
||||
Tool Search
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="network-settings" className="flex-none rounded-none px-4 py-2">
|
||||
Network Settings
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="submitted" className="flex-none rounded-none px-4 py-2">
|
||||
Submitted MCPs
|
||||
</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
{isProxyAdminTierRole(userRole) && (
|
||||
<TabsTrigger value="connections" className="flex-none rounded-none px-4 py-2">
|
||||
|
|
@ -596,13 +618,11 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
value={selectedTeam}
|
||||
onValueChange={(v: string | null) => handleTeamChange(v ?? "all")}
|
||||
>
|
||||
<SelectTrigger className="w-55">
|
||||
<SelectTrigger className="w-55" aria-label="Team">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{isInternalUser ? "All Available Servers" : "All Servers"}
|
||||
</SelectItem>
|
||||
<SelectItem value="all">{teamSelectItems.all}</SelectItem>
|
||||
<SelectItem value="personal">Personal</SelectItem>
|
||||
{uniqueTeams.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
|
|
@ -636,7 +656,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
value={selectedMcpAccessGroup}
|
||||
onValueChange={(v: string | null) => handleMcpAccessGroupChange(v ?? "all")}
|
||||
>
|
||||
<SelectTrigger className="w-55">
|
||||
<SelectTrigger className="w-55" aria-label="Access Group">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -737,24 +757,23 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID, i
|
|||
<MCPConnect />
|
||||
</TabsContent>
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsContent value="semantic-filter" keepMounted>
|
||||
<MCPSemanticFilterSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsContent value="tool-search" keepMounted>
|
||||
<MCPToolSearchSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsContent value="network-settings" keepMounted>
|
||||
<MCPNetworkSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabsContent value="submitted" keepMounted>
|
||||
<MCPSubmissionsTab accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
<>
|
||||
<TabsContent value="semantic-filter" keepMounted>
|
||||
<MCPSemanticFilterSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tool-search" keepMounted>
|
||||
<MCPToolSearchSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network-settings" keepMounted>
|
||||
<MCPNetworkSettings accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="submitted" keepMounted>
|
||||
<MCPSubmissionsTab accessToken={accessToken} />
|
||||
</TabsContent>
|
||||
</>
|
||||
)}
|
||||
{isProxyAdminTierRole(userRole) && (
|
||||
<TabsContent value="connections">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue