Skip to content

Commit f3e361b

Browse files
committed
feat(FR-1240): add agent statistics and resource monitoring capabilities (#4344)
Resolves #3951 ([FR-1240](https://lablup.atlassian.net/browse/FR-1240)) # Add Agent Statistics to Admin Dashboard This PR adds a new Agent Statistics feature to the Admin Dashboard, providing system administrators with a comprehensive view of resource utilization across all agents in the system. The new component displays total used and free resources for CPU, memory, and accelerators. Key changes: - Added new `AgentStats` component that shows system-wide resource usage - Updated GraphQL schema with new `agentStats` query and related types - Refactored resource statistics components to use consistent terminology (`used`/`free` instead of `using`/`remaining`) - Updated all resource panel descriptions in i18n files to reflect the new terminology - Added feature detection for `agent-stats` capability (requires manager version 25.15.0+) The PR also includes: - Schema updates for artifact registry management and delegation features - Renamed storage namespace-related mutations for better clarity - Fixed z-index issues in the main layout components ![image.png](https://app.graphite.dev/user-attachments/assets/c4633512-6ac0-4db2-977f-cc8e03220b86.png) ![image.png](https://app.graphite.dev/user-attachments/assets/7186c053-193f-4baf-835c-03e995ceb563.png) ## Checklist: - [x] Updated i18n strings in all supported languages - [x] Added feature detection for backward compatibility - [x] Minimum required manager version: 25.15.0 (for agent-stats feature) - [x] Specific setting for review: Admin access required to view agent statistics [FR-1240]: https://lablup.atlassian.net/browse/FR-1240?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 7119a62 commit f3e361b

34 files changed

+872
-314
lines changed

data/schema.graphql

Lines changed: 280 additions & 49 deletions
Large diffs are not rendered by default.

packages/backend.ai-ui/src/components/ResourceStatistics.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,26 @@ import { useTranslation } from 'react-i18next';
88

99
interface ResourceData {
1010
cpu: {
11-
using: { current: number; total?: number };
12-
remaining: { current: number; total?: number };
11+
used: { current: number; total?: number };
12+
free: { current: number; total?: number };
1313
metadata: { title: string; displayUnit: string };
1414
} | null;
1515
memory: {
16-
using: { current: number; total?: number };
17-
remaining: { current: number; total?: number };
16+
used: { current: number; total?: number };
17+
free: { current: number; total?: number };
1818
metadata: { title: string; displayUnit: string };
1919
} | null;
2020
accelerators: Array<{
2121
key: string;
22-
using: { current: number; total?: number };
23-
remaining: { current: number; total?: number };
22+
used: { current: number; total?: number };
23+
free: { current: number; total?: number };
2424
metadata: { title: string; displayUnit: string };
2525
}>;
2626
}
2727

2828
interface ResourceStatisticsProps {
2929
resourceData: ResourceData;
30-
displayType: 'using' | 'remaining';
30+
displayType: 'used' | 'free';
3131
showProgress?: boolean;
3232
precision?: number;
3333
progressSteps?: number;

packages/backend.ai-ui/src/components/fragments/BAIBucketSelect.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const BAIBucketSelect = ({
6666
edges {
6767
node {
6868
id
69-
bucket
69+
namespace
7070
}
7171
}
7272
}
@@ -95,7 +95,7 @@ const BAIBucketSelect = ({
9595
);
9696

9797
const selectedOptions = _.map(paginationData, (item) => ({
98-
label: item.node.bucket,
98+
label: item.node.namespace,
9999
value: item.node.id,
100100
}));
101101

react/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import MainLayout from './components/MainLayout/MainLayout';
1010
import WebUINavigate from './components/WebUINavigate';
1111
import { useSuspendedBackendaiClient } from './hooks';
1212
import { useBAISettingUserState } from './hooks/useBAISetting';
13-
import AdminDashboardPage from './pages/AdminDashboardPage';
1413
// High priority to import the component
1514
import ComputeSessionListPage from './pages/ComputeSessionListPage';
1615
import ModelStoreListPage from './pages/ModelStoreListPage';
@@ -35,6 +34,9 @@ const EndpointDetailPage = React.lazy(
3534
);
3635
const StartPage = React.lazy(() => import('./pages/StartPage'));
3736
const DashboardPage = React.lazy(() => import('./pages/DashboardPage'));
37+
const AdminDashboardPage = React.lazy(
38+
() => import('./pages/AdminDashboardPage'),
39+
);
3840
const EnvironmentPage = React.lazy(() => import('./pages/EnvironmentPage'));
3941
const MyEnvironmentPage = React.lazy(() => import('./pages/MyEnvironmentPage'));
4042
const StorageHostSettingPage = React.lazy(
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { useResourceSlotsDetails } from '../hooks/backendai';
2+
import BAIFetchKeyButton from './BAIFetchKeyButton';
3+
import { useControllableValue } from 'ahooks';
4+
import { Segmented, Skeleton, theme, Typography } from 'antd';
5+
import {
6+
BAIFlex,
7+
BAIBoardItemTitle,
8+
ResourceStatistics,
9+
convertToNumber,
10+
processMemoryValue,
11+
BAIFlexProps,
12+
} from 'backend.ai-ui';
13+
import _ from 'lodash';
14+
import { useTransition, ReactNode } from 'react';
15+
import { useTranslation } from 'react-i18next';
16+
import { graphql, useRefetchableFragment } from 'react-relay';
17+
import { AgentStatsFragment$key } from 'src/__generated__/AgentStatsFragment.graphql';
18+
19+
interface AgentStatsProps extends BAIFlexProps {
20+
queryRef: AgentStatsFragment$key;
21+
isRefetching?: boolean;
22+
displayType?: 'used' | 'free';
23+
onDisplayTypeChange?: (type: 'used' | 'free') => void;
24+
extra?: ReactNode;
25+
}
26+
27+
const AgentStats: React.FC<AgentStatsProps> = ({
28+
queryRef,
29+
isRefetching,
30+
extra,
31+
...props
32+
}) => {
33+
const { t } = useTranslation();
34+
const { token } = theme.useToken();
35+
36+
const [isPendingRefetch, startRefetchTransition] = useTransition();
37+
38+
const [displayType, setDisplayType] = useControllableValue<
39+
Exclude<AgentStatsProps['displayType'], undefined>
40+
>(props, {
41+
defaultValue: 'used',
42+
trigger: 'onDisplayTypeChange',
43+
defaultValuePropName: 'defaultDisplayType',
44+
});
45+
46+
const [data, refetch] = useRefetchableFragment(
47+
graphql`
48+
fragment AgentStatsFragment on Query
49+
@refetchable(queryName: "AgentStatsRefetchQuery") {
50+
agentStats @since(version: "25.15.0") {
51+
totalResource {
52+
free
53+
used
54+
capacity
55+
}
56+
}
57+
}
58+
`,
59+
queryRef,
60+
);
61+
62+
const resourceSlotsDetails = useResourceSlotsDetails();
63+
64+
const agentStatsData = (() => {
65+
const totalResource = data.agentStats.totalResource;
66+
if (!totalResource) {
67+
return { cpu: null, memory: null, accelerators: [] };
68+
}
69+
70+
const free = totalResource.free as Record<string, number>;
71+
const used = totalResource.used as Record<string, number>;
72+
const capacity = totalResource.capacity as Record<string, number>;
73+
74+
const cpuSlot = resourceSlotsDetails?.resourceSlotsInRG?.['cpu'];
75+
const memSlot = resourceSlotsDetails?.resourceSlotsInRG?.['mem'];
76+
77+
const cpuData = cpuSlot
78+
? {
79+
used: {
80+
current: convertToNumber(used['cpu'] || 0),
81+
total: convertToNumber(capacity['cpu'] || 0),
82+
},
83+
free: {
84+
current: convertToNumber(free['cpu'] || 0),
85+
total: convertToNumber(capacity['cpu'] || 0),
86+
},
87+
metadata: {
88+
title: cpuSlot.human_readable_name,
89+
displayUnit: cpuSlot.display_unit,
90+
},
91+
}
92+
: null;
93+
94+
const memoryData = memSlot
95+
? {
96+
used: {
97+
current: processMemoryValue(used['mem'] || 0, memSlot.display_unit),
98+
total: processMemoryValue(
99+
capacity['mem'] || 0,
100+
memSlot.display_unit,
101+
),
102+
},
103+
free: {
104+
current: processMemoryValue(free['mem'] || 0, memSlot.display_unit),
105+
total: processMemoryValue(
106+
capacity['mem'] || 0,
107+
memSlot.display_unit,
108+
),
109+
},
110+
metadata: {
111+
title: memSlot.human_readable_name,
112+
displayUnit: memSlot.display_unit,
113+
},
114+
}
115+
: null;
116+
117+
const accelerators = _.chain(resourceSlotsDetails?.resourceSlotsInRG)
118+
.omit(['cpu', 'mem'])
119+
.map((resourceSlot, key) => {
120+
if (!resourceSlot) return null;
121+
122+
const freeValue = free[key] || 0;
123+
const usedValue = used[key] || 0;
124+
const capacityValue = capacity[key] || 0;
125+
126+
return {
127+
key,
128+
used: {
129+
current: convertToNumber(usedValue),
130+
total: convertToNumber(capacityValue),
131+
},
132+
free: {
133+
current: convertToNumber(freeValue),
134+
total: convertToNumber(capacityValue),
135+
},
136+
metadata: {
137+
title: resourceSlot.human_readable_name,
138+
displayUnit: resourceSlot.display_unit,
139+
},
140+
};
141+
})
142+
.compact()
143+
.filter((item) => !!(item.used.current || item.used.total))
144+
.value();
145+
146+
return { cpu: cpuData, memory: memoryData, accelerators };
147+
})();
148+
149+
return (
150+
<BAIFlex
151+
direction="column"
152+
align="stretch"
153+
style={{
154+
paddingInline: token.paddingXL,
155+
paddingBottom: token.padding,
156+
...props.style,
157+
}}
158+
{..._.omit(props, ['style'])}
159+
>
160+
<BAIBoardItemTitle
161+
title={
162+
<Typography.Text
163+
style={{
164+
fontSize: token.fontSizeHeading5,
165+
fontWeight: token.fontWeightStrong,
166+
}}
167+
>
168+
{t('agentStats.AgentStats')}
169+
</Typography.Text>
170+
}
171+
tooltip={t('agentStats.AgentStatsDescription')}
172+
extra={
173+
<BAIFlex gap={'xs'} wrap="wrap">
174+
<Segmented<Exclude<AgentStatsProps['displayType'], undefined>>
175+
size="small"
176+
options={[
177+
{
178+
label: t('dashboard.Used'),
179+
value: 'used',
180+
},
181+
{
182+
value: 'free',
183+
label: t('dashboard.Free'),
184+
},
185+
]}
186+
value={displayType}
187+
onChange={(v) => setDisplayType(v)}
188+
/>
189+
<BAIFetchKeyButton
190+
size="small"
191+
loading={isPendingRefetch || isRefetching}
192+
value=""
193+
onChange={() => {
194+
startRefetchTransition(() => {
195+
refetch(
196+
{},
197+
{
198+
fetchPolicy: 'network-only',
199+
},
200+
);
201+
});
202+
}}
203+
type="text"
204+
style={{
205+
backgroundColor: 'transparent',
206+
}}
207+
/>
208+
{extra}
209+
</BAIFlex>
210+
}
211+
/>
212+
{resourceSlotsDetails.isLoading ? (
213+
<Skeleton active />
214+
) : (
215+
<ResourceStatistics
216+
resourceData={agentStatsData}
217+
displayType={displayType === 'used' ? 'used' : 'free'}
218+
showProgress={true}
219+
/>
220+
)}
221+
</BAIFlex>
222+
);
223+
};
224+
225+
export default AgentStats;

react/src/components/ConfigurableResourceCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ const ConfigurableResourceCard: React.FC<ConfigurableResourceCardProps> = ({
138138
titleStyle: {
139139
paddingLeft: 0,
140140
},
141-
..._.omit(props, ['style']),
141+
..._.omit(props, ['style', 'title']),
142142
};
143143

144144
switch (currentPanelType) {

react/src/components/MainLayout/WebUISider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export type MenuKeys =
106106
| 'resource-policy'
107107
| 'reservoir'
108108
// superAdminMenu keys
109+
| 'admin-dashboard'
109110
| 'agent'
110111
| 'settings'
111112
| 'maintenance'
@@ -307,7 +308,7 @@ const WebUISider: React.FC<WebUISiderProps> = (props) => {
307308
},
308309
]);
309310

310-
const superAdminMenu: MenuProps['items'] = [
311+
const superAdminMenu: MenuProps['items'] = filterOutEmpty([
311312
{
312313
label: <WebUILink to="/agent">{t('webui.menu.Resources')}</WebUILink>,
313314
icon: <HddOutlined style={{ color: token.colorInfo }} />,
@@ -334,7 +335,7 @@ const WebUISider: React.FC<WebUISiderProps> = (props) => {
334335
icon: <InfoCircleOutlined style={{ color: token.colorInfo }} />,
335336
key: 'information',
336337
},
337-
];
338+
]);
338339

339340
const pluginMap: Record<string, MenuProps['items']> = {
340341
'menuitem-user': generalMenu,

0 commit comments

Comments
 (0)