Skip to content

Commit

Permalink
Combine siblings in autocomplete (#8610)
Browse files Browse the repository at this point in the history
  • Loading branch information
Joshua Eilers authored and yoonhyejin committed Aug 24, 2023
1 parent aae1fd7 commit cc8b007
Show file tree
Hide file tree
Showing 21 changed files with 804 additions and 622 deletions.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions datahub-web-react/src/app/entity/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ANTD_GRAY = {
export const ANTD_GRAY_V2 = {
2: '#F3F5F6',
5: '#DDE0E4',
6: '#B2B8BD',
8: '#5E666E',
10: '#1B1E22',
};
Expand Down
80 changes: 37 additions & 43 deletions datahub-web-react/src/app/entity/shared/siblingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import merge from 'deepmerge';
import { unionBy, keyBy, values } from 'lodash';
import { useLocation } from 'react-router-dom';
import * as QueryString from 'query-string';
import { Dataset, Entity, MatchedField, Maybe, SiblingProperties } from '../../../types.generated';
import { Dataset, Entity, Maybe, SiblingProperties } from '../../../types.generated';
import { GenericEntityProperties } from './types';

export function stripSiblingsFromEntity(entity: any) {
Expand Down Expand Up @@ -215,54 +215,48 @@ export const combineEntityDataWithSiblings = <T>(baseEntity: T): T => {
return { [baseEntityKey]: combinedBaseEntity } as unknown as T;
};

export type CombinedSearchResult = {
export type CombinedEntity = {
entity: Entity;
matchedFields: MatchedField[];
matchedEntities?: Entity[];
matchedEntities?: Array<Entity>;
};

export function combineSiblingsInSearchResults(
results:
| {
entity: Entity;
matchedFields: MatchedField[];
}[]
| undefined,
) {
const combinedResults: CombinedSearchResult[] | undefined = [];
const siblingsToPair: Record<string, CombinedSearchResult> = {};

// set sibling associations
results?.forEach((result) => {
if (result.entity.urn in siblingsToPair) {
// filter from repeating
// const siblingsCombinedResult = siblingsToPair[result.entity.urn];
// siblingsCombinedResult.matchedEntities?.push(result.entity);
return;
}
type CombinedEntityResult =
| {
skipped: true;
}
| {
skipped: false;
combinedEntity: CombinedEntity;
};

export function combineSiblingsForEntity(entity: Entity, visitedSiblingUrns: Set<string>): CombinedEntityResult {
if (visitedSiblingUrns.has(entity.urn)) return { skipped: true };

const combinedEntity: CombinedEntity = { entity: combineEntityWithSiblings({ ...entity }) };
const siblings = (combinedEntity.entity as GenericEntityProperties).siblings?.siblings ?? [];
const isPrimary = (combinedEntity.entity as GenericEntityProperties).siblings?.isPrimary;
const siblingUrns = siblings.map((sibling) => sibling?.urn);

if (siblingUrns.length > 0) {
combinedEntity.matchedEntities = isPrimary
? [stripSiblingsFromEntity(combinedEntity.entity), ...siblings]
: [...siblings, stripSiblingsFromEntity(combinedEntity.entity)];

combinedEntity.matchedEntities = combinedEntity.matchedEntities.filter(
(resultToFilter) => (resultToFilter as Dataset).exists,
);

siblingUrns.forEach((urn) => urn && visitedSiblingUrns.add(urn));
}

const combinedResult: CombinedSearchResult = result;
combinedResult.entity = combineEntityWithSiblings({ ...result.entity });
const { entity }: { entity: any } = result;
const siblingUrns = entity?.siblings?.siblings?.map((sibling) => sibling.urn) || [];
if (siblingUrns.length > 0) {
combinedResult.matchedEntities = entity.siblings.isPrimary
? [stripSiblingsFromEntity(entity), ...entity.siblings.siblings]
: [...entity.siblings.siblings, stripSiblingsFromEntity(entity)];

combinedResult.matchedEntities = combinedResult.matchedEntities.filter(
(resultToFilter) => (resultToFilter as Dataset).exists,
);

siblingUrns.forEach((urn) => {
siblingsToPair[urn] = combinedResult;
});
}
combinedResults.push(combinedResult);
});
return { combinedEntity, skipped: false };
}

return combinedResults;
export function createSiblingEntityCombiner() {
const visitedSiblingUrns: Set<string> = new Set();
return (entity: Entity) => combineSiblingsForEntity(entity, visitedSiblingUrns);
}

// used to determine whether sibling entities should be shown merged or not
export const SEPARATE_SIBLINGS_URL_PARAM = 'separate_siblings';

Expand Down
1 change: 1 addition & 0 deletions datahub-web-react/src/app/home/HomePageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const HomePageHeader = () => {
autoCompleteStyle={styles.searchBox}
entityRegistry={entityRegistry}
viewsEnabled={viewsEnabled}
combineSiblings
showQuickFilters
/>
{searchResultsToShow && searchResultsToShow.length > 0 && (
Expand Down
1 change: 0 additions & 1 deletion datahub-web-react/src/app/preview/DefaultPreviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ export default function DefaultPreviewCard({
/>
)}
</EntityTitleContainer>

{degree !== undefined && degree !== null && (
<Tooltip
title={`This entity is a ${getNumberWithOrdinal(degree)} degree connection to ${
Expand Down
42 changes: 24 additions & 18 deletions datahub-web-react/src/app/search/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Input, AutoComplete, Button } from 'antd';
import { CloseCircleFilled, SearchOutlined } from '@ant-design/icons';
import styled from 'styled-components/macro';
import { useHistory } from 'react-router';
import { AutoCompleteResultForEntity, Entity, EntityType, FacetFilterInput, ScenarioType } from '../../types.generated';
import { AutoCompleteResultForEntity, EntityType, FacetFilterInput, ScenarioType } from '../../types.generated';
import EntityRegistry from '../entity/EntityRegistry';
import filterSearchQuery from './utils/filterSearchQuery';
import { ANTD_GRAY, ANTD_GRAY_V2 } from '../entity/shared/constants';
Expand All @@ -23,6 +23,7 @@ import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
import { getQuickFilterDetails } from './autoComplete/quickFilters/utils';
import ViewAllSearchItem from './ViewAllSearchItem';
import { ViewSelect } from '../entity/view/select/ViewSelect';
import { combineSiblingsInAutoComplete } from './utils/combineSiblingsInAutoComplete';

const StyledAutoComplete = styled(AutoComplete)`
width: 100%;
Expand Down Expand Up @@ -88,15 +89,6 @@ const QUICK_FILTER_AUTO_COMPLETE_OPTION = {
],
};

const renderItem = (query: string, entity: Entity) => {
return {
value: entity.urn,
label: <AutoCompleteItem query={query} entity={entity} />,
type: entity.type,
style: { padding: '12px 12px 12px 16px' },
};
};

const renderRecommendedQuery = (query: string) => {
return {
value: query,
Expand All @@ -123,6 +115,7 @@ interface Props {
hideRecommendations?: boolean;
showQuickFilters?: boolean;
viewsEnabled?: boolean;
combineSiblings?: boolean;
setIsSearchBarFocused?: (isSearchBarFocused: boolean) => void;
onFocus?: () => void;
onBlur?: () => void;
Expand All @@ -149,6 +142,7 @@ export const SearchBar = ({
hideRecommendations,
showQuickFilters,
viewsEnabled = false,
combineSiblings = false,
setIsSearchBarFocused,
onFocus,
onBlur,
Expand Down Expand Up @@ -227,14 +221,26 @@ export const SearchBar = ({
];
}, [showQuickFilters, suggestions.length, effectiveQuery, selectedQuickFilter, entityRegistry]);

const autoCompleteEntityOptions = useMemo(
() =>
suggestions.map((entity: AutoCompleteResultForEntity) => ({
label: <SectionHeader entityType={entity.type} />,
options: [...entity.entities.map((e: Entity) => renderItem(effectiveQuery, e))],
})),
[effectiveQuery, suggestions],
);
const autoCompleteEntityOptions = useMemo(() => {
return suggestions.map((suggestion: AutoCompleteResultForEntity) => {
const combinedSuggestion = combineSiblingsInAutoComplete(suggestion, { combineSiblings });
return {
label: <SectionHeader entityType={combinedSuggestion.type} />,
options: combinedSuggestion.combinedEntities.map((combinedEntity) => ({
value: combinedEntity.entity.urn,
label: (
<AutoCompleteItem
query={effectiveQuery}
entity={combinedEntity.entity}
siblings={combineSiblings ? combinedEntity.matchedEntities : undefined}
/>
),
type: combinedEntity.entity.type,
style: { padding: '12px 12px 12px 16px' },
})),
};
});
}, [combineSiblings, effectiveQuery, suggestions]);

const previousSelectedQuickFilterValue = usePrevious(selectedQuickFilter?.value);
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions datahub-web-react/src/app/search/SearchHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const SearchHeader = ({
entityRegistry={entityRegistry}
setIsSearchBarFocused={setIsSearchBarFocused}
viewsEnabled={viewsEnabled}
combineSiblings
fixAutoComplete
showQuickFilters
/>
Expand Down
3 changes: 2 additions & 1 deletion datahub-web-react/src/app/search/SearchResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { useHistory } from 'react-router';
import { RocketOutlined } from '@ant-design/icons';
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
import { ANTD_GRAY } from '../entity/shared/constants';
import { CombinedSearchResult, SEPARATE_SIBLINGS_URL_PARAM } from '../entity/shared/siblingUtils';
import { SEPARATE_SIBLINGS_URL_PARAM } from '../entity/shared/siblingUtils';
import { CompactEntityNameList } from '../recommendations/renderer/component/CompactEntityNameList';
import { useEntityRegistry } from '../useEntityRegistry';
import { SearchResult } from '../../types.generated';
import analytics, { EventType } from '../analytics';
import { EntityAndType } from '../entity/shared/types';
import { useIsSearchV2 } from './useSearchAndBrowseVersion';
import { CombinedSearchResult } from './utils/combineSiblingsInSearchResults';

const ResultList = styled(List)`
&&& {
Expand Down
2 changes: 1 addition & 1 deletion datahub-web-react/src/app/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Entity, FacetFilterInput, FacetMetadata, MatchedField } from '../../typ
import { SearchCfg } from '../../conf';
import { SearchResultsRecommendations } from './SearchResultsRecommendations';
import SearchExtendedMenu from '../entity/shared/components/styled/search/SearchExtendedMenu';
import { combineSiblingsInSearchResults } from '../entity/shared/siblingUtils';
import { SearchSelectBar } from '../entity/shared/components/styled/search/SearchSelectBar';
import { SearchResultList } from './SearchResultList';
import { isListSubset } from '../entity/shared/utils';
Expand All @@ -26,6 +25,7 @@ import { BrowseProvider } from './sidebar/BrowseContext';
import { useIsBrowseV2, useIsSearchV2 } from './useSearchAndBrowseVersion';
import useToggleSidebar from './useToggleSidebar';
import SearchSortSelect from './sorting/SearchSortSelect';
import { combineSiblingsInSearchResults } from './utils/combineSiblingsInSearchResults';

const SearchResultsWrapper = styled.div<{ v2Styles: boolean }>`
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Image, Typography } from 'antd';
import { Typography } from 'antd';
import React from 'react';
import styled from 'styled-components/macro';
import { Entity } from '../../../types.generated';
import { Entity, EntityType } from '../../../types.generated';
import { useEntityRegistry } from '../../useEntityRegistry';
import { getPlatformName } from '../../entity/shared/utils';
import { IconStyleType } from '../../entity/Entity';
import { getAutoCompleteEntityText } from './utils';
import { SuggestionText } from './AutoCompleteUser';
import ParentContainers from './ParentContainers';
import { ANTD_GRAY } from '../../entity/shared/constants';
import { ANTD_GRAY_V2 } from '../../entity/shared/constants';
import AutoCompleteEntityIcon from './AutoCompleteEntityIcon';
import { SuggestionText } from './styledComponents';
import AutoCompletePlatformNames from './AutoCompletePlatformNames';
import { getPlatformName } from '../../entity/shared/utils';

const AutoCompleteEntityWrapper = styled.div`
display: flex;
Expand All @@ -17,12 +18,8 @@ const AutoCompleteEntityWrapper = styled.div`
align-items: center;
`;

const PreviewImage = styled(Image)`
height: 22px;
width: 22px;
width: auto;
object-fit: contain;
background-color: transparent;
const IconsContainer = styled.div`
display: flex;
`;

const ContentWrapper = styled.div`
Expand All @@ -32,42 +29,74 @@ const ContentWrapper = styled.div`
`;

const Subtype = styled.span`
color: ${ANTD_GRAY[9]};
border: 1px solid ${ANTD_GRAY[9]};
color: ${ANTD_GRAY_V2[8]};
border: 1px solid ${ANTD_GRAY_V2[6]};
border-radius: 16px;
padding: 4px 8px;
line-height: 12px;
font-size: 12px;
margin-right: 8px;
`;

const ItemHeader = styled.div`
display: flex;
align-items: center;
margin-bottom: 3px;
gap: 8px;
`;

const Divider = styled.div`
border-right: 1px solid ${ANTD_GRAY_V2[6]};
height: 12px;
`;

interface Props {
query: string;
entity: Entity;
siblings?: Array<Entity>;
hasParentTooltip: boolean;
}

export default function AutoCompleteEntity({ query, entity, hasParentTooltip }: Props) {
export default function AutoCompleteEntity({ query, entity, siblings, hasParentTooltip }: Props) {
const entityRegistry = useEntityRegistry();
const genericEntityProps = entityRegistry.getGenericEntityProperties(entity.type, entity);
const platformName = getPlatformName(genericEntityProps);
const platformLogoUrl = genericEntityProps?.platform?.properties?.logoUrl;
const displayName = entityRegistry.getDisplayName(entity.type, entity);
const icon =
(platformLogoUrl && <PreviewImage preview={false} src={platformLogoUrl} alt={platformName || ''} />) ||
entityRegistry.getIcon(entity.type, 12, IconStyleType.ACCENT);
const { matchedText, unmatchedText } = getAutoCompleteEntityText(displayName, query);
const entities = siblings?.length ? siblings : [entity];
const platforms =
genericEntityProps?.siblingPlatforms
?.map(
(platform) =>
getPlatformName(entityRegistry.getGenericEntityProperties(EntityType.DataPlatform, platform)) || '',
)
.filter(Boolean) ?? [];

const parentContainers = genericEntityProps?.parentContainers?.containers || [];
// Need to reverse parentContainers since it returns direct parent first.
const orderedParentContainers = [...parentContainers].reverse();
const subtype = genericEntityProps?.subTypes?.typeNames?.[0];

const showPlatforms = !!platforms.length;
const showPlatformDivider = !!platforms.length && !!parentContainers.length;
const showParentContainers = !!parentContainers.length;
const showHeader = showPlatforms || showParentContainers;

return (
<AutoCompleteEntityWrapper data-testid={`auto-complete-entity-name-${displayName}`}>
<ContentWrapper>
{icon}
<SuggestionText>
<ParentContainers parentContainers={orderedParentContainers} />
{showHeader && (
<ItemHeader>
<IconsContainer>
{entities.map((ent) => (
<AutoCompleteEntityIcon key={ent.urn} entity={ent} />
))}
</IconsContainer>
{showPlatforms && <AutoCompletePlatformNames platforms={platforms} />}
{showPlatformDivider && <Divider />}
{showParentContainers && <ParentContainers parentContainers={orderedParentContainers} />}
</ItemHeader>
)}
<Typography.Text
ellipsis={
hasParentTooltip ? {} : { tooltip: { title: displayName, color: 'rgba(0, 0, 0, 0.9)' } }
Expand Down
Loading

0 comments on commit cc8b007

Please sign in to comment.