Skip to content

Commit 5ac8303

Browse files
committed
feat(FR-1727): customizing optional text in form (#4712)
resolves #4711 (FR-1727) This PR enhances the form field labeling by explicitly marking optional fields with "(optional)" text instead of using the default Ant Design approach. The changes include: 1. Updated the `requiredMark` configuration in `BAIConfigProvider` to display "(optional)" text for non-required fields 2. Added a new translation key "Optional" across all language files 3. Improved the layout of the Resource Preset Setting form by: - Using vertical layout for better readability - Adding consistent styling for input fields - Removing unnecessary Row/Col structure These changes make it clearer to users which fields are optional vs. required in forms throughout the application. ![CleanShot 2025-11-26 at 12.50.59@2x.png](https://app.graphite.com/user-attachments/assets/4d77b838-bdcd-4eac-9680-9af3e524b7d5.png) **Checklist:** (if applicable) - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after
1 parent 6616480 commit 5ac8303

File tree

23 files changed

+147
-100
lines changed

23 files changed

+147
-100
lines changed

react/src/components/DefaultProviders.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { StyleProvider, createCache } from '@ant-design/cssinjs';
1515
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1616
import { useSessionStorageState, useUpdateEffect } from 'ahooks';
1717
import { App, AppProps, theme, Typography } from 'antd';
18-
import { BAIConfigProvider } from 'backend.ai-ui';
18+
import { BAIConfigProvider, BAIText } from 'backend.ai-ui';
1919
import dayjs from 'dayjs';
2020
import 'dayjs/locale/de';
2121
import 'dayjs/locale/el';
@@ -208,6 +208,8 @@ const DefaultProvidersForWebComponent: React.FC<DefaultProvidersProps> = ({
208208
}) => {
209209
const cache = useMemo(() => createCache(), []);
210210
const [lang] = useCurrentLanguage();
211+
const { t } = useTranslation();
212+
const { token } = theme.useToken();
211213

212214
const [userCustomThemeConfig] = useBAISettingUserState('custom_theme_config');
213215
const [isThemePreviewMode] = useSessionStorageState('isThemePreviewMode', {
@@ -287,7 +289,22 @@ const DefaultProvidersForWebComponent: React.FC<DefaultProvidersProps> = ({
287289
}}
288290
clientPromise={backendaiClientPromise}
289291
form={{
290-
requiredMark: 'optional',
292+
requiredMark: (label, { required }) => (
293+
<>
294+
{label}
295+
{!required && (
296+
<BAIText
297+
type="secondary"
298+
style={{
299+
marginLeft: token.marginXXS,
300+
wordBreak: 'keep-all',
301+
}}
302+
>
303+
{`(${t('general.Optional')})`}
304+
</BAIText>
305+
)}
306+
</>
307+
),
291308
}}
292309
anonymousClientFactory={createAnonymousBackendaiClient}
293310
>
@@ -355,6 +372,8 @@ export const DefaultProvidersForReactRoot: React.FC<
355372
Partial<DefaultProvidersProps>
356373
> = ({ children }) => {
357374
const [lang] = useCurrentLanguage();
375+
const { t } = useTranslation();
376+
const { token } = theme.useToken();
358377
const { isDarkMode } = useThemeMode();
359378

360379
const [userCustomThemeConfig] = useBAISettingUserState('custom_theme_config');
@@ -408,7 +427,22 @@ export const DefaultProvidersForReactRoot: React.FC<
408427
clientPromise={backendaiClientPromise}
409428
anonymousClientFactory={createAnonymousBackendaiClient}
410429
form={{
411-
requiredMark: 'optional',
430+
requiredMark: (label, { required }) => (
431+
<>
432+
{label}
433+
{!required && (
434+
<BAIText
435+
type="secondary"
436+
style={{
437+
marginLeft: token.marginXXS,
438+
wordBreak: 'keep-all',
439+
}}
440+
>
441+
{`(${t('general.Optional')})`}
442+
</BAIText>
443+
)}
444+
</>
445+
),
412446
}}
413447
>
414448
<QueryParamProvider adapter={ReactRouter6Adapter}>

react/src/components/ResourcePresetSettingModal.tsx

Lines changed: 89 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useResourceSlots, useResourceSlotsDetails } from '../hooks/backendai';
1414
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
1515
import DynamicUnitInputNumber from './DynamicUnitInputNumber';
1616
import ResourceGroupSelect from './ResourceGroupSelect';
17-
import { App, Col, Form, FormInstance, Input, InputNumber, Row } from 'antd';
17+
import { App, Form, FormInstance, Input, InputNumber } from 'antd';
1818
import { BAIModal, BAIModalProps } from 'backend.ai-ui';
1919
import _ from 'lodash';
2020
import React, { Fragment, useRef } from 'react';
@@ -295,104 +295,96 @@ const ResourcePresetSettingModal: React.FC<ResourcePresetSettingModalProps> = ({
295295
/>
296296
</Form.Item>
297297
)}
298-
<Form.Item label={t('resourcePreset.ResourcePreset')} required>
299-
<Row gutter={16}>
300-
{_.map(
301-
_.chunk(_.keys(resourceSlots), 2),
302-
(resourceSlotKeys, index) => (
303-
<Fragment key={index}>
304-
{_.map(resourceSlotKeys, (resourceSlotKey) => (
305-
<Col
306-
span={12}
307-
key={resourceSlotKey}
308-
style={{ alignSelf: 'end' }}
309-
>
310-
<Form.Item
311-
label={
298+
<Form.Item
299+
label={t('resourcePreset.ResourcePreset')}
300+
required
301+
layout="vertical"
302+
>
303+
{_.map(
304+
_.chunk(_.keys(resourceSlots), 2),
305+
(resourceSlotKeys, index) => (
306+
<Fragment key={index}>
307+
{_.map(resourceSlotKeys, (resourceSlotKey) => (
308+
<Form.Item
309+
label={
310+
_.get(mergedResourceSlots, resourceSlotKey)
311+
?.description || resourceSlotKey
312+
}
313+
name={['resource_slots', resourceSlotKey]}
314+
rules={[
315+
_.includes(['cpu', 'mem'], resourceSlotKey)
316+
? {
317+
required: true,
318+
message: t('data.explorer.ValueRequired'),
319+
}
320+
: {},
321+
{
322+
validator(__, value) {
323+
if (
324+
value &&
325+
_.includes(resourceSlotKey, 'mem') &&
326+
// @ts-ignore
327+
convertToBinaryUnit(value, 'p').number >
328+
// @ts-ignore
329+
convertToBinaryUnit('300p', 'p').number
330+
) {
331+
return Promise.reject(
332+
new Error(
333+
'Memory size should be less than 300 PiB',
334+
),
335+
);
336+
}
337+
return Promise.resolve();
338+
},
339+
},
340+
]}
341+
>
342+
{_.includes(resourceSlotKey, 'mem') ? (
343+
<DynamicUnitInputNumber style={{ width: '100%' }} />
344+
) : (
345+
<InputNumber
346+
stringMode
347+
min={resourceSlotKey === 'cpu' ? 1 : 0}
348+
step={_.includes(resourceSlotKey, '.shares') ? 0.1 : 1}
349+
addonAfter={
312350
_.get(mergedResourceSlots, resourceSlotKey)
313-
?.description || resourceSlotKey
351+
?.display_unit
314352
}
315-
name={['resource_slots', resourceSlotKey]}
316-
rules={[
317-
_.includes(['cpu', 'mem'], resourceSlotKey)
318-
? {
319-
required: true,
320-
message: t('data.explorer.ValueRequired'),
321-
}
322-
: {},
323-
{
324-
validator(__, value) {
325-
if (
326-
value &&
327-
_.includes(resourceSlotKey, 'mem') &&
328-
// @ts-ignore
329-
convertToBinaryUnit(value, 'p').number >
330-
// @ts-ignore
331-
convertToBinaryUnit('300p', 'p').number
332-
) {
333-
return Promise.reject(
334-
new Error(
335-
'Memory size should be less than 300 PiB',
336-
),
337-
);
338-
}
339-
return Promise.resolve();
340-
},
341-
},
342-
]}
343-
>
344-
{_.includes(resourceSlotKey, 'mem') ? (
345-
<DynamicUnitInputNumber />
346-
) : (
347-
<InputNumber
348-
stringMode
349-
min={resourceSlotKey === 'cpu' ? 1 : 0}
350-
step={
351-
_.includes(resourceSlotKey, '.shares') ? 0.1 : 1
352-
}
353-
addonAfter={
354-
_.get(mergedResourceSlots, resourceSlotKey)
355-
?.display_unit
356-
}
357-
style={{ width: '100%' }}
358-
/>
359-
)}
360-
</Form.Item>
361-
</Col>
362-
))}
363-
</Fragment>
364-
),
365-
)}
366-
<Col span={12} style={{ alignSelf: 'end' }}>
367-
<Form.Item
368-
label={t('resourcePreset.SharedMemory')}
369-
name="shared_memory"
370-
dependencies={[['resource_slots', 'mem']]}
371-
rules={[
372-
({ getFieldValue }) => ({
373-
validator(__, value) {
374-
if (
375-
value &&
376-
getFieldValue('resource_slots')?.mem &&
377-
(convertToBinaryUnit(
378-
getFieldValue('resource_slots')?.mem,
379-
'',
380-
)?.number ?? 0) <
381-
(convertToBinaryUnit(value, '')?.number ?? 0)
382-
) {
383-
return Promise.reject(
384-
t('resourcePreset.MemoryShouldBeLargerThanSHMEM'),
385-
);
386-
}
387-
return Promise.resolve();
388-
},
389-
}),
390-
]}
391-
>
392-
<DynamicUnitInputNumber max="7.999p" />
393-
</Form.Item>
394-
</Col>
395-
</Row>
353+
style={{ width: '100%' }}
354+
/>
355+
)}
356+
</Form.Item>
357+
))}
358+
</Fragment>
359+
),
360+
)}
361+
<Form.Item
362+
label={t('resourcePreset.SharedMemory')}
363+
name="shared_memory"
364+
dependencies={[['resource_slots', 'mem']]}
365+
rules={[
366+
({ getFieldValue }) => ({
367+
validator(__, value) {
368+
if (
369+
value &&
370+
getFieldValue('resource_slots')?.mem &&
371+
(convertToBinaryUnit(
372+
getFieldValue('resource_slots')?.mem,
373+
'',
374+
)?.number ?? 0) <
375+
(convertToBinaryUnit(value, '')?.number ?? 0)
376+
) {
377+
return Promise.reject(
378+
t('resourcePreset.MemoryShouldBeLargerThanSHMEM'),
379+
);
380+
}
381+
return Promise.resolve();
382+
},
383+
}),
384+
]}
385+
>
386+
<DynamicUnitInputNumber max="7.999p" style={{ width: '100%' }} />
387+
</Form.Item>
396388
</Form.Item>
397389
</Form>
398390
</BAIModal>

resources/i18n/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@
706706
"NavigateToDetailPage": "Zur Detailseite gehen",
707707
"NewPassword": "Neues Kennwort",
708708
"None": "Keine",
709+
"Optional": "Optional",
709710
"Password": "Passwort",
710711
"RemainingLoginSessionTime": "Zeit bis zur automatischen Abmeldung",
711712
"ResourceGroup": "Ressourcengruppe",

resources/i18n/el.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@
703703
"NavigateToDetailPage": "Μεταβείτε στη σελίδα λεπτομερειών",
704704
"NewPassword": "νέος κωδικός πρόσβασης",
705705
"None": "Κανένα",
706+
"Optional": "Προαιρετικό",
706707
"Password": "Κωδικός πρόσβασης",
707708
"RemainingLoginSessionTime": "Χρόνος μέχρι την αυτόματη αποσύνδεση",
708709
"ResourceGroup": "Ομάδα πόρων",

resources/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@
711711
"NavigateToDetailPage": "Go to the details page",
712712
"NewPassword": "New Password",
713713
"None": "None",
714+
"Optional": "optional",
714715
"Password": "Password",
715716
"RemainingLoginSessionTime": "Time until auto logout",
716717
"ResourceGroup": "Resource Group",

resources/i18n/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@
706706
"NavigateToDetailPage": "Ir a la página de detalles",
707707
"NewPassword": "Nueva contraseña",
708708
"None": "Ninguno",
709+
"Optional": "opcional",
709710
"Password": "Contraseña",
710711
"RemainingLoginSessionTime": "Tiempo hasta el cierre de sesión automático",
711712
"ResourceGroup": "Grupo de recursos",

resources/i18n/fi.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@
706706
"NavigateToDetailPage": "Siirry yksityiskohtien sivulle",
707707
"NewPassword": "Uusi salasana",
708708
"None": "Ei ole",
709+
"Optional": "valinnainen",
709710
"Password": "Salasana",
710711
"RemainingLoginSessionTime": "Aika automaattiseen uloskirjautumiseen asti",
711712
"ResourceGroup": "Resurssiryhmä",

resources/i18n/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@
706706
"NavigateToDetailPage": "Aller à la page de détails",
707707
"NewPassword": "nouveau mot de passe",
708708
"None": "Aucun",
709+
"Optional": "facultatif",
709710
"Password": "Mot de passe",
710711
"RemainingLoginSessionTime": "Délai avant déconnexion automatique",
711712
"ResourceGroup": "Groupe de ressources",

resources/i18n/id.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@
705705
"NavigateToDetailPage": "Buka halaman detail",
706706
"NewPassword": "Kata Sandi Baru",
707707
"None": "Tidak ada",
708+
"Optional": "opsional",
708709
"Password": "Kata sandi",
709710
"RemainingLoginSessionTime": "Waktu Hingga Keluar Otomatis",
710711
"ResourceGroup": "Grup Sumber Daya",

resources/i18n/it.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@
705705
"NavigateToDetailPage": "Vai alla pagina dei dettagli",
706706
"NewPassword": "nuova password",
707707
"None": "Nessuno",
708+
"Optional": "opzionale",
708709
"Password": "Parola d'ordine",
709710
"RemainingLoginSessionTime": "Tempo fino alla disconnessione automatica",
710711
"ResourceGroup": "Gruppo di risorse",

0 commit comments

Comments
 (0)