Skip to content

Commit 330e05a

Browse files
committed
feat(FR-1120): add duplicate check logic and modify validation message to service and session name input
1 parent 7479856 commit 330e05a

26 files changed

+271
-135
lines changed

react/src/components/ComputeSessionNodeItems/EditableSessionName.tsx

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { EditableSessionNameDuplicatedCheckQuery } from '../../__generated__/EditableSessionNameDuplicatedCheckQuery.graphql';
21
import { EditableSessionNameFragment$key } from '../../__generated__/EditableSessionNameFragment.graphql';
32
import { EditableSessionNameRefetchQuery } from '../../__generated__/EditableSessionNameRefetchQuery.graphql';
43
import { useBaiSignedRequestWithPromise } from '../../helper';
54
import { useCurrentUserInfo } from '../../hooks/backendai';
65
import { useTanMutation } from '../../hooks/reactQueryAlias';
76
import { useCurrentProjectValue } from '../../hooks/useCurrentProject';
8-
import { getSessionNameRules } from '../SessionNameFormItem';
7+
import { useValidateSessionName } from '../../hooks/useValidateSessionName';
98
import { theme, Form, Input, App } from 'antd';
109
import Text, { TextProps } from 'antd/es/typography/Text';
1110
import Title, { TitleProps } from 'antd/es/typography/Title';
@@ -35,7 +34,6 @@ const EditableSessionName: React.FC<EditableSessionNameProps> = ({
3534
}) => {
3635
const relayEvn = useRelayEnvironment();
3736
const currentProject = useCurrentProjectValue();
38-
3937
const session = useFragment(
4038
graphql`
4139
fragment EditableSessionNameFragment on ComputeSessionNode {
@@ -49,6 +47,8 @@ const EditableSessionName: React.FC<EditableSessionNameProps> = ({
4947
`,
5048
sessionFrgmt,
5149
);
50+
const validationRules = useValidateSessionName();
51+
5252
const [optimisticName, setOptimisticName] = useState(session.name);
5353
const [userInfo] = useCurrentUserInfo();
5454

@@ -166,48 +166,7 @@ const EditableSessionName: React.FC<EditableSessionNameProps> = ({
166166
<Form.Item
167167
name="sessionName"
168168
validateDebounce={1000}
169-
rules={[
170-
...getSessionNameRules(t),
171-
{
172-
validator: async (rule, value) => {
173-
if (value === session.name) {
174-
return Promise.resolve();
175-
}
176-
const hasSameName =
177-
await fetchQuery<EditableSessionNameDuplicatedCheckQuery>(
178-
relayEvn,
179-
graphql`
180-
query EditableSessionNameDuplicatedCheckQuery(
181-
$projectId: UUID!
182-
$filter: String
183-
) {
184-
compute_session_nodes(
185-
project_id: $projectId
186-
filter: $filter
187-
) {
188-
count
189-
}
190-
}
191-
`,
192-
{
193-
projectId: currentProject.id,
194-
filter: `status != "TERMINATED" & status != "CANCELLED" & name == "${value}"`,
195-
},
196-
)
197-
.toPromise()
198-
.then((data) => {
199-
return data?.compute_session_nodes?.count !== 0;
200-
})
201-
.catch(() => {
202-
// ignore duplicated check error
203-
return false;
204-
});
205-
return hasSameName
206-
? Promise.reject(t('session.launcher.SessionAlreadyExists'))
207-
: Promise.resolve();
208-
},
209-
},
210-
]}
169+
rules={validationRules}
211170
style={{
212171
margin: 0,
213172
}}

react/src/components/ServiceLauncherPageContent.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { KnownAcceleratorResourceSlotName } from '../hooks/backendai';
1717
import { useSuspenseTanQuery, useTanMutation } from '../hooks/reactQueryAlias';
1818
import { useCurrentResourceGroupState } from '../hooks/useCurrentProject';
19+
import { useValidateServiceName } from '../hooks/useValidateServiceName';
1920
import BAIModal, { DEFAULT_BAI_MODAL_Z_INDEX } from './BAIModal';
2021
import EnvVarFormList, { EnvVarFormListValue } from './EnvVarFormList';
2122
import Flex from './Flex';
@@ -134,7 +135,6 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
134135
}) => {
135136
const { token } = theme.useToken();
136137
const { message } = App.useApp();
137-
138138
const { t } = useTranslation();
139139

140140
const [{ model }] = useQueryParams({
@@ -145,7 +145,7 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
145145
const baiClient = useSuspendedBackendaiClient();
146146
const baiRequestWithPromise = useBaiSignedRequestWithPromise();
147147
const currentDomain = useCurrentDomainValue();
148-
148+
const validationRules = useValidateServiceName();
149149
const [isOpenServiceValidationModal, setIsOpenServiceValidationModal] =
150150
useState(false);
151151

@@ -636,7 +636,6 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
636636
};
637637

638638
const [validateServiceData, setValidateServiceData] = useState<any>();
639-
640639
const getAIAcceleratorWithStringifiedKey = (resourceSlot: any) => {
641640
if (Object.keys(resourceSlot).length <= 0) {
642641
return undefined;
@@ -751,31 +750,8 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
751750
<Form.Item
752751
label={t('modelService.ServiceName')}
753752
name="serviceName"
754-
rules={[
755-
{
756-
min: 4,
757-
message: t('modelService.ServiceNameMinLength'),
758-
type: 'string',
759-
},
760-
{
761-
max: 24,
762-
message: t('modelService.ServiceNameMaxLength'),
763-
type: 'string',
764-
},
765-
{
766-
pattern: /^(?:[^-]|[^-].*[^-])$/,
767-
message: t(
768-
'modelService.ServiceNameCannotStartWithHyphen',
769-
),
770-
},
771-
{
772-
pattern: /^[\w-]+$/,
773-
message: t('modelService.ServiceNameRule'),
774-
},
775-
{
776-
required: true,
777-
},
778-
]}
753+
validateDebounce={500}
754+
rules={validationRules}
779755
>
780756
<Input disabled={!!endpoint} />
781757
</Form.Item>

react/src/components/SessionNameFormItem.tsx

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import { useValidateSessionName } from '../hooks/useValidateSessionName';
12
import { Form, FormItemProps, Input } from 'antd';
2-
import { TFunction } from 'i18next';
3-
import _ from 'lodash';
43
import React from 'react';
54
import { useTranslation } from 'react-i18next';
65

@@ -10,54 +9,19 @@ export interface SessionNameFormItemValue {
109
sessionName: string;
1110
}
1211

13-
export const getSessionNameRules = (
14-
t: TFunction,
15-
): Exclude<FormItemProps['rules'], undefined> => [
16-
{
17-
min: 4,
18-
message: t('session.validation.SessionNameTooShort'),
19-
},
20-
{
21-
max: 64,
22-
message: t('session.validation.SessionNameTooLong64'),
23-
},
24-
{
25-
validator(f, value) {
26-
if (_.isEmpty(value)) {
27-
return Promise.resolve();
28-
}
29-
if (!/^\w/.test(value)) {
30-
return Promise.reject(
31-
t('session.validation.SessionNameShouldStartWith'),
32-
);
33-
}
34-
35-
if (!/^[\w.-]*$/.test(value)) {
36-
return Promise.reject(
37-
t('session.validation.SessionNameInvalidCharacter'),
38-
);
39-
}
40-
41-
if (!/\w$/.test(value) && value.length >= 4) {
42-
return Promise.reject(t('session.validation.SessionNameShouldEndWith'));
43-
}
44-
return Promise.resolve();
45-
},
46-
},
47-
];
48-
4912
const SessionNameFormItem: React.FC<SessionNameFormItemProps> = ({
5013
...formItemProps
5114
}) => {
52-
/* TODO: check SessionNameAlreadyExist */
5315
const { t } = useTranslation();
16+
const validationRules = useValidateSessionName();
5417
return (
5518
<Form.Item
5619
label={t('session.launcher.SessionName')}
5720
name="sessionName"
21+
validateDebounce={500}
5822
// Original rule : /^(?=.{4,64}$)\w[\w.-]*\w$/
5923
// https://github.com/lablup/backend.ai/blob/main/src/ai/backend/manager/api/session.py#L355-L356
60-
rules={getSessionNameRules(t)}
24+
rules={validationRules}
6125
{...formItemProps}
6226
>
6327
<Input allowClear autoComplete="off" />
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useValidateServiceNameQuery } from '../__generated__/useValidateServiceNameQuery.graphql';
2+
import { useCurrentProjectValue } from './useCurrentProject';
3+
import { FormItemProps } from 'antd';
4+
import type { RuleObject } from 'antd/es/form';
5+
import _ from 'lodash';
6+
import { useTranslation } from 'react-i18next';
7+
import { graphql, useRelayEnvironment, fetchQuery } from 'react-relay';
8+
9+
export const useValidateServiceName = (): Exclude<
10+
FormItemProps['rules'],
11+
undefined
12+
> => {
13+
const { t } = useTranslation();
14+
const relayEvn = useRelayEnvironment();
15+
const currentProject = useCurrentProjectValue();
16+
return [
17+
{
18+
min: 4,
19+
message: t('session.validation.SessionNameTooShort'),
20+
},
21+
{
22+
max: 24,
23+
message: t('modelService.ServiceNameMaxLength'),
24+
type: 'string',
25+
},
26+
{
27+
validator(f: RuleObject, value: string) {
28+
if (_.isEmpty(value)) {
29+
return Promise.resolve();
30+
}
31+
if (!/^\w/.test(value)) {
32+
return Promise.reject(
33+
t('session.validation.SessionNameShouldStartWith'),
34+
);
35+
}
36+
37+
if (!/\w$/.test(value)) {
38+
return Promise.reject(
39+
t('session.validation.SessionNameShouldEndWith'),
40+
);
41+
}
42+
43+
if (!/^[\w.-]*$/.test(value)) {
44+
return Promise.reject(
45+
t('session.validation.SessionNameInvalidCharacter'),
46+
);
47+
}
48+
return Promise.resolve();
49+
},
50+
},
51+
{
52+
validator: async (f: RuleObject, value: string) => {
53+
if (!value) return Promise.resolve();
54+
const hasSameName = await fetchQuery<useValidateServiceNameQuery>(
55+
relayEvn,
56+
graphql`
57+
query useValidateServiceNameQuery(
58+
$projectID: UUID!
59+
$filter: String
60+
$offset: Int!
61+
$limit: Int!
62+
) {
63+
endpoint_list(
64+
project: $projectID
65+
filter: $filter
66+
offset: $offset
67+
limit: $limit
68+
) {
69+
total_count
70+
}
71+
}
72+
`,
73+
{
74+
projectID: currentProject.id,
75+
filter: `name == "${value}"`,
76+
offset: 0,
77+
limit: 1,
78+
},
79+
)
80+
.toPromise()
81+
.then((data) => (data?.endpoint_list?.total_count ?? 0) > 0)
82+
.catch(() => {
83+
return false;
84+
});
85+
return hasSameName
86+
? Promise.reject(t('session.launcher.SessionAlreadyExists'))
87+
: Promise.resolve();
88+
},
89+
},
90+
{
91+
required: true,
92+
},
93+
];
94+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useValidateSessionNameQuery } from '../__generated__/useValidateSessionNameQuery.graphql';
2+
import { useCurrentProjectValue } from './useCurrentProject';
3+
import type { RuleObject } from 'antd/es/form';
4+
import _ from 'lodash';
5+
import { useTranslation } from 'react-i18next';
6+
import { graphql, useRelayEnvironment, fetchQuery } from 'react-relay';
7+
8+
export const useValidateSessionName = () => {
9+
const { t } = useTranslation();
10+
const relayEvn = useRelayEnvironment();
11+
const currentProject = useCurrentProjectValue();
12+
return [
13+
{
14+
min: 4,
15+
message: t('session.validation.SessionNameTooShort'),
16+
},
17+
{
18+
max: 64,
19+
message: t('session.validation.SessionNameTooLong64'),
20+
},
21+
{
22+
validator(f: RuleObject, value: string) {
23+
if (_.isEmpty(value)) {
24+
return Promise.resolve();
25+
}
26+
if (!/^\w/.test(value)) {
27+
return Promise.reject(
28+
t('session.validation.SessionNameShouldStartWith'),
29+
);
30+
}
31+
32+
if (!/\w$/.test(value)) {
33+
return Promise.reject(
34+
t('session.validation.SessionNameShouldEndWith'),
35+
);
36+
}
37+
38+
if (!/^[\w.-]*$/.test(value)) {
39+
return Promise.reject(
40+
t('session.validation.SessionNameInvalidCharacter'),
41+
);
42+
}
43+
return Promise.resolve();
44+
},
45+
},
46+
{
47+
validator: async (f: RuleObject, value: string) => {
48+
const hasSameName = await fetchQuery<useValidateSessionNameQuery>(
49+
relayEvn,
50+
graphql`
51+
query useValidateSessionNameQuery(
52+
$projectId: UUID!
53+
$filter: String
54+
) {
55+
compute_session_nodes(project_id: $projectId, filter: $filter) {
56+
count
57+
}
58+
}
59+
`,
60+
{
61+
projectId: currentProject.id,
62+
filter: `status != "TERMINATED" & status != "CANCELLED" & name == "${value}"`,
63+
},
64+
)
65+
.toPromise()
66+
.then((data) => {
67+
return data?.compute_session_nodes?.count !== 0;
68+
})
69+
.catch(() => {
70+
return false;
71+
});
72+
return hasSameName
73+
? Promise.reject(t('session.launcher.SessionAlreadyExists'))
74+
: Promise.resolve();
75+
},
76+
},
77+
];
78+
};

0 commit comments

Comments
 (0)