@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/README.md +122 -10
- package/dist/index.d.mts +156 -6
- package/dist/index.d.ts +156 -6
- package/dist/index.js +1445 -650
- package/dist/index.mjs +1457 -665
- package/package.json +2 -2
- package/src/components/CyvestGraph.tsx +115 -46
- package/src/components/FloatingEdge.tsx +41 -11
- package/src/components/Icons.tsx +729 -0
- package/src/components/InvestigationGraph.tsx +107 -36
- package/src/components/InvestigationNode.tsx +129 -112
- package/src/components/ObservableNode.tsx +116 -135
- package/src/components/ObservablesGraph.tsx +241 -111
- package/src/hooks/useForceLayout.ts +136 -62
- package/src/index.ts +25 -2
- package/src/types.ts +9 -11
- package/src/utils/observables.ts +9 -97
- package/tests/observables.test.ts +10 -17
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/components/CyvestGraph.tsx
|
|
2
|
-
import { useState as useState2, useCallback as useCallback4 } from "react";
|
|
2
|
+
import { useState as useState2, useCallback as useCallback4, useMemo as useMemo8 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/components/ObservablesGraph.tsx
|
|
5
|
-
import React3, { useMemo as
|
|
5
|
+
import React3, { useMemo as useMemo4, useCallback as useCallback2, useState, useRef as useRef2 } from "react";
|
|
6
6
|
import {
|
|
7
7
|
ReactFlow,
|
|
8
8
|
ReactFlowProvider,
|
|
@@ -11,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:
|
|
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.
|
|
62
|
+
return lightenHexColor(getLevelColor(level), 0.88);
|
|
122
63
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
129
|
-
|
|
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 =
|
|
135
|
-
var
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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:
|
|
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
|
-
|
|
780
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out"
|
|
164
781
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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,
|
|
292
|
-
import { jsx as
|
|
808
|
+
import { memo as memo2, useMemo as useMemo2 } from "react";
|
|
809
|
+
import { BaseEdge, getBezierPath } from "@xyflow/react";
|
|
810
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
811
|
+
function getControlOffset(sourceX, sourceY, targetX, targetY) {
|
|
812
|
+
const dx = targetX - sourceX;
|
|
813
|
+
const dy = targetY - sourceY;
|
|
814
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
815
|
+
return Math.min(Math.max(distance * 0.15, 20), 60);
|
|
816
|
+
}
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
345
|
-
const forceConfig =
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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.
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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__ */
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
{
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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__ */
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
{
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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__ */
|
|
640
|
-
"
|
|
1261
|
+
/* @__PURE__ */ jsx4(
|
|
1262
|
+
"input",
|
|
641
1263
|
{
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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__ */
|
|
723
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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__ */
|
|
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 } =
|
|
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 } =
|
|
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__ */
|
|
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
|
|
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
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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: "
|
|
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: "
|
|
1089
|
-
|
|
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 } =
|
|
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 } =
|
|
1790
|
+
const { nodes: layoutNodes, edges: layoutEdges } = useMemo7(() => {
|
|
1108
1791
|
return computeDagreLayout(initialNodes, initialEdges, {
|
|
1109
1792
|
direction: "LR",
|
|
1110
|
-
nodeSpacing:
|
|
1111
|
-
rankSpacing:
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1848
|
+
Background2,
|
|
1187
1849
|
{
|
|
1188
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1868
|
+
MiniMap2,
|
|
1204
1869
|
{
|
|
1205
|
-
|
|
1870
|
+
nodeColor: miniMapNodeColor,
|
|
1871
|
+
zoomable: true,
|
|
1872
|
+
pannable: true,
|
|
1206
1873
|
style: {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
};
|