@cyvest/cyvest-vis 3.2.0 → 4.1.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 +122 -10
- package/dist/index.d.mts +156 -6
- package/dist/index.d.ts +156 -6
- package/dist/index.js +1485 -700
- package/dist/index.mjs +1494 -712
- package/package.json +9 -9
- package/src/components/CyvestGraph.tsx +115 -46
- package/src/components/FloatingEdge.tsx +41 -11
- package/src/components/Icons.tsx +730 -0
- package/src/components/InvestigationGraph.tsx +120 -42
- package/src/components/InvestigationNode.tsx +129 -112
- package/src/components/ObservableNode.tsx +116 -135
- package/src/components/ObservablesGraph.tsx +258 -123
- package/src/hooks/useForceLayout.ts +136 -62
- package/src/index.ts +25 -2
- package/src/types.ts +9 -11
- package/src/utils/observables.ts +28 -115
- package/tests/observables.test.ts +13 -21
- package/vitest.config.ts +14 -0
|
@@ -14,11 +14,12 @@ import {
|
|
|
14
14
|
type Node,
|
|
15
15
|
type Edge,
|
|
16
16
|
type NodeTypes,
|
|
17
|
+
BackgroundVariant,
|
|
18
|
+
MarkerType,
|
|
17
19
|
} from "@xyflow/react";
|
|
18
20
|
import "@xyflow/react/dist/style.css";
|
|
19
21
|
|
|
20
22
|
import type { CyvestInvestigation, Check, Container } from "@cyvest/cyvest-js";
|
|
21
|
-
import { findRootObservables } from "@cyvest/cyvest-js";
|
|
22
23
|
|
|
23
24
|
import type {
|
|
24
25
|
InvestigationGraphProps,
|
|
@@ -26,11 +27,7 @@ import type {
|
|
|
26
27
|
InvestigationNodeType,
|
|
27
28
|
} from "../types";
|
|
28
29
|
import { InvestigationNode } from "./InvestigationNode";
|
|
29
|
-
import {
|
|
30
|
-
getInvestigationNodeEmoji,
|
|
31
|
-
getLevelColor,
|
|
32
|
-
truncateLabel,
|
|
33
|
-
} from "../utils/observables";
|
|
30
|
+
import { getLevelColor, truncateLabel } from "../utils/observables";
|
|
34
31
|
import { computeDagreLayout } from "../hooks/useDagreLayout";
|
|
35
32
|
|
|
36
33
|
/**
|
|
@@ -40,6 +37,23 @@ const nodeTypes: NodeTypes = {
|
|
|
40
37
|
investigation: InvestigationNode,
|
|
41
38
|
};
|
|
42
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Default edge style
|
|
42
|
+
*/
|
|
43
|
+
const defaultEdgeOptions = {
|
|
44
|
+
type: "smoothstep",
|
|
45
|
+
style: {
|
|
46
|
+
stroke: "#94a3b8",
|
|
47
|
+
strokeWidth: 1.5,
|
|
48
|
+
},
|
|
49
|
+
markerEnd: {
|
|
50
|
+
type: MarkerType.ArrowClosed,
|
|
51
|
+
width: 16,
|
|
52
|
+
height: 16,
|
|
53
|
+
color: "#94a3b8",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* Flatten containers recursively to get all container keys.
|
|
45
59
|
*/
|
|
@@ -67,13 +81,21 @@ function createInvestigationGraph(
|
|
|
67
81
|
const nodes: Node<InvestigationNodeData>[] = [];
|
|
68
82
|
const edges: Edge[] = [];
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
84
|
+
const rootType = investigation.data_extraction.root_type;
|
|
85
|
+
const normalizedRootType = rootType?.toLowerCase().trim();
|
|
86
|
+
const rootsByType = normalizedRootType
|
|
87
|
+
? Object.values(investigation.observables).filter(
|
|
88
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
89
|
+
)
|
|
90
|
+
: [];
|
|
91
|
+
const primaryRoot = rootsByType[0] ?? null;
|
|
73
92
|
|
|
74
93
|
// If no root found, use the first observable or create a placeholder
|
|
75
|
-
const rootKey = primaryRoot?.key ??
|
|
76
|
-
const rootValue =
|
|
94
|
+
const rootKey = primaryRoot?.key ?? investigation.investigation_id;
|
|
95
|
+
const rootValue =
|
|
96
|
+
primaryRoot?.value ??
|
|
97
|
+
investigation.investigation_name ??
|
|
98
|
+
investigation.investigation_id;
|
|
77
99
|
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
78
100
|
|
|
79
101
|
// Create root node
|
|
@@ -82,7 +104,6 @@ function createInvestigationGraph(
|
|
|
82
104
|
nodeType: "root",
|
|
83
105
|
level: rootLevel,
|
|
84
106
|
score: primaryRoot?.score ?? investigation.score,
|
|
85
|
-
emoji: getInvestigationNodeEmoji("root"),
|
|
86
107
|
};
|
|
87
108
|
|
|
88
109
|
nodes.push({
|
|
@@ -90,8 +111,20 @@ function createInvestigationGraph(
|
|
|
90
111
|
type: "investigation",
|
|
91
112
|
position: { x: 0, y: 0 },
|
|
92
113
|
data: rootNodeData,
|
|
114
|
+
selectable: true,
|
|
115
|
+
draggable: true,
|
|
93
116
|
});
|
|
94
117
|
|
|
118
|
+
// Collect all check keys that belong to containers
|
|
119
|
+
// These checks should NOT have a direct link to the root node
|
|
120
|
+
const allContainers = flattenContainers(investigation.containers);
|
|
121
|
+
const checksInContainers = new Set<string>();
|
|
122
|
+
for (const container of allContainers) {
|
|
123
|
+
for (const checkKey of container.checks) {
|
|
124
|
+
checksInContainers.add(checkKey);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
// Add check nodes
|
|
96
129
|
// Group checks by scope for better organization
|
|
97
130
|
const allChecks: Check[] = [];
|
|
@@ -111,7 +144,6 @@ function createInvestigationGraph(
|
|
|
111
144
|
level: check.level,
|
|
112
145
|
score: check.score,
|
|
113
146
|
description: truncateLabel(check.description, 30),
|
|
114
|
-
emoji: getInvestigationNodeEmoji("check"),
|
|
115
147
|
};
|
|
116
148
|
|
|
117
149
|
nodes.push({
|
|
@@ -119,28 +151,34 @@ function createInvestigationGraph(
|
|
|
119
151
|
type: "investigation",
|
|
120
152
|
position: { x: 0, y: 0 },
|
|
121
153
|
data: checkNodeData,
|
|
154
|
+
selectable: true,
|
|
155
|
+
draggable: true,
|
|
122
156
|
});
|
|
123
157
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
// Only create edge from root to check if check is NOT in a container
|
|
159
|
+
// Checks in containers will be linked through their container instead
|
|
160
|
+
if (!checksInContainers.has(check.key)) {
|
|
161
|
+
edges.push({
|
|
162
|
+
id: `edge-root-${check.key}`,
|
|
163
|
+
source: rootKey,
|
|
164
|
+
target: `check-${check.key}`,
|
|
165
|
+
type: "smoothstep",
|
|
166
|
+
animated: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
// Add container nodes
|
|
134
|
-
const allContainers = flattenContainers(investigation.containers);
|
|
135
|
-
|
|
136
172
|
for (const container of allContainers) {
|
|
137
173
|
const containerNodeData: InvestigationNodeData = {
|
|
138
|
-
label: truncateLabel(
|
|
174
|
+
label: truncateLabel(
|
|
175
|
+
container.path.split("/").pop() ?? container.path,
|
|
176
|
+
20
|
|
177
|
+
),
|
|
139
178
|
nodeType: "container",
|
|
140
179
|
level: container.aggregated_level,
|
|
141
180
|
score: container.aggregated_score,
|
|
142
181
|
path: container.path,
|
|
143
|
-
emoji: getInvestigationNodeEmoji("container"),
|
|
144
182
|
};
|
|
145
183
|
|
|
146
184
|
nodes.push({
|
|
@@ -148,6 +186,8 @@ function createInvestigationGraph(
|
|
|
148
186
|
type: "investigation",
|
|
149
187
|
position: { x: 0, y: 0 },
|
|
150
188
|
data: containerNodeData,
|
|
189
|
+
selectable: true,
|
|
190
|
+
draggable: true,
|
|
151
191
|
});
|
|
152
192
|
|
|
153
193
|
// Edge from root to container
|
|
@@ -155,7 +195,8 @@ function createInvestigationGraph(
|
|
|
155
195
|
id: `edge-root-container-${container.key}`,
|
|
156
196
|
source: rootKey,
|
|
157
197
|
target: `container-${container.key}`,
|
|
158
|
-
type: "
|
|
198
|
+
type: "smoothstep",
|
|
199
|
+
animated: false,
|
|
159
200
|
});
|
|
160
201
|
|
|
161
202
|
// Edges from container to its checks
|
|
@@ -165,8 +206,8 @@ function createInvestigationGraph(
|
|
|
165
206
|
id: `edge-container-check-${container.key}-${checkKey}`,
|
|
166
207
|
source: `container-${container.key}`,
|
|
167
208
|
target: `check-${checkKey}`,
|
|
168
|
-
type: "
|
|
169
|
-
|
|
209
|
+
type: "smoothstep",
|
|
210
|
+
animated: false,
|
|
170
211
|
});
|
|
171
212
|
}
|
|
172
213
|
}
|
|
@@ -196,8 +237,8 @@ export const InvestigationGraph: React.FC<InvestigationGraphProps> = ({
|
|
|
196
237
|
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(() => {
|
|
197
238
|
return computeDagreLayout(initialNodes, initialEdges, {
|
|
198
239
|
direction: "LR",
|
|
199
|
-
nodeSpacing:
|
|
200
|
-
rankSpacing:
|
|
240
|
+
nodeSpacing: 40,
|
|
241
|
+
rankSpacing: 140,
|
|
201
242
|
});
|
|
202
243
|
}, [initialNodes, initialEdges]);
|
|
203
244
|
|
|
@@ -226,15 +267,19 @@ export const InvestigationGraph: React.FC<InvestigationGraphProps> = ({
|
|
|
226
267
|
return getLevelColor(data.level);
|
|
227
268
|
}, []);
|
|
228
269
|
|
|
270
|
+
// Container styles
|
|
271
|
+
const containerStyle = useMemo(
|
|
272
|
+
() => ({
|
|
273
|
+
width,
|
|
274
|
+
height,
|
|
275
|
+
position: "relative" as const,
|
|
276
|
+
background: "linear-gradient(180deg, #fafbfc 0%, #f0f4f8 100%)",
|
|
277
|
+
}),
|
|
278
|
+
[width, height]
|
|
279
|
+
);
|
|
280
|
+
|
|
229
281
|
return (
|
|
230
|
-
<div
|
|
231
|
-
className={className}
|
|
232
|
-
style={{
|
|
233
|
-
width,
|
|
234
|
-
height,
|
|
235
|
-
position: "relative",
|
|
236
|
-
}}
|
|
237
|
-
>
|
|
282
|
+
<div className={className} style={containerStyle}>
|
|
238
283
|
<ReactFlow
|
|
239
284
|
nodes={nodes}
|
|
240
285
|
edges={edges}
|
|
@@ -242,15 +287,48 @@ export const InvestigationGraph: React.FC<InvestigationGraphProps> = ({
|
|
|
242
287
|
onEdgesChange={onEdgesChange}
|
|
243
288
|
onNodeClick={handleNodeClick}
|
|
244
289
|
nodeTypes={nodeTypes}
|
|
290
|
+
defaultEdgeOptions={defaultEdgeOptions}
|
|
245
291
|
fitView
|
|
246
|
-
fitViewOptions={{ padding: 0.
|
|
292
|
+
fitViewOptions={{ padding: 0.3, maxZoom: 1.5 }}
|
|
247
293
|
minZoom={0.1}
|
|
248
|
-
maxZoom={2}
|
|
294
|
+
maxZoom={2.5}
|
|
249
295
|
proOptions={{ hideAttribution: true }}
|
|
296
|
+
// UX settings
|
|
297
|
+
nodesDraggable={true}
|
|
298
|
+
nodesConnectable={false}
|
|
299
|
+
elementsSelectable={true}
|
|
300
|
+
selectNodesOnDrag={false}
|
|
301
|
+
panOnDrag={true}
|
|
302
|
+
zoomOnScroll={true}
|
|
303
|
+
zoomOnPinch={true}
|
|
304
|
+
panOnScroll={false}
|
|
250
305
|
>
|
|
251
|
-
<Background
|
|
252
|
-
|
|
253
|
-
|
|
306
|
+
<Background
|
|
307
|
+
variant={BackgroundVariant.Dots}
|
|
308
|
+
gap={24}
|
|
309
|
+
size={1}
|
|
310
|
+
color="#d1d5db"
|
|
311
|
+
/>
|
|
312
|
+
<Controls
|
|
313
|
+
showInteractive={false}
|
|
314
|
+
style={{
|
|
315
|
+
borderRadius: 10,
|
|
316
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
317
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
<MiniMap
|
|
321
|
+
nodeColor={miniMapNodeColor}
|
|
322
|
+
zoomable
|
|
323
|
+
pannable
|
|
324
|
+
style={{
|
|
325
|
+
borderRadius: 10,
|
|
326
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
327
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
328
|
+
background: "rgba(255,255,255,0.9)",
|
|
329
|
+
}}
|
|
330
|
+
maskColor="rgba(0,0,0,0.08)"
|
|
331
|
+
/>
|
|
254
332
|
</ReactFlow>
|
|
255
333
|
</div>
|
|
256
334
|
);
|
|
@@ -1,143 +1,160 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom node component for the Investigation Graph (Dagre layout).
|
|
3
|
-
*
|
|
3
|
+
* Professional design with SVG icons for root, check, and container nodes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { memo } from "react";
|
|
6
|
+
import React, { memo, useMemo } from "react";
|
|
7
7
|
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
|
8
8
|
import type { InvestigationNodeData } from "../types";
|
|
9
9
|
import { getLevelColor, getLevelBackgroundColor } from "../utils/observables";
|
|
10
|
+
import { getInvestigationIcon } from "./Icons";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
+
* Node style configuration by type
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const NODE_CONFIG = {
|
|
16
|
+
root: {
|
|
17
|
+
minWidth: 140,
|
|
18
|
+
padding: "10px 18px",
|
|
19
|
+
borderRadius: 20,
|
|
20
|
+
fontWeight: 600 as const,
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
iconSize: 18,
|
|
23
|
+
showIcon: true,
|
|
24
|
+
alignCenter: true,
|
|
25
|
+
},
|
|
26
|
+
check: {
|
|
27
|
+
minWidth: 140,
|
|
28
|
+
padding: "8px 14px",
|
|
29
|
+
borderRadius: 8,
|
|
30
|
+
fontWeight: 500 as const,
|
|
31
|
+
fontSize: 12,
|
|
32
|
+
iconSize: 14,
|
|
33
|
+
showIcon: false, // No icon for checks
|
|
34
|
+
alignCenter: false, // Left-aligned
|
|
35
|
+
},
|
|
36
|
+
container: {
|
|
37
|
+
minWidth: 120,
|
|
38
|
+
padding: "8px 14px",
|
|
39
|
+
borderRadius: 16,
|
|
40
|
+
fontWeight: 500 as const,
|
|
41
|
+
fontSize: 12,
|
|
42
|
+
iconSize: 16,
|
|
43
|
+
showIcon: true,
|
|
44
|
+
alignCenter: true,
|
|
45
|
+
},
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Investigation node component with professional design.
|
|
50
|
+
*/
|
|
51
|
+
function InvestigationNodeComponent({ data, selected }: NodeProps) {
|
|
18
52
|
const nodeData = data as unknown as InvestigationNodeData;
|
|
19
|
-
const {
|
|
20
|
-
label,
|
|
21
|
-
emoji,
|
|
22
|
-
nodeType,
|
|
23
|
-
level,
|
|
24
|
-
description,
|
|
25
|
-
} = nodeData;
|
|
53
|
+
const { label, nodeType, level, description } = nodeData;
|
|
26
54
|
|
|
27
55
|
const borderColor = getLevelColor(level);
|
|
28
56
|
const backgroundColor = getLevelBackgroundColor(level);
|
|
57
|
+
const config = NODE_CONFIG[nodeType] || NODE_CONFIG.check;
|
|
29
58
|
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
padding: "6px 12px",
|
|
58
|
-
borderRadius: 4,
|
|
59
|
-
fontWeight: 400 as const,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
};
|
|
59
|
+
// Get the appropriate icon component
|
|
60
|
+
const IconComponent = useMemo(
|
|
61
|
+
() => getInvestigationIcon(nodeType),
|
|
62
|
+
[nodeType]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Memoize node style
|
|
66
|
+
const nodeStyle = useMemo(
|
|
67
|
+
() => ({
|
|
68
|
+
minWidth: config.minWidth,
|
|
69
|
+
padding: config.padding,
|
|
70
|
+
borderRadius: config.borderRadius,
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column" as const,
|
|
73
|
+
alignItems: config.alignCenter ? "center" : "flex-start",
|
|
74
|
+
backgroundColor,
|
|
75
|
+
border: `2px solid ${borderColor}`,
|
|
76
|
+
boxShadow: selected
|
|
77
|
+
? `0 0 0 3px ${borderColor}40, 0 4px 12px rgba(0,0,0,0.15)`
|
|
78
|
+
: "0 2px 8px rgba(0,0,0,0.08)",
|
|
79
|
+
cursor: "pointer",
|
|
80
|
+
fontFamily:
|
|
81
|
+
"'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
82
|
+
transition: "box-shadow 0.15s ease-out, transform 0.1s ease-out",
|
|
83
|
+
}),
|
|
84
|
+
[config, backgroundColor, borderColor, selected]
|
|
85
|
+
);
|
|
63
86
|
|
|
64
|
-
const
|
|
87
|
+
const headerStyle = useMemo(
|
|
88
|
+
() => ({
|
|
89
|
+
display: "flex",
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
gap: 8,
|
|
92
|
+
width: config.alignCenter ? "auto" : "100%",
|
|
93
|
+
}),
|
|
94
|
+
[config.alignCenter]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const labelStyle = useMemo(
|
|
98
|
+
() => ({
|
|
99
|
+
fontSize: config.fontSize,
|
|
100
|
+
fontWeight: config.fontWeight,
|
|
101
|
+
maxWidth: 180,
|
|
102
|
+
overflow: "hidden",
|
|
103
|
+
textOverflow: "ellipsis",
|
|
104
|
+
whiteSpace: "nowrap" as const,
|
|
105
|
+
color: "#1f2937",
|
|
106
|
+
letterSpacing: "-0.01em",
|
|
107
|
+
}),
|
|
108
|
+
[config]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const descriptionStyle = useMemo(
|
|
112
|
+
() => ({
|
|
113
|
+
marginTop: 4,
|
|
114
|
+
fontSize: 10,
|
|
115
|
+
color: "#6b7280",
|
|
116
|
+
maxWidth: 170,
|
|
117
|
+
overflow: "hidden",
|
|
118
|
+
textOverflow: "ellipsis",
|
|
119
|
+
whiteSpace: "nowrap" as const,
|
|
120
|
+
lineHeight: 1.3,
|
|
121
|
+
width: "100%",
|
|
122
|
+
textAlign: config.alignCenter ? ("center" as const) : ("left" as const),
|
|
123
|
+
}),
|
|
124
|
+
[config.alignCenter]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Hidden handle style - edges connect but no visible dots
|
|
128
|
+
const handleStyle: React.CSSProperties = {
|
|
129
|
+
width: 1,
|
|
130
|
+
height: 1,
|
|
131
|
+
background: "transparent",
|
|
132
|
+
border: "none",
|
|
133
|
+
opacity: 0,
|
|
134
|
+
};
|
|
65
135
|
|
|
66
136
|
return (
|
|
67
|
-
<div
|
|
68
|
-
|
|
69
|
-
style={
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
>
|
|
137
|
+
<div className="investigation-node" style={nodeStyle}>
|
|
138
|
+
{/* Header with optional icon and label */}
|
|
139
|
+
<div style={headerStyle}>
|
|
140
|
+
{config.showIcon && (
|
|
141
|
+
<IconComponent size={config.iconSize} color={borderColor} />
|
|
142
|
+
)}
|
|
143
|
+
<span style={labelStyle} title={label}>
|
|
100
144
|
{label}
|
|
101
145
|
</span>
|
|
102
146
|
</div>
|
|
103
147
|
|
|
104
148
|
{/* Description for checks */}
|
|
105
149
|
{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
|
-
>
|
|
150
|
+
<div style={descriptionStyle} title={description}>
|
|
118
151
|
{description}
|
|
119
152
|
</div>
|
|
120
153
|
)}
|
|
121
154
|
|
|
122
|
-
{/*
|
|
123
|
-
<Handle
|
|
124
|
-
|
|
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
|
-
/>
|
|
155
|
+
{/* Hidden handles for edges - edges still connect but no visible dots */}
|
|
156
|
+
<Handle type="target" position={Position.Left} style={handleStyle} />
|
|
157
|
+
<Handle type="source" position={Position.Right} style={handleStyle} />
|
|
141
158
|
</div>
|
|
142
159
|
);
|
|
143
160
|
}
|