flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b51__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.

Files changed (116) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/METADATA +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/RECORD +116 -6
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/WHEEL +0 -0
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/entry_points.txt +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.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
+ }