feat(dashboard): unify agent lifecycle controls in AgentsView
- Remove Stop buttons from active/paused/running/error in both grid and list views; Pause/Resume/Retry cover the reversible transitions and Delete stays available on idle/terminated for teardown. Matches the existing shape of AgentDetailView after the earlier cleanup. - Drop the redundant interval badge next to the heartbeat dropdown — the selected <option> already shows the current value. - Add a "Next: HH:MM:SS" indicator alongside "Last:" for active/running agents, derived from lastHeartbeatAt + configuredIntervalMs (full timestamp in the tooltip). Paused/idle/error/terminated agents show Last only since there's no scheduled next tick. - Remove now-unused Square and Heart icon imports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
|
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, ChevronDown, GitBranch, Filter, Upload, Network } from "lucide-react";
|
||||||
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
|
import type { Agent, AgentCapability, AgentState, OrgTreeNode } from "../api";
|
||||||
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
|
import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree } from "../api";
|
||||||
import { AgentDetailView } from "./AgentDetailView";
|
import { AgentDetailView } from "./AgentDetailView";
|
||||||
@@ -754,32 +754,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
>
|
>
|
||||||
<Pause size={14} />
|
<Pause size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} />
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{agent.state === "paused" && (
|
{agent.state === "paused" && (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleStateChange(agent.id, "active")}
|
||||||
onClick={() => void handleStateChange(agent.id, "active")}
|
title="Resume"
|
||||||
title="Resume"
|
>
|
||||||
>
|
<Play size={14} />
|
||||||
<Play size={14} />
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{agent.state === "running" && (
|
{agent.state === "running" && (
|
||||||
<>
|
<>
|
||||||
@@ -798,32 +782,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
>
|
>
|
||||||
<Pause size={14} />
|
<Pause size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} />
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{agent.state === "error" && (
|
{agent.state === "error" && (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleStateChange(agent.id, "active")}
|
||||||
onClick={() => void handleStateChange(agent.id, "active")}
|
title="Retry"
|
||||||
title="Retry"
|
>
|
||||||
>
|
<Play size={14} />
|
||||||
<Play size={14} />
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{agent.state === "terminated" && (
|
{agent.state === "terminated" && (
|
||||||
<>
|
<>
|
||||||
@@ -947,7 +915,6 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
)}
|
)}
|
||||||
<div className="agent-heartbeat-control">
|
<div className="agent-heartbeat-control">
|
||||||
<span className="text-secondary">Heartbeat:</span>
|
<span className="text-secondary">Heartbeat:</span>
|
||||||
<span className="badge text-secondary">{formatHeartbeatInterval(configuredIntervalMs)}</span>
|
|
||||||
<select
|
<select
|
||||||
className="select agent-heartbeat-select"
|
className="select agent-heartbeat-select"
|
||||||
value={configuredIntervalMs}
|
value={configuredIntervalMs}
|
||||||
@@ -962,11 +929,23 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{isUpdatingHeartbeat && <span className="agent-heartbeat-saving text-secondary">Saving…</span>}
|
{isUpdatingHeartbeat && <span className="agent-heartbeat-saving text-secondary">Saving…</span>}
|
||||||
{agent.lastHeartbeatAt && (
|
{agent.lastHeartbeatAt && (() => {
|
||||||
<span className="agent-heartbeat-last text-secondary" title={new Date(agent.lastHeartbeatAt).toLocaleString()}>
|
const lastAt = new Date(agent.lastHeartbeatAt);
|
||||||
Last: {new Date(agent.lastHeartbeatAt).toLocaleTimeString()}
|
const nextAt = new Date(lastAt.getTime() + configuredIntervalMs);
|
||||||
</span>
|
const isTicking = agent.state === "active" || agent.state === "running";
|
||||||
)}
|
return (
|
||||||
|
<>
|
||||||
|
<span className="agent-heartbeat-last text-secondary" title={lastAt.toLocaleString()}>
|
||||||
|
Last: {lastAt.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
{isTicking && (
|
||||||
|
<span className="agent-heartbeat-next text-secondary" title={nextAt.toLocaleString()}>
|
||||||
|
Next: {nextAt.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1006,32 +985,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
>
|
>
|
||||||
<Pause size={14} /> Pause
|
<Pause size={14} /> Pause
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} /> Stop
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{agent.state === "paused" && (
|
{agent.state === "paused" && (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleStateChange(agent.id, "active")}
|
||||||
onClick={() => void handleStateChange(agent.id, "active")}
|
title="Resume"
|
||||||
title="Resume"
|
>
|
||||||
>
|
<Play size={14} /> Resume
|
||||||
<Play size={14} /> Resume
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} /> Stop
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{agent.state === "running" && (
|
{agent.state === "running" && (
|
||||||
<>
|
<>
|
||||||
@@ -1050,32 +1013,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
|||||||
>
|
>
|
||||||
<Pause size={14} /> Pause
|
<Pause size={14} /> Pause
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} /> Stop
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{agent.state === "error" && (
|
{agent.state === "error" && (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn btn--sm"
|
||||||
className="btn btn--sm"
|
onClick={() => void handleStateChange(agent.id, "active")}
|
||||||
onClick={() => void handleStateChange(agent.id, "active")}
|
title="Retry"
|
||||||
title="Retry"
|
>
|
||||||
>
|
<Play size={14} /> Retry
|
||||||
<Play size={14} /> Retry
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn--sm btn--danger"
|
|
||||||
onClick={() => void handleStateChange(agent.id, "terminated")}
|
|
||||||
title="Stop"
|
|
||||||
>
|
|
||||||
<Square size={14} /> Stop
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{agent.state === "terminated" && (
|
{agent.state === "terminated" && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user