Skip to content

Commit 4c1efbe

Browse files
committed
feat(FR-2822): replace deployment launcher with deployment-only create modal
1 parent 00572ee commit 4c1efbe

29 files changed

Lines changed: 311 additions & 407 deletions

react/src/components/DeploymentConfigurationSection.tsx

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
55
import { DeploymentConfigurationSectionQuery } from '../__generated__/DeploymentConfigurationSectionQuery.graphql';
6-
import { useWebUINavigate } from '../hooks';
6+
import DeploymentAddRevisionModal from './DeploymentAddRevisionModal';
77
import DeploymentRevisionDetailDrawer from './DeploymentRevisionDetailDrawer';
88
import FolderLink from './FolderLink';
99
import SourceCodeView from './SourceCodeView';
1010
import {
1111
CheckOutlined,
1212
CloseOutlined,
13-
EditOutlined,
1413
LoadingOutlined,
1514
} from '@ant-design/icons';
1615
import {
@@ -27,7 +26,6 @@ import { DescriptionsItemType } from 'antd/es/descriptions';
2726
import {
2827
filterOutEmpty,
2928
filterOutNullAndUndefined,
30-
BAIButton,
3129
BAICard,
3230
BAIFetchKeyButton,
3331
BAIFlex,
@@ -376,19 +374,20 @@ const DeploymentRevisionInfoContent: React.FC<{
376374

377375
const DeploymentConfigurationSection: React.FC<
378376
DeploymentConfigurationSectionProps
379-
> = ({ deploymentId, isDeploymentDestroying = false }) => {
377+
> = ({
378+
deploymentId,
379+
isDeploymentDestroying: _isDeploymentDestroying = false,
380+
}) => {
380381
'use memo';
381382

382383
const { t } = useTranslation();
383-
const webuiNavigate = useWebUINavigate();
384384
const [isPendingRefetch, startRefetchTransition] = useTransition();
385385
const [fetchKey, setFetchKey] = useState(0);
386386
const [drawerRevisionId, setDrawerRevisionId] = useState<string | null>(null);
387387
const [drawerCurrentRevisionId, setDrawerCurrentRevisionId] = useState<
388388
string | null
389389
>(null);
390-
391-
const deploymentLocalId = toLocalId(deploymentId);
390+
const [addRevisionOpen, setAddRevisionOpen] = useState(false);
392391

393392
const handleShowRevisionDrawer = (
394393
revisionId: string,
@@ -411,16 +410,6 @@ const DeploymentConfigurationSection: React.FC<
411410
value=""
412411
onChange={handleRefetch}
413412
/>
414-
<BAIButton
415-
type="primary"
416-
icon={<EditOutlined />}
417-
disabled={isDeploymentDestroying}
418-
onClick={() => {
419-
webuiNavigate(`/deployments/${deploymentLocalId}/edit`);
420-
}}
421-
>
422-
{t('deployment.EditConfiguration')}
423-
</BAIButton>
424413
</BAIFlex>
425414
);
426415

@@ -450,6 +439,7 @@ const DeploymentConfigurationSection: React.FC<
450439
fetchKey={fetchKey}
451440
overviewExtra={overviewExtra}
452441
onShowRevisionDrawer={handleShowRevisionDrawer}
442+
onAddRevision={() => setAddRevisionOpen(true)}
453443
/>
454444
</Suspense>
455445
<BAIUnmountAfterClose>
@@ -460,6 +450,15 @@ const DeploymentConfigurationSection: React.FC<
460450
onClose={() => setDrawerRevisionId(null)}
461451
/>
462452
</BAIUnmountAfterClose>
453+
<DeploymentAddRevisionModal
454+
open={addRevisionOpen}
455+
onClose={() => setAddRevisionOpen(false)}
456+
onSuccess={() => {
457+
setAddRevisionOpen(false);
458+
handleRefetch();
459+
}}
460+
deploymentId={deploymentId}
461+
/>
463462
</>
464463
);
465464
};
@@ -480,7 +479,14 @@ const DeploymentConfigurationCards: React.FC<{
480479
revisionId: string,
481480
currentRevisionId: string | null,
482481
) => void;
483-
}> = ({ deploymentId, fetchKey, overviewExtra, onShowRevisionDrawer }) => {
482+
onAddRevision: () => void;
483+
}> = ({
484+
deploymentId,
485+
fetchKey,
486+
overviewExtra,
487+
onShowRevisionDrawer,
488+
onAddRevision,
489+
}) => {
484490
'use memo';
485491
const { t } = useTranslation();
486492

@@ -569,8 +575,23 @@ const DeploymentConfigurationCards: React.FC<{
569575
{ fetchKey, fetchPolicy: 'network-only' },
570576
);
571577

578+
const hasNoRevision = !deployment?.currentRevision;
579+
572580
return (
573581
<>
582+
{hasNoRevision && (
583+
<Alert
584+
type="warning"
585+
showIcon
586+
title={t('deployment.NoCurrentRevisionDeployed')}
587+
description={t('deployment.NoCurrentRevisionDeployedDescription')}
588+
action={
589+
<Button onClick={onAddRevision}>
590+
{t('deployment.AddRevision')}
591+
</Button>
592+
}
593+
/>
594+
)}
574595
<BAICard
575596
title={t('deployment.Overview')}
576597
extra={overviewExtra}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
@license
3+
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
4+
*/
5+
import { DeploymentCreateModalMutation } from '../__generated__/DeploymentCreateModalMutation.graphql';
6+
import { useCurrentDomainValue, useWebUINavigate } from '../hooks';
7+
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
8+
import { App, Button, Checkbox, Form, Input, InputNumber, Select } from 'antd';
9+
import { BAIButton, BAIFlex, BAIModal, toLocalId } from 'backend.ai-ui';
10+
import React from 'react';
11+
import { useTranslation } from 'react-i18next';
12+
import { graphql, useMutation } from 'react-relay';
13+
14+
interface FormValues {
15+
name: string;
16+
tags: string[];
17+
openToPublic: boolean;
18+
replicaCount: number;
19+
}
20+
21+
interface DeploymentCreateModalProps {
22+
open: boolean;
23+
onClose: () => void;
24+
}
25+
26+
const DeploymentCreateModal: React.FC<DeploymentCreateModalProps> = ({
27+
open,
28+
onClose,
29+
}) => {
30+
'use memo';
31+
const { t } = useTranslation();
32+
const [form] = Form.useForm<FormValues>();
33+
const navigate = useWebUINavigate();
34+
const { message } = App.useApp();
35+
const { id: projectId } = useCurrentProjectValue();
36+
const currentDomain = useCurrentDomainValue();
37+
38+
const [commitCreate, isCreating] = useMutation<DeploymentCreateModalMutation>(
39+
graphql`
40+
mutation DeploymentCreateModalMutation($input: CreateDeploymentInput!) {
41+
createModelDeployment(input: $input) {
42+
deployment {
43+
id
44+
}
45+
}
46+
}
47+
`,
48+
);
49+
50+
const handleFinish = (values: FormValues) => {
51+
if (!projectId) {
52+
message.error(t('general.ErrorOccurred'));
53+
return;
54+
}
55+
commitCreate({
56+
variables: {
57+
input: {
58+
metadata: {
59+
projectId,
60+
domainName: currentDomain,
61+
name: values.name,
62+
tags: values.tags?.length ? values.tags : null,
63+
},
64+
networkAccess: {
65+
// TODO: expose preferredDomainName once backend business logic is in place
66+
preferredDomainName: null,
67+
openToPublic: values.openToPublic,
68+
},
69+
// TODO: expose strategy type selection once BLUE_GREEN is supported server-side
70+
defaultDeploymentStrategy: { type: 'ROLLING' },
71+
replicaCount: values.replicaCount,
72+
initialRevision: null,
73+
},
74+
},
75+
onCompleted: (response, errors) => {
76+
if (errors && errors.length > 0) {
77+
message.error(
78+
errors.map((e) => e.message).join('\n') ||
79+
t('deployment.FailedToCreateDeployment'),
80+
);
81+
return;
82+
}
83+
const newId = toLocalId(response.createModelDeployment.deployment.id);
84+
form.resetFields();
85+
onClose();
86+
navigate(`/deployments/${newId}`);
87+
},
88+
onError: (err) => {
89+
message.error(err.message ?? t('deployment.FailedToCreateDeployment'));
90+
},
91+
});
92+
};
93+
94+
const handleCancel = () => {
95+
form.resetFields();
96+
onClose();
97+
};
98+
99+
return (
100+
<BAIModal
101+
open={open}
102+
title={t('deployment.CreateDeployment')}
103+
onCancel={handleCancel}
104+
width={520}
105+
footer={
106+
<BAIFlex justify="end" gap="xs">
107+
<Button onClick={handleCancel}>{t('button.Cancel')}</Button>
108+
<BAIButton
109+
type="primary"
110+
loading={isCreating}
111+
onClick={() => form.submit()}
112+
>
113+
{t('button.Create')}
114+
</BAIButton>
115+
</BAIFlex>
116+
}
117+
>
118+
<Form<FormValues>
119+
form={form}
120+
layout="vertical"
121+
onFinish={handleFinish}
122+
initialValues={{ openToPublic: false, replicaCount: 1, tags: [] }}
123+
style={{ marginTop: 8 }}
124+
>
125+
<Form.Item
126+
name="name"
127+
label={t('deployment.DeploymentName')}
128+
rules={[{ required: true, message: t('deployment.NameRequired') }]}
129+
>
130+
<Input placeholder={t('deployment.NamePlaceholder')} />
131+
</Form.Item>
132+
<Form.Item name="tags" label={t('deployment.Tags')}>
133+
<Select
134+
mode="tags"
135+
placeholder={t('deployment.TagsPlaceholder')}
136+
tokenSeparators={[',', '\n']}
137+
open={false}
138+
/>
139+
</Form.Item>
140+
<Form.Item
141+
name="replicaCount"
142+
label={t('deployment.DesiredReplicas')}
143+
rules={[{ required: true }]}
144+
>
145+
<InputNumber min={1} style={{ width: '100%' }} />
146+
</Form.Item>
147+
<Form.Item name="openToPublic" valuePropName="checked" required>
148+
<Checkbox>{t('deployment.OpenToPublic')}</Checkbox>
149+
</Form.Item>
150+
</Form>
151+
</BAIModal>
152+
);
153+
};
154+
155+
export default DeploymentCreateModal;

0 commit comments

Comments
 (0)