feat(FN-3375): rework agents view org chart spacing

Rebuilt the agents/org chart layout with improved spacing, adding corresponding tests for the updated card styling and mobile responsiveness.

Fusion-Task-Id: FN-3375
This commit is contained in:
Fusion
2026-05-04 19:33:53 -07:00
committed by gsxdsm
parent bf8cc6e8a6
commit b0d8d8fc88
5 changed files with 67 additions and 14 deletions

View File

@@ -184,6 +184,8 @@ describe("Agent CSS classes", () => {
expect(orgChartSection).toContain("gap: var(--space-xl)");
expect(orgChartSection).toContain("padding: var(--space-lg)");
expect(orgChartSection).toContain("--org-chart-node-width: calc(var(--space-xl) * 9 + var(--space-xs))");
expect(orgChartSection).toContain("--org-chart-sibling-gap: var(--space-xl)");
expect(orgChartSection).toContain("--org-chart-children-offset: calc(var(--space-lg) + var(--space-sm))");
expect(orgChartSection).toContain("min-height: var(--org-chart-node-width)");
expect(orgChartSection).toContain("touch-action: pan-x pan-y");
expect(orgChartSection).toContain("transform-origin: top left");
@@ -192,6 +194,8 @@ describe("Agent CSS classes", () => {
expect(orgChartSection).toContain("color: var(--text-muted)");
expect(orgChartSection).toContain("border-radius: var(--radius-pill)");
expect(orgChartSection).toContain("transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast)");
expect(orgChartSection).toContain("--org-chart-subtree-leaves-number: var(--org-chart-subtree-leaves, 1)");
expect(orgChartSection).toContain("min-width: calc(");
expect(orgChartSection).not.toContain("var(--border-color)");
expect(orgChartSection).not.toContain("var(--text-primary)");
expect(orgChartSection).not.toContain("var(--text-secondary)");

View File

@@ -843,7 +843,9 @@
.agent-org-chart {
--org-chart-node-width: calc(var(--space-xl) * 9 + var(--space-xs));
--org-chart-connector-gap: var(--space-xs);
--org-chart-sibling-gap: var(--space-xl);
--org-chart-connector-gap: var(--space-sm);
--org-chart-children-offset: calc(var(--space-lg) + var(--space-sm));
display: flex;
align-items: flex-start;
justify-content: flex-start;
@@ -861,17 +863,22 @@
}
.org-chart-node {
--org-chart-subtree-leaves-number: var(--org-chart-subtree-leaves, 1);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: var(--org-chart-node-width);
min-width: var(--org-chart-node-width);
width: max-content;
min-width: calc(
var(--org-chart-subtree-leaves-number) * var(--org-chart-node-width) +
(var(--org-chart-subtree-leaves-number) - 1) * var(--org-chart-sibling-gap)
);
flex: 0 0 auto;
}
.org-chart-node-card {
width: 100%;
width: var(--org-chart-node-width);
max-width: 100%;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
@@ -918,8 +925,9 @@
.org-chart-node__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
justify-content: center;
flex-wrap: wrap;
gap: var(--space-xs);
}
.org-chart-node__badge {
@@ -945,7 +953,7 @@
transform: translateX(-50%);
bottom: calc(var(--org-chart-connector-gap) * -1);
width: 1px;
height: calc(var(--org-chart-connector-gap) * 2);
height: var(--org-chart-children-offset);
background: var(--border);
}
@@ -953,9 +961,10 @@
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
gap: var(--space-lg);
padding-top: var(--space-xl);
justify-content: space-between;
width: 100%;
gap: var(--org-chart-sibling-gap);
padding-top: var(--org-chart-children-offset);
margin-top: var(--org-chart-connector-gap);
}
@@ -972,11 +981,11 @@
.org-chart-children > .org-chart-node::before {
content: "";
position: absolute;
top: calc(var(--space-xl) * -1);
top: calc(var(--org-chart-children-offset) * -1);
left: 50%;
transform: translateX(-50%);
width: 1px;
height: var(--space-xl);
height: var(--org-chart-children-offset);
background: var(--border);
}
@@ -1077,6 +1086,8 @@
.agent-org-chart {
--org-chart-node-width: calc(var(--space-2xl) * 5);
--org-chart-sibling-gap: var(--space-sm);
--org-chart-children-offset: var(--space-lg);
padding: var(--space-sm);
padding-bottom: calc(var(--space-lg) + var(--space-sm));
gap: var(--space-sm);

View File

@@ -1,5 +1,5 @@
import "./AgentsView.css";
import { useState, useEffect, useCallback, useRef, useMemo, useId, lazy, Suspense } from "react";
import { useState, useEffect, useCallback, useRef, useMemo, useId, lazy, Suspense, type CSSProperties } from "react";
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, ChevronDown, ChevronUp, Filter, Upload, Network, SlidersHorizontal, Copy, Check, ZoomIn, ZoomOut, Minimize2 } from "lucide-react";
import type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTreeNode } from "../api";
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
@@ -155,6 +155,14 @@ export function CollapsibleErrorDisplay({
);
}
function getOrgChartLeafCount(node: OrgTreeNode): number {
if (node.children.length === 0) {
return 1;
}
return node.children.reduce((sum, child) => sum + getOrgChartLeafCount(child), 0);
}
function OrgChartNode({
node,
onSelect,
@@ -174,9 +182,14 @@ function OrgChartNode({
const health = getHealthStatus(agent);
const stateBadgeClass = getStateBadgeClass(agent.state);
const stateNodeClass = getStateCardClass("org-chart-node-card", agent.state);
const subtreeLeafCount = getOrgChartLeafCount(node);
const nodeStyle = { "--org-chart-subtree-leaves": String(subtreeLeafCount) } as CSSProperties;
return (
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
<div
className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}
style={nodeStyle}
>
<div
className={`${stateNodeClass}${selectedAgentId === agent.id ? " agent-card--selected" : ""}`}
onClick={() => onSelect(agent.id)}

View File

@@ -1291,6 +1291,29 @@ describe("AgentsView", () => {
});
});
it("sizes org chart subtree containers based on descendant leaf counts", async () => {
mockFetchOrgTree.mockResolvedValue(orgTree);
const { container } = render(<AgentsView addToast={mockAddToast} />);
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
await waitFor(() => {
expect(screen.getByText("Chief Agent")).toBeTruthy();
});
const rootNode = screen.getByText("Chief Agent").closest(".org-chart-node") as HTMLElement;
const nestedParentNode = screen.getByText("Director One").closest(".org-chart-node") as HTMLElement;
const leafNode = screen.getByText("Manager Alpha").closest(".org-chart-node") as HTMLElement;
const rootChildren = rootNode.querySelector(":scope > .org-chart-children") as HTMLElement;
expect(rootNode.style.getPropertyValue("--org-chart-subtree-leaves")).toBe("2");
expect(nestedParentNode.style.getPropertyValue("--org-chart-subtree-leaves")).toBe("1");
expect(leafNode.style.getPropertyValue("--org-chart-subtree-leaves")).toBe("1");
expect(rootChildren).toBeTruthy();
expect(rootChildren.className).toContain("org-chart-children");
expect(container.querySelectorAll(".org-chart-node--has-children").length).toBeGreaterThan(0);
});
it("shows mobile zoom controls for org chart and keeps node selection working", async () => {
mockViewportMode.mockReturnValue("mobile");
mockFetchOrgTree.mockResolvedValue(orgTree);

View File

@@ -265,6 +265,8 @@ describe("agents-view mobile CSS", () => {
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart-viewport")).toContain("min-height: calc(var(--space-2xl) * 4)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("gap: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-node-width: calc(var(--space-2xl) * 5)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-sibling-gap: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("--org-chart-children-offset: var(--space-lg)");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node-card")).toContain("padding: var(--space-sm)");
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node__badge")).toContain("font-size: calc(var(--space-sm) + var(--space-xs) * 0.625)");
});