dbviewer 0.3.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,564 @@
|
|
1
|
+
<% content_for :title, "Entity Relationship Diagram" %>
|
2
|
+
|
3
|
+
<% content_for :sidebar do %>
|
4
|
+
<%= render 'dbviewer/shared/sidebar' %>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<div class="container-fluid h-100">
|
8
|
+
<div class="row h-100">
|
9
|
+
<div class="col-md-12 p-0">
|
10
|
+
<div class="card h-100">
|
11
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
12
|
+
<h5 class="mb-0">
|
13
|
+
<i class="bi bi-diagram-3"></i> Entity Relationship Diagram
|
14
|
+
</h5>
|
15
|
+
<div class="d-flex align-items-center">
|
16
|
+
<span id="zoomPercentage" class="me-2">100%</span>
|
17
|
+
<button id="zoomIn" class="btn btn-sm btn-outline-secondary me-1">
|
18
|
+
<i class="bi bi-zoom-in"></i>
|
19
|
+
</button>
|
20
|
+
<button id="zoomOut" class="btn btn-sm btn-outline-secondary me-1">
|
21
|
+
<i class="bi bi-zoom-out"></i>
|
22
|
+
</button>
|
23
|
+
<button id="resetView" class="btn btn-sm btn-outline-secondary me-1">
|
24
|
+
<i class="bi bi-arrow-counterclockwise"></i> Reset
|
25
|
+
</button>
|
26
|
+
<div class="dropdown">
|
27
|
+
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="downloadButton" data-bs-toggle="dropdown" aria-expanded="false">
|
28
|
+
<i class="bi bi-download"></i> Download
|
29
|
+
</button>
|
30
|
+
<ul class="dropdown-menu" aria-labelledby="downloadButton">
|
31
|
+
<li><a class="dropdown-item" href="#" id="downloadSvg">SVG Format</a></li>
|
32
|
+
<li><a class="dropdown-item" href="#" id="downloadPng">PNG Format</a></li>
|
33
|
+
</ul>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<div class="card-body p-0">
|
38
|
+
<div id="erd-container" class="w-100 h-100">
|
39
|
+
<div id="erd-loading" class="d-flex justify-content-center align-items-center h-100">
|
40
|
+
<div class="text-center">
|
41
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
42
|
+
<span class="visually-hidden">Loading...</span>
|
43
|
+
</div>
|
44
|
+
<p>Generating Entity Relationship Diagram...</p>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
<!-- The ERD will be rendered here -->
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
|
55
|
+
<%# Include mermaid.js for diagram rendering %>
|
56
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
57
|
+
<%# Include svg-pan-zoom for better diagram interaction %>
|
58
|
+
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
59
|
+
|
60
|
+
<script>
|
61
|
+
document.addEventListener('DOMContentLoaded', function() {
|
62
|
+
// Initialize mermaid
|
63
|
+
mermaid.initialize({
|
64
|
+
startOnLoad: true,
|
65
|
+
theme: 'neutral',
|
66
|
+
securityLevel: 'loose',
|
67
|
+
er: {
|
68
|
+
diagramPadding: 20,
|
69
|
+
layoutDirection: 'TB',
|
70
|
+
minEntityWidth: 100,
|
71
|
+
minEntityHeight: 75,
|
72
|
+
entityPadding: 15,
|
73
|
+
stroke: 'gray',
|
74
|
+
fill: 'honeydew',
|
75
|
+
fontSize: 20
|
76
|
+
}
|
77
|
+
});
|
78
|
+
|
79
|
+
// ER Diagram download functionality
|
80
|
+
let diagramReady = false;
|
81
|
+
|
82
|
+
// Function to show a temporary downloading indicator
|
83
|
+
function showDownloadingIndicator(format) {
|
84
|
+
// Create toast element
|
85
|
+
const toastEl = document.createElement('div');
|
86
|
+
toastEl.className = 'position-fixed bottom-0 end-0 p-3';
|
87
|
+
toastEl.style.zIndex = '5000';
|
88
|
+
toastEl.innerHTML = `
|
89
|
+
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
90
|
+
<div class="toast-header">
|
91
|
+
<strong class="me-auto"><i class="bi bi-download"></i> Downloading ERD</strong>
|
92
|
+
<small>just now</small>
|
93
|
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
94
|
+
</div>
|
95
|
+
<div class="toast-body">
|
96
|
+
<div class="d-flex align-items-center">
|
97
|
+
<div class="spinner-border spinner-border-sm me-2" role="status">
|
98
|
+
<span class="visually-hidden">Loading...</span>
|
99
|
+
</div>
|
100
|
+
Preparing ${format} file for download...
|
101
|
+
</div>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
`;
|
105
|
+
|
106
|
+
document.body.appendChild(toastEl);
|
107
|
+
|
108
|
+
// Automatically remove after a delay
|
109
|
+
setTimeout(() => {
|
110
|
+
toastEl.remove();
|
111
|
+
}, 3000);
|
112
|
+
}
|
113
|
+
|
114
|
+
// Generate the ERD diagram
|
115
|
+
const tables = <%= raw @tables.to_json %>;
|
116
|
+
const relationships = <%= raw @table_relationships.to_json %>;
|
117
|
+
|
118
|
+
console.log(tables, relationships)
|
119
|
+
|
120
|
+
// Create the ER diagram definition in Mermaid syntax
|
121
|
+
let mermaidDefinition = 'erDiagram\n';
|
122
|
+
|
123
|
+
// We'll store table column data here as we fetch it
|
124
|
+
const tableColumns = {};
|
125
|
+
|
126
|
+
// First pass: add all tables with minimal info
|
127
|
+
tables.forEach(function(table) {
|
128
|
+
const tableName = table.name;
|
129
|
+
mermaidDefinition += ` ${tableName} {\n`;
|
130
|
+
mermaidDefinition += ` string id\n`;
|
131
|
+
mermaidDefinition += ' }\n';
|
132
|
+
|
133
|
+
// Start loading column data asynchronously
|
134
|
+
fetch(`<%= dbviewer.tables_path %>/${tableName}?format=json`, {
|
135
|
+
headers: {
|
136
|
+
'Accept': 'application/json',
|
137
|
+
'X-Requested-With': 'XMLHttpRequest'
|
138
|
+
}
|
139
|
+
})
|
140
|
+
.then(response => response.json())
|
141
|
+
.then(data => {
|
142
|
+
if (data && data.columns) {
|
143
|
+
tableColumns[tableName] = data.columns;
|
144
|
+
updateDiagramWithColumns();
|
145
|
+
}
|
146
|
+
})
|
147
|
+
.catch(error => {
|
148
|
+
console.error(`Error fetching columns for table ${tableName}:`, error);
|
149
|
+
});
|
150
|
+
});
|
151
|
+
|
152
|
+
// Track if we're currently updating the diagram
|
153
|
+
let isUpdatingDiagram = false;
|
154
|
+
|
155
|
+
// Function to update the diagram once we have columns
|
156
|
+
function updateDiagramWithColumns() {
|
157
|
+
// Prevent multiple simultaneous updates
|
158
|
+
if (isUpdatingDiagram) return;
|
159
|
+
|
160
|
+
// Check if we have all the tables loaded
|
161
|
+
if (Object.keys(tableColumns).length === tables.length) {
|
162
|
+
isUpdatingDiagram = true;
|
163
|
+
console.log('Updating diagram with full column data');
|
164
|
+
|
165
|
+
// Regenerate the diagram with complete column data
|
166
|
+
let updatedDefinition = 'erDiagram\n';
|
167
|
+
|
168
|
+
tables.forEach(function(table) {
|
169
|
+
const tableName = table.name;
|
170
|
+
updatedDefinition += ` ${tableName} {\n`;
|
171
|
+
|
172
|
+
const columns = tableColumns[tableName] || [];
|
173
|
+
columns.forEach(column => {
|
174
|
+
updatedDefinition += ` ${column.type || 'string'} ${column.name}\n`;
|
175
|
+
});
|
176
|
+
|
177
|
+
updatedDefinition += ' }\n';
|
178
|
+
});
|
179
|
+
|
180
|
+
// Add relationships
|
181
|
+
if (relationships && relationships.length > 0) {
|
182
|
+
relationships.forEach(function(rel) {
|
183
|
+
updatedDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
|
184
|
+
});
|
185
|
+
} else {
|
186
|
+
updatedDefinition += ' %% No relationships found in the database schema\n';
|
187
|
+
}
|
188
|
+
|
189
|
+
// Create a new diagram element
|
190
|
+
const updatedErdDiv = document.createElement('div');
|
191
|
+
updatedErdDiv.className = 'mermaid';
|
192
|
+
updatedErdDiv.innerHTML = updatedDefinition;
|
193
|
+
|
194
|
+
// Get the container but don't clear it yet
|
195
|
+
const container = document.getElementById('erd-container');
|
196
|
+
|
197
|
+
// First, clean up any previous zoom instance
|
198
|
+
if (panZoomInstance) {
|
199
|
+
panZoomInstance.destroy();
|
200
|
+
panZoomInstance = null;
|
201
|
+
}
|
202
|
+
|
203
|
+
// Create a temporary container
|
204
|
+
const tempContainer = document.createElement('div');
|
205
|
+
tempContainer.style.visibility = 'hidden';
|
206
|
+
tempContainer.style.position = 'absolute';
|
207
|
+
tempContainer.style.width = '100%';
|
208
|
+
tempContainer.appendChild(updatedErdDiv);
|
209
|
+
document.body.appendChild(tempContainer);
|
210
|
+
|
211
|
+
// Render in the temporary container first
|
212
|
+
mermaid.init(undefined, updatedErdDiv).then(function() {
|
213
|
+
console.log('Diagram fully updated with column data');
|
214
|
+
|
215
|
+
// Clear original container and move the rendered content
|
216
|
+
try {
|
217
|
+
// Remove from temp container without destroying
|
218
|
+
tempContainer.removeChild(updatedErdDiv);
|
219
|
+
|
220
|
+
// Clear main container and add the diagram
|
221
|
+
container.innerHTML = '';
|
222
|
+
container.appendChild(updatedErdDiv);
|
223
|
+
|
224
|
+
// Remove temp container
|
225
|
+
document.body.removeChild(tempContainer);
|
226
|
+
|
227
|
+
// Wait a bit for the DOM to stabilize before initializing pan-zoom
|
228
|
+
setTimeout(() => {
|
229
|
+
setupZoomControls();
|
230
|
+
// Mark diagram as ready for download
|
231
|
+
diagramReady = true;
|
232
|
+
isUpdatingDiagram = false;
|
233
|
+
}, 100);
|
234
|
+
} catch(err) {
|
235
|
+
console.error('Error moving diagram to container:', err);
|
236
|
+
isUpdatingDiagram = false;
|
237
|
+
}
|
238
|
+
}).catch(function(error) {
|
239
|
+
console.error('Error rendering updated diagram:', error);
|
240
|
+
document.body.removeChild(tempContainer);
|
241
|
+
isUpdatingDiagram = false;
|
242
|
+
});
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
// Add relationships
|
247
|
+
if (relationships && relationships.length > 0) {
|
248
|
+
relationships.forEach(function(rel) {
|
249
|
+
// Format: "Customer ||--o{ Order : places"
|
250
|
+
mermaidDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
|
251
|
+
});
|
252
|
+
} else {
|
253
|
+
// Add a note if no relationships are found
|
254
|
+
mermaidDefinition += ' %% No relationships found in the database schema\n';
|
255
|
+
}
|
256
|
+
|
257
|
+
// Create a div for the initial diagram
|
258
|
+
const erdDiv = document.createElement('div');
|
259
|
+
erdDiv.className = 'mermaid';
|
260
|
+
erdDiv.innerHTML = mermaidDefinition;
|
261
|
+
|
262
|
+
// Get the container reference for later use
|
263
|
+
const container = document.getElementById('erd-container');
|
264
|
+
|
265
|
+
// Create a temporary container for initial rendering
|
266
|
+
const tempInitContainer = document.createElement('div');
|
267
|
+
tempInitContainer.style.visibility = 'hidden';
|
268
|
+
tempInitContainer.style.position = 'absolute';
|
269
|
+
tempInitContainer.style.width = '100%';
|
270
|
+
tempInitContainer.appendChild(erdDiv);
|
271
|
+
document.body.appendChild(tempInitContainer);
|
272
|
+
|
273
|
+
// Render the initial diagram in the temporary container
|
274
|
+
mermaid.init(undefined, erdDiv).then(function() {
|
275
|
+
try {
|
276
|
+
// Remove from temp container without destroying
|
277
|
+
tempInitContainer.removeChild(erdDiv);
|
278
|
+
|
279
|
+
// Hide the loading indicator
|
280
|
+
document.getElementById('erd-loading').style.display = 'none';
|
281
|
+
|
282
|
+
// Add the rendered diagram to the main container
|
283
|
+
container.appendChild(erdDiv);
|
284
|
+
|
285
|
+
// Remove temp container
|
286
|
+
document.body.removeChild(tempInitContainer);
|
287
|
+
|
288
|
+
// Setup zoom controls after diagram is rendered
|
289
|
+
setTimeout(() => {
|
290
|
+
setupZoomControls();
|
291
|
+
}, 100);
|
292
|
+
} catch(err) {
|
293
|
+
console.error('Error moving initial diagram to container:', err);
|
294
|
+
}
|
295
|
+
}).catch(function(error) {
|
296
|
+
console.error('Error rendering diagram:', error);
|
297
|
+
document.body.removeChild(tempInitContainer);
|
298
|
+
document.getElementById('erd-loading').innerHTML =
|
299
|
+
'<div class="alert alert-danger">Error generating diagram. Please try again or check console for details.</div>';
|
300
|
+
});
|
301
|
+
|
302
|
+
// SVG Pan Zoom instance
|
303
|
+
let panZoomInstance = null;
|
304
|
+
|
305
|
+
// Setup zoom controls using svg-pan-zoom library
|
306
|
+
function setupZoomControls() {
|
307
|
+
const diagramContainer = document.getElementById('erd-container');
|
308
|
+
const svgElement = diagramContainer.querySelector('svg');
|
309
|
+
|
310
|
+
if (!svgElement) {
|
311
|
+
console.warn('SVG element not found for zoom controls');
|
312
|
+
return;
|
313
|
+
}
|
314
|
+
|
315
|
+
// Make sure SVG has proper attributes for zooming
|
316
|
+
svgElement.setAttribute('width', '100%');
|
317
|
+
svgElement.setAttribute('height', '100%');
|
318
|
+
|
319
|
+
// Initialize svg-pan-zoom
|
320
|
+
panZoomInstance = svgPanZoom(svgElement, {
|
321
|
+
zoomEnabled: true,
|
322
|
+
controlIconsEnabled: false,
|
323
|
+
fit: true,
|
324
|
+
center: true,
|
325
|
+
minZoom: 0.1,
|
326
|
+
maxZoom: 20,
|
327
|
+
zoomScaleSensitivity: 0.3,
|
328
|
+
onZoom: function(newZoom) {
|
329
|
+
// Update zoom percentage display
|
330
|
+
const zoomDisplay = document.getElementById('zoomPercentage');
|
331
|
+
if (zoomDisplay) {
|
332
|
+
zoomDisplay.textContent = `${Math.round(newZoom * 100)}%`;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
});
|
336
|
+
|
337
|
+
// Set initial zoom to 100%
|
338
|
+
panZoomInstance.zoom(1);
|
339
|
+
|
340
|
+
// Add event listeners for zoom controls
|
341
|
+
document.getElementById('zoomIn').addEventListener('click', function() {
|
342
|
+
panZoomInstance.zoomIn();
|
343
|
+
});
|
344
|
+
|
345
|
+
document.getElementById('zoomOut').addEventListener('click', function() {
|
346
|
+
panZoomInstance.zoomOut();
|
347
|
+
});
|
348
|
+
|
349
|
+
document.getElementById('resetView').addEventListener('click', function() {
|
350
|
+
panZoomInstance.reset();
|
351
|
+
});
|
352
|
+
|
353
|
+
// Update initial percentage display
|
354
|
+
const zoomDisplay = document.getElementById('zoomPercentage');
|
355
|
+
if (zoomDisplay) {
|
356
|
+
zoomDisplay.textContent = '100%';
|
357
|
+
}
|
358
|
+
|
359
|
+
// Mark diagram as ready for download
|
360
|
+
diagramReady = true;
|
361
|
+
}
|
362
|
+
|
363
|
+
// Function to download the ERD as SVG
|
364
|
+
function downloadAsSVG() {
|
365
|
+
if (!diagramReady) {
|
366
|
+
alert('Please wait for the diagram to finish loading.');
|
367
|
+
return;
|
368
|
+
}
|
369
|
+
|
370
|
+
// Show loading indicator
|
371
|
+
showDownloadingIndicator('SVG');
|
372
|
+
|
373
|
+
try {
|
374
|
+
// Get the SVG element
|
375
|
+
const svgElement = document.querySelector('#erd-container svg');
|
376
|
+
if (!svgElement) {
|
377
|
+
alert('SVG diagram not found.');
|
378
|
+
return;
|
379
|
+
}
|
380
|
+
|
381
|
+
// Create a clone of the SVG to modify for download
|
382
|
+
const clonedSvg = svgElement.cloneNode(true);
|
383
|
+
|
384
|
+
// Set explicit dimensions to ensure proper rendering
|
385
|
+
clonedSvg.setAttribute('width', svgElement.getBoundingClientRect().width);
|
386
|
+
clonedSvg.setAttribute('height', svgElement.getBoundingClientRect().height);
|
387
|
+
|
388
|
+
// Convert SVG to a string
|
389
|
+
const serializer = new XMLSerializer();
|
390
|
+
let svgString = serializer.serializeToString(clonedSvg);
|
391
|
+
|
392
|
+
// Add XML declaration and doctype
|
393
|
+
svgString = '<?xml version="1.0" standalone="no"?>\n' + svgString;
|
394
|
+
|
395
|
+
// Create a Blob with the SVG data
|
396
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
397
|
+
|
398
|
+
// Create a timestamp for filename
|
399
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
400
|
+
|
401
|
+
// Create download link and trigger download
|
402
|
+
const downloadLink = document.createElement('a');
|
403
|
+
downloadLink.href = URL.createObjectURL(blob);
|
404
|
+
downloadLink.download = `database_erd_${timestamp}.svg`;
|
405
|
+
document.body.appendChild(downloadLink);
|
406
|
+
downloadLink.click();
|
407
|
+
document.body.removeChild(downloadLink);
|
408
|
+
} catch (error) {
|
409
|
+
console.error('Error downloading SVG:', error);
|
410
|
+
alert('Error downloading SVG. Please check console for details.');
|
411
|
+
}
|
412
|
+
}
|
413
|
+
|
414
|
+
// Function to download the ERD as PNG
|
415
|
+
function downloadAsPNG() {
|
416
|
+
if (!diagramReady) {
|
417
|
+
alert('Please wait for the diagram to finish loading.');
|
418
|
+
return;
|
419
|
+
}
|
420
|
+
|
421
|
+
// Show loading indicator
|
422
|
+
showDownloadingIndicator('PNG');
|
423
|
+
|
424
|
+
try {
|
425
|
+
// Get the SVG element
|
426
|
+
const svgElement = document.querySelector('#erd-container svg');
|
427
|
+
if (!svgElement) {
|
428
|
+
alert('SVG diagram not found.');
|
429
|
+
return;
|
430
|
+
}
|
431
|
+
|
432
|
+
// Create a clone of the SVG to modify for download
|
433
|
+
const clonedSvg = svgElement.cloneNode(true);
|
434
|
+
|
435
|
+
// Set explicit dimensions to ensure proper rendering
|
436
|
+
const width = svgElement.getBoundingClientRect().width;
|
437
|
+
const height = svgElement.getBoundingClientRect().height;
|
438
|
+
clonedSvg.setAttribute('width', width);
|
439
|
+
clonedSvg.setAttribute('height', height);
|
440
|
+
|
441
|
+
// Convert SVG to a string
|
442
|
+
const serializer = new XMLSerializer();
|
443
|
+
const svgString = serializer.serializeToString(clonedSvg);
|
444
|
+
|
445
|
+
// Create a Blob with the SVG data
|
446
|
+
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
447
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
448
|
+
|
449
|
+
// Create an Image object to draw to canvas
|
450
|
+
const img = new Image();
|
451
|
+
img.onload = function() {
|
452
|
+
// Create canvas with appropriate dimensions
|
453
|
+
const canvas = document.createElement('canvas');
|
454
|
+
canvas.width = width * 2; // Scale up for better quality
|
455
|
+
canvas.height = height * 2;
|
456
|
+
|
457
|
+
// Get drawing context and scale it
|
458
|
+
const ctx = canvas.getContext('2d');
|
459
|
+
ctx.scale(2, 2); // Scale up for better quality
|
460
|
+
|
461
|
+
// Draw white background (SVG may have transparency)
|
462
|
+
ctx.fillStyle = 'white';
|
463
|
+
ctx.fillRect(0, 0, width, height);
|
464
|
+
|
465
|
+
// Draw the image onto the canvas
|
466
|
+
ctx.drawImage(img, 0, 0, width, height);
|
467
|
+
|
468
|
+
// Create timestamp for filename
|
469
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
470
|
+
|
471
|
+
// Convert canvas to PNG and trigger download
|
472
|
+
canvas.toBlob(function(blob) {
|
473
|
+
const downloadLink = document.createElement('a');
|
474
|
+
downloadLink.href = URL.createObjectURL(blob);
|
475
|
+
downloadLink.download = `database_erd_${timestamp}.png`;
|
476
|
+
document.body.appendChild(downloadLink);
|
477
|
+
downloadLink.click();
|
478
|
+
document.body.removeChild(downloadLink);
|
479
|
+
}, 'image/png');
|
480
|
+
|
481
|
+
// Clean up
|
482
|
+
URL.revokeObjectURL(svgUrl);
|
483
|
+
};
|
484
|
+
|
485
|
+
// Set the image source to the SVG URL
|
486
|
+
img.src = svgUrl;
|
487
|
+
} catch (error) {
|
488
|
+
console.error('Error downloading PNG:', error);
|
489
|
+
alert('Error downloading PNG. Please check console for details.');
|
490
|
+
}
|
491
|
+
}
|
492
|
+
|
493
|
+
// Set up event listeners for download buttons
|
494
|
+
document.getElementById('downloadSvg').addEventListener('click', function(e) {
|
495
|
+
e.preventDefault();
|
496
|
+
downloadAsSVG();
|
497
|
+
});
|
498
|
+
|
499
|
+
document.getElementById('downloadPng').addEventListener('click', function(e) {
|
500
|
+
e.preventDefault();
|
501
|
+
downloadAsPNG();
|
502
|
+
});
|
503
|
+
});
|
504
|
+
</script>
|
505
|
+
|
506
|
+
<style>
|
507
|
+
#erd-container {
|
508
|
+
overflow: auto;
|
509
|
+
height: calc(100vh - 125px);
|
510
|
+
padding: 20px;
|
511
|
+
/* background-color: #fafafa; */
|
512
|
+
position: relative;
|
513
|
+
}
|
514
|
+
|
515
|
+
.mermaid {
|
516
|
+
display: flex;
|
517
|
+
justify-content: center;
|
518
|
+
min-width: 100%;
|
519
|
+
}
|
520
|
+
|
521
|
+
/* SVG Pan Zoom styles */
|
522
|
+
.svg-pan-zoom_viewport {
|
523
|
+
transition: 0.2s;
|
524
|
+
}
|
525
|
+
|
526
|
+
/* Make sure SVG maintains its size */
|
527
|
+
#erd-container svg {
|
528
|
+
width: 100%;
|
529
|
+
height: auto;
|
530
|
+
display: block;
|
531
|
+
min-width: 800px;
|
532
|
+
min-height: 600px;
|
533
|
+
}
|
534
|
+
|
535
|
+
/* Override mermaid defaults for a better look */
|
536
|
+
.entityBox {
|
537
|
+
fill: #f8f9fa;
|
538
|
+
stroke: #6c757d;
|
539
|
+
}
|
540
|
+
|
541
|
+
.entityLabel, .mermaid .label {
|
542
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
543
|
+
font-size: 20px !important;
|
544
|
+
}
|
545
|
+
|
546
|
+
/* Zoom percentage display styling */
|
547
|
+
#zoomPercentage {
|
548
|
+
font-size: 0.9rem;
|
549
|
+
/* color: #495057; */
|
550
|
+
font-weight: 500;
|
551
|
+
width: 45px;
|
552
|
+
display: inline-block;
|
553
|
+
text-align: center;
|
554
|
+
}
|
555
|
+
|
556
|
+
/* Mermaid override for text size */
|
557
|
+
.mermaid .entityLabel div {
|
558
|
+
font-size: 20px !important;
|
559
|
+
}
|
560
|
+
|
561
|
+
.mermaid .er.relationshipLabel {
|
562
|
+
font-size: 20px !important;
|
563
|
+
}
|
564
|
+
</style>
|