@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/README.md +57 -50
- package/dist/index.cjs +537 -637
- package/dist/index.d.cts +49 -60
- package/dist/index.d.ts +49 -60
- package/dist/index.js +541 -636
- package/dist/styles.css +29 -86
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
// src/components/
|
|
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/
|
|
4
|
+
// src/adapters/observablesElements.ts
|
|
8
5
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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"
|
|
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: "#
|
|
68
|
-
gridColor: "#
|
|
69
|
-
panelBackground: "rgba(255, 255, 255, 0.
|
|
70
|
-
panelBorder: "#
|
|
71
|
-
panelText: "#
|
|
72
|
-
panelTextMuted: "#
|
|
73
|
-
accent: "#
|
|
74
|
-
edgeColor: "#
|
|
75
|
-
edgeSelectedColor: "#
|
|
76
|
-
fontFamily: "
|
|
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
|
-
|
|
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
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
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
|
-
|
|
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/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
if (rootObservable) {
|
|
180
|
+
function getArrowShapes(edge) {
|
|
181
|
+
if (edge.direction === "bidirectional") {
|
|
153
182
|
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
score: rootObservable.score
|
|
183
|
+
sourceArrowShape: "triangle",
|
|
184
|
+
targetArrowShape: "triangle"
|
|
157
185
|
};
|
|
158
186
|
}
|
|
159
|
-
|
|
160
|
-
if (firstObservable) {
|
|
187
|
+
if (edge.direction === "inbound") {
|
|
161
188
|
return {
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
|
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":
|
|
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":
|
|
276
|
+
"text-max-width": 190,
|
|
335
277
|
"text-halign": "center",
|
|
336
278
|
"text-valign": "bottom",
|
|
337
|
-
"text-margin-y":
|
|
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": "
|
|
344
|
-
"background-height": "
|
|
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.
|
|
357
|
-
"border-width":
|
|
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":
|
|
371
|
-
opacity: 0.
|
|
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":
|
|
361
|
+
"font-size": 9,
|
|
384
362
|
"font-family": resolved.fontFamily,
|
|
385
363
|
"text-background-color": resolved.panelBackground,
|
|
386
|
-
"text-background-opacity": 0.
|
|
364
|
+
"text-background-opacity": 0.94,
|
|
387
365
|
"text-background-padding": 2
|
|
388
366
|
}
|
|
389
367
|
}
|
|
390
368
|
];
|
|
391
369
|
}
|
|
392
|
-
function
|
|
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
|
-
"
|
|
460
|
-
"
|
|
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/
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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:
|
|
405
|
+
animate: true,
|
|
406
|
+
animationDuration: 420
|
|
475
407
|
};
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
411
|
+
function resolveOptions(overrides) {
|
|
490
412
|
return {
|
|
491
|
-
...
|
|
492
|
-
...overrides
|
|
493
|
-
extra: {
|
|
494
|
-
...getDefaultElkOptions(view).extra ?? {},
|
|
495
|
-
...overrides?.extra ?? {}
|
|
496
|
-
}
|
|
413
|
+
...getDefaultForceOptions(),
|
|
414
|
+
...overrides
|
|
497
415
|
};
|
|
498
416
|
}
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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: "
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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.
|
|
562
|
-
minZoom: 0.
|
|
563
|
-
maxZoom:
|
|
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
|
-
|
|
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
|
|
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: "
|
|
737
|
-
"aria-label": "
|
|
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 {
|
|
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 =
|
|
900
|
+
const elements = useMemo2(
|
|
931
901
|
() => buildObservablesElements(investigation, {
|
|
932
902
|
maxLabelLength,
|
|
933
|
-
|
|
903
|
+
theme
|
|
934
904
|
}),
|
|
935
|
-
[investigation, maxLabelLength, theme
|
|
905
|
+
[investigation, maxLabelLength, theme]
|
|
936
906
|
);
|
|
937
|
-
const stylesheet =
|
|
907
|
+
const stylesheet = useMemo2(
|
|
938
908
|
() => createObservablesStylesheet(theme),
|
|
939
909
|
[theme]
|
|
940
910
|
);
|
|
941
|
-
const
|
|
942
|
-
() =>
|
|
943
|
-
[layout]
|
|
911
|
+
const forceLayout = useMemo2(
|
|
912
|
+
() => createForceLayout(elements, layout),
|
|
913
|
+
[elements, layout]
|
|
944
914
|
);
|
|
945
|
-
return /* @__PURE__ */
|
|
915
|
+
return /* @__PURE__ */ jsx2(
|
|
946
916
|
CytoscapeCanvas,
|
|
947
917
|
{
|
|
948
|
-
view: "observables",
|
|
949
918
|
elements,
|
|
950
919
|
stylesheet,
|
|
951
|
-
layout:
|
|
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
|
|
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
|
-
|
|
979
|
-
showViewToggle = true,
|
|
980
|
-
onViewChange,
|
|
946
|
+
physics = true,
|
|
981
947
|
showToolbar = true,
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
};
|