dbviewer 0.4.6 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +1 -45
- data/app/controllers/dbviewer/tables_controller.rb +20 -4
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +149 -8
- data/app/views/dbviewer/home/index.html.erb +177 -23
- data/app/views/dbviewer/tables/show.html.erb +101 -42
- data/lib/dbviewer/configuration.rb +4 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/generators/dbviewer/templates/initializer.rb +3 -0
- 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: 03dfb13567e8cf7a2eaf2977e421fec7a2117ff12a6462515399a4b7cfb056d1
|
4
|
+
data.tar.gz: 77ae23ada80eabbc1a5744376a96c0d5df4f20f0cb3723be4c0f9c9967994716
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35d69706133dc764c081a6bc172dc0d99f39c2eaa7d62db9c17664c5a1fe05b78255932bd14a3f9a9542e99cb74020cb2926d69c9d21d9a339776fc4023098e9
|
7
|
+
data.tar.gz: b3a4e2029d81da79e922fb5ee0e889fc9e1f66e66129136333f6820d66dbc1dd7648da63a4ab99d3ed822e8cc64c77e9815a1c1bd34b4d0a11b208118d2eb72c
|
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:
|
@@ -103,11 +103,9 @@ module Dbviewer
|
|
103
103
|
@current_page = [ 1, params[:page].to_i ].max
|
104
104
|
@per_page = params[:per_page] ? params[:per_page].to_i : self.class.default_per_page
|
105
105
|
@per_page = self.class.default_per_page unless self.class.per_page_options.include?(@per_page)
|
106
|
-
@order_by = params[:order_by].presence ||
|
107
|
-
database_manager.primary_key(@table_name).presence ||
|
108
|
-
(@columns.first ? @columns.first[:name] : nil)
|
106
|
+
@order_by = params[:order_by].presence || determine_default_order_column
|
109
107
|
@order_direction = params[:order_direction].upcase if params[:order_direction].present?
|
110
|
-
@order_direction = "
|
108
|
+
@order_direction = "DESC" unless self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
|
111
109
|
@column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
|
112
110
|
end
|
113
111
|
|
@@ -158,5 +156,23 @@ module Dbviewer
|
|
158
156
|
end
|
159
157
|
end
|
160
158
|
end
|
159
|
+
|
160
|
+
# Determine the default order column using configurable ordering logic
|
161
|
+
def determine_default_order_column
|
162
|
+
# Get the table columns to check what's available
|
163
|
+
columns = @columns || fetch_table_columns(@table_name)
|
164
|
+
column_names = columns.map { |col| col[:name] }
|
165
|
+
|
166
|
+
# Try the configured default order column first
|
167
|
+
default_column = Dbviewer.configuration.default_order_column
|
168
|
+
return default_column if default_column && column_names.include?(default_column)
|
169
|
+
|
170
|
+
# Fall back to primary key
|
171
|
+
primary_key = database_manager.primary_key(@table_name)
|
172
|
+
return primary_key if primary_key.present?
|
173
|
+
|
174
|
+
# Final fallback to first column
|
175
|
+
columns.first ? columns.first[:name] : nil
|
176
|
+
end
|
161
177
|
end
|
162
178
|
end
|
@@ -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>
|
@@ -400,73 +400,173 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
400
400
|
</script>
|
401
401
|
|
402
402
|
<style>
|
403
|
+
/* ================================================
|
404
|
+
CSS Custom Properties (CSS Variables)
|
405
|
+
================================================ */
|
406
|
+
:root {
|
407
|
+
/* Colors */
|
408
|
+
--dbviewer-code-bg: rgba(0, 0, 0, 0.05);
|
409
|
+
--dbviewer-code-border: rgba(0, 0, 0, 0.1);
|
410
|
+
--dbviewer-muted-color: #6c757d;
|
411
|
+
--dbviewer-success-color: #28a745;
|
412
|
+
--dbviewer-danger-color: #dc3545;
|
413
|
+
--dbviewer-warning-color: #ffc107;
|
414
|
+
|
415
|
+
/* Skeleton loader colors */
|
416
|
+
--skeleton-base-color: #f0f0f0;
|
417
|
+
--skeleton-highlight-color: #e0e0e0;
|
418
|
+
|
419
|
+
/* Typography */
|
420
|
+
--dbviewer-monospace-font: 'Courier New', Courier, monospace;
|
421
|
+
--dbviewer-code-font-size: 0.85rem;
|
422
|
+
|
423
|
+
/* Spacing and sizing */
|
424
|
+
--dbviewer-border-radius: 4px;
|
425
|
+
--dbviewer-border-radius-sm: 3px;
|
426
|
+
--dbviewer-padding-sm: 2px 4px;
|
427
|
+
}
|
428
|
+
|
429
|
+
/* ================================================
|
430
|
+
Dark Mode Support
|
431
|
+
================================================ */
|
432
|
+
@media (prefers-color-scheme: dark) {
|
433
|
+
:root {
|
434
|
+
--dbviewer-code-bg: rgba(255, 255, 255, 0.1);
|
435
|
+
--dbviewer-code-border: rgba(255, 255, 255, 0.15);
|
436
|
+
--dbviewer-muted-color: #adb5bd;
|
437
|
+
--skeleton-base-color: #2a2a2a;
|
438
|
+
--skeleton-highlight-color: #404040;
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
/* Bootstrap dark mode support */
|
443
|
+
[data-bs-theme="dark"] {
|
444
|
+
--dbviewer-code-bg: rgba(255, 255, 255, 0.1);
|
445
|
+
--dbviewer-code-border: rgba(255, 255, 255, 0.15);
|
446
|
+
--dbviewer-muted-color: #adb5bd;
|
447
|
+
--skeleton-base-color: #2a2a2a;
|
448
|
+
--skeleton-highlight-color: #404040;
|
449
|
+
}
|
450
|
+
|
451
|
+
/* ================================================
|
452
|
+
SQL Query Styling
|
453
|
+
================================================ */
|
403
454
|
.sql-query-code {
|
404
|
-
font-family:
|
405
|
-
font-size:
|
406
|
-
background-color:
|
407
|
-
padding:
|
408
|
-
border-radius:
|
455
|
+
font-family: var(--dbviewer-monospace-font);
|
456
|
+
font-size: var(--dbviewer-code-font-size);
|
457
|
+
background-color: var(--dbviewer-code-bg);
|
458
|
+
padding: var(--dbviewer-padding-sm);
|
459
|
+
border-radius: var(--dbviewer-border-radius-sm);
|
460
|
+
border: 1px solid var(--dbviewer-code-border);
|
461
|
+
transition: background-color 0.2s ease, border-color 0.2s ease;
|
409
462
|
}
|
410
|
-
|
463
|
+
|
464
|
+
.sql-query-code:hover {
|
465
|
+
background-color: var(--dbviewer-code-bg);
|
466
|
+
filter: brightness(0.95);
|
467
|
+
}
|
468
|
+
|
469
|
+
/* ================================================
|
470
|
+
Query Performance Indicators
|
471
|
+
================================================ */
|
411
472
|
.query-duration {
|
412
|
-
color:
|
473
|
+
color: var(--dbviewer-success-color);
|
413
474
|
font-weight: 500;
|
475
|
+
font-variant-numeric: tabular-nums;
|
476
|
+
transition: color 0.2s ease;
|
414
477
|
}
|
415
478
|
|
416
479
|
.query-duration-slow {
|
417
|
-
color:
|
480
|
+
color: var(--dbviewer-danger-color);
|
418
481
|
font-weight: 600;
|
482
|
+
font-variant-numeric: tabular-nums;
|
483
|
+
transition: color 0.2s ease;
|
419
484
|
}
|
420
485
|
|
421
486
|
.query-timestamp {
|
422
|
-
color:
|
487
|
+
color: var(--dbviewer-muted-color);
|
488
|
+
font-variant-numeric: tabular-nums;
|
489
|
+
transition: color 0.2s ease;
|
423
490
|
}
|
424
|
-
|
491
|
+
|
492
|
+
/* ================================================
|
493
|
+
Empty States and Messages
|
494
|
+
================================================ */
|
425
495
|
.empty-data-message {
|
426
|
-
color:
|
496
|
+
color: var(--dbviewer-muted-color);
|
497
|
+
transition: color 0.2s ease;
|
427
498
|
}
|
428
|
-
|
429
|
-
|
499
|
+
|
500
|
+
.empty-data-message p {
|
501
|
+
margin-bottom: 0.5rem;
|
502
|
+
font-weight: 500;
|
503
|
+
}
|
504
|
+
|
505
|
+
.empty-data-message small {
|
506
|
+
opacity: 0.8;
|
507
|
+
}
|
508
|
+
|
509
|
+
/* ================================================
|
510
|
+
Loading States
|
511
|
+
================================================ */
|
430
512
|
.spinner-border-sm {
|
431
513
|
width: 1rem;
|
432
514
|
height: 1rem;
|
433
515
|
}
|
434
|
-
|
435
|
-
/*
|
516
|
+
|
517
|
+
/* ================================================
|
518
|
+
Skeleton Loader System
|
519
|
+
================================================ */
|
436
520
|
.skeleton-loader {
|
437
521
|
display: inline-block;
|
438
522
|
height: 1.2em;
|
439
523
|
width: 100%;
|
440
|
-
background: linear-gradient(
|
524
|
+
background: linear-gradient(
|
525
|
+
90deg,
|
526
|
+
var(--skeleton-base-color) 25%,
|
527
|
+
var(--skeleton-highlight-color) 37%,
|
528
|
+
var(--skeleton-base-color) 63%
|
529
|
+
);
|
441
530
|
background-size: 400% 100%;
|
442
531
|
animation: skeleton-loading 1.2s ease-in-out infinite;
|
443
|
-
border-radius:
|
532
|
+
border-radius: var(--dbviewer-border-radius);
|
444
533
|
}
|
445
|
-
|
534
|
+
|
535
|
+
/* Skeleton loader variants */
|
536
|
+
.skeleton-loader.number-loader {
|
446
537
|
width: 2.5em;
|
447
538
|
height: 1.5em;
|
448
539
|
margin-bottom: 0.2em;
|
449
540
|
}
|
450
|
-
|
541
|
+
|
542
|
+
.skeleton-loader.table-cell-loader {
|
451
543
|
width: 6em;
|
452
544
|
height: 1.2em;
|
453
545
|
}
|
454
|
-
|
546
|
+
|
547
|
+
.skeleton-loader.records-loader {
|
455
548
|
width: 3em;
|
456
549
|
height: 1.2em;
|
457
550
|
}
|
458
|
-
|
551
|
+
|
552
|
+
.skeleton-loader.query-cell-loader {
|
459
553
|
width: 12em;
|
460
554
|
height: 1.2em;
|
461
555
|
}
|
462
|
-
|
556
|
+
|
557
|
+
.skeleton-loader.duration-cell-loader {
|
463
558
|
width: 4em;
|
464
559
|
height: 1.2em;
|
465
560
|
}
|
466
|
-
|
561
|
+
|
562
|
+
.skeleton-loader.time-cell-loader {
|
467
563
|
width: 7em;
|
468
564
|
height: 1.2em;
|
469
565
|
}
|
566
|
+
|
567
|
+
/* ================================================
|
568
|
+
Animations
|
569
|
+
================================================ */
|
470
570
|
@keyframes skeleton-loading {
|
471
571
|
0% {
|
472
572
|
background-position: 100% 50%;
|
@@ -475,4 +575,58 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
475
575
|
background-position: 0 50%;
|
476
576
|
}
|
477
577
|
}
|
578
|
+
|
579
|
+
/* ================================================
|
580
|
+
Table Enhancements
|
581
|
+
================================================ */
|
582
|
+
.table-hover tbody tr:hover .sql-query-code {
|
583
|
+
background-color: var(--dbviewer-code-bg);
|
584
|
+
filter: brightness(0.9);
|
585
|
+
}
|
586
|
+
|
587
|
+
/* ================================================
|
588
|
+
Responsive Design
|
589
|
+
================================================ */
|
590
|
+
@media (max-width: 768px) {
|
591
|
+
.sql-query-code {
|
592
|
+
font-size: 0.75rem;
|
593
|
+
padding: 1px 3px;
|
594
|
+
}
|
595
|
+
|
596
|
+
.query-cell-loader {
|
597
|
+
width: 8em;
|
598
|
+
}
|
599
|
+
|
600
|
+
.duration-cell-loader {
|
601
|
+
width: 3em;
|
602
|
+
}
|
603
|
+
|
604
|
+
.time-cell-loader {
|
605
|
+
width: 5em;
|
606
|
+
}
|
607
|
+
}
|
608
|
+
|
609
|
+
/* ================================================
|
610
|
+
Accessibility Improvements
|
611
|
+
================================================ */
|
612
|
+
@media (prefers-reduced-motion: reduce) {
|
613
|
+
.skeleton-loader {
|
614
|
+
animation: none;
|
615
|
+
background: var(--skeleton-base-color);
|
616
|
+
}
|
617
|
+
|
618
|
+
.sql-query-code,
|
619
|
+
.query-duration,
|
620
|
+
.query-duration-slow,
|
621
|
+
.query-timestamp,
|
622
|
+
.empty-data-message {
|
623
|
+
transition: none;
|
624
|
+
}
|
625
|
+
}
|
626
|
+
|
627
|
+
/* Focus states for better keyboard navigation */
|
628
|
+
.sql-query-code:focus-visible {
|
629
|
+
outline: 2px solid var(--dbviewer-success-color);
|
630
|
+
outline-offset: 2px;
|
631
|
+
}
|
478
632
|
</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
|
|
@@ -38,6 +38,9 @@ module Dbviewer
|
|
38
38
|
# @example { username: 'admin', password: 'secret' }
|
39
39
|
attr_accessor :admin_credentials
|
40
40
|
|
41
|
+
# Default column to order table details by (e.g., 'updated_at')
|
42
|
+
attr_accessor :default_order_column
|
43
|
+
|
41
44
|
def initialize
|
42
45
|
@per_page_options = [ 10, 20, 50, 100 ]
|
43
46
|
@default_per_page = 20
|
@@ -51,6 +54,7 @@ module Dbviewer
|
|
51
54
|
@max_memory_queries = 1000
|
52
55
|
@enable_query_logging = true
|
53
56
|
@admin_credentials = nil
|
57
|
+
@default_order_column = "updated_at"
|
54
58
|
end
|
55
59
|
end
|
56
60
|
end
|
data/lib/dbviewer/version.rb
CHANGED
@@ -15,4 +15,7 @@ Dbviewer.configure do |config|
|
|
15
15
|
|
16
16
|
# Authentication options
|
17
17
|
# config.admin_credentials = { username: "admin", password: "your_secure_password" } # Basic HTTP auth credentials
|
18
|
+
|
19
|
+
# Default table ordering options
|
20
|
+
config.default_order_column = "updated_at" # Primary column to order by
|
18
21
|
end
|