diff --git a/e2e/virtual-servers.spec.ts b/e2e/virtual-servers.spec.ts index b629c29b..2d65d95d 100644 --- a/e2e/virtual-servers.spec.ts +++ b/e2e/virtual-servers.spec.ts @@ -1236,13 +1236,18 @@ test.describe("Virtual Servers page", () => { 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.getByRole("heading", { name: "Sources" })).toBeVisible(); + await expect(panel.getByRole("button", { name: "All sources" })).toBeVisible(); + await expect(panel.getByRole("button", { name: "github-mcp" })).toBeVisible(); + await expect(panel.getByRole("button", { 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 panel.getByRole("button", { name: "slack-mcp" }).click(); + await expect(panel.getByRole("button", { name: "slack-mcp" })).toHaveAttribute( + "aria-pressed", + "true", + ); await expect(panel.getByText("post_message")).toBeVisible(); await expect(panel.getByText("search_issues")).toHaveCount(0); }); diff --git a/src/components/gateways/VirtualServerDetailsPanel.test.tsx b/src/components/gateways/VirtualServerDetailsPanel.test.tsx index 1f1a5f6f..fc700e10 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.test.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.test.tsx @@ -241,9 +241,9 @@ describe("VirtualServerDetailsPanel components list", () => { expect(searchBox).toHaveValue("Titled"); }); - it("renders source tabs and filters components by source", async () => { + it("renders source chips and filters components by source", async () => { const user = userEvent.setup(); - // Fetched components carry a gateway_id, which drives the source tabs. + // Fetched components carry a gateway_id, which drives the source chips. mswServer.use( http.get("*/v1/virtual-servers/:id/tools", () => HttpResponse.json({ @@ -275,24 +275,33 @@ describe("VirtualServerDetailsPanel components list", () => { await user.click(await screen.findByRole("tab", { name: "Components" })); - // Source tabs resolve from the gateways response. - expect(await screen.findByRole("tab", { name: "Gateway A" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Gateway B" })).toBeInTheDocument(); + // Source chips resolve from the gateways response. + expect(await screen.findByRole("button", { name: "Gateway A" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Sources" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Gateway B" })).toBeInTheDocument(); expect(screen.getByText("tool_one")).toBeInTheDocument(); expect(screen.getByText("tool_two")).toBeInTheDocument(); + // All sources is pressed until another chip is chosen. + expect(screen.getByRole("button", { name: "All sources" })).toHaveAttribute( + "aria-pressed", + "true", + ); + // Selecting a source filters the component list to that gateway. - await user.click(screen.getByRole("tab", { name: "Gateway A" })); + await user.click(screen.getByRole("button", { name: "Gateway A" })); await waitFor(() => { expect(screen.queryByText("tool_two")).not.toBeInTheDocument(); }); expect(screen.getByText("tool_one")).toBeInTheDocument(); - - // Arrow keys move focus across the source tablist. - const allSources = screen.getByRole("tab", { name: "All sources" }); - allSources.focus(); - await user.keyboard("{ArrowRight}"); - expect(screen.getByRole("tab", { name: "Gateway A" })).toHaveFocus(); + expect(screen.getByRole("button", { name: "Gateway A" })).toHaveAttribute( + "aria-pressed", + "true", + ); + expect(screen.getByRole("button", { name: "All sources" })).toHaveAttribute( + "aria-pressed", + "false", + ); }); it("hides the source selector when the server has a single source", async () => { @@ -321,8 +330,10 @@ describe("VirtualServerDetailsPanel components list", () => { 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(); + expect(screen.queryByRole("button", { name: "All sources" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Gateway A" })).not.toBeInTheDocument(); + expect(screen.queryByRole("group", { name: "Filter sources" })).not.toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Sources" })).not.toBeInTheDocument(); }); it("moves the active tab with arrow keys", async () => { diff --git a/src/components/gateways/VirtualServerDetailsPanel.tsx b/src/components/gateways/VirtualServerDetailsPanel.tsx index 4cf81bae..c4708cf5 100644 --- a/src/components/gateways/VirtualServerDetailsPanel.tsx +++ b/src/components/gateways/VirtualServerDetailsPanel.tsx @@ -226,19 +226,6 @@ export function VirtualServerDetailsPanel({ [], ); - const handleSourceTabKeyDown = useCallback( - (e: KeyboardEvent, currentIndex: number, sourceCount: number) => { - if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; - e.preventDefault(); - const nextIndex = - e.key === "ArrowRight" - ? (currentIndex + 1) % sourceCount - : (currentIndex - 1 + sourceCount) % sourceCount; - document.getElementById(`source-tab-${nextIndex}`)?.focus(); - }, - [], - ); - // Build API paths - only when server exists const toolsPath = useMemo(() => { if (!server?.id) return ""; @@ -636,54 +623,57 @@ export function VirtualServerDetailsPanel({ ) : ( <> {sourceTabs.length > 1 && ( -
- {[ - { - id: "all", - label: intl.formatMessage({ - id: "gateways.details.filter.allSources", - }), - isTruncated: false, - fullValue: undefined as string | undefined, - }, - ...sourceTabs, - ].map((source, index, sources) => { - const isSelected = sourceFilter === source.id; - const tabButton = ( - - ); +
+

+ {intl.formatMessage({ id: "gateways.details.sources" })} +

+
+ {[ + { + id: "all", + label: intl.formatMessage({ + id: "gateways.details.filter.allSources", + }), + isTruncated: false, + fullValue: undefined as string | undefined, + }, + ...sourceTabs, + ].map((source) => { + const isSelected = sourceFilter === source.id; + const sourceButton = ( + + ); - return ( - - {tabButton} - {source.isTruncated && ( - {source.fullValue} - )} - - ); - })} + return ( + + {sourceButton} + {source.isTruncated && ( + {source.fullValue} + )} + + ); + })} +
)} diff --git a/src/i18n/locales/en-US/gateways.json b/src/i18n/locales/en-US/gateways.json index c8e11a4b..047bfe16 100644 --- a/src/i18n/locales/en-US/gateways.json +++ b/src/i18n/locales/en-US/gateways.json @@ -113,6 +113,7 @@ "gateways.details.filter.tools": "Tools", "gateways.details.filter.resources": "Resources", "gateways.details.filter.prompts": "Prompts", + "gateways.details.sources": "Sources", "gateways.details.filterSources": "Filter sources", "gateways.details.searchComponents": "Search components", "gateways.details.filterComponents": "Filter components", diff --git a/src/i18n/locales/es-ES/gateways.json b/src/i18n/locales/es-ES/gateways.json index 5d14d6a6..e0f66a4c 100644 --- a/src/i18n/locales/es-ES/gateways.json +++ b/src/i18n/locales/es-ES/gateways.json @@ -113,6 +113,7 @@ "gateways.details.filter.tools": "Herramientas", "gateways.details.filter.resources": "Recursos", "gateways.details.filter.prompts": "Prompts", + "gateways.details.sources": "Fuentes", "gateways.details.filterSources": "Filtrar fuentes", "gateways.details.searchComponents": "Buscar componentes", "gateways.details.filterComponents": "Filtrar componentes", diff --git a/src/i18n/locales/pt-BR/gateways.json b/src/i18n/locales/pt-BR/gateways.json index 2267549c..81127ebc 100644 --- a/src/i18n/locales/pt-BR/gateways.json +++ b/src/i18n/locales/pt-BR/gateways.json @@ -113,6 +113,7 @@ "gateways.details.filter.tools": "Ferramentas", "gateways.details.filter.resources": "Recursos", "gateways.details.filter.prompts": "Prompts", + "gateways.details.sources": "Fontes", "gateways.details.filterSources": "Filtrar fontes", "gateways.details.searchComponents": "Buscar componentes", "gateways.details.filterComponents": "Filtrar componentes",