@cyvest/cyvest-vis 4.0.0 → 4.2.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/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 useMemo2, useCallback as useCallback2, useState } from "react";
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,7 +11,9 @@ 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
19
  import { getObservableGraph } from "@cyvest/cyvest-js";
@@ -21,77 +23,16 @@ var DEFAULT_FORCE_CONFIG = {
21
23
  chargeStrength: -200,
22
24
  linkDistance: 80,
23
25
  centerStrength: 0.05,
24
- collisionRadius: 40,
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
35
  import { getColorForLevel } from "@cyvest/cyvest-js";
34
- var OBSERVABLE_EMOJI_MAP = {
35
- // Network
36
- "ipv4-addr": "\u{1F310}",
37
- "ipv6-addr": "\u{1F310}",
38
- "domain-name": "\u{1F3E0}",
39
- url: "\u{1F517}",
40
- "autonomous-system": "\u{1F30D}",
41
- "mac-addr": "\u{1F4F6}",
42
- // Email
43
- "email-addr": "\u{1F4E7}",
44
- "email-message": "\u2709\uFE0F",
45
- // File
46
- file: "\u{1F4C4}",
47
- "file-hash": "\u{1F510}",
48
- "file:hash:md5": "\u{1F510}",
49
- "file:hash:sha1": "\u{1F510}",
50
- "file:hash:sha256": "\u{1F510}",
51
- // User/Identity
52
- user: "\u{1F464}",
53
- "user-account": "\u{1F464}",
54
- identity: "\u{1FAAA}",
55
- // Process/System
56
- process: "\u2699\uFE0F",
57
- software: "\u{1F4BF}",
58
- "windows-registry-key": "\u{1F4DD}",
59
- // Threat Intelligence
60
- "threat-actor": "\u{1F479}",
61
- malware: "\u{1F9A0}",
62
- "attack-pattern": "\u2694\uFE0F",
63
- campaign: "\u{1F3AF}",
64
- indicator: "\u{1F6A8}",
65
- // Artifacts
66
- artifact: "\u{1F9EA}",
67
- certificate: "\u{1F4DC}",
68
- "x509-certificate": "\u{1F4DC}",
69
- // Default
70
- unknown: "\u2753"
71
- };
72
- function getObservableEmoji(observableType) {
73
- const normalized = observableType.toLowerCase().trim();
74
- return OBSERVABLE_EMOJI_MAP[normalized] ?? OBSERVABLE_EMOJI_MAP.unknown;
75
- }
76
- var OBSERVABLE_SHAPE_MAP = {
77
- // Domains get squares
78
- "domain-name": "square",
79
- // URLs get circles
80
- url: "circle",
81
- // IPs get triangles
82
- "ipv4-addr": "triangle",
83
- "ipv6-addr": "triangle",
84
- // Root/files get rectangles (default for root)
85
- file: "rectangle",
86
- "email-message": "rectangle"
87
- };
88
- function getObservableShape(observableType, isRoot) {
89
- if (isRoot) {
90
- return "rectangle";
91
- }
92
- const normalized = observableType.toLowerCase().trim();
93
- return OBSERVABLE_SHAPE_MAP[normalized] ?? "circle";
94
- }
95
36
  function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
96
37
  if (value.length <= maxLength) {
97
38
  return value;
@@ -118,178 +59,761 @@ function lightenHexColor(hex, amount) {
118
59
  return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
119
60
  }
120
61
  function getLevelBackgroundColor(level) {
121
- return lightenHexColor(getLevelColor(level), 0.85);
62
+ return lightenHexColor(getLevelColor(level), 0.88);
122
63
  }
123
- var INVESTIGATION_NODE_EMOJI = {
124
- root: "\u{1F3AF}",
125
- check: "\u2705",
126
- container: "\u{1F4E6}"
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
127
688
  };
128
- function getInvestigationNodeEmoji(nodeType) {
129
- return INVESTIGATION_NODE_EMOJI[nodeType] ?? "\u2753";
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;
130
695
  }
131
696
 
132
697
  // src/components/ObservableNode.tsx
133
- import { jsx, jsxs } from "react/jsx-runtime";
134
- var NODE_SIZE = 28;
135
- var ROOT_NODE_SIZE = 36;
136
- function ObservableNodeComponent({
137
- data,
138
- selected
139
- }) {
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 }) {
140
745
  const nodeData = data;
141
- const {
142
- label,
143
- emoji,
144
- shape,
145
- level,
146
- isRoot,
147
- whitelisted,
148
- fullValue
149
- } = nodeData;
150
- const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
746
+ const { label, level, isRoot, whitelisted, fullValue, observableType } = nodeData;
151
747
  const borderColor = getLevelColor(level);
152
748
  const backgroundColor = getLevelBackgroundColor(level);
153
- const getShapeStyle = () => {
154
- const baseStyle = {
155
- width: size,
156
- height: size,
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%",
157
773
  display: "flex",
158
774
  alignItems: "center",
159
775
  justifyContent: "center",
160
776
  backgroundColor,
161
- border: `${selected ? 3 : 2}px solid ${borderColor}`,
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)",
162
779
  opacity: whitelisted ? 0.5 : 1,
163
- fontSize: isRoot ? 14 : 12
780
+ transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
164
781
  };
165
- switch (shape) {
166
- case "square":
167
- return { ...baseStyle, borderRadius: 4 };
168
- case "circle":
169
- return { ...baseStyle, borderRadius: "50%" };
170
- case "triangle":
171
- return {
172
- ...baseStyle,
173
- borderRadius: 0,
174
- border: "none",
175
- background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
176
- clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
177
- position: "relative"
178
- };
179
- case "rectangle":
180
- default:
181
- return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
182
- }
183
- };
184
- const isTriangle = shape === "triangle";
185
- return /* @__PURE__ */ jsxs(
186
- "div",
187
- {
188
- className: "observable-node",
189
- style: {
190
- display: "flex",
191
- flexDirection: "column",
192
- alignItems: "center",
193
- cursor: "pointer"
194
- },
195
- children: [
196
- /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
197
- isTriangle ? (
198
- // Triangle using SVG
199
- /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 100 100", children: [
200
- /* @__PURE__ */ jsx(
201
- "polygon",
202
- {
203
- points: "50,10 90,90 10,90",
204
- fill: backgroundColor,
205
- stroke: borderColor,
206
- strokeWidth: selected ? 6 : 4,
207
- opacity: whitelisted ? 0.5 : 1
208
- }
209
- ),
210
- /* @__PURE__ */ jsx(
211
- "text",
212
- {
213
- x: "50",
214
- y: "65",
215
- textAnchor: "middle",
216
- fontSize: "32",
217
- dominantBaseline: "middle",
218
- children: emoji
219
- }
220
- )
221
- ] })
222
- ) : (
223
- // Other shapes using CSS
224
- /* @__PURE__ */ jsx("div", { style: getShapeStyle(), children: /* @__PURE__ */ jsx("span", { style: { userSelect: "none" }, children: emoji }) })
225
- ),
226
- /* @__PURE__ */ jsx(
227
- Handle,
228
- {
229
- type: "source",
230
- position: Position.Right,
231
- id: "source",
232
- style: {
233
- position: "absolute",
234
- top: "50%",
235
- left: "50%",
236
- transform: "translate(-50%, -50%)",
237
- width: 1,
238
- height: 1,
239
- background: "transparent",
240
- border: "none",
241
- opacity: 0
242
- }
243
- }
244
- ),
245
- /* @__PURE__ */ jsx(
246
- Handle,
247
- {
248
- type: "target",
249
- position: Position.Left,
250
- id: "target",
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
- ] }),
265
- /* @__PURE__ */ jsx(
266
- "div",
267
- {
268
- style: {
269
- marginTop: 2,
270
- fontSize: 9,
271
- maxWidth: 70,
272
- textAlign: "center",
273
- overflow: "hidden",
274
- textOverflow: "ellipsis",
275
- whiteSpace: "nowrap",
276
- color: "#374151",
277
- fontFamily: "system-ui, sans-serif"
278
- },
279
- title: fullValue,
280
- children: label
281
- }
282
- )
283
- ]
284
- }
782
+ }, [isRoot, backgroundColor, borderColor, selected, whitelisted]);
783
+ const labelStyle = useMemo(
784
+ () => ({
785
+ ...nodeStyles.label,
786
+ color: whitelisted ? "#9ca3af" : "#374151"
787
+ }),
788
+ [whitelisted]
285
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
+ ] });
286
804
  }
