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