feat(FN-4065): fix sidebar action overflow in AgentsView
Fixes sidebar action containment in the dashboard AgentsView component and adds test coverage for the sidebar action overflow contract. Fusion-Task-Id: FN-4065
This commit is contained in:
@@ -698,16 +698,30 @@
|
||||
}
|
||||
|
||||
.agent-card-actions {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
container-type: inline-size;
|
||||
container-name: agent-card-actions;
|
||||
}
|
||||
|
||||
.agent-card-actions-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-card-actions-group--secondary {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.agent-card-actions .btn {
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -715,21 +729,29 @@
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agents-split-sidebar .agent-card-actions-group {
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agents-split-sidebar .agent-card-actions .btn {
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
@container agent-card-actions (max-width: calc(var(--space-2xl) * 9)) {
|
||||
.agents-split-sidebar .agent-card-actions {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.agents-split-sidebar .agent-card-actions-group--secondary {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.agents-split-sidebar .agent-card-actions .agent-card-action-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-card-details-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.agent-card--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1368,94 +1368,98 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
</div>
|
||||
|
||||
<div className="agent-card-actions">
|
||||
{agent.state === "idle" && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Activate"
|
||||
>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Start</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.state === "active" && (
|
||||
<>
|
||||
<div className="agent-card-actions-group agent-card-actions-group--primary">
|
||||
{agent.state === "idle" && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Run Now"
|
||||
aria-label={`Run now for ${agent.name}`}
|
||||
title="Activate"
|
||||
>
|
||||
<Activity size={14} /> <span className="agent-card-action-label">Run Now</span>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Start</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.state === "active" && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Run Now"
|
||||
aria-label={`Run now for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> <span className="agent-card-action-label">Run Now</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
>
|
||||
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{agent.state === "paused" && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
title="Resume"
|
||||
>
|
||||
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Resume</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{agent.state === "paused" && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Resume"
|
||||
>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Resume</span>
|
||||
</button>
|
||||
)}
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
)}
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
|
||||
title="View live run details"
|
||||
aria-label={`View live run details for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> <span className="agent-card-action-label">Running</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
>
|
||||
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{agent.state === "error" && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
|
||||
title="View live run details"
|
||||
aria-label={`View live run details for ${agent.name}`}
|
||||
>
|
||||
<Activity size={14} /> <span className="agent-card-action-label">Running</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
title="Retry"
|
||||
>
|
||||
<Pause size={14} /> <span className="agent-card-action-label">Pause</span>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Retry</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{agent.state === "error" && (
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-card-actions-group agent-card-actions-group--secondary">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Retry"
|
||||
className="btn btn-sm agent-card-details-btn"
|
||||
onClick={() => openAgentDetail(agent.id)}
|
||||
title={`View details for ${agent.name}`}
|
||||
aria-label={`View details for ${agent.name}`}
|
||||
>
|
||||
<Play size={14} /> <span className="agent-card-action-label">Retry</span>
|
||||
<Info size={14} /> <span className="agent-card-action-label">Details</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm agent-card-details-btn"
|
||||
onClick={() => openAgentDetail(agent.id)}
|
||||
title={`View details for ${agent.name}`}
|
||||
aria-label={`View details for ${agent.name}`}
|
||||
>
|
||||
<Info size={14} /> <span className="agent-card-action-label">Details</span>
|
||||
</button>
|
||||
{(agent.state === "idle" || agent.state === "paused") && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => void handleDelete(agent.id, agent.name)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={14} /> <span className="agent-card-action-label">Delete</span>
|
||||
</button>
|
||||
)}
|
||||
{(agent.state === "idle" || agent.state === "paused") && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => void handleDelete(agent.id, agent.name)}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={14} /> <span className="agent-card-action-label">Delete</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -651,18 +651,36 @@ describe("AgentsView", () => {
|
||||
expect(screen.getAllByText("Details").length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("keeps a visible icon affordance on View Details buttons when labels are compacted", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
it("keeps a visible icon affordance on split-sidebar action buttons when labels are compacted", async () => {
|
||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
const detailsButton = await screen.findByRole("button", { name: "View details for Test Agent 1" });
|
||||
const sidebarCard = await waitFor(() => {
|
||||
const card = container.querySelector(".agents-split-sidebar .agent-card");
|
||||
expect(card).toBeTruthy();
|
||||
return card as HTMLElement;
|
||||
});
|
||||
const actions = sidebarCard.querySelector(".agent-card-actions");
|
||||
const primaryGroup = sidebarCard.querySelector(".agent-card-actions-group--primary");
|
||||
const secondaryGroup = sidebarCard.querySelector(".agent-card-actions-group--secondary");
|
||||
const detailsButton = within(sidebarCard).getByRole("button", { name: "View details for Test Agent 1" });
|
||||
|
||||
expect(actions).toBeTruthy();
|
||||
expect(primaryGroup).toBeTruthy();
|
||||
expect(secondaryGroup).toBeTruthy();
|
||||
expect(primaryGroup?.querySelector("button")).toBeTruthy();
|
||||
expect(secondaryGroup?.contains(detailsButton)).toBe(true);
|
||||
expect(detailsButton.querySelector("svg")).toBeTruthy();
|
||||
expect(sidebarCard.querySelectorAll(".agent-card-action-label").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("hides split-sidebar action labels only within an agent-card-actions container query", () => {
|
||||
it("uses a grid-and-wrap containment contract for split-sidebar action rows", () => {
|
||||
const css = loadAllAppCss();
|
||||
|
||||
expect(css).toMatch(/\.agent-card-actions\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*minmax\(0, 1fr\) auto;[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*\}/);
|
||||
expect(css).toMatch(/\.agent-card-actions-group\s*\{[^}]*display:\s*flex;[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;[^}]*\}/);
|
||||
expect(css).not.toContain(".agents-split-sidebar .agent-card-actions .agent-card-action-label {\n display: none;\n}");
|
||||
expect(css).toContain("@container agent-card-actions (max-width: calc(var(--space-2xl) * 9))");
|
||||
expect(css).toContain(".agents-split-sidebar .agent-card-actions {\n grid-template-columns: minmax(0, 1fr);\n }");
|
||||
expect(css).toContain(".agents-split-sidebar .agent-card-actions .agent-card-action-label {\n display: none;\n }");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user