dbviewer 0.6.7 → 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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
  3. data/app/assets/javascripts/dbviewer/home.js +287 -0
  4. data/app/assets/javascripts/dbviewer/layout.js +194 -0
  5. data/app/assets/javascripts/dbviewer/query.js +277 -0
  6. data/app/assets/javascripts/dbviewer/table.js +1563 -0
  7. data/app/assets/stylesheets/dbviewer/application.css +1460 -21
  8. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
  9. data/app/assets/stylesheets/dbviewer/home.css +229 -0
  10. data/app/assets/stylesheets/dbviewer/logs.css +64 -0
  11. data/app/assets/stylesheets/dbviewer/query.css +171 -0
  12. data/app/assets/stylesheets/dbviewer/table.css +1144 -0
  13. data/app/views/dbviewer/connections/index.html.erb +0 -30
  14. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
  15. data/app/views/dbviewer/home/index.html.erb +9 -499
  16. data/app/views/dbviewer/logs/index.html.erb +5 -220
  17. data/app/views/dbviewer/tables/index.html.erb +0 -65
  18. data/app/views/dbviewer/tables/query.html.erb +129 -565
  19. data/app/views/dbviewer/tables/show.html.erb +4 -2429
  20. data/app/views/layouts/dbviewer/application.html.erb +13 -1544
  21. data/lib/dbviewer/version.rb +1 -1
  22. metadata +12 -7
  23. data/app/assets/javascripts/dbviewer/connections.js +0 -70
  24. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  25. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  26. data/app/views/dbviewer/connections/new.html.erb +0 -79
  27. 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: b12cc2cd164b8f28545ecdcc490ce5a9351a469c4d702ed87a64d5f617713f82
4
- data.tar.gz: 919d3a36d71de8320f2d7001c0a2b31e8b9c9b9ce0ee29f1ed31e40aa3ec4189
3
+ metadata.gz: b63b072de9449e22dadd1dd7ec47dcdb4a1952e90c4461823783a844e147f026
4
+ data.tar.gz: ea463968ddf5b69a22a82b9d4213e1d8e60856878be5d8bbe72f0345b1417794
5
5
  SHA512:
6
- metadata.gz: b101e0c8b7e4f64ec7b8b0d8b446a555e860195a9ffa2b99bc34532c35e1c7b8eea87c7072d316265115194796f02ebfea487f9a3d4859036eebd7b2f1d89221
7
- data.tar.gz: '096938554cc411491970f725d117df540112779247a210c71ff8b14bd7f418580721c6c91830c2b6083394528f3ebaab08dce8ff111bd421fd0002a5e04b416d'
6
+ metadata.gz: 47ea6ee4e345f6ac05c5c475918aa84fabbe28469409fc62755997991c3700a1797a394183a75db91721c49fcde0c493680621f6be93e6625ab7f7055b66379e
7
+ data.tar.gz: 7cffe71b1bcfd22583c0796c3816eb10a0d164baf1a304e62a4b541a3db75c13b671c61aa0b94450a1ee5cd33bcf365c43e27320b0c9ff970c13c715f57953df
@@ -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
+ });