2022-05-23 17:21:30 -04:00
|
|
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
2021-04-03 11:13:25 -07:00
|
|
|
import { Group } from '@vx/group';
|
2021-04-05 19:23:07 -07:00
|
|
|
import { LinkHorizontal } from '@vx/shape';
|
2021-04-03 11:13:25 -07:00
|
|
|
import styled from 'styled-components';
|
|
|
|
|
2021-04-17 00:46:02 +08:00
|
|
|
import { useEntityRegistry } from '../useEntityRegistry';
|
|
|
|
import { IconStyleType } from '../entity/Entity';
|
2022-05-23 17:21:30 -04:00
|
|
|
import { NodeData, Direction, VizNode, EntitySelectParams, EntityAndType } from './types';
|
2021-08-31 22:00:56 -07:00
|
|
|
import { ANTD_GRAY } from '../entity/shared/constants';
|
2022-01-27 10:33:12 -08:00
|
|
|
import { capitalizeFirstLetter } from '../shared/textUtil';
|
2022-01-07 17:29:15 -08:00
|
|
|
import { nodeHeightFromTitleLength } from './utils/nodeHeightFromTitleLength';
|
|
|
|
import { LineageExplorerContext } from './utils/LineageExplorerContext';
|
2022-05-23 17:21:30 -04:00
|
|
|
import useLazyGetEntityQuery from './utils/useLazyGetEntityQuery';
|
2021-04-03 11:13:25 -07:00
|
|
|
|
2021-10-22 15:46:46 -07:00
|
|
|
const CLICK_DELAY_THRESHOLD = 1000;
|
|
|
|
const DRAG_DISTANCE_THRESHOLD = 20;
|
|
|
|
|
2021-04-03 11:13:25 -07:00
|
|
|
function truncate(input, length) {
|
|
|
|
if (!input) return '';
|
|
|
|
if (input.length > length) {
|
|
|
|
return `${input.substring(0, length)}...`;
|
|
|
|
}
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
2021-10-22 15:46:46 -07:00
|
|
|
function getLastTokenOfTitle(title?: string): string {
|
|
|
|
if (!title) return '';
|
|
|
|
|
2021-08-05 18:17:09 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-04-09 01:24:33 +08:00
|
|
|
export const width = 212;
|
2021-04-05 12:15:05 -07:00
|
|
|
export const height = 80;
|
2021-08-31 22:00:56 -07:00
|
|
|
const iconWidth = 32;
|
|
|
|
const iconHeight = 32;
|
|
|
|
const iconX = -width / 2 + 22;
|
2021-04-09 01:24:33 +08:00
|
|
|
const iconY = -iconHeight / 2;
|
2021-04-03 11:13:25 -07:00
|
|
|
const centerX = -width / 2;
|
|
|
|
const centerY = -height / 2;
|
2021-04-09 01:24:33 +08:00
|
|
|
const textX = iconX + iconWidth + 8;
|
2021-04-03 11:13:25 -07:00
|
|
|
|
|
|
|
const PointerGroup = styled(Group)`
|
|
|
|
cursor: pointer;
|
|
|
|
`;
|
|
|
|
|
2021-10-22 15:46:46 -07:00
|
|
|
const UnselectableText = styled.text`
|
|
|
|
user-select: none;
|
|
|
|
`;
|
|
|
|
|
2022-01-07 17:29:15 -08:00
|
|
|
const MultilineTitleText = styled.p`
|
|
|
|
margin-top: -2px;
|
|
|
|
font-size: 14px;
|
|
|
|
width: 125px;
|
|
|
|
word-break: break-all;
|
|
|
|
`;
|
|
|
|
|
2021-04-03 11:13:25 -07:00
|
|
|
export default function LineageEntityNode({
|
|
|
|
node,
|
|
|
|
isSelected,
|
|
|
|
isHovered,
|
|
|
|
onEntityClick,
|
2021-04-23 00:18:39 -07:00
|
|
|
onEntityCenter,
|
2021-04-03 11:13:25 -07:00
|
|
|
onHover,
|
2021-10-22 15:46:46 -07:00
|
|
|
onDrag,
|
2021-04-03 11:13:25 -07:00
|
|
|
onExpandClick,
|
|
|
|
direction,
|
|
|
|
isCenterNode,
|
2021-04-05 19:23:07 -07:00
|
|
|
nodesToRenderByUrn,
|
2021-04-03 11:13:25 -07:00
|
|
|
}: {
|
|
|
|
node: { x: number; y: number; data: Omit<NodeData, 'children'> };
|
|
|
|
isSelected: boolean;
|
|
|
|
isHovered: boolean;
|
|
|
|
isCenterNode: boolean;
|
|
|
|
onEntityClick: (EntitySelectParams) => void;
|
2021-04-23 00:18:39 -07:00
|
|
|
onEntityCenter: (EntitySelectParams) => void;
|
2021-04-03 11:13:25 -07:00
|
|
|
onHover: (EntitySelectParams) => void;
|
2021-10-22 15:46:46 -07:00
|
|
|
onDrag: (params: EntitySelectParams, event: React.MouseEvent) => void;
|
2022-05-23 17:21:30 -04:00
|
|
|
onExpandClick: (data: EntityAndType) => void;
|
2021-04-03 11:13:25 -07:00
|
|
|
direction: Direction;
|
2021-10-22 15:46:46 -07:00
|
|
|
nodesToRenderByUrn: Record<string, VizNode>;
|
2021-04-03 11:13:25 -07:00
|
|
|
}) {
|
2022-01-07 17:29:15 -08:00
|
|
|
const { expandTitles } = useContext(LineageExplorerContext);
|
2021-10-29 11:20:02 -07:00
|
|
|
const [isExpanding, setIsExpanding] = useState(false);
|
|
|
|
const [expandHover, setExpandHover] = useState(false);
|
2022-05-23 17:21:30 -04:00
|
|
|
const { getAsyncEntity, asyncData } = useLazyGetEntityQuery();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (asyncData) {
|
|
|
|
onExpandClick(asyncData);
|
|
|
|
}
|
|
|
|
}, [asyncData, onExpandClick]);
|
2021-10-29 11:20:02 -07:00
|
|
|
|
2021-04-17 00:46:02 +08:00
|
|
|
const entityRegistry = useEntityRegistry();
|
2021-04-05 19:23:07 -07:00
|
|
|
const unexploredHiddenChildren =
|
|
|
|
node?.data?.countercurrentChildrenUrns?.filter((urn) => !(urn in nodesToRenderByUrn))?.length || 0;
|
|
|
|
|
2021-10-22 15:46:46 -07:00
|
|
|
// 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,
|
|
|
|
}),
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
2022-04-01 10:37:51 -07:00
|
|
|
const nodeHeight = nodeHeightFromTitleLength(expandTitles ? node.data.expandedName || node.data.name : undefined);
|
2022-01-07 17:29:15 -08:00
|
|
|
|
2021-04-03 11:13:25 -07:00
|
|
|
return (
|
|
|
|
<PointerGroup data-testid={`node-${node.data.urn}-${direction}`} top={node.x} left={node.y}>
|
2021-10-22 15:46:46 -07:00
|
|
|
{unexploredHiddenChildren && (isHovered || isSelected) ? (
|
2021-04-05 19:23:07 -07:00
|
|
|
<Group>
|
|
|
|
{[...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 (
|
|
|
|
<LinkHorizontal
|
2021-10-22 15:46:46 -07:00
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
|
|
key={`link-${index}-${direction}`}
|
2021-04-05 19:23:07 -07:00
|
|
|
data={link}
|
|
|
|
stroke={`url(#gradient-${direction})`}
|
|
|
|
strokeWidth="1"
|
|
|
|
fill="none"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</Group>
|
2021-04-09 01:24:33 +08:00
|
|
|
) : null}
|
2021-10-29 11:20:02 -07:00
|
|
|
{node.data.unexploredChildren &&
|
|
|
|
(!isExpanding ? (
|
|
|
|
<Group
|
|
|
|
onClick={() => {
|
|
|
|
setIsExpanding(true);
|
2022-05-23 17:21:30 -04:00
|
|
|
if (node.data.urn && node.data.type) {
|
|
|
|
getAsyncEntity(node.data.urn, node.data.type);
|
|
|
|
}
|
2021-10-29 11:20:02 -07:00
|
|
|
}}
|
|
|
|
onMouseOver={() => {
|
|
|
|
setExpandHover(true);
|
|
|
|
}}
|
|
|
|
onMouseOut={() => {
|
|
|
|
setExpandHover(false);
|
|
|
|
}}
|
|
|
|
pointerEvents="bounding-box"
|
|
|
|
>
|
|
|
|
<circle
|
|
|
|
fill="none"
|
2022-01-07 17:29:15 -08:00
|
|
|
cy={centerY + nodeHeight / 2}
|
2021-10-29 11:20:02 -07:00
|
|
|
cx={direction === Direction.Upstream ? centerX - 10 : centerX + width + 10}
|
|
|
|
r="20"
|
|
|
|
/>
|
|
|
|
<circle
|
|
|
|
fill="none"
|
2022-01-07 17:29:15 -08:00
|
|
|
cy={centerY + nodeHeight / 2}
|
2021-10-29 11:20:02 -07:00
|
|
|
cx={direction === Direction.Upstream ? centerX - 30 : centerX + width + 30}
|
|
|
|
r="30"
|
|
|
|
/>
|
|
|
|
<g
|
|
|
|
fill={expandHover ? ANTD_GRAY[5] : ANTD_GRAY[6]}
|
|
|
|
transform={`translate(${
|
|
|
|
direction === Direction.Upstream ? centerX - 52 : width / 2 + 10
|
|
|
|
} -21.5) scale(0.04 0.04)`}
|
|
|
|
>
|
|
|
|
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm192 472c0 4.4-3.6 8-8 8H544v152c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V544H328c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h152V328c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v152h152c4.4 0 8 3.6 8 8v48z" />
|
|
|
|
</g>
|
|
|
|
</Group>
|
|
|
|
) : (
|
2021-04-03 11:13:25 -07:00
|
|
|
<g
|
2021-08-31 22:00:56 -07:00
|
|
|
fill={ANTD_GRAY[6]}
|
2021-04-03 11:13:25 -07:00
|
|
|
transform={`translate(${
|
2021-08-31 22:00:56 -07:00
|
|
|
direction === Direction.Upstream ? centerX - 52 : width / 2 + 10
|
2021-04-03 11:13:25 -07:00
|
|
|
} -21.5) scale(0.04 0.04)`}
|
|
|
|
>
|
2021-10-29 11:20:02 -07:00
|
|
|
<path
|
|
|
|
className="lineageExpandLoading"
|
|
|
|
d="M512 1024c-69.1 0-136.2-13.5-199.3-40.2C251.7 958 197 921 150 874c-47-47-84-101.7-109.8-162.7C13.5 648.2 0 581.1 0 512c0-19.9 16.1-36 36-36s36 16.1 36 36c0 59.4 11.6 117 34.6 171.3 22.2 52.4 53.9 99.5 94.3 139.9 40.4 40.4 87.5 72.2 139.9 94.3C395 940.4 452.6 952 512 952c59.4 0 117-11.6 171.3-34.6 52.4-22.2 99.5-53.9 139.9-94.3 40.4-40.4 72.2-87.5 94.3-139.9C940.4 629 952 571.4 952 512c0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.2C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3s-13.5 136.2-40.2 199.3C958 772.3 921 827 874 874c-47 47-101.8 83.9-162.7 109.7-63.1 26.8-130.2 40.3-199.3 40.3z"
|
|
|
|
/>
|
2021-04-03 11:13:25 -07:00
|
|
|
</g>
|
2021-10-29 11:20:02 -07:00
|
|
|
))}
|
2021-04-03 11:13:25 -07:00
|
|
|
<Group
|
2021-04-23 00:18:39 -07:00
|
|
|
onDoubleClick={() => onEntityCenter({ urn: node.data.urn, type: node.data.type })}
|
2021-10-22 15:46:46 -07:00
|
|
|
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 });
|
|
|
|
}
|
2021-04-03 11:13:25 -07:00
|
|
|
}}
|
|
|
|
onMouseOver={() => {
|
2021-04-09 11:55:25 -07:00
|
|
|
onHover({ urn: node.data.urn, type: node.data.type });
|
2021-04-03 11:13:25 -07:00
|
|
|
}}
|
|
|
|
onMouseOut={() => {
|
|
|
|
onHover(undefined);
|
|
|
|
}}
|
2021-10-22 15:46:46 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}}
|
2021-04-03 11:13:25 -07:00
|
|
|
>
|
|
|
|
<rect
|
2022-01-07 17:29:15 -08:00
|
|
|
height={nodeHeight}
|
2021-04-03 11:13:25 -07:00
|
|
|
width={width}
|
|
|
|
y={centerY}
|
|
|
|
x={centerX}
|
|
|
|
fill="white"
|
2021-04-23 00:18:39 -07:00
|
|
|
stroke={
|
|
|
|
// eslint-disable-next-line no-nested-ternary
|
2021-08-31 22:00:56 -07:00
|
|
|
isSelected ? '#1890FF' : isHovered ? '#1890FF' : 'rgba(192, 190, 190, 0.25)'
|
2021-04-23 00:18:39 -07:00
|
|
|
}
|
|
|
|
strokeWidth={isCenterNode ? 2 : 1}
|
2021-04-03 11:13:25 -07:00
|
|
|
strokeOpacity={1}
|
2021-04-23 00:18:39 -07:00
|
|
|
rx={5}
|
|
|
|
// eslint-disable-next-line react/style-prop-object
|
2021-08-31 22:00:56 -07:00
|
|
|
style={{ filter: isSelected ? 'url(#shadow1-selected)' : 'url(#shadow1)' }}
|
2021-04-03 11:13:25 -07:00
|
|
|
/>
|
2021-04-09 01:24:33 +08:00
|
|
|
{node.data.icon ? (
|
|
|
|
<image href={node.data.icon} height={iconHeight} width={iconWidth} x={iconX} y={iconY} />
|
2021-04-17 00:46:02 +08:00
|
|
|
) : (
|
|
|
|
node.data.type && (
|
|
|
|
<svg
|
|
|
|
viewBox="64 64 896 896"
|
|
|
|
focusable="false"
|
|
|
|
x={iconX}
|
|
|
|
y={iconY}
|
|
|
|
height={iconHeight}
|
|
|
|
width={iconWidth}
|
|
|
|
fill="currentColor"
|
|
|
|
aria-hidden="true"
|
|
|
|
>
|
|
|
|
{entityRegistry.getIcon(node.data.type, 16, IconStyleType.SVG)}
|
|
|
|
</svg>
|
|
|
|
)
|
|
|
|
)}
|
2021-04-23 00:18:39 -07:00
|
|
|
<Group>
|
2021-10-22 15:46:46 -07:00
|
|
|
<UnselectableText
|
2021-04-23 00:18:39 -07:00
|
|
|
dy="-1em"
|
|
|
|
x={textX}
|
|
|
|
fontSize={8}
|
2021-08-31 22:00:56 -07:00
|
|
|
fontFamily="Manrope"
|
2021-04-23 00:18:39 -07:00
|
|
|
fontWeight="bold"
|
|
|
|
textAnchor="start"
|
2021-08-31 22:00:56 -07:00
|
|
|
fill="#8C8C8C"
|
2021-04-23 00:18:39 -07:00
|
|
|
>
|
2021-08-31 22:00:56 -07:00
|
|
|
<tspan>{truncate(capitalizeFirstLetter(node.data.platform), 16)}</tspan>
|
|
|
|
<tspan dx=".25em" dy="2px" fill="#dadada" fontSize={12} fontWeight="normal">
|
|
|
|
{' '}
|
|
|
|
|{' '}
|
|
|
|
</tspan>
|
|
|
|
<tspan dx=".25em" dy="-2px">
|
2022-03-10 15:23:47 -08:00
|
|
|
{capitalizeFirstLetter(
|
|
|
|
node.data.subtype || (node.data.type && entityRegistry.getEntityName(node.data.type)),
|
|
|
|
)}
|
2021-08-31 22:00:56 -07:00
|
|
|
</tspan>
|
2021-10-22 15:46:46 -07:00
|
|
|
</UnselectableText>
|
2022-01-07 17:29:15 -08:00
|
|
|
{expandTitles ? (
|
|
|
|
<foreignObject x={textX} width="125" height="200">
|
2022-04-01 10:37:51 -07:00
|
|
|
<MultilineTitleText>{node.data.expandedName || node.data.name}</MultilineTitleText>
|
2022-01-07 17:29:15 -08:00
|
|
|
</foreignObject>
|
|
|
|
) : (
|
|
|
|
<UnselectableText
|
|
|
|
dy="1em"
|
|
|
|
x={textX}
|
|
|
|
fontSize={14}
|
|
|
|
fontFamily="Manrope"
|
|
|
|
textAnchor="start"
|
|
|
|
fill={isCenterNode ? '#1890FF' : 'black'}
|
|
|
|
>
|
|
|
|
{truncate(getLastTokenOfTitle(node.data.name), 16)}
|
|
|
|
</UnselectableText>
|
|
|
|
)}
|
2021-04-23 00:18:39 -07:00
|
|
|
</Group>
|
2021-04-09 01:24:33 +08:00
|
|
|
{unexploredHiddenChildren && isHovered ? (
|
2021-10-22 15:46:46 -07:00
|
|
|
<UnselectableText
|
2021-04-09 01:24:33 +08:00
|
|
|
dy=".33em"
|
|
|
|
dx={textX}
|
|
|
|
fontSize={16}
|
|
|
|
fontFamily="Arial"
|
|
|
|
textAnchor="middle"
|
|
|
|
fill="black"
|
|
|
|
y={centerY - 20}
|
|
|
|
>
|
2021-04-05 19:23:07 -07:00
|
|
|
{unexploredHiddenChildren} hidden {direction === Direction.Upstream ? 'downstream' : 'upstream'}{' '}
|
|
|
|
{unexploredHiddenChildren > 1 ? 'dependencies' : 'dependency'}
|
2021-10-22 15:46:46 -07:00
|
|
|
</UnselectableText>
|
2021-04-09 01:24:33 +08:00
|
|
|
) : null}
|
2021-04-03 11:13:25 -07:00
|
|
|
</Group>
|
|
|
|
</PointerGroup>
|
|
|
|
);
|
|
|
|
}
|