@cyvest/cyvest-vis 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -10
- package/dist/index.d.mts +156 -6
- package/dist/index.d.ts +156 -6
- package/dist/index.js +1445 -650
- package/dist/index.mjs +1457 -665
- package/package.json +2 -2
- package/src/components/CyvestGraph.tsx +115 -46
- package/src/components/FloatingEdge.tsx +41 -11
- package/src/components/Icons.tsx +730 -0
- package/src/components/InvestigationGraph.tsx +107 -36
- package/src/components/InvestigationNode.tsx +129 -112
- package/src/components/ObservableNode.tsx +116 -135
- package/src/components/ObservablesGraph.tsx +241 -111
- package/src/hooks/useForceLayout.ts +136 -62
- package/src/index.ts +25 -2
- package/src/types.ts +9 -11
- package/src/utils/observables.ts +9 -97
- package/tests/observables.test.ts +10 -17
|
@@ -1,169 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom node component for the Observables Graph.
|
|
3
|
-
*
|
|
3
|
+
* Professional design with circular nodes and SVG icons.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { memo } from "react";
|
|
6
|
+
import React, { memo, useMemo } from "react";
|
|
7
7
|
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
|
8
|
-
import type { ObservableNodeData
|
|
8
|
+
import type { ObservableNodeData } from "../types";
|
|
9
9
|
import { getLevelColor, getLevelBackgroundColor } from "../utils/observables";
|
|
10
|
+
import { getObservableIcon, CrosshairIcon } from "./Icons";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Node size constants
|
|
13
|
+
* Node size constants
|
|
13
14
|
*/
|
|
14
|
-
const NODE_SIZE =
|
|
15
|
-
const
|
|
15
|
+
const NODE_SIZE = 40;
|
|
16
|
+
const ROOT_NODE_WIDTH = 56;
|
|
17
|
+
const ROOT_NODE_HEIGHT = 40;
|
|
18
|
+
const ICON_SIZE = 18;
|
|
19
|
+
const ROOT_ICON_SIZE = 20;
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
*
|
|
22
|
+
* CSS styles for the node
|
|
19
23
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const nodeStyles = {
|
|
25
|
+
container: {
|
|
26
|
+
display: "flex",
|
|
27
|
+
flexDirection: "column" as const,
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
cursor: "grab",
|
|
30
|
+
transition: "transform 0.1s ease-out",
|
|
31
|
+
},
|
|
32
|
+
shapeWrapper: {
|
|
33
|
+
position: "relative" as const,
|
|
34
|
+
display: "flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
},
|
|
38
|
+
label: {
|
|
39
|
+
marginTop: 4,
|
|
40
|
+
fontSize: 10,
|
|
41
|
+
fontWeight: 500,
|
|
42
|
+
maxWidth: 80,
|
|
43
|
+
textAlign: "center" as const,
|
|
44
|
+
overflow: "hidden",
|
|
45
|
+
textOverflow: "ellipsis",
|
|
46
|
+
whiteSpace: "nowrap" as const,
|
|
47
|
+
fontFamily:
|
|
48
|
+
"'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
49
|
+
letterSpacing: "-0.01em",
|
|
50
|
+
lineHeight: 1.2,
|
|
51
|
+
},
|
|
52
|
+
handle: {
|
|
53
|
+
position: "absolute" as const,
|
|
54
|
+
top: "50%",
|
|
55
|
+
left: "50%",
|
|
56
|
+
transform: "translate(-50%, -50%)",
|
|
57
|
+
width: 1,
|
|
58
|
+
height: 1,
|
|
59
|
+
background: "transparent",
|
|
60
|
+
border: "none",
|
|
61
|
+
opacity: 0,
|
|
62
|
+
pointerEvents: "none" as const,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Observable node component with professional circular design.
|
|
68
|
+
*/
|
|
69
|
+
function ObservableNodeComponent({ data, selected }: NodeProps) {
|
|
24
70
|
const nodeData = data as unknown as ObservableNodeData;
|
|
25
|
-
const {
|
|
26
|
-
|
|
27
|
-
emoji,
|
|
28
|
-
shape,
|
|
29
|
-
level,
|
|
30
|
-
isRoot,
|
|
31
|
-
whitelisted,
|
|
32
|
-
fullValue,
|
|
33
|
-
} = nodeData;
|
|
71
|
+
const { label, level, isRoot, whitelisted, fullValue, observableType } =
|
|
72
|
+
nodeData;
|
|
34
73
|
|
|
35
|
-
const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
|
|
36
74
|
const borderColor = getLevelColor(level);
|
|
37
75
|
const backgroundColor = getLevelBackgroundColor(level);
|
|
38
76
|
|
|
39
|
-
// Get
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
// Get the appropriate icon component
|
|
78
|
+
const IconComponent = useMemo(() => {
|
|
79
|
+
if (isRoot) return CrosshairIcon;
|
|
80
|
+
return getObservableIcon(observableType);
|
|
81
|
+
}, [isRoot, observableType]);
|
|
82
|
+
|
|
83
|
+
// Memoize styles to prevent recalculation
|
|
84
|
+
const shapeStyle = useMemo(() => {
|
|
85
|
+
if (isRoot) {
|
|
86
|
+
// Root node is a rounded rectangle (pill shape)
|
|
87
|
+
return {
|
|
88
|
+
width: ROOT_NODE_WIDTH,
|
|
89
|
+
height: ROOT_NODE_HEIGHT,
|
|
90
|
+
borderRadius: ROOT_NODE_HEIGHT / 2,
|
|
91
|
+
display: "flex",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
backgroundColor,
|
|
95
|
+
border: `2.5px solid ${borderColor}`,
|
|
96
|
+
boxShadow: selected
|
|
97
|
+
? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)`
|
|
98
|
+
: "0 2px 8px rgba(0,0,0,0.08)",
|
|
99
|
+
opacity: whitelisted ? 0.5 : 1,
|
|
100
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// All other nodes are circles
|
|
105
|
+
return {
|
|
106
|
+
width: NODE_SIZE,
|
|
107
|
+
height: NODE_SIZE,
|
|
108
|
+
borderRadius: "50%",
|
|
44
109
|
display: "flex",
|
|
45
110
|
alignItems: "center",
|
|
46
111
|
justifyContent: "center",
|
|
47
112
|
backgroundColor,
|
|
48
|
-
border:
|
|
113
|
+
border: `2px solid ${borderColor}`,
|
|
114
|
+
boxShadow: selected
|
|
115
|
+
? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)`
|
|
116
|
+
: "0 2px 6px rgba(0,0,0,0.08)",
|
|
49
117
|
opacity: whitelisted ? 0.5 : 1,
|
|
50
|
-
|
|
118
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out",
|
|
51
119
|
};
|
|
120
|
+
}, [isRoot, backgroundColor, borderColor, selected, whitelisted]);
|
|
52
121
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
...baseStyle,
|
|
62
|
-
borderRadius: 0,
|
|
63
|
-
border: "none",
|
|
64
|
-
background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
|
|
65
|
-
clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
|
|
66
|
-
position: "relative",
|
|
67
|
-
};
|
|
68
|
-
case "rectangle":
|
|
69
|
-
default:
|
|
70
|
-
return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// For triangle, we need a different approach
|
|
75
|
-
const isTriangle = shape === "triangle";
|
|
122
|
+
const labelStyle = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
...nodeStyles.label,
|
|
125
|
+
color: whitelisted ? "#9ca3af" : "#374151",
|
|
126
|
+
}),
|
|
127
|
+
[whitelisted]
|
|
128
|
+
);
|
|
76
129
|
|
|
77
130
|
return (
|
|
78
|
-
<div
|
|
79
|
-
className="observable-node"
|
|
80
|
-
style={{
|
|
81
|
-
display: "flex",
|
|
82
|
-
flexDirection: "column",
|
|
83
|
-
alignItems: "center",
|
|
84
|
-
cursor: "pointer",
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
131
|
+
<div className="observable-node" style={nodeStyles.container}>
|
|
87
132
|
{/* Shape container */}
|
|
88
|
-
<div style={
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
stroke={borderColor}
|
|
96
|
-
strokeWidth={selected ? 6 : 4}
|
|
97
|
-
opacity={whitelisted ? 0.5 : 1}
|
|
98
|
-
/>
|
|
99
|
-
<text
|
|
100
|
-
x="50"
|
|
101
|
-
y="65"
|
|
102
|
-
textAnchor="middle"
|
|
103
|
-
fontSize="32"
|
|
104
|
-
dominantBaseline="middle"
|
|
105
|
-
>
|
|
106
|
-
{emoji}
|
|
107
|
-
</text>
|
|
108
|
-
</svg>
|
|
109
|
-
) : (
|
|
110
|
-
// Other shapes using CSS
|
|
111
|
-
<div style={getShapeStyle()}>
|
|
112
|
-
<span style={{ userSelect: "none" }}>{emoji}</span>
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
133
|
+
<div style={nodeStyles.shapeWrapper}>
|
|
134
|
+
<div style={shapeStyle}>
|
|
135
|
+
<IconComponent
|
|
136
|
+
size={isRoot ? ROOT_ICON_SIZE : ICON_SIZE}
|
|
137
|
+
color={borderColor}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
115
140
|
|
|
116
|
-
{/*
|
|
117
|
-
<Handle
|
|
118
|
-
|
|
119
|
-
position={Position.Right}
|
|
120
|
-
id="source"
|
|
121
|
-
style={{
|
|
122
|
-
position: "absolute",
|
|
123
|
-
top: "50%",
|
|
124
|
-
left: "50%",
|
|
125
|
-
transform: "translate(-50%, -50%)",
|
|
126
|
-
width: 1,
|
|
127
|
-
height: 1,
|
|
128
|
-
background: "transparent",
|
|
129
|
-
border: "none",
|
|
130
|
-
opacity: 0,
|
|
131
|
-
}}
|
|
132
|
-
/>
|
|
133
|
-
{/* Center handle for target connections */}
|
|
134
|
-
<Handle
|
|
135
|
-
type="target"
|
|
136
|
-
position={Position.Left}
|
|
137
|
-
id="target"
|
|
138
|
-
style={{
|
|
139
|
-
position: "absolute",
|
|
140
|
-
top: "50%",
|
|
141
|
-
left: "50%",
|
|
142
|
-
transform: "translate(-50%, -50%)",
|
|
143
|
-
width: 1,
|
|
144
|
-
height: 1,
|
|
145
|
-
background: "transparent",
|
|
146
|
-
border: "none",
|
|
147
|
-
opacity: 0,
|
|
148
|
-
}}
|
|
149
|
-
/>
|
|
141
|
+
{/* Hidden handles for edge connections - centered */}
|
|
142
|
+
<Handle type="source" position={Position.Right} style={nodeStyles.handle} />
|
|
143
|
+
<Handle type="target" position={Position.Left} style={nodeStyles.handle} />
|
|
150
144
|
</div>
|
|
151
145
|
|
|
152
146
|
{/* Label below the shape */}
|
|
153
|
-
<div
|
|
154
|
-
style={{
|
|
155
|
-
marginTop: 2,
|
|
156
|
-
fontSize: 9,
|
|
157
|
-
maxWidth: 70,
|
|
158
|
-
textAlign: "center",
|
|
159
|
-
overflow: "hidden",
|
|
160
|
-
textOverflow: "ellipsis",
|
|
161
|
-
whiteSpace: "nowrap",
|
|
162
|
-
color: "#374151",
|
|
163
|
-
fontFamily: "system-ui, sans-serif",
|
|
164
|
-
}}
|
|
165
|
-
title={fullValue}
|
|
166
|
-
>
|
|
147
|
+
<div style={labelStyle} title={fullValue}>
|
|
167
148
|
{label}
|
|
168
149
|
</div>
|
|
169
150
|
</div>
|