import React, { useMemo } from 'react'; import { Group } from '@vx/group'; import { LinkHorizontal } from '@vx/shape'; import styled from 'styled-components'; import { useEntityRegistry } from '../useEntityRegistry'; import { IconStyleType } from '../entity/Entity'; import { NodeData, Direction, VizNode, EntitySelectParams } from './types'; import { ANTD_GRAY } from '../entity/shared/constants'; import { capitalizeFirstLetter } from '../shared/capitalizeFirstLetter'; const CLICK_DELAY_THRESHOLD = 1000; const DRAG_DISTANCE_THRESHOLD = 20; function truncate(input, length) { if (!input) return ''; if (input.length > length) { return `${input.substring(0, length)}...`; } return input; } function getLastTokenOfTitle(title?: string): string { if (!title) return ''; const lastToken = title?.split('.').slice(-1)[0]; // if the last token does not contain any content, the string should not be tokenized on `.` if (lastToken.replace(/\s/g, '').length === 0) { return title; } return lastToken; } export const width = 212; export const height = 80; const iconWidth = 32; const iconHeight = 32; const iconX = -width / 2 + 22; const iconY = -iconHeight / 2; const centerX = -width / 2; const centerY = -height / 2; const textX = iconX + iconWidth + 8; const PointerGroup = styled(Group)` cursor: pointer; `; const UnselectableText = styled.text` user-select: none; `; export default function LineageEntityNode({ node, isSelected, isHovered, onEntityClick, onEntityCenter, onHover, onDrag, onExpandClick, direction, isCenterNode, nodesToRenderByUrn, }: { node: { x: number; y: number; data: Omit }; isSelected: boolean; isHovered: boolean; isCenterNode: boolean; onEntityClick: (EntitySelectParams) => void; onEntityCenter: (EntitySelectParams) => void; onHover: (EntitySelectParams) => void; onDrag: (params: EntitySelectParams, event: React.MouseEvent) => void; onExpandClick: (LineageExpandParams) => void; direction: Direction; nodesToRenderByUrn: Record; }) { 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, }), [], ); 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 ? ( { onExpandClick({ urn: node.data.urn, type: node.data.type, direction }); }} > ) : null} 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.icon ? ( ) : ( node.data.type && ( ) )} {truncate(capitalizeFirstLetter(node.data.platform), 16)} {' '} |{' '} {capitalizeFirstLetter(node.data.subtype || node.data.type)} {truncate(getLastTokenOfTitle(node.data.name), 16)} {unexploredHiddenChildren && isHovered ? ( {unexploredHiddenChildren} hidden {direction === Direction.Upstream ? 'downstream' : 'upstream'}{' '} {unexploredHiddenChildren > 1 ? 'dependencies' : 'dependency'} ) : null} ); }