flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b52__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/dashboard/launcher.py +1 -1
- flock/frontend/README.md +678 -0
- flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
- flock/frontend/index.html +12 -0
- flock/frontend/package-lock.json +4347 -0
- flock/frontend/package.json +48 -0
- flock/frontend/src/App.tsx +79 -0
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
- flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
- flock/frontend/src/components/common/BuildInfo.tsx +39 -0
- flock/frontend/src/components/common/EmptyState.module.css +115 -0
- flock/frontend/src/components/common/EmptyState.tsx +128 -0
- flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
- flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
- flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
- flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
- flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
- flock/frontend/src/components/controls/PublishControl.css +547 -0
- flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
- flock/frontend/src/components/controls/PublishControl.tsx +432 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
- flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
- flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
- flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
- flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
- flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
- flock/frontend/src/components/details/tabs.test.tsx +1015 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
- flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
- flock/frontend/src/components/filters/FilterBar.module.css +29 -0
- flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
- flock/frontend/src/components/filters/FilterBar.tsx +33 -0
- flock/frontend/src/components/filters/FilterPills.module.css +79 -0
- flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
- flock/frontend/src/components/filters/FilterPills.tsx +67 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
- flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
- flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
- flock/frontend/src/components/graph/AgentNode.tsx +322 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
- flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
- flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
- flock/frontend/src/components/graph/MessageNode.tsx +116 -0
- flock/frontend/src/components/graph/MiniMap.tsx +47 -0
- flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
- flock/frontend/src/components/layout/DashboardLayout.css +407 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
- flock/frontend/src/components/layout/Header.module.css +88 -0
- flock/frontend/src/components/layout/Header.tsx +52 -0
- flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
- flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
- flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
- flock/frontend/src/components/modules/registerModules.ts +20 -0
- flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
- flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
- flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
- flock/frontend/src/components/settings/SettingsPanel.css +327 -0
- flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
- flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
- flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
- flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
- flock/frontend/src/hooks/useModulePersistence.ts +154 -0
- flock/frontend/src/hooks/useModules.ts +139 -0
- flock/frontend/src/hooks/usePersistence.ts +139 -0
- flock/frontend/src/main.tsx +13 -0
- flock/frontend/src/services/api.ts +213 -0
- flock/frontend/src/services/indexeddb.test.ts +793 -0
- flock/frontend/src/services/indexeddb.ts +794 -0
- flock/frontend/src/services/layout.test.ts +437 -0
- flock/frontend/src/services/layout.ts +146 -0
- flock/frontend/src/services/themeApplicator.ts +140 -0
- flock/frontend/src/services/themeService.ts +77 -0
- flock/frontend/src/services/websocket.test.ts +595 -0
- flock/frontend/src/services/websocket.ts +685 -0
- flock/frontend/src/store/filterStore.test.ts +242 -0
- flock/frontend/src/store/filterStore.ts +103 -0
- flock/frontend/src/store/graphStore.test.ts +186 -0
- flock/frontend/src/store/graphStore.ts +414 -0
- flock/frontend/src/store/moduleStore.test.ts +253 -0
- flock/frontend/src/store/moduleStore.ts +57 -0
- flock/frontend/src/store/settingsStore.ts +188 -0
- flock/frontend/src/store/streamStore.ts +68 -0
- flock/frontend/src/store/uiStore.test.ts +54 -0
- flock/frontend/src/store/uiStore.ts +110 -0
- flock/frontend/src/store/wsStore.ts +34 -0
- flock/frontend/src/styles/index.css +15 -0
- flock/frontend/src/styles/scrollbar.css +47 -0
- flock/frontend/src/styles/variables.css +488 -0
- flock/frontend/src/test/setup.ts +1 -0
- flock/frontend/src/types/filters.ts +14 -0
- flock/frontend/src/types/graph.ts +55 -0
- flock/frontend/src/types/modules.ts +7 -0
- flock/frontend/src/types/theme.ts +55 -0
- flock/frontend/src/utils/mockData.ts +85 -0
- flock/frontend/src/utils/performance.ts +16 -0
- flock/frontend/src/utils/transforms.test.ts +860 -0
- flock/frontend/src/utils/transforms.ts +323 -0
- flock/frontend/src/vite-env.d.ts +17 -0
- flock/frontend/tsconfig.json +27 -0
- flock/frontend/tsconfig.node.json +11 -0
- flock/frontend/vite.config.ts +25 -0
- flock/frontend/vitest.config.ts +11 -0
- flock/helper/cli_helper.py +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { Node, Edge } from '@xyflow/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 4: Graph Visualization & Dual Views - Layout Tests
|
|
6
|
+
*
|
|
7
|
+
* Tests for Dagre-based hierarchical layout algorithm.
|
|
8
|
+
* These tests validate layout generation, node positioning, performance,
|
|
9
|
+
* and edge routing capabilities.
|
|
10
|
+
*
|
|
11
|
+
* SPECIFICATION: docs/specs/003-real-time-dashboard/PLAN.md Phase 4
|
|
12
|
+
* REQUIREMENT: Auto-layout completes <200ms for 10 nodes
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Type definitions for the layout service (to be implemented)
|
|
16
|
+
interface LayoutOptions {
|
|
17
|
+
direction?: 'TB' | 'LR' | 'BT' | 'RL';
|
|
18
|
+
nodeSpacing?: number;
|
|
19
|
+
rankSpacing?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LayoutResult {
|
|
23
|
+
nodes: Node[];
|
|
24
|
+
edges: Edge[];
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mock layout service interface (implementation will be in layout.ts)
|
|
30
|
+
interface LayoutService {
|
|
31
|
+
applyHierarchicalLayout(nodes: Node[], edges: Edge[], options?: LayoutOptions): LayoutResult;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('Layout Service', () => {
|
|
35
|
+
// This will be replaced with actual implementation
|
|
36
|
+
let layoutService: LayoutService;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Mock implementation for testing
|
|
40
|
+
layoutService = {
|
|
41
|
+
applyHierarchicalLayout: (_nodes, _edges, _options) => {
|
|
42
|
+
// Placeholder that will fail until real implementation
|
|
43
|
+
throw new Error('Layout service not implemented');
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Hierarchical Layout Generation', () => {
|
|
49
|
+
it('should generate hierarchical layout in vertical direction (TB)', () => {
|
|
50
|
+
const nodes: Node[] = [
|
|
51
|
+
{ id: 'agent-1', type: 'agent', position: { x: 0, y: 0 }, data: { name: 'Agent 1' } },
|
|
52
|
+
{ id: 'agent-2', type: 'agent', position: { x: 0, y: 0 }, data: { name: 'Agent 2' } },
|
|
53
|
+
{ id: 'agent-3', type: 'agent', position: { x: 0, y: 0 }, data: { name: 'Agent 3' } },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const edges: Edge[] = [
|
|
57
|
+
{ id: 'e1-2', source: 'agent-1', target: 'agent-2' },
|
|
58
|
+
{ id: 'e2-3', source: 'agent-2', target: 'agent-3' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
expect(() => {
|
|
62
|
+
layoutService.applyHierarchicalLayout(nodes, edges, { direction: 'TB' });
|
|
63
|
+
}).toThrow('Layout service not implemented');
|
|
64
|
+
|
|
65
|
+
// Expected behavior after implementation:
|
|
66
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges, { direction: 'TB' });
|
|
67
|
+
//
|
|
68
|
+
// expect(result.nodes).toHaveLength(3);
|
|
69
|
+
//
|
|
70
|
+
// // Verify vertical hierarchy: agent-1 should be above agent-2, agent-2 above agent-3
|
|
71
|
+
// const node1 = result.nodes.find(n => n.id === 'agent-1');
|
|
72
|
+
// const node2 = result.nodes.find(n => n.id === 'agent-2');
|
|
73
|
+
// const node3 = result.nodes.find(n => n.id === 'agent-3');
|
|
74
|
+
//
|
|
75
|
+
// expect(node1!.position.y).toBeLessThan(node2!.position.y);
|
|
76
|
+
// expect(node2!.position.y).toBeLessThan(node3!.position.y);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should generate hierarchical layout in horizontal direction (LR)', () => {
|
|
80
|
+
const nodes: Node[] = [
|
|
81
|
+
{ id: 'agent-1', type: 'agent', position: { x: 0, y: 0 }, data: { name: 'Agent 1' } },
|
|
82
|
+
{ id: 'agent-2', type: 'agent', position: { x: 0, y: 0 }, data: { name: 'Agent 2' } },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const edges: Edge[] = [
|
|
86
|
+
{ id: 'e1-2', source: 'agent-1', target: 'agent-2' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
layoutService.applyHierarchicalLayout(nodes, edges, { direction: 'LR' });
|
|
91
|
+
}).toThrow('Layout service not implemented');
|
|
92
|
+
|
|
93
|
+
// Expected behavior after implementation:
|
|
94
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges, { direction: 'LR' });
|
|
95
|
+
//
|
|
96
|
+
// const node1 = result.nodes.find(n => n.id === 'agent-1');
|
|
97
|
+
// const node2 = result.nodes.find(n => n.id === 'agent-2');
|
|
98
|
+
//
|
|
99
|
+
// // Verify horizontal hierarchy: agent-1 should be left of agent-2
|
|
100
|
+
// expect(node1!.position.x).toBeLessThan(node2!.position.x);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle cyclic graphs gracefully', () => {
|
|
104
|
+
const nodes: Node[] = [
|
|
105
|
+
{ id: 'agent-1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
106
|
+
{ id: 'agent-2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
107
|
+
{ id: 'agent-3', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const edges: Edge[] = [
|
|
111
|
+
{ id: 'e1-2', source: 'agent-1', target: 'agent-2' },
|
|
112
|
+
{ id: 'e2-3', source: 'agent-2', target: 'agent-3' },
|
|
113
|
+
{ id: 'e3-1', source: 'agent-3', target: 'agent-1' }, // Cycle
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
expect(() => {
|
|
117
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
118
|
+
}).toThrow('Layout service not implemented');
|
|
119
|
+
|
|
120
|
+
// Expected behavior: Should not throw, should produce valid layout
|
|
121
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
122
|
+
// expect(result.nodes).toHaveLength(3);
|
|
123
|
+
// result.nodes.forEach(node => {
|
|
124
|
+
// expect(node.position.x).toBeGreaterThanOrEqual(0);
|
|
125
|
+
// expect(node.position.y).toBeGreaterThanOrEqual(0);
|
|
126
|
+
// });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Node Positioning', () => {
|
|
131
|
+
it('should assign x, y coordinates to all nodes', () => {
|
|
132
|
+
const nodes: Node[] = [
|
|
133
|
+
{ id: 'node-1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
134
|
+
{ id: 'node-2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
135
|
+
{ id: 'node-3', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const edges: Edge[] = [
|
|
139
|
+
{ id: 'e1-2', source: 'node-1', target: 'node-2' },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
expect(() => {
|
|
143
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
144
|
+
}).toThrow('Layout service not implemented');
|
|
145
|
+
|
|
146
|
+
// Expected behavior:
|
|
147
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
148
|
+
//
|
|
149
|
+
// result.nodes.forEach(node => {
|
|
150
|
+
// expect(node.position).toHaveProperty('x');
|
|
151
|
+
// expect(node.position).toHaveProperty('y');
|
|
152
|
+
// expect(typeof node.position.x).toBe('number');
|
|
153
|
+
// expect(typeof node.position.y).toBe('number');
|
|
154
|
+
// expect(Number.isFinite(node.position.x)).toBe(true);
|
|
155
|
+
// expect(Number.isFinite(node.position.y)).toBe(true);
|
|
156
|
+
// });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should prevent node overlap with proper spacing', () => {
|
|
160
|
+
const nodes: Node[] = Array.from({ length: 5 }, (_, i) => ({
|
|
161
|
+
id: `node-${i}`,
|
|
162
|
+
type: 'agent',
|
|
163
|
+
position: { x: 0, y: 0 },
|
|
164
|
+
data: { name: `Agent ${i}` },
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const edges: Edge[] = Array.from({ length: 4 }, (_, i) => ({
|
|
168
|
+
id: `e${i}-${i + 1}`,
|
|
169
|
+
source: `node-${i}`,
|
|
170
|
+
target: `node-${i + 1}`,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
expect(() => {
|
|
174
|
+
layoutService.applyHierarchicalLayout(nodes, edges, { nodeSpacing: 50 });
|
|
175
|
+
}).toThrow('Layout service not implemented');
|
|
176
|
+
|
|
177
|
+
// Expected behavior:
|
|
178
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges, { nodeSpacing: 50 });
|
|
179
|
+
//
|
|
180
|
+
// // Check that no two nodes are too close
|
|
181
|
+
// const minDistance = 50; // Based on nodeSpacing
|
|
182
|
+
// for (let i = 0; i < result.nodes.length; i++) {
|
|
183
|
+
// for (let j = i + 1; j < result.nodes.length; j++) {
|
|
184
|
+
// const node1 = result.nodes[i];
|
|
185
|
+
// const node2 = result.nodes[j];
|
|
186
|
+
// const distance = Math.sqrt(
|
|
187
|
+
// Math.pow(node1.position.x - node2.position.x, 2) +
|
|
188
|
+
// Math.pow(node1.position.y - node2.position.y, 2)
|
|
189
|
+
// );
|
|
190
|
+
// expect(distance).toBeGreaterThanOrEqual(minDistance);
|
|
191
|
+
// }
|
|
192
|
+
// }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should respect custom node spacing', () => {
|
|
196
|
+
const nodes: Node[] = [
|
|
197
|
+
{ id: 'node-1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
198
|
+
{ id: 'node-2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const edges: Edge[] = [
|
|
202
|
+
{ id: 'e1-2', source: 'node-1', target: 'node-2' },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
expect(() => {
|
|
206
|
+
const customSpacing = 100;
|
|
207
|
+
layoutService.applyHierarchicalLayout(nodes, edges, { nodeSpacing: customSpacing });
|
|
208
|
+
}).toThrow('Layout service not implemented');
|
|
209
|
+
|
|
210
|
+
// Expected behavior:
|
|
211
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges, { nodeSpacing: 100 });
|
|
212
|
+
//
|
|
213
|
+
// const node1 = result.nodes.find(n => n.id === 'node-1')!;
|
|
214
|
+
// const node2 = result.nodes.find(n => n.id === 'node-2')!;
|
|
215
|
+
//
|
|
216
|
+
// // Distance should be at least the custom spacing
|
|
217
|
+
// const distance = Math.abs(node2.position.y - node1.position.y);
|
|
218
|
+
// expect(distance).toBeGreaterThanOrEqual(100);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Layout Performance', () => {
|
|
223
|
+
it('should complete layout in <200ms for 10 nodes (REQUIREMENT)', () => {
|
|
224
|
+
const nodes: Node[] = Array.from({ length: 10 }, (_, i) => ({
|
|
225
|
+
id: `node-${i}`,
|
|
226
|
+
type: 'agent',
|
|
227
|
+
position: { x: 0, y: 0 },
|
|
228
|
+
data: { name: `Agent ${i}` },
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
// Create a connected graph
|
|
232
|
+
const edges: Edge[] = Array.from({ length: 9 }, (_, i) => ({
|
|
233
|
+
id: `e${i}-${i + 1}`,
|
|
234
|
+
source: `node-${i}`,
|
|
235
|
+
target: `node-${i + 1}`,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
expect(() => {
|
|
239
|
+
const startTime = performance.now();
|
|
240
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
241
|
+
const endTime = performance.now();
|
|
242
|
+
const duration = endTime - startTime;
|
|
243
|
+
|
|
244
|
+
// This will fail until implementation
|
|
245
|
+
expect(duration).toBeLessThan(200);
|
|
246
|
+
}).toThrow('Layout service not implemented');
|
|
247
|
+
|
|
248
|
+
// Expected behavior:
|
|
249
|
+
// const startTime = performance.now();
|
|
250
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
251
|
+
// const endTime = performance.now();
|
|
252
|
+
// const duration = endTime - startTime;
|
|
253
|
+
//
|
|
254
|
+
// expect(duration).toBeLessThan(200); // REQUIREMENT: <200ms for 10 nodes
|
|
255
|
+
// expect(result.nodes).toHaveLength(10);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should handle large graphs efficiently (50 nodes)', () => {
|
|
259
|
+
const nodes: Node[] = Array.from({ length: 50 }, (_, i) => ({
|
|
260
|
+
id: `node-${i}`,
|
|
261
|
+
type: 'agent',
|
|
262
|
+
position: { x: 0, y: 0 },
|
|
263
|
+
data: { name: `Agent ${i}` },
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
// Create edges with some complexity (not just linear)
|
|
267
|
+
const edges: Edge[] = [];
|
|
268
|
+
for (let i = 0; i < 49; i++) {
|
|
269
|
+
edges.push({
|
|
270
|
+
id: `e${i}-${i + 1}`,
|
|
271
|
+
source: `node-${i}`,
|
|
272
|
+
target: `node-${i + 1}`,
|
|
273
|
+
});
|
|
274
|
+
// Add some cross-edges for complexity
|
|
275
|
+
if (i % 5 === 0 && i + 2 < 50) {
|
|
276
|
+
edges.push({
|
|
277
|
+
id: `e${i}-${i + 2}`,
|
|
278
|
+
source: `node-${i}`,
|
|
279
|
+
target: `node-${i + 2}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
expect(() => {
|
|
285
|
+
const startTime = performance.now();
|
|
286
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
287
|
+
const endTime = performance.now();
|
|
288
|
+
const duration = endTime - startTime;
|
|
289
|
+
|
|
290
|
+
// Should still be reasonably fast (<1s)
|
|
291
|
+
expect(duration).toBeLessThan(1000);
|
|
292
|
+
}).toThrow('Layout service not implemented');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('Edge Routing', () => {
|
|
297
|
+
it('should preserve edge connections after layout', () => {
|
|
298
|
+
const nodes: Node[] = [
|
|
299
|
+
{ id: 'a', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
300
|
+
{ id: 'b', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
301
|
+
{ id: 'c', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const edges: Edge[] = [
|
|
305
|
+
{ id: 'e-a-b', source: 'a', target: 'b' },
|
|
306
|
+
{ id: 'e-b-c', source: 'b', target: 'c' },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
expect(() => {
|
|
310
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
311
|
+
}).toThrow('Layout service not implemented');
|
|
312
|
+
|
|
313
|
+
// Expected behavior:
|
|
314
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
315
|
+
//
|
|
316
|
+
// // Edges should remain unchanged in structure
|
|
317
|
+
// expect(result.edges).toHaveLength(2);
|
|
318
|
+
// expect(result.edges.find(e => e.id === 'e-a-b')).toBeDefined();
|
|
319
|
+
// expect(result.edges.find(e => e.id === 'e-b-c')).toBeDefined();
|
|
320
|
+
//
|
|
321
|
+
// // Source and target should still match
|
|
322
|
+
// const edgeAB = result.edges.find(e => e.id === 'e-a-b')!;
|
|
323
|
+
// expect(edgeAB.source).toBe('a');
|
|
324
|
+
// expect(edgeAB.target).toBe('b');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should return layout dimensions (width, height)', () => {
|
|
328
|
+
const nodes: Node[] = [
|
|
329
|
+
{ id: 'node-1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
330
|
+
{ id: 'node-2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const edges: Edge[] = [
|
|
334
|
+
{ id: 'e1-2', source: 'node-1', target: 'node-2' },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
expect(() => {
|
|
338
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
339
|
+
}).toThrow('Layout service not implemented');
|
|
340
|
+
|
|
341
|
+
// Expected behavior:
|
|
342
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
343
|
+
//
|
|
344
|
+
// expect(result).toHaveProperty('width');
|
|
345
|
+
// expect(result).toHaveProperty('height');
|
|
346
|
+
// expect(typeof result.width).toBe('number');
|
|
347
|
+
// expect(typeof result.height).toBe('number');
|
|
348
|
+
// expect(result.width).toBeGreaterThan(0);
|
|
349
|
+
// expect(result.height).toBeGreaterThan(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle disconnected components', () => {
|
|
353
|
+
const nodes: Node[] = [
|
|
354
|
+
{ id: 'a1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
355
|
+
{ id: 'a2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
356
|
+
{ id: 'b1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
357
|
+
{ id: 'b2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const edges: Edge[] = [
|
|
361
|
+
{ id: 'e-a1-a2', source: 'a1', target: 'a2' },
|
|
362
|
+
{ id: 'e-b1-b2', source: 'b1', target: 'b2' },
|
|
363
|
+
// No edges between a* and b* - two separate components
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
expect(() => {
|
|
367
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
368
|
+
}).toThrow('Layout service not implemented');
|
|
369
|
+
|
|
370
|
+
// Expected behavior:
|
|
371
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
372
|
+
//
|
|
373
|
+
// // All nodes should still get positions
|
|
374
|
+
// expect(result.nodes).toHaveLength(4);
|
|
375
|
+
// result.nodes.forEach(node => {
|
|
376
|
+
// expect(Number.isFinite(node.position.x)).toBe(true);
|
|
377
|
+
// expect(Number.isFinite(node.position.y)).toBe(true);
|
|
378
|
+
// });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('Empty and Edge Cases', () => {
|
|
383
|
+
it('should handle empty node list', () => {
|
|
384
|
+
const nodes: Node[] = [];
|
|
385
|
+
const edges: Edge[] = [];
|
|
386
|
+
|
|
387
|
+
expect(() => {
|
|
388
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
389
|
+
}).toThrow('Layout service not implemented');
|
|
390
|
+
|
|
391
|
+
// Expected behavior:
|
|
392
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
393
|
+
// expect(result.nodes).toHaveLength(0);
|
|
394
|
+
// expect(result.edges).toHaveLength(0);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should handle single node with no edges', () => {
|
|
398
|
+
const nodes: Node[] = [
|
|
399
|
+
{ id: 'lone-node', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
400
|
+
];
|
|
401
|
+
const edges: Edge[] = [];
|
|
402
|
+
|
|
403
|
+
expect(() => {
|
|
404
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
405
|
+
}).toThrow('Layout service not implemented');
|
|
406
|
+
|
|
407
|
+
// Expected behavior:
|
|
408
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
409
|
+
// expect(result.nodes).toHaveLength(1);
|
|
410
|
+
// expect(result.nodes[0].position.x).toBeGreaterThanOrEqual(0);
|
|
411
|
+
// expect(result.nodes[0].position.y).toBeGreaterThanOrEqual(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should handle nodes with no connecting edges', () => {
|
|
415
|
+
const nodes: Node[] = [
|
|
416
|
+
{ id: 'node-1', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
417
|
+
{ id: 'node-2', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
418
|
+
{ id: 'node-3', type: 'agent', position: { x: 0, y: 0 }, data: {} },
|
|
419
|
+
];
|
|
420
|
+
const edges: Edge[] = [];
|
|
421
|
+
|
|
422
|
+
expect(() => {
|
|
423
|
+
layoutService.applyHierarchicalLayout(nodes, edges);
|
|
424
|
+
}).toThrow('Layout service not implemented');
|
|
425
|
+
|
|
426
|
+
// Expected behavior:
|
|
427
|
+
// const result = layoutService.applyHierarchicalLayout(nodes, edges);
|
|
428
|
+
// expect(result.nodes).toHaveLength(3);
|
|
429
|
+
//
|
|
430
|
+
// // All nodes should get valid positions
|
|
431
|
+
// result.nodes.forEach(node => {
|
|
432
|
+
// expect(Number.isFinite(node.position.x)).toBe(true);
|
|
433
|
+
// expect(Number.isFinite(node.position.y)).toBe(true);
|
|
434
|
+
// });
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import dagre from 'dagre';
|
|
2
|
+
import { Node, Edge } from '@xyflow/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 4: Graph Visualization & Dual Views - Layout Service
|
|
6
|
+
*
|
|
7
|
+
* Provides Dagre-based hierarchical layout algorithm for automatic node positioning.
|
|
8
|
+
* Supports both vertical (TB) and horizontal (LR) layouts with configurable spacing.
|
|
9
|
+
*
|
|
10
|
+
* REQUIREMENT: Must complete <200ms for 10 nodes
|
|
11
|
+
* SPECIFICATION: docs/specs/003-real-time-dashboard/PLAN.md Phase 4
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface LayoutOptions {
|
|
15
|
+
direction?: 'TB' | 'LR' | 'BT' | 'RL';
|
|
16
|
+
nodeSpacing?: number;
|
|
17
|
+
rankSpacing?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LayoutResult {
|
|
21
|
+
nodes: Node[];
|
|
22
|
+
edges: Edge[];
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Default node dimensions
|
|
28
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
29
|
+
const DEFAULT_NODE_HEIGHT = 80;
|
|
30
|
+
const MESSAGE_NODE_WIDTH = 150;
|
|
31
|
+
const MESSAGE_NODE_HEIGHT = 60;
|
|
32
|
+
|
|
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
|
+
/**
|
|
38
|
+
* Get node dimensions based on node type
|
|
39
|
+
*/
|
|
40
|
+
function getNodeDimensions(node: Node): { width: number; height: number } {
|
|
41
|
+
if (node.type === 'message') {
|
|
42
|
+
return { width: MESSAGE_NODE_WIDTH, height: MESSAGE_NODE_HEIGHT };
|
|
43
|
+
}
|
|
44
|
+
return { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Apply hierarchical layout using Dagre algorithm
|
|
49
|
+
*
|
|
50
|
+
* @param nodes - Array of nodes to layout
|
|
51
|
+
* @param edges - Array of edges defining connections
|
|
52
|
+
* @param options - Layout configuration options
|
|
53
|
+
* @returns Layout result with positioned nodes and graph dimensions
|
|
54
|
+
*/
|
|
55
|
+
export function applyHierarchicalLayout(
|
|
56
|
+
nodes: Node[],
|
|
57
|
+
edges: Edge[],
|
|
58
|
+
options: LayoutOptions = {}
|
|
59
|
+
): LayoutResult {
|
|
60
|
+
const {
|
|
61
|
+
direction = 'TB',
|
|
62
|
+
nodeSpacing = DEFAULT_NODE_SPACING,
|
|
63
|
+
rankSpacing = DEFAULT_RANK_SPACING,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
// Handle empty graph
|
|
67
|
+
if (nodes.length === 0) {
|
|
68
|
+
return { nodes: [], edges, width: 0, height: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create a new directed graph
|
|
72
|
+
const graph = new dagre.graphlib.Graph();
|
|
73
|
+
|
|
74
|
+
// Set graph layout options
|
|
75
|
+
graph.setGraph({
|
|
76
|
+
rankdir: direction,
|
|
77
|
+
nodesep: nodeSpacing,
|
|
78
|
+
ranksep: rankSpacing,
|
|
79
|
+
marginx: 20,
|
|
80
|
+
marginy: 20,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Default edge configuration
|
|
84
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
85
|
+
|
|
86
|
+
// Add nodes to the graph with their dimensions
|
|
87
|
+
nodes.forEach((node) => {
|
|
88
|
+
const { width, height } = getNodeDimensions(node);
|
|
89
|
+
graph.setNode(node.id, { width, height });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Add edges to the graph
|
|
93
|
+
edges.forEach((edge) => {
|
|
94
|
+
graph.setEdge(edge.source, edge.target);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Run the layout algorithm
|
|
98
|
+
dagre.layout(graph);
|
|
99
|
+
|
|
100
|
+
// Extract positioned nodes
|
|
101
|
+
const layoutedNodes = nodes.map((node) => {
|
|
102
|
+
const nodeWithPosition = graph.node(node.id);
|
|
103
|
+
|
|
104
|
+
// Dagre positions nodes at their center, we need top-left corner
|
|
105
|
+
const { width, height } = getNodeDimensions(node);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...node,
|
|
109
|
+
position: {
|
|
110
|
+
x: nodeWithPosition.x - width / 2,
|
|
111
|
+
y: nodeWithPosition.y - height / 2,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Get graph dimensions
|
|
117
|
+
const graphConfig = graph.graph();
|
|
118
|
+
const width = (graphConfig.width || 0) + 40; // Add margin
|
|
119
|
+
const height = (graphConfig.height || 0) + 40; // Add margin
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
nodes: layoutedNodes,
|
|
123
|
+
edges,
|
|
124
|
+
width,
|
|
125
|
+
height,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Legacy function name for backwards compatibility
|
|
131
|
+
* Delegates to applyHierarchicalLayout
|
|
132
|
+
*/
|
|
133
|
+
export function applyDagreLayout(
|
|
134
|
+
nodes: Node[],
|
|
135
|
+
edges: Edge[],
|
|
136
|
+
direction: 'TB' | 'LR' = 'TB',
|
|
137
|
+
nodeSpacing?: number,
|
|
138
|
+
rankSpacing?: number
|
|
139
|
+
): Node[] {
|
|
140
|
+
const result = applyHierarchicalLayout(nodes, edges, {
|
|
141
|
+
direction,
|
|
142
|
+
nodeSpacing,
|
|
143
|
+
rankSpacing
|
|
144
|
+
});
|
|
145
|
+
return result.nodes;
|
|
146
|
+
}
|