287
805
  var ObservableNode = memo(ObservableNodeComponent);
288
806
 
289
807
  // src/components/FloatingEdge.tsx
290
- import { memo as memo2 } from "react";
291
- import { BaseEdge, getStraightPath } from "@xyflow/react";
292
- import { jsx as jsx2 } from "react/jsx-runtime";
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
+ }
293
817
  function FloatingEdgeComponent({
294
818
  id,
295
819
  sourceX,
@@ -297,24 +821,35 @@ function FloatingEdgeComponent({
297
821
  targetX,
298
822
  targetY,
299
823
  style,
300
- markerEnd
824
+ markerEnd,
825
+ selected
301
826
  }) {
302
- const [edgePath] = getStraightPath({
827
+ const offset = useMemo2(
828
+ () => getControlOffset(sourceX, sourceY, targetX, targetY),
829
+ [sourceX, sourceY, targetX, targetY]
830
+ );
831
+ const [edgePath] = getBezierPath({
303
832
  sourceX,
304
833
  sourceY,
305
834
  targetX,
306
- targetY
835
+ targetY,
836
+ curvature: 0.15
307
837
  });
308
- return /* @__PURE__ */ jsx2(
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(
309
848
  BaseEdge,
310
849
  {
311
850
  id,
312
851
  path: edgePath,
313
- style: {
314
- strokeWidth: 1.5,
315
- stroke: "#94a3b8",
316
- ...style
317
- },
852
+ style: edgeStyle,
318
853
  markerEnd
319
854
  }
320
855
  );
@@ -322,7 +857,7 @@ function FloatingEdgeComponent({
322
857
  var FloatingEdge = memo2(FloatingEdgeComponent);
323
858
 
324
859
  // src/hooks/useForceLayout.ts
325
- import { useEffect, useRef, useCallback, useMemo } from "react";
860
+ import { useEffect, useRef, useCallback, useMemo as useMemo3 } from "react";
326
861
  import {
327
862
  forceSimulation,
328
863
  forceLink,
@@ -337,31 +872,42 @@ import {
337
872
  useNodesInitialized,
338
873
  useStore
339
874
  } from "@xyflow/react";
340
- var nodeCountSelector = (state) => state.nodeLookup.size;
875
+ var nodeIdsSelector = (state) => {
876
+ const ids = Array.from(state.nodeLookup.keys()).sort();
877
+ return ids.join(",");
878
+ };
341
879
  function useForceLayout(config = {}, rootNodeId) {
342
880
  const { getNodes, getEdges, setNodes } = useReactFlow();
343
881
  const nodesInitialized = useNodesInitialized();
344
- const nodeCount = useStore(nodeCountSelector);
345
- const forceConfig = useMemo(
882
+ const nodeIds = useStore(nodeIdsSelector);
883
+ const forceConfig = useMemo3(
346
884
  () => ({ ...DEFAULT_FORCE_CONFIG, ...config }),
347
885
  [config]
348
886
  );
349
887
  const simulationRef = useRef(null);
350
- const draggingNodeRef = useRef(null);
888
+ const draggingRef = useRef({ nodeId: null, active: false });
889
+ const nodePositionsRef = useRef(
890
+ /* @__PURE__ */ new Map()
891
+ );
892
+ const rafRef = useRef(null);
351
893
  useEffect(() => {
352
- if (!nodesInitialized || nodeCount === 0) {
894
+ if (!nodesInitialized || !nodeIds) {
353
895
  return;
354
896
  }
355
897
  const nodes = getNodes();
356
898
  const edges = getEdges();
899
+ if (nodes.length === 0) {
900
+ return;
901
+ }
357
902
  const simNodes = nodes.map((node) => {
358
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;
359
906
  return {
360
907
  id: node.id,
361
- // Use existing simulation position or node position
362
- x: existingNode?.x ?? node.position.x ?? Math.random() * 500 - 250,
363
- y: existingNode?.y ?? node.position.y ?? Math.random() * 500 - 250,
364
- // Preserve fixed positions for dragged nodes
908
+ x,
909
+ y,
910
+ // Preserve fixed positions for dragged nodes or root
365
911
  fx: existingNode?.fx ?? null,
366
912
  fy: existingNode?.fy ?? null
367
913
  };
@@ -382,30 +928,33 @@ function useForceLayout(config = {}, rootNodeId) {
382
928
  if (simulationRef.current) {
383
929
  simulationRef.current.stop();
384
930
  }
931
+ if (rafRef.current) {
932
+ cancelAnimationFrame(rafRef.current);
933
+ rafRef.current = null;
934
+ }
385
935
  const simulation = forceSimulation(simNodes).force(
386
936
  "link",
387
- forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.5)
937
+ forceLink(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.4)
388
938
  ).force(
389
939
  "charge",
390
940
  forceManyBody().strength(forceConfig.chargeStrength)
391
- ).force(
392
- "center",
393
- forceCenter(0, 0).strength(forceConfig.centerStrength)
394
- ).force(
395
- "collision",
396
- forceCollide(forceConfig.collisionRadius)
397
- ).force(
398
- "x",
399
- forceX(0).strength(0.01)
400
- ).force(
401
- "y",
402
- forceY(0).strength(0.01)
403
- ).alphaDecay(0.02).velocityDecay(0.4);
404
- 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
+ }
405
951
  setNodes(
406
952
  (currentNodes) => currentNodes.map((node) => {
407
- const simNode = simulation.nodes().find((n) => n.id === node.id);
953
+ const simNode = simNodes2.find((n) => n.id === node.id);
408
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;
409
958
  return {
410
959
  ...node,
411
960
  position: {
@@ -415,14 +964,26 @@ function useForceLayout(config = {}, rootNodeId) {
415
964
  };
416
965
  })
417
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
+ }
418
975
  });
419
976
  simulationRef.current = simulation;
420
977
  return () => {
421
978
  simulation.stop();
979
+ if (rafRef.current) {
980
+ cancelAnimationFrame(rafRef.current);
981
+ rafRef.current = null;
982
+ }
422
983
  };
423
984
  }, [
424
985
  nodesInitialized,
425
- nodeCount,
986
+ nodeIds,
426
987
  getNodes,
427
988
  getEdges,
428
989
  setNodes,
@@ -433,33 +994,34 @@ function useForceLayout(config = {}, rootNodeId) {
433
994
  (_, node) => {
434
995
  const simulation = simulationRef.current;
435
996
  if (!simulation) return;
436
- draggingNodeRef.current = node.id;
437
- simulation.alphaTarget(0.3).restart();
438
- const simNode = simulation.nodes().find((n) => n.id === node.id);
439
- if (simNode) {
440
- simNode.fx = simNode.x;
441
- simNode.fy = simNode.y;
442
- }
443
- },
444
- []
445
- );
446
- const onNodeDrag = useCallback(
447
- (_, node) => {
448
- const simulation = simulationRef.current;
449
- if (!simulation) return;
997
+ draggingRef.current = { nodeId: node.id, active: true };
450
998
  const simNode = simulation.nodes().find((n) => n.id === node.id);
451
999
  if (simNode) {
452
1000
  simNode.fx = node.position.x;
453
1001
  simNode.fy = node.position.y;
454
1002
  }
1003
+ simulation.alphaTarget(0.1).restart();
455
1004
  },
456
1005
  []
457
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
+ }, []);
458
1020
  const onNodeDragStop = useCallback(
459
1021
  (_, node) => {
460
1022
  const simulation = simulationRef.current;
1023
+ draggingRef.current = { nodeId: null, active: false };
461
1024
  if (!simulation) return;
462
- draggingNodeRef.current = null;
463
1025
  simulation.alphaTarget(0);
464
1026
  if (node.id !== rootNodeId) {
465
1027
  const simNode = simulation.nodes().find((n) => n.id === node.id);
@@ -468,6 +1030,11 @@ function useForceLayout(config = {}, rootNodeId) {
468
1030
  simNode.fy = null;
469
1031
  }
470
1032
  }
1033
+ setTimeout(() => {
1034
+ if (simulationRef.current && !draggingRef.current.active) {
1035
+ simulationRef.current.alpha(0.1).restart();
1036
+ }
1037
+ }, 50);
471
1038
  },
472
1039
  [rootNodeId]
473
1040
  );
@@ -493,7 +1060,7 @@ function useForceLayout(config = {}, rootNodeId) {
493
1060
  forceCollide(updates.collisionRadius)
494
1061
  );
495
1062
  }
496
- simulation.alpha(0.5).restart();
1063
+ simulation.alpha(0.3).restart();
497
1064
  },
498
1065
  []
499
1066
  );
@@ -512,32 +1079,34 @@ function useForceLayout(config = {}, rootNodeId) {
512
1079
  }
513
1080
 
514
1081
  // src/components/ObservablesGraph.tsx
515
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1082
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
516
1083
  var nodeTypes = {
517
1084
  observable: ObservableNode
518
1085
  };
519
1086
  var edgeTypes = {
520
1087
  floating: FloatingEdge
521
1088
  };
1089
+ var defaultEdgeOptions = {
1090
+ type: "floating",
1091
+ style: { stroke: "#94a3b8", strokeWidth: 1.5 }
1092
+ };
522
1093
  function createObservableNodes(investigation, rootObservableIds) {
523
1094
  const graph = getObservableGraph(investigation);
524
1095
  return graph.nodes.map((graphNode, index) => {
525
1096
  const isRoot = rootObservableIds.has(graphNode.id);
526
- const shape = getObservableShape(graphNode.type, isRoot);
527
1097
  const nodeData = {
528
- label: truncateLabel(graphNode.value, 18),
1098
+ label: truncateLabel(graphNode.value, 16),
529
1099
  fullValue: graphNode.value,
530
1100
  observableType: graphNode.type,
531
1101
  level: graphNode.level,
532
1102
  score: graphNode.score,
533
- emoji: getObservableEmoji(graphNode.type),
534
- shape,
1103
+ shape: "circle",
535
1104
  isRoot,
536
1105
  whitelisted: graphNode.whitelisted,
537
1106
  internal: graphNode.internal
538
1107
  };
539
1108
  const angle = index / graph.nodes.length * 2 * Math.PI;
540
- const radius = isRoot ? 0 : 150;
1109
+ const radius = isRoot ? 0 : 180;
541
1110
  return {
542
1111
  id: graphNode.id,
543
1112
  type: "observable",
@@ -545,7 +1114,10 @@ function createObservableNodes(investigation, rootObservableIds) {
545
1114
  x: Math.cos(angle) * radius,
546
1115
  y: Math.sin(angle) * radius
547
1116
  },
548
- data: nodeData
1117
+ data: nodeData,
1118
+ // Enable selection for better UX
1119
+ selectable: true,
1120
+ draggable: true
549
1121
  };
550
1122
  });
551
1123
  }
@@ -562,100 +1134,160 @@ function createObservableEdges(investigation) {
562
1134
  target: graphEdge.target,
563
1135
  type: "floating",
564
1136
  data: edgeData,
1137
+ // Animated edges for a modern feel
1138
+ animated: false,
565
1139
  style: { stroke: "#94a3b8", strokeWidth: 1.5 }
566
1140
  };
567
1141
  });
568
1142
  }
569
1143
  var ForceControls = ({ config, onChange, onRestart }) => {
570
- return /* @__PURE__ */ jsxs2(
571
- "div",
572
- {
573
- style: {
574
- position: "absolute",
575
- top: 10,
576
- right: 10,
577
- background: "white",
578
- padding: 12,
579
- borderRadius: 8,
580
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
581
- fontSize: 12,
582
- fontFamily: "system-ui, sans-serif",
583
- zIndex: 10,
584
- minWidth: 160
585
- },
586
- children: [
587
- /* @__PURE__ */ jsx3("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Force Layout" }),
588
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
589
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
590
- "Repulsion: ",
591
- config.chargeStrength
592
- ] }),
593
- /* @__PURE__ */ jsx3(
594
- "input",
595
- {
596
- type: "range",
597
- min: "-500",
598
- max: "-50",
599
- value: config.chargeStrength,
600
- onChange: (e) => onChange({ chargeStrength: Number(e.target.value) }),
601
- style: { width: "100%" }
602
- }
603
- )
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 })
604
1226
  ] }),
605
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
606
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
607
- "Link Distance: ",
608
- config.linkDistance
609
- ] }),
610
- /* @__PURE__ */ jsx3(
611
- "input",
612
- {
613
- type: "range",
614
- min: "30",
615
- max: "200",
616
- value: config.linkDistance,
617
- onChange: (e) => onChange({ linkDistance: Number(e.target.value) }),
618
- style: { width: "100%" }
619
- }
620
- )
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 })
621
1243
  ] }),
