Skip to content

Commit 9a85aa9

Browse files
committed
feat(FR-2681): add DeploymentDetailPage with header, Configuration overview, and 4 tabs
1 parent 2dba9bb commit 9a85aa9

1 file changed

Lines changed: 191 additions & 5 deletions

File tree

react/src/pages/DeploymentDetailPage.tsx

Lines changed: 191 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,201 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5+
import { DeploymentDetailPageDeleteMutation } from '../__generated__/DeploymentDetailPageDeleteMutation.graphql';
6+
import { DeploymentDetailPageQuery } from '../__generated__/DeploymentDetailPageQuery.graphql';
7+
import DeploymentAccessTokensTab from '../components/DeploymentAccessTokensTab';
8+
import DeploymentAutoScalingTab from '../components/DeploymentAutoScalingTab';
9+
import DeploymentConfigurationSection from '../components/DeploymentConfigurationSection';
10+
import DeploymentReplicasTab from '../components/DeploymentReplicasTab';
11+
import DeploymentRevisionHistoryTab from '../components/DeploymentRevisionHistoryTab';
12+
import DeploymentStatusTag, {
13+
DeploymentStatus,
14+
} from '../components/DeploymentStatusTag';
15+
import { useWebUINavigate } from '../hooks';
16+
import { useCurrentUserInfo } from '../hooks/backendai';
17+
import { DeleteOutlined } from '@ant-design/icons';
18+
import { App, Skeleton, Tabs, Typography, theme } from 'antd';
19+
import { BAIButton, BAIFlex, toGlobalId, useBAILogger } from 'backend.ai-ui';
20+
import { parseAsStringLiteral, useQueryState } from 'nuqs';
521
import React from 'react';
22+
import { useTranslation } from 'react-i18next';
23+
import { graphql, useLazyLoadQuery, useMutation } from 'react-relay';
24+
import { useParams } from 'react-router-dom';
25+
26+
const tabValues = ['replicas', 'revisions', 'tokens', 'autoscaling'] as const;
27+
type TabKey = (typeof tabValues)[number];
628

