@cyvest/cyvest-vis 5.4.1 → 6.0.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
@@ -1,13 +1,10 @@
1
- // src/components/CyvestGraph.tsx
2
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo4, useState } from "react";
3
-
4
- // src/components/CyvestInvestigationView.tsx
1
+ // src/components/CyvestObservablesView.tsx
5
2
  import { useMemo as useMemo2 } from "react";
6
3
 
7
- // src/adapters/investigationElements.ts
4
+ // src/adapters/observablesElements.ts
8
5
  import {
9
- getRootObservable,
10
- getTagAncestors
6
+ getObservableGraph,
7
+ getRootObservable
11
8
  } from "@cyvest/cyvest-js";
12
9
 
13
10
  // src/icons/svg.ts
@@ -20,9 +17,7 @@ var ICON_PATHS = {
20
17
  hash: '<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>',
21
18
  flask: '<path d="M10 2v7.5a2 2 0 0 1-.2.9L4.7 20.5a1 1 0 0 0 .9 1.5h12.8a1 1 0 0 0 .9-1.5l-5.1-10.1a2 2 0 0 1-.2-.9V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
22
19
  question: '<circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
23
- crosshair: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>',
24
- check: '<rect x="7" y="3" width="10" height="4" rx="1.5"/><rect x="5" y="5" width="14" height="16" rx="2"/><path d="m9 14 2.5 2.5L16 12"/>',
25
- tag: '<path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3.4 13.4a2 2 0 0 1 0-2.8l7.2-7.2a2 2 0 0 1 2.8 0l7.2 7.2a2 2 0 0 1 0 2.8z"/><circle cx="9" cy="9" r="1.2"/>'
20
+ finding: '<rect x="7" y="3" width="10" height="4" rx="1.5"/><rect x="5" y="5" width="14" height="16" rx="2"/><path d="m9 14 2.5 2.5L16 12"/>'
26
21
  };
