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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9caec4627e8912e40d85d63c9105cdc80b2bae4227e145372693855a40fbc29
4
- data.tar.gz: ca759885a1ec9522860d9926e1814f68457fa453832f7b3ea3298aa5c86222eb
3
+ metadata.gz: 03dfb13567e8cf7a2eaf2977e421fec7a2117ff12a6462515399a4b7cfb056d1
4
+ data.tar.gz: 77ae23ada80eabbc1a5744376a96c0d5df4f20f0cb3723be4c0f9c9967994716
5
5
  SHA512:
6
- metadata.gz: d71176e418170a1d82439a1266687d6f125c27dd96dcf008fb05ccecc169d11dafb52f1c5940549e030f3487fe8c2408f73dda51e7ddae8e05b598cde621943f
7
- data.tar.gz: cebdde38e2e4500a265b20b86d1e6daee3ed7ba95447b348635efb00e963f7d895b8f147ab94b2b8c50656940c74279ebfbd17a90ff3921341cec2a0121b84cd
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
- Configure Basic Authentication in an initializer to secure access (strongly recommended):
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 = "ASC" unless self.class::VALID_SORT_DIRECTIONS.include?(@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
- // Initialize mermaid
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: 'neutral',
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
- document.getElementById('erd-loading').innerHTML =
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: 'Courier New', Courier, monospace;
405
- font-size: 0.85rem;
406
- background-color: rgba(0, 0, 0, 0.05);
407
- padding: 2px 4px;
408
- border-radius: 3px;
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: #28a745;
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: #dc3545;
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: #6c757d;
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: #6c757d;
496
+ color: var(--dbviewer-muted-color);
497
+ transition: color 0.2s ease;
427
498
  }
428
-
429
- /* Loading animations */
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
- /* Skeleton loader styles */
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(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%);
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: 4px;
532
+ border-radius: var(--dbviewer-border-radius);
444
533
  }
445
- .number-loader {
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
- .table-cell-loader {
541
+
542
+ .skeleton-loader.table-cell-loader {
451
543
  width: 6em;
452
544
  height: 1.2em;
453
545
  }
454
- .records-loader {
546
+
547
+ .skeleton-loader.records-loader {
455
548
  width: 3em;
456
549
  height: 1.2em;
457
550
  }
458
- .query-cell-loader {
551
+
552
+ .skeleton-loader.query-cell-loader {
459
553
  width: 12em;
460
554
  height: 1.2em;
461
555
  }
462
- .duration-cell-loader {
556
+
557
+ .skeleton-loader.duration-cell-loader {
463
558
  width: 4em;
464
559
  height: 1.2em;
465
560
  }
466
- .time-cell-loader {
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
- // Initialize SVG Pan-Zoom
985
- const panZoomInstance = svgPanZoom(svgElement, {
986
- zoomEnabled: true,
987
- controlIconsEnabled: true,
988
- fit: true,
989
- center: true,
990
- minZoom: 0.5,
991
- maxZoom: 2.5
992
- });
993
-
994
- // Store the panZoom instance for resize handling
995
- container.panZoomInstance = panZoomInstance;
996
-
997
- // Setup resize observer to maintain full size
998
- const resizeObserver = new ResizeObserver(() => {
999
- if (container.panZoomInstance) {
1000
- // Reset zoom and center when container is resized
1001
- container.panZoomInstance.resize();
1002
- container.panZoomInstance.fit();
1003
- container.panZoomInstance.center();
1004
- }
1005
- });
1006
-
1007
- // Observe the container for size changes
1008
- resizeObserver.observe(container);
1009
-
1010
- // Also handle manual resize on modal resize
1011
- miniErdModal.addEventListener('resize.bs.modal', function() {
1012
- if (container.panZoomInstance) {
1013
- setTimeout(() => {
1014
- container.panZoomInstance.resize();
1015
- container.panZoomInstance.fit();
1016
- container.panZoomInstance.center();
1017
- }, 100);
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
- container.panZoomInstance.resize();
1104
- container.panZoomInstance.fit();
1105
- container.panZoomInstance.center();
1106
- }, 200); // Small delay to ensure modal is fully transitioned
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
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.4.6"
2
+ VERSION = "0.4.8"
3
3
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh