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