@cyvest/cyvest-vis 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +1293 -0
- package/dist/index.mjs +1284 -0
- package/package.json +34 -0
- package/src/components/CyvestGraph.tsx +117 -0
- package/src/components/FloatingEdge.tsx +43 -0
- package/src/components/InvestigationGraph.tsx +257 -0
- package/src/components/InvestigationNode.tsx +145 -0
- package/src/components/ObservableNode.tsx +173 -0
- package/src/components/ObservablesGraph.tsx +376 -0
- package/src/hooks/useDagreLayout.ts +102 -0
- package/src/hooks/useForceLayout.ts +383 -0
- package/src/index.ts +13 -0
- package/src/types.ts +193 -0
- package/src/utils/observables.ts +164 -0
- package/tests/observables.test.ts +43 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for computing force-directed layout using d3-force.
|
|
3
|
+
* Uses an iterative simulation that updates on each tick.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useRef, useCallback, useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
forceSimulation,
|
|
9
|
+
forceLink,
|
|
10
|
+
forceManyBody,
|
|
11
|
+
forceCenter,
|
|
12
|
+
forceCollide,
|
|
13
|
+
forceX,
|
|
14
|
+
forceY,
|
|
15
|
+
type Simulation,
|
|
16
|
+
type SimulationNodeDatum,
|
|
17
|
+
type SimulationLinkDatum,
|
|
18
|
+
} from "d3-force";
|
|
19
|
+
import {
|
|
20
|
+
useReactFlow,
|
|
21
|
+
useNodesInitialized,
|
|
22
|
+
useStore,
|
|
23
|
+
type Node,
|
|
24
|
+
type Edge,
|
|
25
|
+
} from "@xyflow/react";
|
|
26
|
+
import type { ForceLayoutConfig } from "../types";
|
|
27
|
+
import { DEFAULT_FORCE_CONFIG } from "../types";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* D3 simulation node with position.
|
|
31
|
+
*/
|
|
32
|
+
interface SimNode extends SimulationNodeDatum {
|
|
33
|
+
id: string;
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
fx?: number | null;
|
|
37
|
+
fy?: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* D3 simulation link.
|
|
42
|
+
*/
|
|
43
|
+
interface SimLink extends SimulationLinkDatum<SimNode> {
|
|
44
|
+
source: string | SimNode;
|
|
45
|
+
target: string | SimNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Selector to check if nodes are initialized
|
|
50
|
+
*/
|
|
51
|
+
const nodeCountSelector = (state: { nodeLookup: Map<string, Node> }) =>
|
|
52
|
+
state.nodeLookup.size;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook that applies iterative force-directed layout to React Flow nodes.
|
|
56
|
+
* The simulation runs continuously and updates node positions on each tick.
|
|
57
|
+
*/
|
|
58
|
+
export function useForceLayout(
|
|
59
|
+
config: Partial<ForceLayoutConfig> = {},
|
|
60
|
+
rootNodeId?: string
|
|
61
|
+
) {
|
|
62
|
+
const { getNodes, getEdges, setNodes } = useReactFlow();
|
|
63
|
+
const nodesInitialized = useNodesInitialized();
|
|
64
|
+
const nodeCount = useStore(nodeCountSelector);
|
|
65
|
+
|
|
66
|
+
// Merge config with defaults
|
|
67
|
+
const forceConfig = useMemo(
|
|
68
|
+
() => ({ ...DEFAULT_FORCE_CONFIG, ...config }),
|
|
69
|
+
[config]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Store the simulation reference
|
|
73
|
+
const simulationRef = useRef<Simulation<SimNode, SimLink> | null>(null);
|
|
74
|
+
|
|
75
|
+
// Store dragging state
|
|
76
|
+
const draggingNodeRef = useRef<string | null>(null);
|
|
77
|
+
|
|
78
|
+
// Initialize and run the simulation
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!nodesInitialized || nodeCount === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nodes = getNodes();
|
|
85
|
+
const edges = getEdges();
|
|
86
|
+
|
|
87
|
+
// Create simulation nodes from React Flow nodes
|
|
88
|
+
const simNodes: SimNode[] = nodes.map((node) => {
|
|
89
|
+
// Check if this node already exists in the simulation
|
|
90
|
+
const existingNode = simulationRef.current?.nodes().find((n) => n.id === node.id);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: node.id,
|
|
94
|
+
// Use existing simulation position or node position
|
|
95
|
+
x: existingNode?.x ?? node.position.x ?? Math.random() * 500 - 250,
|
|
96
|
+
y: existingNode?.y ?? node.position.y ?? Math.random() * 500 - 250,
|
|
97
|
+
// Preserve fixed positions for dragged nodes
|
|
98
|
+
fx: existingNode?.fx ?? null,
|
|
99
|
+
fy: existingNode?.fy ?? null,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Fix root node at center
|
|
104
|
+
if (rootNodeId) {
|
|
105
|
+
const rootNode = simNodes.find((n) => n.id === rootNodeId);
|
|
106
|
+
if (rootNode) {
|
|
107
|
+
rootNode.x = 0;
|
|
108
|
+
rootNode.y = 0;
|
|
109
|
+
rootNode.fx = 0;
|
|
110
|
+
rootNode.fy = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create simulation links from React Flow edges
|
|
115
|
+
const simLinks: SimLink[] = edges.map((edge) => ({
|
|
116
|
+
source: edge.source,
|
|
117
|
+
target: edge.target,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// Stop existing simulation
|
|
121
|
+
if (simulationRef.current) {
|
|
122
|
+
simulationRef.current.stop();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create the force simulation
|
|
126
|
+
const simulation = forceSimulation<SimNode>(simNodes)
|
|
127
|
+
.force(
|
|
128
|
+
"link",
|
|
129
|
+
forceLink<SimNode, SimLink>(simLinks)
|
|
130
|
+
.id((d) => d.id)
|
|
131
|
+
.distance(forceConfig.linkDistance)
|
|
132
|
+
.strength(0.5)
|
|
133
|
+
)
|
|
134
|
+
.force(
|
|
135
|
+
"charge",
|
|
136
|
+
forceManyBody<SimNode>().strength(forceConfig.chargeStrength)
|
|
137
|
+
)
|
|
138
|
+
.force(
|
|
139
|
+
"center",
|
|
140
|
+
forceCenter(0, 0).strength(forceConfig.centerStrength)
|
|
141
|
+
)
|
|
142
|
+
.force(
|
|
143
|
+
"collision",
|
|
144
|
+
forceCollide<SimNode>(forceConfig.collisionRadius)
|
|
145
|
+
)
|
|
146
|
+
.force(
|
|
147
|
+
"x",
|
|
148
|
+
forceX<SimNode>(0).strength(0.01)
|
|
149
|
+
)
|
|
150
|
+
.force(
|
|
151
|
+
"y",
|
|
152
|
+
forceY<SimNode>(0).strength(0.01)
|
|
153
|
+
)
|
|
154
|
+
.alphaDecay(0.02)
|
|
155
|
+
.velocityDecay(0.4);
|
|
156
|
+
|
|
157
|
+
// Update React Flow nodes on each tick
|
|
158
|
+
simulation.on("tick", () => {
|
|
159
|
+
setNodes((currentNodes) =>
|
|
160
|
+
currentNodes.map((node) => {
|
|
161
|
+
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
162
|
+
if (!simNode) return node;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...node,
|
|
166
|
+
position: {
|
|
167
|
+
x: simNode.x,
|
|
168
|
+
y: simNode.y,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
simulationRef.current = simulation;
|
|
176
|
+
|
|
177
|
+
// Cleanup: stop simulation when unmounting or dependencies change
|
|
178
|
+
return () => {
|
|
179
|
+
simulation.stop();
|
|
180
|
+
};
|
|
181
|
+
}, [
|
|
182
|
+
nodesInitialized,
|
|
183
|
+
nodeCount,
|
|
184
|
+
getNodes,
|
|
185
|
+
getEdges,
|
|
186
|
+
setNodes,
|
|
187
|
+
forceConfig,
|
|
188
|
+
rootNodeId,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle drag start - fix the node position and reheat simulation
|
|
193
|
+
*/
|
|
194
|
+
const onNodeDragStart = useCallback(
|
|
195
|
+
(_: React.MouseEvent, node: Node) => {
|
|
196
|
+
const simulation = simulationRef.current;
|
|
197
|
+
if (!simulation) return;
|
|
198
|
+
|
|
199
|
+
draggingNodeRef.current = node.id;
|
|
200
|
+
|
|
201
|
+
// Reheat the simulation
|
|
202
|
+
simulation.alphaTarget(0.3).restart();
|
|
203
|
+
|
|
204
|
+
// Fix the node position
|
|
205
|
+
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
206
|
+
if (simNode) {
|
|
207
|
+
simNode.fx = simNode.x;
|
|
208
|
+
simNode.fy = simNode.y;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
[]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Handle drag - update the fixed position
|
|
216
|
+
*/
|
|
217
|
+
const onNodeDrag = useCallback(
|
|
218
|
+
(_: React.MouseEvent, node: Node) => {
|
|
219
|
+
const simulation = simulationRef.current;
|
|
220
|
+
if (!simulation) return;
|
|
221
|
+
|
|
222
|
+
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
223
|
+
if (simNode) {
|
|
224
|
+
simNode.fx = node.position.x;
|
|
225
|
+
simNode.fy = node.position.y;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
[]
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handle drag end - unfix the node and let simulation cool down
|
|
233
|
+
*/
|
|
234
|
+
const onNodeDragStop = useCallback(
|
|
235
|
+
(_: React.MouseEvent, node: Node) => {
|
|
236
|
+
const simulation = simulationRef.current;
|
|
237
|
+
if (!simulation) return;
|
|
238
|
+
|
|
239
|
+
draggingNodeRef.current = null;
|
|
240
|
+
|
|
241
|
+
// Let simulation cool down
|
|
242
|
+
simulation.alphaTarget(0);
|
|
243
|
+
|
|
244
|
+
// Unfix the node (unless it's the root)
|
|
245
|
+
if (node.id !== rootNodeId) {
|
|
246
|
+
const simNode = simulation.nodes().find((n) => n.id === node.id);
|
|
247
|
+
if (simNode) {
|
|
248
|
+
simNode.fx = null;
|
|
249
|
+
simNode.fy = null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[rootNodeId]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Update force configuration dynamically
|
|
258
|
+
*/
|
|
259
|
+
const updateForceConfig = useCallback(
|
|
260
|
+
(updates: Partial<ForceLayoutConfig>) => {
|
|
261
|
+
const simulation = simulationRef.current;
|
|
262
|
+
if (!simulation) return;
|
|
263
|
+
|
|
264
|
+
if (updates.chargeStrength !== undefined) {
|
|
265
|
+
simulation.force(
|
|
266
|
+
"charge",
|
|
267
|
+
forceManyBody<SimNode>().strength(updates.chargeStrength)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (updates.linkDistance !== undefined) {
|
|
272
|
+
const linkForce = simulation.force("link") as ReturnType<typeof forceLink<SimNode, SimLink>> | undefined;
|
|
273
|
+
if (linkForce) {
|
|
274
|
+
linkForce.distance(updates.linkDistance);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (updates.collisionRadius !== undefined) {
|
|
279
|
+
simulation.force(
|
|
280
|
+
"collision",
|
|
281
|
+
forceCollide<SimNode>(updates.collisionRadius)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Reheat simulation to apply changes
|
|
286
|
+
simulation.alpha(0.5).restart();
|
|
287
|
+
},
|
|
288
|
+
[]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Manually restart the simulation
|
|
293
|
+
*/
|
|
294
|
+
const restartSimulation = useCallback(() => {
|
|
295
|
+
const simulation = simulationRef.current;
|
|
296
|
+
if (!simulation) return;
|
|
297
|
+
|
|
298
|
+
simulation.alpha(1).restart();
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
onNodeDragStart,
|
|
303
|
+
onNodeDrag,
|
|
304
|
+
onNodeDragStop,
|
|
305
|
+
updateForceConfig,
|
|
306
|
+
restartSimulation,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* One-time force layout computation (for static layouts).
|
|
312
|
+
* Use this when you don't need continuous simulation.
|
|
313
|
+
*/
|
|
314
|
+
export function computeForceLayout(
|
|
315
|
+
nodes: Node[],
|
|
316
|
+
edges: Edge[],
|
|
317
|
+
config: ForceLayoutConfig,
|
|
318
|
+
centerX: number = 0,
|
|
319
|
+
centerY: number = 0,
|
|
320
|
+
rootNodeId?: string
|
|
321
|
+
): { nodes: Node[]; edges: Edge[] } {
|
|
322
|
+
if (nodes.length === 0) {
|
|
323
|
+
return { nodes, edges };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create simulation nodes
|
|
327
|
+
const simNodes: SimNode[] = nodes.map((node) => ({
|
|
328
|
+
id: node.id,
|
|
329
|
+
x: node.position.x || Math.random() * 400 - 200,
|
|
330
|
+
y: node.position.y || Math.random() * 400 - 200,
|
|
331
|
+
fx: null,
|
|
332
|
+
fy: null,
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
// Find root node and fix it at center
|
|
336
|
+
if (rootNodeId) {
|
|
337
|
+
const rootNode = simNodes.find((n) => n.id === rootNodeId);
|
|
338
|
+
if (rootNode) {
|
|
339
|
+
rootNode.x = centerX;
|
|
340
|
+
rootNode.y = centerY;
|
|
341
|
+
rootNode.fx = centerX;
|
|
342
|
+
rootNode.fy = centerY;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Create simulation links
|
|
347
|
+
const simLinks: SimLink[] = edges.map((edge) => ({
|
|
348
|
+
source: edge.source,
|
|
349
|
+
target: edge.target,
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
// Create and run simulation
|
|
353
|
+
const simulation = forceSimulation<SimNode>(simNodes)
|
|
354
|
+
.force(
|
|
355
|
+
"link",
|
|
356
|
+
forceLink<SimNode, SimLink>(simLinks)
|
|
357
|
+
.id((d) => d.id)
|
|
358
|
+
.distance(config.linkDistance)
|
|
359
|
+
)
|
|
360
|
+
.force("charge", forceManyBody().strength(config.chargeStrength))
|
|
361
|
+
.force("center", forceCenter(centerX, centerY).strength(config.centerStrength))
|
|
362
|
+
.force("collision", forceCollide(config.collisionRadius))
|
|
363
|
+
.stop();
|
|
364
|
+
|
|
365
|
+
// Run simulation ticks
|
|
366
|
+
for (let i = 0; i < config.iterations; i++) {
|
|
367
|
+
simulation.tick();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Create positioned nodes
|
|
371
|
+
const positionedNodes = nodes.map((node) => {
|
|
372
|
+
const simNode = simNodes.find((n) => n.id === node.id);
|
|
373
|
+
return {
|
|
374
|
+
...node,
|
|
375
|
+
position: {
|
|
376
|
+
x: simNode?.x ?? node.position.x,
|
|
377
|
+
y: simNode?.y ?? node.position.y,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return { nodes: positionedNodes, edges };
|
|
383
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cyvest Visualization Library
|
|
3
|
+
*
|
|
4
|
+
* React components for visualizing Cyvest investigations using React Flow.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Main component export
|
|
10
|
+
export { CyvestGraph } from "./components/CyvestGraph";
|
|
11
|
+
|
|
12
|
+
// Re-export types for consumers
|
|
13
|
+
export type { CyvestGraphProps, ForceLayoutConfig } from "./types";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for cyvest-vis visualization library.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Node, Edge } from "@xyflow/react";
|
|
6
|
+
import type { Level } from "@cyvest/cyvest-js";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Observable Graph Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shape types for observable nodes.
|
|
14
|
+
*/
|
|
15
|
+
export type ObservableShape = "square" | "circle" | "rectangle" | "triangle";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Data attached to observable graph nodes.
|
|
19
|
+
*/
|
|
20
|
+
export interface ObservableNodeData extends Record<string, unknown> {
|
|
21
|
+
/** Display label (may be truncated) */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Full observable value */
|
|
24
|
+
fullValue: string;
|
|
25
|
+
/** Observable type (e.g., "domain-name", "ipv4-addr") */
|
|
26
|
+
observableType: string;
|
|
27
|
+
/** Security level */
|
|
28
|
+
level: Level;
|
|
29
|
+
/** Numeric score */
|
|
30
|
+
score: number;
|
|
31
|
+
/** Emoji representing the observable type */
|
|
32
|
+
emoji: string;
|
|
33
|
+
/** Shape for this node type */
|
|
34
|
+
shape: ObservableShape;
|
|
35
|
+
/** Whether this is the root observable */
|
|
36
|
+
isRoot: boolean;
|
|
37
|
+
/** Whether the observable is whitelisted */
|
|
38
|
+
whitelisted: boolean;
|
|
39
|
+
/** Whether the observable is internal */
|
|
40
|
+
internal: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Observable graph node type.
|
|
45
|
+
*/
|
|
46
|
+
export type ObservableNode = Node<ObservableNodeData, "observable">;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Data attached to observable graph edges.
|
|
50
|
+
*/
|
|
51
|
+
export interface ObservableEdgeData extends Record<string, unknown> {
|
|
52
|
+
/** Relationship type (e.g., "related-to") */
|
|
53
|
+
relationshipType: string;
|
|
54
|
+
/** Whether this is a bidirectional relationship */
|
|
55
|
+
bidirectional: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Observable graph edge type.
|
|
60
|
+
*/
|
|
61
|
+
export type ObservableEdge = Edge<ObservableEdgeData>;
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Investigation Graph Types (Dagre Layout)
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Node types for the investigation graph view.
|
|
69
|
+
*/
|
|
70
|
+
export type InvestigationNodeType = "root" | "check" | "container";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Data attached to investigation graph nodes.
|
|
74
|
+
*/
|
|
75
|
+
export interface InvestigationNodeData extends Record<string, unknown> {
|
|
76
|
+
/** Display label */
|
|
77
|
+
label: string;
|
|
78
|
+
/** Node type (root, check, or container) */
|
|
79
|
+
nodeType: InvestigationNodeType;
|
|
80
|
+
/** Security level */
|
|
81
|
+
level: Level;
|
|
82
|
+
/** Numeric score */
|
|
83
|
+
score: number;
|
|
84
|
+
/** Description (for checks) */
|
|
85
|
+
description?: string;
|
|
86
|
+
/** Path (for containers) */
|
|
87
|
+
path?: string;
|
|
88
|
+
/** Emoji for the node */
|
|
89
|
+
emoji: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Investigation graph node type.
|
|
94
|
+
*/
|
|
95
|
+
export type InvestigationNode = Node<InvestigationNodeData, "investigation">;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Investigation graph edge type.
|
|
99
|
+
*/
|
|
100
|
+
export type InvestigationEdge = Edge;
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Force Layout Configuration
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Configuration options for d3-force layout.
|
|
108
|
+
*/
|
|
109
|
+
export interface ForceLayoutConfig {
|
|
110
|
+
/** Strength of the charge force (repulsion). Default: -300 */
|
|
111
|
+
chargeStrength: number;
|
|
112
|
+
/** Target distance between linked nodes. Default: 100 */
|
|
113
|
+
linkDistance: number;
|
|
114
|
+
/** Strength of the centering force. Default: 0.1 */
|
|
115
|
+
centerStrength: number;
|
|
116
|
+
/** Strength of the collision force. Default: 30 */
|
|
117
|
+
collisionRadius: number;
|
|
118
|
+
/** Number of simulation iterations. Default: 300 */
|
|
119
|
+
iterations: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Default force layout configuration.
|
|
124
|
+
*/
|
|
125
|
+
export const DEFAULT_FORCE_CONFIG: ForceLayoutConfig = {
|
|
126
|
+
chargeStrength: -200,
|
|
127
|
+
linkDistance: 80,
|
|
128
|
+
centerStrength: 0.05,
|
|
129
|
+
collisionRadius: 40,
|
|
130
|
+
iterations: 300,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Component Props
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Props for the ObservablesGraph component.
|
|
139
|
+
*/
|
|
140
|
+
export interface ObservablesGraphProps {
|
|
141
|
+
/** The Cyvest investigation to visualize */
|
|
142
|
+
investigation: import("@cyvest/cyvest-js").CyvestInvestigation;
|
|
143
|
+
/** Height of the graph container */
|
|
144
|
+
height?: number | string;
|
|
145
|
+
/** Width of the graph container */
|
|
146
|
+
width?: number | string;
|
|
147
|
+
/** Force layout configuration */
|
|
148
|
+
forceConfig?: Partial<ForceLayoutConfig>;
|
|
149
|
+
/** Callback when a node is clicked */
|
|
150
|
+
onNodeClick?: (nodeId: string) => void;
|
|
151
|
+
/** Callback when a node is double-clicked */
|
|
152
|
+
onNodeDoubleClick?: (nodeId: string) => void;
|
|
153
|
+
/** Custom class name for the container */
|
|
154
|
+
className?: string;
|
|
155
|
+
/** Whether to show the force controls panel */
|
|
156
|
+
showControls?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Props for the InvestigationGraph component.
|
|
161
|
+
*/
|
|
162
|
+
export interface InvestigationGraphProps {
|
|
163
|
+
/** The Cyvest investigation to visualize */
|
|
164
|
+
investigation: import("@cyvest/cyvest-js").CyvestInvestigation;
|
|
165
|
+
/** Height of the graph container */
|
|
166
|
+
height?: number | string;
|
|
167
|
+
/** Width of the graph container */
|
|
168
|
+
width?: number | string;
|
|
169
|
+
/** Callback when a node is clicked */
|
|
170
|
+
onNodeClick?: (nodeId: string, nodeType: InvestigationNodeType) => void;
|
|
171
|
+
/** Custom class name for the container */
|
|
172
|
+
className?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Props for the CyvestGraph component (combined view).
|
|
177
|
+
*/
|
|
178
|
+
export interface CyvestGraphProps {
|
|
179
|
+
/** The Cyvest investigation to visualize */
|
|
180
|
+
investigation: import("@cyvest/cyvest-js").CyvestInvestigation;
|
|
181
|
+
/** Height of the graph container */
|
|
182
|
+
height?: number | string;
|
|
183
|
+
/** Width of the graph container */
|
|
184
|
+
width?: number | string;
|
|
185
|
+
/** Initial view to display */
|
|
186
|
+
initialView?: "observables" | "investigation";
|
|
187
|
+
/** Callback when a node is clicked */
|
|
188
|
+
onNodeClick?: (nodeId: string) => void;
|
|
189
|
+
/** Custom class name for the container */
|
|
190
|
+
className?: string;
|
|
191
|
+
/** Whether to show view toggle */
|
|
192
|
+
showViewToggle?: boolean;
|
|
193
|
+
}
|