Skip to content

Commit 0f35a6c

Browse files
committed
feat(FR-1423): add scheduler page and pending session list (#4214)
resolves #4213 ([FR-1423](https://lablup.atlassian.net/browse/FR-1423)) ### Add Scheduler Page for Pending Sessions This PR adds a new Scheduler page that displays pending sessions in a queue. The feature is available for administrators when the backend supports the `pending-session-list` capability (introduced in version 25.13.0). Key changes: - Added a new Scheduler menu item in the admin section of the sidebar - Created a new `SchedulerPage` component with a tab for pending sessions - Implemented `PendingSessionNodeList` component to display sessions waiting in the queue - Added queue position information to session nodes - Added row numbering to the BAITable component - Updated translations for all languages to include the new Scheduler terms - Added feature detection for the pending-session-list capability The Scheduler page allows administrators to monitor and manage sessions that are waiting to be scheduled, providing better visibility into the system's workload. **Checklist:** - [x] Documentation - [x] Minium required manager version: 25.13.0 - [x] Specific setting for review: Backend must support the 'pending-session-list' capability - [x] Minimum requirements to check during review: Verify the Scheduler page appears in admin menu and displays pending sessions correctly - [x] Test case(s) to demonstrate the difference of before/after: Create multiple sessions to see them appear in the pending queue [FR-1423]: https://lablup.atlassian.net/browse/FR-1423?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 64618ef commit 0f35a6c

34 files changed

+1249
-131
lines changed

data/schema.graphql

Lines changed: 550 additions & 53 deletions
Large diffs are not rendered by default.

pnpm-lock.yaml

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

react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"jotai-effect": "^2.0.2",
4444
"katex": "^0.16.21",
4545
"lodash": "^4.17.21",
46-
"lucide-react": "^0.484.0",
46+
"lucide-react": "^0.542.0",
4747
"markdown-to-jsx": "^7.7.4",
4848
"marked": "^12.0.2",
4949
"prettier": "^3.5.3",

react/src/App.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const ChatPage = React.lazy(() => import('./pages/ChatPage'));
8282

8383
const AIAgentPage = React.lazy(() => import('./pages/AIAgentPage'));
8484

85+
const SchedulerPage = React.lazy(() => import('./pages/SchedulerPage'));
86+
8587
interface CustomHandle {
8688
title?: string;
8789
labelKey?: string;
@@ -426,6 +428,23 @@ const router = createBrowserRouter([
426428
handle: { labelKey: 'webui.menu.Environments' },
427429
Component: EnvironmentPage,
428430
},
431+
{
432+
path: '/scheduler',
433+
handle: { labelKey: 'webui.menu.Scheduler' },
434+
Component: () => {
435+
const baiClient = useSuspendedBackendaiClient();
436+
return baiClient?.supports('pending-session-list') ? (
437+
<BAIErrorBoundary>
438+
<Suspense fallback={<Skeleton active />}>
439+
<SchedulerPage />
440+
<SessionDetailAndContainerLogOpenerLegacy />
441+
</Suspense>
442+
</BAIErrorBoundary>
443+
) : (
444+
<WebUINavigate to={'/error'} replace />
445+
);
446+
},
447+
},
429448
{
430449
path: '/agent',
431450
handle: { labelKey: 'webui.menu.Resources' },

react/src/components/ComputeSessionNodeItems/ConnectedKernelList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ const ConnectedKernelList: React.FC<ConnectedKernelListProps> = ({
133133
{
134134
title: t('kernel.AgentId'),
135135
dataIndex: 'agent_id',
136-
render: (id) => <Typography.Text copyable>{id}</Typography.Text>,
136+
render: (id) =>
137+
_.isEmpty(id) ? '-' : <Typography.Text copyable>{id}</Typography.Text>,
137138
},
138139
{
139140
title: t('kernel.KernelId'),

react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Tag, Tooltip, theme } from 'antd';
88
import { BAIFlex } from 'backend.ai-ui';
99
import _ from 'lodash';
1010
import React from 'react';
11+
import { useTranslation } from 'react-i18next';
1112
import { graphql, useFragment } from 'react-relay';
1213

1314
interface SessionStatusTagProps {
@@ -46,6 +47,7 @@ const SessionStatusTag: React.FC<SessionStatusTagProps> = ({
4647
showInfo,
4748
}) => {
4849
const { token } = theme.useToken();
50+
const { t } = useTranslation();
4951

5052
const session = useFragment(
5153
graphql`
@@ -54,29 +56,46 @@ const SessionStatusTag: React.FC<SessionStatusTagProps> = ({
5456
status
5557
status_info
5658
status_data
59+
queue_position @since(version: "25.12.0")
5760
}
5861
`,
5962
sessionFrgmt,
6063
);
6164

65+
const displayQuePosition = _.isNumber(session?.queue_position)
66+
? session?.queue_position + 1
67+
: undefined;
6268
return session ? (
6369
_.isEmpty(session.status_info) || !showInfo ? (
64-
<Tooltip title={session.status_info}>
65-
<Tag
66-
color={
67-
session.status ? _.get(statusTagColor, session.status) : undefined
68-
}
69-
icon={isTransitional(session) ? <LoadingOutlined spin /> : undefined}
70-
// Comment out to match the legacy tag style temporarily
71-
style={{
72-
borderRadius: 11,
73-
paddingLeft: token.paddingSM,
74-
paddingRight: token.paddingSM,
75-
}}
76-
>
77-
{session.status || ' '}
78-
</Tag>
79-
</Tooltip>
70+
<BAIFlex wrap="nowrap">
71+
<Tooltip title={session.status_info}>
72+
<Tag
73+
color={
74+
session.status ? _.get(statusTagColor, session.status) : undefined
75+
}
76+
icon={
77+
isTransitional(session) ? <LoadingOutlined spin /> : undefined
78+
}
79+
// Comment out to match the legacy tag style temporarily
80+
style={{
81+
borderRadius: 11,
82+
paddingLeft: token.paddingSM,
83+
paddingRight: token.paddingSM,
84+
}}
85+
>
86+
{session.status || ' '}
87+
</Tag>
88+
</Tooltip>
89+
{displayQuePosition ? (
90+
<Tooltip title={t('session.PendingPosition')}>
91+
<Tag
92+
style={{
93+
borderRadius: 11,
94+
}}
95+
>{`#${displayQuePosition}`}</Tag>
96+
</Tooltip>
97+
) : null}
98+
</BAIFlex>
8099
) : (
81100
<BAIFlex>
82101
<Tag

react/src/components/MainLayout/WebUISider.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ import {
4949
BAIFlex,
5050
} from 'backend.ai-ui';
5151
import _ from 'lodash';
52-
import { BotMessageSquare, ExternalLinkIcon, LinkIcon } from 'lucide-react';
52+
import {
53+
BotMessageSquare,
54+
ExternalLinkIcon,
55+
LinkIcon,
56+
ClipboardClock,
57+
} from 'lucide-react';
5358
import React, { ReactNode, useContext, useRef } from 'react';
5459
import { useTranslation } from 'react-i18next';
5560
import { useLocation } from 'react-router-dom';
@@ -96,6 +101,7 @@ export type MenuKeys =
96101
// adminMenu keys
97102
| 'credential'
98103
| 'environment'
104+
| 'scheduler'
99105
| 'resource-policy'
100106
// superAdminMenu keys
101107
| 'agent'
@@ -252,7 +258,7 @@ const WebUISider: React.FC<WebUISiderProps> = (props) => {
252258
},
253259
]);
254260

255-
const adminMenu: MenuProps['items'] = [
261+
const adminMenu: MenuProps['items'] = filterOutEmpty([
256262
{
257263
label: <WebUILink to="/credential">{t('webui.menu.Users')}</WebUILink>,
258264
icon: <UserOutlined style={{ color: token.colorInfo }} />,
@@ -265,6 +271,11 @@ const WebUISider: React.FC<WebUISiderProps> = (props) => {
265271
icon: <FileDoneOutlined style={{ color: token.colorInfo }} />,
266272
key: 'environment',
267273
},
274+
baiClient?.supports('pending-session-list') && {
275+
label: <WebUILink to="/scheduler">{t('webui.menu.Scheduler')}</WebUILink>,
276+
icon: <ClipboardClock style={{ color: token.colorInfo }} />,
277+
key: 'scheduler',
278+
},
268279
{
269280
label: (
270281
<WebUILink to="/resource-policy">
@@ -274,7 +285,7 @@ const WebUISider: React.FC<WebUISiderProps> = (props) => {
274285
icon: <SolutionOutlined style={{ color: token.colorInfo }} />,
275286
key: 'resource-policy',
276287
},
277-
];
288+
]);
278289

279290
const superAdminMenu: MenuProps['items'] = [
280291
{
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import BAIFetchKeyButton from './BAIFetchKeyButton';
2+
import ResourceGroupSelectForCurrentProject from './ResourceGroupSelectForCurrentProject';
3+
import SessionNodes from './SessionNodes';
4+
import { Form } from 'antd';
5+
import { BAIFlex, filterOutNullAndUndefined } from 'backend.ai-ui';
6+
import _ from 'lodash';
7+
import { useDeferredValue, useMemo, useState } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { graphql, useLazyLoadQuery } from 'react-relay';
10+
import { useLocation } from 'react-router-dom';
11+
import {
12+
PendingSessionNodeListQuery,
13+
PendingSessionNodeListQuery$variables,
14+
} from 'src/__generated__/PendingSessionNodeListQuery.graphql';
15+
import { INITIAL_FETCH_KEY, useFetchKey, useWebUINavigate } from 'src/hooks';
16+
import { useBAIPaginationOptionStateOnSearchParam } from 'src/hooks/reactPaginationQueryOptions';
17+
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
18+
19+
const PendingSessionNodeList: React.FC = () => {
20+
const { t } = useTranslation();
21+
const [fetchKey, updateFetchKey] = useFetchKey();
22+
const [selectedResourceGroup, setSelectedResourceGroup] = useState<string>();
23+
const deferredFetchKey = useDeferredValue(fetchKey);
24+
const deferredSelectedResourceGroup = useDeferredValue(selectedResourceGroup);
25+
26+
const [columnOverrides, setColumnOverrides] = useBAISettingUserState(
27+
'table_column_overrides.PendingSessionNodeList',
28+
);
29+
30+
const webUINavigate = useWebUINavigate();
31+
const location = useLocation();
32+
33+
const {
34+
baiPaginationOption,
35+
tablePaginationOption,
36+
setTablePaginationOption,
37+
} = useBAIPaginationOptionStateOnSearchParam({
38+
current: 1,
39+
pageSize: 10,
40+
});
41+
42+
const queryVariables: PendingSessionNodeListQuery$variables = useMemo(
43+
() => ({
44+
resource_group_id: deferredSelectedResourceGroup || 'default',
45+
first: baiPaginationOption.first,
46+
offset: baiPaginationOption.offset,
47+
}),
48+
[deferredSelectedResourceGroup, baiPaginationOption],
49+
);
50+
const deferredQueryVariables = useDeferredValue(queryVariables);
51+
52+
const { session_pending_queue } =
53+
useLazyLoadQuery<PendingSessionNodeListQuery>(
54+
graphql`
55+
query PendingSessionNodeListQuery(
56+
$resource_group_id: String!
57+
$first: Int = 20
58+
$offset: Int = 0
59+
) {
60+
session_pending_queue(
61+
resource_group_id: $resource_group_id
62+
first: $first
63+
offset: $offset
64+
) {
65+
edges @required(action: THROW) {
66+
node {
67+
...SessionDetailDrawerFragment
68+
...SessionNodesFragment
69+
}
70+
}
71+
count
72+
}
73+
}
74+
`,
75+
deferredQueryVariables,
76+
{
77+
fetchKey:
78+
deferredFetchKey === INITIAL_FETCH_KEY ? undefined : deferredFetchKey,
79+
fetchPolicy:
80+
deferredFetchKey === INITIAL_FETCH_KEY
81+
? 'store-and-network'
82+
: 'network-only',
83+
},
84+
);
85+
86+
return (
87+
<BAIFlex direction="column" align="stretch" gap="sm">
88+
<BAIFlex align="stretch" justify="between">
89+
<Form.Item
90+
label={t('session.ResourceGroup')}
91+
style={{ marginBottom: 0 }}
92+
>
93+
<ResourceGroupSelectForCurrentProject
94+
showSearch
95+
style={{ minWidth: 100 }}
96+
onChange={(v) => {
97+
setSelectedResourceGroup(v);
98+
setTablePaginationOption({ current: 1 });
99+
}}
100+
loading={selectedResourceGroup !== deferredSelectedResourceGroup}
101+
popupMatchSelectWidth={false}
102+
tooltip={t('general.ResourceGroup')}
103+
/>
104+
</Form.Item>
105+
<BAIFetchKeyButton
106+
loading={
107+
deferredQueryVariables !== queryVariables ||
108+
deferredFetchKey !== fetchKey
109+
}
110+
autoUpdateDelay={7_000}
111+
value={fetchKey}
112+
onChange={(newFetchKey) => {
113+
updateFetchKey(newFetchKey);
114+
}}
115+
/>
116+
</BAIFlex>
117+
118+
<SessionNodes
119+
disableSorter
120+
onClickSessionName={(session) => {
121+
// Set sessionDetailDrawerFrgmt in location state via webUINavigate
122+
// instead of directly setting sessionDetailId query param
123+
// to avoid additional fetch in SessionDetailDrawer
124+
const newSearchParams = new URLSearchParams(location.search);
125+
newSearchParams.set('sessionDetail', session.row_id);
126+
webUINavigate(
127+
{
128+
pathname: location.pathname,
129+
hash: location.hash,
130+
search: newSearchParams.toString(),
131+
},
132+
{
133+
state: {
134+
sessionDetailDrawerFrgmt: session,
135+
createdAt: new Date().toISOString(),
136+
},
137+
},
138+
);
139+
}}
140+
loading={deferredQueryVariables !== queryVariables}
141+
sessionsFrgmt={filterOutNullAndUndefined(
142+
session_pending_queue?.edges.map((e) => e?.node),
143+
)}
144+
pagination={{
145+
pageSize: tablePaginationOption.pageSize,
146+
current: tablePaginationOption.current,
147+
total: session_pending_queue?.count ?? 0,
148+
onChange: (current, pageSize) => {
149+
if (_.isNumber(current) && _.isNumber(pageSize)) {
150+
setTablePaginationOption({ current, pageSize });
151+
}
152+
},
153+
}}
154+
tableSettings={{
155+
columnOverrides: columnOverrides,
156+
onColumnOverridesChange: setColumnOverrides,
157+
}}
158+
/>
159+
</BAIFlex>
160+
);
161+
};
162+
163+
export default PendingSessionNodeList;

react/src/helper/graphql-transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function manipulateGraphQLQueryWithClientDirectives(
1212
// Optionally normalize fragment type conditions from Query to Queries
1313
let newAst = ast;
1414
// Since the super graph, the query type has been changed from Queries to Query
15-
const shouldConvertFragmentTypeToQueries = isNotCompatibleWith('25.14.0');
15+
const shouldConvertFragmentTypeToQueries = isNotCompatibleWith('25.13.0');
1616
if (shouldConvertFragmentTypeToQueries) {
1717
function normalizeFragmentTypeCondition(node: any) {
1818
if (!node.typeCondition) return;

0 commit comments

Comments
 (0)