cleanup in getFormattedEntityData method (#14454)

* cleanup in getFormattedEntityData method

* fix failed test after cleanup in entity-summary-panel-utils

* added unit test for getMapOfListHighlights method

* separate test of different functions in different describe block

* in children table constraints will not pass

* fix a type issue

* children will only return for Column and Field type

* cleanup and remove redundancy in mock

* added unit test for getHighlightOfListItem method

* some cleanup + improve type defination

* minor change

* some cleanup
This commit is contained in:
Abhishek Porwal 2024-01-04 15:10:29 +05:30 committed by GitHub
parent e0f16cf39e
commit d406bf39b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 668 additions and 601 deletions

View File

@ -71,16 +71,10 @@ function ContainerSummary({
<SummaryTagsDescription <SummaryTagsDescription
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={getSortedTagsWithHighlight(
getSortedTagsWithHighlight({ entityDetails.tags,
tags: entityDetails.tags, get(highlights, 'tag.name')
sortTagsBasedOnGivenTagFQNs: get( )}
highlights,
'tag.name',
[] as string[]
),
}) ?? []
}
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -112,15 +112,10 @@ function DashboardSummary({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -80,15 +80,10 @@ const DataModelSummary = ({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -54,15 +54,10 @@ const DatabaseSchemaSummary = ({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
</> </>

View File

@ -73,15 +73,10 @@ const DatabaseSummary = ({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />

View File

@ -82,15 +82,10 @@ function MlModelSummary({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -82,15 +82,10 @@ function PipelineSummary({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -97,15 +97,10 @@ function SearchIndexSummary({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -54,15 +54,10 @@ const ServiceSummary = ({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
</> </>

View File

@ -61,15 +61,10 @@ const StoredProcedureSummary = ({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -22,12 +22,16 @@ import {
} from '../../../../generated/entity/data/table'; } from '../../../../generated/entity/data/table';
import { TagLabel } from '../../../../generated/type/tagLabel'; import { TagLabel } from '../../../../generated/type/tagLabel';
export interface HighlightedTagLabel extends TagLabel {
isHighlighted: boolean;
}
export interface BasicEntityInfo { export interface BasicEntityInfo {
algorithm?: string; algorithm?: string;
name: string; name: string;
title: ReactNode; title: ReactNode;
type?: DataType | ChartType | FeatureType | string; type?: DataType | ChartType | FeatureType | string;
tags?: TagLabel[]; tags?: Array<TagLabel | HighlightedTagLabel>;
description?: string; description?: string;
columnConstraint?: Constraint; columnConstraint?: Constraint;
tableConstraints?: TableConstraint[]; tableConstraints?: TableConstraint[];

View File

@ -247,15 +247,10 @@ function TableSummary({
entityDetail={tableDetails} entityDetail={tableDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: tableDetails.tags, tableDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -167,15 +167,10 @@ function TopicSummary({
entityDetail={entityDetails} entityDetail={entityDetails}
tags={ tags={
tags ?? tags ??
getSortedTagsWithHighlight({ getSortedTagsWithHighlight(
tags: entityDetails.tags, entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get( get(highlights, 'tag.name')
highlights, )
'tag.name',
[] as string[]
),
}) ??
[]
} }
/> />
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />

View File

@ -22,11 +22,11 @@ import { ROUTES } from '../../../constants/constants';
import { TAG_START_WITH } from '../../../constants/Tag.constants'; import { TAG_START_WITH } from '../../../constants/Tag.constants';
import { TagSource } from '../../../generated/type/tagLabel'; import { TagSource } from '../../../generated/type/tagLabel';
import { reduceColorOpacity } from '../../../utils/CommonUtils'; import { reduceColorOpacity } from '../../../utils/CommonUtils';
import { HighlightedTagLabel } from '../../../utils/EntitySummaryPanelUtils';
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import Fqn from '../../../utils/Fqn'; import Fqn from '../../../utils/Fqn';
import { getEncodedFqn } from '../../../utils/StringsUtils'; import { getEncodedFqn } from '../../../utils/StringsUtils';
import { getTagDisplay, getTagTooltip } from '../../../utils/TagsUtils'; import { getTagDisplay, getTagTooltip } from '../../../utils/TagsUtils';
import { HighlightedTagLabel } from '../../Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { TagsV1Props } from './TagsV1.interface'; import { TagsV1Props } from './TagsV1.interface';
import './tagsV1.less'; import './tagsV1.less';

View File

@ -14,7 +14,7 @@
import { TagProps } from 'antd'; import { TagProps } from 'antd';
import { TAG_START_WITH } from '../../../constants/Tag.constants'; import { TAG_START_WITH } from '../../../constants/Tag.constants';
import { TagLabel, TagSource } from '../../../generated/type/tagLabel'; import { TagLabel, TagSource } from '../../../generated/type/tagLabel';
import { HighlightedTagLabel } from '../../../utils/EntitySummaryPanelUtils'; import { HighlightedTagLabel } from '../../Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
export type TagsV1Props = { export type TagsV1Props = {
tag: TagLabel | HighlightedTagLabel; tag: TagLabel | HighlightedTagLabel;

View File

@ -14,15 +14,15 @@ import { Col, Divider, Row, Typography } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import TagsViewer from '../../../components/Tag/TagsViewer/TagsViewer'; import TagsViewer from '../../../components/Tag/TagsViewer/TagsViewer';
import { TagLabel } from '../../../generated/type/tagLabel'; import { BasicEntityInfo } from '../../Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { EntityUnion } from '../../Explore/ExplorePage.interface'; import { EntityUnion } from '../../Explore/ExplorePage.interface';
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
const SummaryTagsDescription = ({ const SummaryTagsDescription = ({
tags, tags = [],
entityDetail, entityDetail,
}: { }: {
tags: TagLabel[]; tags: BasicEntityInfo['tags'];
entityDetail: EntityUnion; entityDetail: EntityUnion;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -15,6 +15,8 @@ import { SummaryEntityType } from '../enums/EntitySummary.enum';
import { Column } from '../generated/entity/data/table'; import { Column } from '../generated/entity/data/table';
import { import {
getFormattedEntityData, getFormattedEntityData,
getHighlightOfListItem,
getMapOfListHighlights,
getSortedTagsWithHighlight, getSortedTagsWithHighlight,
getSummaryListItemType, getSummaryListItemType,
getTitle, getTitle,
@ -24,87 +26,146 @@ import {
mockEntityDataWithNestingResponse, mockEntityDataWithNestingResponse,
mockEntityDataWithoutNesting, mockEntityDataWithoutNesting,
mockEntityDataWithoutNestingResponse, mockEntityDataWithoutNestingResponse,
mockGetHighlightOfListItemResponse,
mockGetMapOfListHighlightsResponse,
mockGetSummaryListItemTypeResponse, mockGetSummaryListItemTypeResponse,
mockHighlights,
mockInvalidDataResponse, mockInvalidDataResponse,
mockLinkBasedSummaryTitleResponse, mockLinkBasedSummaryTitleResponse,
mockListItemNameHighlight,
mockTagFQNsForHighlight, mockTagFQNsForHighlight,
mockTagsDataAfterSortAndHighlight, mockTagsSortAndHighlightResponse,
mockTagsDataBeforeSortAndHighlight,
mockTextBasedSummaryTitleResponse, mockTextBasedSummaryTitleResponse,
} from './mocks/EntitySummaryPanelUtils.mock'; } from './mocks/EntitySummaryPanelUtils.mock';
jest.mock('../constants/EntitySummaryPanelUtils.constant', () => ({
...jest.requireActual('../constants/EntitySummaryPanelUtils.constant'),
SummaryListHighlightKeys: [
'columns.name',
'columns.description',
'columns.children.name',
],
}));
describe('EntitySummaryPanelUtils tests', () => { describe('EntitySummaryPanelUtils tests', () => {
it('getFormattedEntityData should return formatted data properly for table columns data without nesting, and also sort the data based on given arr', () => { describe('getFormattedEntityData', () => {
const highlights = { it('getFormattedEntityData should return formatted data properly for table columns data with nesting, and also sort the data based on highlights', () => {
'tag.name': ['PersonalData.SpecialCategory'], const resultFormattedData = getFormattedEntityData(
}; SummaryEntityType.COLUMN,
const resultFormattedData = getFormattedEntityData( mockEntityDataWithNesting,
SummaryEntityType.COLUMN, mockHighlights
mockEntityDataWithoutNesting, );
highlights
);
expect(resultFormattedData).toEqual(mockEntityDataWithoutNestingResponse); expect(resultFormattedData).toEqual(mockEntityDataWithNestingResponse);
});
it('getFormattedEntityData should return formatted data properly for topic fields data with nesting', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
mockEntityDataWithNesting
);
expect(resultFormattedData).toEqual(mockEntityDataWithNestingResponse);
});
it('getFormattedEntityData should return empty array in case entityType is given other than from type SummaryEntityType', () => {
const resultFormattedData = getFormattedEntityData(
'otherType' as SummaryEntityType,
mockEntityDataWithNesting
);
expect(resultFormattedData).toEqual([]);
});
it('getFormattedEntityData should not throw error if entityDetails sent does not have fields present', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
[{}] as Column[]
);
expect(resultFormattedData).toEqual(mockInvalidDataResponse);
});
it('getSortedTagsWithHighlight should return the sorted and highlighted tags data based on given tagFQN array', () => {
const sortedTags = getSortedTagsWithHighlight({
sortTagsBasedOnGivenTagFQNs: mockTagFQNsForHighlight,
tags: mockTagsDataBeforeSortAndHighlight,
}); });
expect(sortedTags).toEqual(mockTagsDataAfterSortAndHighlight); it('getFormattedEntityData should return formatted data properly for pipeline data without nesting', () => {
}); const resultFormattedData = getFormattedEntityData(
SummaryEntityType.TASK,
mockEntityDataWithoutNesting
);
it('getSummaryListItemType should return the summary item type based on given entityType', () => { expect(resultFormattedData).toEqual(mockEntityDataWithoutNestingResponse);
const summaryItemType = getSummaryListItemType(
SummaryEntityType.COLUMN,
mockEntityDataWithoutNesting[0]
);
expect(summaryItemType).toEqual(mockGetSummaryListItemTypeResponse);
});
it('getTitle should return title as link or text based on sourceUrl present or not in given data', () => {
const textBasedTitle = getTitle({
content: 'Title1',
sourceUrl: undefined,
}); });
expect(textBasedTitle).toEqual(mockTextBasedSummaryTitleResponse); it('getFormattedEntityData should return empty array in case entityType is given other than from type SummaryEntityType', () => {
const resultFormattedData = getFormattedEntityData(
'otherType' as SummaryEntityType,
mockEntityDataWithNesting
);
const linkBasedTitle = getTitle({ expect(resultFormattedData).toEqual([]);
content: 'Title2',
sourceUrl: 'https://task1.com',
}); });
expect(linkBasedTitle).toEqual(mockLinkBasedSummaryTitleResponse); it('getFormattedEntityData should not throw error if entityDetails sent does not have fields present', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
[{}] as Column[]
);
expect(resultFormattedData).toEqual(mockInvalidDataResponse);
});
});
describe('getSortedTagsWithHighlight', () => {
it('getSortedTagsWithHighlight should return the sorted and highlighted tags data based on given tagFQN array', () => {
const sortedTags = getSortedTagsWithHighlight(
mockEntityDataWithNesting[2].tags,
mockTagFQNsForHighlight
);
expect(sortedTags).toEqual(mockTagsSortAndHighlightResponse);
});
});
describe('getSummaryListItemType', () => {
it('getSummaryListItemType should return the summary item type based on given entityType', () => {
const summaryItemType = getSummaryListItemType(
SummaryEntityType.TASK,
mockEntityDataWithoutNesting[0]
);
expect(summaryItemType).toEqual(mockGetSummaryListItemTypeResponse);
});
});
describe('getTitle', () => {
it('getTitle should return title as text if sourceUrl not present in listItem and also apply highlight if present', () => {
const textBasedTitle = getTitle(
mockEntityDataWithNesting[0],
mockListItemNameHighlight
);
expect(textBasedTitle).toEqual(mockTextBasedSummaryTitleResponse);
});
it('getTitle should return title as link if sourceUrl present in listItem', () => {
const linkBasedTitle = getTitle(mockEntityDataWithoutNesting[0]);
expect(linkBasedTitle).toEqual(mockLinkBasedSummaryTitleResponse);
});
});
describe('getMapOfListHighlights', () => {
it('getMapOfListHighlights should returns empty arrays and map when highlights is undefined', () => {
const result = getMapOfListHighlights();
expect(result.listHighlights).toEqual([]);
expect(result.listHighlightsMap).toEqual({});
});
it('getMapOfListHighlights should returns listHighlights and listHighlightsMap correctly', () => {
const result = getMapOfListHighlights(mockHighlights);
expect(result).toEqual(mockGetMapOfListHighlightsResponse);
});
});
describe('getHighlightOfListItem', () => {
it('getHighlightOfListItem should return highlights of listItem undefined, if listHighlights and tagHighlights not passed in params', () => {
const result = getHighlightOfListItem(
mockEntityDataWithNesting[0],
[] as string[],
[] as string[],
{} as { [key: string]: number }
);
expect(result).toEqual({
highlightedTags: undefined,
highlightedTitle: undefined,
highlightedDescription: undefined,
});
});
it('getHighlightOfListItem should return highlights of listItem if listHighlights and tagHighlights get in params', () => {
const result = getHighlightOfListItem(
mockEntityDataWithNesting[1],
mockTagFQNsForHighlight,
mockGetMapOfListHighlightsResponse.listHighlights,
mockGetMapOfListHighlightsResponse.listHighlightsMap
);
expect(result).toEqual(mockGetHighlightOfListItemResponse);
});
}); });
}); });

View File

@ -17,7 +17,10 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { SearchedDataProps } from '../../src/components/SearchedData/SearchedData.interface'; import { SearchedDataProps } from '../../src/components/SearchedData/SearchedData.interface';
import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg'; import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg';
import { BasicEntityInfo } from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface'; import {
BasicEntityInfo,
HighlightedTagLabel,
} from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { NO_DATA_PLACEHOLDER } from '../constants/constants'; import { NO_DATA_PLACEHOLDER } from '../constants/constants';
import { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant'; import { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant';
import { SummaryEntityType } from '../enums/EntitySummary.enum'; import { SummaryEntityType } from '../enums/EntitySummary.enum';
@ -37,20 +40,30 @@ export interface EntityNameProps {
displayName?: string; displayName?: string;
} }
export interface HighlightedTagLabel extends TagLabel { export type SummaryListItem = Column | Field | Chart | Task | MlFeature;
isHighlighted: boolean;
export interface ListItemHighlights {
highlightedTags?: BasicEntityInfo['tags'];
highlightedTitle?: string;
highlightedDescription?: string;
} }
const getTitleName = (data: EntityNameProps) => /* @param {
getEntityName(data) || NO_DATA_PLACEHOLDER; listItem: SummaryItem,
highlightedTitle: will be a string if the title of given summaryItem is present in highlights | undefined
}
@return SummaryItemTitle
*/
export const getTitle = (
listItem: SummaryListItem,
highlightedTitle?: ListItemHighlights['highlightedTitle']
): JSX.Element | JSX.Element[] => {
const title = highlightedTitle
? stringToHTML(highlightedTitle)
: getEntityName(listItem) || NO_DATA_PLACEHOLDER;
const sourceUrl = (listItem as Chart | Task).sourceUrl;
export const getTitle = ({
content,
sourceUrl,
}: {
content: string | JSX.Element | JSX.Element[] | undefined;
sourceUrl: string | undefined;
}) => {
return sourceUrl ? ( return sourceUrl ? (
<Link target="_blank" to={{ pathname: sourceUrl }}> <Link target="_blank" to={{ pathname: sourceUrl }}>
<div className="d-flex"> <div className="d-flex">
@ -58,65 +71,189 @@ export const getTitle = ({
className="entity-title text-link-color font-medium m-r-xss" className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title" data-testid="entity-title"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{content} {title}
</Text> </Text>
<IconExternalLink width={12} /> <IconExternalLink width={12} />
</div> </div>
</Link> </Link>
) : ( ) : (
<Text className="entity-title" data-testid="entity-title"> <Text className="entity-title" data-testid="entity-title">
{content} {title}
</Text> </Text>
); );
}; };
/* @param {
entityType: will be any type of SummaryEntityType,
listItem: SummaryItem
}
@return listItemType
*/
export const getSummaryListItemType = ( export const getSummaryListItemType = (
entityType: SummaryEntityType, entityType: SummaryEntityType,
listItemInfo: Column | Field | Chart | Task | MlFeature listItem: SummaryListItem
) => { ): BasicEntityInfo['type'] => {
switch (entityType) { switch (entityType) {
case SummaryEntityType.COLUMN: case SummaryEntityType.COLUMN:
case SummaryEntityType.FIELD: case SummaryEntityType.FIELD:
case SummaryEntityType.MLFEATURE: case SummaryEntityType.MLFEATURE:
case SummaryEntityType.SCHEMAFIELD: case SummaryEntityType.SCHEMAFIELD:
return (listItemInfo as Column | Field | MlFeature).dataType; return (listItem as Column | Field | MlFeature).dataType;
case SummaryEntityType.CHART: case SummaryEntityType.CHART:
return (listItemInfo as Chart).chartType; return (listItem as Chart).chartType;
case SummaryEntityType.TASK: case SummaryEntityType.TASK:
return (listItemInfo as Task).taskType; return (listItem as Task).taskType;
default: default:
return ''; return '';
} }
}; };
export const getSortedTagsWithHighlight = ({ /*
sortTagsBasedOnGivenTagFQNs, @params {
tags, sortTagsBasedOnGivenTagFQNs: array of TagFQNs,
}: { tags: Tags array,
sortTagsBasedOnGivenTagFQNs: string[];
tags?: TagLabel[];
}): (TagLabel | HighlightedTagLabel)[] => {
const ColumnDataTags: {
tagForSort: HighlightedTagLabel[];
remainingTags: TagLabel[];
} = { tagForSort: [], remainingTags: [] };
tags?.reduce((acc, tag) => {
if (sortTagsBasedOnGivenTagFQNs.includes(tag.tagFQN)) {
acc.tagForSort.push({ ...tag, isHighlighted: true });
} else {
acc.remainingTags.push(tag);
} }
return acc; @return array of tags highlighted and sorted if tagFQN present in sortTagsBasedOnGivenTagFQNs
}, ColumnDataTags); */
export const getSortedTagsWithHighlight = (
tags: TagLabel[] = [],
sortTagsBasedOnGivenTagFQNs: string[] = []
): ListItemHighlights['highlightedTags'] => {
const { sortedTags, remainingTags } = tags.reduce(
(acc, tag) => {
if (sortTagsBasedOnGivenTagFQNs.includes(tag.tagFQN)) {
acc.sortedTags.push({ ...tag, isHighlighted: true });
} else {
acc.remainingTags.push(tag);
}
return [...ColumnDataTags.tagForSort, ...ColumnDataTags.remainingTags]; return acc;
},
{
sortedTags: [] as HighlightedTagLabel[],
remainingTags: [] as TagLabel[],
}
);
return [...sortedTags, ...remainingTags];
}; };
/*
@param {highlights: all the other highlights come from the query api
only omitted displayName and description key as it is already updated in parent component
}
@return {
listHighlights: single array of all highlights get from query api
listHighlightsMap: to reduce the search time complexity in listHighlight
}
Todo: apply highlights on entityData in parent where we apply highlight for entityDisplayName and entityDescription
for that we need to update multiple summary components
*/
export const getMapOfListHighlights = (
highlights?: SearchedDataProps['data'][number]['highlight']
): {
listHighlights: string[];
listHighlightsMap: { [key: string]: number };
} => {
// checking for the all highlight key present in highlight get from query api
// and create a array of highlights
const listHighlights: string[] = [];
SummaryListHighlightKeys.forEach((highlightKey) => {
listHighlights.push(...get(highlights, highlightKey, []));
});
// using hashmap methodology to reduce the search time complexity from O(n) to O(1)
// to get highlight from the listHighlights array for applying highlight
const listHighlightsMap: { [key: string]: number } = {};
listHighlights?.reduce((acc, colHighlight, index) => {
acc[colHighlight.replaceAll(/<\/?span(.*?)>/g, '')] = index;
return acc;
}, listHighlightsMap);
return { listHighlights, listHighlightsMap };
};
/*
@params {
listItem: SummaryItem
tagsHighlights: tagFQNs array to highlight and sort tags
listHighlights: single array of all highlights get from query api
listHighlightsMap: to reduce the search time complexity in listHighlight
}
@return highlights of listItem
*/
export const getHighlightOfListItem = (
listItem: SummaryListItem,
tagHighlights: string[],
listHighlights: string[],
listHighlightsMap: { [key: string]: number }
): ListItemHighlights => {
let highlightedTags;
let highlightedTitle;
let highlightedDescription;
// if any of the listItem.tags present in given tagHighlights list then sort and highlights the tag
const shouldSortListItemTags = listItem.tags?.find((tag) =>
tagHighlights.includes(tag.tagFQN)
);
if (shouldSortListItemTags) {
highlightedTags = getSortedTagsWithHighlight(listItem.tags, tagHighlights);
}
// highlightedListItemNameIndex will be undefined if listItem.name is not present in highlights
const highlightedListItemNameIndex = listHighlightsMap[listItem.name ?? ''];
const shouldApplyHighlightOnTitle = !isUndefined(
highlightedListItemNameIndex
);
if (shouldApplyHighlightOnTitle) {
highlightedTitle = listHighlights[highlightedListItemNameIndex];
}
// highlightedListItemDescriptionIndex will be undefined if listItem.description is not present in highlights
const highlightedListItemDescriptionIndex =
listHighlightsMap[listItem.description ?? ''];
const shouldApplyHighlightOnDescription = !isUndefined(
highlightedListItemDescriptionIndex
);
if (shouldApplyHighlightOnDescription) {
highlightedDescription =
listHighlights[highlightedListItemDescriptionIndex];
}
return {
highlightedTags,
highlightedTitle,
highlightedDescription,
};
};
/*
@params {
entityType: SummaryEntityType,
entityInfo: Array<SummaryListItem> = [],
highlights: highlights get from the query api + highlights added for tags (i.e. tag.name)
tableConstraints: only pass for SummayEntityType.Column
}
@return sorted and highlighted listItem array, but listItem will be type of BasicEntityInfo
Note: SummaryItem will be sort and highlight only if -
# if listItem.tags present in highlights.tags
# if listItem.name present in highlights comes from query api
# if listItem.description present in highlights comes from query api
*/
export const getFormattedEntityData = ( export const getFormattedEntityData = (
entityType: SummaryEntityType, entityType: SummaryEntityType,
entityInfo?: Array<Column | Field | Chart | Task | MlFeature>, entityInfo: Array<SummaryListItem> = [],
highlights?: SearchedDataProps['data'][number]['highlight'], highlights?: SearchedDataProps['data'][number]['highlight'],
tableConstraints?: TableConstraint[] tableConstraints?: TableConstraint[]
): BasicEntityInfo[] => { ): BasicEntityInfo[] => {
@ -124,97 +261,68 @@ export const getFormattedEntityData = (
return []; return [];
} }
// sort and highlights list items based on tags and global search highlights data // Only go ahead if entityType is present in SummaryEntityType enum
if (Object.values(SummaryEntityType).includes(entityType)) { if (Object.values(SummaryEntityType).includes(entityType)) {
// tagHighlights is the array of tagFQNs for highlighting tags
const tagHighlights = get(highlights, 'tag.name', [] as string[]); const tagHighlights = get(highlights, 'tag.name', [] as string[]);
const listHighlights: string[] = [];
const listHighlightsMap: { [key: string]: number } = {};
const SummaryListData = {
listItemWithSortOption: [] as BasicEntityInfo[],
listItemWithoutSortOption: [] as BasicEntityInfo[],
};
SummaryListHighlightKeys.forEach((highlightKey) => { // listHighlights i.e. highlight get from query api
listHighlights.push(...get(highlights, highlightKey, [])); // listHighlightsMap i.e. map of highlight get from api to reduce search time complexity in highlights array
}); const { listHighlights, listHighlightsMap } =
getMapOfListHighlights(highlights);
listHighlights?.reduce((acc, colHighlight, index) => { const { highlightedListItem, remainingListItem } = entityInfo.reduce(
acc[colHighlight.replaceAll(/<\/?span(.*?)>/g, '')] = index; (acc, listItem) => {
// return the highlight of listItem
const { highlightedTags, highlightedTitle, highlightedDescription } =
getHighlightOfListItem(
listItem,
tagHighlights,
listHighlights,
listHighlightsMap
);
return acc; // convert listItem in BasicEntityInfo type
}, listHighlightsMap); const listItemModifiedData = {
name: listItem.name ?? '',
title: getTitle(listItem, highlightedTitle),
type: getSummaryListItemType(entityType, listItem),
tags: highlightedTags ?? listItem.tags,
description: highlightedDescription ?? listItem.description,
...(entityType === SummaryEntityType.COLUMN && {
columnConstraint: (listItem as Column).constraint,
tableConstraints: tableConstraints,
}),
...(entityType === SummaryEntityType.MLFEATURE && {
algorithm: (listItem as MlFeature).featureAlgorithm,
}),
...((entityType === SummaryEntityType.COLUMN ||
entityType === SummaryEntityType.FIELD) && {
children: getFormattedEntityData(
entityType,
(listItem as Column | Field).children,
highlights
),
}),
};
entityInfo?.reduce((acc, listItem) => { // if highlights present in listItem then sort the listItem
const listItemModifiedData = { if (highlightedTags || highlightedTitle || highlightedDescription) {
name: listItem.name ?? '', acc.highlightedListItem.push(listItemModifiedData);
title: getTitle({ } else {
content: getTitleName(listItem), acc.remainingListItem.push(listItemModifiedData);
sourceUrl: (listItem as Chart | Task).sourceUrl,
}),
type: getSummaryListItemType(entityType, listItem),
tags: listItem.tags,
description: listItem.description,
...(entityType === SummaryEntityType.COLUMN && {
columnConstraint: (listItem as Column).constraint,
tableConstraints: tableConstraints,
}),
...(entityType === SummaryEntityType.MLFEATURE && {
algorithm: (listItem as MlFeature).featureAlgorithm,
}),
children: getFormattedEntityData(
entityType,
(listItem as Column | Field).children,
highlights,
tableConstraints
),
};
const isTagHighlightsPresentInListItemTags = listItem.tags?.find((tag) =>
tagHighlights.includes(tag.tagFQN)
);
const highlightedListItemNameIndex =
listHighlightsMap[listItem.name ?? ''];
const highlightedListItemDescriptionIndex =
listHighlightsMap[listItem.description ?? ''];
if (
isTagHighlightsPresentInListItemTags ||
!isUndefined(highlightedListItemNameIndex) ||
!isUndefined(highlightedListItemDescriptionIndex)
) {
if (isTagHighlightsPresentInListItemTags) {
listItemModifiedData.tags = getSortedTagsWithHighlight({
sortTagsBasedOnGivenTagFQNs: tagHighlights,
tags: listItem.tags,
});
} }
if (!isUndefined(highlightedListItemNameIndex)) { return acc;
listItemModifiedData.title = getTitle({ },
content: stringToHTML(listHighlights[highlightedListItemNameIndex]), {
sourceUrl: (listItem as Chart | Task).sourceUrl, highlightedListItem: [] as BasicEntityInfo[],
}); remainingListItem: [] as BasicEntityInfo[],
}
if (!isUndefined(highlightedListItemDescriptionIndex)) {
listItemModifiedData.description =
listHighlights[highlightedListItemDescriptionIndex];
}
acc.listItemWithSortOption.push(listItemModifiedData);
} else {
acc.listItemWithoutSortOption.push(listItemModifiedData);
} }
);
return acc; return [...highlightedListItem, ...remainingListItem];
}, SummaryListData);
return [
...SummaryListData.listItemWithSortOption,
...SummaryListData.listItemWithoutSortOption,
];
} else {
return [];
} }
return [];
}; };

View File

@ -14,6 +14,8 @@
import { Typography } from 'antd'; import { Typography } from 'antd';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { BasicEntityInfo } from '../../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { Task } from '../../generated/entity/data/pipeline';
import { import {
Column, Column,
DataType, DataType,
@ -21,21 +23,189 @@ import {
State, State,
TagSource, TagSource,
} from '../../generated/entity/data/table'; } from '../../generated/entity/data/table';
import { DataTypeTopic, Field } from '../../generated/entity/data/topic';
import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg'; import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg';
const { Text } = Typography; const { Text } = Typography;
export const mockEntityDataWithoutNesting: Column[] = [ export const mockTextBasedSummaryTitleResponse = (
<Text className="entity-title" data-testid="entity-title">
<span className="text-highlighter">title2</span>
</Text>
);
export const mockLinkBasedSummaryTitleResponse = (
<Link
target="_blank"
to={{
pathname:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=dim_address_task',
}}>
<div className="d-flex">
<Text
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
dim_address Task
</Text>
<IconExternalLink width={12} />
</div>
</Link>
);
export const mockGetSummaryListItemTypeResponse = 'PrestoOperator';
export const mockTagsSortAndHighlightResponse = [
{ {
name: 'title', tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
isHighlighted: true,
},
{
tagFQN: 'PersonalData.Category1',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
];
export const mockTagFQNsForHighlight = ['PersonalData.SpecialCategory'];
export const mockListItemNameHighlight =
'<span className="text-highlighter">title2</span>';
const mockListItemDescriptionHighlight =
'some description of <span className="text-highlighter">title2</span>';
export const mockHighlights = {
'columns.name': [mockListItemNameHighlight],
'columns.description': [mockListItemDescriptionHighlight],
'tag.name': mockTagFQNsForHighlight,
};
export const mockGetMapOfListHighlightsResponse = {
listHighlights: [mockListItemNameHighlight, mockListItemDescriptionHighlight],
listHighlightsMap: {
title2: 0,
'some description of title2': 1,
},
};
export const mockGetHighlightOfListItemResponse = {
highlightedTags: undefined,
highlightedTitle: mockListItemNameHighlight,
highlightedDescription: mockListItemDescriptionHighlight,
};
export const mockEntityDataWithoutNesting: Task[] = [
{
name: 'dim_address_task',
displayName: 'dim_address Task',
fullyQualifiedName: 'sample_airflow.dim_address_etl.dim_address_task',
description:
'Airflow operator to perform ETL and generate dim_address table',
sourceUrl:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=dim_address_task',
downstreamTasks: ['assert_table_exists'],
taskType: 'PrestoOperator',
tags: [],
},
{
name: 'assert_table_exists',
displayName: 'Assert Table Exists',
fullyQualifiedName: 'sample_airflow.dim_address_etl.assert_table_exists',
description: 'Assert if a table exists',
sourceUrl:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists',
downstreamTasks: [],
taskType: 'HiveOperator',
tags: [],
},
];
export const mockEntityDataWithoutNestingResponse: BasicEntityInfo[] = [
{
name: 'dim_address_task',
title: mockLinkBasedSummaryTitleResponse,
description:
'Airflow operator to perform ETL and generate dim_address table',
type: mockGetSummaryListItemTypeResponse,
tags: [],
},
{
name: 'assert_table_exists',
title: (
<Link
target="_blank"
to={{
pathname:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists',
}}>
<div className="d-flex">
<Text
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
Assert Table Exists
</Text>
<IconExternalLink width={12} />
</div>
</Link>
),
description: 'Assert if a table exists',
type: 'HiveOperator',
tags: [],
},
];
export const mockEntityDataWithNesting: Column[] = [
{
name: 'Customer',
dataType: DataType.Varchar,
fullyQualifiedName: 'sample_kafka.customer_events.Customer',
tags: [],
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
children: [
{
name: 'id',
dataType: DataType.Varchar,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.id',
tags: [],
},
{
name: 'first_name',
dataType: DataType.Varchar,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.first_name',
tags: [],
},
{
name: 'last_name',
dataType: DataType.Varchar,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.last_name',
tags: [],
},
{
name: 'email',
dataType: DataType.Varchar,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.email',
tags: [],
},
],
},
{
name: 'title2',
dataType: DataType.Varchar, dataType: DataType.Varchar,
dataLength: 100, dataLength: 100,
dataTypeDisplay: 'varchar', dataTypeDisplay: 'varchar',
description: description: 'some description of title2',
'Full name of the app or channel. For example, Point of Sale, Online Store.',
fullyQualifiedName: fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".title', 'sample_data.ecommerce_db.shopify."dim.api/client".title2',
tags: [], tags: [],
ordinalPosition: 2, ordinalPosition: 2,
}, },
@ -69,227 +239,103 @@ export const mockEntityDataWithoutNesting: Column[] = [
}, },
]; ];
export const mockEntityDataWithoutNestingResponse = [ export const mockEntityDataWithNestingResponse: BasicEntityInfo[] = [
{ {
name: 'title2',
title: mockTextBasedSummaryTitleResponse,
type: DataType.Varchar,
description: mockListItemDescriptionHighlight,
tags: [],
tableConstraints: undefined,
columnConstraint: undefined,
children: [], children: [],
description: },
'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.', {
name: 'api_client_id', name: 'api_client_id',
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: 'Classification',
labelType: 'Manual',
state: 'Confirmed',
isHighlighted: true,
},
{
tagFQN: 'PersonalData.Category1',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: 'Classification',
labelType: 'Manual',
state: 'Confirmed',
},
],
title: ( title: (
<Text className="entity-title" data-testid="entity-title"> <Text className="entity-title" data-testid="entity-title">
api_client_id api_client_id
</Text> </Text>
), ),
type: 'NUMERIC', type: DataType.Numeric,
tableConstraints: undefined,
columnConstraint: undefined,
},
{
children: [],
description: description:
'Full name of the app or channel. For example, Point of Sale, Online Store.', 'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.',
name: 'title', tags: mockTagsSortAndHighlightResponse,
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
title
</Text>
),
type: 'VARCHAR',
tableConstraints: undefined, tableConstraints: undefined,
columnConstraint: undefined, columnConstraint: undefined,
children: [],
}, },
];
export const mockEntityDataWithNesting: Field[] = [
{ {
name: 'Customer', name: 'Customer',
dataType: DataTypeTopic.Record,
fullyQualifiedName: 'sample_kafka.customer_events.Customer',
tags: [],
children: [
{
name: 'id',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.id',
tags: [],
},
{
name: 'first_name',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.first_name',
tags: [],
},
{
name: 'last_name',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.last_name',
tags: [],
},
{
name: 'email',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.email',
tags: [],
},
{
name: 'address_line_1',
dataType: DataTypeTopic.String,
fullyQualifiedName:
'sample_kafka.customer_events.Customer.address_line_1',
tags: [],
},
{
name: 'address_line_2',
dataType: DataTypeTopic.String,
fullyQualifiedName:
'sample_kafka.customer_events.Customer.address_line_2',
tags: [],
},
{
name: 'post_code',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.post_code',
tags: [],
},
{
name: 'country',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.country',
tags: [],
},
],
},
];
export const mockEntityDataWithNestingResponse = [
{
children: [
{
children: [],
description: undefined,
name: 'id',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
id
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'first_name',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
first_name
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'last_name',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
last_name
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'email',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
email
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'address_line_1',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
address_line_1
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'address_line_2',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
address_line_2
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'post_code',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
post_code
</Text>
),
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'country',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
country
</Text>
),
type: 'STRING',
},
],
description: undefined,
name: 'Customer',
tags: [],
title: ( title: (
<Text className="entity-title" data-testid="entity-title"> <Text className="entity-title" data-testid="entity-title">
Customer Customer
</Text> </Text>
), ),
type: 'RECORD', type: DataType.Varchar,
tags: [],
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
tableConstraints: undefined,
columnConstraint: undefined,
children: [
{
name: 'id',
title: (
<Text className="entity-title" data-testid="entity-title">
id
</Text>
),
type: DataType.Varchar,
tags: [],
children: [],
description: undefined,
tableConstraints: undefined,
columnConstraint: undefined,
},
{
name: 'first_name',
title: (
<Text className="entity-title" data-testid="entity-title">
first_name
</Text>
),
type: DataType.Varchar,
tags: [],
children: [],
description: undefined,
tableConstraints: undefined,
columnConstraint: undefined,
},
{
name: 'last_name',
title: (
<Text className="entity-title" data-testid="entity-title">
last_name
</Text>
),
type: DataType.Varchar,
tags: [],
children: [],
description: undefined,
tableConstraints: undefined,
columnConstraint: undefined,
},
{
name: 'email',
title: (
<Text className="entity-title" data-testid="entity-title">
email
</Text>
),
type: DataType.Varchar,
tags: [],
children: [],
description: undefined,
tableConstraints: undefined,
columnConstraint: undefined,
},
],
}, },
]; ];
@ -309,94 +355,3 @@ export const mockInvalidDataResponse = [
type: undefined, type: undefined,
}, },
]; ];
export const mockTagsDataBeforeSortAndHighlight = [
{
tagFQN: 'gs1.term1',
name: 'term1',
displayName: '',
description: 'term1 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'gs1.term2',
name: 'term2',
displayName: '',
description: 'term2 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'gs1.term3',
name: 'term3',
displayName: '',
description: 'term3 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
},
];
export const mockTagsDataAfterSortAndHighlight = [
{
tagFQN: 'gs1.term2',
name: 'term2',
displayName: '',
description: 'term2 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
isHighlighted: true,
},
{
tagFQN: 'gs1.term1',
name: 'term1',
displayName: '',
description: 'term1 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'gs1.term3',
name: 'term3',
displayName: '',
description: 'term3 desc',
style: {},
source: TagSource.Glossary,
labelType: LabelType.Manual,
state: State.Confirmed,
},
];
export const mockTagFQNsForHighlight = ['gs1.term2'];
export const mockGetSummaryListItemTypeResponse = DataType.Varchar;
export const mockTextBasedSummaryTitleResponse = (
<Text className="entity-title" data-testid="entity-title">
Title1
</Text>
);
export const mockLinkBasedSummaryTitleResponse = (
<Link target="_blank" to={{ pathname: 'https://task1.com' }}>
<div className="d-flex">
<Text
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
Title2
</Text>
<IconExternalLink width={12} />
</div>
</Link>
);