dbviewer 0.3.6 → 0.3.15

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.
@@ -2,6 +2,37 @@
2
2
  Table: <%= @table_name %>
3
3
  <% end %>
4
4
 
5
+ <% content_for :head do %>
6
+ <!-- Mermaid.js library for ERD diagrams -->
7
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
8
+ <!-- SVG-Pan-Zoom for interactive diagram navigation -->
9
+ <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
10
+ <script>
11
+ // Initialize mermaid when document is ready
12
+ document.addEventListener('DOMContentLoaded', function() {
13
+ // Configure Mermaid for better ERD diagrams
14
+ mermaid.initialize({
15
+ startOnLoad: false,
16
+ theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
17
+ securityLevel: 'loose',
18
+ er: {
19
+ diagramPadding: 20,
20
+ layoutDirection: 'TB',
21
+ minEntityWidth: 100,
22
+ minEntityHeight: 75,
23
+ entityPadding: 15,
24
+ stroke: 'gray',
25
+ fill: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2D3748' : '#f5f5f5',
26
+ fontSize: 14,
27
+ useMaxWidth: true,
28
+ wrapiength: 30
29
+ }
30
+ });
31
+ console.log('Mermaid initialized with theme:', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default');
32
+ });
33
+ </script>
34
+ <% end %>
35
+
5
36
  <% content_for :sidebar_active do %>active<% end %>
6
37
 
7
38
  <% content_for :sidebar do %>
@@ -11,6 +42,9 @@
11
42
  <div class="d-flex justify-content-between align-items-center mb-4">
12
43
  <h1>Table: <%= @table_name %></h1>
13
44
  <div class="d-flex gap-2">
45
+ <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
46
+ <i class="bi bi-diagram-3 me-1"></i> View Relationships
47
+ </button>
14
48
  <% if Dbviewer.configuration.enable_data_export %>
15
49
  <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#csvExportModal">
16
50
  <i class="bi bi-file-earmark-spreadsheet me-1"></i> Export CSV
@@ -63,6 +97,25 @@
63
97
  </div>
64
98
  <% end %>
65
99
 
100
+ <!-- Mini ERD Modal -->
101
+ <div class="modal fade" id="miniErdModal" tabindex="-1" aria-labelledby="miniErdModalLabel" aria-hidden="true">
102
+ <div class="modal-dialog modal-xl modal-dialog-centered">
103
+ <div class="modal-content" id="miniErdModalContent">
104
+ <!-- Content will be loaded dynamically -->
105
+ <div class="modal-body text-center p-0">
106
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
107
+ <div class="text-center">
108
+ <div class="spinner-border text-primary" role="status">
109
+ <span class="visually-hidden">Loading...</span>
110
+ </div>
111
+ <p class="mt-2">Loading relationships diagram...</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
66
119
  <!-- Records Section -->
67
120
  <div class="dbviewer-card card mb-4">
68
121
  <div class="card-header d-flex justify-content-between align-items-center">
@@ -95,44 +148,72 @@
95
148
  <table class="table table-bordered table-striped rounded-none">
96
149
  <thead class="dbviewer-table-header">
97
150
  <tr>
98
- <% @records.columns.each do |column_name| %>
99
- <th class="pe-4">
100
- <%= column_name %>
101
- </th>
151
+ <% if @records && @records.columns %>
152
+ <% @records.columns.each do |column_name| %>
153
+ <th class="pe-4">
154
+ <%= column_name %>
155
+ </th>
156
+ <% end %>
157
+ <% else %>
158
+ <th>No columns available</th>
102
159
  <% end %>
103
160
  </tr>
104
161
  <tr class="column-filters">
105
- <% @records.columns.each do |column_name| %>
106
- <th class="p-0">
107
- <%= form.text_field "column_filters[#{column_name}]",
108
- value: @column_filters[column_name],
109
- placeholder: "",
110
- class: "form-control form-control-sm column-filter rounded-0",
111
- data: { column: column_name } %>
112
- </th>
162
+ <% if @records && @records.columns %>
163
+ <% @records.columns.each do |column_name| %>
164
+ <th class="p-0">
165
+ <%= form.text_field "column_filters[#{column_name}]",
166
+ value: @column_filters[column_name],
167
+ placeholder: "",
168
+ class: "form-control form-control-sm column-filter rounded-0",
169
+ data: { column: column_name } %>
170
+ </th>
171
+ <% end %>
172
+ <% else %>
173
+ <th></th>
113
174
  <% end %>
114
175
  </tr>
115
176
  </thead>
116
177
  <tbody>
117
- <% if @records.empty? %>
178
+ <% if @records.nil? || @records.rows.nil? || @records.empty? %>
118
179
  <tr>
119
180
  <td colspan="100%" class="text-center">No records found or table is empty.</td>
120
181
  </tr>
121
182
  <% end %>
122
- <% @records.rows.each do |row| %>
123
- <tr>
124
- <% row.each do |cell| %>
125
- <% cell_value = format_cell_value(cell) %>
126
- <td title="<%= cell_value %>"><%= cell_value %></td>
127
- <% end %>
128
- </tr>
183
+ <% if @records && @records.rows %>
184
+ <% @records.rows.each do |row| %>
185
+ <tr>
186
+ <% row.each_with_index do |cell, cell_index| %>
187
+ <%
188
+ column_name = @records.columns[cell_index]
189
+ cell_value = format_cell_value(cell)
190
+
191
+ # Check if this column is a foreign key
192
+ foreign_key = @metadata && @metadata[:foreign_keys] ?
193
+ @metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
194
+ nil
195
+ %>
196
+ <% if foreign_key && !cell.nil? %>
197
+ <td title="<%= cell_value %> (Click to view referenced record)">
198
+ <%= link_to cell_value,
199
+ table_path(foreign_key[:to_table],
200
+ column_filters: { foreign_key[:primary_key] => cell }),
201
+ class: "text-decoration-none foreign-key-link" %>
202
+ <i class="bi bi-link-45deg text-muted small"></i>
203
+ </td>
204
+ <% else %>
205
+ <td title="<%= cell_value %>"><%= cell_value %></td>
206
+ <% end %>
207
+ <% end %>
208
+ </tr>
209
+ <% end %>
129
210
  <% end %>
130
211
  </tbody>
131
212
  </table>
132
213
  <% end %> <!-- End of form_with -->
133
214
  </div>
134
215
 
135
- <% if @total_pages > 1 %>
216
+ <% if @total_pages && @total_pages > 1 %>
136
217
  <nav aria-label="Page navigation">
137
218
  <ul class="pagination justify-content-center">
138
219
  <li class="page-item <%= 'disabled' if @current_page == 1 %>">
@@ -267,6 +348,426 @@
267
348
  });
