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.
- 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 +603 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -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 +140 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -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 +118 -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,315 @@
|
|
1
|
+
/**
|
2
|
+
* Simplified Mermaid Service
|
3
|
+
*
|
4
|
+
* Modern implementation using Mermaid v10+ API and svg-pan-zoom library
|
5
|
+
* Replaces 374 lines of complex custom implementation with ~50 lines
|
6
|
+
*/
|
7
|
+
|
8
|
+
const MermaidService = {
|
9
|
+
initialized: false,
|
10
|
+
|
11
|
+
// Initialize Mermaid with optimal settings
|
12
|
+
async initialize() {
|
13
|
+
if (this.initialized) return;
|
14
|
+
|
15
|
+
// Ensure Mermaid is loaded
|
16
|
+
if (!window.mermaid) {
|
17
|
+
await this.loadMermaid();
|
18
|
+
}
|
19
|
+
|
20
|
+
// Configure with modern settings
|
21
|
+
window.mermaid.initialize({
|
22
|
+
startOnLoad: false,
|
23
|
+
theme: 'neutral',
|
24
|
+
useMaxWidth: true,
|
25
|
+
responsive: true,
|
26
|
+
securityLevel: 'loose',
|
27
|
+
|
28
|
+
// Suppress warnings for better UX
|
29
|
+
logLevel: 'error',
|
30
|
+
|
31
|
+
// ER diagram settings
|
32
|
+
er: {
|
33
|
+
useMaxWidth: true,
|
34
|
+
layoutDirection: 'LR',
|
35
|
+
entityPadding: 15,
|
36
|
+
fontSize: 14
|
37
|
+
},
|
38
|
+
|
39
|
+
// Flowchart settings with better compatibility
|
40
|
+
flowchart: {
|
41
|
+
useMaxWidth: true,
|
42
|
+
htmlLabels: true,
|
43
|
+
curve: 'basis',
|
44
|
+
padding: 20,
|
45
|
+
nodeSpacing: 50,
|
46
|
+
rankSpacing: 50
|
47
|
+
},
|
48
|
+
|
49
|
+
// Suppress internal warnings
|
50
|
+
suppressErrorRendering: false,
|
51
|
+
suppressWarnings: true
|
52
|
+
});
|
53
|
+
|
54
|
+
this.initialized = true;
|
55
|
+
console.log('Mermaid service initialized');
|
56
|
+
},
|
57
|
+
|
58
|
+
// Load Mermaid library if not available
|
59
|
+
async loadMermaid() {
|
60
|
+
if (window.mermaid) return;
|
61
|
+
|
62
|
+
return new Promise((resolve, reject) => {
|
63
|
+
const script = document.createElement('script');
|
64
|
+
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
|
65
|
+
script.onload = resolve;
|
66
|
+
script.onerror = () => reject(new Error('Failed to load Mermaid library'));
|
67
|
+
document.head.appendChild(script);
|
68
|
+
});
|
69
|
+
},
|
70
|
+
|
71
|
+
// Render diagram with svg-pan-zoom integration
|
72
|
+
async render(content, container, options = {}) {
|
73
|
+
if (!content || !container) {
|
74
|
+
throw new Error('Content and container are required');
|
75
|
+
}
|
76
|
+
|
77
|
+
await this.initialize();
|
78
|
+
|
79
|
+
try {
|
80
|
+
// Clear container
|
81
|
+
container.innerHTML = '';
|
82
|
+
|
83
|
+
// Create diagram element - ensure full container size
|
84
|
+
const diagramDiv = document.createElement('div');
|
85
|
+
diagramDiv.className = 'mermaid-diagram';
|
86
|
+
|
87
|
+
// Set explicit styles for better browser rendering
|
88
|
+
const styles = {
|
89
|
+
width: '100%',
|
90
|
+
height: '100%',
|
91
|
+
minHeight: '400px',
|
92
|
+
display: 'flex',
|
93
|
+
alignItems: 'center',
|
94
|
+
justifyContent: 'center',
|
95
|
+
position: 'relative',
|
96
|
+
overflow: 'hidden'
|
97
|
+
};
|
98
|
+
|
99
|
+
Object.assign(diagramDiv.style, styles);
|
100
|
+
|
101
|
+
// Get container dimensions before appending
|
102
|
+
const containerWidth = container.clientWidth || 800;
|
103
|
+
const containerHeight = container.clientHeight || 600;
|
104
|
+
|
105
|
+
console.log('Container dimensions in render:', containerWidth, containerHeight);
|
106
|
+
|
107
|
+
// Set container to fill its parent
|
108
|
+
container.style.width = '100%';
|
109
|
+
container.style.height = '100%';
|
110
|
+
container.style.minHeight = '500px';
|
111
|
+
container.style.position = 'relative';
|
112
|
+
container.style.overflow = 'hidden';
|
113
|
+
|
114
|
+
container.appendChild(diagramDiv);
|
115
|
+
|
116
|
+
// Render with modern API and error handling
|
117
|
+
let renderResult;
|
118
|
+
try {
|
119
|
+
renderResult = await window.mermaid.render('diagram-' + Date.now(), content);
|
120
|
+
} catch (renderError) {
|
121
|
+
// Try to clean up content and retry once
|
122
|
+
const cleanContent = this.cleanDiagramContent(content);
|
123
|
+
renderResult = await window.mermaid.render('diagram-' + Date.now() + '-retry', cleanContent);
|
124
|
+
}
|
125
|
+
|
126
|
+
const { svg } = renderResult;
|
127
|
+
diagramDiv.innerHTML = svg;
|
128
|
+
|
129
|
+
// Post-process the SVG to ensure it has proper dimensions
|
130
|
+
const svgElement = diagramDiv.querySelector('svg');
|
131
|
+
if (svgElement) {
|
132
|
+
// Set CSS styles for SVG
|
133
|
+
Object.assign(svgElement.style, {
|
134
|
+
width: '100%',
|
135
|
+
height: '100%',
|
136
|
+
maxWidth: '100%',
|
137
|
+
maxHeight: '100%',
|
138
|
+
display: 'block'
|
139
|
+
});
|
140
|
+
|
141
|
+
// Make sure viewBox is set if not already
|
142
|
+
if (!svgElement.getAttribute('viewBox')) {
|
143
|
+
const width = svgElement.getAttribute('width') || diagramDiv.clientWidth;
|
144
|
+
const height = svgElement.getAttribute('height') || diagramDiv.clientHeight;
|
145
|
+
svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
// Enable pan/zoom if svg-pan-zoom is available
|
150
|
+
let panZoom = null;
|
151
|
+
|
152
|
+
if (svgElement && window.svgPanZoom) {
|
153
|
+
// enableInteractions now returns a Promise or null
|
154
|
+
panZoom = await Promise.resolve(this.enableInteractions(svgElement, options));
|
155
|
+
}
|
156
|
+
|
157
|
+
return { success: true, element: svgElement, panZoom };
|
158
|
+
} catch (error) {
|
159
|
+
console.error('Mermaid rendering failed:', error);
|
160
|
+
this.showError(container, error.message);
|
161
|
+
throw error;
|
162
|
+
}
|
163
|
+
},
|
164
|
+
|
165
|
+
// Enable SVG interactions using svg-pan-zoom library
|
166
|
+
enableInteractions(svgElement, options = {}) {
|
167
|
+
// Safety checks and set default dimensions if needed
|
168
|
+
if (!svgElement) {
|
169
|
+
console.warn('SVG element is null, cannot initialize pan-zoom');
|
170
|
+
return null;
|
171
|
+
}
|
172
|
+
|
173
|
+
// Always ensure the SVG element has dimensions before initializing pan-zoom
|
174
|
+
// Get container dimensions for reference
|
175
|
+
const containerWidth = svgElement.parentElement?.clientWidth || 800;
|
176
|
+
const containerHeight = svgElement.parentElement?.clientHeight || 600;
|
177
|
+
|
178
|
+
console.log('Container dimensions:', containerWidth, containerHeight);
|
179
|
+
|
180
|
+
// Force set dimensions regardless of existing values to ensure they're always set correctly
|
181
|
+
svgElement.setAttribute('width', containerWidth.toString());
|
182
|
+
svgElement.setAttribute('height', containerHeight.toString());
|
183
|
+
|
184
|
+
// Also set CSS dimensions for better browser rendering
|
185
|
+
svgElement.style.width = '100%';
|
186
|
+
svgElement.style.height = '100%';
|
187
|
+
svgElement.style.maxWidth = '100%';
|
188
|
+
svgElement.style.maxHeight = '100%';
|
189
|
+
|
190
|
+
// Set viewBox attribute if it doesn't exist or is invalid
|
191
|
+
const viewBox = svgElement.getAttribute('viewBox');
|
192
|
+
if (!viewBox || viewBox.split(' ').length !== 4) {
|
193
|
+
svgElement.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`);
|
194
|
+
}
|
195
|
+
|
196
|
+
// Set preserveAspectRatio for proper scaling
|
197
|
+
svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
198
|
+
|
199
|
+
// Double-check dimensions are now set and valid
|
200
|
+
const width = parseFloat(svgElement.getAttribute('width'));
|
201
|
+
const height = parseFloat(svgElement.getAttribute('height'));
|
202
|
+
|
203
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
|
204
|
+
console.warn('SVG element has invalid dimensions after setting defaults:', width, height);
|
205
|
+
// Set explicit fallback dimensions as a last resort
|
206
|
+
svgElement.setAttribute('width', '800');
|
207
|
+
svgElement.setAttribute('height', '600');
|
208
|
+
svgElement.setAttribute('viewBox', '0 0 800 600');
|
209
|
+
}
|
210
|
+
|
211
|
+
const config = {
|
212
|
+
zoomEnabled: true,
|
213
|
+
panEnabled: true,
|
214
|
+
controlIconsEnabled: false,
|
215
|
+
fit: true,
|
216
|
+
center: true,
|
217
|
+
minZoom: 0.1,
|
218
|
+
maxZoom: 5,
|
219
|
+
zoomScaleSensitivity: 0.1,
|
220
|
+
...options
|
221
|
+
};
|
222
|
+
|
223
|
+
try {
|
224
|
+
// Ensure SVG is properly rendered in DOM before initializing pan-zoom
|
225
|
+
// This forces a reflow which can help with dimension calculations
|
226
|
+
void svgElement.getBoundingClientRect();
|
227
|
+
|
228
|
+
// Short delay to ensure SVG is fully rendered before initializing pan-zoom
|
229
|
+
return new Promise(resolve => {
|
230
|
+
setTimeout(() => {
|
231
|
+
try {
|
232
|
+
// Initialize pan-zoom with the configured options
|
233
|
+
const panZoom = window.svgPanZoom(svgElement, config);
|
234
|
+
|
235
|
+
// Add keyboard shortcuts for better UX
|
236
|
+
this.addKeyboardShortcuts(panZoom);
|
237
|
+
|
238
|
+
resolve(panZoom);
|
239
|
+
} catch (initError) {
|
240
|
+
console.warn('Failed to initialize svg-pan-zoom:', initError);
|
241
|
+
resolve(null);
|
242
|
+
}
|
243
|
+
}, 100); // Small delay for SVG rendering
|
244
|
+
});
|
245
|
+
} catch (error) {
|
246
|
+
console.warn('Failed to enable SVG interactions:', error);
|
247
|
+
return null;
|
248
|
+
}
|
249
|
+
},
|
250
|
+
|
251
|
+
// Clean diagram content to fix common issues
|
252
|
+
cleanDiagramContent(content) {
|
253
|
+
return content
|
254
|
+
// Remove any problematic characters
|
255
|
+
.replace(/[^\x00-\x7F]/g, '')
|
256
|
+
// Normalize whitespace
|
257
|
+
.replace(/\s+/g, ' ')
|
258
|
+
// Remove empty lines
|
259
|
+
.split('\n')
|
260
|
+
.filter(line => line.trim())
|
261
|
+
.join('\n');
|
262
|
+
},
|
263
|
+
|
264
|
+
// Add keyboard shortcuts for pan/zoom
|
265
|
+
addKeyboardShortcuts(panZoom) {
|
266
|
+
const handleKeydown = (e) => {
|
267
|
+
if (!e.target.closest('.mermaid-diagram')) return;
|
268
|
+
|
269
|
+
switch (e.key) {
|
270
|
+
case '+':
|
271
|
+
case '=':
|
272
|
+
e.preventDefault();
|
273
|
+
panZoom.zoomIn();
|
274
|
+
break;
|
275
|
+
case '-':
|
276
|
+
e.preventDefault();
|
277
|
+
panZoom.zoomOut();
|
278
|
+
break;
|
279
|
+
case '0':
|
280
|
+
e.preventDefault();
|
281
|
+
panZoom.resetZoom();
|
282
|
+
panZoom.center();
|
283
|
+
break;
|
284
|
+
}
|
285
|
+
};
|
286
|
+
|
287
|
+
document.addEventListener('keydown', handleKeydown);
|
288
|
+
|
289
|
+
// Return cleanup function
|
290
|
+
return () => document.removeEventListener('keydown', handleKeydown);
|
291
|
+
},
|
292
|
+
|
293
|
+
// Show error in container
|
294
|
+
showError(container, message) {
|
295
|
+
container.innerHTML = `
|
296
|
+
<div class="flex items-center justify-center h-full">
|
297
|
+
<div class="text-center p-4">
|
298
|
+
<div class="text-red-600 mb-2">
|
299
|
+
<svg class="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
300
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
301
|
+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
302
|
+
</svg>
|
303
|
+
</div>
|
304
|
+
<div class="text-sm text-gray-600">
|
305
|
+
Failed to render diagram<br>
|
306
|
+
<span class="text-xs text-gray-500">${message}</span>
|
307
|
+
</div>
|
308
|
+
</div>
|
309
|
+
</div>
|
310
|
+
`;
|
311
|
+
}
|
312
|
+
};
|
313
|
+
|
314
|
+
// Make available globally
|
315
|
+
window.MermaidService = MermaidService;
|
@@ -0,0 +1,199 @@
|
|
1
|
+
/**
|
2
|
+
* Optimized Mermaid Service
|
3
|
+
*
|
4
|
+
* Modern implementation using Mermaid v10+ API and svg-pan-zoom library
|
5
|
+
* Uses tree-shakable design pattern for minimal code footprint
|
6
|
+
*/
|
7
|
+
|
8
|
+
const MermaidService = {
|
9
|
+
initialized: false,
|
10
|
+
|
11
|
+
// Initialize Mermaid with optimal settings
|
12
|
+
async initialize() {
|
13
|
+
if (this.initialized) return;
|
14
|
+
|
15
|
+
// Ensure Mermaid is loaded
|
16
|
+
if (!window.mermaid) {
|
17
|
+
console.error('Mermaid library not loaded');
|
18
|
+
throw new Error('Mermaid library not loaded');
|
19
|
+
}
|
20
|
+
|
21
|
+
// Configure with modern settings
|
22
|
+
window.mermaid.initialize({
|
23
|
+
startOnLoad: false,
|
24
|
+
theme: 'neutral',
|
25
|
+
useMaxWidth: true,
|
26
|
+
responsive: true,
|
27
|
+
securityLevel: 'loose',
|
28
|
+
logLevel: 'error',
|
29
|
+
er: {
|
30
|
+
useMaxWidth: true,
|
31
|
+
layoutDirection: 'LR',
|
32
|
+
entityPadding: 15,
|
33
|
+
fontSize: 14
|
34
|
+
},
|
35
|
+
flowchart: {
|
36
|
+
useMaxWidth: true,
|
37
|
+
htmlLabels: true,
|
38
|
+
curve: 'basis',
|
39
|
+
padding: 20,
|
40
|
+
nodeSpacing: 50,
|
41
|
+
rankSpacing: 50
|
42
|
+
},
|
43
|
+
suppressErrorRendering: false,
|
44
|
+
suppressWarnings: true
|
45
|
+
});
|
46
|
+
|
47
|
+
this.initialized = true;
|
48
|
+
console.log('Mermaid service initialized');
|
49
|
+
},
|
50
|
+
|
51
|
+
// Render diagram with svg-pan-zoom integration
|
52
|
+
async render(content, container, options = {}) {
|
53
|
+
if (!content || !container) {
|
54
|
+
console.error('Missing required parameters for rendering diagram');
|
55
|
+
return null;
|
56
|
+
}
|
57
|
+
|
58
|
+
await this.initialize();
|
59
|
+
|
60
|
+
try {
|
61
|
+
// Clean content before rendering
|
62
|
+
const cleanedContent = this.cleanDiagramContent(content);
|
63
|
+
|
64
|
+
// Use Mermaid's modern promise-based API
|
65
|
+
const { svg } = await window.mermaid.render('diagram-' + Date.now(), cleanedContent);
|
66
|
+
|
67
|
+
// Insert SVG into container
|
68
|
+
container.innerHTML = svg;
|
69
|
+
|
70
|
+
// Get the SVG element for pan-zoom initialization
|
71
|
+
const svgElement = container.querySelector('svg');
|
72
|
+
if (!svgElement) {
|
73
|
+
throw new Error('Failed to render SVG');
|
74
|
+
}
|
75
|
+
|
76
|
+
// Initialize pan-zoom
|
77
|
+
const panZoomInstance = this.enableInteractions(svgElement, options);
|
78
|
+
|
79
|
+
return {
|
80
|
+
svg: svgElement,
|
81
|
+
panZoom: panZoomInstance
|
82
|
+
};
|
83
|
+
} catch (error) {
|
84
|
+
console.error('Error rendering diagram:', error);
|
85
|
+
this.showError(container, error.message || 'Failed to render diagram');
|
86
|
+
return null;
|
87
|
+
}
|
88
|
+
},
|
89
|
+
|
90
|
+
// Enable SVG interactions using svg-pan-zoom library
|
91
|
+
enableInteractions(svgElement, options = {}) {
|
92
|
+
// Safety checks
|
93
|
+
if (!svgElement || !window.svgPanZoom) {
|
94
|
+
console.error('SVG element or svg-pan-zoom library not available');
|
95
|
+
return null;
|
96
|
+
}
|
97
|
+
|
98
|
+
// Get container dimensions for reference
|
99
|
+
const containerWidth = svgElement.parentElement?.clientWidth || 800;
|
100
|
+
const containerHeight = svgElement.parentElement?.clientHeight || 600;
|
101
|
+
|
102
|
+
// Force set dimensions for better rendering
|
103
|
+
svgElement.setAttribute('width', containerWidth.toString());
|
104
|
+
svgElement.setAttribute('height', containerHeight.toString());
|
105
|
+
svgElement.style.width = '100%';
|
106
|
+
svgElement.style.height = '100%';
|
107
|
+
svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
108
|
+
|
109
|
+
// Initialize pan-zoom
|
110
|
+
try {
|
111
|
+
const panZoomInstance = window.svgPanZoom(svgElement, {
|
112
|
+
zoomEnabled: true,
|
113
|
+
controlIconsEnabled: false,
|
114
|
+
fit: true,
|
115
|
+
center: true,
|
116
|
+
minZoom: 0.2,
|
117
|
+
maxZoom: 5,
|
118
|
+
zoomScaleSensitivity: 0.4
|
119
|
+
});
|
120
|
+
|
121
|
+
// Add keyboard shortcuts
|
122
|
+
this.addKeyboardShortcuts(panZoomInstance);
|
123
|
+
|
124
|
+
return panZoomInstance;
|
125
|
+
} catch (error) {
|
126
|
+
console.error('Error initializing pan-zoom:', error);
|
127
|
+
return null;
|
128
|
+
}
|
129
|
+
},
|
130
|
+
|
131
|
+
// Clean diagram content to fix common issues
|
132
|
+
cleanDiagramContent(content) {
|
133
|
+
if (!content) return '';
|
134
|
+
|
135
|
+
// Replace common problematic patterns
|
136
|
+
return content
|
137
|
+
.replace(/ /g, ' ')
|
138
|
+
.replace(/</g, '<')
|
139
|
+
.replace(/>/g, '>')
|
140
|
+
.replace(/&/g, '&')
|
141
|
+
.trim();
|
142
|
+
},
|
143
|
+
|
144
|
+
// Add keyboard shortcuts for pan/zoom
|
145
|
+
addKeyboardShortcuts(panZoom) {
|
146
|
+
if (!panZoom) return;
|
147
|
+
|
148
|
+
// Add keyboard event listener
|
149
|
+
document.addEventListener('keydown', (event) => {
|
150
|
+
// Only handle events when diagram is focused
|
151
|
+
if (!event.target.closest('.diagram-container')) return;
|
152
|
+
|
153
|
+
switch (event.key) {
|
154
|
+
case '+':
|
155
|
+
case '=':
|
156
|
+
panZoom.zoomIn();
|
157
|
+
break;
|
158
|
+
case '-':
|
159
|
+
panZoom.zoomOut();
|
160
|
+
break;
|
161
|
+
case '0':
|
162
|
+
panZoom.resetZoom();
|
163
|
+
break;
|
164
|
+
case 'ArrowUp':
|
165
|
+
panZoom.panBy({x: 0, y: 50});
|
166
|
+
break;
|
167
|
+
case 'ArrowDown':
|
168
|
+
panZoom.panBy({x: 0, y: -50});
|
169
|
+
break;
|
170
|
+
case 'ArrowLeft':
|
171
|
+
panZoom.panBy({x: 50, y: 0});
|
172
|
+
break;
|
173
|
+
case 'ArrowRight':
|
174
|
+
panZoom.panBy({x: -50, y: 0});
|
175
|
+
break;
|
176
|
+
}
|
177
|
+
});
|
178
|
+
},
|
179
|
+
|
180
|
+
// Show error in container
|
181
|
+
showError(container, message) {
|
182
|
+
if (!container) return;
|
183
|
+
|
184
|
+
container.innerHTML = `
|
185
|
+
<div class="diagram-error p-4 bg-red-50 text-red-700 rounded">
|
186
|
+
<h3 class="font-semibold mb-2">Error rendering diagram</h3>
|
187
|
+
<p>${message || 'An unknown error occurred'}</p>
|
188
|
+
</div>
|
189
|
+
`;
|
190
|
+
}
|
191
|
+
};
|
192
|
+
|
193
|
+
// Register with DBWatcher if available
|
194
|
+
if (window.DBWatcher) {
|
195
|
+
window.DBWatcher.MermaidService = MermaidService;
|
196
|
+
}
|
197
|
+
|
198
|
+
// Make available globally
|
199
|
+
window.MermaidService = MermaidService;
|
@@ -0,0 +1,99 @@
|
|
1
|
+
/**
|
2
|
+
* Browser-compatible date-fns version
|
3
|
+
* Sets up window.dateFns with all date-fns methods
|
4
|
+
*/
|
5
|
+
(function(global) {
|
6
|
+
// Create namespace
|
7
|
+
global.dateFns = {
|
8
|
+
// Core formatting functions
|
9
|
+
format: function(date, formatStr) {
|
10
|
+
if (!date) return '';
|
11
|
+
var d = new Date(date);
|
12
|
+
if (isNaN(d.getTime())) return '';
|
13
|
+
|
14
|
+
formatStr = formatStr || 'yyyy-MM-dd HH:mm:ss';
|
15
|
+
|
16
|
+
var year = d.getFullYear();
|
17
|
+
var month = d.getMonth() + 1;
|
18
|
+
var day = d.getDate();
|
19
|
+
var hours = d.getHours();
|
20
|
+
var minutes = d.getMinutes();
|
21
|
+
var seconds = d.getSeconds();
|
22
|
+
|
23
|
+
// Simple formatting with basic replacements
|
24
|
+
return formatStr
|
25
|
+
.replace(/yyyy/g, year)
|
26
|
+
.replace(/MM/g, month < 10 ? '0' + month : month)
|
27
|
+
.replace(/dd/g, day < 10 ? '0' + day : day)
|
28
|
+
.replace(/HH/g, hours < 10 ? '0' + hours : hours)
|
29
|
+
.replace(/mm/g, minutes < 10 ? '0' + minutes : minutes)
|
30
|
+
.replace(/ss/g, seconds < 10 ? '0' + seconds : seconds);
|
31
|
+
},
|
32
|
+
|
33
|
+
// Date manipulation
|
34
|
+
addDays: function(date, days) {
|
35
|
+
var result = new Date(date);
|
36
|
+
result.setDate(result.getDate() + days);
|
37
|
+
return result;
|
38
|
+
},
|
39
|
+
|
40
|
+
addHours: function(date, hours) {
|
41
|
+
var result = new Date(date);
|
42
|
+
result.setHours(result.getHours() + hours);
|
43
|
+
return result;
|
44
|
+
},
|
45
|
+
|
46
|
+
addMinutes: function(date, minutes) {
|
47
|
+
var result = new Date(date);
|
48
|
+
result.setMinutes(result.getMinutes() + minutes);
|
49
|
+
return result;
|
50
|
+
},
|
51
|
+
|
52
|
+
addSeconds: function(date, seconds) {
|
53
|
+
var result = new Date(date);
|
54
|
+
result.setSeconds(result.getSeconds() + seconds);
|
55
|
+
return result;
|
56
|
+
},
|
57
|
+
|
58
|
+
// Comparison
|
59
|
+
isAfter: function(date, dateToCompare) {
|
60
|
+
return new Date(date) > new Date(dateToCompare);
|
61
|
+
},
|
62
|
+
|
63
|
+
isBefore: function(date, dateToCompare) {
|
64
|
+
return new Date(date) < new Date(dateToCompare);
|
65
|
+
},
|
66
|
+
|
67
|
+
isEqual: function(date, dateToCompare) {
|
68
|
+
return new Date(date).getTime() === new Date(dateToCompare).getTime();
|
69
|
+
},
|
70
|
+
|
71
|
+
isSameDay: function(date, dateToCompare) {
|
72
|
+
var d1 = new Date(date);
|
73
|
+
var d2 = new Date(dateToCompare);
|
74
|
+
return d1.getFullYear() === d2.getFullYear() &&
|
75
|
+
d1.getMonth() === d2.getMonth() &&
|
76
|
+
d1.getDate() === d2.getDate();
|
77
|
+
},
|
78
|
+
|
79
|
+
// Utilities
|
80
|
+
parseISO: function(dateString) {
|
81
|
+
return new Date(dateString);
|
82
|
+
},
|
83
|
+
|
84
|
+
formatDistance: function(date, baseDate) {
|
85
|
+
var seconds = Math.abs(new Date(date) - new Date(baseDate)) / 1000;
|
86
|
+
var minutes = Math.floor(seconds / 60);
|
87
|
+
var hours = Math.floor(minutes / 60);
|
88
|
+
var days = Math.floor(hours / 24);
|
89
|
+
|
90
|
+
if (days > 0) return days + ' day' + (days > 1 ? 's' : '');
|
91
|
+
if (hours > 0) return hours + ' hour' + (hours > 1 ? 's' : '');
|
92
|
+
if (minutes > 0) return minutes + ' minute' + (minutes > 1 ? 's' : '');
|
93
|
+
return seconds + ' second' + (seconds !== 1 ? 's' : '');
|
94
|
+
}
|
95
|
+
};
|
96
|
+
})(window);
|
97
|
+
|
98
|
+
// Add console confirmation
|
99
|
+
console.log('Browser-compatible date-fns loaded successfully');
|