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("gap: var(--space-xl)");
|
||||||
expect(orgChartSection).toContain("padding: var(--space-lg)");
|
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-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("min-height: var(--org-chart-node-width)");
|
||||||
expect(orgChartSection).toContain("touch-action: pan-x pan-y");
|
expect(orgChartSection).toContain("touch-action: pan-x pan-y");
|
||||||
expect(orgChartSection).toContain("transform-origin: top left");
|
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("color: var(--text-muted)");
|
||||||
expect(orgChartSection).toContain("border-radius: var(--radius-pill)");
|
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("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(--border-color)");
|
||||||
expect(orgChartSection).not.toContain("var(--text-primary)");
|
expect(orgChartSection).not.toContain("var(--text-primary)");
|
||||||
expect(orgChartSection).not.toContain("var(--text-secondary)");
|
expect(orgChartSection).not.toContain("var(--text-secondary)");
|
||||||
|
|||||||
@@ -843,7 +843,9 @@
|
|||||||
|
|
||||||
.agent-org-chart {
|
.agent-org-chart {
|
||||||
--org-chart-node-width: calc(var(--space-xl) * 9 + var(--space-xs));
|
--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;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -861,17 +863,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node {
|
.org-chart-node {
|
||||||
|
--org-chart-subtree-leaves-number: var(--org-chart-subtree-leaves, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: var(--org-chart-node-width);
|
width: max-content;
|
||||||
min-width: var(--org-chart-node-width);
|
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;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node-card {
|
.org-chart-node-card {
|
||||||
width: 100%;
|
width: var(--org-chart-node-width);
|
||||||
|
max-width: 100%;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
@@ -918,8 +925,9 @@
|
|||||||
.org-chart-node__meta {
|
.org-chart-node__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
gap: var(--space-sm);
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-node__badge {
|
.org-chart-node__badge {
|
||||||
@@ -945,7 +953,7 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
bottom: calc(var(--org-chart-connector-gap) * -1);
|
bottom: calc(var(--org-chart-connector-gap) * -1);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: calc(var(--org-chart-connector-gap) * 2);
|
height: var(--org-chart-children-offset);
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,9 +961,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
gap: var(--space-lg);
|
width: 100%;
|
||||||
padding-top: var(--space-xl);
|
gap: var(--org-chart-sibling-gap);
|
||||||
|
padding-top: var(--org-chart-children-offset);
|
||||||
margin-top: var(--org-chart-connector-gap);
|
margin-top: var(--org-chart-connector-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,11 +981,11 @@
|
|||||||
.org-chart-children > .org-chart-node::before {
|
.org-chart-children > .org-chart-node::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--space-xl) * -1);
|
top: calc(var(--org-chart-children-offset) * -1);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: var(--space-xl);
|
height: var(--org-chart-children-offset);
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,6 +1086,8 @@
|
|||||||
|
|
||||||
.agent-org-chart {
|
.agent-org-chart {
|
||||||
--org-chart-node-width: calc(var(--space-2xl) * 5);
|
--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: var(--space-sm);
|
||||||
padding-bottom: calc(var(--space-lg) + var(--space-sm));
|
padding-bottom: calc(var(--space-lg) + var(--space-sm));
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./AgentsView.css";
|
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 { 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 type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTreeNode } from "../api";
|
||||||
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } 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({
|
function OrgChartNode({
|
||||||
node,
|
node,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -174,9 +182,14 @@ function OrgChartNode({
|
|||||||
const health = getHealthStatus(agent);
|
const health = getHealthStatus(agent);
|
||||||
const stateBadgeClass = getStateBadgeClass(agent.state);
|
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||||
const stateNodeClass = getStateCardClass("org-chart-node-card", 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 (
|
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
|
<div
|
||||||
className={`${stateNodeClass}${selectedAgentId === agent.id ? " agent-card--selected" : ""}`}
|
className={`${stateNodeClass}${selectedAgentId === agent.id ? " agent-card--selected" : ""}`}
|
||||||
onClick={() => onSelect(agent.id)}
|
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 () => {
|
it("shows mobile zoom controls for org chart and keeps node selection working", async () => {
|
||||||
mockViewportMode.mockReturnValue("mobile");
|
mockViewportMode.mockReturnValue("mobile");
|
||||||
mockFetchOrgTree.mockResolvedValue(orgTree);
|
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-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("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-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-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)");
|
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