dbwatcher 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -210
  3. data/app/assets/config/dbwatcher_manifest.js +15 -0
  4. data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
  5. data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
  6. data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
  7. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
  8. data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
  9. data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
  10. data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
  11. data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
  12. data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
  13. data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
  14. data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
  15. data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
  16. data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
  17. data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
  18. data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
  19. data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
  20. data/app/assets/stylesheets/dbwatcher/application.css +423 -0
  21. data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
  22. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
  23. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
  24. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
  25. data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
  26. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
  27. data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
  28. data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
  29. data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
  30. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
  31. data/app/controllers/dbwatcher/base_controller.rb +8 -2
  32. data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
  33. data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
  34. data/app/helpers/dbwatcher/component_helper.rb +29 -0
  35. data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
  36. data/app/helpers/dbwatcher/session_helper.rb +3 -2
  37. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
  38. data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
  39. data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
  40. data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
  41. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
  42. data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
  43. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
  44. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
  45. data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
  46. data/app/views/dbwatcher/sessions/index.html.erb +14 -10
  47. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
  48. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
  49. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
  50. data/app/views/dbwatcher/sessions/show.html.erb +3 -346
  51. data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
  52. data/app/views/layouts/dbwatcher/application.html.erb +125 -247
  53. data/bin/compile_scss +49 -0
  54. data/config/routes.rb +26 -0
  55. data/lib/dbwatcher/configuration.rb +102 -8
  56. data/lib/dbwatcher/engine.rb +17 -7
  57. data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
  58. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
  59. data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
  60. data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
  61. data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
  62. data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
  63. data/lib/dbwatcher/services/base_service.rb +64 -0
  64. data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
  65. data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
  66. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
  67. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +603 -0
  68. data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
  69. data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -0
  70. data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
  71. data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
  72. data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
  73. data/lib/dbwatcher/services/diagram_data.rb +65 -0
  74. data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
  75. data/lib/dbwatcher/services/diagram_generator.rb +154 -0
  76. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
  77. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
  78. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
  79. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
  80. data/lib/dbwatcher/services/diagram_system.rb +69 -0
  81. data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
  82. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
  83. data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
  84. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +140 -0
  85. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -0
  86. data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
  87. data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
  88. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +118 -0
  89. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
  90. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
  91. data/lib/dbwatcher/storage/api/session_api.rb +47 -0
  92. data/lib/dbwatcher/storage/base_storage.rb +7 -0
  93. data/lib/dbwatcher/version.rb +1 -1
  94. data/lib/dbwatcher.rb +58 -1
  95. metadata +94 -2
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Diagrams Component
3
+ * API-first implementation for DBWatcher diagrams tab
4
+ */
5
+
6
+ // Register component with DBWatcher
7
+ DBWatcher.registerComponent('diagrams', function(config) {
8
+ // Ensure we have a sessionId from config or elsewhere
9
+ const sessionId = config.sessionId || config.session_id || (config.session && config.session.id);
10
+
11
+ return Object.assign(DBWatcher.BaseComponent(config), {
12
+ // Component-specific state
13
+ sessionId: sessionId,
14
+ diagramTypes: {},
15
+ selectedType: 'database_tables',
16
+ diagramContent: null,
17
+ panZoomInstance: null,
18
+ generating: false,
19
+ showCodeView: false, // Add state for code view toggle
20
+
21
+ // Component initialization
22
+ componentInit() {
23
+ // Validate sessionId before proceeding
24
+ if (!this.sessionId) {
25
+ console.error('No session ID available in component. Config was:', config);
26
+ this.handleError(new Error('No session ID provided'));
27
+ return;
28
+ }
29
+
30
+ // First load available diagram types from API
31
+ this.loadDiagramTypes().then(() => {
32
+ // Then load the actual diagram
33
+ this.loadDiagram();
34
+ });
35
+ },
36
+
37
+ // Component cleanup
38
+ componentDestroy() {
39
+ this.safelyDestroyPanZoom();
40
+ },
41
+
42
+ // Load available diagram types from API
43
+ async loadDiagramTypes() {
44
+ this.setLoading(true);
45
+ this.clearError();
46
+
47
+ try {
48
+ const url = `/dbwatcher/api/v1/sessions/diagram_types`;
49
+ const data = await this.fetchData(url);
50
+
51
+ if (data.types) {
52
+ this.diagramTypes = data.types;
53
+
54
+ // If URL has type parameter, use it, otherwise use default
55
+ const urlParams = new URLSearchParams(window.location.search);
56
+ const typeParam = urlParams.get('diagram_type');
57
+
58
+ if (typeParam && this.diagramTypes[typeParam]) {
59
+ this.selectedType = typeParam;
60
+ } else if (data.default_type) {
61
+ this.selectedType = data.default_type;
62
+ }
63
+ } else {
64
+ throw new Error('No diagram types received');
65
+ }
66
+ } catch (error) {
67
+ this.handleError(error);
68
+ } finally {
69
+ this.setLoading(false);
70
+ }
71
+ },
72
+
73
+ // Load diagram data from API
74
+ async loadDiagram() {
75
+ if (!this.sessionId) {
76
+ console.error('No session ID provided to diagrams component');
77
+ this.handleError(new Error('No session ID provided'));
78
+ return;
79
+ }
80
+
81
+ this.generating = true;
82
+ this.clearError();
83
+
84
+ try {
85
+ const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/diagram_data?type=${this.selectedType}`;
86
+ const data = await this.fetchData(url);
87
+
88
+ if (data.content) {
89
+ this.diagramContent = data.content;
90
+ // Update URL to reflect current diagram type
91
+ this.updateURL();
92
+ // Wait for DOM update
93
+ this.$nextTick(() => this.renderDiagram());
94
+ } else {
95
+ throw new Error('No diagram content received');
96
+ }
97
+ } catch (error) {
98
+ this.handleError(error);
99
+ } finally {
100
+ this.generating = false;
101
+ }
102
+ },
103
+
104
+ // Update URL with current diagram type
105
+ updateURL() {
106
+ const url = new URL(window.location.href);
107
+ const params = new URLSearchParams(url.search);
108
+
109
+ params.set('diagram_type', this.selectedType);
110
+
111
+ // Update URL without full page reload
112
+ url.search = params.toString();
113
+ window.history.replaceState({}, '', url.toString());
114
+ },
115
+
116
+ // Change diagram type
117
+ async changeType(newType) {
118
+ if (this.selectedType === newType) return;
119
+
120
+ this.selectedType = newType;
121
+ this.updateURL();
122
+ await this.loadDiagram();
123
+ },
124
+
125
+ // Render diagram using MermaidService
126
+ async renderDiagram() {
127
+ const container = this.$refs.diagramContainer;
128
+
129
+ if (!container || !this.diagramContent) {
130
+ return;
131
+ }
132
+
133
+ try {
134
+ // Cleanup previous pan/zoom instance safely
135
+ this.safelyDestroyPanZoom();
136
+
137
+ if (!window.MermaidService) {
138
+ throw new Error('MermaidService not available');
139
+ }
140
+
141
+ // Set container to full height to maximize diagram display area
142
+ container.style.height = '100%';
143
+ container.style.minHeight = '500px';
144
+
145
+ // Maximize diagram within its container
146
+ this.maximizeInContainer(container);
147
+
148
+ // Render with MermaidService
149
+ const result = await window.MermaidService.render(
150
+ this.diagramContent,
151
+ container,
152
+ {
153
+ fit: true,
154
+ center: true,
155
+ zoomEnabled: true,
156
+ panEnabled: true,
157
+ controlIconsEnabled: true
158
+ }
159
+ );
160
+
161
+ // Store pan/zoom instance if created
162
+ if (result && result.panZoom) {
163
+ this.panZoomInstance = result.panZoom;
164
+ console.log('Pan/zoom instance initialized successfully');
165
+ } else {
166
+ console.warn('Pan/zoom instance was not created');
167
+ }
168
+ } catch (error) {
169
+ this.handleError(error);
170
+ }
171
+ },
172
+
173
+ // Safely destroy pan zoom instance with error handling
174
+ safelyDestroyPanZoom() {
175
+ if (!this.panZoomInstance) return;
176
+
177
+ try {
178
+ this.panZoomInstance.destroy();
179
+ } catch (error) {
180
+ console.warn('Error destroying pan zoom instance:', error);
181
+ } finally {
182
+ this.panZoomInstance = null;
183
+ }
184
+ },
185
+
186
+ // Zoom controls
187
+ zoomIn() {
188
+ if (!this.panZoomInstance) return;
189
+
190
+ try {
191
+ this.panZoomInstance.zoomIn();
192
+ } catch (error) {
193
+ console.warn('Error zooming in:', error);
194
+ }
195
+ },
196
+
197
+ zoomOut() {
198
+ if (!this.panZoomInstance) return;
199
+
200
+ try {
201
+ this.panZoomInstance.zoomOut();
202
+ } catch (error) {
203
+ console.warn('Error zooming out:', error);
204
+ }
205
+ },
206
+
207
+ resetZoom() {
208
+ if (!this.panZoomInstance) return;
209
+
210
+ try {
211
+ this.panZoomInstance.resetZoom();
212
+ this.panZoomInstance.center();
213
+ } catch (error) {
214
+ console.warn('Error resetting zoom:', error);
215
+ }
216
+ },
217
+
218
+ // Reset view - alias for resetZoom for consistency with template
219
+ resetView() {
220
+ this.resetZoom();
221
+ },
222
+
223
+ // Download diagram as SVG
224
+ downloadSVG() {
225
+ const svgElement = this.$refs.diagramContainer?.querySelector('svg');
226
+ if (!svgElement) return;
227
+
228
+ try {
229
+ const svgData = new XMLSerializer().serializeToString(svgElement);
230
+ const blob = new Blob([svgData], { type: 'image/svg+xml' });
231
+ const url = URL.createObjectURL(blob);
232
+
233
+ const link = document.createElement('a');
234
+ link.href = url;
235
+ link.download = `dbwatcher-${this.selectedType}-diagram.svg`;
236
+ document.body.appendChild(link);
237
+ link.click();
238
+ document.body.removeChild(link);
239
+
240
+ URL.revokeObjectURL(url);
241
+ } catch (error) {
242
+ this.handleError(new Error('Failed to download SVG'));
243
+ }
244
+ },
245
+
246
+ // Toggle code view
247
+ toggleCodeView() {
248
+ this.showCodeView = !this.showCodeView;
249
+
250
+ // If showing code view, ensure the code is properly displayed
251
+ if (this.showCodeView && this.diagramContent) {
252
+ const codeContainer = this.$refs.codeContainer;
253
+ if (codeContainer) {
254
+ codeContainer.textContent = this.diagramContent;
255
+
256
+ // Ensure container is properly scrollable for large content
257
+ setTimeout(() => {
258
+ // Reset scroll position to top when showing code
259
+ const container = codeContainer.parentElement;
260
+ if (container) {
261
+ container.scrollTop = 0;
262
+ }
263
+
264
+ // Add specific handling for very large content
265
+ if (codeContainer.scrollHeight > window.innerHeight * 0.8) {
266
+ codeContainer.classList.add('large-content');
267
+ } else {
268
+ codeContainer.classList.remove('large-content');
269
+ }
270
+ }, 10);
271
+ }
272
+ }
273
+ },
274
+
275
+ // Copy diagram code to clipboard
276
+ copyDiagramCode() {
277
+ if (!this.diagramContent) return;
278
+
279
+ try {
280
+ navigator.clipboard.writeText(this.diagramContent).then(() => {
281
+ // Show a temporary success message
282
+ const copyBtn = this.$refs.copyButton;
283
+ if (copyBtn) {
284
+ const originalText = copyBtn.textContent;
285
+ copyBtn.textContent = 'Copied!';
286
+ copyBtn.classList.add('bg-green-500');
287
+
288
+ setTimeout(() => {
289
+ copyBtn.textContent = originalText;
290
+ copyBtn.classList.remove('bg-green-500');
291
+ }, 2000);
292
+ }
293
+ });
294
+ } catch (error) {
295
+ console.error('Failed to copy code:', error);
296
+ }
297
+ },
298
+
299
+ // Get diagram type metadata
300
+ getDiagramTypeInfo(type) {
301
+ return this.availableTypes[type] || {
302
+ display_name: type,
303
+ description: ''
304
+ };
305
+ },
306
+
307
+ // Maximize diagram within its container
308
+ maximizeInContainer(container) {
309
+ if (!container) return;
310
+
311
+ // Apply styles to ensure diagram fills available container space
312
+ const containerStyles = {
313
+ height: '100%',
314
+ minHeight: '500px',
315
+ display: 'flex',
316
+ flexDirection: 'column',
317
+ alignItems: 'center',
318
+ justifyContent: 'center',
319
+ overflow: 'hidden',
320
+ position: 'relative',
321
+ padding: '0.75rem',
322
+ margin: '0',
323
+ boxSizing: 'border-box',
324
+ borderRadius: '0.375rem'
325
+ };
326
+
327
+ Object.assign(container.style, containerStyles);
328
+
329
+ // Find SVG element and ensure it fills the container
330
+ const svgElement = container.querySelector('svg');
331
+ if (svgElement) {
332
+ // Make SVG responsive and fit container with enhanced styling
333
+ const svgStyles = {
334
+ width: '100%',
335
+ height: '100%',
336
+ maxWidth: '100%',
337
+ maxHeight: '100%',
338
+ display: 'block',
339
+ margin: 'auto',
340
+ borderRadius: '0.25rem',
341
+ boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)'
342
+ };
343
+
344
+ Object.assign(svgElement.style, svgStyles);
345
+
346
+ // Update SVG attributes for proper scaling
347
+ if (!svgElement.getAttribute('preserveAspectRatio')) {
348
+ svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
349
+ }
350
+
351
+ // Ensure dimensions are set
352
+ const containerWidth = container.clientWidth || 800;
353
+ const containerHeight = container.clientHeight || 600;
354
+
355
+ if (!svgElement.getAttribute('width') || !svgElement.getAttribute('height')) {
356
+ svgElement.setAttribute('width', containerWidth.toString());
357
+ svgElement.setAttribute('height', containerHeight.toString());
358
+ }
359
+
360
+ if (!svgElement.getAttribute('viewBox')) {
361
+ svgElement.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`);
362
+ }
363
+ }
364
+
365
+ // Ensure any child div is also maximized
366
+ const childDiv = container.querySelector('div.mermaid-diagram');
367
+ if (childDiv) {
368
+ const childStyles = {
369
+ width: '100%',
370
+ height: '100%',
371
+ display: 'flex',
372
+ alignItems: 'center',
373
+ justifyContent: 'center',
374
+ margin: '0',
375
+ padding: '0.5rem',
376
+ borderRadius: '0.375rem',
377
+ backgroundColor: '#f9fafb'
378
+ };
379
+
380
+ Object.assign(childDiv.style, childStyles);
381
+ }
382
+ },
383
+
384
+
385
+
386
+ // Error handling with user-friendly message and diagnostic logging
387
+ handleError(error) {
388
+ console.error('Error in diagrams component:', error);
389
+
390
+ // If we have a diagram container, display a user-friendly error
391
+ if (this.$refs.diagramContainer) {
392
+ this.$refs.diagramContainer.innerHTML = `
393
+ <div class="p-6 text-center bg-gray-50 rounded-md border border-gray-200 shadow-sm">
394
+ <div class="text-red-600 mb-3 font-medium">Error loading diagram</div>
395
+ <div class="text-sm text-gray-600 mb-4 p-2 bg-red-50 rounded border border-red-100">${error.message}</div>
396
+ <div class="mt-4">
397
+ <button class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 shadow-sm"
398
+ onclick="window.location.reload()">
399
+ Refresh Page
400
+ </button>
401
+ </div>
402
+ </div>
403
+ `;
404
+ }
405
+
406
+ // Add additional diagnostic logging
407
+ if (error.stack) {
408
+ console.debug('Error stack:', error.stack);
409
+ }
410
+
411
+ // Cleanup any existing pan/zoom instance
412
+ this.safelyDestroyPanZoom();
413
+ }
414
+ });
415
+ });
416
+
417
+ // Immediate fallback registration for Alpine.js
418
+ if (window.Alpine && window.Alpine.data) {
419
+ window.Alpine.data('diagrams', (config = {}) => {
420
+ console.log('Direct Alpine registration for diagrams called with config:', config);
421
+ if (window.DBWatcher && window.DBWatcher.components && window.DBWatcher.components.diagrams) {
422
+ return window.DBWatcher.components.diagrams(config);
423
+ } else {
424
+ console.error('DBWatcher diagrams component not available, providing fallback');
425
+ return {
426
+ error: 'Component not initialized',
427
+ init() {
428
+ this.error = 'DBWatcher not properly initialized';
429
+ }
430
+ };
431
+ }
432
+ });
433
+ }
434
+
435
+ // Also add a global function as a backup
436
+ window.diagrams = function(config = {}) {
437
+ console.log('Global diagrams function called with config:', config);
438
+ if (window.DBWatcher && window.DBWatcher.components && window.DBWatcher.components.diagrams) {
439
+ return window.DBWatcher.components.diagrams(config);
440
+ } else {
441
+ console.error('DBWatcher diagrams component not available in global function');
442
+ return {
443
+ error: 'Component not initialized',
444
+ init() {
445
+ this.error = 'DBWatcher not properly initialized';
446
+ }
447
+ };
448
+ }
449
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Summary Component
3
+ * API-first implementation for DBWatcher summary tab
4
+ */
5
+
6
+ // Register component with DBWatcher
7
+ DBWatcher.registerComponent('summary', function(config) {
8
+ return Object.assign(DBWatcher.BaseComponent(config), {
9
+ // Component-specific state
10
+ sessionId: config.sessionId,
11
+ summaryData: {},
12
+ autoRefresh: config.autoRefresh || false,
13
+ refreshInterval: null,
14
+
15
+ // Component initialization
16
+ componentInit() {
17
+ // Always load from API in API-first architecture
18
+ this.loadSummaryData();
19
+
20
+ // Setup auto-refresh if enabled
21
+ if (this.autoRefresh) {
22
+ this.startAutoRefresh();
23
+ }
24
+ },
25
+
26
+ // Component cleanup
27
+ componentDestroy() {
28
+ this.stopAutoRefresh();
29
+ },
30
+
31
+ // Load summary data from API
32
+ async loadSummaryData() {
33
+ if (!this.sessionId) {
34
+ console.error('No session ID provided to summary component');
35
+ this.handleError(new Error('No session ID provided'));
36
+ return;
37
+ }
38
+
39
+ this.setLoading(true);
40
+ this.clearError();
41
+
42
+ try {
43
+ const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/summary_data`;
44
+ const data = await this.fetchData(url);
45
+
46
+ if (!data.error) {
47
+ // API returns complete data structure including tables_breakdown and enhanced_stats
48
+ this.summaryData = data;
49
+ } else {
50
+ throw new Error(data.error || 'No summary data received');
51
+ }
52
+ } catch (error) {
53
+ this.handleError(error);
54
+ } finally {
55
+ this.setLoading(false);
56
+ }
57
+ },
58
+
59
+ // Toggle auto-refresh
60
+ toggleAutoRefresh() {
61
+ this.autoRefresh = !this.autoRefresh;
62
+
63
+ if (this.autoRefresh) {
64
+ this.startAutoRefresh();
65
+ } else {
66
+ this.stopAutoRefresh();
67
+ }
68
+ },
69
+
70
+ // Start auto-refresh interval
71
+ startAutoRefresh() {
72
+ if (this.refreshInterval) return;
73
+
74
+ this.refreshInterval = setInterval(() => {
75
+ this.loadSummaryData();
76
+ }, 30000); // 30 seconds
77
+ },
78
+
79
+ // Stop auto-refresh
80
+ stopAutoRefresh() {
81
+ if (this.refreshInterval) {
82
+ clearInterval(this.refreshInterval);
83
+ this.refreshInterval = null;
84
+ }
85
+ },
86
+
87
+ // Get total changes count
88
+ getTotalChanges() {
89
+ return this.summaryData.enhanced_stats?.total_changes || 0;
90
+ },
91
+
92
+ // Get total tables count
93
+ getTotalTables() {
94
+ return this.summaryData.enhanced_stats?.tables_count || 0;
95
+ },
96
+
97
+ // Get operation breakdown for charts
98
+ getOperationBreakdown() {
99
+ return this.summaryData.enhanced_stats?.operations_breakdown || { "INSERT": 0, "UPDATE": 0, "DELETE": 0 };
100
+ },
101
+
102
+ // Get table activity data for visualization
103
+ getTableActivity() {
104
+ if (!this.summaryData.tables_breakdown) return [];
105
+
106
+ return this.summaryData.tables_breakdown.map(table => ({
107
+ name: table.table_name,
108
+ total: table.change_count,
109
+ ...table.operations
110
+ }));
111
+ },
112
+
113
+ // Format percentage
114
+ formatPercentage(value, total) {
115
+ if (!total || total === 0) return '0%';
116
+ const percentage = (value / total) * 100;
117
+ return `${percentage.toFixed(1)}%`;
118
+ },
119
+
120
+ // Get operation color class
121
+ getOperationColor(operation) {
122
+ const colors = {
123
+ insert: 'text-green-600 bg-green-100',
124
+ update: 'text-blue-600 bg-blue-100',
125
+ delete: 'text-red-600 bg-red-100'
126
+ };
127
+ return colors[operation] || 'text-gray-600 bg-gray-100';
128
+ },
129
+
130
+ // Get activity level class
131
+ getActivityLevel(count) {
132
+ if (count === 0) return 'activity-none';
133
+ if (count < 10) return 'activity-low';
134
+ if (count < 50) return 'activity-medium';
135
+ if (count < 100) return 'activity-high';
136
+ return 'activity-very-high';
137
+ },
138
+
139
+ // Format duration using timing info
140
+ formatDuration() {
141
+ if (!this.summaryData.timing) return '--';
142
+ const timing = this.summaryData.timing;
143
+
144
+ if (timing.duration === null) return '--';
145
+
146
+ const ms = timing.duration;
147
+ if (ms < 1000) return `${ms}ms`;
148
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
149
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
150
+
151
+ const hours = Math.floor(ms / 3600000);
152
+ const minutes = Math.floor((ms % 3600000) / 60000);
153
+ return `${hours}h ${minutes}m`;
154
+ },
155
+
156
+ // Format start time
157
+ formatStartTime() {
158
+ if (!this.summaryData.timing?.started_at) return '--';
159
+ return this.formatDate(this.summaryData.timing.started_at, 'MMM dd, yyyy HH:mm:ss');
160
+ },
161
+
162
+ // Format end time
163
+ formatEndTime() {
164
+ if (!this.summaryData.timing?.ended_at) return 'Active';
165
+ return this.formatDate(this.summaryData.timing.ended_at, 'MMM dd, yyyy HH:mm:ss');
166
+ },
167
+
168
+ // Format operations per minute
169
+ formatOperationsPerMinute() {
170
+ if (!this.summaryData.enhanced_stats) return '0';
171
+
172
+ const duration = this.calculateDurationInMinutes();
173
+ if (duration <= 0) return '0';
174
+
175
+ const totalOps = this.summaryData.enhanced_stats.total_changes || 0;
176
+ const opsPerMin = totalOps / duration;
177
+ return opsPerMin.toFixed(1);
178
+ },
179
+
180
+ // Format peak activity time range
181
+ formatPeakActivity() {
182
+ if (!this.summaryData.enhanced_stats || !this.summaryData.enhanced_stats.peak_activity) {
183
+ return 'N/A';
184
+ }
185
+
186
+ const peak = this.summaryData.enhanced_stats.peak_activity;
187
+ return `${peak.count} / ${peak.period}s`;
188
+ },
189
+
190
+ // Calculate duration in minutes for stats
191
+ calculateDurationInMinutes() {
192
+ if (!this.summaryData.timing) return 0;
193
+
194
+ const start = new Date(this.summaryData.timing.started_at);
195
+ const end = this.summaryData.timing.ended_at ?
196
+ new Date(this.summaryData.timing.ended_at) :
197
+ new Date();
198
+
199
+ return (end - start) / (1000 * 60); // Convert ms to minutes
200
+ },
201
+
202
+ // Enhanced time formatting methods
203
+ formatStartTime() {
204
+ if (!this.summaryData.timing || !this.summaryData.timing.started_at) return 'N/A';
205
+ return new Date(this.summaryData.timing.started_at).toLocaleString();
206
+ },
207
+
208
+ formatEndTime() {
209
+ if (!this.summaryData.timing) return 'N/A';
210
+ if (!this.summaryData.timing.ended_at) return 'Active';
211
+ return new Date(this.summaryData.timing.ended_at).toLocaleString();
212
+ },
213
+
214
+ formatDuration() {
215
+ if (!this.summaryData.timing) return 'N/A';
216
+
217
+ const duration = this.summaryData.timing.duration;
218
+ if (!duration) return 'N/A';
219
+
220
+ if (duration < 60) {
221
+ return `${duration}s`;
222
+ } else if (duration < 3600) {
223
+ const minutes = Math.floor(duration / 60);
224
+ const seconds = duration % 60;
225
+ return `${minutes}m ${seconds}s`;
226
+ } else {
227
+ const hours = Math.floor(duration / 3600);
228
+ const minutes = Math.floor((duration % 3600) / 60);
229
+ const seconds = duration % 60;
230
+ return `${hours}h ${minutes}m ${seconds}s`;
231
+ }
232
+ },
233
+ });
234
+ });