622
- /* @__PURE__ */ jsxs2("div", { style: { marginBottom: 8 }, children: [
623
- /* @__PURE__ */ jsxs2("label", { style: { display: "block", marginBottom: 2 }, children: [
624
- "Collision: ",
625
- config.collisionRadius
626
- ] }),
627
- /* @__PURE__ */ jsx3(
628
- "input",
629
- {
630
- type: "range",
631
- min: "10",
632
- max: "80",
633
- value: config.collisionRadius,
634
- onChange: (e) => onChange({ collisionRadius: Number(e.target.value) }),
635
- style: { width: "100%" }
636
- }
637
- )
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 })
638
1260
  ] }),
639
- /* @__PURE__ */ jsx3(
640
- "button",
1261
+ /* @__PURE__ */ jsx4(
1262
+ "input",
641
1263
  {
642
- onClick: onRestart,
643
- style: {
644
- width: "100%",
645
- padding: "6px 12px",
646
- border: "none",
647
- borderRadius: 4,
648
- background: "#3b82f6",
649
- color: "white",
650
- cursor: "pointer",
651
- fontSize: 12
652
- },
653
- 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
654
1270
  }
655
1271
  )
656
- ]
657
- }
658
- );
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
+ ] });
659
1291
  };
660
1292
  var ObservablesGraphInner = ({
661
1293
  initialNodes,
@@ -673,11 +1305,13 @@ var ObservablesGraphInner = ({
673
1305
  ...DEFAULT_FORCE_CONFIG,
674
1306
  ...initialForceConfig
675
1307
  });
1308
+ const initialFitDone = useRef2(false);
676
1309
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
677
1310
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
678
1311
  React3.useEffect(() => {
679
1312
  setNodes(initialNodes);
680
1313
  setEdges(initialEdges);
1314
+ initialFitDone.current = false;
681
1315
  }, [initialNodes, initialEdges, setNodes, setEdges]);
682
1316
  const {
683
1317
  onNodeDragStart,
@@ -709,58 +1343,95 @@ var ObservablesGraphInner = ({
709
1343
  const data = node.data;
710
1344
  return getLevelColor(data.level);
711
1345
  }, []);
712
- return /* @__PURE__ */ jsxs2(
713
- "div",
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,
714
1357
  {
715
- className,
716
- style: {
717
- width,
718
- height,
719
- position: "relative"
720
- },
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,
721
1384
  children: [
722
- /* @__PURE__ */ jsxs2(
723
- ReactFlow,
1385
+ /* @__PURE__ */ jsx4(
1386
+ Background,
1387
+ {
1388
+ variant: BackgroundVariant.Dots,
1389
+ gap: 24,
1390
+ size: 1,
1391
+ color: "#d1d5db"
1392
+ }
1393
+ ),
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,
724
1407
  {
725
- nodes,
726
- edges,
727
- onNodesChange,
728
- onEdgesChange,
729
- onNodeClick: handleNodeClick,
730
- onNodeDoubleClick: handleNodeDoubleClick,
731
- onNodeDragStart,
732
- onNodeDrag,
733
- onNodeDragStop,
734
- nodeTypes,
735
- edgeTypes,
736
- connectionMode: ConnectionMode.Loose,
737
- fitView: true,
738
- fitViewOptions: { padding: 0.3 },
739
- minZoom: 0.1,
740
- maxZoom: 2,
741
- proOptions: { hideAttribution: true },
742
- children: [
743
- /* @__PURE__ */ jsx3(Background, {}),
744
- /* @__PURE__ */ jsx3(Controls, {}),
745
- /* @__PURE__ */ jsx3(MiniMap, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
746
- ]
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)"
747
1418
  }
748
1419
  ),
749
- showControls && /* @__PURE__ */ jsx3(
1420
+ showControls && /* @__PURE__ */ jsx4(Panel, { position: "top-right", children: /* @__PURE__ */ jsx4(
750
1421
  ForceControls,
751
1422
  {
752
1423
  config: forceConfig,
753
1424
  onChange: handleConfigChange,
754
1425
  onRestart: restartSimulation
755
1426
  }
756
- )
1427
+ ) })
757
1428
  ]
758
1429
  }
759
- );
1430
+ ) });
760
1431
  };
