Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
const [editing, setEditing] = useState(isEditing);
const [showFullUrl, setShowFullUrl] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const [selectedTabIndex, setSelectedTabIndex] = useState(0);

const handleSuccess = (updated: MCPServer) => {
setEditing(false);
onBack();
Expand Down Expand Up @@ -72,11 +74,10 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
size="small"
icon={copiedStates["mcp-server_name"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(mcpServer.server_name, "mcp-server_name")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["mcp-server_name"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-server_name"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
{mcpServer.alias && (
<>
Expand All @@ -87,11 +88,10 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
size="small"
icon={copiedStates["mcp-alias"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(mcpServer.alias, "mcp-alias")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["mcp-alias"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-alias"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</>
)}
Expand All @@ -103,18 +103,17 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
size="small"
icon={copiedStates["mcp-server-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(mcpServer.server_id, "mcp-server-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["mcp-server-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-server-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
</div>
</div>

{/* TODO: magic number for index */}
<TabGroup defaultIndex={editing ? 2 : 0}>
<TabGroup index={selectedTabIndex} onIndexChange={setSelectedTabIndex}>
<TabList className="mb-4">
{[
<Tab key="overview">Overview</Tab>,
Expand Down
120 changes: 118 additions & 2 deletions ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import { render, waitFor, screen, fireEvent, act } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import MCPServers from "./mcp_servers";
Expand Down Expand Up @@ -208,7 +208,7 @@ describe("MCPServers", () => {
vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers);
// Mock health check to never resolve (to test loading state)
vi.mocked(networking.fetchMCPServerHealth).mockImplementation(
() => new Promise(() => {}), // Never resolves
() => new Promise(() => { }), // Never resolves
);

const queryClient = createQueryClient();
Expand All @@ -228,4 +228,120 @@ describe("MCPServers", () => {
expect(networking.fetchMCPServerHealth).toHaveBeenCalled();
});
});

it("should filter servers by team when a team is selected", async () => {
// Mock MCP servers with different teams
const mockServers = [
{
server_id: "server-1",
server_name: "Team A Server",
alias: "team-a-server",
url: "https://example.com/mcp",
transport: "http",
auth_type: "none",
created_at: "2024-01-01T00:00:00Z",
created_by: "user-1",
updated_at: "2024-01-01T00:00:00Z",
updated_by: "user-1",
teams: [{ team_id: "team-a", team_alias: "Team A" }],
mcp_access_groups: [],
},
{
server_id: "server-2",
server_name: "Team B Server",
alias: "team-b-server",
url: "https://example2.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: [{ team_id: "team-b", team_alias: "Team B" }],
mcp_access_groups: [],
},
{
server_id: "server-3",
server_name: "Team A Server 2",
alias: "team-a-server-2",
url: "https://example3.com/mcp",
transport: "http",
auth_type: "none",
created_at: "2024-01-03T00:00:00Z",
created_by: "user-1",
updated_at: "2024-01-03T00:00:00Z",
updated_by: "user-1",
teams: [{ team_id: "team-a", team_alias: "Team A" }],
mcp_access_groups: [],
},
];

vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers);
vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]);

const queryClient = createQueryClient();
render(
<QueryClientProvider client={queryClient}>
<MCPServers {...defaultProps} />
</QueryClientProvider>,
);

// Wait for the component to load
await waitFor(() => {
expect(screen.getByText("MCP Servers")).toBeInTheDocument();
});

// Wait for servers to be rendered
await waitFor(() => {
expect(screen.getByText("Team A Server")).toBeInTheDocument();
});

// Verify all servers are initially displayed
expect(screen.getByText("Team A Server")).toBeInTheDocument();
expect(screen.getByText("Team B Server")).toBeInTheDocument();
expect(screen.getByText("Team A Server 2")).toBeInTheDocument();

// Find the team select dropdown by looking for the "Current Team:" label
const teamLabel = screen.getByText("Current Team:");
const teamSelectContainer = teamLabel.closest("div")?.querySelector(".ant-select");
expect(teamSelectContainer).toBeTruthy();

// Open the dropdown by clicking on the selector
const selectSelector = teamSelectContainer?.querySelector(".ant-select-selector");
expect(selectSelector).toBeTruthy();

act(() => {
fireEvent.mouseDown(selectSelector!);
});

// Wait for dropdown to open
await waitFor(
() => {
const dropdownOptions = document.querySelectorAll(".ant-select-item-option");
expect(dropdownOptions.length).toBeGreaterThan(0);
},
{ timeout: 5000 },
);

// Find and click on "Team A" option
const dropdownOptions = document.querySelectorAll(".ant-select-item-option");
const teamAOption = Array.from(dropdownOptions).find((option) =>
option.textContent?.includes("Team A"),
);
expect(teamAOption).toBeTruthy();

act(() => {
fireEvent.click(teamAOption!);
});

// Wait for filtering to complete
await waitFor(() => {
// Team A servers should still be visible
expect(screen.getByText("Team A Server")).toBeInTheDocument();
expect(screen.getByText("Team A Server 2")).toBeInTheDocument();
});

// Team B server should not be visible
expect(screen.queryByText("Team B Server")).not.toBeInTheDocument();
});
});
Loading
Loading