dbviewer 0.6.6 → 0.6.8

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -36
  3. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
  4. data/app/assets/javascripts/dbviewer/home.js +287 -0
  5. data/app/assets/javascripts/dbviewer/layout.js +194 -0
  6. data/app/assets/javascripts/dbviewer/query.js +277 -0
  7. data/app/assets/javascripts/dbviewer/table.js +1563 -0
  8. data/app/assets/stylesheets/dbviewer/application.css +1460 -21
  9. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
  10. data/app/assets/stylesheets/dbviewer/home.css +229 -0
  11. data/app/assets/stylesheets/dbviewer/logs.css +64 -0
  12. data/app/assets/stylesheets/dbviewer/query.css +171 -0
  13. data/app/assets/stylesheets/dbviewer/table.css +1144 -0
  14. data/app/views/dbviewer/connections/index.html.erb +0 -30
  15. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
  16. data/app/views/dbviewer/home/index.html.erb +9 -499
  17. data/app/views/dbviewer/logs/index.html.erb +22 -221
  18. data/app/views/dbviewer/tables/index.html.erb +0 -65
  19. data/app/views/dbviewer/tables/query.html.erb +129 -565
  20. data/app/views/dbviewer/tables/show.html.erb +4 -2429
  21. data/app/views/layouts/dbviewer/application.html.erb +13 -1544
  22. data/lib/dbviewer/version.rb +1 -1
  23. metadata +12 -7
  24. data/app/assets/javascripts/dbviewer/connections.js +0 -70
  25. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  26. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  27. data/app/views/dbviewer/connections/new.html.erb +0 -79
  28. data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e2d222c36ae2bf4e6d20f4f915d59c3b5acae64eead145e8a0dccd7acb3ddbc
4
- data.tar.gz: 786de8d3e46e54532234790345b52c17e3bdf887882b14534cd85b6b1d2b4fe0
3
+ metadata.gz: b63b072de9449e22dadd1dd7ec47dcdb4a1952e90c4461823783a844e147f026
4
+ data.tar.gz: ea463968ddf5b69a22a82b9d4213e1d8e60856878be5d8bbe72f0345b1417794
5
5
  SHA512:
6
- metadata.gz: e461217c6b720cc04ec36e884aa19c1468d034451afdc3d64709056df2004cd9d6830f8f28ae339f9705b62eb7f60cc3d5587378828c9965d2468d7b23659999
7
- data.tar.gz: ffb54b66a8bf3d4d426cdba9dabdf39bf619af8e4b70caeb83a541576ae3ec4bad53fbe1d9f28dea19930e7caf00cb1c71525389e04977a8a4771cfc52a402f1
6
+ metadata.gz: 47ea6ee4e345f6ac05c5c475918aa84fabbe28469409fc62755997991c3700a1797a394183a75db91721c49fcde0c493680621f6be93e6625ab7f7055b66379e
7
+ data.tar.gz: 7cffe71b1bcfd22583c0796c3816eb10a0d164baf1a304e62a4b541a3db75c13b671c61aa0b94450a1ee5cd33bcf365c43e27320b0c9ff970c13c715f57953df
data/README.md CHANGED
@@ -9,42 +9,18 @@ It's designed for development, debugging, and database analysis, offering a clea
9
9
 
10
10
  ## ✨ Features
11
11
 
