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
This commit is contained in:
Ashish Gupta 2023-06-27 17:40:25 +05:30 committed by GitHub
parent 397fc364a8
commit af6cec6c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 987 additions and 692 deletions

View File

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

View File

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

View File

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

View File

@ -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')

View File

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

View File

@ -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', () => {

View File

@ -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<ContainerDataModelProps> = ({
const [editContainerColumnDescription, setEditContainerColumnDescription] =
useState<Column>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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<ContainerDataModelProps> = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
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<ContainerDataModelProps> = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
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<ContainerDataModelProps> = ({
},
],
[
classificationTags,
tagFetchFailed,
glossaryTags,
fetchClassificationTags,
fetchGlossaryTags,
handleFieldTagsChange,
hasDescriptionEditAccess,
hasTagEditAccess,
editContainerColumnDescription,
isReadOnly,
isTagLoading,
isGlossaryLoading,
hasTagEditAccess,
hasDescriptionEditAccess,
editContainerColumnDescription,
handleFieldTagsChange,
]
);

View File

@ -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<boolean>(false);
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
@ -122,13 +109,6 @@ const DashboardDetails = ({
Array<ChartsPermissions>
>([]);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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 (
<TableTags<ChartType>
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) => (
<TableTags<ChartType>
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">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={dashboardDetails.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DASHBOARD}
@ -678,7 +613,7 @@ const DashboardDetails = ({
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={dashboardDetails.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DASHBOARD}

View File

@ -24,7 +24,7 @@ import EntityLineageComponent from 'components/EntityLineage/EntityLineage.compo
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
import SchemaEditor from 'components/schema-editor/SchemaEditor';
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import { getDataModelDetailsPath, getVersionPath } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import { CSMode } from 'enums/codemirror.enum';
@ -195,7 +195,7 @@ const DataModelDetails = ({
data-testid="entity-right-panel"
flex="320px">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={dashboardDataModelFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DASHBOARD_DATA_MODEL}
@ -205,7 +205,7 @@ const DataModelDetails = ({
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={dashboardDataModelFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.DASHBOARD_DATA_MODEL}

View File

@ -17,10 +17,6 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
import { CellRendered } from 'components/ContainerDetail/ContainerDataModel/ContainerDataModel.interface';
import { ModalWithMarkdownEditor } from 'components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TableTags from 'components/TableTags/TableTags.component';
import {
GlossaryTermDetailsProps,
TagsDetailsProps,
} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface';
import { Column } from 'generated/entity/data/dashboardDataModel';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { cloneDeep, isUndefined, map } from 'lodash';
@ -32,11 +28,6 @@ import {
updateDataModelColumnTags,
} from 'utils/DataModelsUtils';
import { getEntityName } from 'utils/EntityUtils';
import {
getGlossaryTermHierarchy,
getGlossaryTermsList,
} from 'utils/GlossaryUtils';
import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils';
import { ModelTabProps } from './ModelTab.interface';
const ModelTab = ({
@ -48,40 +39,6 @@ const ModelTab = ({
}: ModelTabProps) => {
const { t } = useTranslation();
const [editColumnDescription, setEditColumnDescription] = useState<Column>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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) => (
<TableTags<Column>
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) => (
<TableTags<Column>
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,
]
);

View File

@ -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<Column[]>([]);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
const [classificationTags, setClassificationTags] = useState<
TagsDetailsProps[]
>([]);
const sortByOrdinalPosition = useMemo(
() => sortBy(tableColumns, 'ordinalPosition'),
@ -113,34 +99,6 @@ const EntityTable = ({
index: number;
}>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(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}>
<IconRequest
height={16}
height={14}
name={t('message.request-description')}
width={16}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Popover>
</Button>
@ -425,13 +384,14 @@ const EntityTable = ({
{hasDescriptionEditAccess && (
<>
<Button
className="p-0 tw-self-start flex-center w-7 h-7 text-primary d-flex-none hover-cell-icon"
className="p-0 tw-self-start flex-center w-7 h-7 d-flex-none hover-cell-icon"
type="text"
onClick={() => handleUpdate(record, index)}>
<IconEdit
height={16}
height={14}
name={t('label.edit')}
width={16}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Button>
</>
@ -525,21 +485,16 @@ const EntityTable = ({
width: 250,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
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) => (
<TableTags<Column>
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,
]
);

View File

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

View File

@ -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<InfiniteSelectScrollProps> = ({
mode,
onChange,
fetchOptions,
debounceTimeout = 800,
...props
}) => {
const [isLoading, setIsLoading] = useState(false);
const [hasContentLoading, setHasContentLoading] = useState(false);
const [options, setOptions] = useState<SelectOption[]>([]);
const [searchValue, setSearchValue] = useState<string>('');
const [paging, setPaging] = useState<Paging>({} 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: (
<Space className="w-full" direction="vertical" size={0}>
<Typography.Paragraph
ellipsis
className="text-grey-muted m-0 p-0">
{parts.join(FQN_SEPARATOR_CHAR)}
</Typography.Paragraph>
<Typography.Text ellipsis>{lastPartOfTag}</Typography.Text>
</Space>
),
value: tag.value,
};
});
return newTags;
}, [options]);
const onScroll = async (e: React.UIEvent<HTMLDivElement>) => {
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 ? <Loader size="small" /> : null}
</>
);
return (
<Select
autoFocus
showSearch
data-testid="tag-selector"
dropdownRender={dropdownRender}
filterOption={false}
mode={mode}
notFoundContent={isLoading ? <Loader size="small" /> : null}
optionLabelProp="label"
style={{ width: '100%' }}
tagRender={tagRender}
onBlur={() => {
setCurrentPage(1);
setSearchValue('');
setOptions([]);
}}
onChange={onChange}
onFocus={() => loadOptions('')}
onPopupScroll={onScroll}
onSearch={debounceFetcher}
{...props}>
{tagOptions.map(({ label, value, displayName }) => (
<Select.Option data-testid={`tag-${value}`} key={label} value={value}>
<Tooltip
destroyTooltipOnHide
mouseEnterDelay={1.5}
placement="leftTop"
title={label}
trigger="hover">
{displayName}
</Tooltip>
</Select.Option>
))}
</Select>
);
};
export default InfiniteSelectScroll;

View File

@ -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<MlModelDetailProp> = ({
data-testid="entity-right-panel"
flex="320px">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={mlModelDetail.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.MLMODEL}
@ -434,7 +434,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
onThreadLinkSelect={handleThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={mlModelDetail.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.MLMODEL}

View File

@ -23,20 +23,11 @@ import {
} from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import TableTags from 'components/TableTags/TableTags.component';
import {
GlossaryTermDetailsProps,
TagsDetailsProps,
} from 'components/Tag/TagsContainerV1/TagsContainerV1.interface';
import { TagSource } from 'generated/type/schema';
import { isEmpty } from 'lodash';
import { EntityTags } from 'Models';
import React, { Fragment, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
getGlossaryTermHierarchy,
getGlossaryTermsList,
} from 'utils/GlossaryUtils';
import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils';
import { MlFeature } from '../../generated/entity/data/mlmodel';
import { LabelType, State } from '../../generated/type/tagLabel';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
@ -56,16 +47,7 @@ const MlModelFeaturesList = ({
{} as MlFeature
);
const [editDescription, setEditDescription] = useState<boolean>(false);
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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 (
<Fragment>
@ -204,16 +162,11 @@ const MlModelFeaturesList = ({
<Col flex="auto">
<TableTags<MlFeature>
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 = ({
<Col flex="auto">
<TableTags<MlFeature>
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}
/>

View File

@ -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<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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) => (
<TableTags<Task>
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) => (
<TableTags<Task>
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">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={pipelineFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.PIPELINE}
@ -668,7 +608,7 @@ const PipelineDetails = ({
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={pipelineFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.PIPELINE}

View File

@ -12,9 +12,8 @@
*/
import { Button, Tooltip } from 'antd';
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
import classNames from 'classnames';
import TagsContainerEntityTable from 'components/Tag/TagsContainerEntityTable/TagsContainerEntityTable.component';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import { EntityType } from 'enums/entity.enum';
@ -22,7 +21,7 @@ import { ThreadType } from 'generated/entity/feed/thread';
import { TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreads } from 'interface/feed.interface';
import { isEmpty } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import { ReactComponent as IconRequest } from '../../assets/svg/request-icon.svg';
@ -32,33 +31,21 @@ const TableTags = <T extends TableUnion>({
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<T>) => {
const { t } = useTranslation();
const [isEdit, setIsEdit] = useState<boolean>(false);
const showEditTagButton = useMemo(
() =>
tags.length && hasTagEditAccess && !isEdit && !showInlineEditTagButton,
[tags, type, hasTagEditAccess, isEdit, showInlineEditTagButton]
);
const hasTagOperationAccess = useMemo(
() =>
@ -74,14 +61,6 @@ const TableTags = <T extends TableUnion>({
]
);
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 = <T extends TableUnion>({
}, [record, onUpdateTagsHandler, onRequestTagsHandler]);
return (
<div className="hover-icon-group" data-testid={`${dataTestId}-${index}`}>
<div className="hover-icon-group" data-testid={`${type}-tags-${index}`}>
<div
className={classNames(
`d-flex justify-content`,
isEdit || !isEmpty(tags) ? 'flex-col items-start' : 'items-center'
)}
className={classNames('d-flex justify-content flex-col items-start')}
data-testid="tags-wrapper">
<TagsContainerEntityTable
isEditing={isEdit}
isLoading={isTagLoading}
<TagsContainerV2
showBottomEditButton
permission={hasTagEditAccess && !isReadOnly}
selectedTags={tags}
showEditButton={showInlineEditTagButton}
showHeader={false}
showInlineEditButton={showInlineEditTagButton}
tagType={type}
treeData={tagList}
onAddButtonClick={addButtonHandler}
onCancel={() => setIsEdit(false)}
onSelectionChange={async (selectedTags) => {
await handleTagSelection(selectedTags, record);
setIsEdit(false);
}}
/>
}}>
<>
{!isReadOnly && (
<div className="d-flex items-center">
{hasTagOperationAccess && (
<>
{/* Request and Update tags */}
{type === TagSource.Classification && getRequestTagsElement}
{!isReadOnly && (
<div className="m-t-xss d-flex items-center">
{showEditTagButton ? (
<Button
className="p-0 w-7 h-7 flex-center text-primary hover-cell-icon"
data-testid="edit-button"
icon={
<IconEdit
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
}
size="small"
type="text"
onClick={addButtonHandler}
/>
) : null}
{/* List Conversation */}
{getFieldThreadElement(
getColumnName?.(record) ?? '',
EntityField.TAGS,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name?.length)
)}
{hasTagOperationAccess && (
<>
{/* Request and Update tags */}
{type === TagSource.Classification && getRequestTagsElement}
{/* List Conversation */}
{getFieldThreadElement(
getColumnName?.(record) ?? '',
EntityField.TAGS,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name?.length)
{/* List Task */}
{getFieldThreadElement(
getColumnName?.(record) ?? '',
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name),
ThreadType.Task
)}
</>
)}
{/* List Task */}
{getFieldThreadElement(
getColumnName?.(record) ?? '',
EntityField.TAGS,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name),
ThreadType.Task
)}
</>
</div>
)}
</div>
)}
</>
</TagsContainerV2>
</div>
</div>
);

View File

@ -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<T> {
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<T> {
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
entityFieldThreads?: EntityFieldThreads[];
tagFetchFailed: boolean;
type: TagSource;
fetchTags: () => Promise<void>;
dataTestId: string;
showInlineEditTagButton?: boolean;
}

View File

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

View File

@ -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<void>;
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
};

View File

@ -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<TableTagsProps>();
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 ? (
<span onClick={handleAddClick}>
<TagsV1 startWith={TAG_START_WITH.PLUS} tag={TAG_CONSTANT} />
</span>
) : null,
[showAddTagButton]
);
const renderTags = useMemo(
() => (
<TagsViewer
isTextPlaceholder
showNoDataPlaceholder={showNoDataPlaceholder}
tags={tags?.[tagType] ?? []}
type="border"
/>
),
[showNoDataPlaceholder, tags?.[tagType]]
);
const tagsSelectContainer = useMemo(() => {
return (
<TagSelectForm
defaultValue={selectedTagsInternal ?? []}
fetchApi={fetchAPI}
placeholder={getTagPlaceholder(isGlossaryType)}
onCancel={handleCancel}
onSubmit={handleSave}
/>
);
}, [
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) ? (
<Col>
<Button
className="p-0 flex-center"
data-testid="request-entity-tags"
size="small"
type="text"
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
<Tooltip
placement="left"
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
}>
<IconRequest
className="anticon"
height={14}
name="request-tags"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Tooltip>
</Button>
</Col>
) : null;
}, [tags?.[tagType], handleUpdateTags, handleRequestTags]);
const conversationThreadElement = useMemo(
() => (
<Col>
<Button
className="p-0 flex-center"
data-testid="tag-thread"
size="small"
type="text"
onClick={() =>
onThreadLinkSelect?.(
entityThreadLink ??
getEntityFeedLink(entityType, entityFqn, 'tags')
)
}>
<Tooltip
placement="left"
title={t('label.list-entity', {
entity: t('label.conversation'),
})}>
<IconComments
height={14}
name="comments"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Tooltip>
</Button>
</Col>
),
[
entityType,
entityFqn,
entityThreadLink,
getEntityFeedLink,
onThreadLinkSelect,
]
);
const header = useMemo(() => {
return (
showHeader && (
<div className="d-flex justify-between m-b-xss">
<div className="d-flex items-center">
<Typography.Text className="right-panel-label">
{isGlossaryType
? t('label.glossary-term')
: t('label.tag-plural')}
</Typography.Text>
{permission && !isEmpty(tags?.[tagType]) && !isEditTags && (
<Button
className="cursor-pointer flex-center m-l-xss"
data-testid="edit-button"
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
size="small"
type="text"
onClick={handleAddClick}
/>
)}
</div>
{permission && (
<Row gutter={8}>
{tagType === TagSource.Classification && requestTagElement}
{onThreadLinkSelect && conversationThreadElement}
</Row>
)}
</div>
)
);
}, [
tags,
tagType,
showHeader,
isEditTags,
permission,
isGlossaryType,
requestTagElement,
conversationThreadElement,
]);
const editTagButton = useMemo(
() =>
permission && !isEmpty(tags?.[tagType]) ? (
<Button
className="p-0 w-7 h-7 flex-center text-primary hover-cell-icon"
data-testid="edit-button"
icon={
<EditIcon
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
}
size="small"
type="text"
onClick={handleAddClick}
/>
) : null,
[permission, tags, tagType, handleAddClick]
);
useEffect(() => {
setTags(getFilterTags(selectedTags));
}, [selectedTags]);
return (
<div
className="w-full"
data-testid={isGlossaryType ? 'glossary-container' : 'tags-container'}>
{header}
{!isEditTags && (
<Space wrap align="center" data-testid="entity-tags" size={4}>
{addTagButton}
{renderTags}
{showInlineEditButton && editTagButton}
</Space>
)}
{isEditTags && tagsSelectContainer}
<div className="m-t-xss d-flex items-center">
{showBottomEditButton && !showInlineEditButton && editTagButton}
{children}
</div>
</div>
);
};
export default TagsContainerV2;

View File

@ -0,0 +1,76 @@
/*
* 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 { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Col, Form, Row, Space } from 'antd';
import { useForm } from 'antd/lib/form/Form';
import InfiniteSelectScroll from 'components/InfiniteSelectScroll/InfiniteSelectScroll';
import React, { useState } from 'react';
import { TagsSelectFormProps } from './TagsSelectForm.interface';
const TagSelectForm = ({
fetchApi,
defaultValue,
placeholder,
onSubmit,
onCancel,
}: TagsSelectFormProps) => {
const [form] = useForm();
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
return (
<Form
form={form}
initialValues={{ tags: defaultValue }}
name="tagsForm"
onFinish={(data) => {
setIsSubmitLoading(true);
onSubmit(data.tags);
}}>
<Row gutter={[0, 8]}>
<Col className="gutter-row d-flex justify-end" span={24}>
<Space align="center">
<Button
className="p-x-05"
data-testid="cancelAssociatedTag"
disabled={isSubmitLoading}
icon={<CloseOutlined size={12} />}
size="small"
onClick={onCancel}
/>
<Button
className="p-x-05"
data-testid="saveAssociatedTag"
htmlType="submit"
icon={<CheckOutlined size={12} />}
loading={isSubmitLoading}
size="small"
type="primary"
/>
</Space>
</Col>
<Col className="gutter-row" span={24}>
<Form.Item noStyle name="tags">
<InfiniteSelectScroll
fetchOptions={fetchApi}
mode="multiple"
placeholder={placeholder}
/>
</Form.Item>
</Col>
</Row>
</Form>
);
};
export default TagSelectForm;

View File

@ -0,0 +1,30 @@
/*
* 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 { SelectOption } from 'components/InfiniteSelectScroll/InfiniteSelectScroll.interface';
import { Paging } from 'generated/type/paging';
export type TagsSelectFormProps = {
placeholder: string;
defaultValue: string[];
onChange?: (value: string[]) => void;
onSubmit: (tags: string[]) => Promise<void>;
onCancel: () => void;
fetchApi: (
search: string,
page: number
) => Promise<{
data: SelectOption[];
paging: Paging;
}>;
};

View File

@ -20,14 +20,16 @@ import { useHistory } from 'react-router-dom';
import { getTagDisplay, getTagTooltip } from 'utils/TagsUtils';
import { ReactComponent as IconTag } from 'assets/svg/classification.svg';
import { TAG_START_WITH } from 'constants/Tag.constants';
import { reduceColorOpacity } from 'utils/CommonUtils';
import { ReactComponent as IconPage } from '../../../assets/svg/ic-flat-doc.svg';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import { TagsV1Props } from './TagsV1.interface';
import './tagsV1.less';
const color = '';
const TagsV1 = ({ tag, showOnlyName = false }: TagsV1Props) => {
const TagsV1 = ({ tag, startWith, showOnlyName = false }: TagsV1Props) => {
const history = useHistory();
const isGlossaryTag = useMemo(
@ -81,7 +83,7 @@ const TagsV1 = ({ tag, showOnlyName = false }: TagsV1Props) => {
color ? (
<div className="tag-color-bar" style={{ background: color }} />
) : null,
[]
[color]
);
const tagContent = useMemo(
@ -98,23 +100,40 @@ const TagsV1 = ({ tag, showOnlyName = false }: TagsV1Props) => {
</span>
</div>
),
[startIcon, tagName, tag.tagFQN]
[startIcon, tagName, tag.tagFQN, tagColorBar]
);
const tagChip = useMemo(
() => (
<Tag
className={classNames('tag-container-style-v1')}
className={classNames('tag-chip tag-chip-content')}
data-testid="tags"
style={{ backgroundColor: reduceColorOpacity(color, 0.1) }}
onClick={() => redirectLink()}>
{tagContent}
</Tag>
),
[]
[color, tagContent]
);
return (
const addTagChip = useMemo(
() => (
<Tag
className="tag-chip tag-chip-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}>
<Typography.Paragraph
className="m-0 text-xs font-medium text-primary"
data-testid="add-tag">
{getTagDisplay(tagName)}
</Typography.Paragraph>
</Tag>
),
[tagName]
);
return startWith === TAG_START_WITH.PLUS ? (
addTagChip
) : (
<Tooltip
className="cursor-pointer"
mouseEnterDelay={1.5}

View File

@ -11,9 +11,11 @@
* limitations under the License.
*/
import { TAG_START_WITH } from 'constants/Tag.constants';
import { TagLabel } from '../../../generated/type/tagLabel';
export type TagsV1Props = {
tag: TagLabel;
startWith: TAG_START_WITH;
showOnlyName?: boolean;
};

View File

@ -13,20 +13,28 @@
@import url('../../../styles/variables.less');
.tag-container-style-v1 {
.tag-chip {
display: flex;
gap: 4px;
align-items: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
justify-content: center;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
}
.tag-chip-content {
margin: 0 5px 4px 0;
border: none;
padding: 0;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
overflow: hidden;
}
.tag-chip-add-button {
padding: 3px 8px;
background: @white;
}
.tag-color-bar {

View File

@ -10,18 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { TagDetails } from 'components/TableQueries/TableQueryRightPanel/TableQueryRightPanel.interface';
import TagsContainerEntityTable from 'components/Tag/TagsContainerEntityTable/TagsContainerEntityTable.component';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { EntityTags } from 'Models';
import React, { useEffect, useState } from 'react';
import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils';
import React from 'react';
type Props = {
isVersionView?: boolean;
@ -36,13 +29,6 @@ const TagsInput: React.FC<Props> = ({
onTagsUpdate,
isVersionView,
}) => {
const [isEditTags, setIsEditTags] = useState(false);
const [tagDetails, setTagDetails] = useState<TagDetails>({
isLoading: false,
isError: false,
options: [],
});
const handleTagSelection = async (selectedTags: EntityTags[]) => {
const updatedTags: TagLabel[] | undefined = selectedTags?.map((tag) => {
return {
@ -55,7 +41,6 @@ const TagsInput: React.FC<Props> = ({
if (onTagsUpdate) {
await onTagsUpdate(updatedTags);
}
setIsEditTags(false);
};
const getSelectedTags = () => {
@ -71,64 +56,15 @@ const TagsInput: React.FC<Props> = ({
}
};
const fetchTags = async () => {
setTagDetails((pre) => ({ ...pre, isLoading: true }));
try {
const tags = await getAllTagsList();
setTagDetails((pre) => ({
...pre,
options: tags,
}));
} catch (_error) {
setTagDetails((pre) => ({ ...pre, isError: true, options: [] }));
} finally {
setTagDetails((pre) => ({ ...pre, isLoading: false }));
}
};
const addButtonHandler = () => {
setIsEditTags(true);
if (isEmpty(tagDetails.options) || tagDetails.isError) {
fetchTags();
}
};
useEffect(() => {
fetchTags();
}, []);
return (
<div className="tags-input-container" data-testid="tags-input-container">
<div className="d-flex items-center">
<Typography.Text className="right-panel-label">
{t('label.tag-plural')}
</Typography.Text>
{editable && tags.length > 0 && (
<Button
className="cursor-pointer flex-center m-l-xss"
data-testid="edit-button"
disabled={!editable}
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
size="small"
type="text"
onClick={() => setIsEditTags(true)}
/>
)}
</div>
{isVersionView ? (
<TagsViewer sizeCap={-1} tags={tags} type="border" />
) : (
<TagsContainerEntityTable
isEditing={isEditTags}
isLoading={tagDetails.isLoading}
<TagsContainerV2
permission={editable}
selectedTags={getSelectedTags()}
tagType={TagSource.Classification}
treeData={getTagsHierarchy(tagDetails.options)}
onAddButtonClick={addButtonHandler}
onCancel={() => setIsEditTags(false)}
onSelectionChange={handleTagSelection}
/>
)}

View File

@ -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<TopicDetailsProps> = ({
data-testid="entity-right-panel"
flex="320px">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={topicDetails.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.TOPIC}
@ -354,7 +354,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={topicDetails.fullyQualifiedName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.TOPIC}

View File

@ -28,10 +28,6 @@ import classNames from 'classnames';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import SchemaEditor from 'components/schema-editor/SchemaEditor';
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 { CSMode } from 'enums/codemirror.enum';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
@ -40,11 +36,6 @@ import { EntityTags, TagOption } from 'Models';
import React, { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getEntityName } from 'utils/EntityUtils';
import {
getGlossaryTermHierarchy,
getGlossaryTermsList,
} from 'utils/GlossaryUtils';
import { getAllTagsList, getTagsHierarchy } from 'utils/TagsUtils';
import { DataTypeTopic, Field } from '../../../generated/entity/data/topic';
import { getTableExpandableConfig } from '../../../utils/TableUtils';
import {
@ -71,44 +62,10 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
}) => {
const { t } = useTranslation();
const [editFieldDescription, setEditFieldDescription] = useState<Field>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [isGlossaryLoading, setIsGlossaryLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [viewType, setViewType] = useState<SchemaViewType>(
SchemaViewType.FIELDS
);
const [glossaryTags, setGlossaryTags] = useState<GlossaryTermDetailsProps[]>(
[]
);
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<TopicSchemaFieldsProps> = ({
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
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<TopicSchemaFieldsProps> = ({
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
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<TopicSchemaFieldsProps> = ({
},
],
[
handleFieldTagsChange,
fetchGlossaryTags,
isGlossaryLoading,
isReadOnly,
messageSchema,
hasDescriptionEditAccess,
hasTagEditAccess,
editFieldDescription,
isReadOnly,
isTagLoading,
glossaryTags,
tagFetchFailed,
hasDescriptionEditAccess,
handleFieldTagsChange,
]
);

View File

@ -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">
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={containerName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.CONTAINER}
@ -640,7 +640,7 @@ const ContainerPage = () => {
onSelectionChange={handleTagSelection}
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={containerName}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.CONTAINER}

View File

@ -37,7 +37,7 @@ import SchemaTab from 'components/SchemaTab/SchemaTab.component';
import TableProfilerV1 from 'components/TableProfiler/TableProfilerV1';
import TableQueries from 'components/TableQueries/TableQueries';
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
import TagsContainerV1 from 'components/Tag/TagsContainerV1/TagsContainerV1';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import { useTourProvider } from 'components/TourProvider/TourProvider';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { getTableTabPath, getVersionPath } from 'constants/constants';
@ -445,7 +445,7 @@ const TableDetailsPageV1 = () => {
) : null}
<Space className="w-full" direction="vertical" size="large">
<TagsContainerV1
<TagsContainerV2
entityFqn={datasetFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.TABLE}
@ -459,7 +459,7 @@ const TableDetailsPageV1 = () => {
onThreadLinkSelect={onThreadLinkSelect}
/>
<TagsContainerV1
<TagsContainerV2
entityFqn={datasetFQN}
entityThreadLink={getEntityThreadLink(entityFieldThreadCount)}
entityType={EntityType.TABLE}

View File

@ -157,10 +157,21 @@ a[href].link-text-grey,
border-radius: 12px;
}
// Word break
.break-all {
word-break: break-all;
}
.break-word {
word-break: break-word;
}
// Whitespace
.whitespace-normal {
white-space: normal;
}
.mx-auto {
margin-right: auto;
margin-left: auto;

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { Tag } from 'generated/entity/classification/tag';
import { get, isArray, isObject, transform } from 'lodash';
import { FormattedTableData } from 'Models';
import { SearchIndex } from '../enums/search.enum';
@ -130,6 +131,21 @@ export const formatSearchGlossaryTermResponse = (
}));
};
export const formatSearchTagsResponse = (
hits: SearchResponse<SearchIndex.TAG>['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[],

View File

@ -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<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<AntdTag
closable
className="text-sm flex-center m-r-xss p-r-xss m-y-2 border-light-gray"
closeIcon={
<CloseOutlined data-testid="remove-tags" height={8} width={8} />
}
data-testid={`selected-tag-${tagLabel}`}
onClose={onClose}
onMouseDown={onPreventMouseDown}>
<Tooltip
className="cursor-pointer"
mouseEnterDelay={1.5}
placement="topLeft"
title={getTagTooltip(label as string)}
trigger="hover">
<Typography.Paragraph className="m-0 d-inline-block break-all whitespace-normal">
{tagLabel}
</Typography.Paragraph>
</Tooltip>
</AntdTag>
);
};