Skip to content

Commit 0fcddbd

Browse files
committed
feat(FR-1407): add modify and delete action buttons for deployment detail page
1 parent d4be88c commit 0fcddbd

28 files changed

+412
-71
lines changed

packages/backend.ai-ui/src/components/Table/BAITable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ const BAITable = <RecordType extends object = any>({
331331
opacity: loading ? 0.7 : 1,
332332
transition: 'opacity 0.3s ease',
333333
}}
334+
scroll={tableProps.scroll || { x: 'max-content' }}
334335
components={
335336
resizable
336337
? _.merge(components || {}, {

react/src/components/AccessTokenList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const AccessTokenList: React.FC<AccessTokenListProps> = ({
6565
},
6666
{
6767
key: 'validUntil',
68-
title: t('deployment.ExpiredDate'),
68+
title: t('deployment.ExpirationDate'),
6969
dataIndex: 'validUntil',
7070
render: (value) => dayjs(value).format('LLL'),
7171
},
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import BAIModal, { BAIModalProps } from './BAIModal';
2+
import DeploymentMetadataFormItem from './DeploymentMetadataFormItem';
3+
import DeploymentNetworkAccessFormItem from './DeploymentNetworkAccessFormItem';
4+
import DeploymentStrategyFormItem from './DeploymentStrategyFormItem';
5+
import { App, Form, FormInstance } from 'antd';
6+
import { toLocalId } from 'backend.ai-ui';
7+
import { useRef } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { graphql, useFragment, useMutation } from 'react-relay';
10+
import {
11+
DeploymentModifyModalFragment$data,
12+
DeploymentModifyModalFragment$key,
13+
} from 'src/__generated__/DeploymentModifyModalFragment.graphql';
14+
import { DeploymentModifyModalMutation } from 'src/__generated__/DeploymentModifyModalMutation.graphql';
15+
16+
interface DeploymentModifyModalProps extends BAIModalProps {
17+
deploymentFrgmt?: DeploymentModifyModalFragment$key | null;
18+
onRequestClose: (success?: boolean) => void;
19+
}
20+
21+
const DeploymentModifyModal: React.FC<DeploymentModifyModalProps> = ({
22+
onRequestClose,
23+
deploymentFrgmt,
24+
...baiModalProps
25+
}) => {
26+
const { t } = useTranslation();
27+
const { message } = App.useApp();
28+
const formRef =
29+
useRef<FormInstance<DeploymentModifyModalFragment$data>>(null);
30+
31+
const deployment = useFragment(
32+
graphql`
33+
fragment DeploymentModifyModalFragment on ModelDeployment {
34+
id
35+
metadata {
36+
name
37+
tags
38+
}
39+
networkAccess {
40+
openToPublic
41+
preferredDomainName
42+
}
43+
defaultDeploymentStrategy {
44+
type
45+
}
46+
replicaState {
47+
desiredReplicaCount
48+
}
49+
}
50+
`,
51+
deploymentFrgmt,
52+
);
53+
54+
const [commitUpdateDeployment, isInFlightUpdateDeployment] =
55+
useMutation<DeploymentModifyModalMutation>(graphql`
56+
mutation DeploymentModifyModalMutation(
57+
$input: UpdateModelDeploymentInput!
58+
) {
59+
updateModelDeployment(input: $input) {
60+
deployment {
61+
id
62+
metadata {
63+
name
64+
tags
65+
}
66+
networkAccess {
67+
openToPublic
68+
}
69+
defaultDeploymentStrategy {
70+
type
71+
}
72+
}
73+
}
74+
}
75+
`);
76+
77+
const handleOk = () => {
78+
formRef.current?.validateFields().then((values) => {
79+
commitUpdateDeployment({
80+
variables: {
81+
input: {
82+
id: toLocalId(deployment?.id || ''),
83+
name: values.metadata?.name,
84+
tags: values.metadata?.tags,
85+
defaultDeploymentStrategy: values?.defaultDeploymentStrategy,
86+
desiredReplicaCount: values.replicaState?.desiredReplicaCount,
87+
preferredDomainName: values.networkAccess?.preferredDomainName,
88+
openToPublic: values.networkAccess?.openToPublic,
89+
},
90+
},
91+
onCompleted: (res, errors) => {
92+
if (!res?.updateModelDeployment?.deployment?.id) {
93+
message.error(t('message.FailedToUpdate'));
94+
return;
95+
}
96+
if (errors && errors.length > 0) {
97+
const errorMsgList = errors.map((error) => error.message);
98+
for (const error of errorMsgList) {
99+
message.error(error);
100+
}
101+
} else {
102+
message.success(t('message.SuccessfullyUpdated'));
103+
onRequestClose(true);
104+
}
105+
},
106+
onError: (err) => {
107+
message.error(err.message || t('message.FailedToUpdate'));
108+
},
109+
});
110+
});
111+
};
112+
113+
return (
114+
<BAIModal
115+
{...baiModalProps}
116+
destroyOnClose
117+
onOk={handleOk}
118+
onCancel={() => onRequestClose(false)}
119+
okText={t('button.Update')}
120+
confirmLoading={isInFlightUpdateDeployment}
121+
title={t('deployment.ModifyDeployment')}
122+
>
123+
<Form
124+
ref={formRef}
125+
layout="vertical"
126+
initialValues={{
127+
...deployment,
128+
}}
129+
style={{ maxWidth: '100%' }}
130+
>
131+
<DeploymentMetadataFormItem />
132+
<DeploymentStrategyFormItem />
133+
<DeploymentNetworkAccessFormItem />
134+
</Form>
135+
</BAIModal>
136+
);
137+
};
138+
139+
export default DeploymentModifyModal;

react/src/components/DeploymentNetworkAccessFormItem.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ const DeploymentNetworkAccessFormItem: React.FC = () => {
1818
name={['networkAccess', 'preferredDomainName']}
1919
label={t('deployment.launcher.PreferredDomainName')}
2020
>
21-
<Input
22-
placeholder={t('deployment.launcher.PreferredDomainNamePlaceholder')}
23-
/>
21+
<Input placeholder={'my-model.example.com'} />
2422
</Form.Item>
2523

2624
<Form.Item

react/src/components/DeploymentStrategyFormItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const DeploymentStrategyFormItem: React.FC = () => {
103103
}}
104104
</Form.Item>
105105
<Form.Item
106-
name={['desiredReplicaCount']}
106+
name={['replicaState', 'desiredReplicaCount']}
107107
label={t('deployment.NumberOfDesiredReplicas')}
108108
rules={[
109109
{

react/src/components/DeploymentTokenGenerationModal.tsx

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import BAIModal, { BAIModalProps } from './BAIModal';
2-
import { DatePicker, Form, FormInstance, message } from 'antd';
2+
import { DatePicker, Form, FormInstance, message, Select } from 'antd';
33
import { BAIFlex } from 'backend.ai-ui';
44
import dayjs from 'dayjs';
55
import React, { useRef } from 'react';
@@ -18,6 +18,7 @@ const DeploymentTokenGenerationModal: React.FC<
1818
> = ({ onRequestClose, onCancel, deploymentId, ...baiModalProps }) => {
1919
const { t } = useTranslation();
2020
const formRef = useRef<FormInstance>(null);
21+
// expiryOption은 Form.Item에서 관리
2122

2223
const [commitCreateAccessToken, isInFlightCreateAccessToken] =
2324
useMutation<DeploymentTokenGenerationModalMutation>(graphql`
@@ -36,41 +37,51 @@ const DeploymentTokenGenerationModal: React.FC<
3637
`);
3738

3839
const handleOk = () => {
39-
formRef.current?.validateFields().then((values) => {
40-
const validUntil = values.datetime.unix();
41-
commitCreateAccessToken({
42-
variables: {
43-
input: {
44-
validUntil: validUntil,
45-
modelDeploymentId: deploymentId,
40+
formRef.current
41+
?.validateFields()
42+
.then((values) => {
43+
let validUntil;
44+
if (values.expiryOption === 'custom') {
45+
validUntil = values.datetime.unix();
46+
} else {
47+
const daysToAdd = parseInt(values.expiryOption.replace('days', ''));
48+
validUntil = dayjs().add(daysToAdd, 'day').unix();
49+
}
50+
51+
commitCreateAccessToken({
52+
variables: {
53+
input: {
54+
validUntil: validUntil,
55+
modelDeploymentId: deploymentId,
56+
},
4657
},
47-
},
48-
onCompleted: (res, errors) => {
49-
if (!res?.createAccessToken?.accessToken) {
50-
message.error(t('deployment.TokenGenerationFailed'));
51-
return;
52-
}
53-
if (errors && errors.length > 0) {
54-
const errorMsgList = errors.map((error) => error.message);
55-
for (const error of errorMsgList) {
56-
message.error(error);
58+
onCompleted: (res, errors) => {
59+
if (!res?.createAccessToken?.accessToken) {
60+
message.error(t('deployment.TokenGenerationFailed'));
61+
return;
5762
}
58-
} else {
59-
message.success(t('deployment.TokenGenerated'));
60-
onRequestClose(true);
61-
}
62-
},
63-
onError: (err) => {
64-
if (err?.message?.includes('valid_until is older than now')) {
65-
message.error(t('deployment.TokenExpiredDateError'));
66-
return;
67-
} else {
68-
message.error(t('deployment.TokenGenerationFailed'));
69-
console.log(err);
70-
}
71-
},
72-
});
73-
});
63+
if (errors && errors.length > 0) {
64+
const errorMsgList = errors.map((error) => error.message);
65+
for (const error of errorMsgList) {
66+
message.error(error);
67+
}
68+
} else {
69+
message.success(t('deployment.TokenGenerated'));
70+
onRequestClose(true);
71+
}
72+
},
73+
onError: (err) => {
74+
if (err?.message?.includes('valid_until is older than now')) {
75+
message.error(t('deployment.TokenExpiredDateError'));
76+
return;
77+
} else {
78+
message.error(t('deployment.TokenGenerationFailed'));
79+
console.log(err);
80+
}
81+
},
82+
});
83+
})
84+
.catch(() => {});
7485
};
7586

7687
return (
@@ -81,45 +92,75 @@ const DeploymentTokenGenerationModal: React.FC<
8192
onCancel={() => onRequestClose(false)}
8293
okText={t('button.Generate')}
8394
confirmLoading={isInFlightCreateAccessToken}
84-
centered
8595
title={t('deployment.GenerateNewToken')}
8696
>
8797
<Form
8898
ref={formRef}
8999
preserve={false}
90-
labelCol={{ span: 10 }}
100+
labelCol={{ span: 12 }}
91101
initialValues={{
92-
datetime: dayjs().add(24, 'hour'),
102+
expiryOption: '7days',
103+
datetime: dayjs().add(7, 'day'),
93104
}}
94105
validateTrigger={['onChange', 'onBlur']}
95106
>
96107
<BAIFlex direction="column" gap="sm" align="stretch" justify="center">
97-
<Form.Item
98-
name="datetime"
99-
label={t('deployment.ExpiredDate')}
100-
rules={[
101-
{
102-
type: 'object',
103-
required: true,
104-
message: t('deployment.PleaseSelectTime'),
105-
},
106-
() => ({
107-
validator(_, value) {
108-
if (value.isAfter(dayjs())) {
109-
return Promise.resolve();
110-
}
111-
return Promise.reject(
112-
new Error(t('deployment.TokenExpiredDateError')),
113-
);
108+
<BAIFlex direction="row" align="stretch" justify="around">
109+
<Form.Item
110+
name="expiryOption"
111+
label={t('deployment.ExpirationDate')}
112+
rules={[
113+
{
114+
required: true,
115+
message: t('deployment.PleaseSelectTime'),
114116
},
115-
}),
116-
]}
117-
>
118-
<DatePicker
119-
showTime
120-
format="YYYY-MM-DD HH:mm:ss"
121-
style={{ width: 200 }}
122-
/>
117+
]}
118+
>
119+
<Select
120+
style={{ width: 200 }}
121+
options={[
122+
{ value: '7days', label: t('general.Days', { num: 7 }) },
123+
{ value: '30days', label: t('general.Days', { num: 30 }) },
124+
{ value: '90days', label: t('general.Days', { num: 90 }) },
125+
{ value: 'custom', label: t('deployment.Custom') },
126+
]}
127+
/>
128+
</Form.Item>
129+
</BAIFlex>
130+
<Form.Item dependencies={['expiryOption']} noStyle>
131+
{({ getFieldValue }) =>
132+
getFieldValue('expiryOption') === 'custom' ? (
133+
<BAIFlex direction="row" align="stretch" justify="around">
134+
<Form.Item
135+
name="datetime"
136+
label={t('deployment.CustomExpirationDate')}
137+
rules={[
138+
{
139+
type: 'object' as const,
140+
required: true,
141+
message: t('deployment.PleaseSelectTime'),
142+
},
143+
() => ({
144+
validator(_, value) {
145+
if (value && value.isAfter(dayjs())) {
146+
return Promise.resolve();
147+
}
148+
return Promise.reject(
149+
new Error(t('deployment.TokenExpiredDateError')),
150+
);
151+
},
152+
}),
153+
]}
154+
>
155+
<DatePicker
156+
showTime
157+
format="YYYY-MM-DD HH:mm:ss"
158+
style={{ width: 200 }}
159+
/>
160+
</Form.Item>
161+
</BAIFlex>
162+
) : null
163+
}
123164
</Form.Item>
124165
</BAIFlex>
125166
</Form>

0 commit comments

Comments
 (0)