dbwatcher 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- 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
|
+
});
|