7-
// TODO(needs-backend): FR-2681 — Deployment detail page.
8-
// This is a placeholder stub introduced by FR-2664 so that the new
9-
// /deployments/:deploymentId route can be wired before the real page
10-
// lands in Phase 4.
1129
const DeploymentDetailPage: React.FC = () => {
1230
'use memo';
13-
return <div>TODO: DeploymentDetailPage — FR-2681</div>;
31+
const { t } = useTranslation();
32+
const { token } = theme.useToken();
33+
const { message, modal } = App.useApp();
34+
const { logger } = useBAILogger();
35+
const webuiNavigate = useWebUINavigate();
36+
const [currentUser] = useCurrentUserInfo();
37+
38+
const { deploymentId: deploymentIdParam } = useParams<{
39+
deploymentId: string;
40+
}>();
41+
const deploymentId = deploymentIdParam ?? '';
42+
43+
const [activeTab, setActiveTab] = useQueryState(
44+
'tab',
45+
parseAsStringLiteral(tabValues).withDefault('replicas'),
46+
);
47+
48+
const { deployment } = useLazyLoadQuery<DeploymentDetailPageQuery>(
49+
graphql`
50+
query DeploymentDetailPageQuery($deploymentId: ID!) {
51+
deployment(id: $deploymentId) {
52+
id
53+
metadata {
54+
name
55+
status
56+
}
57+
creator @since(version: "26.4.3") {
58+
basicInfo {
59+
email
60+
}
61+
}
62+
...DeploymentConfigurationSection_deployment
63+
...DeploymentReplicasTab_deployment
64+
...DeploymentRevisionHistoryTab_deployment
65+
...DeploymentAccessTokensTab_deployment
66+
...DeploymentAutoScalingTab_deployment
67+
}
68+
}
69+
`,
70+
{
71+
deploymentId: toGlobalId('ModelDeployment', deploymentId),
72+
},
73+
);
74+
75+
const [commitDeleteMutation, isInFlightDeleteMutation] =
76+
useMutation<DeploymentDetailPageDeleteMutation>(graphql`
77+
mutation DeploymentDetailPageDeleteMutation(
78+
$input: DeleteDeploymentInput!
79+
) {
80+
deleteModelDeployment(input: $input) {
81+
id
82+
}
83+
}
84+
`);
85+
86+
if (!deployment) {
87+
return <Skeleton active />;
88+
}
89+
90+
const deploymentName = deployment.metadata.name;
91+
const deploymentStatus = deployment.metadata.status as DeploymentStatus;
92+
// Terminal lifecycle states — Delete should be disabled once the deployment
93+
// is already winding down or gone.
94+
const isDeploymentDestroying =
95+
deploymentStatus === 'STOPPING' ||
96+
deploymentStatus === 'STOPPED' ||
97+
deploymentStatus === 'TERMINATED';
98+
99+
const creatorEmail = deployment.creator?.basicInfo?.email ?? null;
100+
// When the creator email is unresolvable (e.g. manager versions < 26.4.3),
101+
// assume the current user owns the deployment so the UI does not
102+
// over-restrict editing/deletion capabilities.
103+
const isOwnedByCurrentUser =
104+
!creatorEmail || creatorEmail === currentUser.email;
105+
106+
const handleDelete = () => {
107+
modal.confirm({
108+
title: t('deployment.DeleteDeployment'),
109+
content: t('deployment.ConfirmDeleteDeployment', {
110+
name: deploymentName,
111+
}),
112+
okText: t('button.Delete'),
113+
okButtonProps: { danger: true },
114+
onOk: () =>
115+
new Promise<void>((resolve) => {
116+
commitDeleteMutation({
117+
variables: {
118+
input: { id: deployment.id },
119+
},
120+
onCompleted: (_response, errors) => {
121+
resolve();
122+
if (errors && errors.length > 0) {
123+
logger.error('Failed to delete deployment', errors);
124+
message.error(t('deployment.FailedToDeleteDeployment'));
125+
return;
126+
}
127+
message.success(t('deployment.DeploymentDeleted'));
128+
webuiNavigate('/deployments');
129+
},
130+
onError: (error) => {
131+
resolve();
132+
logger.error('Failed to delete deployment', error);
133+
message.error(t('deployment.FailedToDeleteDeployment'));
134+
},
135+
});
136+
}),
137+
});
138+
};
139+
140+
return (
141+
<BAIFlex direction="column" align="stretch" gap="md">
142+
<BAIFlex direction="row" justify="between" align="center" gap="sm">
143+
<BAIFlex direction="row" align="center" gap="sm">
144+
<Typography.Title level={3} style={{ margin: 0 }}>
145+
{deploymentName}
146+
</Typography.Title>
147+
<DeploymentStatusTag status={deploymentStatus} />
148+
</BAIFlex>
149+
<BAIButton
150+
danger
151+
icon={<DeleteOutlined />}
152+
loading={isInFlightDeleteMutation}
153+
disabled={isDeploymentDestroying || !isOwnedByCurrentUser}
154+
onClick={handleDelete}
155+
>
156+
{t('deployment.DeleteDeployment')}
157+
</BAIButton>
158+
</BAIFlex>
159+
<DeploymentConfigurationSection deploymentFrgmt={deployment} />
160+
<Tabs
161+
activeKey={activeTab}
162+
onChange={(key) => {
163+
setActiveTab(key as TabKey);
164+
}}
165+
destroyOnHidden
166+
items={[
167+
{
168+
key: 'replicas',
169+
label: t('deployment.tab.Replicas'),
170+
children: <DeploymentReplicasTab deploymentFrgmt={deployment} />,
171+
},
172+
{
173+
key: 'revisions',
174+
label: t('deployment.tab.RevisionHistory'),
175+
children: (
176+
<DeploymentRevisionHistoryTab deploymentFrgmt={deployment} />
177+
),
178+
},
179+
{
180+
key: 'tokens',
181+
label: t('deployment.tab.AccessTokens'),
182+
children: (
183+
<DeploymentAccessTokensTab
184+
deploymentFrgmt={deployment}
185+
isOwnedByCurrentUser={isOwnedByCurrentUser}
186+
isDeploymentDestroying={isDeploymentDestroying}
187+
/>
188+
),
189+
},
190+
{
191+
key: 'autoscaling',
192+
label: t('deployment.tab.AutoScaling'),
193+
children: <DeploymentAutoScalingTab deploymentFrgmt={deployment} />,
194+
},
195+
]}
196+
style={{ marginTop: token.marginSM }}
197+
/>
198+
</BAIFlex>
199+
);
14200
};
15201

16202
export default DeploymentDetailPage;

0 commit comments

Comments
 (0)