From 71b8b1d5fe411b32694e7c56b3dbb8f451b9f824 Mon Sep 17 00:00:00 2001 From: karanh37 <33024356+karanh37@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:33:58 +0530 Subject: [PATCH] fix: add autocomplete widget for dynamic forms (#14188) * chore(ui): add DataAssetsAsyncSelectList Component * add style for entity icon * address comments * fix: add search index prop * fix: update async select widget * fix: add unit tests --------- Co-authored-by: Sachin Chaurasiya --- .../AppDetails/AppDetails.component.tsx | 2 - .../DataAssetAsyncSelectList.test.tsx | 58 ++++++++++++++++++ .../DataAssetAsyncSelectList.tsx | 61 +++++++++++-------- .../JsonSchemaWidgets/AsyncSelectWidget.tsx | 49 ++++----------- .../ui/src/constants/Applications.constant.ts | 17 ------ .../pages/AppInstall/AppInstall.component.tsx | 6 +- 6 files changed, 106 insertions(+), 87 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppDetails/AppDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppDetails/AppDetails.component.tsx index d45ca169876..266731c5fcc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppDetails/AppDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppDetails/AppDetails.component.tsx @@ -43,7 +43,6 @@ import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg'; import Loader from '../../../components/Loader/Loader'; import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1'; import TabsLabel from '../../../components/TabsLabel/TabsLabel.component'; -import { APPLICATION_UI_SCHEMA } from '../../../constants/Applications.constant'; import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { GlobalSettingOptions, @@ -335,7 +334,6 @@ const AppDetails = () => { serviceCategory={ServiceCategory.DASHBOARD_SERVICES} serviceType="" showTestConnection={false} - uiSchema={APPLICATION_UI_SCHEMA} validator={validator} onCancel={noop} onSubmit={onConfigSave} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.test.tsx index 9f748414008..2d53e9add95 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.test.tsx @@ -12,6 +12,7 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { SearchIndex } from '../../enums/search.enum'; import { searchQuery } from '../../rest/searchAPI'; import DataAssetAsyncSelectList from './DataAssetAsyncSelectList'; import { DataAssetOption } from './DataAssetAsyncSelectList.interface'; @@ -21,6 +22,41 @@ jest.mock('../../utils/TableUtils'); jest.mock('../../utils/EntityUtils', () => ({ getEntityName: jest.fn().mockReturnValue('Test'), })); +jest.mock('../common/ProfilePicture/ProfilePicture', () => { + return jest + .fn() + .mockReturnValue(

ProfilePicture

); +}); + +const mockUserData = { + data: { + hits: { + hits: [ + { + _source: { + id: '4d499590-89ef-438d-9c49-3f05c4041144', + name: 'admin', + fullyQualifiedName: 'admin', + entityType: 'user', + displayName: 'admin', + }, + }, + { + _source: { + id: '93057069-3836-4fc0-b85e-a456e52b4424', + name: 'user1', + fullyQualifiedName: 'user1', + entityType: 'user', + displayName: 'user1', + }, + }, + ], + total: { + value: 2, + }, + }, + }, +}; const mockSearchAPIResponse = { data: { @@ -88,6 +124,28 @@ describe('DataAssetAsyncSelectList', () => { expect(searchQuery).toHaveBeenCalledTimes(1); }); + it('should render profile picture if search index is user', async () => { + const mockSearchQuery = searchQuery as jest.Mock; + mockSearchQuery.mockImplementationOnce((params) => { + expect(params).toEqual( + expect.objectContaining({ searchIndex: SearchIndex.USER }) + ); + + return Promise.resolve(mockUserData.data); + }); + + const { container } = render( + + ); + + await act(async () => { + toggleOpen(container); + }); + + expect(searchQuery).toHaveBeenCalledTimes(1); + expect(screen.getAllByTestId('profile-pic')).toHaveLength(2); + }); + it('should call onChange when an option is selected', async () => { const mockOnChange = jest.fn(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.tsx index 6df76efd0ac..40922811004 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetAsyncSelectList/DataAssetAsyncSelectList.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Select, SelectProps } from 'antd'; +import { Select, SelectProps, Space } from 'antd'; import { AxiosError } from 'axios'; import { debounce } from 'lodash'; import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; @@ -22,6 +22,7 @@ import { searchQuery } from '../../rest/searchAPI'; import { getEntityName } from '../../utils/EntityUtils'; import { getEntityIcon } from '../../utils/TableUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import ProfilePicture from '../common/ProfilePicture/ProfilePicture'; import { DataAssetAsyncSelectListProps, DataAssetOption, @@ -78,6 +79,7 @@ const DataAssetAsyncSelectList: FC = ({ type: _source.entityType, }, displayName: entityName, + name: _source.name, }; }); @@ -112,32 +114,41 @@ const DataAssetAsyncSelectList: FC = ({ const optionList = useMemo(() => { return options.map((option) => { - const label = ( -
-
- {getEntityIcon(option.reference.type)} -
-
- - {option.reference.type} - - - {option.displayName} - -
-
- ); + const { value, reference, displayName } = option; - return { - label, - value: option.value, - reference: option.reference, - displayName: option.displayName, - }; + let label; + if (searchIndex === SearchIndex.USER) { + label = ( + + + {getEntityName(option)} + + ); + } else { + label = ( +
+
+ {getEntityIcon(reference.type)} +
+
+ {reference.type} + {displayName} +
+
+ ); + } + + return { label, value, reference, displayName }; }); - }, [options]); + }, [options, searchIndex]); const debounceFetcher = useMemo( () => debounce(loadOptions, debounceTimeout), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/JsonSchemaWidgets/AsyncSelectWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/JsonSchemaWidgets/AsyncSelectWidget.tsx index 9f2adf55994..3a377b45c9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/JsonSchemaWidgets/AsyncSelectWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/JsonSchemaWidgets/AsyncSelectWidget.tsx @@ -11,51 +11,24 @@ * limitations under the License. */ import { WidgetProps } from '@rjsf/utils'; -import React, { useCallback } from 'react'; -import { PAGE_SIZE_MEDIUM } from '../../constants/constants'; +import React from 'react'; import { SearchIndex } from '../../enums/search.enum'; -import { searchQuery } from '../../rest/searchAPI'; -import { getEntityName } from '../../utils/EntityUtils'; -import { AsyncSelect } from '../AsyncSelect/AsyncSelect'; +import DataAssetAsyncSelectList from '../DataAssetAsyncSelectList/DataAssetAsyncSelectList'; +import { DataAssetOption } from '../DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface'; -const AsyncSelectWidget = ({ - onFocus, - onBlur, - onChange, - schema, - ...rest -}: WidgetProps) => { - const type = rest?.uiSchema?.['ui:options']?.autoCompleteType as SearchIndex; - - const fetchEntities = useCallback(async (searchText: string) => { - try { - const res = await searchQuery({ - pageNumber: 1, - pageSize: PAGE_SIZE_MEDIUM, - searchIndex: type ?? SearchIndex.TABLE, - query: searchText, - }); - - return res.hits.hits.map((value) => ({ - label: getEntityName(value._source), - value: value._source.id, - })); - } catch (_) { - return []; - } - }, []); +const AsyncSelectWidget = ({ onChange, schema }: WidgetProps) => { + const handleChange = (value: DataAssetOption | DataAssetOption[]) => { + const data = value.map((item: DataAssetOption) => item.reference); + onChange(data); + }; return ( - { - onChange(value); - }} + searchIndex={schema?.autoCompleteType ?? SearchIndex.TABLE} + onChange={handleChange} /> ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Applications.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Applications.constant.ts index 6bff623c655..136ca3f460b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Applications.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Applications.constant.ts @@ -12,7 +12,6 @@ */ import { t } from 'i18next'; import { StepperStepType } from 'Models'; -import { SearchIndex } from '../enums/search.enum'; export const STEPS_FOR_APP_INSTALL: Array = [ { @@ -22,19 +21,3 @@ export const STEPS_FOR_APP_INSTALL: Array = [ { name: t('label.configure'), step: 2 }, { name: t('label.schedule'), step: 3 }, ]; - -export const APPLICATION_UI_SCHEMA = { - databases: { - 'ui:widget': 'autoComplete', - 'ui:options': { - autoCompleteType: SearchIndex.DATABASE, - }, - }, - owner: { - 'ui:widget': 'autoComplete', - 'ui:options': { - autoCompleteType: SearchIndex.USER, - }, - }, - type: { 'ui:widget': 'hidden' }, -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx index c168ca3f7ff..4dcbbb67c71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx @@ -25,10 +25,7 @@ import FormBuilder from '../../components/common/FormBuilder/FormBuilder'; import IngestionStepper from '../../components/IngestionStepper/IngestionStepper.component'; import Loader from '../../components/Loader/Loader'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; -import { - APPLICATION_UI_SCHEMA, - STEPS_FOR_APP_INSTALL, -} from '../../constants/Applications.constant'; +import { STEPS_FOR_APP_INSTALL } from '../../constants/Applications.constant'; import { GlobalSettingOptions, GlobalSettingsMenuCategory, @@ -169,7 +166,6 @@ const AppInstall = () => { serviceCategory={ServiceCategory.DASHBOARD_SERVICES} serviceType="" showTestConnection={false} - uiSchema={APPLICATION_UI_SCHEMA} validator={validator} onCancel={() => setActiveServiceStep(1)} onSubmit={onSaveConfiguration}