268
349
  }
269
350
  }
351
+
352
+ // Load Mini ERD when modal is opened
353
+ const miniErdModal = document.getElementById('miniErdModal');
354
+ if (miniErdModal) {
355
+ let isModalLoaded = false;
356
+ let erdData = null;
357
+
358
+ miniErdModal.addEventListener('show.bs.modal', function(event) {
359
+ const modalContent = document.getElementById('miniErdModalContent');
360
+
361
+ // Set loading state
362
+ modalContent.innerHTML = `
363
+ <div class="modal-header">
364
+ <h5 class="modal-title">Relationships for <%= @table_name %></h5>
365
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
366
+ </div>
367
+ <div class="modal-body p-0">
368
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
369
+ <div class="text-center">
370
+ <div class="spinner-border text-primary mb-3" role="status">
371
+ <span class="visually-hidden">Loading...</span>
372
+ </div>
373
+ <p class="mt-2">Loading relationships diagram...</p>
374
+ <small class="text-muted">This may take a moment for tables with many relationships</small>
375
+ </div>
376
+ </div>
377
+ </div>
378
+ `;
379
+
380
+ // Always fetch fresh data when modal is opened
381
+ fetchErdData();
382
+ });
383
+
384
+ // Function to fetch ERD data
385
+ function fetchErdData() {
386
+ // Add cache-busting timestamp to prevent browser caching
387
+ const cacheBuster = new Date().getTime();
388
+ const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
389
+
390
+ // Log loading message
391
+ console.log('Loading fresh Mini ERD data from:', fetchUrl);
392
+
393
+ // Set a timeout to handle long-running requests
394
+ const timeoutPromise = new Promise((_, reject) =>
395
+ setTimeout(() => reject(new Error('Request timeout after 10 seconds')), 10000)
396
+ );
397
+
398
+ // Race the fetch against a timeout
399
+ Promise.race([
400
+ fetch(fetchUrl),
401
+ timeoutPromise
402
+ ])
403
+ .then(response => {
404
+ if (!response.ok) {
405
+ throw new Error(`Server returned ${response.status} ${response.statusText}`);
406
+ }
407
+ return response.json(); // Parse as JSON instead of text
408
+ })
409
+ .then(data => {
410
+ isModalLoaded = true;
411
+ erdData = data; // Store the data
412
+ renderMiniErd(data);
413
+ })
414
+ .catch(error => {
415
+ console.error('Error loading mini ERD:', error);
416
+ showErdError(error);
417
+ });
418
+ }
419
+
420
+ // Function to show error modal
421
+ function showErdError(error) {
422
+ const modalContent = document.getElementById('miniErdModalContent');
423
+ modalContent.innerHTML = `
424
+ <div class="modal-header">
425
+ <h5 class="modal-title">Relationships for <%= @table_name %></h5>
426
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
427
+ </div>
428
+ <div class="modal-body p-0">
429
+ <div class="alert alert-danger m-3">
430
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
431
+ <strong>Error loading relationship diagram</strong>
432
+ <p class="mt-2 mb-0">${error.message}</p>
433
+ </div>
434
+ <div class="m-3">
435
+ <p><strong>Debug Information:</strong></p>
436
+ <code>GET <%= dbviewer.mini_erd_table_path(@table_name, format: :json) %></code> failed
437
+ <p class="mt-3">
438
+ <button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
439
+ <i class="bi bi-arrow-clockwise me-1"></i> Retry
440
+ </button>
441
+ </p>
442
+ </div>
443
+ </div>
444
+ <div class="modal-footer">
445
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
446
+ </div>
447
+ `;
448
+ }
449
+
450
+ // Function to render the ERD with Mermaid
451
+ function renderMiniErd(data) {
452
+ const modalContent = document.getElementById('miniErdModalContent');
453
+
454
+ // Set up the modal content with container for ERD
455
+ modalContent.innerHTML = `
456
+ <div class="modal-header">
457
+ <h5 class="modal-title">Relationships for <%= @table_name %></h5>
458
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
459
+ </div>
460
+ <div class="modal-body p-0"> <!-- Removed padding for full width -->
461
+ <div id="mini-erd-container" class="w-100" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
462
+ <div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
463
+ <div class="text-center">
464
+ <div class="spinner-border text-primary mb-3" role="status">
465
+ <span class="visually-hidden">Loading...</span>
466
+ </div>
467
+ <p>Generating Relationships Diagram...</p>
468
+ </div>
469
+ </div>
470
+ <div id="mini-erd-error" class="alert alert-danger m-3 d-none">
471
+ <h5>Error generating diagram</h5>
472
+ <p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
473
+ <pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
474
+ </div>
475
+ </div>
476
+ <div id="debug-data" class="d-none m-3 border-top pt-3">
477
+ <details>
478
+ <summary>Debug Information</summary>
479
+ <div class="alert alert-info small">
480
+ <pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
481
+ </div>
482
+ </details>
483
+ </div>
484
+ </div>
485
+ <div class="modal-footer">
486
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
487
+ <a href="<%= dbviewer.entity_relationship_diagrams_path %>" class="btn btn-primary">View Full ERD</a>
488
+ </div>
489
+ `;
490
+
491
+ try {
492
+ const tables = data.tables || [];
493
+ const relationships = data.relationships || [];
494
+
495
+ // Validate data before proceeding
496
+ if (!Array.isArray(tables) || !Array.isArray(relationships)) {
497
+ showDiagramError('Invalid data format', 'The relationship data is not in the expected format.');
498
+ console.error('Invalid data format received:', data);
499
+ return;
500
+ }
501
+
502
+ console.log(`Found ${tables.length} tables and ${relationships.length} relationships`);
503
+
504
+ // Create the ER diagram definition in Mermaid syntax
505
+ let mermaidDefinition = 'erDiagram\n';
506
+
507
+ // Add tables to the diagram - ensure we have at least one table
508
+ if (tables.length === 0) {
509
+ mermaidDefinition += ` <%= @table_name.gsub(/[^\w]/, '_') %> {\n`;
510
+ mermaidDefinition += ` string id PK\n`;
511
+ mermaidDefinition += ` }\n`;
512
+ } else {
513
+ tables.forEach(function(table) {
514
+ const tableName = table.name;
515
+
516
+ if (!tableName) {
517
+ console.warn('Table with no name found:', table);
518
+ return; // Skip this table
519
+ }
520
+
521
+ // Clean table name for mermaid (remove special characters)
522
+ const cleanTableName = tableName.replace(/[^\w]/g, '_');
523
+
524
+ // Make the current table stand out with a different visualization
525
+ if (tableName === '<%= @table_name %>') {
526
+ mermaidDefinition += ` ${cleanTableName} {\n`;
527
+ mermaidDefinition += ` string id PK\n`;
528
+ mermaidDefinition += ` }\n`;
529
+ } else {
530
+ mermaidDefinition += ` ${cleanTableName} {\n`;
531
+ mermaidDefinition += ` string id\n`;
532
+ mermaidDefinition += ` }\n`;
533
+ }
534
+ });
535
+ }
536
+
537
+ // Add relationships
538
+ if (relationships && relationships.length > 0) {
539
+ relationships.forEach(function(rel) {
540
+ try {
541
+ // Ensure all required properties exist
542
+ if (!rel.from_table || !rel.to_table) {
543
+ console.error('Missing table in relationship:', rel);
544
+ return; // Skip this relationship
545
+ }
546
+
547
+ // Clean up table names for mermaid (remove special characters)
548
+ const fromTable = rel.from_table.replace(/[^\w]/g, '_');
549
+ const toTable = rel.to_table.replace(/[^\w]/g, '_');
550
+ const relationLabel = rel.from_column || '';
551
+
552
+ // Customize the display based on direction
553
+ mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
554
+ } catch (err) {
555
+ console.error('Error processing relationship:', err, rel);
556
+ }
557
+ });
558
+ } else {
559
+ // Add a note if no relationships are found
560
+ mermaidDefinition += ' %% No relationships found for this table\n';
561
+ }
562
+
563
+ // Log the generated mermaid definition for debugging
564
+ console.log('Mermaid Definition:', mermaidDefinition);
565
+
566
+ // Hide the loading indicator first since render might take time
567
+ document.getElementById('mini-erd-loading').style.display = 'none';
568
+
569
+ // Render the diagram with Mermaid
570
+ mermaid.render('mini-erd-graph', mermaidDefinition)
571
+ .then(function(result) {
572
+ console.log('Mermaid rendering successful');
573
+
574
+ // Get the container
575
+ const container = document.getElementById('mini-erd-container');
576
+
577
+ // Insert the rendered SVG
578
+ container.innerHTML = result.svg;
579
+
580
+ // Style the SVG element for better fit
581
+ const svgElement = container.querySelector('svg');
582
+ if (svgElement) {
583
+ // Set size attributes for the SVG
584
+ svgElement.setAttribute('width', '100%');
585
+ svgElement.setAttribute('height', '100%');
586
+ svgElement.style.minHeight = '450px';
587
+ svgElement.style.width = '100%';
588
+ svgElement.style.height = '100%';
589
+
590
+ // Set viewBox if not present to enable proper scaling
591
+ if (!svgElement.getAttribute('viewBox')) {
592
+ const width = svgElement.getAttribute('width') || '100%';
593
+ const height = svgElement.getAttribute('height') || '100%';
594
+ svgElement.setAttribute('viewBox', `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`);
595
+ }
596
+ }
597
+
598
+ // Apply SVG-Pan-Zoom to make the diagram interactive
599
+ try {
600
+ const svgElement = container.querySelector('svg');
601
+ if (svgElement && typeof svgPanZoom !== 'undefined') {
602
+ // Make SVG take the full container width
603
+ svgElement.setAttribute('width', '100%');
604
+ svgElement.setAttribute('height', '100%');
605
+
606
+ // Initialize SVG Pan-Zoom
607
+ const panZoomInstance = svgPanZoom(svgElement, {
608
+ zoomEnabled: true,
609
+ controlIconsEnabled: true,
610
+ fit: true,
611
+ center: true,
612
+ minZoom: 0.5,
613
+ maxZoom: 2.5
614
+ });
615
+
616
+ // Store the panZoom instance for resize handling
617
+ container.panZoomInstance = panZoomInstance;
618
+
619
+ // Setup resize observer to maintain full size
620
+ const resizeObserver = new ResizeObserver(() => {
621
+ if (container.panZoomInstance) {
622
+ // Reset zoom and center when container is resized
623
+ container.panZoomInstance.resize();
624
+ container.panZoomInstance.fit();
625
+ container.panZoomInstance.center();
626
+ }
627
+ });
628
+
629
+ // Observe the container for size changes
630
+ resizeObserver.observe(container);
631
+
632
+ // Also handle manual resize on modal resize
633
+ miniErdModal.addEventListener('resize.bs.modal', function() {
634
+ if (container.panZoomInstance) {
635
+ setTimeout(() => {
636
+ container.panZoomInstance.resize();
637
+ container.panZoomInstance.fit();
638
+ container.panZoomInstance.center();
639
+ }, 100);
640
+ }
641
+ });
642
+ }
643
+ } catch (e) {
644
+ console.warn('Failed to initialize svg-pan-zoom:', e);
645
+ // Not critical, continue without pan-zoom
646
+ }
647
+
648
+ // Add highlighting for the current table
649
+ setTimeout(function() {
650
+ try {
651
+ const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
652
+ const currentTableElement = container.querySelector(`[id*="${cleanTableName}"]`);
653
+ if (currentTableElement) {
654
+ const rect = currentTableElement.querySelector('rect');
655
+ if (rect) {
656
+ // Highlight the current table
657
+ rect.setAttribute('fill', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2c3034' : '#e2f0ff');
658
+ rect.setAttribute('stroke', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#6ea8fe' : '#0d6efd');
659
+ rect.setAttribute('stroke-width', '2');
660
+ }
661
+ }
662
+ } catch (e) {
663
+ console.error('Error highlighting current table:', e);
664
+ }
665
+ }, 100);
666
+ })
667
+ .catch(function(error) {
668
+ console.error('Error rendering mini ERD:', error);
669
+ showDiagramError(
670
+ 'Error rendering diagram',
671
+ 'There was an error rendering the relationships diagram.',
672
+ error.message || 'Unknown error'
673
+ );
674
+
675
+ // Show debug data when there's an error
676
+ document.getElementById('debug-data').classList.remove('d-none');
677
+ });
678
+ } catch (error) {
679
+ console.error('Exception in renderMiniErd function:', error);
680
+ showDiagramError(
681
+ 'Exception generating diagram',
682
+ 'There was an exception processing the relationships diagram.',
683
+ error.message || 'Unknown error'
684
+ );
685
+
686
+ // Show debug data when there's an error
687
+ document.getElementById('debug-data').classList.remove('d-none');
688
+ }
689
+ }
690
+
691
+ // Function to show diagram error
692
+ function showDiagramError(title, message, details = '') {
693
+ const errorContainer = document.getElementById('mini-erd-error');
694
+ const errorMessage = document.getElementById('mini-erd-error-message');
695
+ const errorDetails = document.getElementById('mini-erd-error-details');
696
+ const loadingIndicator = document.getElementById('mini-erd-loading');
697
+
698
+ if (loadingIndicator) {
699
+ loadingIndicator.style.display = 'none';
700
+ }
701
+
702
+ if (errorContainer && errorMessage) {
703
+ // Set error message
704
+ errorMessage.textContent = message;
705
+
706
+ // Set error details if provided
707
+ if (details && errorDetails) {
708
+ errorDetails.textContent = details;
709
+ errorDetails.classList.remove('d-none');
710
+ } else if (errorDetails) {
711
+ errorDetails.classList.add('d-none');
712
+ }
713
+
714
+ // Show the error container
715
+ errorContainer.classList.remove('d-none');
716
+ }
717
+ }
718
+
719
+ // Handle modal shown event - adjust size after the modal is fully visible
720
+ miniErdModal.addEventListener('shown.bs.modal', function(event) {
721
+ // After modal is fully shown, resize the diagram to fit
722
+ const container = document.getElementById('mini-erd-container');
723
+ if (container && container.panZoomInstance) {
724
+ setTimeout(() => {
725
+ container.panZoomInstance.resize();
726
+ container.panZoomInstance.fit();
727
+ container.panZoomInstance.center();
728
+ }, 200); // Small delay to ensure modal is fully transitioned
729
+ }
730
+ });
731
+
732
+ // Handle modal close to reset state for future opens
733
+ miniErdModal.addEventListener('hidden.bs.modal', function(event) {
734
+ // Reset flags and cached data to ensure fresh fetch on next open
735
+ isModalLoaded = false;
736
+ erdData = null;
737
+ console.log('Modal closed, diagram data will be refetched on next open');
738
+ });
739
+ }
740
+
741
+ // Function to retry loading the Mini ERD
742
+ function retryLoadingMiniERD() {
743
+ console.log('Retrying loading of mini ERD');
744
+ const modalContent = document.getElementById('miniErdModalContent');
745
+
746
+ // Set loading state again
747
+ modalContent.innerHTML = `
748
+ <div class="modal-header">
749
+ <h5 class="modal-title">Relationships for <%= @table_name %></h5>
750
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
751
+ </div>
752
+ <div class="modal-body p-0">
753
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
754
+ <div class="text-center">
755
+ <div class="spinner-border text-primary mb-3" role="status">
756
+ <span class="visually-hidden">Loading...</span>
757
+ </div>
758
+ <p>Retrying to load relationships diagram...</p>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ `;
763
+
764
+ // Reset state to ensure fresh fetch
765
+ isModalLoaded = false;
766
+ erdData = null;
767
+
768
+ // Retry fetching data
769
+ fetchErdData();
770
+ }
270
771
  });
