mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-12 01:44:42 -05:00
feat: floating edge
This commit is contained in:
64
src/components/investigations/floating-connection.tsx
Normal file
64
src/components/investigations/floating-connection.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { getBezierPath, Position } from '@xyflow/react';
|
||||
import { getEdgeParams } from '@/src/lib/utils';
|
||||
|
||||
const FloatingConnectionLine = ({
|
||||
toX,
|
||||
toY,
|
||||
fromPosition,
|
||||
toPosition,
|
||||
fromNode,
|
||||
}: {
|
||||
toX: number,
|
||||
toY: number,
|
||||
fromPosition: Position,
|
||||
toPosition: Position,
|
||||
fromNode: number,
|
||||
}): any => {
|
||||
if (!fromNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetNode = {
|
||||
id: 'connection-target',
|
||||
measured: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
internals: {
|
||||
positionAbsolute: { x: toX, y: toY },
|
||||
},
|
||||
};
|
||||
|
||||
const { sx, sy } = getEdgeParams(fromNode, targetNode);
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: fromPosition,
|
||||
targetPosition: toPosition,
|
||||
targetX: toX,
|
||||
targetY: toY,
|
||||
});
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#222"
|
||||
strokeWidth={1.5}
|
||||
className="animated"
|
||||
d={edgePath}
|
||||
/>
|
||||
<circle
|
||||
cx={toX}
|
||||
cy={toY}
|
||||
fill="#fff"
|
||||
r={3}
|
||||
stroke="#222"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default FloatingConnectionLine;
|
||||
60
src/components/investigations/floating-edge.tsx
Normal file
60
src/components/investigations/floating-edge.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { getEdgeParams } from '@/src/lib/utils';
|
||||
import {
|
||||
EdgeLabelRenderer,
|
||||
getBezierPath,
|
||||
useInternalNode
|
||||
} from '@xyflow/react';
|
||||
import { useInvestigationContext } from '../contexts/investigation-provider';
|
||||
import { Badge } from '@radix-ui/themes';
|
||||
function FloatingEdge(props: any) {
|
||||
const { id, source, target, markerEnd, label, confidence_level, sourceX, sourceY, targetX, targetY, style } = props
|
||||
const sourceNode = useInternalNode(source);
|
||||
const targetNode = useInternalNode(target);
|
||||
const { settings } = useInvestigationContext()
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
);
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
id={id}
|
||||
className="react-flow__edge-path"
|
||||
d={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={style}
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
{settings.showEdgeLabel &&
|
||||
<Badge size={"1"} color={label === "relation" ? 'orange' : "blue"} style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
opacity: style?.opacity || 1
|
||||
}}
|
||||
className="nodrag nopan text-xs px-1">{label} {confidence_level &&
|
||||
<>{confidence_level}%</>}
|
||||
</Badge>}
|
||||
</EdgeLabelRenderer></>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default FloatingEdge;
|
||||
|
||||
|
||||
@@ -33,10 +33,12 @@ import { IconButton, Tooltip, Spinner, Card, Flex, SegmentedControl } from '@rad
|
||||
import { isNode, isEdge, getIncomers, getOutgoers } from "@xyflow/react";
|
||||
import { EdgeBase } from '@xyflow/system';
|
||||
import { useInvestigationContext } from '../contexts/investigation-provider';
|
||||
import FloatingEdge from './floating-edge';
|
||||
import FloatingConnectionLine from './floating-connection';
|
||||
|
||||
const nodeTypes = { individual: IndividualNode, phone: PhoneNode, ip: IpNode, email: EmailNode, social: SocialNode, address: AddressNode };
|
||||
const edgeTypes = {
|
||||
'custom': CustomEdge,
|
||||
'custom': FloatingEdge,
|
||||
};
|
||||
const getLayoutedElements = (nodes: any[], edges: any[], options: { direction: any; }) => {
|
||||
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
@@ -271,6 +273,7 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
minZoom={0.1}
|
||||
connectionLineComponent={FloatingConnectionLine as any}
|
||||
// edgesUpdatable={!isLocked}
|
||||
// edgesFocusable={!isLocked}
|
||||
// nodesDraggable={!isLocked}
|
||||
|
||||
@@ -50,7 +50,7 @@ function EmailNode({ data }: any) {
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className="w-16 !bg-teal-500 hidden"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -65,15 +65,16 @@ function Custom(props: any) {
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className={cn("w-16 !bg-teal-500", showContent ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="w-16 !bg-teal-500"
|
||||
className={cn("w-16 !bg-teal-500", showContent ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
</Box>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
@@ -51,7 +51,7 @@ function Custom({ data }: any) {
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className="w-16 !bg-teal-500 hidden"
|
||||
/>
|
||||
</Box>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
@@ -51,7 +51,7 @@ function Custom({ data }: any) {
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className="w-16 !bg-teal-500 hidden"
|
||||
/>
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -50,7 +50,7 @@ function AddressNode({ data }: any) {
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className="w-16 !bg-teal-500 hidden"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ function Custom({ data }: any) {
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
className="w-16 !bg-teal-500 hidden"
|
||||
/>
|
||||
</Box>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
101
src/lib/utils.ts
101
src/lib/utils.ts
@@ -6,3 +6,104 @@ export function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
export const zoomSelector = (s: { transform: number[]; }) => s.transform[2] >= 0.6;
|
||||
|
||||
import { Position, MarkerType } from '@xyflow/react';
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(intersectionNode: { measured: { width: any; height: any; }; internals: { positionAbsolute: any; }; }, targetNode: { internals: { positionAbsolute: any; }; measured: { width: number; height: number; }; }) {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } =
|
||||
intersectionNode.measured;
|
||||
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
|
||||
const targetPosition = targetNode.internals.positionAbsolute;
|
||||
|
||||
const w = intersectionNodeWidth / 2;
|
||||
const h = intersectionNodeHeight / 2;
|
||||
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
const x1 = targetPosition.x + targetNode.measured.width / 2;
|
||||
const y1 = targetPosition.y + targetNode.measured.height / 2;
|
||||
|
||||
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
|
||||
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
|
||||
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
|
||||
const xx3 = a * xx1;
|
||||
const yy3 = a * yy1;
|
||||
const x = w * (xx3 + yy3) + x2;
|
||||
const y = h * (-xx3 + yy3) + y2;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: { internals: { positionAbsolute: any; }; }, intersectionPoint: { x: any; y: any; }) {
|
||||
const n = { ...node.internals.positionAbsolute, ...node };
|
||||
const nx = Math.round(n.x);
|
||||
const ny = Math.round(n.y);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
const py = Math.round(intersectionPoint.y);
|
||||
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.measured.width - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + n.measured.height - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
return Position.Top;
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source: any, target: any) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
|
||||
const targetPos = getEdgePosition(target, targetIntersectionPoint);
|
||||
|
||||
return {
|
||||
sx: sourceIntersectionPoint.x,
|
||||
sy: sourceIntersectionPoint.y,
|
||||
tx: targetIntersectionPoint.x,
|
||||
ty: targetIntersectionPoint.y,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
};
|
||||
}
|
||||
|
||||
export function initialElements() {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
|
||||
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const degrees = i * (360 / 8);
|
||||
const radians = degrees * (Math.PI / 180);
|
||||
const x = 250 * Math.cos(radians) + center.x;
|
||||
const y = 250 * Math.sin(radians) + center.y;
|
||||
|
||||
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
|
||||
|
||||
edges.push({
|
||||
id: `edge-${i}`,
|
||||
target: 'target',
|
||||
source: `${i}`,
|
||||
type: 'floating',
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user