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