Skip to content

Commit 60cc64d

Browse files
committed
feat(FR-3230): add agent watcher start/stop/restart actions to AgentDetailDrawer (#8087)
Resolves #8085 (FR-3230) ## Summary Migrates the agent node lifecycle controls (start / stop / restart) from the Control Panel into the WebUI. Superadmins can now start, stop, and restart an agent node directly from the **Agent detail drawer**, next to the existing Settings action. ## Test server A superadmin login is required for both. - **Normal flow** (start / stop / restart on a healthy watcher): use the `10.82.0.130` test server (manager API on `:8090`). - **Error handling** (watcher down, agent not loaded with systemctl, etc.): use the `10.82.0.151` test server (manager API on `:8090`). ## Changes - **`AgentLifeCycleControlModal.tsx`** (new): a single modal that owns the start / stop / restart confirmation and request. It takes `open` + a `lifeCycleType` (`'start' | 'stop' | 'restart'`) and branches the title / success message internally; the body shows a shared warning plus the target agent's name in a surface box (borrowed from `BAIDeleteConfirmModal`). The three watcher calls run through `useTanMutation`. - **`AgentActionButtons.tsx`**: renders Settings + Restart / Start / Stop trigger buttons in the agent detail drawer. Each watcher button sets a single `lifeCycleType` state that drives the modal (`open={!!lifeCycleType}`). Status-aware disabling: Start disabled when the agent is `ALIVE`, Stop disabled otherwise; Restart always available. Icons/colors follow `SessionActionButtons` — Stop uses `BAITerminateIcon` + `colorError`, Restart uses the `RefreshCw` (lucide) icon. - **`backend.ai-client` (`client.ts`)**: added `start_watcher_agent` / `stop_watcher_agent` / `restart_watcher_agent` SDK methods (with matching `BackendAIClient` type signatures) so the watcher REST calls are encapsulated and reusable. - **`AgentResources.tsx`** (drawer crash guard): a stopped agent's `live_stat` no longer contains `node.mem`, and three call sites accessed `parsedLiveStat?.node?.mem.capacity` without optional chaining after `mem`, throwing a `TypeError` that crashed the whole drawer. Added the missing `?.` at all three sites, and the **Utilization** section now renders `-` when `live_stat.node` is empty instead of misleading 0% / NaN values. - **`AgentDetailDrawerContent.tsx`**: wrapped `<AgentResources>` with `ErrorBoundaryWithNullFallback` so a stat-parsing failure can never take down the entire drawer again. - **i18n**: added `agent.Watcher*` keys (button labels, success toasts, a single action warning) across all 21 locales. ## API Uses the manager REST watcher endpoints via the new client SDK methods. These were chosen over the legacy `/api/agents/watcher_agent_*` paths (removed on current backend.ai `main`): - `POST /resource/watcher/agent/start` - `POST /resource/watcher/agent/stop` - `POST /resource/watcher/agent/restart` Body: `{ agent_id }` (the agent's `row_id`). ## Error handling The manager proxies these to the agent's watcher daemon, so several non-trivial outcomes are handled explicitly in the modal: - **200 + `{ result: "ok" }`** → success toast. - **200 + a body message** (e.g. `"Agent is not loaded with systemctl."`) → surfaced as a warning. HTTP 200 does **not** guarantee success here. The client also hands back a plain string for `text/*` responses, which is handled too. - **403 (non-superadmin)** → the component is only rendered on superadmin-only pages and the endpoints are superadmin-only on the manager. - **400 (invalid params) / 403 (watcher token mismatch) / 500 (agent not found, watcher unreachable, timeout, etc.)** → error toast resolved through `getErrorMessage`, logged via `useBAILogger`. A missing `row_id` short-circuits before the request. ## Post-action data refresh - `AgentLifeCycleControlModalFragment` is now `@refetchable` (`id`, `status`, `status_changed`). After a successful watcher request the modal refetches it with `network-only`, so every component reading the same `AgentNode` record (table row, detail drawer) picks up the new status automatically via the normalized store. - The refetch runs inside `startTransition` — without it the suspend during refetch collapsed the drawer's Suspense boundary to its skeleton fallback. - `AgentList`'s manual reload button is replaced with `BAIFetchKeyButton` (`autoUpdateDelay` 15s, matching `StorageProxyList`). Node-level refetch cannot change connection membership/count (e.g. a stopped agent leaving the ALIVE tab), and the watcher actions are async on the backend, so the periodic list refresh is what makes the list itself converge. ## Verification `bash scripts/verify.sh` → **Relay / Lint / Format / TypeScript all PASS**. (The single terminology warning is a pre-existing key, `error.UserHasNoGroup`, unrelated to this change.) ## Screenshots _TODO: capture from the `10.82.0.130` test server with a superadmin login._
1 parent 488c36c commit 60cc64d

33 files changed

Lines changed: 880 additions & 218 deletions

packages/backend.ai-client/src/client.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,51 @@ export class Client {
12811281
return this._wrapWithPromise(rqst);
12821282
}
12831283

1284+
/**
1285+
* Start the agent's watcher daemon. (superadmin only)
1286+
*
1287+
* @param {string} agentId - the agent id (row id) to control
1288+
*/
1289+
async start_watcher_agent(agentId: string): Promise<any> {
1290+
const rqst = this.newSignedRequest(
1291+
'POST',
1292+
'/resource/watcher/agent/start',
1293+
{ agent_id: agentId },
1294+
null,
1295+
);
1296+
return this._wrapWithPromise(rqst);
1297+
}
1298+
1299+
/**
1300+
* Stop the agent's watcher daemon. (superadmin only)
1301+
*
1302+
* @param {string} agentId - the agent id (row id) to control
1303+
*/
1304+
async stop_watcher_agent(agentId: string): Promise<any> {
1305+
const rqst = this.newSignedRequest(
1306+
'POST',
1307+
'/resource/watcher/agent/stop',
1308+
{ agent_id: agentId },
1309+
null,
1310+
);
1311+
return this._wrapWithPromise(rqst);
1312+
}
1313+
1314+
/**
1315+
* Restart the agent's watcher daemon. (superadmin only)
1316+
*
1317+
* @param {string} agentId - the agent id (row id) to control
1318+
*/
1319+
async restart_watcher_agent(agentId: string): Promise<any> {
1320+
const rqst = this.newSignedRequest(
1321+
'POST',
1322+
'/resource/watcher/agent/restart',
1323+
{ agent_id: agentId },
1324+
null,
1325+
);
1326+
return this._wrapWithPromise(rqst);
1327+
}
1328+
12841329
/**
12851330
* Create a `compute session` if the session for the given sessionId does not exist.
12861331
* It returns the information for the existing session otherwise, without error.

react/src/__generated__/AgentActionButtonsFragment.graphql.ts

Lines changed: 16 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/__generated__/AgentDetailDrawerRefetchQuery.graphql.ts

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/__generated__/AgentLifeCycleControlModalFragment.graphql.ts

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/__generated__/AgentLifeCycleControlModalRefetchQuery.graphql.ts

Lines changed: 135 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/__generated__/AgentListQuery.graphql.ts

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/src/components/AgentDetailDrawerContent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import AgentActionButtons from './AgentNodeItems/AgentActionButtons';
88
import AgentComputePlugins from './AgentNodeItems/AgentComputePlugins';
99
import AgentResources from './AgentNodeItems/AgentResources';
1010
import AgentStatusTag from './AgentNodeItems/AgentStatusTag';
11+
import BAIErrorBoundary from './BAIErrorBoundary';
1112
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
1213
import { Descriptions, Grid, Tabs, theme, Typography } from 'antd';
1314
import {
@@ -150,7 +151,11 @@ const AgentDetailDrawerContent: React.FC<AgentDetailDrawerContentProps> = ({
150151
{
151152
key: 'resources',
152153
label: t('agent.Resources'),
153-
children: <AgentResources agentNodeFrgmt={agent} />,
154+
children: (
155+
<BAIErrorBoundary>
156+
<AgentResources agentNodeFrgmt={agent} />
157+
</BAIErrorBoundary>
158+
),
154159
},
155160
]}
156161
/>

0 commit comments

Comments
 (0)