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:
karanh37 2023-12-01 11:33:58 +05:30 committed by GitHub
parent 7fcdf08ca4
commit 71b8b1d5fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 87 deletions

View File

@ -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}

View File

@ -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();

View File

@ -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 = (
const { value, reference, displayName } = option;
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-${option.value}`}>
data-testid={`option-${value}`}>
<div className="flex-center data-asset-icon">
{getEntityIcon(option.reference.type)}
{getEntityIcon(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>
<span className="text-grey-muted text-xs">{reference.type}</span>
<span className="font-medium truncate w-56">{displayName}</span>
</div>
</div>
);
}
return {
label,
value: option.value,
reference: option.reference,
displayName: option.displayName,
};
return { label, value, reference, displayName };
});
}, [options]);
}, [options, searchIndex]);
const debounceFetcher = useMemo(
() => debounce(loadOptions, debounceTimeout),

View File

@ -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}
/>
);
};

View File

@ -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' },
};

View File

@ -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}