271
772
  </script>
272
773
 
@@ -293,6 +794,50 @@
293
794
  color: rgba(255,255,255,0.9);
294
795
  border-color: rgba(255,255,255,0.15);
295
796
  }
797
+
798
+ /* Mini ERD modal styling */
799
+ #miniErdModal .modal-dialog {
800
+ max-width: 90%;
801
+ max-height: 90vh;
802
+ height: 90vh;
803
+ }
804
+
805
+ #miniErdModal .modal-content {
806
+ height: 100%;
807
+ }
808
+
809
+ #miniErdModal .modal-body {
810
+ height: calc(100% - 130px); /* Account for header and footer */
811
+ overflow: hidden; /* Prevent scrollbars within the modal body */
812
+ }
813
+
814
+ #miniErdModal #mini-erd-container {
815
+ height: 100%;
816
+ width: 100%;
817
+ }
818
+
819
+ #miniErdModal #mini-erd-container svg {
820
+ width: 100%;
821
+ height: 100%;
822
+ max-height: unset;
823
+ }
824
+
825
+ /* Foreign key link styling */
826
+ .foreign-key-link {
827
+ color: var(--bs-primary);
828
+ position: relative;
829
+ }
830
+
831
+ .foreign-key-link:hover {
832
+ text-decoration: underline !important;
833
+ }
834
+
835
+ .foreign-key-link + .bi-link-45deg {
836
+ font-size: 0.75rem;
837
+ margin-left: 0.25rem;
838
+ position: relative;
839
+ top: -1px;
840
+ }
296
841
  </style>
297
842
 
298
843
  <% if @timestamp_data.present? %>
@@ -551,6 +551,25 @@
551
551
  .stat-card-bg {
552
552
  background-color: var(--bs-light);
553
553
  }
554
+
555
+ /* Metric icon styling */
556
+ .metric-icon {
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: center;
560
+ border-radius: 50%;
561
+ width: 60px;
562
+ height: 60px;
563
+ min-width: 60px;
564
+ text-align: center;
565
+ background-color: rgba(var(--bs-primary-rgb), 0.1);
566
+ color: var(--bs-primary);
567
+ }
568
+
569
+ [data-bs-theme="dark"] .metric-icon {
570
+ background-color: rgba(13, 110, 253, 0.2);
571
+ color: #6ea8fe;
572
+ }
554
573
  </style>
555
574
  </head>
556
575
  <body>
data/config/routes.rb CHANGED
@@ -4,6 +4,7 @@ Dbviewer::Engine.routes.draw do
4
4
  get "query"
5
5
  post "query"
6
6
  get "export_csv"
7
+ get "mini_erd"
7
8
  end
8
9
  end
9
10