@cyvest/cyvest-vis 3.2.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 +1485 -700
- package/dist/index.mjs +1494 -712
- package/package.json +9 -9
- 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 +120 -42
- package/src/components/InvestigationNode.tsx +129 -112
- package/src/components/ObservableNode.tsx +116 -135
- package/src/components/ObservablesGraph.tsx +258 -123
- package/src/hooks/useForceLayout.ts +136 -62
- package/src/index.ts +25 -2
- package/src/types.ts +9 -11
- package/src/utils/observables.ts +28 -115
- package/tests/observables.test.ts +13 -21
- package/vitest.config.ts +14 -0
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/components/CyvestGraph.tsx
|
|
2
|
-
import { useState as useState2, useCallback as useCallback4 } from "react";
|
|
2
|
+
import { useState as useState2, useCallback as useCallback4, useMemo as useMemo8 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/components/ObservablesGraph.tsx
|
|
5
|
-
import React3, { useMemo as
|
|
5
|
+
import React3, { useMemo as useMemo4, useCallback as useCallback2, useState, useRef as useRef2 } from "react";
|
|
6
6
|
import {
|
|
7
7
|
ReactFlow,
|
|
8
8
|
ReactFlowProvider,
|
|
@@ -11,86 +11,28 @@ import {
|
|
|
11
11
|
MiniMap,
|
|
12
12
|
useNodesState,
|
|
13
13
|
useEdgesState,
|
|
14
|
-
ConnectionMode
|
|
14
|
+
ConnectionMode,
|
|
15
|
+
BackgroundVariant,
|
|
16
|
+
Panel
|
|
15
17
|
} from "@xyflow/react";
|
|
16
18
|
import "@xyflow/react/dist/style.css";
|
|
17
|
-
import { getObservableGraph
|
|
19
|
+
import { getObservableGraph } from "@cyvest/cyvest-js";
|
|
18
20
|
|
|
19
21
|
// src/types.ts
|
|
20
22
|
var DEFAULT_FORCE_CONFIG = {
|
|
21
23
|
chargeStrength: -200,
|
|
22
24
|
linkDistance: 80,
|
|
23
25
|
centerStrength: 0.05,
|
|
24
|
-
collisionRadius:
|
|
26
|
+
collisionRadius: 45,
|
|
25
27
|
iterations: 300
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
// src/components/ObservableNode.tsx
|
|
29
|
-
import { memo } from "react";
|
|
31
|
+
import { memo, useMemo } from "react";
|
|
30
32
|
import { Handle, Position } from "@xyflow/react";
|
|
31
33
|
|
|
32
34
|
// src/utils/observables.ts
|
|
33
|
-
|
|
34
|
-
// Network
|
|
35
|
-
"ipv4-addr": "\u{1F310}",
|
|
36
|
-
"ipv6-addr": "\u{1F310}",
|
|
37
|
-
"domain-name": "\u{1F3E0}",
|
|
38
|
-
url: "\u{1F517}",
|
|
39
|
-
"autonomous-system": "\u{1F30D}",
|
|
40
|
-
"mac-addr": "\u{1F4F6}",
|
|
41
|
-
// Email
|
|
42
|
-
"email-addr": "\u{1F4E7}",
|
|
43
|
-
"email-message": "\u2709\uFE0F",
|
|
44
|
-
// File
|
|
45
|
-
file: "\u{1F4C4}",
|
|
46
|
-
"file-hash": "\u{1F510}",
|
|
47
|
-
"file:hash:md5": "\u{1F510}",
|
|
48
|
-
"file:hash:sha1": "\u{1F510}",
|
|
49
|
-
"file:hash:sha256": "\u{1F510}",
|
|
50
|
-
// User/Identity
|
|
51
|
-
user: "\u{1F464}",
|
|
52
|
-
"user-account": "\u{1F464}",
|
|
53
|
-
identity: "\u{1FAAA}",
|
|
54
|
-
// Process/System
|
|
55
|
-
process: "\u2699\uFE0F",
|
|
56
|
-
software: "\u{1F4BF}",
|
|
57
|
-
"windows-registry-key": "\u{1F4DD}",
|
|
58
|
-
// Threat Intelligence
|
|
59
|
-
"threat-actor": "\u{1F479}",
|
|
60
|
-
malware: "\u{1F9A0}",
|
|
61
|
-
"attack-pattern": "\u2694\uFE0F",
|
|
62
|
-
campaign: "\u{1F3AF}",
|
|
63
|
-
indicator: "\u{1F6A8}",
|
|
64
|
-
// Artifacts
|
|
65
|
-
artifact: "\u{1F9EA}",
|
|
66
|
-
certificate: "\u{1F4DC}",
|
|
67
|
-
"x509-certificate": "\u{1F4DC}",
|
|
68
|
-
// Default
|
|
69
|
-
unknown: "\u2753"
|
|
70
|
-
};
|
|
71
|
-
function getObservableEmoji(observableType) {
|
|
72
|
-
const normalized = observableType.toLowerCase().trim();
|
|
73
|
-
return OBSERVABLE_EMOJI_MAP[normalized] ?? OBSERVABLE_EMOJI_MAP.unknown;
|
|
74
|
-
}
|
|
75
|
-
var OBSERVABLE_SHAPE_MAP = {
|
|
76
|
-
// Domains get squares
|
|
77
|
-
"domain-name": "square",
|
|
78
|
-
// URLs get circles
|
|
79
|
-
url: "circle",
|
|
80
|
-
// IPs get triangles
|
|
81
|
-
"ipv4-addr": "triangle",
|
|
82
|
-
"ipv6-addr": "triangle",
|
|
83
|
-
// Root/files get rectangles (default for root)
|
|
84
|
-
file: "rectangle",
|
|
85
|
-
"email-message": "rectangle"
|
|
86
|
-
};
|
|
87
|
-
function getObservableShape(observableType, isRoot) {
|
|
88
|
-
if (isRoot) {
|
|
89
|
-
return "rectangle";
|
|
90
|
-
}
|
|
91
|
-
const normalized = observableType.toLowerCase().trim();
|
|
92
|
-
return OBSERVABLE_SHAPE_MAP[normalized] ?? "circle";
|
|
93
|
-
}
|
|
35
|
+
import { getColorForLevel } from "@cyvest/cyvest-js";
|
|
94
36
|
function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
|
|
95
37
|
if (value.length <= maxLength) {
|
|
96
38
|
return value;
|
|
@@ -102,213 +44,776 @@ function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
|
|
|
102
44
|
return `${value.slice(0, maxLength - 1)}\u2026`;
|
|
103
45
|
}
|
|
104
46
|
function getLevelColor(level) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
MALICIOUS: "#ef4444"
|
|
119
|
-
// red-500
|
|
120
|
-
};
|
|
121
|
-
return colors[level] ?? colors.NONE;
|
|
47
|
+
return getColorForLevel(level);
|
|
48
|
+
}
|
|
49
|
+
function lightenHexColor(hex, amount) {
|
|
50
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
51
|
+
if (normalized.length !== 6) {
|
|
52
|
+
return hex;
|
|
53
|
+
}
|
|
54
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
55
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
56
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
57
|
+
const mix = (channel) => Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
|
|
58
|
+
const toHex = (channel) => channel.toString(16).padStart(2, "0");
|
|
59
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
122
60
|
}
|
|
123
61
|
function getLevelBackgroundColor(level) {
|
|
124
|
-
|
|
125
|
-
NONE: "#f3f4f6",
|
|
126
|
-
// gray-100
|
|
127
|
-
TRUSTED: "#dcfce7",
|
|
128
|
-
// green-100
|
|
129
|
-
INFO: "#dbeafe",
|
|
130
|
-
// blue-100
|
|
131
|
-
SAFE: "#dcfce7",
|
|
132
|
-
// green-100
|
|
133
|
-
NOTABLE: "#fef9c3",
|
|
134
|
-
// yellow-100
|
|
135
|
-
SUSPICIOUS: "#ffedd5",
|
|
136
|
-
// orange-100
|
|
137
|
-
MALICIOUS: "#fee2e2"
|
|
138
|
-
// red-100
|
|
139
|
-
};
|
|
140
|
-
return colors[level] ?? colors.NONE;
|
|
62
|
+
return lightenHexColor(getLevelColor(level), 0.88);
|
|
141
63
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
64
|
+
|
|
65
|
+
// src/components/Icons.tsx
|
|
66
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
67
|
+
var defaultSize = 16;
|
|
68
|
+
var defaultColor = "currentColor";
|
|
69
|
+
var GlobeIcon = ({
|
|
70
|
+
size = defaultSize,
|
|
71
|
+
color = defaultColor,
|
|
72
|
+
className
|
|
73
|
+
}) => /* @__PURE__ */ jsxs(
|
|
74
|
+
"svg",
|
|
75
|
+
{
|
|
76
|
+
width: size,
|
|
77
|
+
height: size,
|
|
78
|
+
viewBox: "0 0 24 24",
|
|
79
|
+
fill: "none",
|
|
80
|
+
stroke: color,
|
|
81
|
+
strokeWidth: "2",
|
|
82
|
+
strokeLinecap: "round",
|
|
83
|
+
strokeLinejoin: "round",
|
|
84
|
+
className,
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
87
|
+
/* @__PURE__ */ jsx("path", { d: "M2 12h20" }),
|
|
88
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" })
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
var DomainIcon = ({
|
|
93
|
+
size = defaultSize,
|
|
94
|
+
color = defaultColor,
|
|
95
|
+
className
|
|
96
|
+
}) => /* @__PURE__ */ jsxs(
|
|
97
|
+
"svg",
|
|
98
|
+
{
|
|
99
|
+
width: size,
|
|
100
|
+
height: size,
|
|
101
|
+
viewBox: "0 0 24 24",
|
|
102
|
+
fill: "none",
|
|
103
|
+
stroke: color,
|
|
104
|
+
strokeWidth: "2",
|
|
105
|
+
strokeLinecap: "round",
|
|
106
|
+
strokeLinejoin: "round",
|
|
107
|
+
className,
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsx("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
|
|
110
|
+
/* @__PURE__ */ jsx("polyline", { points: "9,22 9,12 15,12 15,22" })
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
var LinkIcon = ({
|
|
115
|
+
size = defaultSize,
|
|
116
|
+
color = defaultColor,
|
|
117
|
+
className
|
|
118
|
+
}) => /* @__PURE__ */ jsxs(
|
|
119
|
+
"svg",
|
|
120
|
+
{
|
|
121
|
+
width: size,
|
|
122
|
+
height: size,
|
|
123
|
+
viewBox: "0 0 24 24",
|
|
124
|
+
fill: "none",
|
|
125
|
+
stroke: color,
|
|
126
|
+
strokeWidth: "2",
|
|
127
|
+
strokeLinecap: "round",
|
|
128
|
+
strokeLinejoin: "round",
|
|
129
|
+
className,
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
132
|
+
/* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
var MailIcon = ({
|
|
137
|
+
size = defaultSize,
|
|
138
|
+
color = defaultColor,
|
|
139
|
+
className
|
|
140
|
+
}) => /* @__PURE__ */ jsxs(
|
|
141
|
+
"svg",
|
|
142
|
+
{
|
|
143
|
+
width: size,
|
|
144
|
+
height: size,
|
|
145
|
+
viewBox: "0 0 24 24",
|
|
146
|
+
fill: "none",
|
|
147
|
+
stroke: color,
|
|
148
|
+
strokeWidth: "2",
|
|
149
|
+
strokeLinecap: "round",
|
|
150
|
+
strokeLinejoin: "round",
|
|
151
|
+
className,
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
|
|
154
|
+
/* @__PURE__ */ jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
var EnvelopeIcon = ({
|
|
159
|
+
size = defaultSize,
|
|
160
|
+
color = defaultColor,
|
|
161
|
+
className
|
|
162
|
+
}) => /* @__PURE__ */ jsxs(
|
|
163
|
+
"svg",
|
|
164
|
+
{
|
|
165
|
+
width: size,
|
|
166
|
+
height: size,
|
|
167
|
+
viewBox: "0 0 24 24",
|
|
168
|
+
fill: "none",
|
|
169
|
+
stroke: color,
|
|
170
|
+
strokeWidth: "2",
|
|
171
|
+
strokeLinecap: "round",
|
|
172
|
+
strokeLinejoin: "round",
|
|
173
|
+
className,
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ jsx("path", { d: "M22 12h-6l-2 3h-4l-2-3H2" }),
|
|
176
|
+
/* @__PURE__ */ jsx("path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" })
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
var FileIcon = ({
|
|
181
|
+
size = defaultSize,
|
|
182
|
+
color = defaultColor,
|
|
183
|
+
className
|
|
184
|
+
}) => /* @__PURE__ */ jsxs(
|
|
185
|
+
"svg",
|
|
186
|
+
{
|
|
187
|
+
width: size,
|
|
188
|
+
height: size,
|
|
189
|
+
viewBox: "0 0 24 24",
|
|
190
|
+
fill: "none",
|
|
191
|
+
stroke: color,
|
|
192
|
+
strokeWidth: "2",
|
|
193
|
+
strokeLinecap: "round",
|
|
194
|
+
strokeLinejoin: "round",
|
|
195
|
+
className,
|
|
196
|
+
children: [
|
|
197
|
+
/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
|
|
198
|
+
/* @__PURE__ */ jsx("polyline", { points: "14,2 14,8 20,8" })
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
var HashIcon = ({
|
|
203
|
+
size = defaultSize,
|
|
204
|
+
color = defaultColor,
|
|
205
|
+
className
|
|
206
|
+
}) => /* @__PURE__ */ jsxs(
|
|
207
|
+
"svg",
|
|
208
|
+
{
|
|
209
|
+
width: size,
|
|
210
|
+
height: size,
|
|
211
|
+
viewBox: "0 0 24 24",
|
|
212
|
+
fill: "none",
|
|
213
|
+
stroke: color,
|
|
214
|
+
strokeWidth: "2",
|
|
215
|
+
strokeLinecap: "round",
|
|
216
|
+
strokeLinejoin: "round",
|
|
217
|
+
className,
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "9", x2: "20", y2: "9" }),
|
|
220
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "15", x2: "20", y2: "15" }),
|
|
221
|
+
/* @__PURE__ */ jsx("line", { x1: "10", y1: "3", x2: "8", y2: "21" }),
|
|
222
|
+
/* @__PURE__ */ jsx("line", { x1: "16", y1: "3", x2: "14", y2: "21" })
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
var UserIcon = ({
|
|
227
|
+
size = defaultSize,
|
|
228
|
+
color = defaultColor,
|
|
229
|
+
className
|
|
230
|
+
}) => /* @__PURE__ */ jsxs(
|
|
231
|
+
"svg",
|
|
232
|
+
{
|
|
233
|
+
width: size,
|
|
234
|
+
height: size,
|
|
235
|
+
viewBox: "0 0 24 24",
|
|
236
|
+
fill: "none",
|
|
237
|
+
stroke: color,
|
|
238
|
+
strokeWidth: "2",
|
|
239
|
+
strokeLinecap: "round",
|
|
240
|
+
strokeLinejoin: "round",
|
|
241
|
+
className,
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "5" }),
|
|
244
|
+
/* @__PURE__ */ jsx("path", { d: "M20 21a8 8 0 1 0-16 0" })
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
var IdCardIcon = ({
|
|
249
|
+
size = defaultSize,
|
|
250
|
+
color = defaultColor,
|
|
251
|
+
className
|
|
252
|
+
}) => /* @__PURE__ */ jsxs(
|
|
253
|
+
"svg",
|
|
254
|
+
{
|
|
255
|
+
width: size,
|
|
256
|
+
height: size,
|
|
257
|
+
viewBox: "0 0 24 24",
|
|
258
|
+
fill: "none",
|
|
259
|
+
stroke: color,
|
|
260
|
+
strokeWidth: "2",
|
|
261
|
+
strokeLinecap: "round",
|
|
262
|
+
strokeLinejoin: "round",
|
|
263
|
+
className,
|
|
264
|
+
children: [
|
|
265
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "5", width: "20", height: "14", rx: "2" }),
|
|
266
|
+
/* @__PURE__ */ jsx("circle", { cx: "8", cy: "12", r: "2" }),
|
|
267
|
+
/* @__PURE__ */ jsx("path", { d: "M14 10h4" }),
|
|
268
|
+
/* @__PURE__ */ jsx("path", { d: "M14 14h4" })
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
var GearIcon = ({
|
|
273
|
+
size = defaultSize,
|
|
274
|
+
color = defaultColor,
|
|
275
|
+
className
|
|
276
|
+
}) => /* @__PURE__ */ jsxs(
|
|
277
|
+
"svg",
|
|
278
|
+
{
|
|
279
|
+
width: size,
|
|
280
|
+
height: size,
|
|
281
|
+
viewBox: "0 0 24 24",
|
|
282
|
+
fill: "none",
|
|
283
|
+
stroke: color,
|
|
284
|
+
strokeWidth: "2",
|
|
285
|
+
strokeLinecap: "round",
|
|
286
|
+
strokeLinejoin: "round",
|
|
287
|
+
className,
|
|
288
|
+
children: [
|
|
289
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
|
|
290
|
+
/* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
var AppIcon = ({
|
|
295
|
+
size = defaultSize,
|
|
296
|
+
color = defaultColor,
|
|
297
|
+
className
|
|
298
|
+
}) => /* @__PURE__ */ jsxs(
|
|
299
|
+
"svg",
|
|
300
|
+
{
|
|
301
|
+
width: size,
|
|
302
|
+
height: size,
|
|
303
|
+
viewBox: "0 0 24 24",
|
|
304
|
+
fill: "none",
|
|
305
|
+
stroke: color,
|
|
306
|
+
strokeWidth: "2",
|
|
307
|
+
strokeLinecap: "round",
|
|
308
|
+
strokeLinejoin: "round",
|
|
309
|
+
className,
|
|
310
|
+
children: [
|
|
311
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
312
|
+
/* @__PURE__ */ jsx("path", { d: "M9 3v18" }),
|
|
313
|
+
/* @__PURE__ */ jsx("path", { d: "M3 9h18" })
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
var RegistryIcon = ({
|
|
318
|
+
size = defaultSize,
|
|
319
|
+
color = defaultColor,
|
|
320
|
+
className
|
|
321
|
+
}) => /* @__PURE__ */ jsx(
|
|
322
|
+
"svg",
|
|
323
|
+
{
|
|
324
|
+
width: size,
|
|
325
|
+
height: size,
|
|
326
|
+
viewBox: "0 0 24 24",
|
|
327
|
+
fill: "none",
|
|
328
|
+
stroke: color,
|
|
329
|
+
strokeWidth: "2",
|
|
330
|
+
strokeLinecap: "round",
|
|
331
|
+
strokeLinejoin: "round",
|
|
332
|
+
className,
|
|
333
|
+
children: /* @__PURE__ */ jsx("path", { d: "m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4" })
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
var ThreatActorIcon = ({
|
|
337
|
+
size = defaultSize,
|
|
338
|
+
color = defaultColor,
|
|
339
|
+
className
|
|
340
|
+
}) => /* @__PURE__ */ jsxs(
|
|
341
|
+
"svg",
|
|
342
|
+
{
|
|
343
|
+
width: size,
|
|
344
|
+
height: size,
|
|
345
|
+
viewBox: "0 0 24 24",
|
|
346
|
+
fill: "none",
|
|
347
|
+
stroke: color,
|
|
348
|
+
strokeWidth: "2",
|
|
349
|
+
strokeLinecap: "round",
|
|
350
|
+
strokeLinejoin: "round",
|
|
351
|
+
className,
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "10", r: "7" }),
|
|
354
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.5", fill: color }),
|
|
355
|
+
/* @__PURE__ */ jsx("circle", { cx: "15", cy: "9", r: "1.5", fill: color }),
|
|
356
|
+
/* @__PURE__ */ jsx("path", { d: "M9 17v-2" }),
|
|
357
|
+
/* @__PURE__ */ jsx("path", { d: "M12 17v-2" }),
|
|
358
|
+
/* @__PURE__ */ jsx("path", { d: "M15 17v-2" })
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
var BugIcon = ({
|
|
363
|
+
size = defaultSize,
|
|
364
|
+
color = defaultColor,
|
|
365
|
+
className
|
|
366
|
+
}) => /* @__PURE__ */ jsxs(
|
|
367
|
+
"svg",
|
|
368
|
+
{
|
|
369
|
+
width: size,
|
|
370
|
+
height: size,
|
|
371
|
+
viewBox: "0 0 24 24",
|
|
372
|
+
fill: "none",
|
|
373
|
+
stroke: color,
|
|
374
|
+
strokeWidth: "2",
|
|
375
|
+
strokeLinecap: "round",
|
|
376
|
+
strokeLinejoin: "round",
|
|
377
|
+
className,
|
|
378
|
+
children: [
|
|
379
|
+
/* @__PURE__ */ jsx("path", { d: "m8 2 1.88 1.88" }),
|
|
380
|
+
/* @__PURE__ */ jsx("path", { d: "M14.12 3.88 16 2" }),
|
|
381
|
+
/* @__PURE__ */ jsx("path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }),
|
|
382
|
+
/* @__PURE__ */ jsx("path", { d: "M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" }),
|
|
383
|
+
/* @__PURE__ */ jsx("path", { d: "M12 20v-9" }),
|
|
384
|
+
/* @__PURE__ */ jsx("path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }),
|
|
385
|
+
/* @__PURE__ */ jsx("path", { d: "M6 13H2" }),
|
|
386
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }),
|
|
387
|
+
/* @__PURE__ */ jsx("path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }),
|
|
388
|
+
/* @__PURE__ */ jsx("path", { d: "M22 13h-4" }),
|
|
389
|
+
/* @__PURE__ */ jsx("path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" })
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
var SwordIcon = ({
|
|
394
|
+
size = defaultSize,
|
|
395
|
+
color = defaultColor,
|
|
396
|
+
className
|
|
397
|
+
}) => /* @__PURE__ */ jsxs(
|
|
398
|
+
"svg",
|
|
399
|
+
{
|
|
400
|
+
width: size,
|
|
401
|
+
height: size,
|
|
402
|
+
viewBox: "0 0 24 24",
|
|
403
|
+
fill: "none",
|
|
404
|
+
stroke: color,
|
|
405
|
+
strokeWidth: "2",
|
|
406
|
+
strokeLinecap: "round",
|
|
407
|
+
strokeLinejoin: "round",
|
|
408
|
+
className,
|
|
409
|
+
children: [
|
|
410
|
+
/* @__PURE__ */ jsx("polyline", { points: "14.5,17.5 3,6 3,3 6,3 17.5,14.5" }),
|
|
411
|
+
/* @__PURE__ */ jsx("line", { x1: "13", y1: "19", x2: "19", y2: "13" }),
|
|
412
|
+
/* @__PURE__ */ jsx("line", { x1: "16", y1: "16", x2: "20", y2: "20" }),
|
|
413
|
+
/* @__PURE__ */ jsx("line", { x1: "19", y1: "21", x2: "21", y2: "19" })
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
var TargetIcon = ({
|
|
418
|
+
size = defaultSize,
|
|
419
|
+
color = defaultColor,
|
|
420
|
+
className
|
|
421
|
+
}) => /* @__PURE__ */ jsxs(
|
|
422
|
+
"svg",
|
|
423
|
+
{
|
|
424
|
+
width: size,
|
|
425
|
+
height: size,
|
|
426
|
+
viewBox: "0 0 24 24",
|
|
427
|
+
fill: "none",
|
|
428
|
+
stroke: color,
|
|
429
|
+
strokeWidth: "2",
|
|
430
|
+
strokeLinecap: "round",
|
|
431
|
+
strokeLinejoin: "round",
|
|
432
|
+
className,
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
435
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "6" }),
|
|
436
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2" })
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
var AlertIcon = ({
|
|
441
|
+
size = defaultSize,
|
|
442
|
+
color = defaultColor,
|
|
443
|
+
className
|
|
444
|
+
}) => /* @__PURE__ */ jsxs(
|
|
445
|
+
"svg",
|
|
446
|
+
{
|
|
447
|
+
width: size,
|
|
448
|
+
height: size,
|
|
449
|
+
viewBox: "0 0 24 24",
|
|
450
|
+
fill: "none",
|
|
451
|
+
stroke: color,
|
|
452
|
+
strokeWidth: "2",
|
|
453
|
+
strokeLinecap: "round",
|
|
454
|
+
strokeLinejoin: "round",
|
|
455
|
+
className,
|
|
456
|
+
children: [
|
|
457
|
+
/* @__PURE__ */ jsx("path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
|
|
458
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
|
|
459
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
var FlaskIcon = ({
|
|
464
|
+
size = defaultSize,
|
|
465
|
+
color = defaultColor,
|
|
466
|
+
className
|
|
467
|
+
}) => /* @__PURE__ */ jsxs(
|
|
468
|
+
"svg",
|
|
469
|
+
{
|
|
470
|
+
width: size,
|
|
471
|
+
height: size,
|
|
472
|
+
viewBox: "0 0 24 24",
|
|
473
|
+
fill: "none",
|
|
474
|
+
stroke: color,
|
|
475
|
+
strokeWidth: "2",
|
|
476
|
+
strokeLinecap: "round",
|
|
477
|
+
strokeLinejoin: "round",
|
|
478
|
+
className,
|
|
479
|
+
children: [
|
|
480
|
+
/* @__PURE__ */ jsx("path", { d: "M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" }),
|
|
481
|
+
/* @__PURE__ */ jsx("path", { d: "M8.5 2h7" }),
|
|
482
|
+
/* @__PURE__ */ jsx("path", { d: "M7 16h10" })
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
var CertificateIcon = ({
|
|
487
|
+
size = defaultSize,
|
|
488
|
+
color = defaultColor,
|
|
489
|
+
className
|
|
490
|
+
}) => /* @__PURE__ */ jsxs(
|
|
491
|
+
"svg",
|
|
492
|
+
{
|
|
493
|
+
width: size,
|
|
494
|
+
height: size,
|
|
495
|
+
viewBox: "0 0 24 24",
|
|
496
|
+
fill: "none",
|
|
497
|
+
stroke: color,
|
|
498
|
+
strokeWidth: "2",
|
|
499
|
+
strokeLinecap: "round",
|
|
500
|
+
strokeLinejoin: "round",
|
|
501
|
+
className,
|
|
502
|
+
children: [
|
|
503
|
+
/* @__PURE__ */ jsx("path", { d: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" }),
|
|
504
|
+
/* @__PURE__ */ jsx("path", { d: "M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Z" }),
|
|
505
|
+
/* @__PURE__ */ jsx("path", { d: "m9.5 15.5-3 3v3l3.5-1.5 3.5 1.5v-3l-3-3" })
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
var WifiIcon = ({
|
|
510
|
+
size = defaultSize,
|
|
511
|
+
color = defaultColor,
|
|
512
|
+
className
|
|
513
|
+
}) => /* @__PURE__ */ jsxs(
|
|
514
|
+
"svg",
|
|
515
|
+
{
|
|
516
|
+
width: size,
|
|
517
|
+
height: size,
|
|
518
|
+
viewBox: "0 0 24 24",
|
|
519
|
+
fill: "none",
|
|
520
|
+
stroke: color,
|
|
521
|
+
strokeWidth: "2",
|
|
522
|
+
strokeLinecap: "round",
|
|
523
|
+
strokeLinejoin: "round",
|
|
524
|
+
className,
|
|
525
|
+
children: [
|
|
526
|
+
/* @__PURE__ */ jsx("path", { d: "M5 12.55a11 11 0 0 1 14.08 0" }),
|
|
527
|
+
/* @__PURE__ */ jsx("path", { d: "M1.42 9a16 16 0 0 1 21.16 0" }),
|
|
528
|
+
/* @__PURE__ */ jsx("path", { d: "M8.53 16.11a6 6 0 0 1 6.95 0" }),
|
|
529
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "20", x2: "12.01", y2: "20" })
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
var WorldIcon = ({
|
|
534
|
+
size = defaultSize,
|
|
535
|
+
color = defaultColor,
|
|
536
|
+
className
|
|
537
|
+
}) => /* @__PURE__ */ jsxs(
|
|
538
|
+
"svg",
|
|
539
|
+
{
|
|
540
|
+
width: size,
|
|
541
|
+
height: size,
|
|
542
|
+
viewBox: "0 0 24 24",
|
|
543
|
+
fill: "none",
|
|
544
|
+
stroke: color,
|
|
545
|
+
strokeWidth: "2",
|
|
546
|
+
strokeLinecap: "round",
|
|
547
|
+
strokeLinejoin: "round",
|
|
548
|
+
className,
|
|
549
|
+
children: [
|
|
550
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
551
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
|
|
552
|
+
/* @__PURE__ */ jsx("path", { d: "M2 12h20" })
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
var QuestionIcon = ({
|
|
557
|
+
size = defaultSize,
|
|
558
|
+
color = defaultColor,
|
|
559
|
+
className
|
|
560
|
+
}) => /* @__PURE__ */ jsxs(
|
|
561
|
+
"svg",
|
|
562
|
+
{
|
|
563
|
+
width: size,
|
|
564
|
+
height: size,
|
|
565
|
+
viewBox: "0 0 24 24",
|
|
566
|
+
fill: "none",
|
|
567
|
+
stroke: color,
|
|
568
|
+
strokeWidth: "2",
|
|
569
|
+
strokeLinecap: "round",
|
|
570
|
+
strokeLinejoin: "round",
|
|
571
|
+
className,
|
|
572
|
+
children: [
|
|
573
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
574
|
+
/* @__PURE__ */ jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
|
|
575
|
+
/* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
|
|
576
|
+
]
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
var CheckIcon = ({
|
|
580
|
+
size = defaultSize,
|
|
581
|
+
color = defaultColor,
|
|
582
|
+
className
|
|
583
|
+
}) => /* @__PURE__ */ jsx(
|
|
584
|
+
"svg",
|
|
585
|
+
{
|
|
586
|
+
width: size,
|
|
587
|
+
height: size,
|
|
588
|
+
viewBox: "0 0 24 24",
|
|
589
|
+
fill: "none",
|
|
590
|
+
stroke: color,
|
|
591
|
+
strokeWidth: "2",
|
|
592
|
+
strokeLinecap: "round",
|
|
593
|
+
strokeLinejoin: "round",
|
|
594
|
+
className,
|
|
595
|
+
children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
var BoxIcon = ({
|
|
599
|
+
size = defaultSize,
|
|
600
|
+
color = defaultColor,
|
|
601
|
+
className
|
|
602
|
+
}) => /* @__PURE__ */ jsxs(
|
|
603
|
+
"svg",
|
|
604
|
+
{
|
|
605
|
+
width: size,
|
|
606
|
+
height: size,
|
|
607
|
+
viewBox: "0 0 24 24",
|
|
608
|
+
fill: "none",
|
|
609
|
+
stroke: color,
|
|
610
|
+
strokeWidth: "2",
|
|
611
|
+
strokeLinecap: "round",
|
|
612
|
+
strokeLinejoin: "round",
|
|
613
|
+
className,
|
|
614
|
+
children: [
|
|
615
|
+
/* @__PURE__ */ jsx("path", { d: "M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" }),
|
|
616
|
+
/* @__PURE__ */ jsx("path", { d: "m3.3 7 8.7 5 8.7-5" }),
|
|
617
|
+
/* @__PURE__ */ jsx("path", { d: "M12 22V12" })
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
var CrosshairIcon = ({
|
|
622
|
+
size = defaultSize,
|
|
623
|
+
color = defaultColor,
|
|
624
|
+
className
|
|
625
|
+
}) => /* @__PURE__ */ jsxs(
|
|
626
|
+
"svg",
|
|
627
|
+
{
|
|
628
|
+
width: size,
|
|
629
|
+
height: size,
|
|
630
|
+
viewBox: "0 0 24 24",
|
|
631
|
+
fill: "none",
|
|
632
|
+
stroke: color,
|
|
633
|
+
strokeWidth: "2",
|
|
634
|
+
strokeLinecap: "round",
|
|
635
|
+
strokeLinejoin: "round",
|
|
636
|
+
className,
|
|
637
|
+
children: [
|
|
638
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
639
|
+
/* @__PURE__ */ jsx("line", { x1: "22", y1: "12", x2: "18", y2: "12" }),
|
|
640
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "12", x2: "2", y2: "12" }),
|
|
641
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "6", x2: "12", y2: "2" }),
|
|
642
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "22", x2: "12", y2: "18" })
|
|
643
|
+
]
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
var OBSERVABLE_ICON_MAP = {
|
|
647
|
+
// Network
|
|
648
|
+
"ipv4-addr": GlobeIcon,
|
|
649
|
+
"ipv6-addr": GlobeIcon,
|
|
650
|
+
"domain-name": DomainIcon,
|
|
651
|
+
url: LinkIcon,
|
|
652
|
+
"autonomous-system": WorldIcon,
|
|
653
|
+
"mac-addr": WifiIcon,
|
|
654
|
+
// Email
|
|
655
|
+
"email-addr": MailIcon,
|
|
656
|
+
"email-message": EnvelopeIcon,
|
|
657
|
+
// File
|
|
658
|
+
file: FileIcon,
|
|
659
|
+
"file-hash": HashIcon,
|
|
660
|
+
"file:hash:md5": HashIcon,
|
|
661
|
+
"file:hash:sha1": HashIcon,
|
|
662
|
+
"file:hash:sha256": HashIcon,
|
|
663
|
+
// User/Identity
|
|
664
|
+
user: UserIcon,
|
|
665
|
+
"user-account": UserIcon,
|
|
666
|
+
identity: IdCardIcon,
|
|
667
|
+
// Process/System
|
|
668
|
+
process: GearIcon,
|
|
669
|
+
software: AppIcon,
|
|
670
|
+
"windows-registry-key": RegistryIcon,
|
|
671
|
+
// Threat Intelligence
|
|
672
|
+
"threat-actor": ThreatActorIcon,
|
|
673
|
+
malware: BugIcon,
|
|
674
|
+
"attack-pattern": SwordIcon,
|
|
675
|
+
campaign: TargetIcon,
|
|
676
|
+
indicator: AlertIcon,
|
|
677
|
+
// Artifacts
|
|
678
|
+
artifact: FlaskIcon,
|
|
679
|
+
certificate: CertificateIcon,
|
|
680
|
+
"x509-certificate": CertificateIcon,
|
|
681
|
+
// Default
|
|
682
|
+
unknown: QuestionIcon
|
|
683
|
+
};
|
|
684
|
+
var INVESTIGATION_ICON_MAP = {
|
|
685
|
+
root: CrosshairIcon,
|
|
686
|
+
check: CheckIcon,
|
|
687
|
+
container: BoxIcon
|
|
146
688
|
};
|
|
147
|
-
function
|
|
148
|
-
|
|
689
|
+
function getObservableIcon(observableType) {
|
|
690
|
+
const normalized = observableType.toLowerCase().trim();
|
|
691
|
+
return OBSERVABLE_ICON_MAP[normalized] ?? OBSERVABLE_ICON_MAP.unknown;
|
|
692
|
+
}
|
|
693
|
+
function getInvestigationIcon(nodeType) {
|
|
694
|
+
return INVESTIGATION_ICON_MAP[nodeType] ?? QuestionIcon;
|
|
149
695
|
}
|
|
150
696
|
|
|
151
697
|
// src/components/ObservableNode.tsx
|
|
152
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
153
|
-
var NODE_SIZE =
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
698
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
699
|
+
var NODE_SIZE = 40;
|
|
700
|
+
var ROOT_NODE_WIDTH = 56;
|
|
701
|
+
var ROOT_NODE_HEIGHT = 40;
|
|
702
|
+
var ICON_SIZE = 18;
|
|
703
|
+
var ROOT_ICON_SIZE = 20;
|
|
704
|
+
var nodeStyles = {
|
|
705
|
+
container: {
|
|
706
|
+
display: "flex",
|
|
707
|
+
flexDirection: "column",
|
|
708
|
+
alignItems: "center",
|
|
709
|
+
cursor: "grab",
|
|
710
|
+
transition: "transform 0.1s ease-out"
|
|
711
|
+
},
|
|
712
|
+
shapeWrapper: {
|
|
713
|
+
position: "relative",
|
|
714
|
+
display: "flex",
|
|
715
|
+
alignItems: "center",
|
|
716
|
+
justifyContent: "center"
|
|
717
|
+
},
|
|
718
|
+
label: {
|
|
719
|
+
marginTop: 4,
|
|
720
|
+
fontSize: 10,
|
|
721
|
+
fontWeight: 500,
|
|
722
|
+
maxWidth: 80,
|
|
723
|
+
textAlign: "center",
|
|
724
|
+
overflow: "hidden",
|
|
725
|
+
textOverflow: "ellipsis",
|
|
726
|
+
whiteSpace: "nowrap",
|
|
727
|
+
fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
728
|
+
letterSpacing: "-0.01em",
|
|
729
|
+
lineHeight: 1.2
|
|
730
|
+
},
|
|
731
|
+
handle: {
|
|
732
|
+
position: "absolute",
|
|
733
|
+
top: "50%",
|
|
734
|
+
left: "50%",
|
|
735
|
+
transform: "translate(-50%, -50%)",
|
|
736
|
+
width: 1,
|
|
737
|
+
height: 1,
|
|
738
|
+
background: "transparent",
|
|
739
|
+
border: "none",
|
|
740
|
+
opacity: 0,
|
|
741
|
+
pointerEvents: "none"
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
function ObservableNodeComponent({ data, selected }) {
|
|
159
745
|
const nodeData = data;
|
|
160
|
-
const {
|
|
161
|
-
label,
|
|
162
|
-
emoji,
|
|
163
|
-
shape,
|
|
164
|
-
level,
|
|
165
|
-
isRoot,
|
|
166
|
-
whitelisted,
|
|
167
|
-
fullValue
|
|
168
|
-
} = nodeData;
|
|
169
|
-
const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
|
|
746
|
+
const { label, level, isRoot, whitelisted, fullValue, observableType } = nodeData;
|
|
170
747
|
const borderColor = getLevelColor(level);
|
|
171
748
|
const backgroundColor = getLevelBackgroundColor(level);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
749
|
+
const IconComponent = useMemo(() => {
|
|
750
|
+
if (isRoot) return CrosshairIcon;
|
|
751
|
+
return getObservableIcon(observableType);
|
|
752
|
+
}, [isRoot, observableType]);
|
|
753
|
+
const shapeStyle = useMemo(() => {
|
|
754
|
+
if (isRoot) {
|
|
755
|
+
return {
|
|
756
|
+
width: ROOT_NODE_WIDTH,
|
|
757
|
+
height: ROOT_NODE_HEIGHT,
|
|
758
|
+
borderRadius: ROOT_NODE_HEIGHT / 2,
|
|
759
|
+
display: "flex",
|
|
760
|
+
alignItems: "center",
|
|
761
|
+
justifyContent: "center",
|
|
762
|
+
backgroundColor,
|
|
763
|
+
border: `2.5px solid ${borderColor}`,
|
|
764
|
+
boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 8px rgba(0,0,0,0.08)",
|
|
765
|
+
opacity: whitelisted ? 0.5 : 1,
|
|
766
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
width: NODE_SIZE,
|
|
771
|
+
height: NODE_SIZE,
|
|
772
|
+
borderRadius: "50%",
|
|
176
773
|
display: "flex",
|
|
177
774
|
alignItems: "center",
|
|
178
775
|
justifyContent: "center",
|
|
179
776
|
backgroundColor,
|
|
180
|
-
border:
|
|
777
|
+
border: `2px solid ${borderColor}`,
|
|
778
|
+
boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 6px rgba(0,0,0,0.08)",
|
|
181
779
|
opacity: whitelisted ? 0.5 : 1,
|
|
182
|
-
|
|
780
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
|
|
183
781
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...baseStyle,
|
|
192
|
-
borderRadius: 0,
|
|
193
|
-
border: "none",
|
|
194
|
-
background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
|
|
195
|
-
clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
|
|
196
|
-
position: "relative"
|
|
197
|
-
};
|
|
198
|
-
case "rectangle":
|
|
199
|
-
default:
|
|
200
|
-
return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
const isTriangle = shape === "triangle";
|
|
204
|
-
return /* @__PURE__ */ jsxs(
|
|
205
|
-
"div",
|
|
206
|
-
{
|
|
207
|
-
className: "observable-node",
|
|
208
|
-
style: {
|
|
209
|
-
display: "flex",
|
|
210
|
-
flexDirection: "column",
|
|
211
|
-
alignItems: "center",
|
|
212
|
-
cursor: "pointer"
|
|
213
|
-
},
|
|
214
|
-
children: [
|
|
215
|
-
/* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
|
|
216
|
-
isTriangle ? (
|
|
217
|
-
// Triangle using SVG
|
|
218
|
-
/* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 100 100", children: [
|
|
219
|
-
/* @__PURE__ */ jsx(
|
|
220
|
-
"polygon",
|
|
221
|
-
{
|
|
222
|
-
points: "50,10 90,90 10,90",
|
|
223
|
-
fill: backgroundColor,
|
|
224
|
-
stroke: borderColor,
|
|
225
|
-
strokeWidth: selected ? 6 : 4,
|
|
226
|
-
opacity: whitelisted ? 0.5 : 1
|
|
227
|
-
}
|
|
228
|
-
),
|
|
229
|
-
/* @__PURE__ */ jsx(
|
|
230
|
-
"text",
|
|
231
|
-
{
|
|
232
|
-
x: "50",
|
|
233
|
-
y: "65",
|
|
234
|
-
textAnchor: "middle",
|
|
235
|
-
fontSize: "32",
|
|
236
|
-
dominantBaseline: "middle",
|
|
237
|
-
children: emoji
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
] })
|
|
241
|
-
) : (
|
|
242
|
-
// Other shapes using CSS
|
|
243
|
-
/* @__PURE__ */ jsx("div", { style: getShapeStyle(), children: /* @__PURE__ */ jsx("span", { style: { userSelect: "none" }, children: emoji }) })
|
|
244
|
-
),
|
|
245
|
-
/* @__PURE__ */ jsx(
|
|
246
|
-
Handle,
|
|
247
|
-
{
|
|
248
|
-
type: "source",
|
|
249
|
-
position: Position.Right,
|
|
250
|
-
id: "source",
|
|
251
|
-
style: {
|
|
252
|
-
position: "absolute",
|
|
253
|
-
top: "50%",
|
|
254
|
-
left: "50%",
|
|
255
|
-
transform: "translate(-50%, -50%)",
|
|
256
|
-
width: 1,
|
|
257
|
-
height: 1,
|
|
258
|
-
background: "transparent",
|
|
259
|
-
border: "none",
|
|
260
|
-
opacity: 0
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
),
|
|
264
|
-
/* @__PURE__ */ jsx(
|
|
265
|
-
Handle,
|
|
266
|
-
{
|
|
267
|
-
type: "target",
|
|
268
|
-
position: Position.Left,
|
|
269
|
-
id: "target",
|
|
270
|
-
style: {
|
|
271
|
-
position: "absolute",
|
|
272
|
-
top: "50%",
|
|
273
|
-
left: "50%",
|
|
274
|
-
transform: "translate(-50%, -50%)",
|
|
275
|
-
width: 1,
|
|
276
|
-
height: 1,
|
|
277
|
-
background: "transparent",
|
|
278
|
-
border: "none",
|
|
279
|
-
opacity: 0
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
)
|
|
283
|
-
] }),
|
|
284
|
-
/* @__PURE__ */ jsx(
|
|
285
|
-
"div",
|
|
286
|
-
{
|
|
287
|
-
style: {
|
|
288
|
-
marginTop: 2,
|
|
289
|
-
fontSize: 9,
|
|
290
|
-
maxWidth: 70,
|
|
291
|
-
textAlign: "center",
|
|
292
|
-
overflow: "hidden",
|
|
293
|
-
textOverflow: "ellipsis",
|
|
294
|
-
whiteSpace: "nowrap",
|
|
295
|
-
color: "#374151",
|
|
296
|
-
fontFamily: "system-ui, sans-serif"
|
|
297
|
-
},
|
|
298
|
-
title: fullValue,
|
|
299
|
-
children: label
|
|
300
|
-
}
|
|
301
|
-
)
|
|
302
|
-
]
|
|
303
|
-
}
|
|
782
|
+
}, [isRoot, backgroundColor, borderColor, selected, whitelisted]);
|
|
783
|
+
const labelStyle = useMemo(
|
|
784
|
+
() => ({
|
|
785
|
+
...nodeStyles.label,
|
|
786
|
+
color: whitelisted ? "#9ca3af" : "#374151"
|
|
787
|
+
}),
|
|
788
|
+
[whitelisted]
|
|
304
789
|
);
|
|
790
|
+
return /* @__PURE__ */ jsxs2("div", { className: "observable-node", style: nodeStyles.container, children: [
|
|
791
|
+
/* @__PURE__ */ jsxs2("div", { style: nodeStyles.shapeWrapper, children: [
|
|
792
|
+
/* @__PURE__ */ jsx2("div", { style: shapeStyle, children: /* @__PURE__ */ jsx2(
|
|
793
|
+
IconComponent,
|
|
794
|
+
{
|
|
795
|
+
size: isRoot ? ROOT_ICON_SIZE : ICON_SIZE,
|
|
796
|
+
color: borderColor
|
|
797
|
+
}
|
|
798
|
+
) }),
|
|
799
|
+
/* @__PURE__ */ jsx2(Handle, { type: "source", position: Position.Right, style: nodeStyles.handle }),
|
|
800
|
+
/* @__PURE__ */ jsx2(Handle, { type: "target", position: Position.Left, style: nodeStyles.handle })
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ jsx2("div", { style: labelStyle, title: fullValue, children: label })
|
|
803
|
+
] });
|
|
305
804
|
}
|
|
306
805
|
var ObservableNode = memo(ObservableNodeComponent);
|
|
307
806
|
|
|
308
807
|
// src/components/FloatingEdge.tsx
|
|
309
|
-
import { memo as memo2 } from "react";
|
|
310
|
-
import { BaseEdge,
|
|
311
|
-
import { jsx as
|
|
808
|
+
import { memo as memo2, useMemo as useMemo2 } from "react";
|
|
809
|
+
import { BaseEdge, getBezierPath } from "@xyflow/react";
|
|
810
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
811
|
+
function getControlOffset(sourceX, sourceY, targetX, targetY) {
|
|
812
|
+
const dx = targetX - sourceX;
|
|
813
|
+
const dy = targetY - sourceY;
|
|
814
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
815
|
+
return Math.min(Math.max(distance * 0.15, 20), 60);
|
|
816
|
+
}
|
|
312
817
|
function FloatingEdgeComponent({
|
|
313
818
|
id,
|
|
314
819
|
sourceX,
|
|
@@ -316,24 +821,35 @@ function FloatingEdgeComponent({
|
|
|
316
821
|
targetX,
|
|
317
822
|
targetY,
|
|
318
823
|
style,
|
|
319
|
-
markerEnd
|
|
824
|
+
markerEnd,
|
|
825
|
+
selected
|
|
320
826
|
}) {
|
|
321
|
-
const
|
|
827
|
+
const offset = useMemo2(
|
|
828
|
+
() => getControlOffset(sourceX, sourceY, targetX, targetY),
|
|
829
|
+
[sourceX, sourceY, targetX, targetY]
|
|
830
|
+
);
|
|
831
|
+
const [edgePath] = getBezierPath({
|
|
322
832
|
sourceX,
|
|
323
833
|
sourceY,
|
|
324
834
|
targetX,
|
|
325
|
-
targetY
|
|
835
|
+
targetY,
|
|
836
|
+
curvature: 0.15
|
|
326
837
|
});
|
|
327
|
-
|
|
838
|
+
const edgeStyle = useMemo2(
|
|
839
|
+
() => ({
|
|
840
|
+
strokeWidth: selected ? 2.5 : 1.5,
|
|
841
|
+
stroke: selected ? "#3b82f6" : "#94a3b8",
|
|
842
|
+
transition: "stroke 0.15s ease, stroke-width 0.15s ease",
|
|
843
|
+
...style
|
|
844
|
+
}),
|
|
845
|
+
[selected, style]
|
|
846
|
+
);
|
|
847
|
+
return /* @__PURE__ */ jsx3(
|
|
328
848
|
BaseEdge,
|
|
329
849
|
{
|
|
330
850
|
id,
|
|
331
851
|
path: edgePath,
|
|
332
|
-
style:
|
|
333
|
-
strokeWidth: 1.5,
|
|
334
|
-
stroke: "#94a3b8",
|
|
335
|
-
...style
|
|
336
|
-
},
|
|
852
|
+
style: edgeStyle,
|
|
337
853
|
markerEnd
|
|
338
854
|
}
|
|
339
855
|
);
|
|
@@ -341,7 +857,7 @@ function FloatingEdgeComponent({
|
|
|
341
857
|
var FloatingEdge = memo2(FloatingEdgeComponent);
|
|
342
858
|
|
|
343
859
|
// src/hooks/useForceLayout.ts
|
|
344
|
-
import { useEffect, useRef, useCallback, useMemo } from "react";
|
|
860
|
+
import { useEffect, useRef, useCallback, useMemo as useMemo3 } from "react";
|
|
345
861
|
import {
|
|
346
862
|
forceSimulation,
|
|
347
863
|
forceLink,
|
|
@@ -356,31 +872,42 @@ import {
|
|
|
356
872
|
useNodesInitialized,
|
|
357
873
|
useStore
|
|
358
874
|
} from "@xyflow/react";
|
|
359
|
-
var
|
|
875
|
+
var nodeIdsSelector = (state) => {
|
|
876
|
+
const ids = Array.from(state.nodeLookup.keys()).sort();
|
|
877
|
+
return ids.join(",");
|
|
878
|
+
};
|
|
360
879
|
function useForceLayout(config = {}, rootNodeId) {
|
|
361
880
|
const { getNodes, getEdges, setNodes } = useReactFlow();
|
|
362
881
|
const nodesInitialized = useNodesInitialized();
|
|
363
|
-
const
|
|
364
|
-
const forceConfig =
|
|
882
|
+
const nodeIds = useStore(nodeIdsSelector);
|
|
883
|
+
const forceConfig = useMemo3(
|
|
365
884
|
() => ({ ...DEFAULT_FORCE_CONFIG, ...config }),
|
|
366
885
|
[config]
|
|
367
886
|
);
|
|
368
887
|
const simulationRef = useRef(null);
|
|
369
|
-
const
|
|
888
|
+
const draggingRef = useRef({ nodeId: null, active: false });
|
|
889
|
+
const nodePositionsRef = useRef(
|
|
890
|
+
/* @__PURE__ */ new Map()
|
|
891
|
+
);
|
|
892
|
+
const rafRef = useRef(null);
|
|
370
893
|
useEffect(() => {
|
|
371
|
-
if (!nodesInitialized ||
|
|
894
|
+
if (!nodesInitialized || !nodeIds) {
|
|
372
895
|
return;
|
|
373
896
|
}
|
|
374
897
|
const nodes = getNodes();
|
|
375
898
|
const edges = getEdges();
|
|
899
|
+
if (nodes.length === 0) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
376
902
|
const simNodes = nodes.map((node) => {
|
|
377
903
|
const existingNode = simulationRef.current?.nodes().find((n) => n.id === node.id);
|
|
904
|
+
const x = existingNode?.x ?? nodePositionsRef.current.get(node.id)?.x ?? node.position.x ?? Math.random() * 500 - 250;
|
|
905
|
+
const y = existingNode?.y ?? nodePositionsRef.current.get(node.id)?.y ?? node.position.y ?? Math.random() * 500 - 250;
|
|
378
906
|
return {
|
|
379
907
|
id: node.id,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Preserve fixed positions for dragged nodes
|
|
908
|
+
x,
|
|
909
|
+
y,
|
|
910
|
+
// Preserve fixed positions for dragged nodes or root
|
|
384
911
|
fx: existingNode?.fx ?? null,
|
|
385
912
|
fy: existingNode?.fy ?? null
|
|
386
913
|
};
|
|
@@ -401,30 +928,33 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
401
928
|
if (simulationRef.current) {
|
|
402
929
|
simulationRef.current.stop();
|
|
403
930
|
}
|
|
931
|
+
if (rafRef.current) {
|
|
932
|
+
cancelAnimationFrame(rafRef.current);
|
|
933
|
+
rafRef.current = null;
|
|
934
|
+
}
|
|
404
935
|
const simulation = forceSimulation(simNodes).force(
|
|
405
936
|
"link",
|
|
406
|
-
forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.
|
|
937
|
+
forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.4)
|
|
407
938
|
).force(
|
|
408
939
|
"charge",
|
|
409
940
|
forceManyBody().strength(forceConfig.chargeStrength)
|
|
410
|
-
).force(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"y",
|
|
421
|
-
forceY(0).strength(0.01)
|
|
422
|
-
).alphaDecay(0.02).velocityDecay(0.4);
|
|
423
|
-
simulation.on("tick", () => {
|
|
941
|
+
).force("center", forceCenter(0, 0).strength(forceConfig.centerStrength)).force("collision", forceCollide(forceConfig.collisionRadius)).force("x", forceX(0).strength(8e-3)).force("y", forceY(0).strength(8e-3)).alphaDecay(0.02).velocityDecay(0.35);
|
|
942
|
+
const updateNodes = () => {
|
|
943
|
+
if (draggingRef.current.active) {
|
|
944
|
+
rafRef.current = requestAnimationFrame(updateNodes);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const simNodes2 = simulation.nodes();
|
|
948
|
+
for (const simNode of simNodes2) {
|
|
949
|
+
nodePositionsRef.current.set(simNode.id, { x: simNode.x, y: simNode.y });
|
|
950
|
+
}
|
|
424
951
|
setNodes(
|
|
425
952
|
(currentNodes) => currentNodes.map((node) => {
|
|
426
|
-
const simNode =
|
|
953
|
+
const simNode = simNodes2.find((n) => n.id === node.id);
|
|
427
954
|
if (!simNode) return node;
|
|
955
|
+
const dx = Math.abs(node.position.x - simNode.x);
|
|
956
|
+
const dy = Math.abs(node.position.y - simNode.y);
|
|
957
|
+
if (dx < 0.1 && dy < 0.1) return node;
|
|
428
958
|
return {
|
|
429
959
|
...node,
|
|
430
960
|
position: {
|
|
@@ -434,14 +964,26 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
434
964
|
};
|
|
435
965
|
})
|
|
436
966
|
);
|
|
967
|
+
if (simulation.alpha() > 1e-3) {
|
|
968
|
+
rafRef.current = requestAnimationFrame(updateNodes);
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
simulation.on("tick", () => {
|
|
972
|
+
if (rafRef.current === null && simulation.alpha() > 1e-3) {
|
|
973
|
+
rafRef.current = requestAnimationFrame(updateNodes);
|
|
974
|
+
}
|
|
437
975
|
});
|
|
438
976
|
simulationRef.current = simulation;
|
|
439
977
|
return () => {
|
|
440
978
|
simulation.stop();
|
|
979
|
+
if (rafRef.current) {
|
|
980
|
+
cancelAnimationFrame(rafRef.current);
|
|
981
|
+
rafRef.current = null;
|
|
982
|
+
}
|
|
441
983
|
};
|
|
442
984
|
}, [
|
|
443
985
|
nodesInitialized,
|
|
444
|
-
|
|
986
|
+
nodeIds,
|
|
445
987
|
getNodes,
|
|
446
988
|
getEdges,
|
|
447
989
|
setNodes,
|
|
@@ -452,33 +994,34 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
452
994
|
(_, node) => {
|
|
453
995
|
const simulation = simulationRef.current;
|
|
454
996
|
if (!simulation) return;
|
|
455
|
-
|
|
456
|
-
simulation.alphaTarget(0.3).restart();
|
|
457
|
-
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
458
|
-
if (simNode) {
|
|
459
|
-
simNode.fx = simNode.x;
|
|
460
|
-
simNode.fy = simNode.y;
|
|
461
|
-
}
|
|
462
|
-
},
|
|
463
|
-
[]
|
|
464
|
-
);
|
|
465
|
-
const onNodeDrag = useCallback(
|
|
466
|
-
(_, node) => {
|
|
467
|
-
const simulation = simulationRef.current;
|
|
468
|
-
if (!simulation) return;
|
|
997
|
+
draggingRef.current = { nodeId: node.id, active: true };
|
|
469
998
|
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
470
999
|
if (simNode) {
|
|
471
1000
|
simNode.fx = node.position.x;
|
|
472
1001
|
simNode.fy = node.position.y;
|
|
473
1002
|
}
|
|
1003
|
+
simulation.alphaTarget(0.1).restart();
|
|
474
1004
|
},
|
|
475
1005
|
[]
|
|
476
1006
|
);
|
|
1007
|
+
const onNodeDrag = useCallback((_, node) => {
|
|
1008
|
+
const simulation = simulationRef.current;
|
|
1009
|
+
if (!simulation) return;
|
|
1010
|
+
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
1011
|
+
if (simNode) {
|
|
1012
|
+
simNode.fx = node.position.x;
|
|
1013
|
+
simNode.fy = node.position.y;
|
|
1014
|
+
nodePositionsRef.current.set(node.id, {
|
|
1015
|
+
x: node.position.x,
|
|
1016
|
+
y: node.position.y
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}, []);
|
|
477
1020
|
const onNodeDragStop = useCallback(
|
|
478
1021
|
(_, node) => {
|
|
479
1022
|
const simulation = simulationRef.current;
|
|
1023
|
+
draggingRef.current = { nodeId: null, active: false };
|
|
480
1024
|
if (!simulation) return;
|
|
481
|
-
draggingNodeRef.current = null;
|
|
482
1025
|
simulation.alphaTarget(0);
|
|
483
1026
|
if (node.id !== rootNodeId) {
|
|
484
1027
|
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
@@ -487,6 +1030,11 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
487
1030
|
simNode.fy = null;
|
|
488
1031
|
}
|
|
489
1032
|
}
|
|
1033
|
+
setTimeout(() => {
|
|
1034
|
+
if (simulationRef.current && !draggingRef.current.active) {
|
|
1035
|
+
simulationRef.current.alpha(0.1).restart();
|
|
1036
|
+
}
|
|
1037
|
+
}, 50);
|
|
490
1038
|
},
|
|
491
1039
|
[rootNodeId]
|
|
492
1040
|
);
|
|
@@ -512,7 +1060,7 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
512
1060
|
forceCollide(updates.collisionRadius)
|
|
513
1061
|
);
|
|
514
1062
|
}
|
|
515
|
-
simulation.alpha(0.
|
|
1063
|
+
simulation.alpha(0.3).restart();
|
|
516
1064
|
},
|
|
517
1065
|
[]
|
|
518
1066
|
);
|
|
@@ -531,32 +1079,34 @@ function useForceLayout(config = {}, rootNodeId) {
|
|
|
531
1079
|
}
|
|
532
1080
|
|
|
533
1081
|
// src/components/ObservablesGraph.tsx
|
|
534
|
-
import { jsx as
|
|
1082
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
535
1083
|
var nodeTypes = {
|
|
536
1084
|
observable: ObservableNode
|
|
537
1085
|
};
|
|
538
1086
|
var edgeTypes = {
|
|
539
1087
|
floating: FloatingEdge
|
|
540
1088
|
};
|
|
1089
|
+
var defaultEdgeOptions = {
|
|
1090
|
+
type: "floating",
|
|
1091
|
+
style: { stroke: "#94a3b8", strokeWidth: 1.5 }
|
|
1092
|
+
};
|
|
541
1093
|
function createObservableNodes(investigation, rootObservableIds) {
|
|
542
1094
|
const graph = getObservableGraph(investigation);
|
|
543
1095
|
return graph.nodes.map((graphNode, index) => {
|
|
544
1096
|
const isRoot = rootObservableIds.has(graphNode.id);
|
|
545
|
-
const shape = getObservableShape(graphNode.type, isRoot);
|
|
546
1097
|
const nodeData = {
|
|
547
|
-
label: truncateLabel(graphNode.value,
|
|
1098
|
+
label: truncateLabel(graphNode.value, 16),
|
|
548
1099
|
fullValue: graphNode.value,
|
|
549
1100
|
observableType: graphNode.type,
|
|
550
1101
|
level: graphNode.level,
|
|
551
1102
|
score: graphNode.score,
|
|
552
|
-
|
|
553
|
-
shape,
|
|
1103
|
+
shape: "circle",
|
|
554
1104
|
isRoot,
|
|
555
1105
|
whitelisted: graphNode.whitelisted,
|
|
556
1106
|
internal: graphNode.internal
|
|
557
1107
|
};
|
|
558
1108
|
const angle = index / graph.nodes.length * 2 * Math.PI;
|
|
559
|
-
const radius = isRoot ? 0 :
|
|
1109
|
+
const radius = isRoot ? 0 : 180;
|
|
560
1110
|
return {
|
|
561
1111
|
id: graphNode.id,
|
|
562
1112
|
type: "observable",
|
|
@@ -564,7 +1114,10 @@ function createObservableNodes(investigation, rootObservableIds) {
|
|
|
564
1114
|
x: Math.cos(angle) * radius,
|
|
565
1115
|
y: Math.sin(angle) * radius
|
|
566
1116
|
},
|
|
567
|
-
data: nodeData
|
|
1117
|
+
data: nodeData,
|
|
1118
|
+
// Enable selection for better UX
|
|
1119
|
+
selectable: true,
|
|
1120
|
+
draggable: true
|
|
568
1121
|
};
|
|
569
1122
|
});
|
|
570
1123
|
}
|
|
@@ -581,100 +1134,160 @@ function createObservableEdges(investigation) {
|
|
|
581
1134
|
target: graphEdge.target,
|
|
582
1135
|
type: "floating",
|
|
583
1136
|
data: edgeData,
|
|
1137
|
+
// Animated edges for a modern feel
|
|
1138
|
+
animated: false,
|
|
584
1139
|
style: { stroke: "#94a3b8", strokeWidth: 1.5 }
|
|
585
1140
|
};
|
|
586
1141
|
});
|
|
587
1142
|
}
|
|
588
1143
|
var ForceControls = ({ config, onChange, onRestart }) => {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1144
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
1145
|
+
const panelStyle = {
|
|
1146
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
1147
|
+
backdropFilter: "blur(8px)",
|
|
1148
|
+
padding: isExpanded ? 14 : 10,
|
|
1149
|
+
borderRadius: 12,
|
|
1150
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
|
|
1151
|
+
fontSize: 12,
|
|
1152
|
+
fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
1153
|
+
minWidth: isExpanded ? 180 : "auto",
|
|
1154
|
+
transition: "all 0.2s ease",
|
|
1155
|
+
border: "1px solid rgba(0,0,0,0.06)"
|
|
1156
|
+
};
|
|
1157
|
+
const headerStyle = {
|
|
1158
|
+
display: "flex",
|
|
1159
|
+
alignItems: "center",
|
|
1160
|
+
justifyContent: "space-between",
|
|
1161
|
+
gap: 8,
|
|
1162
|
+
cursor: "pointer"
|
|
1163
|
+
};
|
|
1164
|
+
const titleStyle = {
|
|
1165
|
+
fontWeight: 600,
|
|
1166
|
+
color: "#1f2937",
|
|
1167
|
+
fontSize: 12,
|
|
1168
|
+
letterSpacing: "-0.01em"
|
|
1169
|
+
};
|
|
1170
|
+
const toggleStyle = {
|
|
1171
|
+
background: "none",
|
|
1172
|
+
border: "none",
|
|
1173
|
+
cursor: "pointer",
|
|
1174
|
+
padding: 4,
|
|
1175
|
+
borderRadius: 4,
|
|
1176
|
+
color: "#6b7280",
|
|
1177
|
+
display: "flex",
|
|
1178
|
+
alignItems: "center",
|
|
1179
|
+
transition: "transform 0.2s ease",
|
|
1180
|
+
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)"
|
|
1181
|
+
};
|
|
1182
|
+
const sliderContainerStyle = {
|
|
1183
|
+
marginTop: 12,
|
|
1184
|
+
display: isExpanded ? "block" : "none"
|
|
1185
|
+
};
|
|
1186
|
+
const sliderLabelStyle = {
|
|
1187
|
+
display: "flex",
|
|
1188
|
+
justifyContent: "space-between",
|
|
1189
|
+
marginBottom: 4,
|
|
1190
|
+
color: "#4b5563",
|
|
1191
|
+
fontSize: 11
|
|
1192
|
+
};
|
|
1193
|
+
const sliderStyle = {
|
|
1194
|
+
width: "100%",
|
|
1195
|
+
height: 4,
|
|
1196
|
+
appearance: "none",
|
|
1197
|
+
background: "#e5e7eb",
|
|
1198
|
+
borderRadius: 2,
|
|
1199
|
+
outline: "none",
|
|
1200
|
+
cursor: "pointer"
|
|
1201
|
+
};
|
|
1202
|
+
const buttonStyle = {
|
|
1203
|
+
width: "100%",
|
|
1204
|
+
padding: "8px 12px",
|
|
1205
|
+
border: "none",
|
|
1206
|
+
borderRadius: 8,
|
|
1207
|
+
background: "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)",
|
|
1208
|
+
color: "white",
|
|
1209
|
+
cursor: "pointer",
|
|
1210
|
+
fontSize: 12,
|
|
1211
|
+
fontWeight: 500,
|
|
1212
|
+
marginTop: 12,
|
|
1213
|
+
transition: "transform 0.1s ease, box-shadow 0.1s ease",
|
|
1214
|
+
boxShadow: "0 2px 4px rgba(59, 130, 246, 0.3)"
|
|
1215
|
+
};
|
|
1216
|
+
return /* @__PURE__ */ jsxs3("div", { style: panelStyle, children: [
|
|
1217
|
+
/* @__PURE__ */ jsxs3("div", { style: headerStyle, onClick: () => setIsExpanded(!isExpanded), children: [
|
|
1218
|
+
/* @__PURE__ */ jsx4("span", { style: titleStyle, children: "\u26A1 Force Layout" }),
|
|
1219
|
+
/* @__PURE__ */ jsx4("button", { style: toggleStyle, children: /* @__PURE__ */ jsx4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx4("polyline", { points: "6 9 12 15 18 9" }) }) })
|
|
1220
|
+
] }),
|
|
1221
|
+
/* @__PURE__ */ jsxs3("div", { style: sliderContainerStyle, children: [
|
|
1222
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: 10 }, children: [
|
|
1223
|
+
/* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
|
|
1224
|
+
/* @__PURE__ */ jsx4("span", { children: "Repulsion" }),
|
|
1225
|
+
/* @__PURE__ */ jsx4("span", { children: config.chargeStrength })
|
|
623
1226
|
] }),
|
|
624
|
-
/* @__PURE__ */
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
)
|
|
1227
|
+
/* @__PURE__ */ jsx4(
|
|
1228
|
+
"input",
|
|
1229
|
+
{
|
|
1230
|
+
type: "range",
|
|
1231
|
+
min: "-500",
|
|
1232
|
+
max: "-50",
|
|
1233
|
+
value: config.chargeStrength,
|
|
1234
|
+
onChange: (e) => onChange({ chargeStrength: Number(e.target.value) }),
|
|
1235
|
+
style: sliderStyle
|
|
1236
|
+
}
|
|
1237
|
+
)
|
|
1238
|
+
] }),
|
|
1239
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: 10 }, children: [
|
|
1240
|
+
/* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
|
|
1241
|
+
/* @__PURE__ */ jsx4("span", { children: "Link Distance" }),
|
|
1242
|
+
/* @__PURE__ */ jsx4("span", { children: config.linkDistance })
|
|
640
1243
|
] }),
|
|
641
|
-
/* @__PURE__ */
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
{
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
1244
|
+
/* @__PURE__ */ jsx4(
|
|
1245
|
+
"input",
|
|
1246
|
+
{
|
|
1247
|
+
type: "range",
|
|
1248
|
+
min: "30",
|
|
1249
|
+
max: "200",
|
|
1250
|
+
value: config.linkDistance,
|
|
1251
|
+
onChange: (e) => onChange({ linkDistance: Number(e.target.value) }),
|
|
1252
|
+
style: sliderStyle
|
|
1253
|
+
}
|
|
1254
|
+
)
|
|
1255
|
+
] }),
|
|
1256
|
+
/* @__PURE__ */ jsxs3("div", { style: { marginBottom: 6 }, children: [
|
|
1257
|
+
/* @__PURE__ */ jsxs3("div", { style: sliderLabelStyle, children: [
|
|
1258
|
+
/* @__PURE__ */ jsx4("span", { children: "Collision" }),
|
|
1259
|
+
/* @__PURE__ */ jsx4("span", { children: config.collisionRadius })
|
|
657
1260
|
] }),
|
|
658
|
-
/* @__PURE__ */
|
|
659
|
-
"
|
|
1261
|
+
/* @__PURE__ */ jsx4(
|
|
1262
|
+
"input",
|
|
660
1263
|
{
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
background: "#3b82f6",
|
|
668
|
-
color: "white",
|
|
669
|
-
cursor: "pointer",
|
|
670
|
-
fontSize: 12
|
|
671
|
-
},
|
|
672
|
-
children: "Restart Simulation"
|
|
1264
|
+
type: "range",
|
|
1265
|
+
min: "10",
|
|
1266
|
+
max: "80",
|
|
1267
|
+
value: config.collisionRadius,
|
|
1268
|
+
onChange: (e) => onChange({ collisionRadius: Number(e.target.value) }),
|
|
1269
|
+
style: sliderStyle
|
|
673
1270
|
}
|
|
674
1271
|
)
|
|
675
|
-
]
|
|
676
|
-
|
|
677
|
-
|
|
1272
|
+
] }),
|
|
1273
|
+
/* @__PURE__ */ jsx4(
|
|
1274
|
+
"button",
|
|
1275
|
+
{
|
|
1276
|
+
onClick: onRestart,
|
|
1277
|
+
style: buttonStyle,
|
|
1278
|
+
onMouseEnter: (e) => {
|
|
1279
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
1280
|
+
e.currentTarget.style.boxShadow = "0 4px 8px rgba(59, 130, 246, 0.4)";
|
|
1281
|
+
},
|
|
1282
|
+
onMouseLeave: (e) => {
|
|
1283
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
1284
|
+
e.currentTarget.style.boxShadow = "0 2px 4px rgba(59, 130, 246, 0.3)";
|
|
1285
|
+
},
|
|
1286
|
+
children: "Restart Simulation"
|
|
1287
|
+
}
|
|
1288
|
+
)
|
|
1289
|
+
] })
|
|
1290
|
+
] });
|
|
678
1291
|
};
|
|
679
1292
|
var ObservablesGraphInner = ({
|
|
680
1293
|
initialNodes,
|
|
@@ -692,11 +1305,13 @@ var ObservablesGraphInner = ({
|
|
|
692
1305
|
...DEFAULT_FORCE_CONFIG,
|
|
693
1306
|
...initialForceConfig
|
|
694
1307
|
});
|
|
1308
|
+
const initialFitDone = useRef2(false);
|
|
695
1309
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
696
1310
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
697
1311
|
React3.useEffect(() => {
|
|
698
1312
|
setNodes(initialNodes);
|
|
699
1313
|
setEdges(initialEdges);
|
|
1314
|
+
initialFitDone.current = false;
|
|
700
1315
|
}, [initialNodes, initialEdges, setNodes, setEdges]);
|
|
701
1316
|
const {
|
|
702
1317
|
onNodeDragStart,
|
|
@@ -728,71 +1343,114 @@ var ObservablesGraphInner = ({
|
|
|
728
1343
|
const data = node.data;
|
|
729
1344
|
return getLevelColor(data.level);
|
|
730
1345
|
}, []);
|
|
731
|
-
|
|
732
|
-
|
|
1346
|
+
const containerStyle = useMemo4(
|
|
1347
|
+
() => ({
|
|
1348
|
+
width,
|
|
1349
|
+
height,
|
|
1350
|
+
position: "relative",
|
|
1351
|
+
background: "linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%)"
|
|
1352
|
+
}),
|
|
1353
|
+
[width, height]
|
|
1354
|
+
);
|
|
1355
|
+
return /* @__PURE__ */ jsx4("div", { className, style: containerStyle, children: /* @__PURE__ */ jsxs3(
|
|
1356
|
+
ReactFlow,
|
|
733
1357
|
{
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1358
|
+
nodes,
|
|
1359
|
+
edges,
|
|
1360
|
+
onNodesChange,
|
|
1361
|
+
onEdgesChange,
|
|
1362
|
+
onNodeClick: handleNodeClick,
|
|
1363
|
+
onNodeDoubleClick: handleNodeDoubleClick,
|
|
1364
|
+
onNodeDragStart,
|
|
1365
|
+
onNodeDrag,
|
|
1366
|
+
onNodeDragStop,
|
|
1367
|
+
nodeTypes,
|
|
1368
|
+
edgeTypes,
|
|
1369
|
+
defaultEdgeOptions,
|
|
1370
|
+
connectionMode: ConnectionMode.Loose,
|
|
1371
|
+
fitView: true,
|
|
1372
|
+
fitViewOptions: { padding: 0.4, maxZoom: 1.5 },
|
|
1373
|
+
minZoom: 0.1,
|
|
1374
|
+
maxZoom: 2.5,
|
|
1375
|
+
proOptions: { hideAttribution: true },
|
|
1376
|
+
nodesDraggable: true,
|
|
1377
|
+
nodesConnectable: false,
|
|
1378
|
+
elementsSelectable: true,
|
|
1379
|
+
selectNodesOnDrag: false,
|
|
1380
|
+
panOnDrag: true,
|
|
1381
|
+
zoomOnScroll: true,
|
|
1382
|
+
zoomOnPinch: true,
|
|
1383
|
+
panOnScroll: false,
|
|
740
1384
|
children: [
|
|
741
|
-
/* @__PURE__ */
|
|
742
|
-
|
|
1385
|
+
/* @__PURE__ */ jsx4(
|
|
1386
|
+
Background,
|
|
743
1387
|
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
onNodeClick: handleNodeClick,
|
|
749
|
-
onNodeDoubleClick: handleNodeDoubleClick,
|
|
750
|
-
onNodeDragStart,
|
|
751
|
-
onNodeDrag,
|
|
752
|
-
onNodeDragStop,
|
|
753
|
-
nodeTypes,
|
|
754
|
-
edgeTypes,
|
|
755
|
-
connectionMode: ConnectionMode.Loose,
|
|
756
|
-
fitView: true,
|
|
757
|
-
fitViewOptions: { padding: 0.3 },
|
|
758
|
-
minZoom: 0.1,
|
|
759
|
-
maxZoom: 2,
|
|
760
|
-
proOptions: { hideAttribution: true },
|
|
761
|
-
children: [
|
|
762
|
-
/* @__PURE__ */ jsx3(Background, {}),
|
|
763
|
-
/* @__PURE__ */ jsx3(Controls, {}),
|
|
764
|
-
/* @__PURE__ */ jsx3(MiniMap, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
|
|
765
|
-
]
|
|
1388
|
+
variant: BackgroundVariant.Dots,
|
|
1389
|
+
gap: 24,
|
|
1390
|
+
size: 1,
|
|
1391
|
+
color: "#d1d5db"
|
|
766
1392
|
}
|
|
767
1393
|
),
|
|
768
|
-
|
|
1394
|
+
/* @__PURE__ */ jsx4(
|
|
1395
|
+
Controls,
|
|
1396
|
+
{
|
|
1397
|
+
showInteractive: false,
|
|
1398
|
+
style: {
|
|
1399
|
+
borderRadius: 10,
|
|
1400
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
1401
|
+
border: "1px solid rgba(0,0,0,0.06)"
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
),
|
|
1405
|
+
/* @__PURE__ */ jsx4(
|
|
1406
|
+
MiniMap,
|
|
1407
|
+
{
|
|
1408
|
+
nodeColor: miniMapNodeColor,
|
|
1409
|
+
zoomable: true,
|
|
1410
|
+
pannable: true,
|
|
1411
|
+
style: {
|
|
1412
|
+
borderRadius: 10,
|
|
1413
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
1414
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
1415
|
+
background: "rgba(255,255,255,0.9)"
|
|
1416
|
+
},
|
|
1417
|
+
maskColor: "rgba(0,0,0,0.08)"
|
|
1418
|
+
}
|
|
1419
|
+
),
|
|
1420
|
+
showControls && /* @__PURE__ */ jsx4(Panel, { position: "top-right", children: /* @__PURE__ */ jsx4(
|
|
769
1421
|
ForceControls,
|
|
770
1422
|
{
|
|
771
1423
|
config: forceConfig,
|
|
772
1424
|
onChange: handleConfigChange,
|
|
773
1425
|
onRestart: restartSimulation
|
|
774
1426
|
}
|
|
775
|
-
)
|
|
1427
|
+
) })
|
|
776
1428
|
]
|
|
777
1429
|
}
|
|
778
|
-
);
|
|
1430
|
+
) });
|
|
779
1431
|
};
|
|
780
1432
|
var ObservablesGraph = (props) => {
|
|
781
1433
|
const { investigation } = props;
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
1434
|
+
const { rootKeys, primaryRootId } = useMemo4(() => {
|
|
1435
|
+
const rootType = investigation.data_extraction.root_type;
|
|
1436
|
+
if (!rootType) {
|
|
1437
|
+
return { rootKeys: /* @__PURE__ */ new Set(), primaryRootId: void 0 };
|
|
1438
|
+
}
|
|
1439
|
+
const normalizedRootType = rootType.toLowerCase().trim();
|
|
1440
|
+
const rootsByType = Object.values(investigation.observables).filter(
|
|
1441
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
1442
|
+
);
|
|
1443
|
+
return {
|
|
1444
|
+
rootKeys: new Set(rootsByType.map((obs) => obs.key)),
|
|
1445
|
+
primaryRootId: rootsByType[0]?.key
|
|
1446
|
+
};
|
|
789
1447
|
}, [investigation]);
|
|
790
|
-
const { initialNodes, initialEdges } =
|
|
791
|
-
const nodes = createObservableNodes(investigation,
|
|
1448
|
+
const { initialNodes, initialEdges } = useMemo4(() => {
|
|
1449
|
+
const nodes = createObservableNodes(investigation, rootKeys);
|
|
792
1450
|
const edges = createObservableEdges(investigation);
|
|
793
1451
|
return { initialNodes: nodes, initialEdges: edges };
|
|
794
|
-
}, [investigation,
|
|
795
|
-
return /* @__PURE__ */
|
|
1452
|
+
}, [investigation, rootKeys]);
|
|
1453
|
+
return /* @__PURE__ */ jsx4(ReactFlowProvider, { children: /* @__PURE__ */ jsx4(
|
|
796
1454
|
ObservablesGraphInner,
|
|
797
1455
|
{
|
|
798
1456
|
...props,
|
|
@@ -804,160 +1462,142 @@ var ObservablesGraph = (props) => {
|
|
|
804
1462
|
};
|
|
805
1463
|
|
|
806
1464
|
// src/components/InvestigationGraph.tsx
|
|
807
|
-
import React5, { useMemo as
|
|
1465
|
+
import React5, { useMemo as useMemo7, useCallback as useCallback3 } from "react";
|
|
808
1466
|
import {
|
|
809
1467
|
ReactFlow as ReactFlow2,
|
|
810
1468
|
Background as Background2,
|
|
811
1469
|
Controls as Controls2,
|
|
812
1470
|
MiniMap as MiniMap2,
|
|
813
1471
|
useNodesState as useNodesState2,
|
|
814
|
-
useEdgesState as useEdgesState2
|
|
1472
|
+
useEdgesState as useEdgesState2,
|
|
1473
|
+
BackgroundVariant as BackgroundVariant2,
|
|
1474
|
+
MarkerType
|
|
815
1475
|
} from "@xyflow/react";
|
|
816
1476
|
import "@xyflow/react/dist/style.css";
|
|
817
|
-
import { findRootObservables as findRootObservables2 } from "@cyvest/cyvest-js";
|
|
818
1477
|
|
|
819
1478
|
// src/components/InvestigationNode.tsx
|
|
820
|
-
import { memo as memo3 } from "react";
|
|
1479
|
+
import { memo as memo3, useMemo as useMemo5 } from "react";
|
|
821
1480
|
import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
|
|
822
|
-
import { jsx as
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1481
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1482
|
+
var NODE_CONFIG = {
|
|
1483
|
+
root: {
|
|
1484
|
+
minWidth: 140,
|
|
1485
|
+
padding: "10px 18px",
|
|
1486
|
+
borderRadius: 20,
|
|
1487
|
+
fontWeight: 600,
|
|
1488
|
+
fontSize: 13,
|
|
1489
|
+
iconSize: 18,
|
|
1490
|
+
showIcon: true,
|
|
1491
|
+
alignCenter: true
|
|
1492
|
+
},
|
|
1493
|
+
check: {
|
|
1494
|
+
minWidth: 140,
|
|
1495
|
+
padding: "8px 14px",
|
|
1496
|
+
borderRadius: 8,
|
|
1497
|
+
fontWeight: 500,
|
|
1498
|
+
fontSize: 12,
|
|
1499
|
+
iconSize: 14,
|
|
1500
|
+
showIcon: false,
|
|
1501
|
+
// No icon for checks
|
|
1502
|
+
alignCenter: false
|
|
1503
|
+
// Left-aligned
|
|
1504
|
+
},
|
|
1505
|
+
container: {
|
|
1506
|
+
minWidth: 120,
|
|
1507
|
+
padding: "8px 14px",
|
|
1508
|
+
borderRadius: 16,
|
|
1509
|
+
fontWeight: 500,
|
|
1510
|
+
fontSize: 12,
|
|
1511
|
+
iconSize: 16,
|
|
1512
|
+
showIcon: true,
|
|
1513
|
+
alignCenter: true
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
function InvestigationNodeComponent({ data, selected }) {
|
|
827
1517
|
const nodeData = data;
|
|
828
|
-
const {
|
|
829
|
-
label,
|
|
830
|
-
emoji,
|
|
831
|
-
nodeType,
|
|
832
|
-
level,
|
|
833
|
-
description
|
|
834
|
-
} = nodeData;
|
|
1518
|
+
const { label, nodeType, level, description } = nodeData;
|
|
835
1519
|
const borderColor = getLevelColor(level);
|
|
836
1520
|
const backgroundColor = getLevelBackgroundColor(level);
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
minWidth: 120,
|
|
842
|
-
padding: "8px 16px",
|
|
843
|
-
borderRadius: 8,
|
|
844
|
-
fontWeight: 600
|
|
845
|
-
};
|
|
846
|
-
case "check":
|
|
847
|
-
return {
|
|
848
|
-
minWidth: 100,
|
|
849
|
-
padding: "6px 12px",
|
|
850
|
-
borderRadius: 4,
|
|
851
|
-
fontWeight: 400
|
|
852
|
-
};
|
|
853
|
-
case "container":
|
|
854
|
-
return {
|
|
855
|
-
minWidth: 100,
|
|
856
|
-
padding: "6px 12px",
|
|
857
|
-
borderRadius: 12,
|
|
858
|
-
fontWeight: 400
|
|
859
|
-
};
|
|
860
|
-
default:
|
|
861
|
-
return {
|
|
862
|
-
minWidth: 80,
|
|
863
|
-
padding: "6px 12px",
|
|
864
|
-
borderRadius: 4,
|
|
865
|
-
fontWeight: 400
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
const style = getNodeStyle();
|
|
870
|
-
return /* @__PURE__ */ jsxs3(
|
|
871
|
-
"div",
|
|
872
|
-
{
|
|
873
|
-
className: "investigation-node",
|
|
874
|
-
style: {
|
|
875
|
-
...style,
|
|
876
|
-
display: "flex",
|
|
877
|
-
flexDirection: "column",
|
|
878
|
-
alignItems: "center",
|
|
879
|
-
backgroundColor,
|
|
880
|
-
border: `${selected ? 3 : 2}px solid ${borderColor}`,
|
|
881
|
-
cursor: "pointer",
|
|
882
|
-
fontFamily: "system-ui, sans-serif"
|
|
883
|
-
},
|
|
884
|
-
children: [
|
|
885
|
-
/* @__PURE__ */ jsxs3(
|
|
886
|
-
"div",
|
|
887
|
-
{
|
|
888
|
-
style: {
|
|
889
|
-
display: "flex",
|
|
890
|
-
alignItems: "center",
|
|
891
|
-
gap: 6
|
|
892
|
-
},
|
|
893
|
-
children: [
|
|
894
|
-
/* @__PURE__ */ jsx4("span", { style: { fontSize: 14 }, children: emoji }),
|
|
895
|
-
/* @__PURE__ */ jsx4(
|
|
896
|
-
"span",
|
|
897
|
-
{
|
|
898
|
-
style: {
|
|
899
|
-
fontSize: 12,
|
|
900
|
-
fontWeight: style.fontWeight,
|
|
901
|
-
maxWidth: 150,
|
|
902
|
-
overflow: "hidden",
|
|
903
|
-
textOverflow: "ellipsis",
|
|
904
|
-
whiteSpace: "nowrap"
|
|
905
|
-
},
|
|
906
|
-
title: label,
|
|
907
|
-
children: label
|
|
908
|
-
}
|
|
909
|
-
)
|
|
910
|
-
]
|
|
911
|
-
}
|
|
912
|
-
),
|
|
913
|
-
description && /* @__PURE__ */ jsx4(
|
|
914
|
-
"div",
|
|
915
|
-
{
|
|
916
|
-
style: {
|
|
917
|
-
marginTop: 4,
|
|
918
|
-
fontSize: 10,
|
|
919
|
-
color: "#6b7280",
|
|
920
|
-
maxWidth: 140,
|
|
921
|
-
overflow: "hidden",
|
|
922
|
-
textOverflow: "ellipsis",
|
|
923
|
-
whiteSpace: "nowrap"
|
|
924
|
-
},
|
|
925
|
-
title: description,
|
|
926
|
-
children: description
|
|
927
|
-
}
|
|
928
|
-
),
|
|
929
|
-
/* @__PURE__ */ jsx4(
|
|
930
|
-
Handle2,
|
|
931
|
-
{
|
|
932
|
-
type: "target",
|
|
933
|
-
position: Position2.Left,
|
|
934
|
-
style: {
|
|
935
|
-
width: 8,
|
|
936
|
-
height: 8,
|
|
937
|
-
background: borderColor
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
),
|
|
941
|
-
/* @__PURE__ */ jsx4(
|
|
942
|
-
Handle2,
|
|
943
|
-
{
|
|
944
|
-
type: "source",
|
|
945
|
-
position: Position2.Right,
|
|
946
|
-
style: {
|
|
947
|
-
width: 8,
|
|
948
|
-
height: 8,
|
|
949
|
-
background: borderColor
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
)
|
|
953
|
-
]
|
|
954
|
-
}
|
|
1521
|
+
const config = NODE_CONFIG[nodeType] || NODE_CONFIG.check;
|
|
1522
|
+
const IconComponent = useMemo5(
|
|
1523
|
+
() => getInvestigationIcon(nodeType),
|
|
1524
|
+
[nodeType]
|
|
955
1525
|
);
|
|
1526
|
+
const nodeStyle = useMemo5(
|
|
1527
|
+
() => ({
|
|
1528
|
+
minWidth: config.minWidth,
|
|
1529
|
+
padding: config.padding,
|
|
1530
|
+
borderRadius: config.borderRadius,
|
|
1531
|
+
display: "flex",
|
|
1532
|
+
flexDirection: "column",
|
|
1533
|
+
alignItems: config.alignCenter ? "center" : "flex-start",
|
|
1534
|
+
backgroundColor,
|
|
1535
|
+
border: `2px solid ${borderColor}`,
|
|
1536
|
+
boxShadow: selected ? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)` : "0 2px 8px rgba(0,0,0,0.08)",
|
|
1537
|
+
cursor: "pointer",
|
|
1538
|
+
fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
1539
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
|
|
1540
|
+
}),
|
|
1541
|
+
[config, backgroundColor, borderColor, selected]
|
|
1542
|
+
);
|
|
1543
|
+
const headerStyle = useMemo5(
|
|
1544
|
+
() => ({
|
|
1545
|
+
display: "flex",
|
|
1546
|
+
alignItems: "center",
|
|
1547
|
+
gap: 8,
|
|
1548
|
+
width: config.alignCenter ? "auto" : "100%"
|
|
1549
|
+
}),
|
|
1550
|
+
[config.alignCenter]
|
|
1551
|
+
);
|
|
1552
|
+
const labelStyle = useMemo5(
|
|
1553
|
+
() => ({
|
|
1554
|
+
fontSize: config.fontSize,
|
|
1555
|
+
fontWeight: config.fontWeight,
|
|
1556
|
+
maxWidth: 180,
|
|
1557
|
+
overflow: "hidden",
|
|
1558
|
+
textOverflow: "ellipsis",
|
|
1559
|
+
whiteSpace: "nowrap",
|
|
1560
|
+
color: "#1f2937",
|
|
1561
|
+
letterSpacing: "-0.01em"
|
|
1562
|
+
}),
|
|
1563
|
+
[config]
|
|
1564
|
+
);
|
|
1565
|
+
const descriptionStyle = useMemo5(
|
|
1566
|
+
() => ({
|
|
1567
|
+
marginTop: 4,
|
|
1568
|
+
fontSize: 10,
|
|
1569
|
+
color: "#6b7280",
|
|
1570
|
+
maxWidth: 170,
|
|
1571
|
+
overflow: "hidden",
|
|
1572
|
+
textOverflow: "ellipsis",
|
|
1573
|
+
whiteSpace: "nowrap",
|
|
1574
|
+
lineHeight: 1.3,
|
|
1575
|
+
width: "100%",
|
|
1576
|
+
textAlign: config.alignCenter ? "center" : "left"
|
|
1577
|
+
}),
|
|
1578
|
+
[config.alignCenter]
|
|
1579
|
+
);
|
|
1580
|
+
const handleStyle = {
|
|
1581
|
+
width: 1,
|
|
1582
|
+
height: 1,
|
|
1583
|
+
background: "transparent",
|
|
1584
|
+
border: "none",
|
|
1585
|
+
opacity: 0
|
|
1586
|
+
};
|
|
1587
|
+
return /* @__PURE__ */ jsxs4("div", { className: "investigation-node", style: nodeStyle, children: [
|
|
1588
|
+
/* @__PURE__ */ jsxs4("div", { style: headerStyle, children: [
|
|
1589
|
+
config.showIcon && /* @__PURE__ */ jsx5(IconComponent, { size: config.iconSize, color: borderColor }),
|
|
1590
|
+
/* @__PURE__ */ jsx5("span", { style: labelStyle, title: label, children: label })
|
|
1591
|
+
] }),
|
|
1592
|
+
description && /* @__PURE__ */ jsx5("div", { style: descriptionStyle, title: description, children: description }),
|
|
1593
|
+
/* @__PURE__ */ jsx5(Handle2, { type: "target", position: Position2.Left, style: handleStyle }),
|
|
1594
|
+
/* @__PURE__ */ jsx5(Handle2, { type: "source", position: Position2.Right, style: handleStyle })
|
|
1595
|
+
] });
|
|
956
1596
|
}
|
|
957
1597
|
var InvestigationNode = memo3(InvestigationNodeComponent);
|
|
958
1598
|
|
|
959
1599
|
// src/hooks/useDagreLayout.ts
|
|
960
|
-
import { useMemo as
|
|
1600
|
+
import { useMemo as useMemo6 } from "react";
|
|
961
1601
|
import Dagre from "@dagrejs/dagre";
|
|
962
1602
|
var DEFAULT_OPTIONS = {
|
|
963
1603
|
direction: "LR",
|
|
@@ -1003,10 +1643,23 @@ function computeDagreLayout(nodes, edges, options = {}) {
|
|
|
1003
1643
|
}
|
|
1004
1644
|
|
|
1005
1645
|
// src/components/InvestigationGraph.tsx
|
|
1006
|
-
import { jsx as
|
|
1646
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1007
1647
|
var nodeTypes2 = {
|
|
1008
1648
|
investigation: InvestigationNode
|
|
1009
1649
|
};
|
|
1650
|
+
var defaultEdgeOptions2 = {
|
|
1651
|
+
type: "smoothstep",
|
|
1652
|
+
style: {
|
|
1653
|
+
stroke: "#94a3b8",
|
|
1654
|
+
strokeWidth: 1.5
|
|
1655
|
+
},
|
|
1656
|
+
markerEnd: {
|
|
1657
|
+
type: MarkerType.ArrowClosed,
|
|
1658
|
+
width: 16,
|
|
1659
|
+
height: 16,
|
|
1660
|
+
color: "#94a3b8"
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1010
1663
|
function flattenContainers(containers) {
|
|
1011
1664
|
const result = [];
|
|
1012
1665
|
for (const container of Object.values(containers)) {
|
|
@@ -1020,24 +1673,36 @@ function flattenContainers(containers) {
|
|
|
1020
1673
|
function createInvestigationGraph(investigation) {
|
|
1021
1674
|
const nodes = [];
|
|
1022
1675
|
const edges = [];
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1676
|
+
const rootType = investigation.data_extraction.root_type;
|
|
1677
|
+
const normalizedRootType = rootType?.toLowerCase().trim();
|
|
1678
|
+
const rootsByType = normalizedRootType ? Object.values(investigation.observables).filter(
|
|
1679
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
1680
|
+
) : [];
|
|
1681
|
+
const primaryRoot = rootsByType[0] ?? null;
|
|
1682
|
+
const rootKey = primaryRoot?.key ?? investigation.investigation_id;
|
|
1683
|
+
const rootValue = primaryRoot?.value ?? investigation.investigation_name ?? investigation.investigation_id;
|
|
1027
1684
|
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
1028
1685
|
const rootNodeData = {
|
|
1029
1686
|
label: truncateLabel(rootValue, 24),
|
|
1030
1687
|
nodeType: "root",
|
|
1031
1688
|
level: rootLevel,
|
|
1032
|
-
score: primaryRoot?.score ?? investigation.score
|
|
1033
|
-
emoji: getInvestigationNodeEmoji("root")
|
|
1689
|
+
score: primaryRoot?.score ?? investigation.score
|
|
1034
1690
|
};
|
|
1035
1691
|
nodes.push({
|
|
1036
1692
|
id: rootKey,
|
|
1037
1693
|
type: "investigation",
|
|
1038
1694
|
position: { x: 0, y: 0 },
|
|
1039
|
-
data: rootNodeData
|
|
1695
|
+
data: rootNodeData,
|
|
1696
|
+
selectable: true,
|
|
1697
|
+
draggable: true
|
|
1040
1698
|
});
|
|
1699
|
+
const allContainers = flattenContainers(investigation.containers);
|
|
1700
|
+
const checksInContainers = /* @__PURE__ */ new Set();
|
|
1701
|
+
for (const container of allContainers) {
|
|
1702
|
+
for (const checkKey of container.checks) {
|
|
1703
|
+
checksInContainers.add(checkKey);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1041
1706
|
const allChecks = [];
|
|
1042
1707
|
for (const checksForKey of Object.values(investigation.checks)) {
|
|
1043
1708
|
allChecks.push(...checksForKey);
|
|
@@ -1051,43 +1716,51 @@ function createInvestigationGraph(investigation) {
|
|
|
1051
1716
|
nodeType: "check",
|
|
1052
1717
|
level: check.level,
|
|
1053
1718
|
score: check.score,
|
|
1054
|
-
description: truncateLabel(check.description, 30)
|
|
1055
|
-
emoji: getInvestigationNodeEmoji("check")
|
|
1719
|
+
description: truncateLabel(check.description, 30)
|
|
1056
1720
|
};
|
|
1057
1721
|
nodes.push({
|
|
1058
1722
|
id: `check-${check.key}`,
|
|
1059
1723
|
type: "investigation",
|
|
1060
1724
|
position: { x: 0, y: 0 },
|
|
1061
|
-
data: checkNodeData
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
id: `edge-root-${check.key}`,
|
|
1065
|
-
source: rootKey,
|
|
1066
|
-
target: `check-${check.key}`,
|
|
1067
|
-
type: "default"
|
|
1725
|
+
data: checkNodeData,
|
|
1726
|
+
selectable: true,
|
|
1727
|
+
draggable: true
|
|
1068
1728
|
});
|
|
1729
|
+
if (!checksInContainers.has(check.key)) {
|
|
1730
|
+
edges.push({
|
|
1731
|
+
id: `edge-root-${check.key}`,
|
|
1732
|
+
source: rootKey,
|
|
1733
|
+
target: `check-${check.key}`,
|
|
1734
|
+
type: "smoothstep",
|
|
1735
|
+
animated: false
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1069
1738
|
}
|
|
1070
|
-
const allContainers = flattenContainers(investigation.containers);
|
|
1071
1739
|
for (const container of allContainers) {
|
|
1072
1740
|
const containerNodeData = {
|
|
1073
|
-
label: truncateLabel(
|
|
1741
|
+
label: truncateLabel(
|
|
1742
|
+
container.path.split("/").pop() ?? container.path,
|
|
1743
|
+
20
|
|
1744
|
+
),
|
|
1074
1745
|
nodeType: "container",
|
|
1075
1746
|
level: container.aggregated_level,
|
|
1076
1747
|
score: container.aggregated_score,
|
|
1077
|
-
path: container.path
|
|
1078
|
-
emoji: getInvestigationNodeEmoji("container")
|
|
1748
|
+
path: container.path
|
|
1079
1749
|
};
|
|
1080
1750
|
nodes.push({
|
|
1081
1751
|
id: `container-${container.key}`,
|
|
1082
1752
|
type: "investigation",
|
|
1083
1753
|
position: { x: 0, y: 0 },
|
|
1084
|
-
data: containerNodeData
|
|
1754
|
+
data: containerNodeData,
|
|
1755
|
+
selectable: true,
|
|
1756
|
+
draggable: true
|
|
1085
1757
|
});
|
|
1086
1758
|
edges.push({
|
|
1087
1759
|
id: `edge-root-container-${container.key}`,
|
|
1088
1760
|
source: rootKey,
|
|
1089
1761
|
target: `container-${container.key}`,
|
|
1090
|
-
type: "
|
|
1762
|
+
type: "smoothstep",
|
|
1763
|
+
animated: false
|
|
1091
1764
|
});
|
|
1092
1765
|
for (const checkKey of container.checks) {
|
|
1093
1766
|
if (seenCheckIds.has(checkKey)) {
|
|
@@ -1095,8 +1768,8 @@ function createInvestigationGraph(investigation) {
|
|
|
1095
1768
|
id: `edge-container-check-${container.key}-${checkKey}`,
|
|
1096
1769
|
source: `container-${container.key}`,
|
|
1097
1770
|
target: `check-${checkKey}`,
|
|
1098
|
-
type: "
|
|
1099
|
-
|
|
1771
|
+
type: "smoothstep",
|
|
1772
|
+
animated: false
|
|
1100
1773
|
});
|
|
1101
1774
|
}
|
|
1102
1775
|
}
|
|
@@ -1110,15 +1783,15 @@ var InvestigationGraph = ({
|
|
|
1110
1783
|
onNodeClick,
|
|
1111
1784
|
className
|
|
1112
1785
|
}) => {
|
|
1113
|
-
const { initialNodes, initialEdges } =
|
|
1786
|
+
const { initialNodes, initialEdges } = useMemo7(() => {
|
|
1114
1787
|
const { nodes: nodes2, edges: edges2 } = createInvestigationGraph(investigation);
|
|
1115
1788
|
return { initialNodes: nodes2, initialEdges: edges2 };
|
|
1116
1789
|
}, [investigation]);
|
|
1117
|
-
const { nodes: layoutNodes, edges: layoutEdges } =
|
|
1790
|
+
const { nodes: layoutNodes, edges: layoutEdges } = useMemo7(() => {
|
|
1118
1791
|
return computeDagreLayout(initialNodes, initialEdges, {
|
|
1119
1792
|
direction: "LR",
|
|
1120
|
-
nodeSpacing:
|
|
1121
|
-
rankSpacing:
|
|
1793
|
+
nodeSpacing: 40,
|
|
1794
|
+
rankSpacing: 140
|
|
1122
1795
|
});
|
|
1123
1796
|
}, [initialNodes, initialEdges]);
|
|
1124
1797
|
const [nodes, setNodes, onNodesChange] = useNodesState2(layoutNodes);
|
|
@@ -1138,97 +1811,200 @@ var InvestigationGraph = ({
|
|
|
1138
1811
|
const data = node.data;
|
|
1139
1812
|
return getLevelColor(data.level);
|
|
1140
1813
|
}, []);
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
},
|
|
1150
|
-
children: /* @__PURE__ */ jsxs4(
|
|
1151
|
-
ReactFlow2,
|
|
1152
|
-
{
|
|
1153
|
-
nodes,
|
|
1154
|
-
edges,
|
|
1155
|
-
onNodesChange,
|
|
1156
|
-
onEdgesChange,
|
|
1157
|
-
onNodeClick: handleNodeClick,
|
|
1158
|
-
nodeTypes: nodeTypes2,
|
|
1159
|
-
fitView: true,
|
|
1160
|
-
fitViewOptions: { padding: 0.2 },
|
|
1161
|
-
minZoom: 0.1,
|
|
1162
|
-
maxZoom: 2,
|
|
1163
|
-
proOptions: { hideAttribution: true },
|
|
1164
|
-
children: [
|
|
1165
|
-
/* @__PURE__ */ jsx5(Background2, {}),
|
|
1166
|
-
/* @__PURE__ */ jsx5(Controls2, {}),
|
|
1167
|
-
/* @__PURE__ */ jsx5(MiniMap2, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
|
|
1168
|
-
]
|
|
1169
|
-
}
|
|
1170
|
-
)
|
|
1171
|
-
}
|
|
1814
|
+
const containerStyle = useMemo7(
|
|
1815
|
+
() => ({
|
|
1816
|
+
width,
|
|
1817
|
+
height,
|
|
1818
|
+
position: "relative",
|
|
1819
|
+
background: "linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%)"
|
|
1820
|
+
}),
|
|
1821
|
+
[width, height]
|
|
1172
1822
|
);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
// src/components/CyvestGraph.tsx
|
|
1176
|
-
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1177
|
-
var ViewToggle = ({ currentView, onChange }) => {
|
|
1178
|
-
return /* @__PURE__ */ jsxs5(
|
|
1179
|
-
"div",
|
|
1823
|
+
return /* @__PURE__ */ jsx6("div", { className, style: containerStyle, children: /* @__PURE__ */ jsxs5(
|
|
1824
|
+
ReactFlow2,
|
|
1180
1825
|
{
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1826
|
+
nodes,
|
|
1827
|
+
edges,
|
|
1828
|
+
onNodesChange,
|
|
1829
|
+
onEdgesChange,
|
|
1830
|
+
onNodeClick: handleNodeClick,
|
|
1831
|
+
nodeTypes: nodeTypes2,
|
|
1832
|
+
defaultEdgeOptions: defaultEdgeOptions2,
|
|
1833
|
+
fitView: true,
|
|
1834
|
+
fitViewOptions: { padding: 0.3, maxZoom: 1.5 },
|
|
1835
|
+
minZoom: 0.1,
|
|
1836
|
+
maxZoom: 2.5,
|
|
1837
|
+
proOptions: { hideAttribution: true },
|
|
1838
|
+
nodesDraggable: true,
|
|
1839
|
+
nodesConnectable: false,
|
|
1840
|
+
elementsSelectable: true,
|
|
1841
|
+
selectNodesOnDrag: false,
|
|
1842
|
+
panOnDrag: true,
|
|
1843
|
+
zoomOnScroll: true,
|
|
1844
|
+
zoomOnPinch: true,
|
|
1845
|
+
panOnScroll: false,
|
|
1194
1846
|
children: [
|
|
1195
1847
|
/* @__PURE__ */ jsx6(
|
|
1196
|
-
|
|
1848
|
+
Background2,
|
|
1849
|
+
{
|
|
1850
|
+
variant: BackgroundVariant2.Dots,
|
|
1851
|
+
gap: 24,
|
|
1852
|
+
size: 1,
|
|
1853
|
+
color: "#d1d5db"
|
|
1854
|
+
}
|
|
1855
|
+
),
|
|
1856
|
+
/* @__PURE__ */ jsx6(
|
|
1857
|
+
Controls2,
|
|
1197
1858
|
{
|
|
1198
|
-
|
|
1859
|
+
showInteractive: false,
|
|
1199
1860
|
style: {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
fontSize: 12,
|
|
1205
|
-
fontWeight: currentView === "observables" ? 600 : 400,
|
|
1206
|
-
background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
|
|
1207
|
-
color: currentView === "observables" ? "white" : "#374151"
|
|
1208
|
-
},
|
|
1209
|
-
children: "Observables"
|
|
1861
|
+
borderRadius: 10,
|
|
1862
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
1863
|
+
border: "1px solid rgba(0,0,0,0.06)"
|
|
1864
|
+
}
|
|
1210
1865
|
}
|
|
1211
1866
|
),
|
|
1212
1867
|
/* @__PURE__ */ jsx6(
|
|
1213
|
-
|
|
1868
|
+
MiniMap2,
|
|
1214
1869
|
{
|
|
1215
|
-
|
|
1870
|
+
nodeColor: miniMapNodeColor,
|
|
1871
|
+
zoomable: true,
|
|
1872
|
+
pannable: true,
|
|
1216
1873
|
style: {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
fontSize: 12,
|
|
1222
|
-
fontWeight: currentView === "investigation" ? 600 : 400,
|
|
1223
|
-
background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
|
|
1224
|
-
color: currentView === "investigation" ? "white" : "#374151"
|
|
1874
|
+
borderRadius: 10,
|
|
1875
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
1876
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
1877
|
+
background: "rgba(255,255,255,0.9)"
|
|
1225
1878
|
},
|
|
1226
|
-
|
|
1879
|
+
maskColor: "rgba(0,0,0,0.08)"
|
|
1227
1880
|
}
|
|
1228
1881
|
)
|
|
1229
1882
|
]
|
|
1230
1883
|
}
|
|
1884
|
+
) });
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// src/components/CyvestGraph.tsx
|
|
1888
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1889
|
+
var ViewToggle = ({ currentView, onChange }) => {
|
|
1890
|
+
const containerStyle = useMemo8(
|
|
1891
|
+
() => ({
|
|
1892
|
+
position: "absolute",
|
|
1893
|
+
top: 12,
|
|
1894
|
+
left: 12,
|
|
1895
|
+
display: "flex",
|
|
1896
|
+
gap: 2,
|
|
1897
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
1898
|
+
backdropFilter: "blur(8px)",
|
|
1899
|
+
padding: 4,
|
|
1900
|
+
borderRadius: 10,
|
|
1901
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
1902
|
+
zIndex: 10,
|
|
1903
|
+
fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
1904
|
+
border: "1px solid rgba(0,0,0,0.06)"
|
|
1905
|
+
}),
|
|
1906
|
+
[]
|
|
1907
|
+
);
|
|
1908
|
+
const getButtonStyle = useCallback4(
|
|
1909
|
+
(isActive) => ({
|
|
1910
|
+
padding: "8px 14px",
|
|
1911
|
+
border: "none",
|
|
1912
|
+
borderRadius: 7,
|
|
1913
|
+
cursor: "pointer",
|
|
1914
|
+
fontSize: 12,
|
|
1915
|
+
fontWeight: isActive ? 600 : 500,
|
|
1916
|
+
background: isActive ? "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)" : "transparent",
|
|
1917
|
+
color: isActive ? "white" : "#4b5563",
|
|
1918
|
+
transition: "all 0.15s ease",
|
|
1919
|
+
letterSpacing: "-0.01em"
|
|
1920
|
+
}),
|
|
1921
|
+
[]
|
|
1231
1922
|
);
|
|
1923
|
+
return /* @__PURE__ */ jsxs6("div", { style: containerStyle, children: [
|
|
1924
|
+
/* @__PURE__ */ jsx7(
|
|
1925
|
+
"button",
|
|
1926
|
+
{
|
|
1927
|
+
onClick: () => onChange("observables"),
|
|
1928
|
+
style: getButtonStyle(currentView === "observables"),
|
|
1929
|
+
onMouseEnter: (e) => {
|
|
1930
|
+
if (currentView !== "observables") {
|
|
1931
|
+
e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
|
|
1932
|
+
e.currentTarget.style.color = "#3b82f6";
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
onMouseLeave: (e) => {
|
|
1936
|
+
if (currentView !== "observables") {
|
|
1937
|
+
e.currentTarget.style.background = "transparent";
|
|
1938
|
+
e.currentTarget.style.color = "#4b5563";
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
children: /* @__PURE__ */ jsxs6("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
|
|
1942
|
+
/* @__PURE__ */ jsxs6(
|
|
1943
|
+
"svg",
|
|
1944
|
+
{
|
|
1945
|
+
width: "14",
|
|
1946
|
+
height: "14",
|
|
1947
|
+
viewBox: "0 0 24 24",
|
|
1948
|
+
fill: "none",
|
|
1949
|
+
stroke: "currentColor",
|
|
1950
|
+
strokeWidth: "2",
|
|
1951
|
+
strokeLinecap: "round",
|
|
1952
|
+
strokeLinejoin: "round",
|
|
1953
|
+
children: [
|
|
1954
|
+
/* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "3" }),
|
|
1955
|
+
/* @__PURE__ */ jsx7("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1956
|
+
/* @__PURE__ */ jsx7("line", { x1: "12", y1: "2", x2: "12", y2: "4" }),
|
|
1957
|
+
/* @__PURE__ */ jsx7("line", { x1: "12", y1: "20", x2: "12", y2: "22" }),
|
|
1958
|
+
/* @__PURE__ */ jsx7("line", { x1: "2", y1: "12", x2: "4", y2: "12" }),
|
|
1959
|
+
/* @__PURE__ */ jsx7("line", { x1: "20", y1: "12", x2: "22", y2: "12" })
|
|
1960
|
+
]
|
|
1961
|
+
}
|
|
1962
|
+
),
|
|
1963
|
+
"Observables"
|
|
1964
|
+
] })
|
|
1965
|
+
}
|
|
1966
|
+
),
|
|
1967
|
+
/* @__PURE__ */ jsx7(
|
|
1968
|
+
"button",
|
|
1969
|
+
{
|
|
1970
|
+
onClick: () => onChange("investigation"),
|
|
1971
|
+
style: getButtonStyle(currentView === "investigation"),
|
|
1972
|
+
onMouseEnter: (e) => {
|
|
1973
|
+
if (currentView !== "investigation") {
|
|
1974
|
+
e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
|
|
1975
|
+
e.currentTarget.style.color = "#3b82f6";
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
onMouseLeave: (e) => {
|
|
1979
|
+
if (currentView !== "investigation") {
|
|
1980
|
+
e.currentTarget.style.background = "transparent";
|
|
1981
|
+
e.currentTarget.style.color = "#4b5563";
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
children: /* @__PURE__ */ jsxs6("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
|
|
1985
|
+
/* @__PURE__ */ jsxs6(
|
|
1986
|
+
"svg",
|
|
1987
|
+
{
|
|
1988
|
+
width: "14",
|
|
1989
|
+
height: "14",
|
|
1990
|
+
viewBox: "0 0 24 24",
|
|
1991
|
+
fill: "none",
|
|
1992
|
+
stroke: "currentColor",
|
|
1993
|
+
strokeWidth: "2",
|
|
1994
|
+
strokeLinecap: "round",
|
|
1995
|
+
strokeLinejoin: "round",
|
|
1996
|
+
children: [
|
|
1997
|
+
/* @__PURE__ */ jsx7("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
1998
|
+
/* @__PURE__ */ jsx7("path", { d: "M9 3v18" }),
|
|
1999
|
+
/* @__PURE__ */ jsx7("path", { d: "M3 9h18" })
|
|
2000
|
+
]
|
|
2001
|
+
}
|
|
2002
|
+
),
|
|
2003
|
+
"Investigation"
|
|
2004
|
+
] })
|
|
2005
|
+
}
|
|
2006
|
+
)
|
|
2007
|
+
] });
|
|
1232
2008
|
};
|
|
1233
2009
|
var CyvestGraph = ({
|
|
1234
2010
|
investigation,
|
|
@@ -1239,46 +2015,52 @@ var CyvestGraph = ({
|
|
|
1239
2015
|
className,
|
|
1240
2016
|
showViewToggle = true
|
|
1241
2017
|
}) => {
|
|
1242
|
-
const [view, setView] = useState2(
|
|
2018
|
+
const [view, setView] = useState2(
|
|
2019
|
+
initialView
|
|
2020
|
+
);
|
|
1243
2021
|
const handleNodeClick = useCallback4(
|
|
1244
2022
|
(nodeId, _nodeType) => {
|
|
1245
2023
|
onNodeClick?.(nodeId);
|
|
1246
2024
|
},
|
|
1247
2025
|
[onNodeClick]
|
|
1248
2026
|
);
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
position: "relative"
|
|
1257
|
-
},
|
|
1258
|
-
children: [
|
|
1259
|
-
showViewToggle && /* @__PURE__ */ jsx6(ViewToggle, { currentView: view, onChange: setView }),
|
|
1260
|
-
view === "observables" ? /* @__PURE__ */ jsx6(
|
|
1261
|
-
ObservablesGraph,
|
|
1262
|
-
{
|
|
1263
|
-
investigation,
|
|
1264
|
-
height: "100%",
|
|
1265
|
-
width: "100%",
|
|
1266
|
-
onNodeClick: handleNodeClick,
|
|
1267
|
-
showControls: true
|
|
1268
|
-
}
|
|
1269
|
-
) : /* @__PURE__ */ jsx6(
|
|
1270
|
-
InvestigationGraph,
|
|
1271
|
-
{
|
|
1272
|
-
investigation,
|
|
1273
|
-
height: "100%",
|
|
1274
|
-
width: "100%",
|
|
1275
|
-
onNodeClick: handleNodeClick
|
|
1276
|
-
}
|
|
1277
|
-
)
|
|
1278
|
-
]
|
|
1279
|
-
}
|
|
2027
|
+
const containerStyle = useMemo8(
|
|
2028
|
+
() => ({
|
|
2029
|
+
width,
|
|
2030
|
+
height,
|
|
2031
|
+
position: "relative"
|
|
2032
|
+
}),
|
|
2033
|
+
[width, height]
|
|
1280
2034
|
);
|
|
2035
|
+
return /* @__PURE__ */ jsxs6("div", { className, style: containerStyle, children: [
|
|
2036
|
+
showViewToggle && /* @__PURE__ */ jsx7(ViewToggle, { currentView: view, onChange: setView }),
|
|
2037
|
+
view === "observables" ? /* @__PURE__ */ jsx7(
|
|
2038
|
+
ObservablesGraph,
|
|
2039
|
+
{
|
|
2040
|
+
investigation,
|
|
2041
|
+
height: "100%",
|
|
2042
|
+
width: "100%",
|
|
2043
|
+
onNodeClick: handleNodeClick,
|
|
2044
|
+
showControls: true
|
|
2045
|
+
}
|
|
2046
|
+
) : /* @__PURE__ */ jsx7(
|
|
2047
|
+
InvestigationGraph,
|
|
2048
|
+
{
|
|
2049
|
+
investigation,
|
|
2050
|
+
height: "100%",
|
|
2051
|
+
width: "100%",
|
|
2052
|
+
onNodeClick: handleNodeClick
|
|
2053
|
+
}
|
|
2054
|
+
)
|
|
2055
|
+
] });
|
|
1281
2056
|
};
|
|
1282
2057
|
export {
|
|
1283
|
-
CyvestGraph
|
|
2058
|
+
CyvestGraph,
|
|
2059
|
+
DEFAULT_FORCE_CONFIG,
|
|
2060
|
+
INVESTIGATION_ICON_MAP,
|
|
2061
|
+
InvestigationGraph,
|
|
2062
|
+
OBSERVABLE_ICON_MAP,
|
|
2063
|
+
ObservablesGraph,
|
|
2064
|
+
getInvestigationIcon,
|
|
2065
|
+
getObservableIcon
|
|
1284
2066
|
};
|