Skip to content

Commit 8c7a9c4

Browse files
authored
refactor(ui): migrate Add Custom Property from MUI to ui-core-components (#29407)
* refactor(ui): migrate Add Custom Property from MUI to ui-core-components * header styling * address gitar * fix checkstyle * fix dropdown select * fix X alignment
1 parent ed7f327 commit 8c7a9c4

10 files changed

Lines changed: 191 additions & 563 deletions

File tree

openmetadata-ui-core-components/src/main/resources/ui/src/components/application/slideout-menus/slideout-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const Header = ({
152152
)}>
153153
{children}
154154
<CloseButton
155-
className="tw:absolute tw:top-3 tw:right-3 tw:shrink-0"
155+
className="tw:absolute tw:top-1/2 tw:right-3 tw:shrink-0 tw:-translate-y-1/2"
156156
size="md"
157157
onClick={onClose}
158158
/>

openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/CustomProperties.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3697,7 +3697,7 @@ test.describe('Custom property name validation', () => {
36973697
await page.click('[data-testid="add-field-button"]');
36983698
});
36993699

3700-
const nameInput = '[data-testid="name"] input';
3700+
const nameInput = '[data-testid="name"]';
37013701
const nameError = '#name_help';
37023702

37033703
test('should show error when name starts with a non-alphanumeric character', async ({

openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ export const addCustomPropertiesForEntity = async ({
660660

661661
// Validation check — name must start with a letter/number and must not contain: " * : ^ $ \ < > & ~ /
662662
await page.fill(
663-
'[data-testid="name"] input',
663+
'[data-testid="name"]',
664664
CUSTOM_PROPERTY_INVALID_NAMES.DISALLOWED_COLON
665665
);
666666

@@ -669,14 +669,14 @@ export const addCustomPropertiesForEntity = async ({
669669
);
670670

671671
// Correct name
672-
await page.fill('[data-testid="name"] input', propertyName);
672+
await page.fill('[data-testid="name"]', propertyName);
673673

674674
// displayName
675-
await page.fill('[data-testid="display-name"] input', propertyName);
675+
await page.fill('[data-testid="display-name"]', propertyName);
676676

677677
// Select custom type
678678
await page.locator('[data-testid="propertyType"]').click();
679-
await page.getByTitle(`${customType}`, { exact: true }).click();
679+
await page.getByRole('option', { name: customType, exact: true }).click();
680680

681681
// Enum configuration
682682
if (customType === 'Enum' && enumConfig) {
@@ -750,7 +750,7 @@ export const addCustomPropertiesForEntity = async ({
750750
await page.keyboard.type(customPropertyData.description, { delay: 50 });
751751

752752
// Click on name field to blur description and trigger validation without closing modal
753-
await page.click('[data-testid="name"] input');
753+
await page.click('[data-testid="name"]');
754754

755755
await expect(page.locator('#propertyType_help')).not.toBeVisible();
756756
await expect(page.locator('#description_help')).not.toBeVisible();

openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.test.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,6 @@ jest.mock('../../../common/ServiceDocPanel/ServiceDocPanel', () =>
232232
jest.fn().mockImplementation(() => <div>ServiceDocPanel.component</div>)
233233
);
234234

235-
jest.mock('../../../common/MUISelect/MUISelect', () =>
236-
jest.fn(({ label }) => (
237-
<div data-testid="propertyType">
238-
<label>{label}</label>
239-
<select />
240-
</div>
241-
))
242-
);
243-
244235
describe('Test Add Custom Property Component', () => {
245236
it('Should render the child components', async () => {
246237
render(<AddCustomProperty />);

openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.tsx

Lines changed: 124 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@
1111
* limitations under the License.
1212
*/
1313

14-
import { Badge } from '@openmetadata/ui-core-components';
14+
import {
15+
Badge,
16+
Button as CoreButton,
17+
SelectItemType,
18+
SlideoutMenu,
19+
Toggle,
20+
Typography,
21+
} from '@openmetadata/ui-core-components';
1522
import { Button, Col, Form, FormInstance, Row } from 'antd';
1623
import { AxiosError } from 'axios';
1724
import { ReactComponent as ColumnIcon } from '../../../../assets/svg/ic-column.svg';
18-
import MuiDrawer from '../../../common/MuiDrawer/MuiDrawer';
1925

2026
import { isArray, isUndefined, map, omit, omitBy, startCase } from 'lodash';
2127
import { FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
@@ -92,6 +98,7 @@ const AddCustomProperty = ({
9298
const [activeField, setActiveField] = useState<string>('');
9399
const [isCreating, setIsCreating] = useState<boolean>(false);
94100
const [isFormInvalid, setIsFormInvalid] = useState<boolean>(true);
101+
const [showSidePanel, setShowSidePanel] = useState<boolean>(false);
95102

96103
const watchedPropertyType = Form.useWatch('propertyType', form);
97104

@@ -118,7 +125,7 @@ const AddCustomProperty = ({
118125
[entityType]
119126
);
120127

121-
const propertyTypeOptions = useMemo(() => {
128+
const propertyTypeOptions: SelectItemType[] = useMemo(() => {
122129
return map(propertyTypes, (type) => {
123130
const Icon =
124131
CUSTOM_PROPERTIES_ICON_MAP[
@@ -129,15 +136,9 @@ const AddCustomProperty = ({
129136
const title = startCase(getEntityName(type).replaceAll('-cp', ''));
130137

131138
return {
132-
searchField: title,
133-
key: type.name,
134-
label: (
135-
<div className="d-flex gap-2 items-center" title={title}>
136-
{Icon && <Icon width={20} />}
137-
<span>{title}</span>
138-
</div>
139-
),
140-
value: type.id,
139+
id: type.id ?? '',
140+
label: title,
141+
icon: Icon,
141142
};
142143
});
143144
}, [propertyTypes]);
@@ -146,32 +147,30 @@ const AddCustomProperty = ({
146147
hasEnumConfig,
147148
hasFormatConfig,
148149
hasEntityReferenceConfig,
149-
watchedOption,
150+
watchedTypeName,
150151
hasTableTypeConfig,
151152
} = useMemo(() => {
152-
const watchedOption = propertyTypeOptions.find(
153-
(option) => option.value === watchedPropertyType
154-
);
155-
const watchedOptionKey = watchedOption?.key ?? '';
153+
const watchedTypeName =
154+
propertyTypes.find((type) => type.id === watchedPropertyType)?.name ?? '';
156155

157-
const hasEnumConfig = watchedOptionKey === 'enum';
156+
const hasEnumConfig = watchedTypeName === 'enum';
158157

159-
const hasTableTypeConfig = watchedOptionKey === TABLE_TYPE_CUSTOM_PROPERTY;
158+
const hasTableTypeConfig = watchedTypeName === TABLE_TYPE_CUSTOM_PROPERTY;
160159

161160
const hasFormatConfig =
162-
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
161+
PROPERTY_TYPES_WITH_FORMAT.includes(watchedTypeName);
163162

164163
const hasEntityReferenceConfig =
165-
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(watchedOptionKey);
164+
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(watchedTypeName);
166165

167166
return {
168167
hasEnumConfig,
169168
hasFormatConfig,
170169
hasEntityReferenceConfig,
171-
watchedOption,
170+
watchedTypeName,
172171
hasTableTypeConfig,
173172
};
174-
}, [watchedPropertyType, propertyTypeOptions]);
173+
}, [watchedPropertyType, propertyTypes]);
175174

176175
const fetchPropertyType = async () => {
177176
try {
@@ -247,13 +246,12 @@ const AddCustomProperty = ({
247246
}, [t]);
248247

249248
const supportedFormats = useMemo(() => {
250-
const propertyName = watchedOption?.key ?? '';
251-
252249
return (
253-
SUPPORTED_FORMAT_MAP[propertyName as keyof typeof SUPPORTED_FORMAT_MAP] ??
254-
[]
250+
SUPPORTED_FORMAT_MAP[
251+
watchedTypeName as keyof typeof SUPPORTED_FORMAT_MAP
252+
] ?? []
255253
);
256-
}, [watchedOption]);
254+
}, [watchedTypeName]);
257255

258256
const formFields: FieldProp[] = useMemo(
259257
() => [
@@ -262,7 +260,7 @@ const AddCustomProperty = ({
262260
required: true,
263261
label: t('label.name'),
264262
id: 'root/name',
265-
type: FieldTypes.TEXT_MUI,
263+
type: FieldTypes.UT_TEXT,
266264
props: {
267265
'data-testid': 'name',
268266
autoComplete: 'off',
@@ -289,7 +287,7 @@ const AddCustomProperty = ({
289287
label: t('label.display-name'),
290288
required: false,
291289
placeholder: t('label.display-name'),
292-
type: FieldTypes.TEXT_MUI,
290+
type: FieldTypes.UT_TEXT,
293291
props: {
294292
'data-testid': 'display-name',
295293
},
@@ -299,20 +297,13 @@ const AddCustomProperty = ({
299297
required: true,
300298
label: t('label.type'),
301299
id: 'root/propertyType',
302-
type: FieldTypes.SELECT_MUI,
303-
placeholder: t('label.select-property-type'),
300+
type: FieldTypes.UT_SELECT,
301+
placeholder: `${t('label.select-field', {
302+
field: t('label.type'),
303+
})}`,
304304
props: {
305305
'data-testid': 'propertyType',
306-
options: propertyTypeOptions,
307-
placeholder: `${t('label.select-field', {
308-
field: t('label.type'),
309-
})}`,
310-
showSearch: true,
311-
filterOption: (input: string, option: { searchField: string }) => {
312-
return (option?.searchField ?? '')
313-
.toLowerCase()
314-
.includes(input.toLowerCase());
315-
},
306+
items: propertyTypeOptions,
316307
},
317308
},
318309
...(entityType === EntityType.TABLE_COLUMN
@@ -361,6 +352,8 @@ const AddCustomProperty = ({
361352
placeholder: t('label.enum-value-plural'),
362353
open: false,
363354
className: 'trim-select',
355+
getPopupContainer: (triggerNode: HTMLElement) =>
356+
triggerNode.parentElement ?? document.body,
364357
},
365358
rules: [
366359
{
@@ -395,12 +388,12 @@ const AddCustomProperty = ({
395388
required: false,
396389
label: t('label.format'),
397390
id: 'root/formatConfig',
398-
type: FieldTypes.SELECT_MUI,
391+
type: FieldTypes.UT_SELECT,
399392
props: {
400393
'data-testid': 'formatConfig',
401-
options: supportedFormats.map((option) => ({
394+
items: supportedFormats.map((option) => ({
395+
id: option,
402396
label: option,
403-
value: option,
404397
})),
405398
},
406399
placeholder: t('label.format'),
@@ -437,6 +430,8 @@ const AddCustomProperty = ({
437430
placeholder: `${t('label.select-field', {
438431
field: t('label.type'),
439432
})}`,
433+
getPopupContainer: (triggerNode: HTMLElement) =>
434+
triggerNode.parentElement ?? document.body,
440435
},
441436
}),
442437
[t]
@@ -454,6 +449,8 @@ const AddCustomProperty = ({
454449
'data-testid': 'columns',
455450
mode: 'tags',
456451
placeholder: t('label.column-plural'),
452+
getPopupContainer: (triggerNode: HTMLElement) =>
453+
triggerNode.parentElement ?? document.body,
457454
},
458455
rules: [
459456
{
@@ -612,26 +609,89 @@ const AddCustomProperty = ({
612609
);
613610

614611
if (!isUndefined(open)) {
612+
const isDrawerLoading = loading || isCreating;
613+
615614
return (
616-
<MuiDrawer
617-
hasSidePanel
618-
formRef={form}
619-
isFormInvalid={isFormInvalid}
620-
isLoading={loading || isCreating}
621-
open={open}
622-
sidePanel={
623-
<div className="service-doc-panel">
624-
<ServiceDocPanel
625-
activeField={activeField}
626-
serviceName={CUSTOM_PROPERTY_CATEGORY}
627-
serviceType={OPEN_METADATA as ServiceCategory}
628-
/>
629-
</div>
630-
}
631-
title={t('label.add-entity', { entity: t('label.custom-property') })}
632-
onClose={onClose ?? handleCancel}>
633-
{formContent}
634-
</MuiDrawer>
615+
<SlideoutMenu
616+
isDismissable
617+
className="tw:z-1000"
618+
data-testid="add-custom-property-drawer"
619+
isOpen={open}
620+
width={showSidePanel ? 1200 : 700}
621+
onOpenChange={(isDrawerOpen) => {
622+
if (!isDrawerOpen) {
623+
(onClose ?? handleCancel)();
624+
}
625+
}}>
626+
{({ close }) => (
627+
<>
628+
<SlideoutMenu.Header
629+
className="tw:flex tw:items-center tw:justify-between tw:gap-4 tw:border-b tw:border-secondary tw:py-4"
630+
onClose={close}>
631+
<Typography
632+
className="tw:text-primary"
633+
size="text-lg"
634+
weight="semibold">
635+
{t('label.add-entity', {
636+
entity: t('label.custom-property'),
637+
})}
638+
</Typography>
639+
<div className="tw:mr-8 tw:flex tw:items-center tw:gap-2">
640+
<Toggle
641+
data-testid="show-side-panel-switch"
642+
isSelected={showSidePanel}
643+
onChange={setShowSidePanel}
644+
/>
645+
<Typography className="tw:text-secondary" size="text-sm">
646+
{t('label.show-help-text')}
647+
</Typography>
648+
</div>
649+
</SlideoutMenu.Header>
650+
651+
<SlideoutMenu.Content>
652+
<div className="tw:flex tw:size-full tw:gap-6">
653+
<div
654+
className={
655+
showSidePanel ? 'tw:w-3/5 tw:overflow-y-auto' : 'tw:w-full'
656+
}>
657+
{formContent}
658+
</div>
659+
{showSidePanel && (
660+
<div className="tw:w-2/5 tw:overflow-y-auto tw:border-l tw:border-secondary tw:pl-6">
661+
<div className="service-doc-panel">
662+
<ServiceDocPanel
663+
activeField={activeField}
664+
serviceName={CUSTOM_PROPERTY_CATEGORY}
665+
serviceType={OPEN_METADATA as ServiceCategory}
666+
/>
667+
</div>
668+
</div>
669+
)}
670+
</div>
671+
</SlideoutMenu.Content>
672+
673+
<SlideoutMenu.Footer>
674+
<div className="tw:flex tw:justify-end tw:gap-4">
675+
<CoreButton
676+
color="tertiary"
677+
data-testid="cancel-button"
678+
isDisabled={isDrawerLoading}
679+
onClick={close}>
680+
{t('label.cancel')}
681+
</CoreButton>
682+
<CoreButton
683+
color="primary"
684+
data-testid="create-button"
685+
isDisabled={isDrawerLoading || isFormInvalid}
686+
isLoading={isDrawerLoading}
687+
onClick={() => form.submit()}>
688+
{t('label.create')}
689+
</CoreButton>
690+
</div>
691+
</SlideoutMenu.Footer>
692+
</>
693+
)}
694+
</SlideoutMenu>
635695
);
636696
}
637697

0 commit comments

Comments
 (0)