Skip to content

Commit 66cb905

Browse files
committed
feat: show empty state message in deployment list table when no results
1 parent 47e5b2d commit 66cb905

4 files changed

Lines changed: 297 additions & 202 deletions

File tree

react/src/components/DeploymentList.tsx

Lines changed: 140 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5+
import type { DeploymentListDeleteMutation } from '../__generated__/DeploymentListDeleteMutation.graphql';
56
import {
67
DeploymentList_modelDeploymentConnection$data,
78
DeploymentList_modelDeploymentConnection$key,
@@ -10,22 +11,28 @@ import { useSuspendedBackendaiClient } from '../hooks';
1011
import BAIRadioGroup from './BAIRadioGroup';
1112
import DeploymentOwnerInfo from './DeploymentOwnerInfo';
1213
import DeploymentStatusTag, { DeploymentStatus } from './DeploymentStatusTag';
13-
import { Tag, Typography } from 'antd';
14+
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
15+
import { Alert, App, Tag, Typography, theme } from 'antd';
1416
import {
17+
BAIConfirmModalWithInput,
1518
BAIFlex,
1619
BAIGraphQLPropertyFilter,
20+
BAINameActionCell,
1721
BAITable,
1822
filterOutEmpty,
1923
filterOutNullAndUndefined,
24+
toLocalId,
25+
useBAILogger,
2026
type BAIColumnType,
27+
type BAINameActionCellAction,
2128
type BAITableProps,
2229
type GraphQLFilter,
2330
} from 'backend.ai-ui';
2431
import dayjs from 'dayjs';
2532
import * as _ from 'lodash-es';
26-
import React from 'react';
33+
import React, { useState } from 'react';
2734
import { useTranslation } from 'react-i18next';
28-
import { graphql, useFragment } from 'react-relay';
35+
import { graphql, useFragment, useMutation } from 'react-relay';
2936

3037
type DeploymentEdge = NonNullable<
3138
NonNullable<DeploymentList_modelDeploymentConnection$data['edges']>[number]
@@ -116,6 +123,10 @@ export interface DeploymentListProps extends Omit<
116123
mode: 'user' | 'admin';
117124
/** Called when a row name is clicked. Receives the deployment global ID. */
118125
onRowClick?: (deploymentId: string) => void;
126+
/** Called when the edit action button is clicked. Receives the deployment global ID. */
127+
onEditClick?: (deploymentId: string) => void;
128+
/** Called after a deployment is successfully deleted. Use to refresh the list. */
129+
onDeleteComplete?: () => void;
119130
/** Extra elements rendered at the end of the toolbar row (e.g. refresh + create buttons). */
120131
toolbarEnd?: React.ReactNode;
121132
}
@@ -129,12 +140,30 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
129140
onStatusCategoryChange,
130141
mode,
131142
onRowClick,
143+
onEditClick,
144+
onDeleteComplete,
132145
toolbarEnd,
133146
...tableProps
134147
}) => {
135148
'use memo';
136149
const { t } = useTranslation();
150+
const { message } = App.useApp();
151+
const { token } = theme.useToken();
152+
const { logger } = useBAILogger();
137153
const baiClient = useSuspendedBackendaiClient();
154+
const [deletingDeployment, setDeletingDeployment] = useState<{
155+
id: string;
156+
name: string;
157+
} | null>(null);
158+
159+
const [commitDeleteMutation, isInFlightDeleteMutation] =
160+
useMutation<DeploymentListDeleteMutation>(graphql`
161+
mutation DeploymentListDeleteMutation($input: DeleteDeploymentInput!) {
162+
deleteModelDeployment(input: $input) {
163+
id
164+
}
165+
}
166+
`);
138167

139168
const connection = useFragment(
140169
graphql`
@@ -246,16 +275,34 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
246275
fixed: 'left' as const,
247276
render: (_text, row) => {
248277
const name = row.metadata?.name ?? '-';
249-
if (!onRowClick) {
250-
return <Typography.Text>{name}</Typography.Text>;
278+
const isDestroying = ['STOPPING', 'STOPPED', 'TERMINATED'].includes(
279+
row.metadata?.status ?? '',
280+
);
281+
const actions: BAINameActionCellAction[] = [];
282+
if (onEditClick) {
283+
actions.push({
284+
key: 'edit',
285+
title: t('deployment.EditDeployment'),
286+
icon: <EditOutlined />,
287+
disabled: isDestroying,
288+
onClick: () => onEditClick(row.id),
289+
});
251290
}
291+
actions.push({
292+
key: 'delete',
293+
title: t('deployment.DeleteDeployment'),
294+
icon: <DeleteOutlined />,
295+
type: 'danger',
296+
disabled: isDestroying,
297+
onClick: () => setDeletingDeployment({ id: row.id, name }),
298+
});
252299
return (
253-
<Typography.Link
254-
onClick={() => onRowClick(row.id)}
255-
style={{ maxWidth: 240 }}
256-
>
257-
{name}
258-
</Typography.Link>
300+
<BAINameActionCell
301+
title={name}
302+
onTitleClick={onRowClick ? () => onRowClick(row.id) : undefined}
303+
actions={actions}
304+
showActions="always"
305+
/>
259306
);
260307
},
261308
},
@@ -370,42 +417,91 @@ const DeploymentList: React.FC<DeploymentListProps> = ({
370417
: { ...tableProps.pagination, total: totalCount };
371418

372419
return (
373-
<BAIFlex direction="column" align="stretch" gap="sm">
374-
<BAIFlex justify="between" wrap="wrap" gap="sm">
375-
<BAIFlex gap="sm" align="start" wrap="wrap" style={{ flexShrink: 1 }}>
376-
<BAIRadioGroup
377-
value={statusCategory}
378-
onChange={(e) => onStatusCategoryChange?.(e.target.value)}
379-
options={[
380-
{ label: t('deployment.Running'), value: 'running' },
381-
{ label: t('deployment.status.Terminated'), value: 'finished' },
382-
]}
383-
/>
384-
<BAIGraphQLPropertyFilter
385-
filterProperties={filterProperties}
386-
value={filterValue}
387-
onChange={(next) => {
388-
setFilter(stringifyFilter(next));
420+
<>
421+
<BAIFlex direction="column" align="stretch" gap="sm">
422+
<BAIFlex justify="between" wrap="wrap" gap="sm">
423+
<BAIFlex gap="sm" align="start" wrap="wrap" style={{ flexShrink: 1 }}>
424+
<BAIRadioGroup
425+
value={statusCategory}
426+
onChange={(e) => onStatusCategoryChange?.(e.target.value)}
427+
options={[
428+
{ label: t('deployment.Running'), value: 'running' },
429+
{ label: t('deployment.status.Terminated'), value: 'finished' },
430+
]}
431+
/>
432+
<BAIGraphQLPropertyFilter
433+
filterProperties={filterProperties}
434+
value={filterValue}
435+
onChange={(next) => {
436+
setFilter(stringifyFilter(next));
437+
}}
438+
/>
439+
</BAIFlex>
440+
{toolbarEnd}
441+
</BAIFlex>
442+
<div style={{ overflowX: 'auto' }}>
443+
<BAITable<DeploymentNode>
444+
rowKey="id"
445+
scroll={{ x: 'max-content' }}
446+
showSorterTooltip={false}
447+
locale={{ emptyText: t('deployment.NoDeployments') }}
448+
{...tableProps}
449+
dataSource={deployments}
450+
columns={columns}
451+
onChangeOrder={(order) => {
452+
onChangeOrder?.(order || null);
389453
}}
454+
pagination={paginationWithTotal}
390455
/>
391-
</BAIFlex>
392-
{toolbarEnd}
456+
</div>
393457
</BAIFlex>
394-
<div style={{ overflowX: 'auto' }}>
395-
<BAITable<DeploymentNode>
396-
rowKey="id"
397-
scroll={{ x: 'max-content' }}
398-
showSorterTooltip={false}
399-
{...tableProps}
400-
dataSource={deployments}
401-
columns={columns}
402-
onChangeOrder={(order) => {
403-
onChangeOrder?.(order || null);
404-
}}
405-
pagination={paginationWithTotal}
406-
/>
407-
</div>
408-
</BAIFlex>
458+
<BAIConfirmModalWithInput
459+
open={!!deletingDeployment}
460+
title={t('deployment.DeleteDeployment')}
461+
content={
462+
<BAIFlex direction="column" gap="md" align="stretch">
463+
<Alert type="warning" title={t('dialog.warning.CannotBeUndone')} />
464+
<BAIFlex>
465+
<Typography.Text style={{ marginRight: token.marginXXS }}>
466+
{t('dialog.TypeNameToConfirmDeletion')}
467+
</Typography.Text>
468+
(
469+
<Typography.Text code>{deletingDeployment?.name}</Typography.Text>
470+
)
471+
</BAIFlex>
472+
</BAIFlex>
473+
}
474+
confirmText={deletingDeployment?.name ?? ''}
475+
inputProps={{ placeholder: deletingDeployment?.name ?? '' }}
476+
okText={t('button.Delete')}
477+
okButtonProps={{ loading: isInFlightDeleteMutation }}
478+
onOk={() => {
479+
if (!deletingDeployment) return;
480+
commitDeleteMutation({
481+
variables: {
482+
input: {
483+
id: toLocalId(deletingDeployment.id) ?? deletingDeployment.id,
484+
},
485+
},
486+
onCompleted: (_response, errors) => {
487+
if (errors && errors.length > 0) {
488+
logger.error('Failed to delete deployment', errors);
489+
message.error(t('deployment.FailedToDeleteDeployment'));
490+
return;
491+
}
492+
message.success(t('deployment.DeploymentDeleted'));
493+
setDeletingDeployment(null);
494+
onDeleteComplete?.();
495+
},
496+
onError: (error) => {
497+
logger.error('Failed to delete deployment', error);
498+
message.error(t('deployment.FailedToDeleteDeployment'));
499+
},
500+
});
501+
}}
502+
onCancel={() => setDeletingDeployment(null)}
503+
/>
504+
</>
409505
);
410506
};
411507

react/src/pages/AdminDeploymentListPage.tsx

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ const parseFilterVariable = (
4848

4949
const AdminDeploymentListPageContent: React.FC = () => {
5050
'use memo';
51-
const { t } = useTranslation();
5251
const webUINavigate = useWebUINavigate();
5352

5453
const {
@@ -135,66 +134,67 @@ const AdminDeploymentListPageContent: React.FC = () => {
135134
const isLoading =
136135
deferredQueryVariables !== queryVariables || deferredFetchKey !== fetchKey;
137136

138-
return (
139-
<BAICard
140-
title={t('deployment.Deployments')}
141-
styles={{
142-
header: { borderBottom: 'none' },
143-
body: { paddingTop: 0 },
137+
return adminDeployments ? (
138+
<DeploymentList
139+
mode="admin"
140+
deploymentsFrgmt={adminDeployments}
141+
filter={queryParams.filter}
142+
setFilter={(value) => {
143+
setQueryParams({ filter: value || null });
144+
setTablePaginationOption({ current: 1 });
144145
}}
145-
>
146-
{adminDeployments ? (
147-
<DeploymentList
148-
mode="admin"
149-
deploymentsFrgmt={adminDeployments}
150-
filter={queryParams.filter}
151-
setFilter={(value) => {
152-
setQueryParams({ filter: value || null });
153-
setTablePaginationOption({ current: 1 });
154-
}}
155-
order={queryParams.order ?? undefined}
156-
onChangeOrder={(order) => {
157-
setQueryParams({ order: (order as DeploymentOrderValue) ?? null });
158-
}}
159-
statusCategory={queryParams.statusCategory}
160-
onStatusCategoryChange={(value) => {
161-
setQueryParams({ statusCategory: value });
162-
setTablePaginationOption({ current: 1 });
163-
}}
164-
pagination={{
165-
...tablePaginationOption,
166-
onChange: (current, pageSize) => {
167-
setTablePaginationOption({ current, pageSize });
168-
},
169-
}}
170-
tableSettings={{
171-
columnOverrides,
172-
onColumnOverridesChange: setColumnOverrides,
173-
}}
146+
order={queryParams.order ?? undefined}
147+
onChangeOrder={(order) => {
148+
setQueryParams({ order: (order as DeploymentOrderValue) ?? null });
149+
}}
150+
statusCategory={queryParams.statusCategory}
151+
onStatusCategoryChange={(value) => {
152+
setQueryParams({ statusCategory: value });
153+
setTablePaginationOption({ current: 1 });
154+
}}
155+
pagination={{
156+
...tablePaginationOption,
157+
onChange: (current, pageSize) => {
158+
setTablePaginationOption({ current, pageSize });
159+
},
160+
}}
161+
tableSettings={{
162+
columnOverrides,
163+
onColumnOverridesChange: setColumnOverrides,
164+
}}
165+
loading={isLoading}
166+
onRowClick={(deploymentId) => {
167+
webUINavigate(`/deployments/${toLocalId(deploymentId)}`);
168+
}}
169+
onEditClick={(deploymentId) => {
170+
webUINavigate(`/deployments/${toLocalId(deploymentId)}/edit`);
171+
}}
172+
onDeleteComplete={updateFetchKey}
173+
toolbarEnd={
174+
<BAIFetchKeyButton
175+
value={fetchKey}
176+
onChange={updateFetchKey}
177+
autoUpdateDelay={15_000}
174178
loading={isLoading}
175-
onRowClick={(deploymentId) => {
176-
webUINavigate(`/deployments/${toLocalId(deploymentId)}`);
177-
}}
178-
toolbarEnd={
179-
<BAIFetchKeyButton
180-
value={fetchKey}
181-
onChange={updateFetchKey}
182-
autoUpdateDelay={15_000}
183-
loading={isLoading}
184-
/>
185-
}
186179
/>
187-
) : null}
188-
</BAICard>
189-
);
180+
}
181+
/>
182+
) : null;
190183
};
191184

192185
const AdminDeploymentListPage: React.FC = () => {
193186
'use memo';
187+
const { t } = useTranslation();
194188
return (
195-
<Suspense fallback={<Skeleton active />}>
196-
<AdminDeploymentListPageContent />
197-
</Suspense>
189+
<BAICard
190+
variant="borderless"
191+
title={t('webui.menu.Deployments')}
192+
styles={{ body: { paddingTop: 0 } }}
193+
>
194+
<Suspense fallback={<Skeleton active />}>
195+
<AdminDeploymentListPageContent />
196+
</Suspense>
197+
</BAICard>
198198
);
199199
};
200200

0 commit comments

Comments
 (0)