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
entityDetail={entityDetails}
tags={
getSortedTagsWithHighlight({
tags: entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get(
highlights,
'tag.name',
[] as string[]
),
}) ?? []
}
tags={getSortedTagsWithHighlight(
entityDetails.tags,
get(highlights, 'tag.name')
)}
/>
<Divider className="m-y-xs" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -167,15 +167,10 @@ function TopicSummary({
entityDetail={entityDetails}
tags={
tags ??
getSortedTagsWithHighlight({
tags: entityDetails.tags,
sortTagsBasedOnGivenTagFQNs: get(
highlights,
'tag.name',
[] as string[]
),
}) ??
[]
getSortedTagsWithHighlight(
entityDetails.tags,
get(highlights, 'tag.name')
)
}
/>
<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 { TagSource } from '../../../generated/type/tagLabel';
import { reduceColorOpacity } from '../../../utils/CommonUtils';
import { HighlightedTagLabel } from '../../../utils/EntitySummaryPanelUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import Fqn from '../../../utils/Fqn';
import { getEncodedFqn } from '../../../utils/StringsUtils';
import { getTagDisplay, getTagTooltip } from '../../../utils/TagsUtils';
import { HighlightedTagLabel } from '../../Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { TagsV1Props } from './TagsV1.interface';
import './tagsV1.less';

View File

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

View File

@ -14,15 +14,15 @@ import { Col, Divider, Row, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
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 RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
const SummaryTagsDescription = ({
tags,
tags = [],
entityDetail,
}: {
tags: TagLabel[];
tags: BasicEntityInfo['tags'];
entityDetail: EntityUnion;
}) => {
const { t } = useTranslation();

View File

@ -15,6 +15,8 @@ import { SummaryEntityType } from '../enums/EntitySummary.enum';
import { Column } from '../generated/entity/data/table';
import {
getFormattedEntityData,
getHighlightOfListItem,
getMapOfListHighlights,
getSortedTagsWithHighlight,
getSummaryListItemType,
getTitle,
@ -24,87 +26,146 @@ import {
mockEntityDataWithNestingResponse,
mockEntityDataWithoutNesting,
mockEntityDataWithoutNestingResponse,
mockGetHighlightOfListItemResponse,
mockGetMapOfListHighlightsResponse,
mockGetSummaryListItemTypeResponse,
mockHighlights,
mockInvalidDataResponse,
mockLinkBasedSummaryTitleResponse,
mockListItemNameHighlight,
mockTagFQNsForHighlight,
mockTagsDataAfterSortAndHighlight,
mockTagsDataBeforeSortAndHighlight,
mockTagsSortAndHighlightResponse,
mockTextBasedSummaryTitleResponse,
} 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', () => {
it('getFormattedEntityData should return formatted data properly for table columns data without nesting, and also sort the data based on given arr', () => {
const highlights = {
'tag.name': ['PersonalData.SpecialCategory'],
};
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
mockEntityDataWithoutNesting,
highlights
);
describe('getFormattedEntityData', () => {
it('getFormattedEntityData should return formatted data properly for table columns data with nesting, and also sort the data based on highlights', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
mockEntityDataWithNesting,
mockHighlights
);
expect(resultFormattedData).toEqual(mockEntityDataWithoutNestingResponse);
});
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(resultFormattedData).toEqual(mockEntityDataWithNestingResponse);
});
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', () => {
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(resultFormattedData).toEqual(mockEntityDataWithoutNestingResponse);
});
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({
content: 'Title2',
sourceUrl: 'https://task1.com',
expect(resultFormattedData).toEqual([]);
});
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 { SearchedDataProps } from '../../src/components/SearchedData/SearchedData.interface';
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 { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant';
import { SummaryEntityType } from '../enums/EntitySummary.enum';
@ -37,20 +40,30 @@ export interface EntityNameProps {
displayName?: string;
}
export interface HighlightedTagLabel extends TagLabel {
isHighlighted: boolean;
export type SummaryListItem = Column | Field | Chart | Task | MlFeature;
export interface ListItemHighlights {
highlightedTags?: BasicEntityInfo['tags'];
highlightedTitle?: string;
highlightedDescription?: string;
}
const getTitleName = (data: EntityNameProps) =>
getEntityName(data) || NO_DATA_PLACEHOLDER;
/* @param {
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 ? (
<Link target="_blank" to={{ pathname: sourceUrl }}>
<div className="d-flex">
@ -58,65 +71,189 @@ export const getTitle = ({
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
{content}
{title}
</Text>
<IconExternalLink width={12} />
</div>
</Link>
) : (
<Text className="entity-title" data-testid="entity-title">
{content}
{title}
</Text>
);
};
/* @param {
entityType: will be any type of SummaryEntityType,
listItem: SummaryItem
}
@return listItemType
*/
export const getSummaryListItemType = (
entityType: SummaryEntityType,
listItemInfo: Column | Field | Chart | Task | MlFeature
) => {
listItem: SummaryListItem
): BasicEntityInfo['type'] => {
switch (entityType) {
case SummaryEntityType.COLUMN:
case SummaryEntityType.FIELD:
case SummaryEntityType.MLFEATURE:
case SummaryEntityType.SCHEMAFIELD:
return (listItemInfo as Column | Field | MlFeature).dataType;
return (listItem as Column | Field | MlFeature).dataType;
case SummaryEntityType.CHART:
return (listItemInfo as Chart).chartType;
return (listItem as Chart).chartType;
case SummaryEntityType.TASK:
return (listItemInfo as Task).taskType;
return (listItem as Task).taskType;
default:
return '';
}
};
export const getSortedTagsWithHighlight = ({
sortTagsBasedOnGivenTagFQNs,
tags,
}: {
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);
/*
@params {
sortTagsBasedOnGivenTagFQNs: array of TagFQNs,
tags: Tags array,
}
return acc;
}, ColumnDataTags);
@return array of tags highlighted and sorted if tagFQN present in sortTagsBasedOnGivenTagFQNs
*/
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 = (
entityType: SummaryEntityType,
entityInfo?: Array<Column | Field | Chart | Task | MlFeature>,
entityInfo: Array<SummaryListItem> = [],
highlights?: SearchedDataProps['data'][number]['highlight'],
tableConstraints?: TableConstraint[]
): BasicEntityInfo[] => {
@ -124,97 +261,68 @@ export const getFormattedEntityData = (
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)) {
// tagHighlights is the array of tagFQNs for highlighting tags
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.push(...get(highlights, highlightKey, []));
});
// listHighlights i.e. highlight get from query api
// 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) => {
acc[colHighlight.replaceAll(/<\/?span(.*?)>/g, '')] = index;
const { highlightedListItem, remainingListItem } = entityInfo.reduce(
(acc, listItem) => {
// return the highlight of listItem
const { highlightedTags, highlightedTitle, highlightedDescription } =
getHighlightOfListItem(
listItem,
tagHighlights,
listHighlights,
listHighlightsMap
);
return acc;
}, listHighlightsMap);
// convert listItem in BasicEntityInfo type
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) => {
const listItemModifiedData = {
name: listItem.name ?? '',
title: getTitle({
content: getTitleName(listItem),
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 highlights present in listItem then sort the listItem
if (highlightedTags || highlightedTitle || highlightedDescription) {
acc.highlightedListItem.push(listItemModifiedData);
} else {
acc.remainingListItem.push(listItemModifiedData);
}
if (!isUndefined(highlightedListItemNameIndex)) {
listItemModifiedData.title = getTitle({
content: stringToHTML(listHighlights[highlightedListItemNameIndex]),
sourceUrl: (listItem as Chart | Task).sourceUrl,
});
}
if (!isUndefined(highlightedListItemDescriptionIndex)) {
listItemModifiedData.description =
listHighlights[highlightedListItemDescriptionIndex];
}
acc.listItemWithSortOption.push(listItemModifiedData);
} else {
acc.listItemWithoutSortOption.push(listItemModifiedData);
return acc;
},
{
highlightedListItem: [] as BasicEntityInfo[],
remainingListItem: [] as BasicEntityInfo[],
}
);
return acc;
}, SummaryListData);
return [
...SummaryListData.listItemWithSortOption,
...SummaryListData.listItemWithoutSortOption,
];
} else {
return [];
return [...highlightedListItem, ...remainingListItem];
}
return [];
};

View File

@ -14,6 +14,8 @@
import { Typography } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import { BasicEntityInfo } from '../../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { Task } from '../../generated/entity/data/pipeline';
import {
Column,
DataType,
@ -21,21 +23,189 @@ import {
State,
TagSource,
} from '../../generated/entity/data/table';
import { DataTypeTopic, Field } from '../../generated/entity/data/topic';
import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg';
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,
dataLength: 100,
dataTypeDisplay: 'varchar',
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
description: 'some description of title2',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".title',
'sample_data.ecommerce_db.shopify."dim.api/client".title2',
tags: [],
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: [],
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',
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: (
<Text className="entity-title" data-testid="entity-title">
api_client_id
</Text>
),
type: 'NUMERIC',
tableConstraints: undefined,
columnConstraint: undefined,
},
{
children: [],
type: DataType.Numeric,
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
name: 'title',
tags: [],
title: (
<Text className="entity-title" data-testid="entity-title">
title
</Text>
),
type: 'VARCHAR',
'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.',
tags: mockTagsSortAndHighlightResponse,
tableConstraints: undefined,
columnConstraint: undefined,
children: [],
},
];
export const mockEntityDataWithNesting: Field[] = [
{
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: (
<Text className="entity-title" data-testid="entity-title">
Customer
</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,
},
];
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>
);