27
22
  var OBSERVABLE_ICON_NAME_MAP = {
28
23
  ipv4: "globe",
@@ -32,17 +27,18 @@ var OBSERVABLE_ICON_NAME_MAP = {
32
27
  email: "mail",
33
28
  hash: "hash",
34
29
  file: "file",
35
- artifact: "flask"
36
- };
37
- var INVESTIGATION_ICON_NAME_MAP = {
38
- root: "crosshair",
39
- check: "check",
40
- tag: "tag"
30
+ artifact: "flask",
31
+ host: "domain",
32
+ process: "finding",
33
+ user: "mail",
34
+ command_line: "link",
35
+ cloud_resource: "globe"
41
36
  };
42
37
  function toSvgDataUri(iconName, color) {
43
38
  const body = ICON_PATHS[iconName] ?? ICON_PATHS.question;
44
39
  const svg = [
45
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"',
40
+ '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"',
41
+ ' viewBox="0 0 24 24" fill="none"',
46
42
  ` stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">`,
47
43
  body,
48
44
  "</svg>"
@@ -54,48 +50,93 @@ function getObservableIconSvg(observableType, options) {
54
50
  const icon = OBSERVABLE_ICON_NAME_MAP[normalizedType] ?? "question";
55
51
  return toSvgDataUri(icon, options?.color ?? "#314264");
56
52
  }
57
- function getInvestigationIconSvg(nodeType, options) {
58
- const icon = INVESTIGATION_ICON_NAME_MAP[nodeType] ?? "question";
59
- return toSvgDataUri(icon, options?.color ?? "#314264");
60
- }
61
-
62
- // src/utils/colors.ts
63
- import { getColorForLevel } from "@cyvest/cyvest-js";
64
53
 
65
54
  // src/types.ts
66
55
  var DEFAULT_CYVEST_THEME = {
67
- background: "#f4f7fb",
68
- gridColor: "#d7dfeb",
69
- panelBackground: "rgba(255, 255, 255, 0.96)",
70
- panelBorder: "#d3dae6",
71
- panelText: "#172033",
72
- panelTextMuted: "#556079",
73
- accent: "#1f6feb",
74
- edgeColor: "#8a95aa",
75
- edgeSelectedColor: "#1f6feb",
76
- fontFamily: "'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
56
+ background: "#f8fafc",
57
+ gridColor: "#e2e8f0",
58
+ panelBackground: "rgba(255, 255, 255, 0.97)",
59
+ panelBorder: "#e2e8f0",
60
+ panelText: "#0f172a",
61
+ panelTextMuted: "#64748b",
62
+ accent: "#334155",
63
+ edgeColor: "#cbd5e1",
64
+ edgeSelectedColor: "#475569",
65
+ fontFamily: "IBM Plex Sans, Segoe UI, Helvetica Neue, Arial, sans-serif",
66
+ nodeSurface: "#ffffff",
67
+ rootSurface: "#1e293b",
68
+ rootText: "#ffffff",
69
+ iconColor: "#314264",
70
+ iconMutedColor: "#475569",
71
+ levelSurfaceMix: "#ffffff",
72
+ levelSurfaceMixRatio: 0.94
73
+ };
74
+ var DARK_CYVEST_THEME = {
75
+ background: "#0f172a",
76
+ gridColor: "#1e293b",
77
+ panelBackground: "rgba(15, 23, 42, 0.97)",
78
+ panelBorder: "#334155",
79
+ panelText: "#e2e8f0",
80
+ panelTextMuted: "#94a3b8",
81
+ accent: "#cbd5e1",
82
+ edgeColor: "#475569",
83
+ edgeSelectedColor: "#cbd5e1",
84
+ fontFamily: "IBM Plex Sans, Segoe UI, Helvetica Neue, Arial, sans-serif",
85
+ nodeSurface: "#1e293b",
86
+ rootSurface: "#020617",
87
+ rootText: "#f8fafc",
88
+ iconColor: "#cbd5e1",
89
+ iconMutedColor: "#cbd5e1",
90
+ levelSurfaceMix: "#0f172a",
91
+ levelSurfaceMixRatio: 0.7
77
92
  };
78
93
 
79
94
  // src/utils/colors.ts
80
95
  function getLevelColor(level) {
81
- return getColorForLevel(level);
96
+ const colors = {
97
+ NONE: "#cbd5e1",
98
+ TRUSTED: "#94a3b8",
99
+ INFO: "#94a3b8",
100
+ SAFE: "#648b79",
101
+ NOTABLE: "#aa8958",
102
+ SUSPICIOUS: "#ad704b",
103
+ MALICIOUS: "#ad5555"
104
+ };
105
+ return colors[level] ?? colors.INFO;
82
106
  }
83
107
  function clampChannel(channel) {
84
108
  return Math.max(0, Math.min(255, Math.round(channel)));
85
109
  }
86
- function lightenHexColor(hex, ratio) {
110
+ function parseHex(hex) {
87
111
  const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
88
112
  if (!/^[0-9a-fA-F]{6}$/.test(normalized)) {
113
+ return null;
114
+ }
115
+ return [
116
+ Number.parseInt(normalized.slice(0, 2), 16),
117
+ Number.parseInt(normalized.slice(2, 4), 16),
118
+ Number.parseInt(normalized.slice(4, 6), 16)
119
+ ];
120
+ }
121
+ function mixHexColor(hex, target, ratio) {
122
+ const from = parseHex(hex);
123
+ const to = parseHex(target);
124
+ if (!from || !to) {
89
125
  return hex;
90
126
  }
91
- const red = Number.parseInt(normalized.slice(0, 2), 16);
92
- const green = Number.parseInt(normalized.slice(2, 4), 16);
93
- const blue = Number.parseInt(normalized.slice(4, 6), 16);
94
- const mix = (channel) => clampChannel(channel + (255 - channel) * ratio).toString(16).padStart(2, "0");
95
- return `#${mix(red)}${mix(green)}${mix(blue)}`;
127
+ const mix = (a, b) => clampChannel(a + (b - a) * ratio).toString(16).padStart(2, "0");
128
+ return `#${mix(from[0], to[0])}${mix(from[1], to[1])}${mix(from[2], to[2])}`;
129
+ }
130
+ function lightenHexColor(hex, ratio) {
131
+ return mixHexColor(hex, "#ffffff", ratio);
96
132
  }
97
- function getLevelBackgroundColor(level) {
98
- return lightenHexColor(getLevelColor(level), 0.9);
133
+ function getLevelBackgroundColor(level, theme) {
134
+ const resolved = resolveTheme(theme);
135
+ return mixHexColor(
136
+ getLevelColor(level),
137
+ resolved.levelSurfaceMix,
138
+ resolved.levelSurfaceMixRatio
139
+ );
99
140
  }
100
141
  function resolveTheme(theme) {
101
142
  return {
@@ -117,206 +158,106 @@ function truncateLabel(value, maxLength = 28, truncateMiddle = true) {
117
158
  return `${value.slice(0, leftLength)}\u2026${value.slice(-rightLength)}`;
118
159
  }
119
160
 
120
- // src/adapters/investigationElements.ts
121
- var ROOT_NODE_ID = "inv-root";
122
- function createNodeData(nodeId, nodeType, label, level, score, maxLabelLength) {
123
- const borderColor = getLevelColor(level);
124
- const sizeByType = {
125
- root: { width: 190, height: 52 },
126
- tag: { width: 156, height: 46 },
127
- check: { width: 174, height: 52 }
128
- };
129
- const dimensions = sizeByType[nodeType];
130
- return {
131
- id: nodeId,
132
- nodeType,
133
- labelShort: truncateLabel(
134
- label,
135
- nodeType === "root" ? maxLabelLength + 4 : maxLabelLength,
136
- true
137
- ),
138
- labelFull: label,
139
- level,
140
- score,
141
- borderColor,
142
- fillColor: getLevelBackgroundColor(level),
143
- icon: getInvestigationIconSvg(nodeType, { color: borderColor }),
144
- width: dimensions.width,
145
- height: dimensions.height,
146
- shape: "round-rectangle",
147
- borderWidth: 2
148
- };
161
+ // src/adapters/observablesElements.ts
162
+ function findFallbackRootId(graph) {
163
+ if (graph.nodes.length === 0) {
164
+ return void 0;
165
+ }
166
+ const incoming = /* @__PURE__ */ new Map();
167
+ for (const node of graph.nodes) {
168
+ incoming.set(node.id, 0);
169
+ }
170
+ for (const edge of graph.edges) {
171
+ incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
172
+ }
173
+ const sourceCandidates = graph.nodes.filter((node) => (incoming.get(node.id) ?? 0) === 0);
174
+ if (sourceCandidates.length === 0) {
175
+ return graph.nodes[0]?.id;
176
+ }
177
+ sourceCandidates.sort((a, b) => b.score - a.score);
178
+ return sourceCandidates[0]?.id;
149
179
  }
150
- function getRootLabel(investigation) {
151
- const rootObservable = getRootObservable(investigation);
152
- if (rootObservable) {
180
+ function getArrowShapes(edge) {
181
+ if (edge.direction === "bidirectional") {
153
182
  return {
154
- value: rootObservable.value,
155
- level: rootObservable.level,
156
- score: rootObservable.score
183
+ sourceArrowShape: "triangle",
184
+ targetArrowShape: "triangle"
157
185
  };
158
186
  }
159
- const firstObservable = Object.values(investigation.observables)[0];
160
- if (firstObservable) {
187
+ if (edge.direction === "inbound") {
161
188
  return {
162
- value: firstObservable.value,
163
- level: firstObservable.level,
164
- score: firstObservable.score
189
+ sourceArrowShape: "triangle",
190
+ targetArrowShape: "none"
165
191
  };
166
192
  }
167
193
  return {
168
- value: investigation.investigation_name ?? investigation.investigation_id,
169
- level: investigation.level,
170
- score: investigation.score
171
- };
172
- }
173
- function getTagMap(tags) {
174
- const map = /* @__PURE__ */ new Map();
175
- for (const tag of Object.values(tags)) {
176
- map.set(tag.name, tag);
177
- }
178
- return map;
179
- }
180
- function createEdge(id, source, target, relationshipType, edgeColor) {
181
- const data = {
182
- id,
183
- relationshipType,
184
- color: edgeColor,
185
- width: 1.5,
186
194
  sourceArrowShape: "none",
187
195
  targetArrowShape: "triangle"
188
196
  };
189
- return {
190
- group: "edges",
191
- data: {
192
- ...data,
193
- source,
194
- target
195
- }
196
- };
197
197
  }
198
- function buildInvestigationElements(investigation, options) {
199
- const maxLabelLength = options?.maxLabelLength ?? 26;
200
- const edgeColor = options?.edgeColor ?? "#8a95aa";
201
- const nodes = [];
202
- const edges = [];
203
- const root = getRootLabel(investigation);
204
- nodes.push({
205
- group: "nodes",
206
- data: createNodeData(
207
- ROOT_NODE_ID,
208
- "root",
209
- root.value,
210
- root.level,
211
- root.score,
212
- maxLabelLength
213
- )
214
- });
215
- const checks = Object.values(investigation.checks);
216
- const allTags = Object.values(investigation.tags);
217
- const tagByName = getTagMap(investigation.tags);
218
- const checksInTags = /* @__PURE__ */ new Set();
219
- for (const tag of allTags) {
220
- for (const checkKey of tag.checks) {
221
- checksInTags.add(checkKey);
222
- }
223
- }
224
- for (const check of checks) {
225
- const checkNodeId = `inv-check:${check.key}`;
226
- const nodeData = createNodeData(
227
- checkNodeId,
228
- "check",
229
- check.check_name,
230
- check.level,
231
- check.score,
232
- maxLabelLength
233
- );
234
- nodes.push({ group: "nodes", data: nodeData });
235
- if (!checksInTags.has(check.key)) {
236
- edges.push(
237
- createEdge(
238
- `inv-edge-root-check:${check.key}`,
239
- ROOT_NODE_ID,
240
- checkNodeId,
241
- "contains-check",
242
- edgeColor
243
- )
244
- );
245
- }
246
- }
247
- const tagNames = /* @__PURE__ */ new Set();
248
- for (const tag of allTags) {
249
- tagNames.add(tag.name);
250
- for (const ancestor of getTagAncestors(tag.name)) {
251
- tagNames.add(ancestor);
252
- }
253
- }
254
- for (const tagName of tagNames) {
255
- const tag = tagByName.get(tagName);
256
- const tagLevel = tag?.direct_level ?? "INFO";
257
- const tagScore = tag?.direct_score ?? 0;
258
- const shortTagName = tagName.split(":").pop() ?? tagName;
259
- nodes.push({
198
+ function buildObservablesElements(investigation, options) {
199
+ const graph = getObservableGraph(investigation);
200
+ const rootObservable = getRootObservable(investigation);
201
+ const rootId = rootObservable?.key ?? findFallbackRootId(graph);
202
+ const maxLabelLength = options?.maxLabelLength ?? 28;
203
+ const theme = resolveTheme(options?.theme);
204
+ const edgeColor = options?.edgeColor ?? theme.edgeColor;
205
+ const nodes = graph.nodes.map((node) => {
206
+ const isRoot = node.id === rootId;
207
+ const borderColor = getLevelColor(node.level);
208
+ const dimension = isRoot ? 52 : 38;
209
+ const iconColor = isRoot ? theme.rootText : theme.iconMutedColor;
210
+ const data = {
211
+ id: node.id,
212
+ nodeType: "observable",
213
+ labelShort: truncateLabel(node.value, maxLabelLength, true),
214
+ labelFull: node.value,
215
+ observableType: node.type,
216
+ level: node.level,
217
+ score: node.score,
218
+ isRoot,
219
+ whitelisted: node.whitelisted,
220
+ internal: node.internal,
221
+ shape: "ellipse",
222
+ width: dimension,
223
+ height: dimension,
224
+ borderWidth: isRoot ? 1 : 2,
225
+ borderColor: isRoot ? theme.rootSurface : borderColor,
226
+ fillColor: isRoot ? theme.rootSurface : node.internal ? theme.nodeSurface : getLevelBackgroundColor(node.level, theme),
227
+ icon: getObservableIconSvg(node.type, { color: iconColor }),
228
+ opacity: node.whitelisted ? 0.52 : 1
229
+ };
230
+ return {
260
231
  group: "nodes",
261
- data: createNodeData(
262
- `inv-tag:${tagName}`,
263
- "tag",
264
- shortTagName,
265
- tagLevel,
266
- tagScore,
267
- maxLabelLength
268
- )
269
- });
270
- }
271
- for (const tagName of tagNames) {
272
- const parts = tagName.split(":");
273
- if (parts.length === 1) {
274
- edges.push(
275
- createEdge(
276
- `inv-edge-root-tag:${tagName}`,
277
- ROOT_NODE_ID,
278
- `inv-tag:${tagName}`,
279
- "contains-tag",
280
- edgeColor
281
- )
282
- );
283
- continue;
284
- }
285
- const parentName = parts.slice(0, -1).join(":");
286
- edges.push(
287
- createEdge(
288
- `inv-edge-tag:${parentName}->${tagName}`,
289
- `inv-tag:${parentName}`,
290
- `inv-tag:${tagName}`,
291
- "tag-hierarchy",
292
- edgeColor
293
- )
294
- );
295
- }
296
- const checksByKey = new Map(
297
- checks.map((check) => [check.key, check])
298
- );
299
- for (const tag of allTags) {
300
- for (const checkKey of tag.checks) {
301
- if (!checksByKey.has(checkKey)) {
302
- continue;
232
+ data
233
+ };
234
+ });
235
+ const nodeIds = new Set(graph.nodes.map((node) => node.id));
236
+ const edges = graph.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge, index) => {
237
+ const arrowShape = getArrowShapes(edge);
238
+ const data = {
239
+ id: `obs-edge-${index}-${edge.source}-${edge.target}-${edge.type}`,
240
+ relationshipType: edge.type,
241
+ direction: edge.direction,
242
+ color: edgeColor,
243
+ width: 1.15,
244
+ sourceArrowShape: arrowShape.sourceArrowShape,
245
+ targetArrowShape: arrowShape.targetArrowShape
246
+ };
247
+ return {
248
+ group: "edges",
249
+ data: {
250
+ ...data,
251
+ source: edge.source,
252
+ target: edge.target
303
253
  }
304
- edges.push(
305
- createEdge(
306
- `inv-edge-tag-check:${tag.name}->${checkKey}`,
307
- `inv-tag:${tag.name}`,
308
- `inv-check:${checkKey}`,
309
- "tag-check",
310
- edgeColor
311
- )
312
- );
313
- }
314
- }
254
+ };
255
+ });
315
256
  return [...nodes, ...edges];
316
257
  }
317
258
 
318
259
  // src/core/styles.ts
319
- function createObservablesStylesheet(theme) {
260
+ function createSharedStylesheet(theme) {
320
261
  const resolved = resolveTheme(theme);
321
262
  return [
322
263
  {
@@ -327,34 +268,58 @@ function createObservablesStylesheet(theme) {
327
268
  height: "data(height)",
328
269
  label: "data(labelShort)",
329
270
  color: resolved.panelText,
330
- "font-size": 11,
271
+ "font-size": 10.5,
331
272
  "font-family": resolved.fontFamily,
332
273
  "font-weight": 500,
274
+ "min-zoomed-font-size": 8,
333
275
  "text-wrap": "none",
334
- "text-max-width": 210,
276
+ "text-max-width": 190,
335
277
  "text-halign": "center",
336
278
  "text-valign": "bottom",
337
- "text-margin-y": 10,
279
+ "text-margin-y": 9,
280
+ "text-background-color": resolved.panelBackground,
281
+ "text-background-opacity": 0.86,
282
+ "text-background-padding": 2,
283
+ "text-background-shape": "roundrectangle",
338
284
  "background-color": "data(fillColor)",
339
285
  "border-color": "data(borderColor)",
340
286
  "border-width": "data(borderWidth)",
341
287
  "background-image": "data(icon)",
342
288
  "background-fit": "none",
343
- "background-width": "13px",
344
- "background-height": "13px",
289
+ "background-width": "48%",
290
+ "background-height": "48%",
345
291
  "background-position-x": "50%",
346
292
  "background-position-y": "50%",
347
293
  "background-image-opacity": 1,
348
294
  "background-opacity": 1,
349
295
  opacity: "data(opacity)",
350
- "overlay-opacity": 0
296
+ "overlay-opacity": 0,
297
+ "transition-property": "opacity, border-width, border-color, underlay-opacity, underlay-padding",
298
+ "transition-duration": 140
351
299
  }
352
300
  },
353
301
  {
354
302
  selector: "node:selected",
355
303
  style: {
356
- "border-color": resolved.accent,
357
- "border-width": 3
304
+ "border-color": resolved.edgeSelectedColor,
305
+ "border-width": 2.5,
306
+ "underlay-color": resolved.accent,
307
+ "underlay-opacity": 0.12,
308
+ "underlay-padding": 7
309
+ }
310
+ },
311
+ {
312
+ selector: "node.cyvest-focus",
313
+ style: {
314
+ "underlay-color": resolved.accent,
315
+ "underlay-opacity": 0.08,
316
+ "underlay-padding": 6
317
+ }
318
+ },
319
+ {
320
+ selector: ".cyvest-dimmed",
321
+ style: {
322
+ opacity: 0.16
358
323
  }
359
324
  },
360
325
  {
@@ -367,169 +332,295 @@ function createObservablesStylesheet(theme) {
367
332
  "target-arrow-shape": "data(targetArrowShape)",
368
333
  "source-arrow-shape": "data(sourceArrowShape)",
369
334
  "curve-style": "bezier",
370
- "arrow-scale": 1,
371
- opacity: 0.88,
372
- "overlay-opacity": 0
335
+ "arrow-scale": 0.62,
336
+ opacity: 0.78,
337
+ "overlay-opacity": 0,
338
+ "transition-property": "opacity, line-color, width",
339
+ "transition-duration": 140
340
+ }
341
+ },
342
+ {
343
+ selector: "edge.cyvest-focus",
344
+ style: {
345
+ width: 1.7,
346
+ "line-color": resolved.edgeSelectedColor,
347
+ "target-arrow-color": resolved.edgeSelectedColor,
348
+ "source-arrow-color": resolved.edgeSelectedColor,
349
+ opacity: 0.9
373
350
  }
374
351
  },
375
352
  {
376
353
  selector: "edge:selected",
377
354
  style: {
355
+ width: 1.9,
378
356
  "line-color": resolved.edgeSelectedColor,
379
357
  "target-arrow-color": resolved.edgeSelectedColor,
380
358
  "source-arrow-color": resolved.edgeSelectedColor,
381
359
  label: "data(relationshipType)",
382
360
  color: resolved.panelTextMuted,
383
- "font-size": 10,
361
+ "font-size": 9,
384
362
  "font-family": resolved.fontFamily,
385
363
  "text-background-color": resolved.panelBackground,
386
- "text-background-opacity": 0.98,
364
+ "text-background-opacity": 0.94,
387
365
  "text-background-padding": 2
388
366
  }
389
367
  }
390
368
  ];
391
369
  }
392
- function createInvestigationStylesheet(theme) {
393
- const resolved = resolveTheme(theme);
370
+ function createObservablesStylesheet(theme) {
394
371
  return [
372
+ ...createSharedStylesheet(theme),
395
373
  {
396
- selector: "node",
397
- style: {
398
- shape: "data(shape)",
399
- width: "data(width)",
400
- height: "data(height)",
401
- label: "data(labelShort)",
402
- color: resolved.panelText,
403
- "font-size": 10,
404
- "font-family": resolved.fontFamily,
405
- "font-weight": 500,
406
- "text-wrap": "none",
407
- "text-max-width": 190,
408
- "text-halign": "center",
409
- "text-valign": "center",
410
- "text-justification": "center",
411
- "background-color": "data(fillColor)",
412
- "border-color": "data(borderColor)",
413
- "border-width": "data(borderWidth)",
414
- "background-image": "data(icon)",
415
- "background-fit": "none",
416
- "background-width": "18px",
417
- "background-height": "18px",
418
- "background-position-x": "10px",
419
- "background-position-y": "50%",
420
- "background-image-opacity": 0.9,
421
- "text-margin-x": 0,
422
- "overlay-opacity": 0
423
- }
424
- },
425
- {
426
- selector: "node[nodeType = 'check']",
427
- style: {
428
- "text-halign": "center",
429
- "background-position-x": "10px"
430
- }
431
- },
432
- {
433
- selector: "node:selected",
434
- style: {
435
- "border-color": resolved.accent,
436
- "border-width": 3
437
- }
438
- },
439
- {
440
- selector: "edge",
441
- style: {
442
- width: "data(width)",
443
- "line-color": "data(color)",
444
- "target-arrow-color": "data(color)",
445
- "source-arrow-color": "data(color)",
446
- "target-arrow-shape": "data(targetArrowShape)",
447
- "source-arrow-shape": "data(sourceArrowShape)",
448
- "curve-style": "taxi",
449
- "taxi-direction": "rightward",
450
- "taxi-turn": "26px",
451
- "arrow-scale": 0.95,
452
- opacity: 0.9,
453
- "overlay-opacity": 0
454
- }
455
- },
456
- {
457
- selector: "edge:selected",
374
+ selector: "node[?isRoot]",
458
375
  style: {
459
- "line-color": resolved.edgeSelectedColor,
460
- "target-arrow-color": resolved.edgeSelectedColor,
461
- "source-arrow-color": resolved.edgeSelectedColor
376
+ "font-weight": 700,
377
+ "text-margin-y": 11
462
378
  }
463
379
  }
464
380
  ];
465
381
  }
466
382
 
467
- // src/layout/elk.ts
383
+ // src/layout/force.ts
384
+ import {
385
+ forceCenter,
386
+ forceCollide,
387
+ forceLink,
388
+ forceManyBody,
389
+ forceRadial,
390
+ forceSimulation,
391
+ forceX,
392
+ forceY
393
+ } from "d3-force";
468
394
  var DEFAULT_OBSERVABLES_LAYOUT = {
469
- algorithm: "stress",
470
- spacingNodeNode: 70,
471
- spacingEdgeNode: 40,
472
- padding: 56,
395
+ linkDistance: 118,
396
+ linkStrength: 0.72,
397
+ chargeStrength: -420,
398
+ collisionPadding: 30,
399
+ radialStep: 128,
400
+ radialStrength: 0.38,
401
+ centerStrength: 0.055,
402
+ iterations: 320,
403
+ padding: 72,
473
404
  fit: true,
474
- animate: false
405
+ animate: true,
406
+ animationDuration: 420
475
407
  };
476
- var DEFAULT_INVESTIGATION_LAYOUT = {
477
- algorithm: "dagre",
478
- direction: "RIGHT",
479
- spacingNodeNode: 50,
480
- spacingEdgeNode: 30,
481
- spacingBetweenLayers: 120,
482
- padding: 56,
483
- fit: true,
484
- animate: false
485
- };
486
- function getDefaultElkOptions(view) {
487
- return view === "observables" ? { ...DEFAULT_OBSERVABLES_LAYOUT } : { ...DEFAULT_INVESTIGATION_LAYOUT };
408
+ function getDefaultForceOptions() {
409
+ return { ...DEFAULT_OBSERVABLES_LAYOUT };
488
410
  }
489
- function mergeElkOptions(view, overrides) {
411
+ function resolveOptions(overrides) {
490
412
  return {
491
- ...getDefaultElkOptions(view),
492
- ...overrides,
493
- extra: {
494
- ...getDefaultElkOptions(view).extra ?? {},
495
- ...overrides?.extra ?? {}
496
- }
413
+ ...getDefaultForceOptions(),
414
+ ...overrides
497
415
  };
498
416
  }
499
- function createElkLayout(view, overrides) {
500
- const merged = mergeElkOptions(view, overrides);
501
- if (view === "investigation") {
417
+ function configureSimulation(nodes, links, options) {
418
+ return forceSimulation(nodes).alpha(1).alphaDecay(1 - Math.pow(1e-3, 1 / options.iterations)).velocityDecay(0.34).force(
419
+ "link",
420
+ forceLink(links).id((node) => node.id).distance((link) => {
421
+ const sourceDepth = typeof link.source === "string" ? 0 : link.source.depth;
422
+ const targetDepth = typeof link.target === "string" ? 0 : link.target.depth;
423
+ return options.linkDistance + Math.abs(sourceDepth - targetDepth) * 8;
424
+ }).strength(options.linkStrength)
425
+ ).force("charge", forceManyBody().strength(options.chargeStrength)).force(
426
+ "collision",
427
+ forceCollide().radius((node) => node.radius + options.collisionPadding).strength(0.92).iterations(3)
428
+ ).force(
429
+ "radial",
430
+ forceRadial(
431
+ (node) => node.depth * options.radialStep,
432
+ 0,
433
+ 0
434
+ ).strength((node) => node.isRoot ? 1 : options.radialStrength)
435
+ ).force("center", forceCenter(0, 0).strength(options.centerStrength)).force("x", forceX(0).strength(options.centerStrength)).force("y", forceY(0).strength(options.centerStrength));
436
+ }
437
+ function getNodeId(element) {
438
+ return String(element.data.id);
439
+ }
440
+ function getNodeRadius(element) {
441
+ const width = Number(element.data.width ?? 40);
442
+ const height = Number(element.data.height ?? width);
443
+ return Math.max(width, height) / 2;
444
+ }
445
+ function findRootId(nodes) {
446
+ const explicitRoot = nodes.find(
447
+ (node) => node.data.isRoot === true || node.data.nodeType === "root"
448
+ );
449
+ return explicitRoot ? getNodeId(explicitRoot) : nodes[0] ? getNodeId(nodes[0]) : void 0;
450
+ }
451
+ function calculateDepths(nodeIds, links, rootId) {
452
+ const depths = /* @__PURE__ */ new Map();
453
+ if (!rootId) {
454
+ return depths;
455
+ }
456
+ const neighbors = /* @__PURE__ */ new Map();
457
+ for (const id of nodeIds) {
458
+ neighbors.set(id, []);
459
+ }
460
+ for (const link of links) {
461
+ const source = String(link.source);
462
+ const target = String(link.target);
463
+ neighbors.get(source)?.push(target);
464
+ neighbors.get(target)?.push(source);
465
+ }
466
+ const queue = [rootId];
467
+ depths.set(rootId, 0);
468
+ while (queue.length > 0) {
469
+ const current = queue.shift();
470
+ if (!current) continue;
471
+ const nextDepth = (depths.get(current) ?? 0) + 1;
472
+ for (const neighbor of neighbors.get(current) ?? []) {
473
+ if (depths.has(neighbor)) continue;
474
+ depths.set(neighbor, nextDepth);
475
+ queue.push(neighbor);
476
+ }
477
+ }
478
+ const fallbackDepth = Math.max(1, ...depths.values()) + 1;
479
+ for (const nodeId of nodeIds) {
480
+ if (!depths.has(nodeId)) {
481
+ depths.set(nodeId, fallbackDepth);
482
+ }
483
+ }
484
+ return depths;
485
+ }
486
+ function computeForcePositions(elements, overrides) {
487
+ const options = resolveOptions(overrides);
488
+ const nodeElements = elements.filter((element) => element.group === "nodes");
489
+ const edgeElements = elements.filter((element) => element.group === "edges");
490
+ const nodeIds = nodeElements.map(getNodeId);
491
+ const rootId = findRootId(nodeElements);
492
+ const links = edgeElements.map((edge) => ({
493
+ source: String(edge.data.source),
494
+ target: String(edge.data.target)
495
+ }));
496
+ const depths = calculateDepths(nodeIds, links, rootId);
497
+ const nodes = nodeElements.map((element) => {
498
+ const id = getNodeId(element);
499
+ const isRoot = id === rootId;
502
500
  return {
503
- name: "dagre",
504
- fit: merged.fit ?? true,
505
- padding: merged.padding ?? 56,
506
- animate: merged.animate ?? false,
507
- // Force horizontal left-to-right investigation flow.
508
- rankDir: "LR",
509
- rankSep: merged.spacingBetweenLayers ?? 120,
510
- nodeSep: merged.spacingNodeNode ?? 50,
511
- edgeSep: merged.spacingEdgeNode ?? 30,
512
- ...merged.extra ?? {}
501
+ id,
502
+ radius: getNodeRadius(element),
503
+ depth: depths.get(id) ?? 1,
504
+ isRoot,
505
+ ...isRoot ? { fx: 0, fy: 0 } : {}
513
506
  };
507
+ });
508
+ const simulation = configureSimulation(nodes, links, options).stop();
509
+ for (let index = 0; index < options.iterations; index += 1) {
510
+ simulation.tick();
514
511
  }
515
- const elkOptions = {
516
- "elk.algorithm": merged.algorithm ?? "stress",
517
- "elk.spacing.nodeNode": merged.spacingNodeNode ?? 60,
518
- "elk.spacing.edgeNode": merged.spacingEdgeNode ?? 30,
519
- ...merged.direction ? { "elk.direction": merged.direction } : {},
520
- ...merged.spacingBetweenLayers ? {
521
- "elk.layered.spacing.nodeNodeBetweenLayers": merged.spacingBetweenLayers
522
- } : {},
523
- ...merged.extra ?? {}
512
+ return Object.fromEntries(
513
+ nodes.map((node) => [
514
+ node.id,
515
+ {
516
+ x: Number.isFinite(node.x) ? node.x ?? 0 : 0,
517
+ y: Number.isFinite(node.y) ? node.y ?? 0 : 0
518
+ }
519
+ ])
520
+ );
521
+ }
522
+ function startForceSimulation(cy, overrides) {
523
+ if (cy.nodes().empty()) {
524
+ return {
525
+ reheat: () => void 0,
526
+ stop: () => void 0
527
+ };
528
+ }
529
+ const options = resolveOptions(overrides);
530
+ const cyNodes = cy.nodes();
531
+ const nodeIds = cyNodes.map((node) => node.id());
532
+ const rootNode = cyNodes.filter(
533
+ (node) => node.data("isRoot") === true || node.data("nodeType") === "root"
534
+ ).first();
535
+ const rootId = (!rootNode.empty() ? rootNode : cyNodes.first()).id();
536
+ const links = cy.edges().map((edge) => ({
537
+ source: edge.source().id(),
538
+ target: edge.target().id()
539
+ }));
540
+ const depths = calculateDepths(nodeIds, links, rootId);
541
+ const nodeById = /* @__PURE__ */ new Map();
542
+ const nodes = cyNodes.map((node) => {
543
+ const position = node.position();
544
+ const forceNode = {
545
+ id: node.id(),
546
+ radius: Math.max(
547
+ Number(node.data("width") ?? 40),
548
+ Number(node.data("height") ?? 40)
549
+ ) / 2,
550
+ depth: depths.get(node.id()) ?? 1,
551
+ isRoot: node.id() === rootId,
552
+ x: position.x,
553
+ y: position.y
554
+ };
555
+ if (forceNode.isRoot) {
556
+ forceNode.fx = position.x;
557
+ forceNode.fy = position.y;
558
+ }
559
+ nodeById.set(forceNode.id, forceNode);
560
+ return forceNode;
561
+ });
562
+ const simulation = configureSimulation(nodes, links, options).alpha(0.42).alphaTarget(0).on("tick", () => {
563
+ cy.batch(() => {
564
+ for (const forceNode of nodes) {
565
+ const cyNode = cy.getElementById(forceNode.id);
566
+ if (cyNode.empty() || cyNode.grabbed()) continue;
567
+ cyNode.position({
568
+ x: forceNode.x ?? 0,
569
+ y: forceNode.y ?? 0
570
+ });
571
+ }
572
+ });
573
+ });
574
+ const handleGrab = (event) => {
575
+ const forceNode = nodeById.get(event.target.id());
576
+ if (!forceNode) return;
577
+ const position = event.target.position();
578
+ forceNode.fx = position.x;
579
+ forceNode.fy = position.y;
580
+ simulation.alphaTarget(0.22).restart();
581
+ };
582
+ const handleDrag = (event) => {
583
+ const forceNode = nodeById.get(event.target.id());
584
+ if (!forceNode) return;
585
+ const position = event.target.position();
586
+ forceNode.fx = position.x;
587
+ forceNode.fy = position.y;
524
588
  };
589
+ const handleFree = (event) => {
590
+ const forceNode = nodeById.get(event.target.id());
591
+ if (!forceNode) return;
592
+ if (!forceNode.isRoot) {
593
+ forceNode.fx = null;
594
+ forceNode.fy = null;
595
+ }
596
+ simulation.alphaTarget(0);
597
+ };
598
+ cy.on("grab", "node", handleGrab);
599
+ cy.on("drag", "node", handleDrag);
600
+ cy.on("free", "node", handleFree);
601
+ return {
602
+ reheat: () => {
603
+ simulation.alpha(0.72).alphaTarget(0).restart();
604
+ },
605
+ stop: () => {
606
+ simulation.stop();
607
+ cy.removeListener("grab", "node", handleGrab);
608
+ cy.removeListener("drag", "node", handleDrag);
609
+ cy.removeListener("free", "node", handleFree);
610
+ }
611
+ };
612
+ }
613
+ function createForceLayout(elements, overrides) {
614
+ const options = resolveOptions(overrides);
615
+ const positions = computeForcePositions(elements, overrides);
525
616
  return {
526
- name: "elk",
527
- fit: merged.fit ?? true,
528
- padding: merged.padding ?? 56,
529
- animate: merged.animate ?? false,
530
- nodeDimensionsIncludeLabels: true,
531
- // cytoscape-elk reads these options and forwards them to elkjs
532
- elk: elkOptions
617
+ name: "preset",
618
+ positions,
619
+ fit: options.fit,
620
+ padding: options.padding,
621
+ animate: options.animate,
622
+ animationDuration: options.animationDuration,
623
+ animationEasing: "ease-out-cubic"
533
624
  };
534
625
  }
535
626
 
@@ -538,19 +629,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
538
629
 
539
630
  // src/core/createCyInstance.ts
540
631
  import cytoscape from "cytoscape";
541
- import dagre from "cytoscape-dagre";
542
- import elk from "cytoscape-elk";
543
- var pluginRegistered = false;
544
- function ensurePluginsRegistered() {
545
- if (pluginRegistered) {
546
- return;
547
- }
548
- cytoscape.use(elk);
549
- cytoscape.use(dagre);
550
- pluginRegistered = true;
551
- }
552
632
  function createCyInstance(container) {
553
- ensurePluginsRegistered();
554
633
  return cytoscape({
555
634
  container,
556
635
  elements: [],
@@ -558,9 +637,9 @@ function createCyInstance(container) {
558
637
  autoungrabify: false,
559
638
  boxSelectionEnabled: false,
560
639
  selectionType: "single",
561
- wheelSensitivity: 0.2,
562
- minZoom: 0.1,
563
- maxZoom: 2.4
640
+ wheelSensitivity: 0.16,
641
+ minZoom: 0.16,
642
+ maxZoom: 3
564
643
  });
565
644
  }
566
645
 
@@ -587,10 +666,11 @@ function joinClassNames(...names) {
587
666
  return names.filter(Boolean).join(" ");
588
667
  }
589
668
  var CytoscapeCanvas = ({
590
- view,
591
669
  elements,
592
670
  stylesheet,
593
671
  layout,
672
+ forceOptions,
673
+ physics = true,
594
674
  width,
595
675
  height,
596
676
  className,
@@ -603,7 +683,12 @@ var CytoscapeCanvas = ({
603
683
  const containerRef = useRef(null);
604
684
  const cyRef = useRef(null);
605
685
  const layoutRef = useRef(layout);
686
+ const forceOptionsRef = useRef(forceOptions);
687
+ const physicsRef = useRef(physics);
688
+ const simulationRef = useRef(null);
606
689
  layoutRef.current = layout;
690
+ forceOptionsRef.current = forceOptions;
691
+ physicsRef.current = physics;
607
692
  useEffect(() => {
608
693
  if (!containerRef.current) {
609
694
  return;
@@ -612,6 +697,8 @@ var CytoscapeCanvas = ({
612
697
  cyRef.current = cy;
613
698
  onCyReady?.(cy);
614
699
  return () => {
700
+ simulationRef.current?.stop();
701
+ simulationRef.current = null;
615
702
  cy.destroy();
616
703
  cyRef.current = null;
617
704
  };
@@ -621,8 +708,19 @@ var CytoscapeCanvas = ({
621
708
  if (!cy) {
622
709
  return;
623
710
  }
624
- const runner = cy.layout(layoutRef.current);
711
+ simulationRef.current?.stop();
712
+ simulationRef.current = null;
713
+ const runner = cy.layout({
714
+ ...layoutRef.current,
715
+ animate: false
716
+ });
625
717
  runner.run();
718
+ if (physicsRef.current) {
719
+ simulationRef.current = startForceSimulation(
720
+ cy,
721
+ forceOptionsRef.current
722
+ );
723
+ }
626
724
  }, []);
627
725
  useEffect(() => {
628
726
  const cy = cyRef.current;
@@ -649,7 +747,7 @@ var CytoscapeCanvas = ({
649
747
  const data = node.data();
650
748
  const rawLabel = data.labelFull ?? data.labelShort ?? node.id();
651
749
  onNodeSelect({
652
- view,
750
+ view: "observables",
653
751
  nodeId: node.id(),
654
752
  nodeType: typeof data.nodeType === "string" ? data.nodeType : "unknown",
655
753
  label: String(rawLabel),
@@ -664,7 +762,7 @@ var CytoscapeCanvas = ({
664
762
  const edge = event.target;
665
763
  const data = edge.data();
666
764
  onEdgeSelect({
667
- view,
765
+ view: "observables",
668
766
  edgeId: edge.id(),
669
767
  sourceId: edge.source().id(),
670
768
  targetId: edge.target().id(),
@@ -673,13 +771,35 @@ var CytoscapeCanvas = ({
673
771
  element: edge
674
772
  });
675
773
  };
774
+ const handleNodeMouseOver = (event) => {
775
+ const node = event.target;
776
+ const neighborhood = node.closedNeighborhood();
777
+ cy.elements().addClass("cyvest-dimmed");
778
+ neighborhood.removeClass("cyvest-dimmed");
779
+ node.addClass("cyvest-focus");
780
+ node.connectedEdges().addClass("cyvest-focus");
781
+ };
782
+ const clearFocus = () => {
783
+ cy.elements().removeClass("cyvest-dimmed cyvest-focus");
784
+ };
785
+ const handleCanvasTap = (event) => {
786
+ if (event.target === cy) {
787
+ cy.elements().unselect();
788
+ }
789
+ };
676
790
  cy.on("tap", "node", handleNodeTap);
677
791
  cy.on("tap", "edge", handleEdgeTap);
792
+ cy.on("mouseover", "node", handleNodeMouseOver);
793
+ cy.on("mouseout", "node", clearFocus);
794
+ cy.on("tap", handleCanvasTap);
678
795
  return () => {
679
796
  cy.removeListener("tap", "node", handleNodeTap);
680
797
  cy.removeListener("tap", "edge", handleEdgeTap);
798
+ cy.removeListener("mouseover", "node", handleNodeMouseOver);
799
+ cy.removeListener("mouseout", "node", clearFocus);
800
+ cy.removeListener("tap", handleCanvasTap);
681
801
  };
682
- }, [onEdgeSelect, onNodeSelect, view]);
802
+ }, [onEdgeSelect, onNodeSelect]);
683
803
  const handleFit = useCallback(() => {
684
804
  const cy = cyRef.current;
685
805
  if (!cy) {
@@ -733,8 +853,8 @@ var CytoscapeCanvas = ({
733
853
  type: "button",
734
854
  className: "cyvest-toolbar__button",
735
855
  onClick: runLayout,
736
- title: "Re-run layout",
737
- "aria-label": "Re-run layout",
856
+ title: "Reheat physics",
857
+ "aria-label": "Reheat physics",
738
858
  children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
739
859
  /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }),
740
860
  /* @__PURE__ */ jsx("path", { d: "M21 3v6h-6" })
@@ -761,159 +881,8 @@ var CytoscapeCanvas = ({
761
881
  );
762
882
  };
763
883
 
764
- // src/components/CyvestInvestigationView.tsx
765
- import { jsx as jsx2 } from "react/jsx-runtime";
766
- var CyvestInvestigationView = ({
767
- investigation,
768
- height = 500,
769
- width = "100%",
770
- className,
771
- theme,
772
- onCyReady,
773
- onNodeSelect,
774
- onEdgeSelect,
775
- showToolbar = true,
776
- layout,
777
- maxLabelLength = 26
778
- }) => {
779
- const elements = useMemo2(
780
- () => buildInvestigationElements(investigation, {
781
- maxLabelLength,
782
- edgeColor: theme?.edgeColor
783
- }),
784
- [investigation, maxLabelLength, theme?.edgeColor]
785
- );
786
- const stylesheet = useMemo2(
787
- () => createInvestigationStylesheet(theme),
788
- [theme]
789
- );
790
- const elkLayout = useMemo2(
791
- () => createElkLayout("investigation", layout),
792
- [layout]
793
- );
794
- return /* @__PURE__ */ jsx2(
795
- CytoscapeCanvas,
796
- {
797
- view: "investigation",
798
- elements,
799
- stylesheet,
800
- layout: elkLayout,
801
- width,
802
- height,
803
- className,
804
- theme,
805
- onCyReady,
806
- onNodeSelect,
807
- onEdgeSelect,
808
- showToolbar
809
- }
810
- );
811
- };
812
-
813
884
  // src/components/CyvestObservablesView.tsx
814
- import { useMemo as useMemo3 } from "react";
815
-
816
- // src/adapters/observablesElements.ts
817
- import {
818
- getObservableGraph,
819
- getRootObservable as getRootObservable2
820
- } from "@cyvest/cyvest-js";
821
- function findFallbackRootId(graph) {
822
- if (graph.nodes.length === 0) {
823
- return void 0;
824
- }
825
- const incoming = /* @__PURE__ */ new Map();
826
- for (const node of graph.nodes) {
827
- incoming.set(node.id, 0);
828
- }
829
- for (const edge of graph.edges) {
830
- incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
831
- }
832
- const sourceCandidates = graph.nodes.filter((node) => (incoming.get(node.id) ?? 0) === 0);
833
- if (sourceCandidates.length === 0) {
834
- return graph.nodes[0]?.id;
835
- }
836
- sourceCandidates.sort((a, b) => b.score - a.score);
837
- return sourceCandidates[0]?.id;
838
- }
839
- function getArrowShapes(edge) {
840
- if (edge.direction === "bidirectional") {
841
- return {
842
- sourceArrowShape: "triangle",
843
- targetArrowShape: "triangle"
844
- };
845
- }
846
- if (edge.direction === "inbound") {
847
- return {
848
- sourceArrowShape: "triangle",
849
- targetArrowShape: "none"
850
- };
851
- }
852
- return {
853
- sourceArrowShape: "none",
854
- targetArrowShape: "triangle"
855
- };
856
- }
857
- function buildObservablesElements(investigation, options) {
858
- const graph = getObservableGraph(investigation);
859
- const rootObservable = getRootObservable2(investigation);
860
- const rootId = rootObservable?.key ?? findFallbackRootId(graph);
861
- const maxLabelLength = options?.maxLabelLength ?? 28;
862
- const edgeColor = options?.edgeColor ?? "#8a95aa";
863
- const nodes = graph.nodes.map((node) => {
864
- const isRoot = node.id === rootId;
865
- const borderColor = getLevelColor(node.level);
866
- const data = {
867
- id: node.id,
868
- nodeType: "observable",
869
- labelShort: truncateLabel(node.value, maxLabelLength, true),
870
- labelFull: node.value,
871
- observableType: node.type,
872
- level: node.level,
873
- score: node.score,
874
- isRoot,
875
- whitelisted: node.whitelisted,
876
- internal: node.internal,
877
- shape: "ellipse",
878
- width: 48,
879
- height: 48,
880
- borderWidth: 2,
881
- borderColor,
882
- fillColor: getLevelBackgroundColor(node.level),
883
- icon: getObservableIconSvg(node.type, { color: borderColor }),
884
- opacity: node.whitelisted ? 0.5 : 1
885
- };
886
- return {
887
- group: "nodes",
888
- data
889
- };
890
- });
891
- const nodeIds = new Set(graph.nodes.map((node) => node.id));
892
- const edges = graph.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge, index) => {
893
- const arrowShape = getArrowShapes(edge);
894
- const data = {
895
- id: `obs-edge-${index}-${edge.source}-${edge.target}-${edge.type}`,
896
- relationshipType: edge.type,
897
- direction: edge.direction,
898
- color: edgeColor,
899
- width: 1.6,
900
- sourceArrowShape: arrowShape.sourceArrowShape,
901
- targetArrowShape: arrowShape.targetArrowShape
902
- };
903
- return {
904
- group: "edges",
905
- data: {
906
- ...data,
907
- source: edge.source,
908
- target: edge.target
909
- }
910
- };
911
- });
912
- return [...nodes, ...edges];
913
- }
914
-
915
- // src/components/CyvestObservablesView.tsx
916
- import { jsx as jsx3 } from "react/jsx-runtime";
885
+ import { jsx as jsx2 } from "react/jsx-runtime";
917
886
  var CyvestObservablesView = ({
918
887
  investigation,
919
888
  height = 500,
@@ -923,32 +892,34 @@ var CyvestObservablesView = ({
923
892
  onCyReady,
924
893
  onNodeSelect,
925
894
  onEdgeSelect,
895
+ physics = true,
926
896
  showToolbar = true,
927
897
  layout,
928
898
  maxLabelLength = 28
929
899
  }) => {
930
- const elements = useMemo3(
900
+ const elements = useMemo2(
931
901
  () => buildObservablesElements(investigation, {
932
902
  maxLabelLength,
933
- edgeColor: theme?.edgeColor
903
+ theme
934
904
  }),
935
- [investigation, maxLabelLength, theme?.edgeColor]
905
+ [investigation, maxLabelLength, theme]
936
906
  );
937
- const stylesheet = useMemo3(
907
+ const stylesheet = useMemo2(
938
908
  () => createObservablesStylesheet(theme),
939
909
  [theme]
940
910
  );
941
- const elkLayout = useMemo3(
942
- () => createElkLayout("observables", layout),
943
- [layout]
911
+ const forceLayout = useMemo2(
912
+ () => createForceLayout(elements, layout),
913
+ [elements, layout]
944
914
  );
945
- return /* @__PURE__ */ jsx3(
915
+ return /* @__PURE__ */ jsx2(
946
916
  CytoscapeCanvas,
947
917
  {
948
- view: "observables",
949
918
  elements,
950
919
  stylesheet,
951
- layout: elkLayout,
920
+ layout: forceLayout,
921
+ forceOptions: layout,
922
+ physics,
952
923
  width,
953
924
  height,
954
925
  className,
@@ -962,10 +933,7 @@ var CyvestObservablesView = ({
962
933
  };
963
934
 
964
935
  // src/components/CyvestGraph.tsx
965
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
966
- function normalizeInitialView(view) {
967
- return view === "investigation" ? "investigation" : "observables";
968
- }
936
+ import { jsx as jsx3 } from "react/jsx-runtime";
969
937
  var CyvestGraph = ({
970
938
  investigation,
971
939
  height = 500,
@@ -975,104 +943,41 @@ var CyvestGraph = ({
975
943
  onCyReady,
976
944
  onNodeSelect,
977
945
  onEdgeSelect,
978
- initialView = "observables",
979
- showViewToggle = true,
980
- onViewChange,
946
+ physics = true,
981
947
  showToolbar = true,
982
- observablesLayout,
983
- investigationLayout,
984
- maxObservableLabelLength = 28,
985
- maxInvestigationLabelLength = 26
986
- }) => {
987
- const [activeView, setActiveView] = useState(
988
- () => normalizeInitialView(initialView)
989
- );
990
- useEffect2(() => {
991
- setActiveView(normalizeInitialView(initialView));
992
- }, [initialView]);
993
- useEffect2(() => {
994
- onViewChange?.(activeView);
995
- }, [activeView, onViewChange]);
996
- const containerStyle = useMemo4(
997
- () => ({
998
- width,
999
- height,
1000
- position: "relative"
1001
- }),
1002
- [width, height]
1003
- );
1004
- const handleViewChange = useCallback2((view) => {
1005
- setActiveView(view);
1006
- }, []);
1007
- return /* @__PURE__ */ jsxs2("div", { className, style: containerStyle, children: [
1008
- showViewToggle && /* @__PURE__ */ jsxs2("div", { className: "cyvest-view-toggle", role: "tablist", "aria-label": "Graph view mode", children: [
1009
- /* @__PURE__ */ jsx4(
1010
- "button",
1011
- {
1012
- type: "button",
1013
- className: activeView === "observables" ? "cyvest-view-toggle__button cyvest-view-toggle__button--active" : "cyvest-view-toggle__button",
1014
- onClick: () => handleViewChange("observables"),
1015
- role: "tab",
1016
- "aria-selected": activeView === "observables",
1017
- children: "Observables"
1018
- }
1019
- ),
1020
- /* @__PURE__ */ jsx4(
1021
- "button",
1022
- {
1023
- type: "button",
1024
- className: activeView === "investigation" ? "cyvest-view-toggle__button cyvest-view-toggle__button--active" : "cyvest-view-toggle__button",
1025
- onClick: () => handleViewChange("investigation"),
1026
- role: "tab",
1027
- "aria-selected": activeView === "investigation",
1028
- children: "Investigation"
1029
- }
1030
- )
1031
- ] }),
1032
- activeView === "observables" ? /* @__PURE__ */ jsx4(
1033
- CyvestObservablesView,
1034
- {
1035
- investigation,
1036
- width: "100%",
1037
- height: "100%",
1038
- theme,
1039
- onCyReady,
1040
- onNodeSelect,
1041
- onEdgeSelect,
1042
- showToolbar,
1043
- layout: observablesLayout,
1044
- maxLabelLength: maxObservableLabelLength
1045
- }
1046
- ) : /* @__PURE__ */ jsx4(
1047
- CyvestInvestigationView,
1048
- {
1049
- investigation,
1050
- width: "100%",
1051
- height: "100%",
1052
- theme,
1053
- onCyReady,
1054
- onNodeSelect,
1055
- onEdgeSelect,
1056
- showToolbar,
1057
- layout: investigationLayout,
1058
- maxLabelLength: maxInvestigationLabelLength
1059
- }
1060
- )
1061
- ] });
1062
- };
948
+ layout,
949
+ maxLabelLength = 28
950
+ }) => /* @__PURE__ */ jsx3(
951
+ CyvestObservablesView,
952
+ {
953
+ investigation,
954
+ width,
955
+ height,
956
+ className,
957
+ theme,
958
+ onCyReady,
959
+ onNodeSelect,
960
+ onEdgeSelect,
961
+ physics,
962
+ showToolbar,
963
+ layout,
964
+ maxLabelLength
965
+ }
966
+ );
1063
967
  export {
1064
968
  CyvestGraph,
1065
- CyvestInvestigationView,
1066
969
  CyvestObservablesView,
970
+ DARK_CYVEST_THEME,
1067
971
  DEFAULT_CYVEST_THEME,
1068
- INVESTIGATION_ICON_NAME_MAP,
1069
972
  OBSERVABLE_ICON_NAME_MAP,
1070
- createElkLayout,
1071
- getDefaultElkOptions,
1072
- getInvestigationIconSvg,
973
+ computeForcePositions,
974
+ createForceLayout,
975
+ getDefaultForceOptions,
1073
976
  getLevelBackgroundColor,
1074
977
  getLevelColor,
1075
978
  getObservableIconSvg,
1076
979
  lightenHexColor,
980
+ mixHexColor,
981
+ startForceSimulation,
1077
982
  truncateLabel
1078
983
  };