mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-17 11:43:54 +00:00
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:
parent
397fc364a8
commit
af6cec6c9f
@ -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');
|
||||
};
|
||||
|
||||
|
@ -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',
|
||||
};
|
||||
|
||||
|
@ -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'],
|
||||
},
|
||||
];
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}>;
|
||||
}
|
@ -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;
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}>;
|
||||
};
|
@ -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}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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[],
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user