From 9971dbd51a6a4ad9724912f3bc10a8d01e645bce Mon Sep 17 00:00:00 2001 From: everson-junior <45975050+everson-junior@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:42:44 -0300 Subject: [PATCH] feat(topology): add click navigation to provider page and filter inactive providers (#9024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log --- .../(dashboard)/dashboard/HomePageClient.tsx | 9 +++- src/app/(dashboard)/home/ProviderTopology.tsx | 25 ++++++++-- .../unit/topology-filtering-and-click.test.ts | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/unit/topology-filtering-and-click.test.ts diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index 3b1e2a91da..b5b5883dfd 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -498,6 +498,12 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const canonicalProviderId = normalizeProviderId(rawProviderId); if (!canonicalProviderId || byProvider.has(canonicalProviderId)) return; + // Exclude providers with no active connections (or where all connections are deactivated) + const hasActiveConn = providerConnections.some( + (c) => normalizeProviderId(c.provider) === canonicalProviderId && c.isActive !== false + ); + if (!hasActiveConn) return; + const resolvedName = getProviderDisplayLabel(rawProviderId, providerNodes) || name || @@ -515,10 +521,11 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { providerStats .filter((provider) => provider.total > 0) .forEach((provider) => addProvider(provider.id, provider.provider.name)); + providerConnections.forEach((conn) => addProvider(conn.provider)); Object.keys(providerMetrics).forEach((provider) => addProvider(provider)); return Array.from(byProvider.values()); - }, [providerStats, providerMetrics, providerNodes]); + }, [providerStats, providerMetrics, providerNodes, providerConnections]); const { lastProvider, errorProvider } = providerTopology; diff --git a/src/app/(dashboard)/home/ProviderTopology.tsx b/src/app/(dashboard)/home/ProviderTopology.tsx index 2324d93d85..413a7a7a7c 100644 --- a/src/app/(dashboard)/home/ProviderTopology.tsx +++ b/src/app/(dashboard)/home/ProviderTopology.tsx @@ -1,6 +1,7 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useCallback } from "react"; +import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Handle, Position, type Node, type Edge, type NodeTypes } from "@xyflow/react"; import { AI_PROVIDERS } from "@/shared/constants/providers"; @@ -58,7 +59,7 @@ function ProviderNode({ data }: { data: ProviderNodeData }) { return (
{ + if (node.type !== "provider") return; + const providerId = + (node.data as ProviderNodeData | undefined)?.providerId || + node.id.replace(/^provider-/, ""); + if (providerId) { + router.push(`/dashboard/providers/${providerId}`); + } + }, + [router] + ); + const containerClass = "h-[300px] w-full min-w-0 rounded-xl border border-border bg-bg-subtle/20 overflow-hidden sm:h-[420px]"; @@ -351,6 +365,7 @@ export default function ProviderTopology({ nodeTypes={nodeTypes} fitKey={providersKey} className={containerClass} + onNodeClick={handleNodeClick} /> ); } diff --git a/tests/unit/topology-filtering-and-click.test.ts b/tests/unit/topology-filtering-and-click.test.ts new file mode 100644 index 0000000000..aa16c27253 --- /dev/null +++ b/tests/unit/topology-filtering-and-click.test.ts @@ -0,0 +1,48 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const homePageClientSrc = readFileSync( + fileURLToPath(new URL("../../src/app/(dashboard)/dashboard/HomePageClient.tsx", import.meta.url)), + "utf8" +); + +const providerTopologySrc = readFileSync( + fileURLToPath(new URL("../../src/app/(dashboard)/home/ProviderTopology.tsx", import.meta.url)), + "utf8" +); + +test("HomePageClient filters out providers where all connections are deactivated (isActive === false)", () => { + assert.match( + homePageClientSrc, + /hasActiveConn\s*=\s*providerConnections\.some\(\s*\(c\)\s*=>\s*normalizeProviderId\(c\.provider\)\s*===\s*canonicalProviderId\s*&&\s*c\.isActive\s*!==\s*false\s*\)/, + "HomePageClient must exclude providers whose connections are all inactive/disabled" + ); + + assert.match( + homePageClientSrc, + /\}, \[providerStats, providerMetrics, providerNodes, providerConnections\]\);/, + "topologyProviders must depend on providerConnections to reflect switch toggle state changes" + ); +}); + +test("ProviderTopology configures click-to-navigate on provider nodes", () => { + assert.match( + providerTopologySrc, + /router\.push\(`\/dashboard\/providers\/\${providerId}`\)/, + "ProviderTopology must navigate to the clicked provider page /dashboard/providers/${providerId}" + ); + + assert.match( + providerTopologySrc, + /onNodeClick=\{handleNodeClick\}/, + "ProviderTopology must pass handleNodeClick to FlowCanvas" + ); + + assert.match( + providerTopologySrc, + /cursor-pointer hover:scale-105/, + "ProviderNode must render with a pointer cursor and visual hover effect" + ); +});