dbviewer 0.4.6 → 0.4.7
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 +1 -45
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +149 -8
- data/app/views/dbviewer/tables/show.html.erb +101 -42
- data/lib/dbviewer/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61c23f612495654e7fba0280fd361cf872d668ea21618c74c95d05f4ea7fdb6a
|
4
|
+
data.tar.gz: eeea9728edb2716f44e66eb1851767a7e77cf38c53f3ee4c676fd3d1d9e3608d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6674c2063fb577a3468ded6ffab856bcc8092f5a6fa46a28eb727f3e6a140556401c05777a7bc4ab7dd9e2a78c90b4576800db5a6b724f31d6a1e8f649cc1392
|
7
|
+
data.tar.gz: 1714aa447da6c73754b4702e2114e63dca6c1dce5be4be790bd0b7ae89aa7972f9bbbfc54de588da5237f45ca69066f2fde1b50ae15017e0a52842c954ee9372
|
data/README.md
CHANGED
@@ -92,23 +92,10 @@ Rails.application.routes.draw do
|
|
92
92
|
|
93
93
|
# Mount the DBViewer engine
|
94
94
|
mount Dbviewer::Engine, at: "/dbviewer"
|
95
|
-
# The engine can be mounted in any environment when using Basic Authentication
|
96
95
|
end
|
97
96
|
```
|
98
97
|
|
99
|
-
|
100
|
-
|
101
|
-
```ruby
|
102
|
-
# config/initializers/dbviewer.rb
|
103
|
-
Dbviewer.configure do |config|
|
104
|
-
config.admin_credentials = {
|
105
|
-
username: "your_username",
|
106
|
-
password: "your_secure_password"
|
107
|
-
}
|
108
|
-
end
|
109
|
-
```
|
110
|
-
|
111
|
-
Then, visit `/dbviewer` in your browser to access the database viewer. You'll be prompted for your username and password.
|
98
|
+
Then, visit `/dbviewer` in your browser to access the database viewer.
|
112
99
|
|
113
100
|
### Rails API-only Applications
|
114
101
|
|
@@ -136,37 +123,6 @@ This is necessary because API-only Rails applications don't include the Flash mi
|
|
136
123
|
- **ERD View** (`/dbviewer/entity_relationship_diagrams`): Interactive Entity Relationship Diagram of your database
|
137
124
|
- **SQL Query Logs** (`/dbviewer/logs`): View and analyze logged SQL queries with performance metrics
|
138
125
|
|
139
|
-
## 🤝🏻 Extending DBViewer
|
140
|
-
|
141
|
-
### Adding Custom Functionality
|
142
|
-
|
143
|
-
You can extend the database manager with custom methods:
|
144
|
-
|
145
|
-
```ruby
|
146
|
-
# config/initializers/dbviewer_extensions.rb
|
147
|
-
Rails.application.config.to_prepare do
|
148
|
-
Dbviewer::DatabaseManager.class_eval do
|
149
|
-
def table_statistics(table_name)
|
150
|
-
# Your custom code to generate table statistics
|
151
|
-
{
|
152
|
-
avg_row_size: calculate_avg_row_size(table_name),
|
153
|
-
last_updated: last_updated_timestamp(table_name)
|
154
|
-
}
|
155
|
-
end
|
156
|
-
|
157
|
-
private
|
158
|
-
|
159
|
-
def calculate_avg_row_size(table_name)
|
160
|
-
# Implementation...
|
161
|
-
end
|
162
|
-
|
163
|
-
def last_updated_timestamp(table_name)
|
164
|
-
# Implementation...
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
```
|
169
|
-
|
170
126
|
## ⚙️ Configuration Options
|
171
127
|
|
172
128
|
You can configure DBViewer by using our generator to create an initializer in your application:
|
@@ -31,16 +31,22 @@
|
|
31
31
|
</div>
|
32
32
|
</div>
|
33
33
|
<div class="card-body p-0">
|
34
|
-
<div id="erd-container" class="w-100 h-100">
|
35
|
-
<div id="erd-loading" class="d-flex justify-content-center align-items-center h-100">
|
34
|
+
<div id="erd-container" class="w-100 h-100" style="min-height: 450px;">
|
35
|
+
<div id="erd-loading" class="d-flex justify-content-center align-items-center h-100" style="min-height: 450px;">
|
36
36
|
<div class="text-center">
|
37
37
|
<div class="spinner-border text-primary mb-3" role="status">
|
38
38
|
<span class="visually-hidden">Loading...</span>
|
39
39
|
</div>
|
40
40
|
<p>Generating Entity Relationship Diagram...</p>
|
41
|
+
<small class="text-muted">This may take a moment for databases with many tables</small>
|
41
42
|
</div>
|
42
43
|
</div>
|
43
44
|
<!-- The ERD will be rendered here -->
|
45
|
+
<div id="erd-error" class="alert alert-danger m-3 d-none">
|
46
|
+
<h5>Error generating diagram</h5>
|
47
|
+
<p id="erd-error-message">There was an error rendering the entity relationship diagram.</p>
|
48
|
+
<pre id="erd-error-details" class="bg-light p-2 small mt-2 d-none"></pre>
|
49
|
+
</div>
|
44
50
|
</div>
|
45
51
|
</div>
|
46
52
|
</div>
|
@@ -55,10 +61,17 @@
|
|
55
61
|
|
56
62
|
<script>
|
57
63
|
document.addEventListener('DOMContentLoaded', function() {
|
58
|
-
//
|
64
|
+
// Check if mermaid is loaded first
|
65
|
+
if (typeof mermaid === 'undefined') {
|
66
|
+
console.error('Mermaid library not loaded!');
|
67
|
+
showError('Mermaid library not loaded', 'The diagram library could not be loaded. Please check your internet connection and try again.');
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
|
71
|
+
// Initialize mermaid with theme detection like mini ERD
|
59
72
|
mermaid.initialize({
|
60
73
|
startOnLoad: true,
|
61
|
-
theme: '
|
74
|
+
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
|
62
75
|
securityLevel: 'loose',
|
63
76
|
er: {
|
64
77
|
diagramPadding: 20,
|
@@ -72,6 +85,34 @@
|
|
72
85
|
}
|
73
86
|
});
|
74
87
|
|
88
|
+
// Function to show error messages
|
89
|
+
function showError(title, message, details = '') {
|
90
|
+
const errorContainer = document.getElementById('erd-error');
|
91
|
+
const errorMessage = document.getElementById('erd-error-message');
|
92
|
+
const errorDetails = document.getElementById('erd-error-details');
|
93
|
+
const loadingIndicator = document.getElementById('erd-loading');
|
94
|
+
|
95
|
+
if (loadingIndicator) {
|
96
|
+
loadingIndicator.style.display = 'none';
|
97
|
+
}
|
98
|
+
|
99
|
+
if (errorContainer && errorMessage) {
|
100
|
+
// Set error message
|
101
|
+
errorMessage.textContent = message;
|
102
|
+
|
103
|
+
// Set error details if provided
|
104
|
+
if (details && errorDetails) {
|
105
|
+
errorDetails.textContent = details;
|
106
|
+
errorDetails.classList.remove('d-none');
|
107
|
+
} else if (errorDetails) {
|
108
|
+
errorDetails.classList.add('d-none');
|
109
|
+
}
|
110
|
+
|
111
|
+
// Show the error container
|
112
|
+
errorContainer.classList.remove('d-none');
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
75
116
|
// ER Diagram download functionality
|
76
117
|
let diagramReady = false;
|
77
118
|
|
@@ -235,6 +276,7 @@
|
|
235
276
|
console.error('Error rendering updated diagram:', error);
|
236
277
|
document.body.removeChild(tempContainer);
|
237
278
|
isUpdatingDiagram = false;
|
279
|
+
showError('Error rendering diagram', 'There was an error updating the diagram with complete data.', error.message);
|
238
280
|
});
|
239
281
|
}
|
240
282
|
}
|
@@ -291,8 +333,7 @@
|
|
291
333
|
}).catch(function(error) {
|
292
334
|
console.error('Error rendering diagram:', error);
|
293
335
|
document.body.removeChild(tempInitContainer);
|
294
|
-
|
295
|
-
'<div class="alert alert-danger">Error generating diagram. Please try again or check console for details.</div>';
|
336
|
+
showError('Error generating diagram', 'There was an error generating the entity relationship diagram.', error.message);
|
296
337
|
});
|
297
338
|
|
298
339
|
// SVG Pan Zoom instance
|
@@ -504,7 +545,6 @@
|
|
504
545
|
overflow: auto;
|
505
546
|
height: calc(100vh - 125px);
|
506
547
|
padding: 20px;
|
507
|
-
/* background-color: #fafafa; */
|
508
548
|
position: relative;
|
509
549
|
}
|
510
550
|
|
@@ -514,6 +554,37 @@
|
|
514
554
|
min-width: 100%;
|
515
555
|
}
|
516
556
|
|
557
|
+
/* Loading state styling */
|
558
|
+
#erd-loading {
|
559
|
+
background-color: var(--bs-body-bg);
|
560
|
+
}
|
561
|
+
|
562
|
+
#erd-loading .text-center p {
|
563
|
+
margin-bottom: 0.5rem;
|
564
|
+
font-weight: 500;
|
565
|
+
}
|
566
|
+
|
567
|
+
#erd-loading .text-center small {
|
568
|
+
font-size: 0.875rem;
|
569
|
+
}
|
570
|
+
|
571
|
+
/* Error state styling */
|
572
|
+
#erd-error {
|
573
|
+
max-width: 600px;
|
574
|
+
margin: 2rem auto;
|
575
|
+
}
|
576
|
+
|
577
|
+
#erd-error h5 {
|
578
|
+
color: var(--bs-danger);
|
579
|
+
margin-bottom: 0.75rem;
|
580
|
+
}
|
581
|
+
|
582
|
+
#erd-error-details {
|
583
|
+
font-size: 0.8rem;
|
584
|
+
max-height: 150px;
|
585
|
+
overflow-y: auto;
|
586
|
+
}
|
587
|
+
|
517
588
|
/* SVG Pan Zoom styles */
|
518
589
|
.svg-pan-zoom_viewport {
|
519
590
|
transition: 0.2s;
|
@@ -539,10 +610,67 @@
|
|
539
610
|
font-size: 20px !important;
|
540
611
|
}
|
541
612
|
|
613
|
+
/* Dark mode overrides - comprehensive styling like mini ERD */
|
614
|
+
[data-bs-theme="dark"] .entityBox {
|
615
|
+
fill: #2D3748;
|
616
|
+
stroke: #6ea8fe;
|
617
|
+
}
|
618
|
+
|
619
|
+
[data-bs-theme="dark"] .entityLabel,
|
620
|
+
[data-bs-theme="dark"] .mermaid .label {
|
621
|
+
color: #f8f9fa;
|
622
|
+
}
|
623
|
+
|
624
|
+
[data-bs-theme="dark"] #erd-error-details {
|
625
|
+
background-color: var(--bs-dark) !important;
|
626
|
+
color: var(--bs-light);
|
627
|
+
border-color: var(--bs-border-color);
|
628
|
+
}
|
629
|
+
|
630
|
+
/* Dark mode: Update mermaid diagram elements */
|
631
|
+
[data-bs-theme="dark"] .mermaid .er .entityBox {
|
632
|
+
fill: #2D3748 !important;
|
633
|
+
stroke: #6ea8fe !important;
|
634
|
+
stroke-width: 1.5px !important;
|
635
|
+
}
|
636
|
+
|
637
|
+
[data-bs-theme="dark"] .mermaid .er .entityLabel {
|
638
|
+
fill: #f8f9fa !important;
|
639
|
+
color: #f8f9fa !important;
|
640
|
+
}
|
641
|
+
|
642
|
+
[data-bs-theme="dark"] .mermaid .er .relationshipLine {
|
643
|
+
stroke: #6ea8fe !important;
|
644
|
+
stroke-width: 2px !important;
|
645
|
+
}
|
646
|
+
|
647
|
+
[data-bs-theme="dark"] .mermaid .er .relationshipLabel {
|
648
|
+
fill: #f8f9fa !important;
|
649
|
+
color: #f8f9fa !important;
|
650
|
+
}
|
651
|
+
|
652
|
+
[data-bs-theme="dark"] .mermaid .er .attributeBoxEven,
|
653
|
+
[data-bs-theme="dark"] .mermaid .er .attributeBoxOdd {
|
654
|
+
fill: #374151 !important;
|
655
|
+
}
|
656
|
+
|
657
|
+
[data-bs-theme="dark"] .mermaid text {
|
658
|
+
fill: #f8f9fa !important;
|
659
|
+
}
|
660
|
+
|
661
|
+
/* Loading indicator dark mode */
|
662
|
+
[data-bs-theme="dark"] #erd-loading {
|
663
|
+
background-color: var(--bs-dark);
|
664
|
+
color: var(--bs-light);
|
665
|
+
}
|
666
|
+
|
667
|
+
[data-bs-theme="dark"] #erd-loading .spinner-border {
|
668
|
+
color: #6ea8fe;
|
669
|
+
}
|
670
|
+
|
542
671
|
/* Zoom percentage display styling */
|
543
672
|
#zoomPercentage {
|
544
673
|
font-size: 0.9rem;
|
545
|
-
/* color: #495057; */
|
546
674
|
font-weight: 500;
|
547
675
|
width: 45px;
|
548
676
|
display: inline-block;
|
@@ -557,4 +685,17 @@
|
|
557
685
|
.mermaid .er.relationshipLabel {
|
558
686
|
font-size: 20px !important;
|
559
687
|
}
|
688
|
+
|
689
|
+
/* Enhanced table highlighting for current table */
|
690
|
+
.current-table-highlight rect {
|
691
|
+
fill: var(--bs-primary-bg-subtle) !important;
|
692
|
+
stroke: var(--bs-primary) !important;
|
693
|
+
stroke-width: 2px !important;
|
694
|
+
}
|
695
|
+
|
696
|
+
[data-bs-theme="dark"] .current-table-highlight rect {
|
697
|
+
fill: #2c3034 !important;
|
698
|
+
stroke: #6ea8fe !important;
|
699
|
+
stroke-width: 2px !important;
|
700
|
+
}
|
560
701
|
</style>
|
@@ -836,7 +836,7 @@
|
|
836
836
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
837
837
|
</div>
|
838
838
|
<div class="modal-body p-0"> <!-- Removed padding for full width -->
|
839
|
-
<div id="mini-erd-container" class="w-100" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
|
839
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
|
840
840
|
<div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
|
841
841
|
<div class="text-center">
|
842
842
|
<div class="spinner-border text-primary mb-3" role="status">
|
@@ -977,53 +977,100 @@
|
|
977
977
|
try {
|
978
978
|
const svgElement = container.querySelector('svg');
|
979
979
|
if (svgElement && typeof svgPanZoom !== 'undefined') {
|
980
|
-
// Make SVG take the full container width
|
980
|
+
// Make SVG take the full container width and ensure it has valid dimensions
|
981
981
|
svgElement.setAttribute('width', '100%');
|
982
982
|
svgElement.setAttribute('height', '100%');
|
983
983
|
|
984
|
-
//
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
984
|
+
// Wait for SVG to be fully rendered with proper dimensions
|
985
|
+
setTimeout(() => {
|
986
|
+
try {
|
987
|
+
// Get dimensions to ensure they're valid before initializing pan-zoom
|
988
|
+
const clientRect = svgElement.getBoundingClientRect();
|
989
|
+
|
990
|
+
// Only initialize if we have valid dimensions
|
991
|
+
if (clientRect.width > 0 && clientRect.height > 0) {
|
992
|
+
// Initialize SVG Pan-Zoom with more robust error handling
|
993
|
+
const panZoomInstance = svgPanZoom(svgElement, {
|
994
|
+
zoomEnabled: true,
|
995
|
+
controlIconsEnabled: true,
|
996
|
+
fit: false, // Don't automatically fit on init - can cause the matrix error
|
997
|
+
center: false, // Don't automatically center - can cause the matrix error
|
998
|
+
minZoom: 0.5,
|
999
|
+
maxZoom: 2.5,
|
1000
|
+
beforeZoom: function() {
|
1001
|
+
// Check if the SVG is valid for zooming
|
1002
|
+
return svgElement.getBoundingClientRect().width > 0 &&
|
1003
|
+
svgElement.getBoundingClientRect().height > 0;
|
1004
|
+
}
|
1005
|
+
});
|
1006
|
+
|
1007
|
+
// Store the panZoom instance for resize handling
|
1008
|
+
container.panZoomInstance = panZoomInstance;
|
1009
|
+
|
1010
|
+
// Manually fit and center after a slight delay
|
1011
|
+
setTimeout(() => {
|
1012
|
+
try {
|
1013
|
+
panZoomInstance.resize();
|
1014
|
+
panZoomInstance.fit();
|
1015
|
+
panZoomInstance.center();
|
1016
|
+
} catch(err) {
|
1017
|
+
console.warn("Error during fit/center operation:", err);
|
1018
|
+
}
|
1019
|
+
}, 300);
|
1020
|
+
|
1021
|
+
// Setup resize observer to maintain full size
|
1022
|
+
const resizeObserver = new ResizeObserver(() => {
|
1023
|
+
if (container.panZoomInstance) {
|
1024
|
+
try {
|
1025
|
+
// Reset zoom and center when container is resized
|
1026
|
+
container.panZoomInstance.resize();
|
1027
|
+
// Only fit and center if the element is visible with valid dimensions
|
1028
|
+
if (svgElement.getBoundingClientRect().width > 0 &&
|
1029
|
+
svgElement.getBoundingClientRect().height > 0) {
|
1030
|
+
container.panZoomInstance.fit();
|
1031
|
+
container.panZoomInstance.center();
|
1032
|
+
}
|
1033
|
+
} catch(err) {
|
1034
|
+
console.warn("Error during resize observer callback:", err);
|
1035
|
+
}
|
1036
|
+
}
|
1037
|
+
});
|
1038
|
+
|
1039
|
+
// Observe the container for size changes
|
1040
|
+
resizeObserver.observe(container);
|
1041
|
+
|
1042
|
+
// Also handle manual resize on modal resize
|
1043
|
+
miniErdModal.addEventListener('resize.bs.modal', function() {
|
1044
|
+
if (container.panZoomInstance) {
|
1045
|
+
setTimeout(() => {
|
1046
|
+
try {
|
1047
|
+
container.panZoomInstance.resize();
|
1048
|
+
// Only fit and center if the element is visible with valid dimensions
|
1049
|
+
if (svgElement.getBoundingClientRect().width > 0 &&
|
1050
|
+
svgElement.getBoundingClientRect().height > 0) {
|
1051
|
+
container.panZoomInstance.fit();
|
1052
|
+
container.panZoomInstance.center();
|
1053
|
+
}
|
1054
|
+
} catch(err) {
|
1055
|
+
console.warn("Error during modal resize handler:", err);
|
1056
|
+
}
|
1057
|
+
}, 300);
|
1058
|
+
}
|
1059
|
+
});
|
1060
|
+
} else {
|
1061
|
+
console.warn("Cannot initialize SVG-Pan-Zoom: SVG has invalid dimensions", clientRect);
|
1062
|
+
}
|
1063
|
+
} catch(err) {
|
1064
|
+
console.warn("Error initializing SVG-Pan-Zoom:", err);
|
1018
1065
|
}
|
1019
|
-
});
|
1066
|
+
}, 500); // Increased delay to ensure SVG is fully rendered with proper dimensions
|
1020
1067
|
}
|
1021
1068
|
} catch (e) {
|
1022
1069
|
console.warn('Failed to initialize svg-pan-zoom:', e);
|
1023
1070
|
// Not critical, continue without pan-zoom
|
1024
1071
|
}
|
1025
1072
|
|
1026
|
-
// Add highlighting for the current table
|
1073
|
+
// Add highlighting for the current table after a delay to ensure SVG is fully processed
|
1027
1074
|
setTimeout(function() {
|
1028
1075
|
try {
|
1029
1076
|
const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
|
@@ -1100,10 +1147,22 @@
|
|
1100
1147
|
const container = document.getElementById('mini-erd-container');
|
1101
1148
|
if (container && container.panZoomInstance) {
|
1102
1149
|
setTimeout(() => {
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1150
|
+
try {
|
1151
|
+
// Check if the SVG still has valid dimensions before operating on it
|
1152
|
+
const svgElement = container.querySelector('svg');
|
1153
|
+
if (svgElement &&
|
1154
|
+
svgElement.getBoundingClientRect().width > 0 &&
|
1155
|
+
svgElement.getBoundingClientRect().height > 0) {
|
1156
|
+
container.panZoomInstance.resize();
|
1157
|
+
container.panZoomInstance.fit();
|
1158
|
+
container.panZoomInstance.center();
|
1159
|
+
} else {
|
1160
|
+
console.warn("Cannot perform pan-zoom operations: SVG has invalid dimensions");
|
1161
|
+
}
|
1162
|
+
} catch(err) {
|
1163
|
+
console.warn("Error during modal shown handler:", err);
|
1164
|
+
}
|
1165
|
+
}, 500); // Increased delay to ensure modal is fully transitioned and SVG is rendered
|
1107
1166
|
}
|
1108
1167
|
});
|
1109
1168
|
|
data/lib/dbviewer/version.rb
CHANGED