mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 02:29:03 +00:00 
			
		
		
		
	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 <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
		
							parent
							
								
									7fcdf08ca4
								
							
						
					
					
						commit
						71b8b1d5fe
					
				| @ -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} | ||||
|  | ||||
| @ -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(<p data-testid="profile-pic">ProfilePicture</p>); | ||||
| }); | ||||
| 
 | ||||
| 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( | ||||
|       <DataAssetAsyncSelectList searchIndex={SearchIndex.USER} /> | ||||
|     ); | ||||
| 
 | ||||
|     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(); | ||||
| 
 | ||||
|  | ||||
| @ -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<DataAssetAsyncSelectListProps> = ({ | ||||
|             type: _source.entityType, | ||||
|           }, | ||||
|           displayName: entityName, | ||||
|           name: _source.name, | ||||
|         }; | ||||
|       }); | ||||
| 
 | ||||
| @ -112,32 +114,41 @@ const DataAssetAsyncSelectList: FC<DataAssetAsyncSelectListProps> = ({ | ||||
| 
 | ||||
|   const optionList = useMemo(() => { | ||||
|     return options.map((option) => { | ||||
|       const label = ( | ||||
|         <div | ||||
|           className="d-flex items-center gap-2" | ||||
|           data-testid={`option-${option.value}`}> | ||||
|           <div className="flex-center data-asset-icon"> | ||||
|             {getEntityIcon(option.reference.type)} | ||||
|           </div> | ||||
|           <div className="d-flex flex-col"> | ||||
|             <span className="text-grey-muted text-xs"> | ||||
|               {option.reference.type} | ||||
|             </span> | ||||
|             <span className="font-medium truncate w-56"> | ||||
|               {option.displayName} | ||||
|             </span> | ||||
|           </div> | ||||
|         </div> | ||||
|       ); | ||||
|       const { value, reference, displayName } = option; | ||||
| 
 | ||||
|       return { | ||||
|         label, | ||||
|         value: option.value, | ||||
|         reference: option.reference, | ||||
|         displayName: option.displayName, | ||||
|       }; | ||||
|       let label; | ||||
|       if (searchIndex === SearchIndex.USER) { | ||||
|         label = ( | ||||
|           <Space> | ||||
|             <ProfilePicture | ||||
|               className="d-flex" | ||||
|               id="" | ||||
|               name={option.name ?? ''} | ||||
|               type="circle" | ||||
|               width="24" | ||||
|             /> | ||||
|             <span className="m-l-xs">{getEntityName(option)}</span> | ||||
|           </Space> | ||||
|         ); | ||||
|       } else { | ||||
|         label = ( | ||||
|           <div | ||||
|             className="d-flex items-center gap-2" | ||||
|             data-testid={`option-${value}`}> | ||||
|             <div className="flex-center data-asset-icon"> | ||||
|               {getEntityIcon(reference.type)} | ||||
|             </div> | ||||
|             <div className="d-flex flex-col"> | ||||
|               <span className="text-grey-muted text-xs">{reference.type}</span> | ||||
|               <span className="font-medium truncate w-56">{displayName}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       return { label, value, reference, displayName }; | ||||
|     }); | ||||
|   }, [options]); | ||||
|   }, [options, searchIndex]); | ||||
| 
 | ||||
|   const debounceFetcher = useMemo( | ||||
|     () => debounce(loadOptions, debounceTimeout), | ||||
|  | ||||
| @ -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 ( | ||||
|     <AsyncSelect | ||||
|       api={fetchEntities} | ||||
|       className="d-block" | ||||
|       data-testid="edit-query-used-in" | ||||
|     <DataAssetAsyncSelectList | ||||
|       defaultValue={schema.value} | ||||
|       mode="multiple" | ||||
|       placeholder={schema.placeholder ?? ''} | ||||
|       onChange={(value) => { | ||||
|         onChange(value); | ||||
|       }} | ||||
|       searchIndex={schema?.autoCompleteType ?? SearchIndex.TABLE} | ||||
|       onChange={handleChange} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -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<StepperStepType> = [ | ||||
|   { | ||||
| @ -22,19 +21,3 @@ export const STEPS_FOR_APP_INSTALL: Array<StepperStepType> = [ | ||||
|   { 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' }, | ||||
| }; | ||||
|  | ||||
| @ -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} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 karanh37
						karanh37