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
This commit is contained in:
gsxdsm
2026-06-23 00:08:27 -07:00
parent 2d038e7ffa
commit deaa64218b
2 changed files with 49 additions and 36 deletions

View File

@@ -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 {

View File

@@ -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(<AgentsView addToast={vi.fn()} />);
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(<AgentsView addToast={vi.fn()} />);
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(<AgentsView addToast={vi.fn()} />);
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(<AgentsView addToast={vi.fn()} />);