feat(FN-785): style Agents view state selector and document controls
- Replace plain <select> dropdown with styled agent-state-filter container featuring Filter icon, hover/focus states, and consistent dashboard theming - Add aria-label for accessibility on the state filter dropdown - Add CSS styles for .agent-state-filter wrapper with border, color transitions, and focus ring - Add unit test for styled filter container rendering and aria-label presence - Document Agents view features (state filter, view modes, CRUD, health monitoring) in README
This commit is contained in:
@@ -92,6 +92,16 @@ A persistent footer status bar at the bottom of the dashboard displays real-time
|
||||
**API Endpoint**:
|
||||
- `GET /api/executor/stats` - Returns `globalPause`, `enginePaused`, `maxConcurrent`, and `lastActivityAt` for state derivation. Column-based counts (running, blocked, stuck, queued, in-review) are derived client-side from the shared task list.
|
||||
|
||||
### Agents View
|
||||
Manage AI agents with a dedicated control surface accessible from the main dashboard navigation.
|
||||
|
||||
**Features**:
|
||||
- **State Filter**: Styled dropdown to filter agents by state (All States, Idle, Active, Paused, Terminated) with icon and consistent dashboard styling
|
||||
- **View Modes**: Board (compact grid) and list (detailed card) layouts, persisted to localStorage
|
||||
- **Agent CRUD**: Create agents with name and role, change state, update roles inline, delete terminated agents
|
||||
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated)
|
||||
- **Agent Detail**: Click any agent card to open a detail modal with full agent information
|
||||
|
||||
### Interactive Terminal
|
||||
Access a fully functional PTY (pseudo-terminal) shell directly from the dashboard. Click the terminal icon in the header to open the interactive terminal modal.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { JSX } from "react";
|
||||
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight } from "lucide-react";
|
||||
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, Filter } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentState } from "../api";
|
||||
import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api";
|
||||
import { AgentDetailView } from "./AgentDetailView";
|
||||
@@ -189,17 +189,21 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<div className="agents-view-content">
|
||||
{/* Filter and Create Bar */}
|
||||
<div className="agent-controls">
|
||||
<select
|
||||
className="select"
|
||||
value={filterState}
|
||||
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
|
||||
>
|
||||
<option value="all">All States</option>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
<div className="agent-state-filter">
|
||||
<Filter size={14} />
|
||||
<select
|
||||
className="agent-state-filter-select"
|
||||
value={filterState}
|
||||
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
|
||||
aria-label="Filter agents by state"
|
||||
>
|
||||
<option value="all">All States</option>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn--primary"
|
||||
@@ -568,8 +572,38 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-controls .select {
|
||||
width: auto;
|
||||
.agent-state-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-state-filter:hover {
|
||||
border-color: var(--text-dim);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.agent-state-filter:focus-within {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-state-filter-select {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-primary);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.agent-create-form {
|
||||
|
||||
@@ -216,6 +216,23 @@ describe("AgentsView", () => {
|
||||
});
|
||||
|
||||
describe("filter agents by state", () => {
|
||||
it("renders the state filter with styled container", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All States")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Styled filter container exists
|
||||
const filterContainer = document.querySelector(".agent-state-filter");
|
||||
expect(filterContainer).toBeTruthy();
|
||||
|
||||
// Select has correct aria-label
|
||||
const filterSelect = screen.getByLabelText("Filter agents by state");
|
||||
expect(filterSelect).toBeTruthy();
|
||||
expect(filterSelect).toHaveValue("all");
|
||||
});
|
||||
|
||||
it("can filter agents by state", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user