import React, { useContext, useEffect, useMemo, useState } from 'react'; import { Group } from '@visx/group'; import { LinkHorizontal } from '@visx/shape'; import styled from 'styled-components'; import { useEntityRegistry } from '../useEntityRegistry'; import { IconStyleType } from '../entity/Entity'; import { Direction, VizNode, EntitySelectParams, EntityAndType, UpdatedLineages } from './types'; import { ANTD_GRAY } from '../entity/shared/constants'; import { capitalizeFirstLetterOnly } from '../shared/textUtil'; import { getShortenedTitle, nodeHeightFromTitleLength } from './utils/titleUtils'; import { LineageExplorerContext } from './utils/LineageExplorerContext'; import { useGetEntityLineageLazyQuery } from '../../graphql/lineage.generated'; import { useIsSeparateSiblingsMode } from '../entity/shared/siblingUtils'; import { centerX, centerY, iconHeight, iconWidth, iconX, iconY, textX, width, healthX, healthY } from './constants'; import LineageEntityColumns from './LineageEntityColumns'; import { convertInputFieldsToSchemaFields } from './utils/columnLineageUtils'; import ManageLineageMenu from './manage/ManageLineageMenu'; import { useGetLineageTimeParams } from './utils/useGetLineageTimeParams'; import { EntityHealth } from '../entity/shared/containers/profile/header/EntityHealth'; const CLICK_DELAY_THRESHOLD = 1000; const DRAG_DISTANCE_THRESHOLD = 20; const PointerGroup = styled(Group)` cursor: pointer; `; const UnselectableText = styled.text` user-select: none; `; const MultilineTitleText = styled.p` margin-top: -2px; font-size: 14px; width: 125px; word-break: break-all; `; export default function LineageEntityNode({ node, isSelected, isHovered, onEntityClick, onEntityCenter, onHover, onDrag, onExpandClick, isCenterNode, nodesToRenderByUrn, setUpdatedLineages, }: { node: VizNode; isSelected: boolean; isHovered: boolean; isCenterNode: boolean; onEntityClick: (EntitySelectParams) => void; onEntityCenter: (EntitySelectParams) => void; onHover: (EntitySelectParams) => void; onDrag: (params: EntitySelectParams, event: React.MouseEvent) => void; onExpandClick: (data: EntityAndType) => void; nodesToRenderByUrn: Record; setUpdatedLineages: React.Dispatch>; }) { const { direction } = node; const { expandTitles, collapsedColumnsNodes, showColumns, refetchCenterNode } = useContext(LineageExplorerContext); const { startTimeMillis, endTimeMillis } = useGetLineageTimeParams(); const [hasExpanded, setHasExpanded] = useState(false); const [isExpanding, setIsExpanding] = useState(false); const [expandHover, setExpandHover] = useState(false); const [getAsyncEntityLineage, { data: asyncLineageData, loading }] = useGetEntityLineageLazyQuery(); const isHideSiblingMode = useIsSeparateSiblingsMode(); const areColumnsCollapsed = !!collapsedColumnsNodes[node?.data?.urn || 'noop']; function fetchEntityLineage() { if (node.data.urn) { if (isCenterNode) { refetchCenterNode(); } else { // update non-center node using onExpandClick in useEffect below getAsyncEntityLineage({ variables: { urn: node.data.urn, separateSiblings: isHideSiblingMode, showColumns, startTimeMillis, endTimeMillis, }, }); setTimeout(() => setHasExpanded(false), 0); } } } useEffect(() => { if (asyncLineageData && asyncLineageData.entity && !hasExpanded && !loading) { const entityAndType = { type: asyncLineageData.entity.type, entity: { ...asyncLineageData.entity }, } as EntityAndType; onExpandClick(entityAndType); setHasExpanded(true); } }, [asyncLineageData, onExpandClick, hasExpanded, loading]); const entityRegistry = useEntityRegistry(); const unexploredHiddenChildren = node?.data?.countercurrentChildrenUrns?.filter((urn) => !(urn in nodesToRenderByUrn))?.length || 0; // we need to track lastMouseDownCoordinates to differentiate between clicks and drags. It doesn't use useState because // it shouldn't trigger re-renders const lastMouseDownCoordinates = useMemo( () => ({ ts: 0, x: 0, y: 0, }), [], ); let platformDisplayText = node.data.platform?.properties?.displayName || capitalizeFirstLetterOnly(node.data.platform?.name); if (node.data.siblingPlatforms && !isHideSiblingMode) { platformDisplayText = node.data.siblingPlatforms .map((platform) => platform.properties?.displayName || capitalizeFirstLetterOnly(platform.name)) .join(' & '); } const nodeHeight = nodeHeightFromTitleLength( expandTitles ? node.data.expandedName || node.data.name : undefined, node.data.schemaMetadata?.fields || convertInputFieldsToSchemaFields(node.data.inputFields), showColumns, areColumnsCollapsed, ); const entityName = capitalizeFirstLetterOnly(node.data.subtype) || (node.data.type && entityRegistry.getEntityName(node.data.type)); // Health const { health } = node.data; const baseUrl = node.data.type && node.data.urn && entityRegistry.getEntityUrl(node.data.type, node.data.urn); const hasHealth = (health && baseUrl) || false; return ( {unexploredHiddenChildren && (isHovered || isSelected) ? ( {[...Array(unexploredHiddenChildren)].map((_, index) => { const link = { source: { x: 0, y: direction === Direction.Upstream ? 70 : -70, }, target: { x: (0.5 / (index + 1)) * 80 * (index % 2 === 0 ? 1 : -1), y: direction === Direction.Upstream ? 150 : -150, }, }; return ( ); })} ) : null} {node.data.unexploredChildren && (!isExpanding ? ( { setIsExpanding(true); if (node.data.urn && node.data.type) { // getAsyncEntity(node.data.urn, node.data.type); getAsyncEntityLineage({ variables: { urn: node.data.urn, separateSiblings: isHideSiblingMode, showColumns, startTimeMillis, endTimeMillis, }, }); } }} onMouseOver={() => { setExpandHover(true); }} onMouseOut={() => { setExpandHover(false); }} pointerEvents="bounding-box" > ) : ( ))} onEntityCenter({ urn: node.data.urn, type: node.data.type })} onClick={(event) => { if ( event.timeStamp < lastMouseDownCoordinates.ts + CLICK_DELAY_THRESHOLD && Math.sqrt( (event.clientX - lastMouseDownCoordinates.x) ** 2 + (event.clientY - lastMouseDownCoordinates.y) ** 2, ) < DRAG_DISTANCE_THRESHOLD ) { onEntityClick({ urn: node.data.urn, type: node.data.type }); } }} onMouseOver={() => { onHover({ urn: node.data.urn, type: node.data.type }); }} onMouseOut={() => { onHover(undefined); }} onMouseDown={(event) => { lastMouseDownCoordinates.ts = event.timeStamp; lastMouseDownCoordinates.x = event.clientX; lastMouseDownCoordinates.y = event.clientY; if (node.data.urn && node.data.type) { onDrag({ urn: node.data.urn, type: node.data.type }, event); } }} > {node.data.siblingPlatforms && !isHideSiblingMode && ( )} {(!node.data.siblingPlatforms || isHideSiblingMode) && node.data.icon && ( )} {!node.data.icon && (!node.data.siblingPlatforms || isHideSiblingMode) && node.data.type && ( )} e.stopPropagation()} > onEntityCenter({ urn: node.data.urn, type: node.data.type })} entityType={node.data.type} entityPlatform={node.data.platform?.name} canEditLineage={node.data.canEditLineage} /> {getShortenedTitle(platformDisplayText || '', width)} {' '} |{' '} {entityName} {expandTitles ? ( {node.data.expandedName || node.data.name} ) : ( {getShortenedTitle(node.data.name, width)} )} {hasHealth && ( )} {unexploredHiddenChildren && isHovered ? ( {unexploredHiddenChildren} hidden {direction === Direction.Upstream ? 'downstream' : 'upstream'}{' '} {unexploredHiddenChildren > 1 ? 'dependencies' : 'dependency'} ) : null} {showColumns && (node.data.schemaMetadata || node.data.inputFields) && ( )} ); }