From fa718ebd6829737ccbf4402636aa25ffb6ca267a Mon Sep 17 00:00:00 2001 From: Marek Dano Date: Thu, 15 Oct 2026 11:43:53 +0100 Subject: [PATCH] fix(gateways): hide source selector for single-source virtual servers The virtual server details panel showed the "All sources | " tabs even when every component came from one MCP server, where the selector offers nothing to choose between. Render it only when there is more than one source. The tab row no longer appears while sources are still loading, which avoids a flash of tabs on single-source servers. Adds Vitest coverage for the single-source case and a Playwright test for filtering components by source when there are several. Signed-off-by: Marek Dano --- e2e/virtual-servers.spec.ts | 39 +++++++++++++++++++ .../VirtualServerDetailsPanel.test.tsx | 30 ++++++++++++++ .../gateways/VirtualServerDetailsPanel.tsx | 6 +-- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/e2e/virtual-servers.spec.ts b/e2e/virtual-servers.spec.ts index 9022f902..b629c29b 100644 --- a/e2e/virtual-servers.spec.ts +++ b/e2e/virtual-servers.spec.ts @@ -1208,6 +1208,45 @@ test.describe("Virtual Servers page", () => { await expect(detailsPanel.getByText("github://repo/{owner}/{repo}")).toHaveCount(0); }); + test("filters components by source in the details panel when there are several", async ({ + page, + }) => { + await routeVirtualServerTryIt(page, [ + makeTryItTool(), + makeTryItTool({ + id: "tool-post", + name: "slack.post_message", + originalName: "post_message", + gatewayId: MOCK_MCP_SERVER_2.id, + }), + ]); + // Registered after the helper's route, so it takes precedence. + await page.route("**/v1/mcp-servers?*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ gateways: [MOCK_MCP_SERVER, MOCK_MCP_SERVER_2] }), + }); + }); + + await page.goto(APP.GATEWAYS); + await page.getByRole("button", { name: "Actions for testVS" }).click(); + await page.getByRole("menuitem", { name: "View details" }).click(); + + const panel = page.getByRole("region", { name: "testVS details" }); + await panel.getByRole("tab", { name: "Components" }).click(); + + await expect(panel.getByRole("tab", { name: "All sources" })).toBeVisible(); + await expect(panel.getByRole("tab", { name: "github-mcp" })).toBeVisible(); + await expect(panel.getByRole("tab", { name: "slack-mcp" })).toBeVisible(); + await expect(panel.getByText("search_issues")).toBeVisible(); + await expect(panel.getByText("post_message")).toBeVisible(); + + await panel.getByRole("tab", { name: "slack-mcp" }).click(); + await expect(panel.getByText("post_message")).toBeVisible(); + await expect(panel.getByText("search_issues")).toHaveCount(0); + }); + test("shows a tooltip with the full endpoint when it's truncated in the Try it tab", async ({ page, }) => { diff --git a/src/components/gateways/VirtualServerDetailsPanel.test.tsx b/src/components/gateways/VirtualServerDetailsPanel.test.tsx index 70e219c1..1f1a5f6f 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.test.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.test.tsx @@ -295,6 +295,36 @@ describe("VirtualServerDetailsPanel components list", () => { expect(screen.getByRole("tab", { name: "Gateway A" })).toHaveFocus(); }); + it("hides the source selector when the server has a single source", async () => { + const user = userEvent.setup(); + mswServer.use( + http.get("*/v1/virtual-servers/:id/tools", () => + HttpResponse.json({ + tools: [{ id: "t1", name: "Tool One", originalName: "tool_one", gateway_id: "gwA" }], + }), + ), + http.get("*/v1/mcp-servers", () => + HttpResponse.json({ gateways: [{ id: "gwA", name: "Gateway A" }] }), + ), + ); + + render( + , + ); + + await user.click(await screen.findByRole("tab", { name: "Components" })); + expect(await screen.findByText("tool_one")).toBeInTheDocument(); + + expect(screen.queryByRole("tab", { name: "All sources" })).not.toBeInTheDocument(); + expect(screen.queryByRole("tab", { name: "Gateway A" })).not.toBeInTheDocument(); + }); + it("moves the active tab with arrow keys", async () => { const user = userEvent.setup(); renderWithComponents(); diff --git a/src/components/gateways/VirtualServerDetailsPanel.tsx b/src/components/gateways/VirtualServerDetailsPanel.tsx index 2b02ff4d..4cf81bae 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.tsx @@ -399,9 +399,7 @@ export function VirtualServerDetailsPanel({ return `/v1/mcp-servers?${params}`; }, []); - const { data: sourcesData, isLoading: sourcesLoading } = useQuery< - MCPServersResponse | MCPServer[] - >(sourcesPath, { + const { data: sourcesData } = useQuery(sourcesPath, { enabled: fetchEnabled && sourceIds.length > 0, }); @@ -637,7 +635,7 @@ export function VirtualServerDetailsPanel({ /> ) : ( <> - {(sourcesLoading || sourceTabs.length > 0) && ( + {sourceTabs.length > 1 && (