Skip to content

Commit da4a350

Browse files
committed
feat(UI): add autocomplete entity item component
1 parent 998f45d commit da4a350

14 files changed

+279
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CorpUser, Entity, EntityType } from '@src/types.generated';
2+
3+
/**
4+
* Type guard for users
5+
*/
6+
export function isItCorpUserEntity(entity?: Entity | null | undefined): entity is CorpUser {
7+
return !!entity && entity.type === EntityType.CorpUser;
8+
}

datahub-web-react/src/app/previewV2/BrowsePaths.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function BrowsePaths(props: Props) {
110110
{remainingParentPaths &&
111111
remainingParentPaths.map((container, index) => {
112112
return (
113-
<PlatformText>
113+
<PlatformText key={`${index}-${container.name}`}>
114114
<BrowsePathSection path={container} linksDisabled={linksDisabled} />
115115
{index < remainingParentPaths.length - 1 && <ContextPathSeparator />}
116116
</PlatformText>

datahub-web-react/src/app/previewV2/ContextPath.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ interface Props {
9696
contentRef: React.RefObject<HTMLDivElement>;
9797
isContentTruncated?: boolean;
9898
linksDisabled?: boolean;
99+
showPlatformText?: boolean;
99100
}
100101

101102
function ContextPath(props: Props) {
@@ -110,6 +111,7 @@ function ContextPath(props: Props) {
110111
contentRef,
111112
isContentTruncated = false,
112113
linksDisabled,
114+
showPlatformText = true,
113115
} = props;
114116

115117
const entityRegistry = useEntityRegistryV2();
@@ -125,15 +127,17 @@ function ContextPath(props: Props) {
125127

126128
return (
127129
<PlatformContentWrapper>
128-
<PlatformText
129-
$maxWidth={entityTitleWidth}
130-
$isCompactView={isCompactView}
131-
title={capitalizeFirstLetterOnly(type)}
132-
>
133-
{entityTypeIcon && <TypeIconWrapper>{entityTypeIcon}</TypeIconWrapper>}
134-
<PlatFormTitle>{capitalizeFirstLetterOnly(type)}</PlatFormTitle>
135-
{showEntityTypeDivider && divider}
136-
</PlatformText>
130+
{showPlatformText && (
131+
<PlatformText
132+
$maxWidth={entityTitleWidth}
133+
$isCompactView={isCompactView}
134+
title={capitalizeFirstLetterOnly(type)}
135+
>
136+
{entityTypeIcon && <TypeIconWrapper>{entityTypeIcon}</TypeIconWrapper>}
137+
<PlatFormTitle>{capitalizeFirstLetterOnly(type)}</PlatFormTitle>
138+
{showEntityTypeDivider && divider}
139+
</PlatformText>
140+
)}
137141
{hasBrowsePath ? (
138142
<BrowsePaths
139143
browsePaths={browsePaths}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Text } from '@src/alchemy-components';
2+
import { MatchText } from '@src/alchemy-components/components/MatchText';
3+
import { Entity } from '@src/types.generated';
4+
import styled from 'styled-components';
5+
import React from 'react';
6+
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
7+
import EntityIcon from './components/icon/EntityIcon';
8+
import EntitySubtitle from './components/subtitle/EntitySubtitle';
9+
import { getEntityDisplayType } from './utils';
10+
11+
const Container = styled.div`
12+
display: flex;
13+
flex-direction: row;
14+
justify-content: space-between;
15+
padding: 8px;
16+
`;
17+
18+
const ContentContainer = styled.div`
19+
display: flex;
20+
flex-direction: row;
21+
gap: 8px;
22+
`;
23+
24+
const DescriptionContainer = styled.div`
25+
display: flex;
26+
flex-direction: column;
27+
max-width: 400px;
28+
`;
29+
30+
const EntityTitleContainer = styled.div``;
31+
32+
const IconContainer = styled.div`
33+
display: flex;
34+
align-items: center;
35+
justify-content: center;
36+
width: 32px;
37+
`;
38+
39+
const TypeContainer = styled.div`
40+
display: flex;
41+
align-items: center;
42+
`;
43+
44+
interface EntityAutocompleteItemProps {
45+
entity: Entity;
46+
query?: string;
47+
siblings?: Entity[];
48+
}
49+
50+
export default function AutoCompleteEntityItem({ entity, query, siblings }: EntityAutocompleteItemProps) {
51+
const entityRegistry = useEntityRegistryV2();
52+
const displayName = entityRegistry.getDisplayName(entity.type, entity);
53+
const displayType = getEntityDisplayType(entity, entityRegistry);
54+
55+
return (
56+
<Container>
57+
<ContentContainer>
58+
<IconContainer>
59+
<EntityIcon entity={entity} siblings={siblings} />
60+
</IconContainer>
61+
62+
<DescriptionContainer>
63+
<EntityTitleContainer>
64+
<MatchText text={displayName} highlight={query ?? ''} />
65+
</EntityTitleContainer>
66+
67+
<EntitySubtitle entity={entity} />
68+
</DescriptionContainer>
69+
</ContentContainer>
70+
71+
<TypeContainer>
72+
<Text color="gray" size="sm">
73+
{displayType}
74+
</Text>
75+
</TypeContainer>
76+
</Container>
77+
);
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, { useMemo } from 'react';
2+
import styled from 'styled-components';
3+
import { EntityIconProps } from './types';
4+
import { SingleEntityIcon } from './SingleEntityIcon';
5+
6+
const Container = styled.div`
7+
display: flex;
8+
`;
9+
10+
const ICON_SIZE = 20;
11+
const SIBLING_ICON_SIZE = 16;
12+
13+
export default function DefaultEntityIcon({ entity, siblings }: EntityIconProps) {
14+
const hasSiblings = useMemo(() => (siblings?.length ?? 0) > 0, [siblings?.length]);
15+
const entitiesToShowIcons = useMemo(() => (hasSiblings ? siblings : [entity]), [hasSiblings, siblings, entity]);
16+
const iconSize = useMemo(() => (hasSiblings ? SIBLING_ICON_SIZE : ICON_SIZE), [hasSiblings]);
17+
18+
return (
19+
<Container>
20+
{entitiesToShowIcons?.map((entityToShowIcon) => (
21+
<SingleEntityIcon entity={entityToShowIcon} key={entityToShowIcon.urn} size={iconSize} />
22+
))}
23+
</Container>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { EntityType } from '@src/types.generated';
3+
import { EntityIconProps } from './types';
4+
import UserEntityIcon from './UserEntityIcon';
5+
import DefaultEntityIcon from './DefaultEntityIcon';
6+
7+
export default function EntityIcon(props: EntityIconProps) {
8+
const { entity } = props;
9+
10+
switch (entity.type) {
11+
case EntityType.CorpUser:
12+
return <UserEntityIcon {...props} />;
13+
default:
14+
return <DefaultEntityIcon {...props} />;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { IconStyleType } from '@src/app/entityV2/Entity';
2+
import { getPlatformName } from '@src/app/entityV2/shared/utils';
3+
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
4+
import { Entity } from '@src/types.generated';
5+
import { Image } from 'antd';
6+
import React, { useState } from 'react';
7+
import styled from 'styled-components';
8+
9+
const ImageIcon = styled(Image)<{ $size: number }>`
10+
height: ${(props) => props.$size}px;
11+
width: ${(props) => props.$size}px;
12+
object-fit: contain;
13+
background-color: transparent;
14+
`;
15+
16+
interface Props {
17+
entity: Entity;
18+
size: number;
19+
}
20+
21+
export function SingleEntityIcon({ entity, size }: Props) {
22+
const [isBrokenPlatformLogoUrl, setIsBrokenPlatformLogoUrl] = useState<boolean>(false);
23+
const entityRegistry = useEntityRegistryV2();
24+
25+
const properties = entityRegistry.getGenericEntityProperties(entity.type, entity);
26+
const platformLogoUrl = properties?.platform?.properties?.logoUrl;
27+
const platformName = getPlatformName(properties);
28+
29+
return (
30+
(platformLogoUrl && !isBrokenPlatformLogoUrl && (
31+
<ImageIcon
32+
preview={false}
33+
src={platformLogoUrl}
34+
alt={platformName || ''}
35+
$size={size}
36+
onError={() => setIsBrokenPlatformLogoUrl(true)}
37+
/>
38+
)) ||
39+
entityRegistry.getIcon(entity.type, size, IconStyleType.ACCENT)
40+
);
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { Avatar } from '@src/alchemy-components';
3+
import { isItCorpUserEntity } from '@src/app/entityV2/user/utils';
4+
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
5+
import { EntityIconProps } from './types';
6+
7+
export default function UserEntityIcon({ entity }: EntityIconProps) {
8+
const entityRegistry = useEntityRegistryV2();
9+
10+
if (!isItCorpUserEntity(entity)) return null;
11+
12+
const imageUrl = entity?.editableProperties?.pictureLink;
13+
const displayName = entityRegistry.getDisplayName(entity.type, entity);
14+
15+
return <Avatar name={displayName} imageUrl={imageUrl} size="md" />;
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Entity } from '@src/types.generated';
2+
3+
export interface EntityIconProps {
4+
entity: Entity;
5+
siblings?: Entity[];
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { getContextPath } from '@src/app/entityV2/shared/containers/profile/header/getContextPath';
3+
import ContextPath from '@src/app/previewV2/ContextPath';
4+
import useContentTruncation from '@src/app/shared/useContentTruncation';
5+
import { useEntityRegistryV2 } from '@src/app/useEntityRegistry';
6+
import { EntitySubtitleProps } from './types';
7+
8+
export default function DefaultEntitySubtitle({ entity }: EntitySubtitleProps) {
9+
const entityRegistry = useEntityRegistryV2();
10+
const genericEntityProperties = entityRegistry.getGenericEntityProperties(entity.type, entity);
11+
const parentEntities = getContextPath(genericEntityProperties);
12+
13+
const { contentRef, isContentTruncated } = useContentTruncation(genericEntityProperties);
14+
15+
return (
16+
<ContextPath
17+
instanceId={genericEntityProperties?.dataPlatformInstance?.instanceId}
18+
showPlatformText={false}
19+
entityType={entity.type}
20+
browsePaths={genericEntityProperties?.browsePathV2}
21+
parentEntities={parentEntities}
22+
contentRef={contentRef}
23+
isContentTruncated={isContentTruncated}
24+
/>
25+
);
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { EntityType } from '@src/types.generated';
3+
import { EntityIconProps } from '../icon/types';
4+
import UserEntitySubtitle from './UserEntitySubtitle';
5+
import DefaultEntitySubtitle from './DefaultEntitySubtitle';
6+
7+
export default function EntitySubtitle(props: EntityIconProps) {
8+
const { entity } = props;
9+
10+
switch (entity.type) {
11+
case EntityType.CorpUser:
12+
return <UserEntitySubtitle {...props} />;
13+
default:
14+
return <DefaultEntitySubtitle {...props} />;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { Text } from '@src/alchemy-components';
3+
import { isItCorpUserEntity } from '@src/app/entityV2/user/utils';
4+
import { EntitySubtitleProps } from './types';
5+
6+
export default function UserEntitySubtitle({ entity }: EntitySubtitleProps) {
7+
if (!isItCorpUserEntity(entity)) return null;
8+
9+
const userName = entity?.username;
10+
11+
return (
12+
<Text color="gray" size="sm">
13+
{userName}
14+
</Text>
15+
);
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Entity } from '@src/types.generated';
2+
3+
export interface EntitySubtitleProps {
4+
entity: Entity;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import EntityRegistry from '@src/app/entityV2/EntityRegistry';
2+
import { capitalizeFirstLetterOnly } from '@src/app/shared/textUtil';
3+
import { Entity } from '@src/types.generated';
4+
5+
export function getEntityDisplayType(entity: Entity, registry: EntityRegistry) {
6+
const properties = registry.getGenericEntityProperties(entity.type, entity);
7+
8+
const subtype = properties?.subTypes?.typeNames?.[0];
9+
const entityName = registry.getEntityName(entity.type);
10+
const displayType = capitalizeFirstLetterOnly((subtype ?? entityName)?.toLocaleLowerCase());
11+
return displayType;
12+
}

0 commit comments

Comments
 (0)