feat(FN-1698): merge fusion/fn-1698
This commit is contained in:
@@ -229,6 +229,7 @@ Manage AI agents with a dedicated control surface accessible from the main dashb
|
||||
|
||||
**Features**:
|
||||
- **State Filter**: Styled dropdown to filter agents by state (All States, Idle, Active, Paused, Terminated) with Filter icon, aria-label, and consistent dashboard styling using design tokens (`--radius-sm`, `--border`, `--bg`, `--focus-ring`)
|
||||
- **Terminated Agent Filtering**: By default ("All States" filter), terminated agents are automatically hidden from the agent list to reduce clutter from frequently-terminating runtime task-worker agents. Terminated agents remain accessible by explicitly selecting the "Terminated" filter option, enabling intentional inspection and cleanup when needed. This behavior applies to both the main AgentsView and the AgentListModal.
|
||||
- **View Modes**: Board (compact grid) and list (detailed card) layouts, persisted to localStorage
|
||||
- **Agent CRUD**: Create agents with name and role (create form's text input and role/type select both use tokenized styling — `var(--surface)`, `var(--text)`, `var(--border)`, `var(--radius-sm)`, `var(--focus-ring)` — for consistent theme-aware rendering across all color themes and light/dark modes), change state, update roles inline, delete idle and terminated agents (active and paused agents must be stopped/terminated first)
|
||||
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated) using CSS variable references for theme consistency
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { X, Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, Filter } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState } from "../api";
|
||||
@@ -62,6 +62,15 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
const [editingRoleForAgent, setEditingRoleForAgent] = useState<string | null>(null);
|
||||
const roleSelectRef = useRef<HTMLSelectElement>(null);
|
||||
|
||||
// Filter agents for display: hide terminated agents in default "All States" view
|
||||
// but show them when the user explicitly filters to "terminated"
|
||||
const displayAgents = useMemo(() => {
|
||||
if (filterState === "all") {
|
||||
return agents.filter(a => a.state !== "terminated");
|
||||
}
|
||||
return agents;
|
||||
}, [agents, filterState]);
|
||||
|
||||
const loadAgents = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -270,7 +279,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
|
||||
{/* Agent List */}
|
||||
<div className={view === "board" ? "agent-board" : "agent-list"}>
|
||||
{agents.length === 0 ? (
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
@@ -278,7 +287,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
</div>
|
||||
) : view === "board" ? (
|
||||
// Board view: compact grid layout
|
||||
agents.map(agent => {
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
return (
|
||||
@@ -409,7 +418,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
|
||||
})
|
||||
) : (
|
||||
// List view: detailed card layout
|
||||
agents.map(agent => {
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
|
||||
@@ -223,6 +223,35 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
|
||||
const hierarchy = useAgentHierarchy(agents, projectId);
|
||||
|
||||
// Filter agents for display: hide terminated agents in default "All States" view
|
||||
// but show them when the user explicitly filters to "terminated"
|
||||
const displayAgents = useMemo(() => {
|
||||
if (filterState === "all") {
|
||||
return agents.filter(a => a.state !== "terminated");
|
||||
}
|
||||
return agents;
|
||||
}, [agents, filterState]);
|
||||
|
||||
// Filter org tree to exclude terminated agents in default view
|
||||
const displayOrgTree = useMemo(() => {
|
||||
if (filterState === "all") {
|
||||
// Recursively filter out terminated agents from the org tree
|
||||
const filterNode = (node: OrgTreeNode): OrgTreeNode | null => {
|
||||
if (node.agent.state === "terminated") return null;
|
||||
return {
|
||||
...node,
|
||||
children: node.children
|
||||
.map(filterNode)
|
||||
.filter((n): n is OrgTreeNode => n !== null),
|
||||
};
|
||||
};
|
||||
return orgTree
|
||||
.map(filterNode)
|
||||
.filter((n): n is OrgTreeNode => n !== null);
|
||||
}
|
||||
return orgTree;
|
||||
}, [orgTree, filterState]);
|
||||
|
||||
const loadAgents = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -501,7 +530,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
{/* Agent List */}
|
||||
{agentView === "tree" ? (
|
||||
<div className="agent-tree__view">
|
||||
{agents.length === 0 ? (
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
@@ -529,14 +558,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<RefreshCw size={18} className="spin" />
|
||||
<span>Loading org chart...</span>
|
||||
</div>
|
||||
) : orgTree.length === 0 ? (
|
||||
) : displayOrgTree.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
<p className="text-secondary">Create an agent to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
orgTree.map((node) => (
|
||||
displayOrgTree.map((node) => (
|
||||
<OrgChartNode
|
||||
key={node.agent.id}
|
||||
node={node}
|
||||
@@ -549,7 +578,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
|
||||
{agents.length === 0 ? (
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
@@ -557,7 +586,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
</div>
|
||||
) : agentView === "board" ? (
|
||||
// Board view: compact grid layout
|
||||
agents.map(agent => {
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
return (
|
||||
@@ -716,7 +745,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
})
|
||||
) : (
|
||||
// List view: detailed card layout
|
||||
agents.map(agent => {
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
return (
|
||||
|
||||
@@ -183,7 +183,31 @@ describe("AgentListModal", () => {
|
||||
expect(screen.getByText("idle")).toBeTruthy();
|
||||
expect(screen.getByText("active")).toBeTruthy();
|
||||
expect(screen.getByText("paused")).toBeTruthy();
|
||||
// Terminated agents are hidden in default "All States" view
|
||||
expect(screen.queryByText("terminated")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows terminated agents when explicitly filtered", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("terminated")).toBeTruthy();
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -567,7 +591,7 @@ describe("AgentListModal", () => {
|
||||
});
|
||||
|
||||
describe("agent deletion", () => {
|
||||
it("shows Delete button for idle and terminated agents", async () => {
|
||||
it("shows Delete button for idle agents in default view (terminated filtered out)", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
@@ -577,12 +601,40 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Multiple delete buttons: one for idle (agent-001) and one for terminated (agent-004)
|
||||
// In default "All States" view, only idle agent (agent-001) should have delete button
|
||||
// Terminated agents (agent-004) are filtered out
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(2);
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Verify Start button appears for terminated agent (agent-004)
|
||||
// Verify terminated agent is not visible
|
||||
expect(screen.queryByText("Test Agent 4")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows Delete button for terminated agents when explicitly filtered", async () => {
|
||||
render(
|
||||
<AgentListModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
// Now we should see the Delete button for terminated agent
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Verify Start button appears for terminated agent
|
||||
const agentCards = document.querySelectorAll(".agent-card");
|
||||
let terminatedCard: Element | null = null;
|
||||
agentCards.forEach(card => {
|
||||
@@ -592,7 +644,7 @@ describe("AgentListModal", () => {
|
||||
expect(terminatedStartBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
it("confirms before deleting agent", async () => {
|
||||
it("confirms before deleting terminated agent (from terminated filter)", async () => {
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
|
||||
render(
|
||||
@@ -604,7 +656,16 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter to see terminated agent
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Find delete button for terminated agent (agent-004)
|
||||
@@ -624,7 +685,7 @@ describe("AgentListModal", () => {
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("deletes agent after confirmation", async () => {
|
||||
it("deletes terminated agent after confirmation (from terminated filter)", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
|
||||
render(
|
||||
@@ -636,7 +697,15 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter to see terminated agent
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Find delete button for terminated agent (agent-004)
|
||||
@@ -658,7 +727,7 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("deletes idle agent after confirmation", async () => {
|
||||
it("deletes idle agent after confirmation (from default view)", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
|
||||
render(
|
||||
@@ -670,7 +739,10 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
// Only idle agent (agent-001) should have delete button in default view
|
||||
// Terminated agent (agent-004) is filtered out
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Find delete button for idle agent (agent-001)
|
||||
@@ -705,17 +777,19 @@ describe("AgentListModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
// Only idle agent (agent-001) should have delete button in default view
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Click the first available delete button
|
||||
// Click the idle agent's delete button
|
||||
const agentCards = document.querySelectorAll(".agent-card");
|
||||
let terminatedCard: Element | null = null;
|
||||
let idleCard: Element | null = null;
|
||||
agentCards.forEach(card => {
|
||||
if (card.textContent?.includes("agent-004")) terminatedCard = card;
|
||||
if (card.textContent?.includes("agent-001")) idleCard = card;
|
||||
});
|
||||
const terminatedDeleteBtn = terminatedCard?.querySelector('[title="Delete"]') as HTMLElement;
|
||||
fireEvent.click(terminatedDeleteBtn);
|
||||
const idleDeleteBtn = idleCard?.querySelector('[title="Delete"]') as HTMLElement;
|
||||
fireEvent.click(idleDeleteBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -1059,8 +1133,9 @@ describe("AgentListModal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Board view should render compact cards
|
||||
// 4 agents total, but terminated (agent-004) is filtered out in default view
|
||||
const boardCards = document.querySelectorAll(".agent-board-card");
|
||||
expect(boardCards.length).toBe(mockAgents.length);
|
||||
expect(boardCards.length).toBe(3);
|
||||
});
|
||||
|
||||
// Check that board view elements are present
|
||||
@@ -1122,8 +1197,9 @@ describe("AgentListModal", () => {
|
||||
expect(listContainer).toBeTruthy();
|
||||
|
||||
// Detailed cards should be present
|
||||
// 4 agents total, but terminated (agent-004) is filtered out in default view
|
||||
const agentCards = document.querySelectorAll(".agent-card");
|
||||
expect(agentCards.length).toBe(mockAgents.length);
|
||||
expect(agentCards.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +139,23 @@ describe("AgentsView", () => {
|
||||
expect(screen.getAllByText("idle").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("paused").length).toBeGreaterThanOrEqual(1);
|
||||
// Terminated agents are hidden in default "All States" view
|
||||
expect(screen.queryAllByText("terminated").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows terminated agents when explicitly filtered", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("terminated").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -194,7 +211,8 @@ describe("AgentsView", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const boardCards = document.querySelectorAll(".agent-board-card");
|
||||
expect(boardCards.length).toBe(mockAgents.length);
|
||||
// 4 agents total, but terminated (agent-004) is filtered out in default view
|
||||
expect(boardCards.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -659,13 +677,35 @@ describe("AgentsView", () => {
|
||||
});
|
||||
|
||||
describe("delete agent", () => {
|
||||
it("shows Delete button for idle and terminated agents", async () => {
|
||||
it("shows Delete button for idle agents in default view (terminated filtered out)", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// There should be multiple Delete buttons: one for idle (agent-001) and one for terminated (agent-004)
|
||||
// In default "All States" view, only idle agent (agent-001) should show Delete button
|
||||
// Terminated agents (agent-004) are filtered out
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(2);
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Verify terminated agent is not visible
|
||||
expect(screen.queryByText("Test Agent 4")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows Delete button for terminated agents when explicitly filtered", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
// Now we should see the Delete button for terminated agent
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -688,12 +728,21 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("confirms before deleting agent", async () => {
|
||||
it("confirms before deleting terminated agent (from terminated filter)", async () => {
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter to see terminated agent
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
// Click the delete button for the terminated agent (agent-004)
|
||||
const agentCards = document.querySelectorAll(".agent-card");
|
||||
let terminatedCard: Element | null = null;
|
||||
@@ -713,13 +762,21 @@ describe("AgentsView", () => {
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("deletes agent after confirmation", async () => {
|
||||
it("deletes terminated agent after confirmation (from terminated filter)", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Switch to terminated filter to see terminated agent
|
||||
const filterSelect = screen.getByDisplayValue("All States");
|
||||
fireEvent.change(filterSelect, { target: { value: "terminated" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Agent 4")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Find the delete button for terminated agent (agent-004)
|
||||
@@ -741,13 +798,16 @@ describe("AgentsView", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("deletes idle agent after confirmation", async () => {
|
||||
it("deletes idle agent after confirmation (from default view)", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(2);
|
||||
// Only idle agent (agent-001) should have delete button in default view
|
||||
// Terminated agent (agent-004) is filtered out
|
||||
const deleteButtons = screen.getAllByTitle("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Find the delete button for idle agent (agent-001)
|
||||
|
||||
Reference in New Issue
Block a user