@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
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cyvest/cyvest-vis",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"main": "dist/index.cjs",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"react": "^18.0.0",
|
|
10
|
+
"react-dom": "^18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@dagrejs/dagre": "^1.1.4",
|
|
14
|
+
"@xyflow/react": "^12.5.0",
|
|
15
|
+
"d3-force": "^3.0.0",
|
|
16
|
+
"@cyvest/cyvest-js": "2.0.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/d3-force": "^3.0.10",
|
|
20
|
+
"@types/react": "^19.2.7",
|
|
21
|
+
"@types/react-dom": "^19.2.3",
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.6.0",
|
|
24
|
+
"vitest": "^2.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --dts --format cjs,esm --external react --external react-dom",
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"test:ci": "vitest run --reporter=dot"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CyvestGraph component - combined view with toggle between observables and investigation views.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState, useCallback } from "react";
|
|
6
|
+
import type { CyvestGraphProps, InvestigationNodeType } from "../types";
|
|
7
|
+
import { ObservablesGraph } from "./ObservablesGraph";
|
|
8
|
+
import { InvestigationGraph } from "./InvestigationGraph";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* View toggle button component.
|
|
12
|
+
*/
|
|
13
|
+
const ViewToggle: React.FC<{
|
|
14
|
+
currentView: "observables" | "investigation";
|
|
15
|
+
onChange: (view: "observables" | "investigation") => void;
|
|
16
|
+
}> = ({ currentView, onChange }) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
style={{
|
|
20
|
+
position: "absolute",
|
|
21
|
+
top: 10,
|
|
22
|
+
left: 10,
|
|
23
|
+
display: "flex",
|
|
24
|
+
gap: 4,
|
|
25
|
+
background: "white",
|
|
26
|
+
padding: 4,
|
|
27
|
+
borderRadius: 8,
|
|
28
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
29
|
+
zIndex: 10,
|
|
30
|
+
fontFamily: "system-ui, sans-serif",
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => onChange("observables")}
|
|
35
|
+
style={{
|
|
36
|
+
padding: "6px 12px",
|
|
37
|
+
border: "none",
|
|
38
|
+
borderRadius: 4,
|
|
39
|
+
cursor: "pointer",
|
|
40
|
+
fontSize: 12,
|
|
41
|
+
fontWeight: currentView === "observables" ? 600 : 400,
|
|
42
|
+
background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
|
|
43
|
+
color: currentView === "observables" ? "white" : "#374151",
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
Observables
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => onChange("investigation")}
|
|
50
|
+
style={{
|
|
51
|
+
padding: "6px 12px",
|
|
52
|
+
border: "none",
|
|
53
|
+
borderRadius: 4,
|
|
54
|
+
cursor: "pointer",
|
|
55
|
+
fontSize: 12,
|
|
56
|
+
fontWeight: currentView === "investigation" ? 600 : 400,
|
|
57
|
+
background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
|
|
58
|
+
color: currentView === "investigation" ? "white" : "#374151",
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
Investigation
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CyvestGraph component - provides toggle between ObservablesGraph and InvestigationGraph.
|
|
69
|
+
*/
|
|
70
|
+
export const CyvestGraph: React.FC<CyvestGraphProps> = ({
|
|
71
|
+
investigation,
|
|
72
|
+
height = 500,
|
|
73
|
+
width = "100%",
|
|
74
|
+
initialView = "observables",
|
|
75
|
+
onNodeClick,
|
|
76
|
+
className,
|
|
77
|
+
showViewToggle = true,
|
|
78
|
+
}) => {
|
|
79
|
+
const [view, setView] = useState<"observables" | "investigation">(initialView);
|
|
80
|
+
|
|
81
|
+
const handleNodeClick = useCallback(
|
|
82
|
+
(nodeId: string, _nodeType?: InvestigationNodeType) => {
|
|
83
|
+
onNodeClick?.(nodeId);
|
|
84
|
+
},
|
|
85
|
+
[onNodeClick]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
className={className}
|
|
91
|
+
style={{
|
|
92
|
+
width,
|
|
93
|
+
height,
|
|
94
|
+
position: "relative",
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{showViewToggle && <ViewToggle currentView={view} onChange={setView} />}
|
|
98
|
+
|
|
99
|
+
{view === "observables" ? (
|
|
100
|
+
<ObservablesGraph
|
|
101
|
+
investigation={investigation}
|
|
102
|
+
height="100%"
|
|
103
|
+
width="100%"
|
|
104
|
+
onNodeClick={handleNodeClick}
|
|
105
|
+
showControls={true}
|
|
106
|
+
/>
|
|
107
|
+
) : (
|
|
108
|
+
<InvestigationGraph
|
|
109
|
+
investigation={investigation}
|
|
110
|
+
height="100%"
|
|
111
|
+
width="100%"
|
|
112
|
+
onNodeClick={handleNodeClick}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating Edge component for use with force-directed layout.
|
|
3
|
+
* Uses simple straight lines that connect to node edges.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { memo } from "react";
|
|
7
|
+
import { BaseEdge, getStraightPath, type EdgeProps } from "@xyflow/react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Floating edge component that uses straight lines.
|
|
11
|
+
* React Flow passes sourceX, sourceY, targetX, targetY based on node positions.
|
|
12
|
+
*/
|
|
13
|
+
function FloatingEdgeComponent({
|
|
14
|
+
id,
|
|
15
|
+
sourceX,
|
|
16
|
+
sourceY,
|
|
17
|
+
targetX,
|
|
18
|
+
targetY,
|
|
19
|
+
style,
|
|
20
|
+
markerEnd,
|
|
21
|
+
}: EdgeProps) {
|
|
22
|
+
const [edgePath] = getStraightPath({
|
|
23
|
+
sourceX,
|
|
24
|
+
sourceY,
|
|
25
|
+
targetX,
|
|
26
|
+
targetY,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<BaseEdge
|
|
31
|
+
id={id}
|
|
32
|
+
path={edgePath}
|
|
33
|
+
style={{
|
|
34
|
+
strokeWidth: 1.5,
|
|
35
|
+
stroke: "#94a3b8",
|
|
36
|
+
...style,
|
|
37
|
+
}}
|
|
38
|
+
markerEnd={markerEnd}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const FloatingEdge = memo(FloatingEdgeComponent);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InvestigationGraph component - displays investigation structure with Dagre layout.
|
|
3
|
+
* Shows root observable, checks, and containers in a hierarchical view.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import {
|
|
8
|
+
ReactFlow,
|
|
9
|
+
Background,
|
|
10
|
+
Controls,
|
|
11
|
+
MiniMap,
|
|
12
|
+
useNodesState,
|
|
13
|
+
useEdgesState,
|
|
14
|
+
type Node,
|
|
15
|
+
type Edge,
|
|
16
|
+
type NodeTypes,
|
|
17
|
+
} from "@xyflow/react";
|
|
18
|
+
import "@xyflow/react/dist/style.css";
|
|
19
|
+
|
|
20
|
+
import type { CyvestInvestigation, Check, Container } from "@cyvest/cyvest-js";
|
|
21
|
+
import { findRootObservables } from "@cyvest/cyvest-js";
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
InvestigationGraphProps,
|
|
25
|
+
InvestigationNodeData,
|
|
26
|
+
InvestigationNodeType,
|
|
27
|
+
} from "../types";
|
|
28
|
+
import { InvestigationNode } from "./InvestigationNode";
|
|
29
|
+
import {
|
|
30
|
+
getInvestigationNodeEmoji,
|
|
31
|
+
getLevelColor,
|
|
32
|
+
truncateLabel,
|
|
33
|
+
} from "../utils/observables";
|
|
34
|
+
import { computeDagreLayout } from "../hooks/useDagreLayout";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom node types for React Flow.
|
|
38
|
+
*/
|
|
39
|
+
const nodeTypes: NodeTypes = {
|
|
40
|
+
investigation: InvestigationNode,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Flatten containers recursively to get all container keys.
|
|
45
|
+
*/
|
|
46
|
+
function flattenContainers(
|
|
47
|
+
containers: Record<string, Container>
|
|
48
|
+
): Container[] {
|
|
49
|
+
const result: Container[] = [];
|
|
50
|
+
|
|
51
|
+
for (const container of Object.values(containers)) {
|
|
52
|
+
result.push(container);
|
|
53
|
+
if (container.sub_containers) {
|
|
54
|
+
result.push(...flattenContainers(container.sub_containers));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create investigation graph nodes and edges.
|
|
63
|
+
*/
|
|
64
|
+
function createInvestigationGraph(
|
|
65
|
+
investigation: CyvestInvestigation
|
|
66
|
+
): { nodes: Node<InvestigationNodeData>[]; edges: Edge[] } {
|
|
67
|
+
const nodes: Node<InvestigationNodeData>[] = [];
|
|
68
|
+
const edges: Edge[] = [];
|
|
69
|
+
|
|
70
|
+
// Find root observable(s)
|
|
71
|
+
const roots = findRootObservables(investigation);
|
|
72
|
+
const primaryRoot = roots.length > 0 ? roots[0] : null;
|
|
73
|
+
|
|
74
|
+
// If no root found, use the first observable or create a placeholder
|
|
75
|
+
const rootKey = primaryRoot?.key ?? "investigation-root";
|
|
76
|
+
const rootValue = primaryRoot?.value ?? "Investigation";
|
|
77
|
+
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
78
|
+
|
|
79
|
+
// Create root node
|
|
80
|
+
const rootNodeData: InvestigationNodeData = {
|
|
81
|
+
label: truncateLabel(rootValue, 24),
|
|
82
|
+
nodeType: "root",
|
|
83
|
+
level: rootLevel,
|
|
84
|
+
score: primaryRoot?.score ?? investigation.score,
|
|
85
|
+
emoji: getInvestigationNodeEmoji("root"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
nodes.push({
|
|
89
|
+
id: rootKey,
|
|
90
|
+
type: "investigation",
|
|
91
|
+
position: { x: 0, y: 0 },
|
|
92
|
+
data: rootNodeData,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Add check nodes
|
|
96
|
+
// Group checks by scope for better organization
|
|
97
|
+
const allChecks: Check[] = [];
|
|
98
|
+
for (const checksForKey of Object.values(investigation.checks)) {
|
|
99
|
+
allChecks.push(...checksForKey);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create unique check nodes (by check_id to avoid duplicates)
|
|
103
|
+
const seenCheckIds = new Set<string>();
|
|
104
|
+
for (const check of allChecks) {
|
|
105
|
+
if (seenCheckIds.has(check.key)) continue;
|
|
106
|
+
seenCheckIds.add(check.key);
|
|
107
|
+
|
|
108
|
+
const checkNodeData: InvestigationNodeData = {
|
|
109
|
+
label: truncateLabel(check.check_id, 20),
|
|
110
|
+
nodeType: "check",
|
|
111
|
+
level: check.level,
|
|
112
|
+
score: check.score,
|
|
113
|
+
description: truncateLabel(check.description, 30),
|
|
114
|
+
emoji: getInvestigationNodeEmoji("check"),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
nodes.push({
|
|
118
|
+
id: `check-${check.key}`,
|
|
119
|
+
type: "investigation",
|
|
120
|
+
position: { x: 0, y: 0 },
|
|
121
|
+
data: checkNodeData,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Edge from root to check
|
|
125
|
+
edges.push({
|
|
126
|
+
id: `edge-root-${check.key}`,
|
|
127
|
+
source: rootKey,
|
|
128
|
+
target: `check-${check.key}`,
|
|
129
|
+
type: "default",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add container nodes
|
|
134
|
+
const allContainers = flattenContainers(investigation.containers);
|
|
135
|
+
|
|
136
|
+
for (const container of allContainers) {
|
|
137
|
+
const containerNodeData: InvestigationNodeData = {
|
|
138
|
+
label: truncateLabel(container.path.split("/").pop() ?? container.path, 20),
|
|
139
|
+
nodeType: "container",
|
|
140
|
+
level: container.aggregated_level,
|
|
141
|
+
score: container.aggregated_score,
|
|
142
|
+
path: container.path,
|
|
143
|
+
emoji: getInvestigationNodeEmoji("container"),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
nodes.push({
|
|
147
|
+
id: `container-${container.key}`,
|
|
148
|
+
type: "investigation",
|
|
149
|
+
position: { x: 0, y: 0 },
|
|
150
|
+
data: containerNodeData,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Edge from root to container
|
|
154
|
+
edges.push({
|
|
155
|
+
id: `edge-root-container-${container.key}`,
|
|
156
|
+
source: rootKey,
|
|
157
|
+
target: `container-${container.key}`,
|
|
158
|
+
type: "default",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Edges from container to its checks
|
|
162
|
+
for (const checkKey of container.checks) {
|
|
163
|
+
if (seenCheckIds.has(checkKey)) {
|
|
164
|
+
edges.push({
|
|
165
|
+
id: `edge-container-check-${container.key}-${checkKey}`,
|
|
166
|
+
source: `container-${container.key}`,
|
|
167
|
+
target: `check-${checkKey}`,
|
|
168
|
+
type: "default",
|
|
169
|
+
style: { strokeDasharray: "5,5" },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { nodes, edges };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* InvestigationGraph component.
|
|
180
|
+
* Displays investigation structure with horizontal Dagre layout.
|
|
181
|
+
*/
|
|
182
|
+
export const InvestigationGraph: React.FC<InvestigationGraphProps> = ({
|
|
183
|
+
investigation,
|
|
184
|
+
height = 500,
|
|
185
|
+
width = "100%",
|
|
186
|
+
onNodeClick,
|
|
187
|
+
className,
|
|
188
|
+
}) => {
|
|
189
|
+
// Create initial graph structure
|
|
190
|
+
const { initialNodes, initialEdges } = useMemo(() => {
|
|
191
|
+
const { nodes, edges } = createInvestigationGraph(investigation);
|
|
192
|
+
return { initialNodes: nodes, initialEdges: edges };
|
|
193
|
+
}, [investigation]);
|
|
194
|
+
|
|
195
|
+
// Apply Dagre layout
|
|
196
|
+
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(() => {
|
|
197
|
+
return computeDagreLayout(initialNodes, initialEdges, {
|
|
198
|
+
direction: "LR",
|
|
199
|
+
nodeSpacing: 30,
|
|
200
|
+
rankSpacing: 120,
|
|
201
|
+
});
|
|
202
|
+
}, [initialNodes, initialEdges]);
|
|
203
|
+
|
|
204
|
+
// React Flow state
|
|
205
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
|
206
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);
|
|
207
|
+
|
|
208
|
+
// Update nodes when layout changes
|
|
209
|
+
React.useEffect(() => {
|
|
210
|
+
setNodes(layoutNodes);
|
|
211
|
+
setEdges(layoutEdges);
|
|
212
|
+
}, [layoutNodes, layoutEdges, setNodes, setEdges]);
|
|
213
|
+
|
|
214
|
+
// Handle node click
|
|
215
|
+
const handleNodeClick = useCallback(
|
|
216
|
+
(_: React.MouseEvent, node: Node) => {
|
|
217
|
+
const data = node.data as unknown as InvestigationNodeData;
|
|
218
|
+
onNodeClick?.(node.id, data.nodeType);
|
|
219
|
+
},
|
|
220
|
+
[onNodeClick]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// MiniMap node color based on level
|
|
224
|
+
const miniMapNodeColor = useCallback((node: Node) => {
|
|
225
|
+
const data = node.data as unknown as InvestigationNodeData;
|
|
226
|
+
return getLevelColor(data.level);
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
className={className}
|
|
232
|
+
style={{
|
|
233
|
+
width,
|
|
234
|
+
height,
|
|
235
|
+
position: "relative",
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<ReactFlow
|
|
239
|
+
nodes={nodes}
|
|
240
|
+
edges={edges}
|
|
241
|
+
onNodesChange={onNodesChange}
|
|
242
|
+
onEdgesChange={onEdgesChange}
|
|
243
|
+
onNodeClick={handleNodeClick}
|
|
244
|
+
nodeTypes={nodeTypes}
|
|
245
|
+
fitView
|
|
246
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
247
|
+
minZoom={0.1}
|
|
248
|
+
maxZoom={2}
|
|
249
|
+
proOptions={{ hideAttribution: true }}
|
|
250
|
+
>
|
|
251
|
+
<Background />
|
|
252
|
+
<Controls />
|
|
253
|
+
<MiniMap nodeColor={miniMapNodeColor} zoomable pannable />
|
|
254
|
+
</ReactFlow>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom node component for the Investigation Graph (Dagre layout).
|
|
3
|
+
* Renders root, check, and container nodes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { memo } from "react";
|
|
7
|
+
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
|
8
|
+
import type { InvestigationNodeData } from "../types";
|
|
9
|
+
import { getLevelColor, getLevelBackgroundColor } from "../utils/observables";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Investigation node component.
|
|
13
|
+
*/
|
|
14
|
+
function InvestigationNodeComponent({
|
|
15
|
+
data,
|
|
16
|
+
selected,
|
|
17
|
+
}: NodeProps) {
|
|
18
|
+
const nodeData = data as unknown as InvestigationNodeData;
|
|
19
|
+
const {
|
|
20
|
+
label,
|
|
21
|
+
emoji,
|
|
22
|
+
nodeType,
|
|
23
|
+
level,
|
|
24
|
+
description,
|
|
25
|
+
} = nodeData;
|
|
26
|
+
|
|
27
|
+
const borderColor = getLevelColor(level);
|
|
28
|
+
const backgroundColor = getLevelBackgroundColor(level);
|
|
29
|
+
|
|
30
|
+
// Different styles based on node type
|
|
31
|
+
const getNodeStyle = () => {
|
|
32
|
+
switch (nodeType) {
|
|
33
|
+
case "root":
|
|
34
|
+
return {
|
|
35
|
+
minWidth: 120,
|
|
36
|
+
padding: "8px 16px",
|
|
37
|
+
borderRadius: 8,
|
|
38
|
+
fontWeight: 600 as const,
|
|
39
|
+
};
|
|
40
|
+
case "check":
|
|
41
|
+
return {
|
|
42
|
+
minWidth: 100,
|
|
43
|
+
padding: "6px 12px",
|
|
44
|
+
borderRadius: 4,
|
|
45
|
+
fontWeight: 400 as const,
|
|
46
|
+
};
|
|
47
|
+
case "container":
|
|
48
|
+
return {
|
|
49
|
+
minWidth: 100,
|
|
50
|
+
padding: "6px 12px",
|
|
51
|
+
borderRadius: 12,
|
|
52
|
+
fontWeight: 400 as const,
|
|
53
|
+
};
|
|
54
|
+
default:
|
|
55
|
+
return {
|
|
56
|
+
minWidth: 80,
|
|
57
|
+
padding: "6px 12px",
|
|
58
|
+
borderRadius: 4,
|
|
59
|
+
fontWeight: 400 as const,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const style = getNodeStyle();
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className="investigation-node"
|
|
69
|
+
style={{
|
|
70
|
+
...style,
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
backgroundColor,
|
|
75
|
+
border: `${selected ? 3 : 2}px solid ${borderColor}`,
|
|
76
|
+
cursor: "pointer",
|
|
77
|
+
fontFamily: "system-ui, sans-serif",
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{/* Header with emoji and label */}
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
display: "flex",
|
|
84
|
+
alignItems: "center",
|
|
85
|
+
gap: 6,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<span style={{ fontSize: 14 }}>{emoji}</span>
|
|
89
|
+
<span
|
|
90
|
+
style={{
|
|
91
|
+
fontSize: 12,
|
|
92
|
+
fontWeight: style.fontWeight,
|
|
93
|
+
maxWidth: 150,
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
textOverflow: "ellipsis",
|
|
96
|
+
whiteSpace: "nowrap",
|
|
97
|
+
}}
|
|
98
|
+
title={label}
|
|
99
|
+
>
|
|
100
|
+
{label}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Description for checks */}
|
|
105
|
+
{description && (
|
|
106
|
+
<div
|
|
107
|
+
style={{
|
|
108
|
+
marginTop: 4,
|
|
109
|
+
fontSize: 10,
|
|
110
|
+
color: "#6b7280",
|
|
111
|
+
maxWidth: 140,
|
|
112
|
+
overflow: "hidden",
|
|
113
|
+
textOverflow: "ellipsis",
|
|
114
|
+
whiteSpace: "nowrap",
|
|
115
|
+
}}
|
|
116
|
+
title={description}
|
|
117
|
+
>
|
|
118
|
+
{description}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{/* Handles for edges */}
|
|
123
|
+
<Handle
|
|
124
|
+
type="target"
|
|
125
|
+
position={Position.Left}
|
|
126
|
+
style={{
|
|
127
|
+
width: 8,
|
|
128
|
+
height: 8,
|
|
129
|
+
background: borderColor,
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
<Handle
|
|
133
|
+
type="source"
|
|
134
|
+
position={Position.Right}
|
|
135
|
+
style={{
|
|
136
|
+
width: 8,
|
|
137
|
+
height: 8,
|
|
138
|
+
background: borderColor,
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const InvestigationNode = memo(InvestigationNodeComponent);
|