12
- - **Dashboard**: View a comprehensive dashboard with database analytics, largest tables, most complex tables, and recent SQL queries
13
- - **Table Overview**: View a list of all tables with record count, column count, and quick access links
14
- - **Detailed Schema Information**:
15
- - View columns with their types, nullability, defaults, and primary key indicators
16
- - Examine table indexes and their uniqueness constraints
17
- - Explore foreign key relationships between tables
18
- - **Entity Relationship Diagram (ERD)**:
19
- - Interactive visualization of database schema and table relationships
20
- - Zoomable and pannable diagram to explore complex database structures
21
- - Full table details including all columns and their data types
22
- - Visual representation of foreign key relationships between tables
23
- - **Data Browsing**:
24
- - Browse table records with customizable pagination (10, 20, 50, or 100 records per page)
25
- - Sort data by any column in ascending or descending order
26
- - Navigate through large datasets with an intuitive pagination interface
27
- - Scrollable table with fixed headers for improved navigation
28
- - Single-line cell display with ellipsis for wide content (tooltips on hover)
29
- - Export table data to CSV format (configurable via `enable_data_export` option)
30
- - **SQL Queries**:
31
- - Run custom SELECT queries against your database in a secure, read-only environment
32
- - View table structure reference while writing queries
33
- - Protection against potentially harmful SQL operations
34
- - Query execution statistics and timing
35
- - **Multiple Database Connections**:
36
- - Connect to multiple databases within your application
37
- - Switch between connections on-the-fly to view different database schemas
38
- - Add new database connections from the UI without code changes
39
- - Test connections to verify they're working properly
40
- - **Enhanced UI Features**:
41
- - Responsive, Bootstrap-based interface that works on desktop and mobile
42
- - Fixed header navigation with quick access to all features
43
- - Modern sidebar layout with improved filtering and scrollable table list
44
- - Clean tabbed interface for exploring different aspects of table structure
45
- - Advanced table filtering with keyboard navigation support
46
- - Proper formatting for various data types (dates, JSON, arrays, etc.)
47
- - Enhanced data presentation with appropriate styling
12
+ - **Dashboard**
13
+ - **Table Overview**
14
+ - **Detailed Schema Information**
15
+ - **Entity Relationship Diagram (ERD)**
16
+ - **Data Browsing**
17
+ - **SQL Queries**
18
+ - **Multiple Database Connections**
19
+ - **Enhanced UI Features**
20
+
21
+ ## 🧪 Demo Application
22
+
23
+ You can explore a live demo of DBViewer at [https://dbviewer-demo.wailantirajoh.tech/](https://dbviewer-demo.wailantirajoh.tech/). This demo showcases all the features of DBViewer on a sample database, allowing you to try out the tool before installing it in your own application.
48
24
 
49
25
  ## 📸 Screenshots
50
26
 
@@ -0,0 +1,553 @@
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ // Check if mermaid is loaded first
3
+ if (typeof mermaid === "undefined") {
4
+ console.error("Mermaid library not loaded!");
5
+ showError(
6
+ "Mermaid library not loaded",
7
+ "The diagram library could not be loaded. Please check your internet connection and try again."
8
+ );
9
+ return;
10
+ }
11
+
12
+ // Initialize mermaid with theme detection like mini ERD
13
+ mermaid.initialize({
14
+ startOnLoad: true,
15
+ theme:
16
+ document.documentElement.getAttribute("data-bs-theme") === "dark"
17
+ ? "dark"
18
+ : "default",
19
+ securityLevel: "loose",
20
+ er: {
21
+ diagramPadding: 20,
22
+ layoutDirection: "TB",
23
+ minEntityWidth: 100,
24
+ minEntityHeight: 75,
25
+ entityPadding: 15,
26
+ stroke: "gray",
27
+ fill: "honeydew",
28
+ fontSize: 20,
29
+ },
30
+ });
31
+
32
+ // Function to show error messages
33
+ function showError(title, message, details = "") {
34
+ const errorContainer = document.getElementById("erd-error");
35
+ const errorMessage = document.getElementById("erd-error-message");
36
+ const errorDetails = document.getElementById("erd-error-details");
37
+ const loadingIndicator = document.getElementById("erd-loading");
38
+
39
+ if (loadingIndicator) {
40
+ loadingIndicator.style.display = "none";
41
+ }
42
+
43
+ if (errorContainer && errorMessage) {
44
+ // Set error message
45
+ errorMessage.textContent = message;
46
+
47
+ // Set error details if provided
48
+ if (details && errorDetails) {
49
+ errorDetails.textContent = details;
50
+ errorDetails.classList.remove("d-none");
51
+ } else if (errorDetails) {
52
+ errorDetails.classList.add("d-none");
53
+ }
54
+
55
+ // Show the error container
56
+ errorContainer.classList.remove("d-none");
57
+ }
58
+ }
59
+
60
+ // ER Diagram download functionality
61
+ let diagramReady = false;
62
+
63
+ // Function to show a temporary downloading indicator
64
+ function showDownloadingIndicator(format) {
65
+ // Create toast element
66
+ const toastEl = document.createElement("div");
67
+ toastEl.className = "position-fixed bottom-0 end-0 p-3";
68
+ toastEl.style.zIndex = "5000";
69
+ toastEl.innerHTML = `
70
+ <div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
71
+ <div class="toast-header">
72
+ <strong class="me-auto"><i class="bi bi-download"></i> Downloading ERD</strong>
73
+ <small>just now</small>
74
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
75
+ </div>
76
+ <div class="toast-body">
77
+ <div class="d-flex align-items-center">
78
+ <div class="spinner-border spinner-border-sm me-2" role="status">
79
+ <span class="visually-hidden">Loading...</span>
80
+ </div>
81
+ Preparing ${format} file for download...
82
+ </div>
83
+ </div>
84
+ </div>
85
+ `;
86
+
87
+ document.body.appendChild(toastEl);
88
+
89
+ // Automatically remove after a delay
90
+ setTimeout(() => {
91
+ toastEl.remove();
92
+ }, 3000);
93
+ }
94
+
95
+ // Generate the ERD diagram
96
+ const tables = JSON.parse(document.getElementById("tables").value);
97
+
98
+ // Initialize empty relationships - will be loaded asynchronously
99
+ let relationships = [];
100
+ let relationshipsLoaded = false;
101
+
102
+ // Function to fetch relationships asynchronously
103
+ function fetchRelationships() {
104
+ const apiPath = document.getElementById("relationships_api_path").value;
105
+ return fetch(apiPath, {
106
+ headers: {
107
+ Accept: "application/json",
108
+ "X-Requested-With": "XMLHttpRequest",
109
+ },
110
+ })
111
+ .then((response) => {
112
+ if (!response.ok) {
113
+ throw new Error(`HTTP error! status: ${response.status}`);
114
+ }
115
+ return response.json();
116
+ })
117
+ .then((data) => {
118
+ relationships = data.relationships || [];
119
+ relationshipsLoaded = true;
120
+ updateRelationshipsStatus(true);
121
+ return relationships;
122
+ })
123
+ .catch((error) => {
124
+ console.error("Error fetching relationships:", error);
125
+ relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
126
+ updateRelationshipsStatus(true);
127
+ return [];
128
+ });
129
+ }
130
+
131
+ // Function to update loading status
132
+ function updateLoadingStatus(message) {
133
+ const loadingElement = document.getElementById("erd-loading");
134
+ const loadingPhase = document.getElementById("loading-phase");
135
+ if (loadingPhase) {
136
+ loadingPhase.textContent = message;
137
+ }
138
+ }
139
+
140
+ // Function to update table loading progress
141
+ function updateTableProgress(loaded, total) {
142
+ const progressBar = document.getElementById("table-progress-bar");
143
+ const progressText = document.getElementById("table-progress-text");
144
+
145
+ if (progressBar && progressText) {
146
+ const percentage = total > 0 ? Math.round((loaded / total) * 100) : 0;
147
+ progressBar.style.width = percentage + "%";
148
+ progressBar.setAttribute("aria-valuenow", percentage);
149
+ progressText.textContent = `${loaded} / ${total}`;
150
+
151
+ // Update progress bar color based on completion
152
+ if (percentage === 100) {
153
+ progressBar.classList.remove("bg-primary");
154
+ progressBar.classList.add("bg-success");
155
+ }
156
+ }
157
+ }
158
+
159
+ // Function to update relationships status
160
+ function updateRelationshipsStatus(loaded) {
161
+ const relationshipsStatus = document.getElementById("relationships-status");
162
+ if (relationshipsStatus) {
163
+ if (loaded) {
164
+ relationshipsStatus.innerHTML = `
165
+ <i class="bi bi-check-circle text-success me-2"></i>
166
+ <small class="text-success">Relationships loaded</small>
167
+ `;
168
+ } else {
169
+ relationshipsStatus.innerHTML = `
170
+ <div class="spinner-border spinner-border-sm text-secondary me-2" role="status">
171
+ <span class="visually-hidden">Loading...</span>
172
+ </div>
173
+ <small class="text-muted">Loading relationships...</small>
174
+ `;
175
+ }
176
+ }
177
+ }
178
+
179
+ // Create the ER diagram definition in Mermaid syntax
180
+ let mermaidDefinition = "erDiagram\n";
181
+
182
+ // We'll store table column data here as we fetch it
183
+ const tableColumns = {};
184
+
185
+ // Track loading progress
186
+ let columnsLoadedCount = 0;
187
+ const totalTables = tables.length;
188
+
189
+ // Initialize progress bar
190
+ updateTableProgress(0, totalTables);
191
+ updateLoadingStatus("Loading table details...");
192
+
193
+ // Start fetching relationships immediately
194
+ updateRelationshipsStatus(false);
195
+ const relationshipsPromise = fetchRelationships();
196
+ const tablePath = document.getElementById("tables_path").value;
197
+
198
+ // First pass: add all tables with minimal info and start loading columns
199
+ tables.forEach(function (table) {
200
+ const tableName = table.name;
201
+ mermaidDefinition += ` ${tableName} {\n`;
202
+ mermaidDefinition += ` string id\n`;
203
+ mermaidDefinition += " }\n";
204
+
205
+ // Start loading column data asynchronously
206
+ fetch(`${tablePath}/${tableName}?format=json`, {
207
+ headers: {
208
+ Accept: "application/json",
209
+ "X-Requested-With": "XMLHttpRequest",
210
+ },
211
+ })
212
+ .then((response) => response.json())
213
+ .then((data) => {
214
+ if (data && data.columns) {
215
+ tableColumns[tableName] = data.columns;
216
+ columnsLoadedCount++;
217
+
218
+ // Update progress bar
219
+ updateTableProgress(columnsLoadedCount, totalTables);
220
+
221
+ checkIfReadyToUpdate();
222
+ }
223
+ })
224
+ .catch((error) => {
225
+ console.error(`Error fetching columns for table ${tableName}:`, error);
226
+ columnsLoadedCount++;
227
+ updateTableProgress(columnsLoadedCount, totalTables);
228
+ checkIfReadyToUpdate();
229
+ });
230
+ });
231
+
232
+ // Function to check if we're ready to update the diagram with full data
233
+ function checkIfReadyToUpdate() {
234
+ if (columnsLoadedCount === totalTables && relationshipsLoaded) {
235
+ updateDiagramWithFullData();
236
+ }
237
+ }
238
+
239
+ // Wait for relationships to load and check if ready
240
+ relationshipsPromise.finally(() => {
241
+ checkIfReadyToUpdate();
242
+ });
243
+
244
+ // Track if we're currently updating the diagram
245
+ let isUpdatingDiagram = false;
246
+
247
+ // Function to update the diagram once we have all data
248
+ function updateDiagramWithFullData() {
249
+ // Prevent multiple simultaneous updates
250
+ if (isUpdatingDiagram) return;
251
+
252
+ isUpdatingDiagram = true;
253
+
254
+ updateLoadingStatus("Generating final diagram...");
255
+
256
+ // Regenerate the diagram with complete data
257
+ let updatedDefinition = "erDiagram\n";
258
+
259
+ tables.forEach(function (table) {
260
+ const tableName = table.name;
261
+ updatedDefinition += ` ${tableName} {\n`;
262
+
263
+ const columns = tableColumns[tableName] || [];
264
+ columns.forEach((column) => {
265
+ updatedDefinition += ` ${column.type || "string"} ${column.name}\n`;
266
+ });
267
+
268
+ updatedDefinition += " }\n";
269
+ });
270
+
271
+ // Add relationships
272
+ if (relationships && relationships.length > 0) {
273
+ relationships.forEach(function (rel) {
274
+ updatedDefinition += ` ${rel.from_table} }|--|| ${rel.to_table} : "${rel.from_column} → ${rel.to_column}"\n`;
275
+ });
276
+ } else {
277
+ updatedDefinition +=
278
+ " %% No relationships found in the database schema\n";
279
+ }
280
+
281
+ // Create the diagram element
282
+ const erdDiv = document.createElement("div");
283
+ erdDiv.className = "mermaid";
284
+ erdDiv.innerHTML = updatedDefinition;
285
+
286
+ // Create a temporary container for rendering
287
+ const tempContainer = document.createElement("div");
288
+ tempContainer.style.visibility = "hidden";
289
+ tempContainer.style.position = "absolute";
290
+ tempContainer.style.width = "100%";
291
+ tempContainer.appendChild(erdDiv);
292
+ document.body.appendChild(tempContainer);
293
+
294
+ // Render the diagram in the temporary container
295
+ mermaid
296
+ .init(undefined, erdDiv)
297
+ .then(function () {
298
+ console.log("Diagram fully rendered with all data");
299
+
300
+ try {
301
+ // Remove from temp container without destroying
302
+ tempContainer.removeChild(erdDiv);
303
+
304
+ // Hide loading indicator
305
+ document.getElementById("erd-loading").style.display = "none";
306
+
307
+ // Clear main container and add the diagram
308
+ container.innerHTML = "";
309
+ container.appendChild(erdDiv);
310
+
311
+ // Remove temp container
312
+ document.body.removeChild(tempContainer);
313
+
314
+ // Wait a bit for the DOM to stabilize before initializing pan-zoom
315
+ setTimeout(() => {
316
+ setupZoomControls();
317
+ // Mark diagram as ready for download
318
+ diagramReady = true;
319
+ isUpdatingDiagram = false;
320
+ }, 100);
321
+ } catch (err) {
322
+ console.error("Error moving diagram to container:", err);
323
+ isUpdatingDiagram = false;
324
+ }
325
+ })
326
+ .catch(function (error) {
327
+ console.error("Error rendering diagram:", error);
328
+ document.body.removeChild(tempContainer);
329
+ isUpdatingDiagram = false;
330
+ showError(
331
+ "Error rendering diagram",
332
+ "There was an error rendering the entity relationship diagram.",
333
+ error.message
334
+ );
335
+ });
336
+ }
337
+
338
+ // Get the container reference for later use
339
+ const container = document.getElementById("erd-container");
340
+
341
+ // SVG Pan Zoom instance
342
+ let panZoomInstance = null;
343
+
344
+ // Setup zoom controls using svg-pan-zoom library
345
+ function setupZoomControls() {
346
+ const diagramContainer = document.getElementById("erd-container");
347
+ const svgElement = diagramContainer.querySelector("svg");
348
+
349
+ if (!svgElement) {
350
+ console.warn("SVG element not found for zoom controls");
351
+ return;
352
+ }
353
+
354
+ // Make sure SVG has proper attributes for zooming
355
+ svgElement.setAttribute("width", "100%");
356
+ svgElement.setAttribute("height", "100%");
357
+
358
+ // Initialize svg-pan-zoom
359
+ panZoomInstance = svgPanZoom(svgElement, {
360
+ zoomEnabled: true,
361
+ controlIconsEnabled: false,
362
+ fit: true,
363
+ center: true,
364
+ minZoom: 0.1,
365
+ maxZoom: 20,
366
+ zoomScaleSensitivity: 0.3,
367
+ onZoom: function (newZoom) {
368
+ // Update zoom percentage display
369
+ const zoomDisplay = document.getElementById("zoomPercentage");
370
+ if (zoomDisplay) {
371
+ zoomDisplay.textContent = `${Math.round(newZoom * 100)}%`;
372
+ }
373
+ },
374
+ });
375
+
376
+ // Set initial zoom to 100%
377
+ panZoomInstance.zoom(1);
378
+
379
+ // Add event listeners for zoom controls
380
+ document.getElementById("zoomIn").addEventListener("click", function () {
381
+ panZoomInstance.zoomIn();
382
+ });
383
+
384
+ document.getElementById("zoomOut").addEventListener("click", function () {
385
+ panZoomInstance.zoomOut();
386
+ });
387
+
388
+ document.getElementById("resetView").addEventListener("click", function () {
389
+ panZoomInstance.reset();
390
+ });
391
+
392
+ // Update initial percentage display
393
+ const zoomDisplay = document.getElementById("zoomPercentage");
394
+ if (zoomDisplay) {
395
+ zoomDisplay.textContent = "100%";
396
+ }
397
+
398
+ // Mark diagram as ready for download
399
+ diagramReady = true;
400
+ }
401
+
402
+ // Function to download the ERD as SVG
403
+ function downloadAsSVG() {
404
+ if (!diagramReady) {
405
+ alert("Please wait for the diagram to finish loading.");
406
+ return;
407
+ }
408
+
409
+ // Show loading indicator
410
+ showDownloadingIndicator("SVG");
411
+
412
+ try {
413
+ // Get the SVG element
414
+ const svgElement = document.querySelector("#erd-container svg");
415
+ if (!svgElement) {
416
+ alert("SVG diagram not found.");
417
+ return;
418
+ }
419
+
420
+ // Create a clone of the SVG to modify for download
421
+ const clonedSvg = svgElement.cloneNode(true);
422
+
423
+ // Set explicit dimensions to ensure proper rendering
424
+ clonedSvg.setAttribute("width", svgElement.getBoundingClientRect().width);
425
+ clonedSvg.setAttribute(
426
+ "height",
427
+ svgElement.getBoundingClientRect().height
428
+ );
429
+
430
+ // Convert SVG to a string
431
+ const serializer = new XMLSerializer();
432
+ let svgString = serializer.serializeToString(clonedSvg);
433
+
434
+ // Add XML declaration and doctype
435
+ svgString = '<?xml version="1.0" standalone="no"?>\n' + svgString;
436
+
437
+ // Create a Blob with the SVG data
438
+ const blob = new Blob([svgString], {
439
+ type: "image/svg+xml;charset=utf-8",
440
+ });
441
+
442
+ // Create a timestamp for filename
443
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
444
+
445
+ // Create download link and trigger download
446
+ const downloadLink = document.createElement("a");
447
+ downloadLink.href = URL.createObjectURL(blob);
448
+ downloadLink.download = `database_erd_${timestamp}.svg`;
449
+ document.body.appendChild(downloadLink);
450
+ downloadLink.click();
451
+ document.body.removeChild(downloadLink);
452
+ } catch (error) {
453
+ console.error("Error downloading SVG:", error);
454
+ alert("Error downloading SVG. Please check console for details.");
455
+ }
456
+ }
457
+
458
+ // Function to download the ERD as PNG
459
+ function downloadAsPNG() {
460
+ if (!diagramReady) {
461
+ alert("Please wait for the diagram to finish loading.");
462
+ return;
463
+ }
464
+
465
+ // Show loading indicator
466
+ showDownloadingIndicator("PNG");
467
+
468
+ try {
469
+ // Get the SVG element
470
+ const svgElement = document.querySelector("#erd-container svg");
471
+ if (!svgElement) {
472
+ alert("SVG diagram not found.");
473
+ return;
474
+ }
475
+
476
+ // Create a clone of the SVG to modify for download
477
+ const clonedSvg = svgElement.cloneNode(true);
478
+
479
+ // Set explicit dimensions to ensure proper rendering
480
+ const width = svgElement.getBoundingClientRect().width;
481
+ const height = svgElement.getBoundingClientRect().height;
482
+ clonedSvg.setAttribute("width", width);
483
+ clonedSvg.setAttribute("height", height);
484
+
485
+ // Convert SVG to a string
486
+ const serializer = new XMLSerializer();
487
+ const svgString = serializer.serializeToString(clonedSvg);
488
+
489
+ // Create a Blob with the SVG data
490
+ const svgBlob = new Blob([svgString], {
491
+ type: "image/svg+xml;charset=utf-8",
492
+ });
493
+ const svgUrl = URL.createObjectURL(svgBlob);
494
+
495
+ // Create an Image object to draw to canvas
496
+ const img = new Image();
497
+ img.onload = function () {
498
+ // Create canvas with appropriate dimensions
499
+ const canvas = document.createElement("canvas");
500
+ canvas.width = width * 2; // Scale up for better quality
501
+ canvas.height = height * 2;
502
+
503
+ // Get drawing context and scale it
504
+ const ctx = canvas.getContext("2d");
505
+ ctx.scale(2, 2); // Scale up for better quality
506
+
507
+ // Draw white background (SVG may have transparency)
508
+ ctx.fillStyle = "white";
509
+ ctx.fillRect(0, 0, width, height);
510
+
511
+ // Draw the image onto the canvas
512
+ ctx.drawImage(img, 0, 0, width, height);
513
+
514
+ // Create timestamp for filename
515
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
516
+
517
+ // Convert canvas to PNG and trigger download
518
+ canvas.toBlob(function (blob) {
519
+ const downloadLink = document.createElement("a");
520
+ downloadLink.href = URL.createObjectURL(blob);
521
+ downloadLink.download = `database_erd_${timestamp}.png`;
522
+ document.body.appendChild(downloadLink);
523
+ downloadLink.click();
524
+ document.body.removeChild(downloadLink);
525
+ }, "image/png");
526
+
527
+ // Clean up
528
+ URL.revokeObjectURL(svgUrl);
529
+ };
530
+
531
+ // Set the image source to the SVG URL
532
+ img.src = svgUrl;
533
+ } catch (error) {
534
+ console.error("Error downloading PNG:", error);
535
+ alert("Error downloading PNG. Please check console for details.");
536
+ }
537
+ }
538
+
539
+ // Set up event listeners for download buttons
540
+ document
541
+ .getElementById("downloadSvg")
542
+ .addEventListener("click", function (e) {
543
+ e.preventDefault();
544
+ downloadAsSVG();
545
+ });
546
+
547
+ document
548
+ .getElementById("downloadPng")
549
+ .addEventListener("click", function (e) {
550
+ e.preventDefault();
551
+ downloadAsPNG();
552
+ });
553
+ });