From af6cec6c9f17295830cf54aaa4e085635d00de78 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Tue, 27 Jun 2023 17:40:25 +0530 Subject: [PATCH] feat(ui): supported pagination and search in tags and term select for both table and entity level (#12155) * supported pagination and search in tags and term select for both table and entity level * supported pagination of tags in glossary overiew section for tags * fix cypress and address comments * fix cypress issue --- .../resources/ui/cypress/common/common.js | 13 +- .../ui/cypress/constants/constants.js | 1 + .../constants/tagsAddRemove.constants.js | 15 +- .../ui/cypress/e2e/Flow/TagsAddRemove.spec.js | 24 +- .../ui/cypress/e2e/Pages/Glossary.spec.js | 29 +- .../ui/cypress/e2e/Pages/Tags.spec.js | 2 +- .../ContainerDataModel/ContainerDataModel.tsx | 68 +-- .../DashboardDetails.component.tsx | 73 +--- .../DataModels/DataModelDetails.component.tsx | 6 +- .../ModelTab/ModelTab.component.tsx | 64 +-- .../EntityTable/EntityTable.component.tsx | 78 +--- .../InfiniteSelectScroll.interface.ts | 33 ++ .../InfiniteSelectScroll.tsx | 159 +++++++ .../MlModelDetail/MlModelDetail.component.tsx | 6 +- .../MlModelDetail/MlModelFeaturesList.tsx | 52 --- .../PipelineDetails.component.tsx | 72 +-- .../TableTags/TableTags.component.tsx | 148 +++---- .../TableTags/TableTags.interface.ts | 6 - .../components/TableTags/TableTags.test.tsx | 16 +- .../TagsContainerV2.interface.ts | 32 ++ .../Tag/TagsContainerV2/TagsContainerV2.tsx | 411 ++++++++++++++++++ .../TagsSelectForm.component.tsx | 76 ++++ .../TagsSelectForm.interface.ts | 30 ++ .../Tag/TagsV1/TagsV1.component.tsx | 31 +- .../components/Tag/TagsV1/TagsV1.interface.ts | 2 + .../ui/src/components/Tag/TagsV1/tagsV1.less | 16 +- .../TagsInput/TagsInput.component.tsx | 70 +-- .../TopicDetails/TopicDetails.component.tsx | 6 +- .../TopicDetails/TopicSchema/TopicSchema.tsx | 64 +-- .../src/pages/ContainerPage/ContainerPage.tsx | 6 +- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 6 +- .../src/main/resources/ui/src/styles/app.less | 11 + .../main/resources/ui/src/utils/APIUtils.ts | 16 + .../main/resources/ui/src/utils/TagsUtils.tsx | 37 +- 34 files changed, 987 insertions(+), 692 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.interface.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/common.js b/openmetadata-ui/src/main/resources/ui/cypress/common/common.js index 1d8cc779bac..cdc7261d0c9 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/common.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/common.js @@ -617,6 +617,7 @@ export const visitEntityDetailsPage = ( // add new tag to entity and its table export const addNewTagToEntity = (entityObj, term) => { + const { name, fqn } = term; visitEntityDetailsPage( entityObj.term, entityObj.serviceName, @@ -624,27 +625,27 @@ export const addNewTagToEntity = (entityObj, term) => { ); cy.wait(500); cy.get( - '[data-testid="classification-tags-0"] [data-testid="entity-tags"] [data-testid="add-tag"]' + '[data-testid="Classification-tags-0"] [data-testid="entity-tags"] [data-testid="add-tag"]' ) .eq(0) .should('be.visible') .scrollIntoView() .click(); - cy.get('[data-testid="tag-selector"] input').should('be.visible').type(term); + cy.get('[data-testid="tag-selector"] input').should('be.visible').type(name); - cy.get(`[title="${term}"]`).should('be.visible').click(); + cy.get(`[data-testid="tag-${fqn}"]`).should('be.visible').click(); // to close popup cy.clickOutside(); - cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains(term); + cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains(name); cy.get('[data-testid="saveAssociatedTag"]') .scrollIntoView() .should('be.visible') .click(); - cy.get('[data-testid="classification-tags-0"] [data-testid="tags-container"]') + cy.get('[data-testid="Classification-tags-0"] [data-testid="tags-container"]') .scrollIntoView() - .contains(term) + .contains(name) .should('exist'); }; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.js b/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.js index 183bad13a85..9ca8bbc7053 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.js @@ -226,6 +226,7 @@ export const NEW_TAG = { name: 'CypressTag', displayName: 'CypressTag', renamedName: 'CypressTag-1', + fqn: `${NEW_CLASSIFICATION.name}.CypressTag`, description: 'This is the CypressTag', }; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js b/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js index 26f3e5ffc8d..97ae6050f3a 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/tagsAddRemove.constants.js @@ -18,8 +18,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ entity: 'tables', serviceName: 'sample_data', fieldName: 'SKU', - tags: ['Personal', 'Sensitive'], - entityTags: 'Personal', + tags: ['PersonalData.Personal', 'PII.Sensitive'], }, { term: 'address_book', @@ -27,8 +26,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ entity: 'topics', serviceName: 'sample_kafka', fieldName: 'AddressBook', - tags: ['Personal', 'Sensitive'], - entityTags: 'Personal', + tags: ['PersonalData.Personal', 'PII.Sensitive'], }, { term: 'deck.gl Demo', @@ -37,8 +35,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ insideEntity: 'charts', serviceName: 'sample_superset', fieldName: 'e3cfd274-44f8-4bf3-b75d-d40cf88869ba', - tags: ['Personal', 'Sensitive'], - entityTags: 'Personal', + tags: ['PersonalData.Personal', 'PII.Sensitive'], }, { term: 'dim_address_etl', @@ -46,8 +43,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ entity: 'pipelines', serviceName: 'sample_airflow', fieldName: 'dim_address_task', - tags: ['Personal', 'Sensitive'], - entityTags: 'Personal', + tags: ['PersonalData.Personal', 'PII.Sensitive'], }, { term: 'eta_predictions', @@ -55,7 +51,6 @@ export const TAGS_ADD_REMOVE_ENTITIES = [ entity: 'mlmodels', serviceName: 'mlflow_svc', fieldName: 'sales', - tags: ['Personal', 'Sensitive'], - entityTags: 'Personal', + tags: ['PersonalData.Personal', 'PII.Sensitive'], }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/TagsAddRemove.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/TagsAddRemove.spec.js index 0c7da7d6bf3..d961f4e2242 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/TagsAddRemove.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/TagsAddRemove.spec.js @@ -23,9 +23,9 @@ const addTags = (tag) => { .scrollIntoView() .should('be.visible') .click() - .type(tag); + .type(tag.split('.')[1]); - cy.get(`[title="${tag}"]`).should('be.visible').click(); + cy.get(`[data-testid='tag-${tag}']`).should('be.visible').click(); cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains(tag); }; @@ -38,7 +38,7 @@ const checkTags = (tag, checkForParentEntity) => { .contains(tag); } else { cy.get( - '[data-testid="classification-tags-0"] [data-testid="tags-container"] [data-testid="entity-tags"] ' + '[data-testid="Classification-tags-0"] [data-testid="tags-container"] [data-testid="entity-tags"] ' ) .scrollIntoView() .contains(tag); @@ -56,9 +56,12 @@ const removeTags = (checkForParentEntity) => { cy.get('[data-testid="remove-tags"]').should('be.visible').click(); - cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); + cy.get('[data-testid="saveAssociatedTag"]') + .scrollIntoView() + .should('be.visible') + .click(); } else { - cy.get('[data-testid="classification-tags-0"] [data-testid="edit-button"]') + cy.get('[data-testid="Classification-tags-0"] [data-testid="edit-button"]') .scrollIntoView() .trigger('mouseover') .click(); @@ -91,11 +94,14 @@ describe('Check if tags addition and removal flow working properly from tables', .should('be.visible') .click(); - addTags(entityDetails.entityTags); + addTags(entityDetails.tags[0]); interceptURL('PATCH', `/api/v1/${entityDetails.entity}/*`, 'tagsChange'); - cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); + cy.get('[data-testid="saveAssociatedTag"]') + .scrollIntoView() + .should('be.visible') + .click(); verifyResponseStatusCode('@tagsChange', 200); @@ -105,13 +111,13 @@ describe('Check if tags addition and removal flow working properly from tables', if (entityDetails.entity === 'mlmodels') { cy.get( - `[data-testid="feature-card-${entityDetails.fieldName}"] [data-testid="classification-tags-0"] [data-testid="add-tag"]` + `[data-testid="feature-card-${entityDetails.fieldName}"] [data-testid="Classification-tags-0"] [data-testid="add-tag"]` ) .should('be.visible') .click(); } else { cy.get( - `.ant-table-tbody [data-testid="classification-tags-0"] [data-testid="tags-container"] [data-testid="entity-tags"]` + `.ant-table-tbody [data-testid="Classification-tags-0"] [data-testid="tags-container"] [data-testid="entity-tags"]` ) .scrollIntoView() .should('be.visible') diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.js index 83bf7f86de2..6d11ddd988b 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.js @@ -45,7 +45,6 @@ const visitGlossaryTermPage = (termName, fqn, fetchPermission) => { '/api/v1/permissions/glossaryTerm/*', 'waitForTermPermission' ); - interceptURL('GET', '/api/v1/tags*', 'getTagsList'); cy.get(`[data-row-key="${fqn}"]`) .scrollIntoView() @@ -55,7 +54,6 @@ const visitGlossaryTermPage = (termName, fqn, fetchPermission) => { .click(); verifyResponseStatusCode('@getGlossaryTerms', 200); - verifyResponseStatusCode('@getTagsList', 200); // verifyResponseStatusCode('@glossaryAPI', 200); if (fetchPermission) { verifyResponseStatusCode('@waitForTermPermission', 200); @@ -244,15 +242,23 @@ const updateSynonyms = (uSynonyms) => { }; const updateTags = (inTerm) => { + // visit glossary page + interceptURL( + 'GET', + `/api/v1/search/query?q=%2A&index=tag_search_index&from=0&size=10&query_filter=%7B%7D`, + 'tags' + ); cy.get( '[data-testid="tags-input-container"] [data-testid="add-tag"]' ).click(); + verifyResponseStatusCode('@tags', 200); + cy.get('[data-testid="tag-selector"]') .scrollIntoView() .should('be.visible') .type('personal'); - cy.get('[role="tree"] [title="Personal"]').click(); + cy.get('[data-testid="tag-PersonalData.Personal"]').click(); // to close popup cy.clickOutside(); @@ -558,10 +564,6 @@ describe('Glossary page should work properly', () => { }); it('Updating data of glossary should work properly', () => { - // visit glossary page - interceptURL('GET', `/api/v1/tags?limit=*`, 'tags'); - verifyResponseStatusCode('@tags', 200); - // updating tags updateTags(false); @@ -578,16 +580,12 @@ describe('Glossary page should work properly', () => { // visit glossary page interceptURL('GET', `/api/v1/glossaryTerms?glossary=*`, 'glossaryTerm'); interceptURL('GET', `/api/v1/permissions/glossary/*`, 'permissions'); - interceptURL('GET', `/api/v1/tags?limit=*`, 'tags'); cy.get('.ant-menu-item') .contains(NEW_GLOSSARY_1.name) .should('be.visible') .click(); - verifyMultipleResponseStatusCode( - ['@glossaryTerm', '@permissions', '@tags'], - 200 - ); + verifyMultipleResponseStatusCode(['@glossaryTerm', '@permissions'], 200); // visit glossary term page interceptURL( @@ -606,12 +604,7 @@ describe('Glossary page should work properly', () => { .should('be.visible') .click(); verifyMultipleResponseStatusCode( - [ - '@glossaryTermDetails', - '@listGlossaryTerm', - '@glossaryTermPermission', - '@tags', - ], + ['@glossaryTermDetails', '@listGlossaryTerm', '@glossaryTermPermission'], 200 ); cy.wait(5000); // adding manual wait as edit icon takes time to appear on screen diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js index 06832e2fac7..b81f15f34a8 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js @@ -234,7 +234,7 @@ describe('Tags page should work', () => { it('Use newly created tag to any entity should work', () => { const entity = SEARCH_ENTITY_TABLE.table_3; - addNewTagToEntity(entity, `${NEW_TAG.name}`); + addNewTagToEntity(entity, NEW_TAG); }); it('Add tag at DatabaseSchema level should work', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContainerDetail/ContainerDataModel/ContainerDataModel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContainerDetail/ContainerDataModel/ContainerDataModel.tsx index a0df65006ab..06496a86360 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContainerDetail/ContainerDataModel/ContainerDataModel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContainerDetail/ContainerDataModel/ContainerDataModel.tsx @@ -17,10 +17,6 @@ import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlac import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer'; import { ModalWithMarkdownEditor } from 'components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import TableTags from 'components/TableTags/TableTags.component'; -import { - GlossaryTermDetailsProps, - TagsDetailsProps, -} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface'; import { TABLE_SCROLL_VALUE } from 'constants/Table.constants'; import { Column, TagLabel } from 'generated/entity/data/container'; import { TagSource } from 'generated/type/tagLabel'; @@ -33,12 +29,7 @@ import { updateContainerColumnTags, } from 'utils/ContainerDetailUtils'; import { getEntityName } from 'utils/EntityUtils'; -import { - getGlossaryTermHierarchy, - getGlossaryTermsList, -} from 'utils/GlossaryUtils'; import { getTableExpandableConfig } from 'utils/TableUtils'; -import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils'; import { CellRendered, ContainerDataModelProps, @@ -56,40 +47,6 @@ const ContainerDataModel: FC = ({ const [editContainerColumnDescription, setEditContainerColumnDescription] = useState(); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); - - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - const handleFieldTagsChange = useCallback( async (selectedTags: EntityTags[], editColumnTag: Column) => { const newSelectedTags: TagOption[] = map(selectedTags, (tag) => ({ @@ -221,16 +178,11 @@ const ContainerDataModel: FC = ({ width: 300, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} /> @@ -244,16 +196,11 @@ const ContainerDataModel: FC = ({ width: 300, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} /> @@ -261,18 +208,11 @@ const ContainerDataModel: FC = ({ }, ], [ - classificationTags, - tagFetchFailed, - glossaryTags, - fetchClassificationTags, - fetchGlossaryTags, - handleFieldTagsChange, - hasDescriptionEditAccess, - hasTagEditAccess, - editContainerColumnDescription, isReadOnly, - isTagLoading, - isGlossaryLoading, + hasTagEditAccess, + hasDescriptionEditAccess, + editContainerColumnDescription, + handleFieldTagsChange, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx index 0d01bcb1c92..25a2be30d1d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx @@ -26,11 +26,7 @@ import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAss import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; import TableTags from 'components/TableTags/TableTags.component'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1'; -import { - GlossaryTermDetailsProps, - TagsDetailsProps, -} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import { getDashboardDetailsPath } from 'constants/constants'; import { compare } from 'fast-json-patch'; import { TagSource } from 'generated/type/schema'; @@ -42,11 +38,6 @@ import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; import { restoreDashboard } from 'rest/dashboardAPI'; import { getEntityName, getEntityThreadLink } from 'utils/EntityUtils'; -import { - getGlossaryTermHierarchy, - getGlossaryTermsList, -} from 'utils/GlossaryUtils'; -import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils'; import { ReactComponent as ExternalLinkIcon } from '../../assets/svg/external-links.svg'; import { EntityField } from '../../constants/Feeds.constants'; import { EntityTabs, EntityType } from '../../enums/entity.enum'; @@ -106,10 +97,6 @@ const DashboardDetails = ({ EntityFieldThreadCount[] >([]); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [threadLink, setThreadLink] = useState(''); const [threadType, setThreadType] = useState( @@ -122,13 +109,6 @@ const DashboardDetails = ({ Array >([]); - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); - const { owner, description, @@ -231,30 +211,6 @@ const DashboardDetails = ({ [dashboardDetails] ); - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - useEffect(() => { if (charts) { getAllChartsPermissions(charts); @@ -555,16 +511,11 @@ const DashboardDetails = ({ render: (tags: TagLabel[], record: ChartType, index: number) => { return ( - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleChartTagSelection} hasTagEditAccess={hasEditTagAccess(record)} index={index} isReadOnly={deleted} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} /> @@ -579,34 +530,18 @@ const DashboardDetails = ({ width: 300, render: (tags: TagLabel[], record: ChartType, index: number) => ( - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleChartTagSelection} hasTagEditAccess={hasEditTagAccess(record)} index={index} isReadOnly={deleted} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} /> ), }, ], - [ - deleted, - isTagLoading, - isGlossaryLoading, - tagFetchFailed, - glossaryTags, - classificationTags, - renderDescription, - fetchGlossaryTags, - handleChartTagSelection, - hasEditTagAccess, - ] + [deleted, renderDescription, handleChartTagSelection, hasEditTagAccess] ); const tabs = useMemo( @@ -663,7 +598,7 @@ const DashboardDetails = ({ data-testid="entity-right-panel" flex="320px"> - - - - { const { t } = useTranslation(); const [editColumnDescription, setEditColumnDescription] = useState(); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); - - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; const handleFieldTagsChange = useCallback( async (selectedTags: EntityTags[], editColumnTag: Column) => { @@ -192,16 +149,11 @@ const ModelTab = ({ width: 300, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasEditTagsPermission} index={index} isReadOnly={isReadOnly} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} /> @@ -215,16 +167,11 @@ const ModelTab = ({ width: 300, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasEditTagsPermission} index={index} isReadOnly={isReadOnly} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} /> @@ -232,18 +179,11 @@ const ModelTab = ({ }, ], [ - fetchClassificationTags, - fetchGlossaryTags, - handleFieldTagsChange, - glossaryTags, - classificationTags, - tagFetchFailed, + isReadOnly, hasEditTagsPermission, editColumnDescription, hasEditDescriptionPermission, - isReadOnly, - isTagLoading, - isGlossaryLoading, + handleFieldTagsChange, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx index 20cc6718997..6dfb93650c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx @@ -16,10 +16,7 @@ import { ColumnsType } from 'antd/lib/table'; import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg'; import FilterTablePlaceHolder from 'components/common/error-with-placeholder/FilterTablePlaceHolder'; import TableTags from 'components/TableTags/TableTags.component'; -import { - GlossaryTermDetailsProps, - TagsDetailsProps, -} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface'; +import { DE_ACTIVE_COLOR } from 'constants/constants'; import { TABLE_SCROLL_VALUE } from 'constants/Table.constants'; import { LabelType, State, TagSource } from 'generated/type/schema'; import { @@ -36,11 +33,6 @@ import { EntityTags, TagOption } from 'Models'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; -import { - getGlossaryTermHierarchy, - getGlossaryTermsList, -} from 'utils/GlossaryUtils'; -import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils'; import { ReactComponent as IconRequest } from '../../assets/svg/request-icon.svg'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { EntityField } from '../../constants/Feeds.constants'; @@ -91,12 +83,6 @@ const EntityTable = ({ const { t } = useTranslation(); const [searchedColumns, setSearchedColumns] = useState([]); - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); const sortByOrdinalPosition = useMemo( () => sortBy(tableColumns, 'ordinalPosition'), @@ -113,34 +99,6 @@ const EntityTable = ({ index: number; }>(); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - const handleEditColumn = (column: Column, index: number): void => { setEditColumn({ column, index }); }; @@ -355,9 +313,10 @@ const EntityTable = ({ trigger="hover" zIndex={9999}> @@ -425,13 +384,14 @@ const EntityTable = ({ {hasDescriptionEditAccess && ( <> @@ -525,21 +485,16 @@ const EntityTable = ({ width: 250, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="classification-tags" entityFieldTasks={entityFieldTasks} entityFieldThreads={entityFieldThreads} entityFqn={entityFqn} - fetchTags={fetchClassificationTags} getColumnFieldFQN={getColumnFieldFQN(record)} getColumnName={getColumnName} handleTagSelection={handleTagSelection} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} onRequestTagsHandler={onRequestTagsHandler} @@ -556,21 +511,16 @@ const EntityTable = ({ width: 250, render: (tags: TagLabel[], record: Column, index: number) => ( - dataTestId="glossary-tags" entityFieldTasks={entityFieldTasks} entityFieldThreads={entityFieldThreads} entityFqn={entityFqn} - fetchTags={fetchGlossaryTags} getColumnFieldFQN={getColumnFieldFQN(record)} getColumnName={getColumnName} handleTagSelection={handleTagSelection} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} onRequestTagsHandler={onRequestTagsHandler} @@ -581,27 +531,21 @@ const EntityTable = ({ }, ], [ + entityFqn, + isReadOnly, entityFieldTasks, entityFieldThreads, - entityFqn, tableConstraints, - isTagLoading, - isGlossaryLoading, + hasTagEditAccess, handleUpdate, handleTagSelection, renderDataTypeDisplay, renderDescription, - fetchGlossaryTags, getColumnName, handleTagSelection, - hasTagEditAccess, - isReadOnly, - tagFetchFailed, - glossaryTags, onRequestTagsHandler, onUpdateTagsHandler, onThreadLinkSelect, - classificationTags, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.interface.ts new file mode 100644 index 00000000000..95159768b16 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.interface.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Paging } from 'generated/type/paging'; + +export type SelectOption = { + label: string; + value: string; +}; + +export interface InfiniteSelectScrollProps { + mode?: 'multiple'; + placeholder?: string; + debounceTimeout?: number; + onChange?: (newValue: string | string[]) => void; + fetchOptions: ( + search: string, + page: number + ) => Promise<{ + data: SelectOption[]; + paging: Paging; + }>; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.tsx b/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.tsx new file mode 100644 index 00000000000..c5f22c0bf62 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/InfiniteSelectScroll/InfiniteSelectScroll.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Select, Space, Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import Loader from 'components/Loader/Loader'; +import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; +import { Paging } from 'generated/type/paging'; +import { debounce } from 'lodash'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { tagRender } from 'utils/TagsUtils'; +import { showErrorToast } from 'utils/ToastUtils'; +import Fqn from '../../utils/Fqn'; +import { + InfiniteSelectScrollProps, + SelectOption, +} from './InfiniteSelectScroll.interface'; + +const InfiniteSelectScroll: FC = ({ + mode, + onChange, + fetchOptions, + debounceTimeout = 800, + ...props +}) => { + const [isLoading, setIsLoading] = useState(false); + const [hasContentLoading, setHasContentLoading] = useState(false); + const [options, setOptions] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [paging, setPaging] = useState({} as Paging); + const [currentPage, setCurrentPage] = useState(1); + + const loadOptions = useCallback( + async (value: string) => { + setOptions([]); + setIsLoading(true); + try { + const res = await fetchOptions(value, currentPage); + setOptions(res.data); + setPaging(res.paging); + setSearchValue(value); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setIsLoading(false); + } + }, + [fetchOptions] + ); + + const debounceFetcher = useMemo( + () => debounce(loadOptions, debounceTimeout), + [loadOptions, debounceTimeout] + ); + + const tagOptions = useMemo(() => { + const newTags = options + .filter((tag) => !tag.label?.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)) // To filter out Tier tags + .map((tag) => { + const parts = Fqn.split(tag.label); + const lastPartOfTag = parts.slice(-1).join(FQN_SEPARATOR_CHAR); + parts.pop(); + + return { + label: tag.label, + displayName: ( + + + {parts.join(FQN_SEPARATOR_CHAR)} + + {lastPartOfTag} + + ), + value: tag.value, + }; + }); + + return newTags; + }, [options]); + + const onScroll = async (e: React.UIEvent) => { + const { currentTarget } = e; + if ( + currentTarget.scrollTop + currentTarget.offsetHeight === + currentTarget.scrollHeight + ) { + if (options.length < paging.total) { + try { + setHasContentLoading(true); + const res = await fetchOptions(searchValue, currentPage + 1); + setOptions((prev) => [...prev, ...res.data]); + setPaging(res.paging); + setCurrentPage((prev) => prev + 1); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setHasContentLoading(false); + } + } + } + }; + + const dropdownRender = (menu: React.ReactElement) => ( + <> + {menu} + {hasContentLoading ? : null} + + ); + + return ( + + ); +}; + +export default InfiniteSelectScroll; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx index 0f806dfe59d..1b04334a954 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MlModelDetail/MlModelDetail.component.tsx @@ -23,7 +23,7 @@ import PageLayoutV1 from 'components/containers/PageLayoutV1'; import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import { TagLabel, TagSource } from 'generated/type/schema'; import { EntityFieldThreadCount } from 'interface/feed.interface'; import { isEmpty } from 'lodash'; @@ -419,7 +419,7 @@ const MlModelDetail: FC = ({ data-testid="entity-right-panel" flex="320px"> - = ({ onThreadLinkSelect={handleThreadLinkSelect} /> - (false); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); const hasEditPermission = useMemo( () => permissions.EditTags || permissions.EditAll, [permissions] @@ -121,30 +103,6 @@ const MlModelFeaturesList = ({ } }; - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - if (mlFeatures && mlFeatures.length) { return ( @@ -204,16 +162,11 @@ const MlModelFeaturesList = ({ showInlineEditTagButton - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleTagsChange} hasTagEditAccess={hasEditPermission} index={index} isReadOnly={isDeleted} - isTagLoading={isGlossaryLoading} record={feature} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={feature.tags ?? []} type={TagSource.Glossary} /> @@ -231,16 +184,11 @@ const MlModelFeaturesList = ({ showInlineEditTagButton - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleTagsChange} hasTagEditAccess={hasEditPermission} index={index} isReadOnly={isDeleted} - isTagLoading={isTagLoading} record={feature} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={feature.tags ?? []} type={TagSource.Classification} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx index 2c94a8f10c6..44ddd3ad911 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx @@ -29,11 +29,7 @@ import ExecutionsTab from 'components/Execution/Execution.component'; import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; import TableTags from 'components/TableTags/TableTags.component'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1'; -import { - GlossaryTermDetailsProps, - TagsDetailsProps, -} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import TasksDAGView from 'components/TasksDAGView/TasksDAGView'; import { EntityField } from 'constants/Feeds.constants'; import { compare } from 'fast-json-patch'; @@ -45,11 +41,6 @@ import { useTranslation } from 'react-i18next'; import { Link, useHistory, useParams } from 'react-router-dom'; import { postThread } from 'rest/feedsAPI'; import { restorePipeline } from 'rest/pipelineAPI'; -import { - getGlossaryTermHierarchy, - getGlossaryTermsList, -} from 'utils/GlossaryUtils'; -import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils'; import { ReactComponent as ExternalLinkIcon } from '../../assets/svg/external-links.svg'; import { getPipelineDetailsPath, @@ -150,16 +141,6 @@ const PipelineDetails = ({ ); const [activeTab, setActiveTab] = useState(PIPELINE_TASK_TABS.LIST_VIEW); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); - - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); const { getEntityPermission } = usePermissionProvider(); @@ -202,30 +183,6 @@ const PipelineDetails = ({ } }, [pipelineDetails.id, getEntityPermission, setPipelinePermissions]); - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - useEffect(() => { if (pipelineDetails.id) { fetchResourcePermission(); @@ -477,16 +434,11 @@ const PipelineDetails = ({ width: 300, render: (tags, record, index) => ( - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleTableTagSelection} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={deleted} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} /> @@ -500,16 +452,11 @@ const PipelineDetails = ({ width: 300, render: (tags, record, index) => ( - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleTableTagSelection} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={deleted} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} /> @@ -517,18 +464,11 @@ const PipelineDetails = ({ }, ], [ - fetchGlossaryTags, - fetchClassificationTags, - handleTableTagSelection, - classificationTags, + deleted, + editTask, hasTagEditAccess, pipelinePermissions, - editTask, - deleted, - isTagLoading, - isGlossaryLoading, - tagFetchFailed, - glossaryTags, + handleTableTagSelection, ] ); @@ -653,7 +593,7 @@ const PipelineDetails = ({ data-testid="entity-right-panel" flex="320px"> - - ({ tags, record, index, + type, + entityFqn, isReadOnly, - isTagLoading, hasTagEditAccess, - onUpdateTagsHandler, - onRequestTagsHandler, - getColumnName, + entityFieldThreads, getColumnFieldFQN, entityFieldTasks, - onThreadLinkSelect, - entityFieldThreads, - entityFqn, - tagList, - handleTagSelection, - type, - fetchTags, - tagFetchFailed, - dataTestId, showInlineEditTagButton, + getColumnName, + onUpdateTagsHandler, + onRequestTagsHandler, + onThreadLinkSelect, + handleTagSelection, }: TableTagsComponentProps) => { const { t } = useTranslation(); - const [isEdit, setIsEdit] = useState(false); - - const showEditTagButton = useMemo( - () => - tags.length && hasTagEditAccess && !isEdit && !showInlineEditTagButton, - [tags, type, hasTagEditAccess, isEdit, showInlineEditTagButton] - ); const hasTagOperationAccess = useMemo( () => @@ -74,14 +61,6 @@ const TableTags = ({ ] ); - const addButtonHandler = useCallback(() => { - setIsEdit(true); - // Fetch Classification or Glossary only once - if (isEmpty(tagList) || tagFetchFailed) { - fetchTags(); - } - }, [tagList, tagFetchFailed, fetchTags]); - const getRequestTagsElement = useMemo(() => { const hasTags = !isEmpty(record.tags || []); @@ -117,83 +96,58 @@ const TableTags = ({ }, [record, onUpdateTagsHandler, onRequestTagsHandler]); return ( -
+
- setIsEdit(false)} onSelectionChange={async (selectedTags) => { await handleTagSelection(selectedTags, record); - setIsEdit(false); - }} - /> + }}> + <> + {!isReadOnly && ( +
+ {hasTagOperationAccess && ( + <> + {/* Request and Update tags */} + {type === TagSource.Classification && getRequestTagsElement} - {!isReadOnly && ( -
- {showEditTagButton ? ( -
)} -
- )} + +
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts index bd0622e478d..147f9bd122d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.interface.ts @@ -11,7 +11,6 @@ * limitations under the License. */ -import { HierarchyTagsProps } from 'components/Tag/TagsContainerV1/TagsContainerV1.interface'; import { MlFeature } from 'generated/entity/data/mlmodel'; import { Task } from 'generated/entity/data/pipeline'; import { Field } from 'generated/entity/data/topic'; @@ -24,13 +23,11 @@ import { EntityFieldThreads } from '../../interface/feed.interface'; export interface TableTagsComponentProps { tags: TagLabel[]; - tagList: HierarchyTagsProps[]; onUpdateTagsHandler?: (cell: T) => void; isReadOnly?: boolean; entityFqn?: string; record: T; index: number; - isTagLoading: boolean; hasTagEditAccess: boolean; handleTagSelection: ( selectedTags: EntityTags[], @@ -42,10 +39,7 @@ export interface TableTagsComponentProps { entityFieldTasks?: EntityFieldThreads[]; onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; entityFieldThreads?: EntityFieldThreads[]; - tagFetchFailed: boolean; type: TagSource; - fetchTags: () => Promise; - dataTestId: string; showInlineEditTagButton?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.test.tsx index 4242447c429..64788f4fa6c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableTags/TableTags.test.tsx @@ -62,8 +62,6 @@ const requestUpdateTags = { }; const mockProp = { - placeholder: 'Search Tags', - dataTestId: 'tag-container', tags: [], record: { constraint: Constraint.Null, @@ -81,7 +79,6 @@ const mockProp = { }, index: 0, isReadOnly: false, - isTagLoading: false, hasTagEditAccess: true, entityFieldTasks: [], onThreadLinkSelect: jest.fn(), @@ -94,11 +91,8 @@ const mockProp = { }, ], entityFqn: 'sample_data.ecommerce_db.shopify.raw_customer', - tagList: [], handleTagSelection: jest.fn(), type: TagSource.Classification, - fetchTags: jest.fn(), - tagFetchFailed: false, }; describe('Test EntityTableTags Component', () => { @@ -107,7 +101,7 @@ describe('Test EntityTableTags Component', () => { wrapper: MemoryRouter, }); - const tagContainer = await screen.findByTestId('tag-container-0'); + const tagContainer = await screen.findByTestId('Classification-tags-0'); expect(tagContainer).toBeInTheDocument(); }); @@ -128,7 +122,7 @@ describe('Test EntityTableTags Component', () => { } ); - const tagContainer = await screen.findByTestId('tag-container-0'); + const tagContainer = await screen.findByTestId('Classification-tags-0'); expect(tagContainer).toBeInTheDocument(); }); @@ -148,7 +142,7 @@ describe('Test EntityTableTags Component', () => { } ); - const tagContainer = await screen.findByTestId('tag-container-0'); + const tagContainer = await screen.findByTestId('Classification-tags-0'); const tagPersonal = await screen.findByTestId('tag-PersonalData.Personal'); expect(tagContainer).toBeInTheDocument(); @@ -170,7 +164,7 @@ describe('Test EntityTableTags Component', () => { } ); - const tagContainer = await screen.findByTestId('tag-container-0'); + const tagContainer = await screen.findByTestId('Classification-tags-0'); const requestTags = screen.queryByTestId('field-thread-element'); expect(tagContainer).toBeInTheDocument(); @@ -193,7 +187,7 @@ describe('Test EntityTableTags Component', () => { } ); - const tagContainer = await screen.findByTestId('tag-container-0'); + const tagContainer = await screen.findByTestId('Classification-tags-0'); const requestTags = await screen.findAllByTestId('field-thread-element'); expect(tagContainer).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts new file mode 100644 index 00000000000..5b4ec3f39e4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.interface.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ThreadType } from 'generated/api/feed/createThread'; +import { TagSource } from 'generated/type/tagLabel'; +import { EntityTags } from 'Models'; +import { ReactElement } from 'react'; + +export type TagsContainerV2Props = { + permission: boolean; + selectedTags: EntityTags[]; + entityType?: string; + entityThreadLink?: string; + entityFqn?: string; + tagType: TagSource; + showHeader?: boolean; + showBottomEditButton?: boolean; + showInlineEditButton?: boolean; + children?: ReactElement; + onSelectionChange: (selectedTags: EntityTags[]) => Promise; + onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx new file mode 100644 index 00000000000..f962a6ba647 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx @@ -0,0 +1,411 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd'; +import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; +import { AxiosError } from 'axios'; +import { TableTagsProps } from 'components/TableTags/TableTags.interface'; +import { DE_ACTIVE_COLOR, PAGE_SIZE } from 'constants/constants'; +import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants'; +import { EntityType } from 'enums/entity.enum'; +import { SearchIndex } from 'enums/search.enum'; +import { Paging } from 'generated/type/paging'; +import { TagSource } from 'generated/type/tagLabel'; +import { isEmpty } from 'lodash'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { getGlossaryTerms } from 'rest/glossaryAPI'; +import { searchQuery } from 'rest/searchAPI'; +import { getTags } from 'rest/tagAPI'; +import { + formatSearchGlossaryTermResponse, + formatSearchTagsResponse, +} from 'utils/APIUtils'; +import { getEntityFeedLink } from 'utils/EntityUtils'; +import { getFilterTags } from 'utils/TableTags/TableTags.utils'; +import { getTagPlaceholder } from 'utils/TagsUtils'; +import { + getRequestTagsPath, + getUpdateTagsPath, + TASK_ENTITIES, +} from 'utils/TasksUtils'; +import { ReactComponent as IconComments } from '../../../assets/svg/comment.svg'; +import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg'; +import TagSelectForm from '../TagsSelectForm/TagsSelectForm.component'; +import TagsV1 from '../TagsV1/TagsV1.component'; +import TagsViewer from '../TagsViewer/tags-viewer'; +import { TagsContainerV2Props } from './TagsContainerV2.interface'; + +const TagsContainerV2 = ({ + permission, + selectedTags, + entityType, + entityThreadLink, + entityFqn, + tagType, + showHeader = true, + showBottomEditButton, + showInlineEditButton, + onSelectionChange, + onThreadLinkSelect, + children, +}: TagsContainerV2Props) => { + const history = useHistory(); + const [form] = Form.useForm(); + const { t } = useTranslation(); + + const [isEditTags, setIsEditTags] = useState(false); + const [tags, setTags] = useState(); + + const isGlossaryType = useMemo( + () => tagType === TagSource.Glossary, + [tagType] + ); + + const showAddTagButton = useMemo( + () => permission && isEmpty(tags?.[tagType]), + [permission, tags?.[tagType]] + ); + + const selectedTagsInternal = useMemo( + () => tags?.[tagType].map(({ tagFQN }) => tagFQN), + [tags, tagType] + ); + + const fetchTags = useCallback( + async ( + searchQueryParam: string, + page: number + ): Promise<{ + data: { + label: string; + value: string; + }[]; + paging: Paging; + }> => { + try { + const tagResponse = await searchQuery({ + query: searchQueryParam ? searchQueryParam : '*', + pageNumber: page, + pageSize: PAGE_SIZE, + queryFilter: {}, + searchIndex: SearchIndex.TAG, + }); + + return Promise.resolve({ + data: formatSearchTagsResponse(tagResponse.hits.hits ?? []).map( + (item) => ({ + label: item.fullyQualifiedName ?? '', + value: item.fullyQualifiedName ?? '', + }) + ), + paging: { + total: tagResponse.hits.total.value, + }, + }); + } catch (error) { + return Promise.reject({ data: (error as AxiosError).response }); + } + }, + [getTags] + ); + + const fetchGlossaryList = useCallback( + async ( + searchQueryParam: string, + page: number + ): Promise<{ + data: { + label: string; + value: string; + }[]; + paging: Paging; + }> => { + try { + const glossaryResponse = await searchQuery({ + query: searchQueryParam ? searchQueryParam : '*', + pageNumber: page, + pageSize: 10, + queryFilter: {}, + searchIndex: SearchIndex.GLOSSARY, + }); + + return Promise.resolve({ + data: formatSearchGlossaryTermResponse( + glossaryResponse.hits.hits ?? [] + ).map((item) => ({ + label: item.fullyQualifiedName ?? '', + value: item.fullyQualifiedName ?? '', + })), + paging: { + total: glossaryResponse.hits.total.value, + }, + }); + } catch (error) { + return Promise.reject({ data: (error as AxiosError).response }); + } + }, + [searchQuery, getGlossaryTerms, formatSearchGlossaryTermResponse] + ); + + const fetchAPI = useCallback( + (searchValue: string, page: number) => { + if (tagType === TagSource.Classification) { + return fetchTags(searchValue, page); + } else { + return fetchGlossaryList(searchValue, page); + } + }, + [tagType, fetchTags, fetchGlossaryList] + ); + + const showNoDataPlaceholder = useMemo( + () => !showAddTagButton && isEmpty(tags?.[tagType]), + [showAddTagButton, tags?.[tagType]] + ); + + const handleSave = async (data: string[]) => { + const updatedTags = data.map((t) => ({ + tagFQN: t, + source: tagType, + })); + + await onSelectionChange([ + ...updatedTags, + ...((isGlossaryType + ? tags?.[TagSource.Classification] + : tags?.[TagSource.Glossary]) ?? []), + ]); + form.resetFields(); + setIsEditTags(false); + }; + + const handleCancel = useCallback(() => { + setIsEditTags(false); + form.resetFields(); + }, [form]); + + const handleAddClick = useCallback(() => { + setIsEditTags(true); + }, [isGlossaryType]); + + const addTagButton = useMemo( + () => + showAddTagButton ? ( + + + + ) : null, + [showAddTagButton] + ); + + const renderTags = useMemo( + () => ( + + ), + [showNoDataPlaceholder, tags?.[tagType]] + ); + + const tagsSelectContainer = useMemo(() => { + return ( + + ); + }, [ + isGlossaryType, + selectedTagsInternal, + getTagPlaceholder, + fetchAPI, + handleCancel, + handleSave, + ]); + + const handleRequestTags = () => { + history.push(getRequestTagsPath(entityType as string, entityFqn as string)); + }; + const handleUpdateTags = () => { + history.push(getUpdateTagsPath(entityType as string, entityFqn as string)); + }; + + const requestTagElement = useMemo(() => { + const hasTags = !isEmpty(tags?.[tagType]); + + return TASK_ENTITIES.includes(entityType as EntityType) ? ( + + + + ) : null; + }, [tags?.[tagType], handleUpdateTags, handleRequestTags]); + + const conversationThreadElement = useMemo( + () => ( + + + + ), + [ + entityType, + entityFqn, + entityThreadLink, + getEntityFeedLink, + onThreadLinkSelect, + ] + ); + + const header = useMemo(() => { + return ( + showHeader && ( +
+
+ + {isGlossaryType + ? t('label.glossary-term') + : t('label.tag-plural')} + + + {permission && !isEmpty(tags?.[tagType]) && !isEditTags && ( +
+ {permission && ( + + {tagType === TagSource.Classification && requestTagElement} + {onThreadLinkSelect && conversationThreadElement} + + )} +
+ ) + ); + }, [ + tags, + tagType, + showHeader, + isEditTags, + permission, + isGlossaryType, + requestTagElement, + conversationThreadElement, + ]); + + const editTagButton = useMemo( + () => + permission && !isEmpty(tags?.[tagType]) ? ( +
- {isVersionView ? ( ) : ( - setIsEditTags(false)} onSelectionChange={handleTagSelection} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx index 0ccfa4300c7..08ae5e691b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx @@ -22,7 +22,7 @@ import PageLayoutV1 from 'components/containers/PageLayoutV1'; import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component'; import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import { getTopicDetailsPath } from 'constants/constants'; import { TagLabel } from 'generated/type/schema'; import { EntityFieldThreadCount } from 'interface/feed.interface'; @@ -340,7 +340,7 @@ const TopicDetails: React.FC = ({ data-testid="entity-right-panel" flex="320px"> - = ({ onThreadLinkSelect={onThreadLinkSelect} /> - = ({ }) => { const { t } = useTranslation(); const [editFieldDescription, setEditFieldDescription] = useState(); - const [isTagLoading, setIsTagLoading] = useState(false); - const [isGlossaryLoading, setIsGlossaryLoading] = useState(false); - const [tagFetchFailed, setTagFetchFailed] = useState(false); const [viewType, setViewType] = useState( SchemaViewType.FIELDS ); - const [glossaryTags, setGlossaryTags] = useState( - [] - ); - const [classificationTags, setClassificationTags] = useState< - TagsDetailsProps[] - >([]); - - const fetchGlossaryTags = async () => { - setIsGlossaryLoading(true); - try { - const glossaryTermList = await getGlossaryTermsList(); - setGlossaryTags(glossaryTermList); - } catch { - setTagFetchFailed(true); - } finally { - setIsGlossaryLoading(false); - } - }; - - const fetchClassificationTags = async () => { - setIsTagLoading(true); - try { - const tags = await getAllTagsList(); - setClassificationTags(tags); - } catch { - setTagFetchFailed(true); - } finally { - setIsTagLoading(false); - } - }; - const handleFieldTagsChange = async ( selectedTags: EntityTags[], editColumnTag: Field @@ -231,16 +188,11 @@ const TopicSchemaFields: FC = ({ width: 300, render: (tags: TagLabel[], record: Field, index: number) => ( - dataTestId="classification-tags" - fetchTags={fetchClassificationTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isTagLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getTagsHierarchy(classificationTags)} tags={tags} type={TagSource.Classification} /> @@ -254,16 +206,11 @@ const TopicSchemaFields: FC = ({ width: 300, render: (tags: TagLabel[], record: Field, index: number) => ( - dataTestId="glossary-tags" - fetchTags={fetchGlossaryTags} handleTagSelection={handleFieldTagsChange} hasTagEditAccess={hasTagEditAccess} index={index} isReadOnly={isReadOnly} - isTagLoading={isGlossaryLoading} record={record} - tagFetchFailed={tagFetchFailed} - tagList={getGlossaryTermHierarchy(glossaryTags)} tags={tags} type={TagSource.Glossary} /> @@ -271,17 +218,12 @@ const TopicSchemaFields: FC = ({ }, ], [ - handleFieldTagsChange, - fetchGlossaryTags, - isGlossaryLoading, + isReadOnly, messageSchema, - hasDescriptionEditAccess, hasTagEditAccess, editFieldDescription, - isReadOnly, - isTagLoading, - glossaryTags, - tagFetchFailed, + hasDescriptionEditAccess, + handleFieldTagsChange, ] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index 126c5c4d0c4..8f3d4a1bf42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -42,7 +42,7 @@ import { ResourceEntity, } from 'components/PermissionProvider/PermissionProvider.interface'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1'; +import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import { getContainerDetailPath, getVersionPath } from 'constants/constants'; import { EntityField } from 'constants/Feeds.constants'; import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum'; @@ -628,7 +628,7 @@ const ContainerPage = () => { data-testid="entity-right-panel" flex="320px"> - { onSelectionChange={handleTagSelection} onThreadLinkSelect={onThreadLinkSelect} /> - { ) : null} - { onThreadLinkSelect={onThreadLinkSelect} /> - ['hits']['hits'] +): Tag[] => { + return hits.map((d) => ({ + name: d._source.name, + description: d._source.description, + id: d._source.id, + classification: d._source.classification, + displayName: d._source.displayName, + fqdn: d._source.fullyQualifiedName, + fullyQualifiedName: d._source.fullyQualifiedName, + type: d._source.entityType, + })); +}; + export const getURLWithQueryFields = ( url: string, lstQueryFields?: string | string[], diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx index 51a892eae7c..1ee593055c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import { CheckOutlined } from '@ant-design/icons'; +import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { Tag as AntdTag, Tooltip, Typography } from 'antd'; import { RuleObject } from 'antd/lib/form'; import { ReactComponent as DeleteIcon } from 'assets/svg/ic-delete.svg'; import { AxiosError } from 'axios'; @@ -27,6 +28,7 @@ import { delimiterRegex } from 'constants/regex.constants'; import i18next from 'i18next'; import { isEmpty, isUndefined, toLower } from 'lodash'; import { Bucket, EntityTags, TagOption } from 'Models'; +import type { CustomTagProps } from 'rc-select/lib/BaseSelect'; import React from 'react'; import { getAllClassifications, @@ -389,3 +391,36 @@ export const getTagPlaceholder = (isGlossaryType: boolean): string => : i18next.t('label.search-entity', { entity: i18next.t('label.tag-plural'), }); + +export const tagRender = (customTagProps: CustomTagProps) => { + const { label, onClose } = customTagProps; + const tagLabel = getTagDisplay(label as string); + + const onPreventMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + return ( + + } + data-testid={`selected-tag-${tagLabel}`} + onClose={onClose} + onMouseDown={onPreventMouseDown}> + + + {tagLabel} + + + + ); +};