import { Divider, Tooltip, Typography } from 'antd'; import React, { ReactNode, useState } from 'react'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { PreviewType } from '@app/entity/Entity'; import { useEntityData } from '@app/entity/shared/EntityContext'; import ExternalUrlButton from '@app/entity/shared/ExternalUrlButton'; import { usePreviewData } from '@app/entity/shared/PreviewContext'; import { DeprecationPill } from '@app/entity/shared/components/styled/DeprecationPill'; import { ExpandedActorGroup } from '@app/entity/shared/components/styled/ExpandedActorGroup'; import NoMarkdownViewer from '@app/entity/shared/components/styled/StripMarkdownText'; import { ANTD_GRAY } from '@app/entity/shared/constants'; import EntityCount from '@app/entity/shared/containers/profile/header/EntityCount'; import { EntityHealth } from '@app/entity/shared/containers/profile/header/EntityHealth'; import PlatformContentView from '@app/entity/shared/containers/profile/header/PlatformContent/PlatformContentView'; import StructuredPropertyBadge from '@app/entity/shared/containers/profile/header/StructuredPropertyBadge'; import { getNumberWithOrdinal } from '@app/entity/shared/utils'; import EntityPaths from '@app/preview/EntityPaths/EntityPaths'; import { getUniqueOwners } from '@app/preview/utils'; import SearchTextHighlighter from '@app/search/matches/SearchTextHighlighter'; import { DataProductLink } from '@app/shared/tags/DataProductLink'; import TagTermGroup from '@app/shared/tags/TagTermGroup'; import useContentTruncation from '@app/shared/useContentTruncation'; import DataProcessInstanceInfo from '@src/app/preview/DataProcessInstanceInfo'; import { Container, CorpUser, DataProcessRunEvent, DataProduct, Dataset, Deprecation, Domain, Entity, EntityPath, GlobalTags, GlossaryTerms, Health, Maybe, Owner, ParentContainersResult, SearchInsight, } from '@types'; const PreviewContainer = styled.div` display: flex; width: 100%; justify-content: space-between; align-items: center; `; const LeftColumn = styled.div<{ expandWidth: boolean }>` max-width: ${(props) => (props.expandWidth ? '100%' : '60%')}; `; const RightColumn = styled.div` max-width: 40%; display: flex; `; const TitleContainer = styled.div` margin-bottom: 5px; line-height: 30px; .entityCount { margin-bottom: 2px; } `; const EntityTitleContainer = styled.div` display: flex; align-items: center; gap: 8px; `; const EntityTitle = styled(Typography.Text)<{ $titleSizePx?: number }>` display: block; &&&:hover { text-decoration: underline; } &&& { font-size: ${(props) => props.$titleSizePx || 16}px; font-weight: 600; vertical-align: middle; } `; const CardEntityTitle = styled(EntityTitle)` max-width: 350px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const PlatformText = styled(Typography.Text)` font-size: 12px; line-height: 20px; font-weight: 700; color: ${ANTD_GRAY[7]}; `; const PlatformDivider = styled.div` display: inline-block; padding-left: 10px; margin-right: 10px; border-right: 1px solid ${ANTD_GRAY[4]}; height: 21px; vertical-align: text-top; `; const DescriptionContainer = styled.div` color: ${ANTD_GRAY[7]}; margin-bottom: 8px; `; const TagContainer = styled.div` display: inline-flex; margin-left: 0px; margin-top: 3px; flex-wrap: wrap; margin-right: 8px; `; const TagSeparator = styled.div` margin: 2px 8px 0 0; height: 17px; border-right: 1px solid #cccccc; `; const InsightContainer = styled.div` margin-top: 12px; `; const InsightsText = styled(Typography.Text)` font-size: 12px; line-height: 20px; font-weight: 600; color: ${ANTD_GRAY[7]}; `; const InsightIconContainer = styled.span` margin-right: 4px; `; const UserListContainer = styled.div` display: flex; flex-direction: column; justify-content: right; margin-right: 8px; `; const UserListDivider = styled(Divider)` padding: 4px; height: auto; `; const UserListTitle = styled(Typography.Text)` text-align: right; margin-bottom: 10px; padding-right: 12px; `; interface Props { name: string; urn: string; logoUrl?: string; logoComponent?: JSX.Element; url: string; description?: string; type?: string; typeIcon?: JSX.Element; platform?: string; platformInstanceId?: string; platforms?: Maybe[]; logoUrls?: Maybe[]; qualifier?: string | null; tags?: GlobalTags; owners?: Array | null; deprecation?: Deprecation | null; topUsers?: Array | null; externalUrl?: string | null; entityTitleSuffix?: React.ReactNode; subHeader?: React.ReactNode; snippet?: React.ReactNode; insights?: Array | null; glossaryTerms?: GlossaryTerms; container?: Container; domain?: Domain | undefined | null; dataProduct?: DataProduct | undefined | null; entityCount?: number; displayAssetCount?: boolean; dataTestID?: string; titleSizePx?: number; onClick?: () => void; // this is provided by the impact analysis view. it is used to display // how the listed node is connected to the source node degree?: number; parentContainers?: ParentContainersResult | null; parentEntities?: Entity[] | null; previewType?: Maybe; paths?: EntityPath[]; health?: Health[]; parentDataset?: Dataset; lastRunEvent?: DataProcessRunEvent | null; } export default function DefaultPreviewCard({ name, urn, logoUrl, logoComponent, url, description, type, typeIcon, platform, platformInstanceId, // TODO(Gabe): support qualifier in the new preview card // eslint-disable-next-line @typescript-eslint/no-unused-vars qualifier, tags, owners, topUsers, subHeader, snippet, insights, glossaryTerms, domain, dataProduct, container, deprecation, entityCount, displayAssetCount, titleSizePx, dataTestID, externalUrl, entityTitleSuffix, onClick, degree, parentContainers, parentEntities, platforms, logoUrls, previewType, paths, health, parentDataset, lastRunEvent, }: Props) { // sometimes these lists will be rendered inside an entity container (for example, in the case of impact analysis) // in those cases, we may want to enrich the preview w/ context about the container entity const { entityData } = useEntityData(); const previewData = usePreviewData(); const insightViews: Array = [ ...(insights?.map((insight) => ( <> {insight.icon} {insight.text} )) || []), ]; const hasGlossaryTerms = !!glossaryTerms?.terms?.length; const hasTags = !!tags?.tags?.length; if (snippet) { insightViews.push(snippet); } const [descriptionExpanded, setDescriptionExpanded] = useState(false); const { contentRef, isContentTruncated } = useContentTruncation(container); const onPreventMouseDown = (event) => { event.preventDefault(); event.stopPropagation(); }; const shouldShowRightColumn = (topUsers && topUsers.length > 0) || (owners && owners.length > 0) || lastRunEvent?.timestampMillis || lastRunEvent?.durationMillis || lastRunEvent?.result?.resultType; const uniqueOwners = getUniqueOwners(owners); return ( {previewType === PreviewType.HOVER_CARD ? ( {name || ' '} ) : ( )} {deprecation?.deprecated && ( )} {health && health.length > 0 ? : null} {externalUrl && ( )} {entityTitleSuffix} {degree !== undefined && degree !== null && ( {getNumberWithOrdinal(degree)} )} {!!degree && entityCount && } {paths && paths.length > 0 && } {description && description.length > 0 && ( { onPreventMouseDown(e); setDescriptionExpanded(!descriptionExpanded); }} > {descriptionExpanded ? 'Show Less' : 'Show More'} ) : undefined } customRender={(text) => } > {description} )} {(dataProduct || domain || hasGlossaryTerms || hasTags) && ( {/* if there's a domain and dataProduct, show dataProduct */} {dataProduct && } {!dataProduct && domain && } {(dataProduct || domain) && hasGlossaryTerms && } {hasGlossaryTerms && } {((hasGlossaryTerms && hasTags) || ((dataProduct || domain) && hasTags)) && } {hasTags && } )} {subHeader} {insightViews.length > 0 && ( {insightViews.map((insightView, index) => ( {insightView} {index < insightViews.length - 1 && } ))} )} {shouldShowRightColumn && ( {topUsers && topUsers?.length > 0 && ( <> Top Users
)} {(topUsers?.length || 0) > 0 && (uniqueOwners?.length || 0) > 0 && ( )} {uniqueOwners && uniqueOwners?.length > 0 && ( Owners
owner.owner)} max={2} />
)}
)}
); }