flock-core 0.5.0b55__py3-none-any.whl → 0.5.0b57__py3-none-any.whl
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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/frontend/package.json +1 -1
- flock/frontend/src/components/graph/GraphCanvas.tsx +212 -34
- flock/frontend/src/services/layout.ts +227 -16
- flock/logging/telemetry_exporter/duckdb_exporter.py +4 -4
- {flock_core-0.5.0b55.dist-info → flock_core-0.5.0b57.dist-info}/METADATA +378 -112
- {flock_core-0.5.0b55.dist-info → flock_core-0.5.0b57.dist-info}/RECORD +9 -9
- {flock_core-0.5.0b55.dist-info → flock_core-0.5.0b57.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b55.dist-info → flock_core-0.5.0b57.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b55.dist-info → flock_core-0.5.0b57.dist-info}/licenses/LICENSE +0 -0
flock/frontend/package.json
CHANGED
|
@@ -21,16 +21,20 @@ import { useUIStore } from '../../store/uiStore';
|
|
|
21
21
|
import { useModuleStore } from '../../store/moduleStore';
|
|
22
22
|
import { useSettingsStore } from '../../store/settingsStore';
|
|
23
23
|
import { moduleRegistry } from '../modules/ModuleRegistry';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
applyHierarchicalLayout,
|
|
26
|
+
applyCircularLayout,
|
|
27
|
+
applyGridLayout,
|
|
28
|
+
applyRandomLayout
|
|
29
|
+
} from '../../services/layout';
|
|
25
30
|
import { usePersistence } from '../../hooks/usePersistence';
|
|
26
31
|
import { v4 as uuidv4 } from 'uuid';
|
|
27
32
|
|
|
28
33
|
const GraphCanvas: React.FC = () => {
|
|
29
|
-
const { fitView, getIntersectingNodes } = useReactFlow();
|
|
34
|
+
const { fitView, getIntersectingNodes, screenToFlowPosition } = useReactFlow();
|
|
30
35
|
|
|
31
36
|
const mode = useUIStore((state) => state.mode);
|
|
32
37
|
const openDetailWindow = useUIStore((state) => state.openDetailWindow);
|
|
33
|
-
const layoutDirection = useSettingsStore((state) => state.advanced.layoutDirection);
|
|
34
38
|
const nodes = useGraphStore((state) => state.nodes);
|
|
35
39
|
const edges = useGraphStore((state) => state.edges);
|
|
36
40
|
const agents = useGraphStore((state) => state.agents);
|
|
@@ -48,6 +52,7 @@ const GraphCanvas: React.FC = () => {
|
|
|
48
52
|
|
|
49
53
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
50
54
|
const [showModuleSubmenu, setShowModuleSubmenu] = useState(false);
|
|
55
|
+
const [showLayoutSubmenu, setShowLayoutSubmenu] = useState(false);
|
|
51
56
|
|
|
52
57
|
// Persistence hook - loads positions on mount and handles saves
|
|
53
58
|
const { saveNodePosition } = usePersistence();
|
|
@@ -111,21 +116,52 @@ const GraphCanvas: React.FC = () => {
|
|
|
111
116
|
[edges]
|
|
112
117
|
);
|
|
113
118
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
// Generic layout handler
|
|
120
|
+
const applyLayout = useCallback((layoutType: string) => {
|
|
121
|
+
// Get the React Flow pane element to find its actual center
|
|
122
|
+
const pane = document.querySelector('.react-flow__pane');
|
|
123
|
+
let viewportCenter = { x: 0, y: 0 };
|
|
124
|
+
|
|
125
|
+
if (pane) {
|
|
126
|
+
const rect = pane.getBoundingClientRect();
|
|
127
|
+
// Convert screen center of the pane to flow coordinates
|
|
128
|
+
viewportCenter = screenToFlowPosition({
|
|
129
|
+
x: rect.left + rect.width / 2,
|
|
130
|
+
y: rect.top + rect.height / 2,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let result;
|
|
135
|
+
switch (layoutType) {
|
|
136
|
+
case 'hierarchical-vertical':
|
|
137
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
|
|
138
|
+
break;
|
|
139
|
+
case 'hierarchical-horizontal':
|
|
140
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'LR', center: viewportCenter });
|
|
141
|
+
break;
|
|
142
|
+
case 'circular':
|
|
143
|
+
result = applyCircularLayout(nodes, edges, { center: viewportCenter });
|
|
144
|
+
break;
|
|
145
|
+
case 'grid':
|
|
146
|
+
result = applyGridLayout(nodes, edges, { center: viewportCenter });
|
|
147
|
+
break;
|
|
148
|
+
case 'random':
|
|
149
|
+
result = applyRandomLayout(nodes, edges, { center: viewportCenter });
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
|
|
153
|
+
}
|
|
119
154
|
|
|
120
155
|
// Update nodes with new positions
|
|
121
|
-
|
|
156
|
+
result.nodes.forEach((node) => {
|
|
122
157
|
updateNodePosition(node.id, node.position);
|
|
123
158
|
});
|
|
124
159
|
|
|
125
|
-
useGraphStore.setState({ nodes:
|
|
160
|
+
useGraphStore.setState({ nodes: result.nodes });
|
|
126
161
|
setContextMenu(null);
|
|
127
162
|
setShowModuleSubmenu(false);
|
|
128
|
-
|
|
163
|
+
setShowLayoutSubmenu(false);
|
|
164
|
+
}, [nodes, edges, updateNodePosition, screenToFlowPosition]);
|
|
129
165
|
|
|
130
166
|
// Auto-zoom handler
|
|
131
167
|
const handleAutoZoom = useCallback(() => {
|
|
@@ -163,6 +199,7 @@ const GraphCanvas: React.FC = () => {
|
|
|
163
199
|
const onPaneClick = useCallback(() => {
|
|
164
200
|
setContextMenu(null);
|
|
165
201
|
setShowModuleSubmenu(false);
|
|
202
|
+
setShowLayoutSubmenu(false);
|
|
166
203
|
}, []);
|
|
167
204
|
|
|
168
205
|
// Node drag handler - prevent overlaps with collision detection
|
|
@@ -270,29 +307,170 @@ const GraphCanvas: React.FC = () => {
|
|
|
270
307
|
minWidth: 180,
|
|
271
308
|
}}
|
|
272
309
|
>
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
<div style={{ position: 'relative' }}>
|
|
311
|
+
<button
|
|
312
|
+
onMouseEnter={() => setShowLayoutSubmenu(true)}
|
|
313
|
+
onMouseLeave={(e) => {
|
|
314
|
+
const relatedTarget = e.relatedTarget as HTMLElement;
|
|
315
|
+
if (!relatedTarget || !relatedTarget.closest('.layout-submenu')) {
|
|
316
|
+
setShowLayoutSubmenu(false);
|
|
317
|
+
}
|
|
318
|
+
}}
|
|
319
|
+
style={{
|
|
320
|
+
display: 'flex',
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
justifyContent: 'space-between',
|
|
323
|
+
width: '100%',
|
|
324
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
325
|
+
border: 'none',
|
|
326
|
+
background: showLayoutSubmenu ? 'var(--color-bg-overlay)' : 'transparent',
|
|
327
|
+
cursor: 'pointer',
|
|
328
|
+
textAlign: 'left',
|
|
329
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
330
|
+
color: 'var(--color-text-primary)',
|
|
331
|
+
transition: 'var(--transition-colors)',
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<span>Auto Layout</span>
|
|
335
|
+
<span style={{ marginLeft: 'var(--spacing-2)' }}>▶</span>
|
|
336
|
+
</button>
|
|
337
|
+
|
|
338
|
+
{/* Layout Submenu */}
|
|
339
|
+
{showLayoutSubmenu && (
|
|
340
|
+
<div
|
|
341
|
+
className="layout-submenu"
|
|
342
|
+
onMouseEnter={() => setShowLayoutSubmenu(true)}
|
|
343
|
+
onMouseLeave={() => setShowLayoutSubmenu(false)}
|
|
344
|
+
style={{
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
top: 0,
|
|
347
|
+
left: '100%',
|
|
348
|
+
background: 'var(--color-bg-surface)',
|
|
349
|
+
border: 'var(--border-default)',
|
|
350
|
+
borderRadius: 'var(--radius-md)',
|
|
351
|
+
boxShadow: 'var(--shadow-lg)',
|
|
352
|
+
zIndex: 1001,
|
|
353
|
+
minWidth: 180,
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => applyLayout('hierarchical-vertical')}
|
|
358
|
+
style={{
|
|
359
|
+
display: 'block',
|
|
360
|
+
width: '100%',
|
|
361
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
362
|
+
border: 'none',
|
|
363
|
+
background: 'transparent',
|
|
364
|
+
cursor: 'pointer',
|
|
365
|
+
textAlign: 'left',
|
|
366
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
367
|
+
color: 'var(--color-text-primary)',
|
|
368
|
+
transition: 'var(--transition-colors)',
|
|
369
|
+
}}
|
|
370
|
+
onMouseEnter={(e) => {
|
|
371
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
372
|
+
}}
|
|
373
|
+
onMouseLeave={(e) => {
|
|
374
|
+
e.currentTarget.style.background = 'transparent';
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
Hierarchical (Vertical)
|
|
378
|
+
</button>
|
|
379
|
+
<button
|
|
380
|
+
onClick={() => applyLayout('hierarchical-horizontal')}
|
|
381
|
+
style={{
|
|
382
|
+
display: 'block',
|
|
383
|
+
width: '100%',
|
|
384
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
385
|
+
border: 'none',
|
|
386
|
+
background: 'transparent',
|
|
387
|
+
cursor: 'pointer',
|
|
388
|
+
textAlign: 'left',
|
|
389
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
390
|
+
color: 'var(--color-text-primary)',
|
|
391
|
+
transition: 'var(--transition-colors)',
|
|
392
|
+
}}
|
|
393
|
+
onMouseEnter={(e) => {
|
|
394
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
395
|
+
}}
|
|
396
|
+
onMouseLeave={(e) => {
|
|
397
|
+
e.currentTarget.style.background = 'transparent';
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
Hierarchical (Horizontal)
|
|
401
|
+
</button>
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => applyLayout('circular')}
|
|
404
|
+
style={{
|
|
405
|
+
display: 'block',
|
|
406
|
+
width: '100%',
|
|
407
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
408
|
+
border: 'none',
|
|
409
|
+
background: 'transparent',
|
|
410
|
+
cursor: 'pointer',
|
|
411
|
+
textAlign: 'left',
|
|
412
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
413
|
+
color: 'var(--color-text-primary)',
|
|
414
|
+
transition: 'var(--transition-colors)',
|
|
415
|
+
}}
|
|
416
|
+
onMouseEnter={(e) => {
|
|
417
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
418
|
+
}}
|
|
419
|
+
onMouseLeave={(e) => {
|
|
420
|
+
e.currentTarget.style.background = 'transparent';
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
Circular
|
|
424
|
+
</button>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => applyLayout('grid')}
|
|
427
|
+
style={{
|
|
428
|
+
display: 'block',
|
|
429
|
+
width: '100%',
|
|
430
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
431
|
+
border: 'none',
|
|
432
|
+
background: 'transparent',
|
|
433
|
+
cursor: 'pointer',
|
|
434
|
+
textAlign: 'left',
|
|
435
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
436
|
+
color: 'var(--color-text-primary)',
|
|
437
|
+
transition: 'var(--transition-colors)',
|
|
438
|
+
}}
|
|
439
|
+
onMouseEnter={(e) => {
|
|
440
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
441
|
+
}}
|
|
442
|
+
onMouseLeave={(e) => {
|
|
443
|
+
e.currentTarget.style.background = 'transparent';
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
Grid
|
|
447
|
+
</button>
|
|
448
|
+
<button
|
|
449
|
+
onClick={() => applyLayout('random')}
|
|
450
|
+
style={{
|
|
451
|
+
display: 'block',
|
|
452
|
+
width: '100%',
|
|
453
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
454
|
+
border: 'none',
|
|
455
|
+
background: 'transparent',
|
|
456
|
+
cursor: 'pointer',
|
|
457
|
+
textAlign: 'left',
|
|
458
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
459
|
+
color: 'var(--color-text-primary)',
|
|
460
|
+
transition: 'var(--transition-colors)',
|
|
461
|
+
}}
|
|
462
|
+
onMouseEnter={(e) => {
|
|
463
|
+
e.currentTarget.style.background = 'var(--color-bg-overlay)';
|
|
464
|
+
}}
|
|
465
|
+
onMouseLeave={(e) => {
|
|
466
|
+
e.currentTarget.style.background = 'transparent';
|
|
467
|
+
}}
|
|
468
|
+
>
|
|
469
|
+
Random
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
296
474
|
|
|
297
475
|
<button
|
|
298
476
|
onClick={handleAutoZoom}
|
|
@@ -15,6 +15,7 @@ export interface LayoutOptions {
|
|
|
15
15
|
direction?: 'TB' | 'LR' | 'BT' | 'RL';
|
|
16
16
|
nodeSpacing?: number;
|
|
17
17
|
rankSpacing?: number;
|
|
18
|
+
center?: { x: number; y: number }; // Optional center point for layout
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface LayoutResult {
|
|
@@ -30,10 +31,6 @@ const DEFAULT_NODE_HEIGHT = 80;
|
|
|
30
31
|
const MESSAGE_NODE_WIDTH = 150;
|
|
31
32
|
const MESSAGE_NODE_HEIGHT = 60;
|
|
32
33
|
|
|
33
|
-
// Default spacing (increased by 50% for better label visibility)
|
|
34
|
-
const DEFAULT_NODE_SPACING = 75; // Was 50
|
|
35
|
-
const DEFAULT_RANK_SPACING = 150; // Was 100
|
|
36
|
-
|
|
37
34
|
/**
|
|
38
35
|
* Get node dimensions based on node type
|
|
39
36
|
*/
|
|
@@ -59,8 +56,7 @@ export function applyHierarchicalLayout(
|
|
|
59
56
|
): LayoutResult {
|
|
60
57
|
const {
|
|
61
58
|
direction = 'TB',
|
|
62
|
-
|
|
63
|
-
rankSpacing = DEFAULT_RANK_SPACING,
|
|
59
|
+
center,
|
|
64
60
|
} = options;
|
|
65
61
|
|
|
66
62
|
// Handle empty graph
|
|
@@ -68,6 +64,21 @@ export function applyHierarchicalLayout(
|
|
|
68
64
|
return { nodes: [], edges, width: 0, height: 0 };
|
|
69
65
|
}
|
|
70
66
|
|
|
67
|
+
// Calculate dynamic spacing based on actual node sizes
|
|
68
|
+
// This ensures 200px minimum clearance regardless of node dimensions
|
|
69
|
+
let maxWidth = 0;
|
|
70
|
+
let maxHeight = 0;
|
|
71
|
+
|
|
72
|
+
nodes.forEach((node) => {
|
|
73
|
+
const { width, height } = getNodeDimensions(node);
|
|
74
|
+
maxWidth = Math.max(maxWidth, width);
|
|
75
|
+
maxHeight = Math.max(maxHeight, height);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Spacing = half of max node size + 200px minimum clearance
|
|
79
|
+
const nodeSpacing = options.nodeSpacing ?? (maxWidth / 2 + 200);
|
|
80
|
+
const rankSpacing = options.rankSpacing ?? (maxHeight / 2 + 200);
|
|
81
|
+
|
|
71
82
|
// Create a new directed graph
|
|
72
83
|
const graph = new dagre.graphlib.Graph();
|
|
73
84
|
|
|
@@ -97,6 +108,15 @@ export function applyHierarchicalLayout(
|
|
|
97
108
|
// Run the layout algorithm
|
|
98
109
|
dagre.layout(graph);
|
|
99
110
|
|
|
111
|
+
// Get graph dimensions first to calculate offset
|
|
112
|
+
const graphConfig = graph.graph();
|
|
113
|
+
const graphWidth = (graphConfig.width || 0) + 40; // Add margin
|
|
114
|
+
const graphHeight = (graphConfig.height || 0) + 40; // Add margin
|
|
115
|
+
|
|
116
|
+
// Calculate offset to center the layout around viewport center (or 0,0 if no center provided)
|
|
117
|
+
const offsetX = center ? center.x - graphWidth / 2 : 0;
|
|
118
|
+
const offsetY = center ? center.y - graphHeight / 2 : 0;
|
|
119
|
+
|
|
100
120
|
// Extract positioned nodes
|
|
101
121
|
const layoutedNodes = nodes.map((node) => {
|
|
102
122
|
const nodeWithPosition = graph.node(node.id);
|
|
@@ -107,22 +127,211 @@ export function applyHierarchicalLayout(
|
|
|
107
127
|
return {
|
|
108
128
|
...node,
|
|
109
129
|
position: {
|
|
110
|
-
x: nodeWithPosition.x - width / 2,
|
|
111
|
-
y: nodeWithPosition.y - height / 2,
|
|
130
|
+
x: nodeWithPosition.x - width / 2 + offsetX,
|
|
131
|
+
y: nodeWithPosition.y - height / 2 + offsetY,
|
|
112
132
|
},
|
|
113
133
|
};
|
|
114
134
|
});
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
return {
|
|
137
|
+
nodes: layoutedNodes,
|
|
138
|
+
edges,
|
|
139
|
+
width: graphWidth,
|
|
140
|
+
height: graphHeight,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Apply circular layout - nodes arranged in a circle
|
|
146
|
+
*/
|
|
147
|
+
export function applyCircularLayout(
|
|
148
|
+
nodes: Node[],
|
|
149
|
+
edges: Edge[],
|
|
150
|
+
options: LayoutOptions = {}
|
|
151
|
+
): LayoutResult {
|
|
152
|
+
const { center } = options;
|
|
153
|
+
|
|
154
|
+
if (nodes.length === 0) {
|
|
155
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Calculate radius based on number of nodes and their sizes
|
|
159
|
+
let maxWidth = 0;
|
|
160
|
+
let maxHeight = 0;
|
|
161
|
+
nodes.forEach((node) => {
|
|
162
|
+
const { width, height } = getNodeDimensions(node);
|
|
163
|
+
maxWidth = Math.max(maxWidth, width);
|
|
164
|
+
maxHeight = Math.max(maxHeight, height);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const minSpacing = 200; // 200px minimum clearance
|
|
168
|
+
const nodeSize = Math.max(maxWidth, maxHeight);
|
|
169
|
+
const circumference = nodes.length * (nodeSize + minSpacing);
|
|
170
|
+
const radius = circumference / (2 * Math.PI);
|
|
171
|
+
|
|
172
|
+
const centerX = center?.x ?? 0;
|
|
173
|
+
const centerY = center?.y ?? 0;
|
|
174
|
+
|
|
175
|
+
const layoutedNodes = nodes.map((node, index) => {
|
|
176
|
+
const angle = (2 * Math.PI * index) / nodes.length;
|
|
177
|
+
const { width, height } = getNodeDimensions(node);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...node,
|
|
181
|
+
position: {
|
|
182
|
+
x: centerX + radius * Math.cos(angle) - width / 2,
|
|
183
|
+
y: centerY + radius * Math.sin(angle) - height / 2,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const graphWidth = radius * 2 + maxWidth;
|
|
189
|
+
const graphHeight = radius * 2 + maxHeight;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
nodes: layoutedNodes,
|
|
193
|
+
edges,
|
|
194
|
+
width: graphWidth,
|
|
195
|
+
height: graphHeight,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply grid layout - nodes arranged in a grid
|
|
201
|
+
*/
|
|
202
|
+
export function applyGridLayout(
|
|
203
|
+
nodes: Node[],
|
|
204
|
+
edges: Edge[],
|
|
205
|
+
options: LayoutOptions = {}
|
|
206
|
+
): LayoutResult {
|
|
207
|
+
const { center } = options;
|
|
208
|
+
|
|
209
|
+
if (nodes.length === 0) {
|
|
210
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Calculate grid dimensions
|
|
214
|
+
const cols = Math.ceil(Math.sqrt(nodes.length));
|
|
215
|
+
const rows = Math.ceil(nodes.length / cols);
|
|
216
|
+
|
|
217
|
+
let maxWidth = 0;
|
|
218
|
+
let maxHeight = 0;
|
|
219
|
+
nodes.forEach((node) => {
|
|
220
|
+
const { width, height } = getNodeDimensions(node);
|
|
221
|
+
maxWidth = Math.max(maxWidth, width);
|
|
222
|
+
maxHeight = Math.max(maxHeight, height);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const minSpacing = 200;
|
|
226
|
+
const cellWidth = maxWidth + minSpacing;
|
|
227
|
+
const cellHeight = maxHeight + minSpacing;
|
|
228
|
+
|
|
229
|
+
const graphWidth = cols * cellWidth;
|
|
230
|
+
const graphHeight = rows * cellHeight;
|
|
231
|
+
|
|
232
|
+
const startX = center ? center.x - graphWidth / 2 : 0;
|
|
233
|
+
const startY = center ? center.y - graphHeight / 2 : 0;
|
|
234
|
+
|
|
235
|
+
const layoutedNodes = nodes.map((node, index) => {
|
|
236
|
+
const col = index % cols;
|
|
237
|
+
const row = Math.floor(index / cols);
|
|
238
|
+
const { width, height } = getNodeDimensions(node);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
...node,
|
|
242
|
+
position: {
|
|
243
|
+
x: startX + col * cellWidth + (cellWidth - width) / 2,
|
|
244
|
+
y: startY + row * cellHeight + (cellHeight - height) / 2,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
nodes: layoutedNodes,
|
|
251
|
+
edges,
|
|
252
|
+
width: graphWidth,
|
|
253
|
+
height: graphHeight,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Apply random layout - nodes placed randomly with minimum spacing
|
|
259
|
+
*/
|
|
260
|
+
export function applyRandomLayout(
|
|
261
|
+
nodes: Node[],
|
|
262
|
+
edges: Edge[],
|
|
263
|
+
options: LayoutOptions = {}
|
|
264
|
+
): LayoutResult {
|
|
265
|
+
const { center } = options;
|
|
266
|
+
|
|
267
|
+
if (nodes.length === 0) {
|
|
268
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let maxWidth = 0;
|
|
272
|
+
let maxHeight = 0;
|
|
273
|
+
nodes.forEach((node) => {
|
|
274
|
+
const { width, height } = getNodeDimensions(node);
|
|
275
|
+
maxWidth = Math.max(maxWidth, width);
|
|
276
|
+
maxHeight = Math.max(maxHeight, height);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const minSpacing = 200;
|
|
280
|
+
const spreadFactor = 1.5; // How much to spread nodes apart
|
|
281
|
+
const areaSize = Math.sqrt(nodes.length) * (maxWidth + maxHeight + minSpacing) * spreadFactor;
|
|
282
|
+
|
|
283
|
+
const centerX = center?.x ?? 0;
|
|
284
|
+
const centerY = center?.y ?? 0;
|
|
285
|
+
|
|
286
|
+
// Place nodes randomly, checking for collisions
|
|
287
|
+
const layoutedNodes: Node[] = [];
|
|
288
|
+
const maxAttempts = 100;
|
|
289
|
+
|
|
290
|
+
nodes.forEach((node) => {
|
|
291
|
+
const { width, height } = getNodeDimensions(node);
|
|
292
|
+
let placed = false;
|
|
293
|
+
let attempts = 0;
|
|
294
|
+
|
|
295
|
+
while (!placed && attempts < maxAttempts) {
|
|
296
|
+
const x = centerX + (Math.random() - 0.5) * areaSize - width / 2;
|
|
297
|
+
const y = centerY + (Math.random() - 0.5) * areaSize - height / 2;
|
|
298
|
+
|
|
299
|
+
// Check if this position collides with existing nodes
|
|
300
|
+
const collides = layoutedNodes.some((existingNode) => {
|
|
301
|
+
const exDims = getNodeDimensions(existingNode);
|
|
302
|
+
const dx = Math.abs(x - existingNode.position.x);
|
|
303
|
+
const dy = Math.abs(y - existingNode.position.y);
|
|
304
|
+
return dx < (width + exDims.width) / 2 + minSpacing &&
|
|
305
|
+
dy < (height + exDims.height) / 2 + minSpacing;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!collides) {
|
|
309
|
+
layoutedNodes.push({
|
|
310
|
+
...node,
|
|
311
|
+
position: { x, y },
|
|
312
|
+
});
|
|
313
|
+
placed = true;
|
|
314
|
+
}
|
|
315
|
+
attempts++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If we couldn't place it without collision, just place it anyway
|
|
319
|
+
if (!placed) {
|
|
320
|
+
layoutedNodes.push({
|
|
321
|
+
...node,
|
|
322
|
+
position: {
|
|
323
|
+
x: centerX + (Math.random() - 0.5) * areaSize - width / 2,
|
|
324
|
+
y: centerY + (Math.random() - 0.5) * areaSize - height / 2,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
120
329
|
|
|
121
330
|
return {
|
|
122
331
|
nodes: layoutedNodes,
|
|
123
332
|
edges,
|
|
124
|
-
width,
|
|
125
|
-
height,
|
|
333
|
+
width: areaSize,
|
|
334
|
+
height: areaSize,
|
|
126
335
|
};
|
|
127
336
|
}
|
|
128
337
|
|
|
@@ -135,12 +344,14 @@ export function applyDagreLayout(
|
|
|
135
344
|
edges: Edge[],
|
|
136
345
|
direction: 'TB' | 'LR' = 'TB',
|
|
137
346
|
nodeSpacing?: number,
|
|
138
|
-
rankSpacing?: number
|
|
347
|
+
rankSpacing?: number,
|
|
348
|
+
center?: { x: number; y: number }
|
|
139
349
|
): Node[] {
|
|
140
350
|
const result = applyHierarchicalLayout(nodes, edges, {
|
|
141
351
|
direction,
|
|
142
352
|
nodeSpacing,
|
|
143
|
-
rankSpacing
|
|
353
|
+
rankSpacing,
|
|
354
|
+
center
|
|
144
355
|
});
|
|
145
356
|
return result.nodes;
|
|
146
357
|
}
|
|
@@ -88,12 +88,12 @@ class DuckDBSpanExporter(TelemetryExporter):
|
|
|
88
88
|
with duckdb.connect(str(self.db_path)) as conn:
|
|
89
89
|
# Delete spans older than TTL
|
|
90
90
|
# Note: DuckDB doesn't support ? placeholders inside INTERVAL expressions
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
# Safe: ttl_days is guaranteed to be an int, no injection risk
|
|
92
|
+
query = f"""
|
|
93
93
|
DELETE FROM spans
|
|
94
94
|
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '{self.ttl_days} DAYS'
|
|
95
|
-
"""
|
|
96
|
-
)
|
|
95
|
+
""" # nosec B608
|
|
96
|
+
result = conn.execute(query)
|
|
97
97
|
|
|
98
98
|
deleted_count = result.fetchall()[0][0] if result else 0
|
|
99
99
|
|