mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 13:13:10 +00:00
feat(ui): supported pagination of glossary terms in glossary overview section (#12177)
* supported pagination of glossary terms in glossay overview section * remove unwanted try catch * change as per comments
This commit is contained in:
parent
206b155a8b
commit
d9cd484657
@ -288,7 +288,7 @@ const updateTerms = (newTerm) => {
|
|||||||
.contains(newTerm)
|
.contains(newTerm)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click();
|
.click();
|
||||||
cy.get('[data-testid="save-related-term-btn"]').should('be.visible').click();
|
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
|
||||||
verifyResponseStatusCode('@saveGlossaryTermData', 200);
|
verifyResponseStatusCode('@saveGlossaryTermData', 200);
|
||||||
|
|
||||||
cy.get('[data-testid="related-term-container"]')
|
cy.get('[data-testid="related-term-container"]')
|
||||||
|
@ -18,7 +18,7 @@ export type SelectOption = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InfiniteSelectScrollProps {
|
export interface AsyncSelectListProps {
|
||||||
mode?: 'multiple';
|
mode?: 'multiple';
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
debounceTimeout?: number;
|
debounceTimeout?: number;
|
@ -21,11 +21,11 @@ import { tagRender } from 'utils/TagsUtils';
|
|||||||
import { showErrorToast } from 'utils/ToastUtils';
|
import { showErrorToast } from 'utils/ToastUtils';
|
||||||
import Fqn from '../../utils/Fqn';
|
import Fqn from '../../utils/Fqn';
|
||||||
import {
|
import {
|
||||||
InfiniteSelectScrollProps,
|
AsyncSelectListProps,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from './InfiniteSelectScroll.interface';
|
} from './AsyncSelectList.interface';
|
||||||
|
|
||||||
const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||||
mode,
|
mode,
|
||||||
onChange,
|
onChange,
|
||||||
fetchOptions,
|
fetchOptions,
|
||||||
@ -44,10 +44,11 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
|||||||
setOptions([]);
|
setOptions([]);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetchOptions(value, currentPage);
|
const res = await fetchOptions(value, 1);
|
||||||
setOptions(res.data);
|
setOptions(res.data);
|
||||||
setPaging(res.paging);
|
setPaging(res.paging);
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
|
setCurrentPage(1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
} finally {
|
} finally {
|
||||||
@ -156,4 +157,4 @@ const InfiniteSelectScroll: FC<InfiniteSelectScrollProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InfiniteSelectScroll;
|
export default AsyncSelectList;
|
@ -32,7 +32,7 @@ import RelatedTerms from './RelatedTerms';
|
|||||||
type Props = {
|
type Props = {
|
||||||
selectedData: Glossary | GlossaryTerm;
|
selectedData: Glossary | GlossaryTerm;
|
||||||
permissions: OperationPermission;
|
permissions: OperationPermission;
|
||||||
onUpdate: (data: GlossaryTerm | Glossary) => void;
|
onUpdate: (data: GlossaryTerm | Glossary) => Promise<void>;
|
||||||
isGlossary: boolean;
|
isGlossary: boolean;
|
||||||
isVersionView?: boolean;
|
isVersionView?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -11,19 +11,21 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
import { Button, Select, Space, Spin, Tooltip, Typography } from 'antd';
|
|
||||||
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
||||||
import { ReactComponent as IconFlatDoc } from 'assets/svg/ic-flat-doc.svg';
|
import { ReactComponent as IconFlatDoc } from 'assets/svg/ic-flat-doc.svg';
|
||||||
|
import TagSelectForm from 'components/Tag/TagsSelectForm/TagsSelectForm.component';
|
||||||
import TagButton from 'components/TagButton/TagButton.component';
|
import TagButton from 'components/TagButton/TagButton.component';
|
||||||
import { EntityField } from 'constants/Feeds.constants';
|
import { EntityField } from 'constants/Feeds.constants';
|
||||||
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
|
import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil';
|
||||||
import { ChangeDescription } from 'generated/entity/type';
|
import { ChangeDescription } from 'generated/entity/type';
|
||||||
|
import { Paging } from 'generated/type/paging';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { cloneDeep, debounce, includes, isEmpty } from 'lodash';
|
import { cloneDeep, includes, isEmpty, uniqWith } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { searchData } from 'rest/miscAPI';
|
import { searchData } from 'rest/miscAPI';
|
||||||
|
import { formatSearchGlossaryTermResponse } from 'utils/APIUtils';
|
||||||
import { getEntityName } from 'utils/EntityUtils';
|
import { getEntityName } from 'utils/EntityUtils';
|
||||||
import {
|
import {
|
||||||
getChangedEntityNewValue,
|
getChangedEntityNewValue,
|
||||||
@ -41,7 +43,6 @@ import {
|
|||||||
import { SearchIndex } from '../../../enums/search.enum';
|
import { SearchIndex } from '../../../enums/search.enum';
|
||||||
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
||||||
import { EntityReference } from '../../../generated/type/entityReference';
|
import { EntityReference } from '../../../generated/type/entityReference';
|
||||||
import { formatSearchGlossaryTermResponse } from '../../../utils/APIUtils';
|
|
||||||
import { getEntityReferenceFromGlossary } from '../../../utils/GlossaryUtils';
|
import { getEntityReferenceFromGlossary } from '../../../utils/GlossaryUtils';
|
||||||
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
|
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ interface RelatedTermsProps {
|
|||||||
isVersionView?: boolean;
|
isVersionView?: boolean;
|
||||||
permissions: OperationPermission;
|
permissions: OperationPermission;
|
||||||
glossaryTerm: GlossaryTerm;
|
glossaryTerm: GlossaryTerm;
|
||||||
onGlossaryTermUpdate: (data: GlossaryTerm) => void;
|
onGlossaryTermUpdate: (data: GlossaryTerm) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RelatedTerms = ({
|
const RelatedTerms = ({
|
||||||
@ -60,26 +61,21 @@ const RelatedTerms = ({
|
|||||||
}: RelatedTermsProps) => {
|
}: RelatedTermsProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isIconVisible, setIsIconVisible] = useState<boolean>(true);
|
const [isIconVisible, setIsIconVisible] = useState<boolean>(true);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [options, setOptions] = useState<EntityReference[]>([]);
|
const [options, setOptions] = useState<EntityReference[]>([]);
|
||||||
const [selectedOption, setSelectedOption] = useState<EntityReference[]>([]);
|
const [selectedOption, setSelectedOption] = useState<EntityReference[]>([]);
|
||||||
|
|
||||||
const getSearchedTerms = (searchedData: EntityReference[]) => {
|
|
||||||
const currOptions = selectedOption.map(
|
|
||||||
(item) => item.fullyQualifiedName || item.name
|
|
||||||
);
|
|
||||||
const data = searchedData.filter((item: EntityReference) => {
|
|
||||||
return !currOptions.includes(item.fullyQualifiedName);
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...selectedOption, ...data];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRelatedTermClick = (fqn: string) => {
|
const handleRelatedTermClick = (fqn: string) => {
|
||||||
history.push(getGlossaryPath(fqn));
|
history.push(getGlossaryPath(fqn));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRelatedTermsSave = (newOptions: EntityReference[]) => {
|
const handleRelatedTermsSave = async (
|
||||||
|
selectedData: string[]
|
||||||
|
): Promise<void> => {
|
||||||
|
const newOptions = uniqWith(
|
||||||
|
options,
|
||||||
|
(arrVal, othVal) => arrVal.id === othVal.id
|
||||||
|
).filter((item) => includes(selectedData, item.fullyQualifiedName));
|
||||||
|
|
||||||
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
|
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
|
||||||
const oldTerms = newOptions.filter((d) =>
|
const oldTerms = newOptions.filter((d) =>
|
||||||
includes(glossaryTerm.relatedTerms, d)
|
includes(glossaryTerm.relatedTerms, d)
|
||||||
@ -97,33 +93,50 @@ const RelatedTerms = ({
|
|||||||
relatedTerms: [...oldTerms, ...newTerms],
|
relatedTerms: [...oldTerms, ...newTerms],
|
||||||
};
|
};
|
||||||
|
|
||||||
onGlossaryTermUpdate(updatedGlossaryTerm);
|
await onGlossaryTermUpdate(updatedGlossaryTerm);
|
||||||
setIsIconVisible(true);
|
setIsIconVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestionSearch = (searchText = '') => {
|
const fetchGlossaryTerms = async (
|
||||||
setIsLoading(true);
|
searchText = '',
|
||||||
searchData(searchText, 1, PAGE_SIZE, '', '', '', SearchIndex.GLOSSARY)
|
page: number
|
||||||
.then((res) => {
|
): Promise<{
|
||||||
const termResult = formatSearchGlossaryTermResponse(
|
data: {
|
||||||
res.data.hits.hits
|
label: string;
|
||||||
).filter((item) => {
|
value: string;
|
||||||
return item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName;
|
}[];
|
||||||
});
|
paging: Paging;
|
||||||
|
}> => {
|
||||||
|
const res = await searchData(
|
||||||
|
searchText,
|
||||||
|
page,
|
||||||
|
PAGE_SIZE,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
SearchIndex.GLOSSARY
|
||||||
|
);
|
||||||
|
|
||||||
const results = termResult.map(getEntityReferenceFromGlossary);
|
const termResult = formatSearchGlossaryTermResponse(
|
||||||
|
res.data.hits.hits
|
||||||
|
).filter(
|
||||||
|
(item) => item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName
|
||||||
|
);
|
||||||
|
|
||||||
const data = searchText ? getSearchedTerms(results) : results;
|
const results = termResult.map(getEntityReferenceFromGlossary);
|
||||||
setOptions(data);
|
setOptions((prev) => [...prev, ...results]);
|
||||||
})
|
|
||||||
.catch(() => {
|
return {
|
||||||
setOptions(selectedOption);
|
data: results.map((item) => ({
|
||||||
})
|
label: item.fullyQualifiedName ?? '',
|
||||||
.finally(() => setIsLoading(false));
|
value: item.fullyQualifiedName ?? '',
|
||||||
|
})),
|
||||||
|
paging: {
|
||||||
|
total: res.data.hits.total.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounceOnSearch = useCallback(debounce(suggestionSearch, 250), []);
|
|
||||||
|
|
||||||
const formatOptions = (data: EntityReference[]) => {
|
const formatOptions = (data: EntityReference[]) => {
|
||||||
return data.map((value) => ({
|
return data.map((value) => ({
|
||||||
...value,
|
...value,
|
||||||
@ -134,14 +147,13 @@ const RelatedTerms = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setSelectedOption(formatOptions(glossaryTerm.relatedTerms || []));
|
|
||||||
setIsIconVisible(true);
|
setIsIconVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (glossaryTerm.relatedTerms?.length) {
|
if (glossaryTerm) {
|
||||||
setOptions(glossaryTerm.relatedTerms);
|
setOptions(glossaryTerm.relatedTerms ?? []);
|
||||||
setSelectedOption(formatOptions(glossaryTerm.relatedTerms));
|
setSelectedOption(formatOptions(glossaryTerm.relatedTerms ?? []));
|
||||||
}
|
}
|
||||||
}, [glossaryTerm]);
|
}, [glossaryTerm]);
|
||||||
|
|
||||||
@ -279,41 +291,17 @@ const RelatedTerms = ({
|
|||||||
{isIconVisible ? (
|
{isIconVisible ? (
|
||||||
relatedTermsContainer
|
relatedTermsContainer
|
||||||
) : (
|
) : (
|
||||||
<>
|
<TagSelectForm
|
||||||
<Space className="justify-end w-full m-b-xs" size={8}>
|
defaultValue={selectedOption.map(
|
||||||
<Button
|
(item) => item.fullyQualifiedName ?? ''
|
||||||
className="w-6 p-x-05"
|
)}
|
||||||
data-testid="cancel-related-term-btn"
|
fetchApi={fetchGlossaryTerms}
|
||||||
icon={<CloseOutlined size={12} />}
|
placeholder={t('label.add-entity', {
|
||||||
size="small"
|
entity: t('label.related-term-plural'),
|
||||||
onClick={() => handleCancel()}
|
})}
|
||||||
/>
|
onCancel={handleCancel}
|
||||||
<Button
|
onSubmit={handleRelatedTermsSave}
|
||||||
className="w-6 p-x-05"
|
/>
|
||||||
data-testid="save-related-term-btn"
|
|
||||||
icon={<CheckOutlined size={12} />}
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => handleRelatedTermsSave(selectedOption)}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<Select
|
|
||||||
className="glossary-select w-full"
|
|
||||||
filterOption={false}
|
|
||||||
mode="multiple"
|
|
||||||
notFoundContent={isLoading ? <Spin size="small" /> : null}
|
|
||||||
options={formatOptions(options)}
|
|
||||||
placeholder={t('label.add-entity', {
|
|
||||||
entity: t('label.related-term-plural'),
|
|
||||||
})}
|
|
||||||
value={selectedOption}
|
|
||||||
onChange={(_, data) => {
|
|
||||||
setSelectedOption(data as EntityReference[]);
|
|
||||||
}}
|
|
||||||
onFocus={() => suggestionSearch()}
|
|
||||||
onSearch={debounceOnSearch}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
|
import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
|
||||||
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
|
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
|
||||||
import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants';
|
import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants';
|
||||||
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
|
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
|
||||||
@ -94,29 +93,25 @@ const TagsContainerV2 = ({
|
|||||||
}[];
|
}[];
|
||||||
paging: Paging;
|
paging: Paging;
|
||||||
}> => {
|
}> => {
|
||||||
try {
|
const tagResponse = await searchQuery({
|
||||||
const tagResponse = await searchQuery({
|
query: searchQueryParam ? searchQueryParam : '*',
|
||||||
query: searchQueryParam ? searchQueryParam : '*',
|
pageNumber: page,
|
||||||
pageNumber: page,
|
pageSize: PAGE_SIZE,
|
||||||
pageSize: PAGE_SIZE,
|
queryFilter: {},
|
||||||
queryFilter: {},
|
searchIndex: SearchIndex.TAG,
|
||||||
searchIndex: SearchIndex.TAG,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return {
|
||||||
data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map(
|
data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map(
|
||||||
(item) => ({
|
(item) => ({
|
||||||
label: item.fullyQualifiedName ?? '',
|
label: item.fullyQualifiedName ?? '',
|
||||||
value: item.fullyQualifiedName ?? '',
|
value: item.fullyQualifiedName ?? '',
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
paging: {
|
paging: {
|
||||||
total: tagResponse.hits.total.value,
|
total: tagResponse.hits.total.value,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject({ data: (error as AxiosError).response });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[getTags]
|
[getTags]
|
||||||
);
|
);
|
||||||
@ -132,29 +127,25 @@ const TagsContainerV2 = ({
|
|||||||
}[];
|
}[];
|
||||||
paging: Paging;
|
paging: Paging;
|
||||||
}> => {
|
}> => {
|
||||||
try {
|
const glossaryResponse = await searchQuery({
|
||||||
const glossaryResponse = await searchQuery({
|
query: searchQueryParam ? searchQueryParam : '*',
|
||||||
query: searchQueryParam ? searchQueryParam : '*',
|
pageNumber: page,
|
||||||
pageNumber: page,
|
pageSize: 10,
|
||||||
pageSize: 10,
|
queryFilter: {},
|
||||||
queryFilter: {},
|
searchIndex: SearchIndex.GLOSSARY,
|
||||||
searchIndex: SearchIndex.GLOSSARY,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return {
|
||||||
data: formatSearchGlossaryTermResponse(
|
data: formatSearchGlossaryTermResponse(
|
||||||
glossaryResponse.hits.hits ?? []
|
glossaryResponse.hits.hits ?? []
|
||||||
).map((item) => ({
|
).map((item) => ({
|
||||||
label: item.fullyQualifiedName ?? '',
|
label: item.fullyQualifiedName ?? '',
|
||||||
value: item.fullyQualifiedName ?? '',
|
value: item.fullyQualifiedName ?? '',
|
||||||
})),
|
})),
|
||||||
paging: {
|
paging: {
|
||||||
total: glossaryResponse.hits.total.value,
|
total: glossaryResponse.hits.total.value,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject({ data: (error as AxiosError).response });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse]
|
[searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse]
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { Button, Col, Form, Row, Space } from 'antd';
|
import { Button, Col, Form, Row, Space } from 'antd';
|
||||||
import { useForm } from 'antd/lib/form/Form';
|
import { useForm } from 'antd/lib/form/Form';
|
||||||
import InfiniteSelectScroll from 'components/InfiniteSelectScroll/InfiniteSelectScroll';
|
import AsyncSelectList from 'components/AsyncSelectList/AsyncSelectList';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TagsSelectFormProps } from './TagsSelectForm.interface';
|
import { TagsSelectFormProps } from './TagsSelectForm.interface';
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ const TagSelectForm = ({
|
|||||||
|
|
||||||
<Col className="gutter-row" span={24}>
|
<Col className="gutter-row" span={24}>
|
||||||
<Form.Item noStyle name="tags">
|
<Form.Item noStyle name="tags">
|
||||||
<InfiniteSelectScroll
|
<AsyncSelectList
|
||||||
fetchOptions={fetchApi}
|
fetchOptions={fetchApi}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SelectOption } from 'components/InfiniteSelectScroll/InfiniteSelectScroll.interface';
|
import { SelectOption } from 'components/AsyncSelectList/AsyncSelectList.interface';
|
||||||
import { Paging } from 'generated/type/paging';
|
import { Paging } from 'generated/type/paging';
|
||||||
|
|
||||||
export type TagsSelectFormProps = {
|
export type TagsSelectFormProps = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user