FN-8625: remove heartbeat controls from agent org charts
Keep org charts read-only for heartbeat configuration while preserving controls on board and list surfaces. - Remove heartbeat toggle props, markup, and styling from org chart nodes - Cover both org chart layouts and board heartbeat availability with regression tests - Add a patch changeset for the dashboard behavior fix Files changed: .../fn-8625-remove-org-chart-heartbeat-toggle.md | 7 +++++++ packages/dashboard/app/components/AgentsView.css | 7 ------- packages/dashboard/app/components/AgentsView.tsx | 14 +++++--------- .../__tests__/AgentsView.orgchart.test.tsx | 20 ++++++++++++++++++++ .../app/components/__tests__/AgentsView.test.tsx | 20 +++++++++++--------- 5 files changed, 43 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-8625 Fusion-Task-Lineage: 454c05e6-fe31-486b-975a-80ba5e8c518c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8625-remove-org-chart-heartbeat-toggle.md
Normal file
7
.changeset/fn-8625-remove-org-chart-heartbeat-toggle.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Remove the heartbeat enable/disable button from agent org chart nodes.
|
||||
category: fix
|
||||
dev: HeartbeatToggle remains in board view; OrgChartNode no longer receives heartbeat props.
|
||||
@@ -1235,13 +1235,6 @@ AgentsView uses the measured SVG overlay as the single connector system so paren
|
||||
font-size: var(--space-md);
|
||||
}
|
||||
|
||||
/* FNXC:AgentHeartbeatControls 2026-07-23-13:15: Keep the org action outside the card's button-like detail target so keyboard users never encounter nested interactive controls. */
|
||||
.org-chart-node__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.agent-heartbeat-toggle {
|
||||
min-height: var(--touch-target-min-size);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,10 @@ type OrgChartTransform = { scale: number; x: number; y: number };
|
||||
|
||||
/*
|
||||
FNXC:AgentHeartbeatControls 2026-07-23-13:10:
|
||||
List, board, and org-chart cards use one explicit heartbeat action. It changes only runtimeConfig.enabled through the preserved payload helper; lifecycle pause/resume remains a separate control.
|
||||
List and board cards use one explicit heartbeat action. It changes only runtimeConfig.enabled through the preserved payload helper; lifecycle pause/resume remains a separate control.
|
||||
|
||||
FNXC:AgentHeartbeatControls 2026-07-26-19:07:
|
||||
FN-8625 makes the org chart intentionally read-only for heartbeat configuration. Enable and disable actions remain available through board, list, and bulk-control surfaces.
|
||||
*/
|
||||
function HeartbeatToggle({ agent, pending, onToggle }: { agent: Agent; pending: boolean; onToggle: (agent: Agent) => void }) {
|
||||
const { t } = useTranslation("app");
|
||||
@@ -210,15 +213,13 @@ function HeartbeatToggle({ agent, pending, onToggle }: { agent: Agent; pending:
|
||||
type OrgChartNodeProps = {
|
||||
node: OrgTreeNode;
|
||||
onSelect: (id: string) => void;
|
||||
onToggleHeartbeat: (agent: Agent) => void;
|
||||
isHeartbeatPending: (agentId: string) => boolean;
|
||||
getHealthStatus: (agent: Agent) => AgentHealthStatus;
|
||||
selectedAgentId: string | null;
|
||||
registerNodeElement: (id: string, element: HTMLDivElement | null) => void;
|
||||
linksRef: MutableRefObject<OrgChartLink[]>;
|
||||
};
|
||||
|
||||
function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, getHealthStatus, selectedAgentId, registerNodeElement, linksRef }: OrgChartNodeProps) {
|
||||
function OrgChartNode({ node, onSelect, getHealthStatus, selectedAgentId, registerNodeElement, linksRef }: OrgChartNodeProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const { agent, children } = node;
|
||||
const health = getHealthStatus(agent);
|
||||
@@ -258,7 +259,6 @@ function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, g
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="org-chart-node__actions"><HeartbeatToggle agent={agent} pending={isHeartbeatPending(agent.id)} onToggle={onToggleHeartbeat} /></div>
|
||||
{children.length > 0 && (
|
||||
<div className="org-chart-children" role="group" aria-label={t("agents.orgChartEmployees", "{{name}} employees", { name: agent.name })}>
|
||||
{children.map((child) => {
|
||||
@@ -268,8 +268,6 @@ function OrgChartNode({ node, onSelect, onToggleHeartbeat, isHeartbeatPending, g
|
||||
key={child.agent.id}
|
||||
node={child}
|
||||
onSelect={onSelect}
|
||||
onToggleHeartbeat={onToggleHeartbeat}
|
||||
isHeartbeatPending={isHeartbeatPending}
|
||||
getHealthStatus={getHealthStatus}
|
||||
selectedAgentId={selectedAgentId}
|
||||
registerNodeElement={registerNodeElement}
|
||||
@@ -1874,8 +1872,6 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
key={node.agent.id}
|
||||
node={node}
|
||||
onSelect={handleOrgChartNodeSelect}
|
||||
onToggleHeartbeat={(agent) => void handleHeartbeatEnabledChange(agent, !isAgentHeartbeatEnabled(agent))}
|
||||
isHeartbeatPending={(agentId) => isBulkHeartbeatMutationRunning || heartbeatMutationAgentIds.has(agentId)}
|
||||
getHealthStatus={getHealthStatus}
|
||||
selectedAgentId={selectedOrgChartAgentId}
|
||||
registerNodeElement={registerOrgChartNodeElement}
|
||||
|
||||
@@ -147,6 +147,26 @@ describe("AgentsView org chart interactions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders heartbeat controls nowhere in either org-chart layout", async () => {
|
||||
const { container } = render(<AgentsView addToast={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByLabelText("Org Chart view"));
|
||||
|
||||
const assertHeartbeatControlsAreAbsent = () => {
|
||||
const chart = screen.getByTestId("agent-org-chart");
|
||||
expect(container.querySelectorAll(".org-chart-node__actions")).toHaveLength(0);
|
||||
expect(Array.from(chart.querySelectorAll("button")).filter((button) => /Disable heartbeat|Enable heartbeat/i.test(button.getAttribute("aria-label") ?? button.textContent ?? ""))).toHaveLength(0);
|
||||
expect(chart.querySelectorAll(".org-chart-node button")).toHaveLength(0);
|
||||
};
|
||||
|
||||
await screen.findByText("Eng C");
|
||||
assertHeartbeatControlsAreAbsent();
|
||||
|
||||
const layoutToggle = screen.getByTestId("agent-org-chart-layout-toggle");
|
||||
fireEvent.click(layoutToggle.querySelector<HTMLButtonElement>('[data-layout-value="vertical"]')!);
|
||||
await waitFor(() => expect(screen.getByTestId("agent-org-chart")).toHaveAttribute("data-layout-mode", "vertical"));
|
||||
assertHeartbeatControlsAreAbsent();
|
||||
});
|
||||
|
||||
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()} />);
|
||||
|
||||
@@ -841,6 +841,13 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps heartbeat controls available on board cards", async () => {
|
||||
renderView(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Board view" }));
|
||||
expect(await screen.findByRole("button", { name: /Disable heartbeat for Test Agent 1/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays agent task with column context when enriched", async () => {
|
||||
mockFetchAgents.mockResolvedValue([
|
||||
{ ...mockAgents[0], id: "agent-triage", name: "Triage Agent", taskId: "FN-TRIAGE", taskColumn: "triage", state: "active" as AgentState },
|
||||
@@ -2971,19 +2978,14 @@ describe("AgentsView", () => {
|
||||
expect(boardToggle).toHaveAttribute("aria-pressed", "true");
|
||||
});
|
||||
|
||||
it("renders recursive org controls and prevents a pending toggle from issuing duplicate PATCH requests", async () => {
|
||||
let resolveUpdate: ((agent: Agent) => void) | undefined;
|
||||
it("keeps heartbeat mutations out of org chart nodes", async () => {
|
||||
mockFetchOrgTree.mockResolvedValue([{ agent: { ...mockAgents[1], runtimeConfig: { enabled: false, heartbeatIntervalMs: 900_000 } }, children: [] }]);
|
||||
mockUpdateAgent.mockImplementation(() => new Promise<Agent>((resolve) => { resolveUpdate = resolve; }));
|
||||
renderView(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
|
||||
|
||||
const toggle = await screen.findByRole("button", { name: "Enable heartbeat for Test Agent 2" });
|
||||
fireEvent.click(toggle);
|
||||
fireEvent.click(toggle);
|
||||
expect(mockUpdateAgent).toHaveBeenCalledTimes(1);
|
||||
expect(toggle).toBeDisabled();
|
||||
resolveUpdate?.(mockAgents[1]);
|
||||
await screen.findByText("Test Agent 2");
|
||||
expect(screen.queryByRole("button", { name: /heartbeat for Test Agent 2/i })).toBeNull();
|
||||
expect(mockUpdateAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates every eligible current-project durable agent through bulk controls despite filtered display", async () => {
|
||||
|
||||
Reference in New Issue
Block a user