761
1432
  var ObservablesGraph = (props) => {
762
1433
  const { investigation } = props;
763
- const { rootKeys, primaryRootId } = useMemo2(() => {
1434
+ const { rootKeys, primaryRootId } = useMemo4(() => {
764
1435
  const rootType = investigation.data_extraction.root_type;
765
1436
  if (!rootType) {
766
1437
  return { rootKeys: /* @__PURE__ */ new Set(), primaryRootId: void 0 };
@@ -774,12 +1445,12 @@ var ObservablesGraph = (props) => {
774
1445
  primaryRootId: rootsByType[0]?.key
775
1446
  };
776
1447
  }, [investigation]);
777
- const { initialNodes, initialEdges } = useMemo2(() => {
1448
+ const { initialNodes, initialEdges } = useMemo4(() => {
778
1449
  const nodes = createObservableNodes(investigation, rootKeys);
779
1450
  const edges = createObservableEdges(investigation);
780
1451
  return { initialNodes: nodes, initialEdges: edges };
781
1452
  }, [investigation, rootKeys]);
782
- return /* @__PURE__ */ jsx3(ReactFlowProvider, { children: /* @__PURE__ */ jsx3(
1453
+ return /* @__PURE__ */ jsx4(ReactFlowProvider, { children: /* @__PURE__ */ jsx4(
783
1454
  ObservablesGraphInner,
784
1455
  {
785
1456
  ...props,
@@ -791,159 +1462,142 @@ var ObservablesGraph = (props) => {
791
1462
  };
792
1463
 
793
1464
  // src/components/InvestigationGraph.tsx
794
- import React5, { useMemo as useMemo4, useCallback as useCallback3 } from "react";
1465
+ import React5, { useMemo as useMemo7, useCallback as useCallback3 } from "react";
795
1466
  import {
796
1467
  ReactFlow as ReactFlow2,
797
1468
  Background as Background2,
798
1469
  Controls as Controls2,
799
1470
  MiniMap as MiniMap2,
800
1471
  useNodesState as useNodesState2,
801
- useEdgesState as useEdgesState2
1472
+ useEdgesState as useEdgesState2,
1473
+ BackgroundVariant as BackgroundVariant2,
1474
+ MarkerType
802
1475
  } from "@xyflow/react";
803
1476
  import "@xyflow/react/dist/style.css";
804
1477
 
805
1478
  // src/components/InvestigationNode.tsx
806
- import { memo as memo3 } from "react";
1479
+ import { memo as memo3, useMemo as useMemo5 } from "react";
807
1480
  import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
808
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
809
- function InvestigationNodeComponent({
810
- data,
811
- selected
812
- }) {
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 }) {
813
1517
  const nodeData = data;
814
- const {
815
- label,
816
- emoji,
817
- nodeType,
818
- level,
819
- description
820
- } = nodeData;
1518
+ const { label, nodeType, level, description } = nodeData;
821
1519
  const borderColor = getLevelColor(level);
822
1520
  const backgroundColor = getLevelBackgroundColor(level);
823
- const getNodeStyle = () => {
824
- switch (nodeType) {
825
- case "root":
826
- return {
827
- minWidth: 120,
828
- padding: "8px 16px",
829
- borderRadius: 8,
830
- fontWeight: 600
831
- };
832
- case "check":
833
- return {
834
- minWidth: 100,
835
- padding: "6px 12px",
836
- borderRadius: 4,
837
- fontWeight: 400
838
- };
839
- case "container":
840
- return {
841
- minWidth: 100,
842
- padding: "6px 12px",
843
- borderRadius: 12,
844
- fontWeight: 400
845
- };
846
- default:
847
- return {
848
- minWidth: 80,
849
- padding: "6px 12px",
850
- borderRadius: 4,
851
- fontWeight: 400
852
- };
853
- }
854
- };
855
- const style = getNodeStyle();
856
- return /* @__PURE__ */ jsxs3(
857
- "div",
858
- {
859
- className: "investigation-node",
860
- style: {
861
- ...style,
862
- display: "flex",
863
- flexDirection: "column",
864
- alignItems: "center",
865
- backgroundColor,
866
- border: `${selected ? 3 : 2}px solid ${borderColor}`,
867
- cursor: "pointer",
868
- fontFamily: "system-ui, sans-serif"
869
- },
870
- children: [
871
- /* @__PURE__ */ jsxs3(
872
- "div",
873
- {
874
- style: {
875
- display: "flex",
876
- alignItems: "center",
877
- gap: 6
878
- },
879
- children: [
880
- /* @__PURE__ */ jsx4("span", { style: { fontSize: 14 }, children: emoji }),
881
- /* @__PURE__ */ jsx4(
882
- "span",
883
- {
884
- style: {
885
- fontSize: 12,
886
- fontWeight: style.fontWeight,
887
- maxWidth: 150,
888
- overflow: "hidden",
889
- textOverflow: "ellipsis",
890
- whiteSpace: "nowrap"
891
- },
892
- title: label,
893
- children: label
894
- }
895
- )
896
- ]
897
- }
898
- ),
899
- description && /* @__PURE__ */ jsx4(
900
- "div",
901
- {
902
- style: {
903
- marginTop: 4,
904
- fontSize: 10,
905
- color: "#6b7280",
906
- maxWidth: 140,
907
- overflow: "hidden",
908
- textOverflow: "ellipsis",
909
- whiteSpace: "nowrap"
910
- },
911
- title: description,
912
- children: description
913
- }
914
- ),
915
- /* @__PURE__ */ jsx4(
916
- Handle2,
917
- {
918
- type: "target",
919
- position: Position2.Left,
920
- style: {
921
- width: 8,
922
- height: 8,
923
- background: borderColor
924
- }
925
- }
926
- ),
927
- /* @__PURE__ */ jsx4(
928
- Handle2,
929
- {
930
- type: "source",
931
- position: Position2.Right,
932
- style: {
933
- width: 8,
934
- height: 8,
935
- background: borderColor
936
- }
937
- }
938
- )
939
- ]
940
- }
1521
+ const config = NODE_CONFIG[nodeType] || NODE_CONFIG.check;
1522
+ const IconComponent = useMemo5(
1523
+ () => getInvestigationIcon(nodeType),
1524
+ [nodeType]
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]
941
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
+ ] });
942
1596
  }
943
1597
  var InvestigationNode = memo3(InvestigationNodeComponent);
944
1598
 
945
1599
  // src/hooks/useDagreLayout.ts
946
- import { useMemo as useMemo3 } from "react";
1600
+ import { useMemo as useMemo6 } from "react";
947
1601
  import Dagre from "@dagrejs/dagre";
948
1602
  var DEFAULT_OPTIONS = {
949
1603
  direction: "LR",
@@ -989,10 +1643,23 @@ function computeDagreLayout(nodes, edges, options = {}) {
989
1643
  }
990
1644
 
991
1645
  // src/components/InvestigationGraph.tsx
992
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1646
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
993
1647
  var nodeTypes2 = {
994
1648
  investigation: InvestigationNode
995
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
+ };
996
1663
  function flattenContainers(containers) {
997
1664
  const result = [];
998
1665
  for (const container of Object.values(containers)) {
@@ -1019,15 +1686,23 @@ function createInvestigationGraph(investigation) {
1019
1686
  label: truncateLabel(rootValue, 24),
1020
1687
  nodeType: "root",
1021
1688
  level: rootLevel,
1022
- score: primaryRoot?.score ?? investigation.score,
1023
- emoji: getInvestigationNodeEmoji("root")
1689
+ score: primaryRoot?.score ?? investigation.score
1024
1690
  };
1025
1691
  nodes.push({
1026
1692
  id: rootKey,
1027
1693
  type: "investigation",
1028
1694
  position: { x: 0, y: 0 },
1029
- data: rootNodeData
1695
+ data: rootNodeData,
1696
+ selectable: true,
1697
+ draggable: true
1030
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
+ }
1031
1706
  const allChecks = [];
1032
1707
  for (const checksForKey of Object.values(investigation.checks)) {
1033
1708
  allChecks.push(...checksForKey);
@@ -1041,43 +1716,51 @@ function createInvestigationGraph(investigation) {
1041
1716
  nodeType: "check",
1042
1717
  level: check.level,
1043
1718
  score: check.score,
1044
- description: truncateLabel(check.description, 30),
1045
- emoji: getInvestigationNodeEmoji("check")
1719
+ description: truncateLabel(check.description, 30)
1046
1720
  };
1047
1721
  nodes.push({
1048
1722
  id: `check-${check.key}`,
1049
1723
  type: "investigation",
1050
1724
  position: { x: 0, y: 0 },
1051
- data: checkNodeData
1052
- });
1053
- edges.push({
1054
- id: `edge-root-${check.key}`,
1055
- source: rootKey,
1056
- target: `check-${check.key}`,
1057
- type: "default"
1725
+ data: checkNodeData,
1726
+ selectable: true,
1727
+ draggable: true
1058
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
+ }
1059
1738
  }
1060
- const allContainers = flattenContainers(investigation.containers);
1061
1739
  for (const container of allContainers) {
1062
1740
  const containerNodeData = {
1063
- label: truncateLabel(container.path.split("/").pop() ?? container.path, 20),
1741
+ label: truncateLabel(
1742
+ container.path.split("/").pop() ?? container.path,
1743
+ 20
1744
+ ),
1064
1745
  nodeType: "container",
1065
1746
  level: container.aggregated_level,
1066
1747
  score: container.aggregated_score,
1067
- path: container.path,
1068
- emoji: getInvestigationNodeEmoji("container")
1748
+ path: container.path
1069
1749
  };
1070
1750
  nodes.push({
1071
1751
  id: `container-${container.key}`,
1072
1752
  type: "investigation",
1073
1753
  position: { x: 0, y: 0 },
1074
- data: containerNodeData
1754
+ data: containerNodeData,
1755
+ selectable: true,
1756
+ draggable: true
1075
1757
  });
1076
1758
  edges.push({
1077
1759
  id: `edge-root-container-${container.key}`,
1078
1760
  source: rootKey,
1079
1761
  target: `container-${container.key}`,
1080
- type: "default"
1762
+ type: "smoothstep",
1763
+ animated: false
1081
1764
  });
1082
1765
  for (const checkKey of container.checks) {
1083
1766
  if (seenCheckIds.has(checkKey)) {
@@ -1085,8 +1768,8 @@ function createInvestigationGraph(investigation) {
1085
1768
  id: `edge-container-check-${container.key}-${checkKey}`,
1086
1769
  source: `container-${container.key}`,
1087
1770
  target: `check-${checkKey}`,
1088
- type: "default",
1089
- style: { strokeDasharray: "5,5" }
1771
+ type: "smoothstep",
1772
+ animated: false
1090
1773
  });
1091
1774
  }
1092
1775
  }
@@ -1100,15 +1783,15 @@ var InvestigationGraph = ({
1100
1783
  onNodeClick,
1101
1784
  className
1102
1785
  }) => {
1103
- const { initialNodes, initialEdges } = useMemo4(() => {
1786
+ const { initialNodes, initialEdges } = useMemo7(() => {
1104
1787
  const { nodes: nodes2, edges: edges2 } = createInvestigationGraph(investigation);
1105
1788
  return { initialNodes: nodes2, initialEdges: edges2 };
1106
1789
  }, [investigation]);
1107
- const { nodes: layoutNodes, edges: layoutEdges } = useMemo4(() => {
1790
+ const { nodes: layoutNodes, edges: layoutEdges } = useMemo7(() => {
1108
1791
  return computeDagreLayout(initialNodes, initialEdges, {
1109
1792
  direction: "LR",
1110
- nodeSpacing: 30,
1111
- rankSpacing: 120
1793
+ nodeSpacing: 40,
1794
+ rankSpacing: 140
1112
1795
  });
1113
1796
  }, [initialNodes, initialEdges]);
1114
1797
  const [nodes, setNodes, onNodesChange] = useNodesState2(layoutNodes);
@@ -1128,97 +1811,200 @@ var InvestigationGraph = ({
1128
1811
  const data = node.data;
1129
1812
  return getLevelColor(data.level);
1130
1813
  }, []);
1131
- return /* @__PURE__ */ jsx5(
1132
- "div",
1133
- {
1134
- className,
1135
- style: {
1136
- width,
1137
- height,
1138
- position: "relative"
1139
- },
1140
- children: /* @__PURE__ */ jsxs4(
1141
- ReactFlow2,
1142
- {
1143
- nodes,
1144
- edges,
1145
- onNodesChange,
1146
- onEdgesChange,
1147
- onNodeClick: handleNodeClick,
1148
- nodeTypes: nodeTypes2,
1149
- fitView: true,
1150
- fitViewOptions: { padding: 0.2 },
1151
- minZoom: 0.1,
1152
- maxZoom: 2,
1153
- proOptions: { hideAttribution: true },
1154
- children: [
1155
- /* @__PURE__ */ jsx5(Background2, {}),
1156
- /* @__PURE__ */ jsx5(Controls2, {}),
1157
- /* @__PURE__ */ jsx5(MiniMap2, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
1158
- ]
1159
- }
1160
- )
1161
- }
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]
1162
1822
  );
1163
- };
1164
-
1165
- // src/components/CyvestGraph.tsx
1166
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1167
- var ViewToggle = ({ currentView, onChange }) => {
1168
- return /* @__PURE__ */ jsxs5(
1169
- "div",
1823
+ return /* @__PURE__ */ jsx6("div", { className, style: containerStyle, children: /* @__PURE__ */ jsxs5(
1824
+ ReactFlow2,
1170
1825
  {
1171
- style: {
1172
- position: "absolute",
1173
- top: 10,
1174
- left: 10,
1175
- display: "flex",
1176
- gap: 4,
1177
- background: "white",
1178
- padding: 4,
1179
- borderRadius: 8,
1180
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
1181
- zIndex: 10,
1182
- fontFamily: "system-ui, sans-serif"
1183
- },
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,
1184
1846
  children: [
1185
1847
  /* @__PURE__ */ jsx6(
1186
- "button",
1848
+ Background2,
1187
1849
  {
1188
- onClick: () => onChange("observables"),
1850
+ variant: BackgroundVariant2.Dots,
1851
+ gap: 24,
1852
+ size: 1,
1853
+ color: "#d1d5db"
1854
+ }
1855
+ ),
1856
+ /* @__PURE__ */ jsx6(
1857
+ Controls2,
1858
+ {
1859
+ showInteractive: false,
1189
1860
  style: {
1190
- padding: "6px 12px",
1191
- border: "none",
1192
- borderRadius: 4,
1193
- cursor: "pointer",
1194
- fontSize: 12,
1195
- fontWeight: currentView === "observables" ? 600 : 400,
1196
- background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
1197
- color: currentView === "observables" ? "white" : "#374151"
1198
- },
1199
- 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
+ }
1200
1865
  }
1201
1866
  ),
1202
1867
  /* @__PURE__ */ jsx6(
1203
- "button",
1868
+ MiniMap2,
1204
1869
  {
1205
- onClick: () => onChange("investigation"),
1870
+ nodeColor: miniMapNodeColor,
1871
+ zoomable: true,
1872
+ pannable: true,
1206
1873
  style: {
1207
- padding: "6px 12px",
1208
- border: "none",
1209
- borderRadius: 4,
1210
- cursor: "pointer",
1211
- fontSize: 12,
1212
- fontWeight: currentView === "investigation" ? 600 : 400,
1213
- background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
1214
- 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)"
1215
1878
  },
1216
- children: "Investigation"
1879
+ maskColor: "rgba(0,0,0,0.08)"
1217
1880
  }
1218
1881
  )
1219
1882
  ]
1220
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
+ []
1221
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
+ ] });
1222
2008
  };
1223
2009
  var CyvestGraph = ({
1224
2010
  investigation,
@@ -1229,46 +2015,52 @@ var CyvestGraph = ({
1229
2015
  className,
1230
2016
  showViewToggle = true
1231
2017
  }) => {
1232
- const [view, setView] = useState2(initialView);
2018
+ const [view, setView] = useState2(
2019
+ initialView
2020
+ );
1233
2021
  const handleNodeClick = useCallback4(
1234
2022
  (nodeId, _nodeType) => {
1235
2023
  onNodeClick?.(nodeId);
1236
2024
  },
1237
2025
  [onNodeClick]
1238
2026
  );
1239
- return /* @__PURE__ */ jsxs5(
1240
- "div",
1241
- {
1242
- className,
1243
- style: {
1244
- width,
1245
- height,
1246
- position: "relative"
1247
- },
1248
- children: [
1249
- showViewToggle && /* @__PURE__ */ jsx6(ViewToggle, { currentView: view, onChange: setView }),
1250
- view === "observables" ? /* @__PURE__ */ jsx6(
1251
- ObservablesGraph,
1252
- {
1253
- investigation,
1254
- height: "100%",
1255
- width: "100%",
1256
- onNodeClick: handleNodeClick,
1257
- showControls: true
1258
- }
1259
- ) : /* @__PURE__ */ jsx6(
1260
- InvestigationGraph,
1261
- {
1262
- investigation,
1263
- height: "100%",
1264
- width: "100%",
1265
- onNodeClick: handleNodeClick
1266
- }
1267
- )
1268
- ]
1269
- }
2027
+ const containerStyle = useMemo8(
2028
+ () => ({
2029
+ width,
2030
+ height,
2031
+ position: "relative"
2032
+ }),
2033
+ [width, height]
1270
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
+ ] });
1271
2056
  };
1272
2057
  export {
1273
- CyvestGraph
2058
+ CyvestGraph,
2059
+ DEFAULT_FORCE_CONFIG,
2060
+ INVESTIGATION_ICON_MAP,
2061
+ InvestigationGraph,
2062
+ OBSERVABLE_ICON_MAP,
2063
+ ObservablesGraph,
2064
+ getInvestigationIcon,
2065
+ getObservableIcon
1274
2066
  };