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:
@@ -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 {
|
||||
|
||||
@@ -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()} />);
|
||||
|
||||
Reference in New Issue
Block a user