From deaa64218ba3d0ad15b14726455cdac99855f496 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 23 Jun 2026 00:08:27 -0700 Subject: [PATCH] FN-6931: fix org chart connector rendering Render agent org chart connector paths from the measured SVG overlay without stale CSS connector lines. - Size the absolute org chart SVG overlay to the chart canvas so connector paths are not clipped by the default SVG viewport. - Remove legacy pseudo-element connector bus styles that could overlap or contradict measured SVG paths. - Add regression coverage for connector SVG sizing, removed CSS connector rules, vertical paths, and empty/single-root data states. Files changed: packages/dashboard/app/components/AgentsView.css | 41 +++----------------- .../__tests__/AgentsView.orgchart.test.tsx | 44 +++++++++++++++++++++- 2 files changed, 49 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-6931 Fusion-Task-Lineage: d19d4c59-c836-4d91-9826-d4b29e178923 --- .../dashboard/app/components/AgentsView.css | 41 +++-------------- .../__tests__/AgentsView.orgchart.test.tsx | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/dashboard/app/components/AgentsView.css b/packages/dashboard/app/components/AgentsView.css index ea27d08216..eb879bfa11 100644 --- a/packages/dashboard/app/components/AgentsView.css +++ b/packages/dashboard/app/components/AgentsView.css @@ -1047,9 +1047,15 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c transition: none; } +/* +FNXC:AgentsOrgChart 2026-06-22-03:05: +AgentsView uses the measured SVG overlay as the single connector system so parent-child lines remain non-zero and aligned across horizontal/vertical layouts, desktop/mobile sizing, and pan/zoom transforms. Explicitly size the absolute SVG to fill the chart canvas instead of relying on the browser's default 300×150 SVG viewport. +*/ .agent-org-chart-connectors { position: absolute; inset: 0; + width: 100%; + height: 100%; overflow: visible; pointer-events: none; } @@ -1192,41 +1198,6 @@ FN-6774 removes the saturated left-edge status stripe from split-sidebar agent c gap: var(--org-chart-sibling-gap); padding-top: var(--org-chart-children-offset); margin-top: var(--org-chart-connector-gap); - --org-chart-first-child-center-offset: 50%; - --org-chart-last-child-center-offset: 50%; -} - -.org-chart-children::before { - content: ""; - position: absolute; - top: 0; - height: var(--org-chart-children-offset, var(--space-md)); - left: var(--org-chart-first-child-center-offset); - right: var(--org-chart-last-child-center-offset); - border-top: 1px solid var(--border); - pointer-events: none; -} - -.org-chart-children > .org-chart-node::before { - content: ""; - position: absolute; - top: calc(-1 * var(--org-chart-children-offset)); - left: 50%; - width: 1px; - height: var(--org-chart-children-offset); - border-left: 1px solid var(--border); - pointer-events: none; -} - -.agent-org-chart--vertical .org-chart-children::before { - top: 0; - left: var(--space-sm); - right: auto; - bottom: 0; - width: 1px; - height: auto; - border-top: none; - border-left: 1px solid var(--border); } .agent-org-chart--vertical { diff --git a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx index 4031a41bf7..fab0c503f7 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.orgchart.test.tsx @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { AgentsView } from "../AgentsView"; @@ -26,6 +28,17 @@ vi.mock("../../api", async (importOriginal) => { const mockFetchOrgTree = vi.mocked((apiModule as any).fetchOrgTree); const mockFetchAgents = vi.mocked((apiModule as any).fetchAgents); +const COMPONENTS_DIR = resolve(__dirname, ".."); +const AGENTS_VIEW_CSS = join(COMPONENTS_DIR, "AgentsView.css"); + +function extractRuleBlock(css: string, selector: string): string { + const ruleStart = css.indexOf(`${selector} {`); + expect(ruleStart, `Expected ${selector} to exist in AgentsView.css`).toBeGreaterThanOrEqual(0); + const bodyStart = css.indexOf("{", ruleStart); + const bodyEnd = css.indexOf("\n}", bodyStart); + expect(bodyEnd, `Expected ${selector} rule to have a closing brace`).toBeGreaterThan(bodyStart); + return css.slice(bodyStart + 1, bodyEnd); +} const orgTree = [{ agent: { id: "ceo", name: "CEO", role: "scheduler", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [ { agent: { id: "cto", name: "CTO", role: "engineer", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [ @@ -61,6 +74,18 @@ describe("AgentsView org chart interactions", () => { mockRects(); }); + it("keeps SVG connectors explicitly sized and removes the broken CSS connector bus", () => { + const css = readFileSync(AGENTS_VIEW_CSS, "utf8"); + const connectorBlock = extractRuleBlock(css, ".agent-org-chart-connectors"); + expect(connectorBlock).toMatch(/width\s*:\s*100%\s*;/); + expect(connectorBlock).toMatch(/height\s*:\s*100%\s*;/); + expect(connectorBlock).toMatch(/overflow\s*:\s*visible\s*;/); + expect(css).not.toContain("--org-chart-first-child-center-offset"); + expect(css).not.toContain("--org-chart-last-child-center-offset"); + expect(css).not.toContain(".org-chart-children::before"); + expect(css).not.toContain(".org-chart-children > .org-chart-node::before"); + }); + it("renders controls and supports transform interactions", async () => { render(); fireEvent.click(await screen.findByLabelText("Org Chart view")); @@ -112,11 +137,28 @@ describe("AgentsView org chart interactions", () => { fireEvent.click(screen.getByLabelText("Vertical layout")); await waitFor(() => { - const firstPath = document.querySelector(".agent-org-chart-connectors path")?.getAttribute("d") ?? ""; + const paths = document.querySelectorAll(".agent-org-chart-connectors path"); + expect(paths.length).toBe(4); + const firstPath = paths[0]?.getAttribute("d") ?? ""; expect(firstPath).toMatch(/^M\s\d+\s\d+\sL\s\d+\s\d+/); }); }); + it("does not render connector paths for empty or single-root org chart data states", async () => { + mockFetchOrgTree.mockResolvedValueOnce([]); + const empty = render(); + fireEvent.click(await screen.findByLabelText("Org Chart view")); + await screen.findByText("No agents found"); + expect(document.querySelectorAll(".agent-org-chart-connectors path")).toHaveLength(0); + empty.unmount(); + + mockFetchOrgTree.mockResolvedValueOnce([{ agent: { id: "solo", name: "Solo", role: "executor", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} }, children: [] }]); + render(); + fireEvent.click(await screen.findByLabelText("Org Chart view")); + await screen.findByText("Solo"); + await waitFor(() => expect(document.querySelectorAll(".agent-org-chart-connectors path")).toHaveLength(0)); + }); + it("renders mobile controls", async () => { mockViewportMode.mockReturnValue("mobile"); render();