mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-09 17:12:02 +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