Skip to content

Commit c32bf01

Browse files
committed
feat(FR-2672): add AdminDeploymentListPage with extended filters gated by feature flag
1 parent c35aa63 commit c32bf01

1 file changed

Lines changed: 194 additions & 6 deletions

File tree

react/src/pages/AdminDeploymentListPage.tsx

Lines changed: 194 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,203 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5-
import React from 'react';
5+
import type {
6+
AdminDeploymentListPageQuery,
7+
DeploymentFilter,
8+
DeploymentOrderBy,
9+
DeploymentOrderField,
10+
OrderDirection,
11+
} from '../__generated__/AdminDeploymentListPageQuery.graphql';
12+
import DeploymentList, {
13+
type DeploymentSort,
14+
} from '../components/DeploymentList';
15+
import { useWebUINavigate } from '../hooks';
16+
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
17+
import { Skeleton } from 'antd';
18+
import {
19+
BAICard,
20+
BAIFetchKeyButton,
21+
BAIFlex,
22+
INITIAL_FETCH_KEY,
23+
toLocalId,
24+
useFetchKey,
25+
} from 'backend.ai-ui';
26+
import { parseAsString, useQueryStates } from 'nuqs';
27+
import React, { Suspense, useDeferredValue } from 'react';
28+
import { useTranslation } from 'react-i18next';
29+
import { graphql, useLazyLoadQuery } from 'react-relay';
30+
31+
/**
32+
* Encode a `DeploymentSort` into a compact URL-friendly string (e.g.
33+
* `CREATED_AT:DESC`). Empty / undefined sorts are serialized as an empty
34+
* string so `nuqs` drops the param from the URL.
35+
*/
36+
const encodeSort = (sort: DeploymentSort | undefined): string => {
37+
if (!sort) return '';
38+
return `${sort.field}:${sort.order}`;
39+
};
40+
41+
const decodeSort = (
42+
value: string | null | undefined,
43+
): DeploymentSort | undefined => {
44+
if (!value) return undefined;
45+
const [field, order] = value.split(':');
46+
if (!field || (order !== 'ASC' && order !== 'DESC')) return undefined;
47+
return { field, order };
48+
};
49+
50+
/**
51+
* Safely parse the JSON-serialized `filter` URL param into a
52+
* `DeploymentFilter`. Invalid payloads degrade to "no filter" so a
53+
* malformed URL never crashes the page.
54+
*/
55+
const parseFilterVariable = (
56+
filter: string | null | undefined,
57+
): DeploymentFilter | undefined => {
58+
if (!filter) return undefined;
59+
try {
60+
const parsed = JSON.parse(filter);
61+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
62+
return parsed as DeploymentFilter;
63+
}
64+
return undefined;
65+
} catch {
66+
return undefined;
67+
}
68+
};
69+
70+
const toOrderBy = (
71+
sort: DeploymentSort | undefined,
72+
): DeploymentOrderBy[] | undefined => {
73+
if (!sort) return undefined;
74+
return [
75+
{
76+
field: sort.field as DeploymentOrderField,
77+
direction: sort.order as OrderDirection,
78+
},
79+
];
80+
};
81+
82+
const AdminDeploymentListPageContent: React.FC = () => {
83+
'use memo';
84+
const webUINavigate = useWebUINavigate();
85+
86+
const {
87+
baiPaginationOption,
88+
tablePaginationOption,
89+
setTablePaginationOption,
90+
} = useBAIPaginationOptionStateOnSearchParam({
91+
current: 1,
92+
pageSize: 10,
93+
});
94+
95+
const [queryParams, setQueryParams] = useQueryStates(
96+
{
97+
filter: parseAsString.withDefault(''),
98+
sort: parseAsString.withDefault(''),
99+
},
100+
{
101+
history: 'replace',
102+
},
103+
);
104+
105+
const sort = decodeSort(queryParams.sort);
106+
107+
const [fetchKey, updateFetchKey] = useFetchKey();
108+
109+
const queryVariables = {
110+
filter: parseFilterVariable(queryParams.filter),
111+
orderBy: toOrderBy(sort),
112+
first: baiPaginationOption.first ?? baiPaginationOption.limit,
113+
offset: baiPaginationOption.offset,
114+
};
115+
116+
const deferredQueryVariables = useDeferredValue(queryVariables);
117+
const deferredFetchKey = useDeferredValue(fetchKey);
118+
119+
const { adminDeployments } = useLazyLoadQuery<AdminDeploymentListPageQuery>(
120+
graphql`
121+
query AdminDeploymentListPageQuery(
122+
$filter: DeploymentFilter
123+
$orderBy: [DeploymentOrderBy!]
124+
$first: Int
125+
$offset: Int
126+
) {
127+
adminDeployments(
128+
filter: $filter
129+
orderBy: $orderBy
130+
first: $first
131+
offset: $offset
132+
) {
133+
...DeploymentList_modelDeploymentConnection
134+
}
135+
}
136+
`,
137+
deferredQueryVariables,
138+
{
139+
fetchPolicy:
140+
deferredFetchKey === INITIAL_FETCH_KEY
141+
? 'store-and-network'
142+
: 'network-only',
143+
fetchKey: deferredFetchKey,
144+
},
145+
);
146+
147+
const isLoading =
148+
deferredQueryVariables !== queryVariables || deferredFetchKey !== fetchKey;
149+
150+
return (
151+
<BAIFlex direction="column" align="stretch" gap="sm">
152+
<BAIFlex direction="row" justify="end">
153+
<BAIFetchKeyButton
154+
value={fetchKey}
155+
onChange={updateFetchKey}
156+
autoUpdateDelay={15_000}
157+
loading={isLoading}
158+
/>
159+
</BAIFlex>
160+
{adminDeployments ? (
161+
<DeploymentList
162+
mode="admin"
163+
deploymentsFrgmt={adminDeployments}
164+
filter={queryParams.filter}
165+
setFilter={(value) => {
166+
setQueryParams({ filter: value || null });
167+
setTablePaginationOption({ current: 1 });
168+
}}
169+
sort={sort}
170+
setSort={(value) => {
171+
setQueryParams({ sort: encodeSort(value) || null });
172+
}}
173+
page={tablePaginationOption.current}
174+
setPage={(value) => {
175+
setTablePaginationOption({ current: value });
176+
}}
177+
pageSize={tablePaginationOption.pageSize}
178+
setPageSize={(value) => {
179+
setTablePaginationOption({ current: 1, pageSize: value });
180+
}}
181+
loading={isLoading}
182+
onRowClick={(deploymentId) => {
183+
webUINavigate(`/deployments/${toLocalId(deploymentId)}`);
184+
}}
185+
/>
186+
) : null}
187+
</BAIFlex>
188+
);
189+
};
6190

7-
// TODO(needs-backend): FR-2672 — Admin deployment list page.
8-
// This is a placeholder stub introduced by FR-2664 so that the new
9-
// /admin-deployments route can be wired before the real page lands in
10-
// Phase 5.
11191
const AdminDeploymentListPage: React.FC = () => {
12192
'use memo';
13-
return <div>TODO: AdminDeploymentListPage — FR-2672</div>;
193+
const { t } = useTranslation();
194+
195+
return (
196+
<BAICard title={t('deployment.AdminDeployments')}>
197+
<Suspense fallback={<Skeleton active />}>
198+
<AdminDeploymentListPageContent />
199+
</Suspense>
200+
</BAICard>
201+
);
14202
};
15203

16204
export default AdminDeploymentListPage;

0 commit comments

Comments
 (0)