dbviewer 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -36
  3. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
  4. data/app/assets/javascripts/dbviewer/home.js +287 -0
  5. data/app/assets/javascripts/dbviewer/layout.js +194 -0
  6. data/app/assets/javascripts/dbviewer/query.js +277 -0
  7. data/app/assets/javascripts/dbviewer/table.js +1563 -0
  8. data/app/assets/stylesheets/dbviewer/application.css +1460 -21
  9. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
  10. data/app/assets/stylesheets/dbviewer/home.css +229 -0
  11. data/app/assets/stylesheets/dbviewer/logs.css +64 -0
  12. data/app/assets/stylesheets/dbviewer/query.css +171 -0
  13. data/app/assets/stylesheets/dbviewer/table.css +1144 -0
  14. data/app/views/dbviewer/connections/index.html.erb +0 -30
  15. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
  16. data/app/views/dbviewer/home/index.html.erb +9 -499
  17. data/app/views/dbviewer/logs/index.html.erb +22 -221
  18. data/app/views/dbviewer/tables/index.html.erb +0 -65
  19. data/app/views/dbviewer/tables/query.html.erb +129 -565
  20. data/app/views/dbviewer/tables/show.html.erb +4 -2429
  21. data/app/views/layouts/dbviewer/application.html.erb +13 -1544
  22. data/lib/dbviewer/version.rb +1 -1
  23. metadata +12 -7
  24. data/app/assets/javascripts/dbviewer/connections.js +0 -70
  25. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  26. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  27. data/app/views/dbviewer/connections/new.html.erb +0 -79
  28. data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
@@ -3,331 +3,10 @@
3
3
  <% end %>
4
4
 
5
5
  <% content_for :head do %>
6
- <!-- Mermaid.js library for ERD diagrams -->
7
6
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
8
- <!-- SVG-Pan-Zoom for interactive diagram navigation -->
9
7
  <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
10
- <style>
11
- /* Column sorting styles */
12
- .sortable-column {
13
- cursor: pointer;
14
- position: relative;
15
- transition: background-color 0.2s ease;
16
- background-color: inherit;
17
- }
18
-
19
- .sortable-column:hover {
20
- background-color: #f5f5f5;
21
- }
22
-
23
- .sortable-column.sorted {
24
- background-color: #f0f0f0;
25
- }
26
-
27
- .sortable-column .column-sort-link {
28
- display: flex;
29
- align-items: center;
30
- justify-content: space-between;
31
- width: 100%;
32
- height: 100%;
33
- padding: 4px 0;
34
- }
35
-
36
- .sortable-column .column-name {
37
- flex: 1;
38
- overflow: hidden;
39
- text-overflow: ellipsis;
40
- white-space: nowrap;
41
- }
42
-
43
- .sortable-column .sort-icon-container {
44
- flex: 0 0 auto;
45
- width: 20px;
46
- text-align: center;
47
- margin-left: 4px;
48
- }
49
-
50
- .sortable-column .sort-icon {
51
- font-size: 0.8em;
52
- opacity: 0.7;
53
- transition: opacity 0.2s ease, color 0.2s ease;
54
- }
55
-
56
- .sortable-column:hover .sort-icon.invisible {
57
- visibility: visible !important;
58
- opacity: 0.3;
59
- }
60
-
61
- /* Fix scrolling issues with sticky header */
62
- .dbviewer-table-header {
63
- position: sticky !important;
64
- top: 0;
65
- z-index: 10;
66
- background-color: var(--bs-table-striped-bg, #f2f2f2) !important;
67
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
68
- }
69
-
70
- [data-bs-theme="dark"] .dbviewer-table-header {
71
- background-color: var(--bs-dark-bg-subtle, #343a40) !important;
72
- }
73
-
74
- /* Ensure proper layering for sticky elements */
75
- .dbviewer-table-header th {
76
- position: sticky;
77
- top: 0;
78
- z-index: 20;
79
- }
80
-
81
- /* Increase z-index for the intersection point of sticky header and sticky column */
82
- .dbviewer-table-header th.action-column {
83
- z-index: 40 !important;
84
- box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
85
- }
86
-
87
- /* Ensure thead has higher z-index than tbody */
88
- thead tr th.action-column {
89
- z-index: 40 !important;
90
- }
91
-
92
- tbody tr td.action-column {
93
- z-index: 30 !important;
94
- }
95
-
96
- /* Improve mobile display for sort headers */
97
- @media (max-width: 767.98px) {
98
- .sortable-column .column-sort-link {
99
- flex-direction: row;
100
- align-items: center;
101
- }
102
-
103
- .sortable-column .sort-icon-container {
104
- width: 16px;
105
- }
106
- }
107
-
108
- /* Dark mode compatibility */
109
- [data-bs-theme="dark"] .sortable-column:hover {
110
- background-color: rgba(255, 255, 255, 0.05);
111
- }
112
-
113
- [data-bs-theme="dark"] .sortable-column.sorted {
114
- background-color: rgba(255, 255, 255, 0.1);
115
- }
116
-
117
- /* Column filter styling */
118
- .column-filters td {
119
- padding: 0.5rem;
120
- background-color: var(--bs-tertiary-bg, #f8f9fa);
121
- }
122
-
123
- /* Styling for disabled input fields (IS NULL, IS NOT NULL) */
124
- .column-filter:disabled, .disabled-filter {
125
- background-color: var(--bs-tertiary-bg, #f0f0f0);
126
- border-color: var(--bs-border-color, #dee2e6);
127
- color: var(--bs-secondary-color, #6c757d);
128
- opacity: 0.6;
129
- cursor: not-allowed;
130
- }
131
-
132
- /* Action column styling */
133
- .action-column {
134
- width: 100px; /* Increased from 60px to accommodate two buttons */
135
- min-width: 100px; /* Ensure minimum width */
136
- white-space: nowrap;
137
- position: sticky;
138
- left: 0;
139
- z-index: 15;
140
- background-color: var(--bs-table-striped-bg, #f2f2f2);
141
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
142
- }
143
-
144
- .copy-factory-btn {
145
- padding: 0.1rem 0.4rem;
146
- width: 32px;
147
- }
148
-
149
- .copy-factory-btn:hover {
150
- opacity: 0.85;
151
- transform: translateY(-1px);
152
- }
153
-
154
- /* Ensure proper background color for actions column in dark mode */
155
- [data-bs-theme="dark"] .action-column {
156
- background-color: var(--bs-dark-bg-subtle, #343a40);
157
- }
158
-
159
- /* Maintain zebra striping with sticky action column */
160
- .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
161
- background-color: var(--bs-table-striped-bg, #f8f9fa);
162
- }
163
-
164
- [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
165
- background-color: var(--bs-dark-bg-subtle, #343a40);
166
- }
167
-
168
- .view-record-btn {
169
- padding: 0.1rem 0.4rem;
170
- width: 32px;
171
- }
172
-
173
- .view-record-btn:hover {
174
- opacity: 0.85;
175
- transform: translateY(-1px);
176
- }
177
-
178
- /* Make action column header sticky as well */
179
- .action-column-header {
180
- position: sticky;
181
- left: 0;
182
- z-index: 40 !important; /* Even higher z-index to stay on top of everything */
183
- background-color: var(--bs-tertiary-bg, #f8f9fa) !important;
184
- box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04) !important;
185
- }
186
-
187
- [data-bs-theme="dark"] .action-column-header {
188
- background-color: var(--bs-dark-bg-subtle, #343a40) !important;
189
- }
190
-
191
- [data-bs-theme="dark"] .action-column-header {
192
- background-color: var(--bs-dark-bg-subtle, #343a40) !important;
193
- }
194
-
195
- /* Make action column filter cell sticky as well */
196
- .action-column-filter {
197
- position: sticky;
198
- left: 0;
199
- z-index: 40 !important;
200
- background-color: var(--bs-tertiary-bg, #f8f9fa) !important;
201
- }
202
-
203
- [data-bs-theme="dark"] .action-column-filter {
204
- background-color: var(--bs-tertiary-bg, #2b3035) !important;
205
- }
206
-
207
- /* Fix action column for entire table */
208
- .action-column {
209
- box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04);
210
- }
211
-
212
- /* Ensure equal padding for all cells */
213
- .action-column-header, .action-column-filter {
214
- padding-left: 8px !important;
215
- padding-right: 8px !important;
216
- }
217
-
218
- /* Relationship section styles */
219
- #relationshipsSection {
220
- border-top: 1px solid var(--bs-border-color, #dee2e6);
221
- margin-top: 1.5rem;
222
- padding-top: 1.5rem;
223
- }
224
-
225
- #relationshipsSection h6 {
226
- color: var(--bs-primary, #0d6efd);
227
- font-weight: 600;
228
- border-bottom: 2px solid var(--bs-primary, #0d6efd);
229
- padding-bottom: 0.5rem;
230
- margin-bottom: 1rem;
231
- }
232
-
233
- .relationship-section h6 {
234
- font-size: 0.95rem;
235
- margin-bottom: 0.75rem;
236
- padding: 0.5rem 0.75rem;
237
- background: linear-gradient(135deg, var(--bs-primary-bg-subtle, #cfe2ff), transparent);
238
- border-left: 3px solid var(--bs-primary, #0d6efd);
239
- border-radius: 0.25rem;
240
- }
241
-
242
- .relationship-section .table {
243
- margin-bottom: 0;
244
- border: 1px solid var(--bs-border-color, #dee2e6);
245
- }
246
-
247
- .relationship-section .table th {
248
- background-color: var(--bs-light, #f8f9fa);
249
- font-weight: 600;
250
- font-size: 0.875rem;
251
- border-bottom: 2px solid var(--bs-border-color, #dee2e6);
252
- }
253
-
254
- .relationship-section .table td {
255
- vertical-align: middle;
256
- font-size: 0.875rem;
257
- }
258
-
259
- .relationship-section .btn {
260
- font-size: 0.8rem;
261
- padding: 0.375rem 0.75rem;
262
- }
263
-
264
- .relationship-section .btn-outline-primary:hover {
265
- transform: translateX(2px);
266
- transition: transform 0.2s ease;
267
- }
268
-
269
- .relationship-section .btn-outline-success:hover {
270
- transform: translateX(2px);
271
- transition: transform 0.2s ease;
272
- }
273
-
274
- /* Dark mode relationship styles */
275
- [data-bs-theme="dark"] #relationshipsSection {
276
- border-top-color: var(--bs-border-color, #495057);
277
- }
278
-
279
- [data-bs-theme="dark"] .relationship-section h6 {
280
- background: linear-gradient(135deg, var(--bs-primary-bg-subtle, #031633), transparent);
281
- }
282
-
283
- [data-bs-theme="dark"] .relationship-section .table th {
284
- background-color: var(--bs-dark-bg-subtle, #343a40);
285
- color: var(--bs-light, #f8f9fa);
286
- }
287
-
288
- [data-bs-theme="dark"] .relationship-section .table {
289
- border-color: var(--bs-border-color, #495057);
290
- }
291
-
292
- /* Responsive relationship tables */
293
- @media (max-width: 767.98px) {
294
- .relationship-section .table th,
295
- .relationship-section .table td {
296
- font-size: 0.8rem;
297
- padding: 0.5rem 0.25rem;
298
- }
299
-
300
- .relationship-section .btn {
301
- font-size: 0.75rem;
302
- padding: 0.25rem 0.5rem;
303
- }
304
- }
305
- </style>
306
-
307
- <script>
308
- // Initialize mermaid when document is ready
309
- document.addEventListener('DOMContentLoaded', function() {
310
- // Configure Mermaid for better ERD diagrams
311
- mermaid.initialize({
312
- startOnLoad: false,
313
- theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
314
- securityLevel: 'loose',
315
- er: {
316
- diagramPadding: 20,
317
- layoutDirection: 'TB',
318
- minEntityWidth: 100,
319
- minEntityHeight: 75,
320
- entityPadding: 15,
321
- stroke: 'gray',
322
- fill: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2D3748' : '#f5f5f5',
323
- fontSize: 14,
324
- useMaxWidth: true,
325
- wrapiength: 30
326
- }
327
- });
328
- console.log('Mermaid initialized with theme:', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default');
329
- });
330
- </script>
8
+ <%= stylesheet_link_tag "dbviewer/table", "data-turbo-track": "reload" %>
9
+ <%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
331
10
  <% end %>
332
11
 
333
12
  <% content_for :sidebar_active do %>active<% end %>
@@ -525,1247 +204,6 @@
525
204
  </div>
526
205
  </div>
527
206
 
528
- <style>
529
- /* Borderless table styling */
530
- .table {
531
- border-collapse: separate;
532
- border-spacing: 0;
533
- }
534
-
535
- .table th,
536
- .table td {
537
- border: none;
538
- border-bottom: 1px solid var(--bs-border-subtle, rgba(0, 0, 0, 0.05));
539
- }
540
-
541
- .table thead th {
542
- border-bottom: 2px solid var(--bs-border-subtle, rgba(0, 0, 0, 0.08));
543
- font-weight: 500;
544
- }
545
-
546
- /* Add a subtle hover effect on table rows */
547
- .table tbody tr:hover {
548
- background-color: var(--bs-tertiary-bg, rgba(0, 0, 0, 0.02));
549
- }
550
-
551
- /* Dark mode compatibility */
552
- [data-bs-theme="dark"] .table th,
553
- [data-bs-theme="dark"] .table td {
554
- border-bottom: 1px solid var(--bs-border-subtle, rgba(255, 255, 255, 0.05));
555
- }
556
-
557
- [data-bs-theme="dark"] .table thead th {
558
- border-bottom: 2px solid var(--bs-border-subtle, rgba(255, 255, 255, 0.08));
559
- }
560
-
561
- [data-bs-theme="dark"] .table tbody tr:hover {
562
- background-color: var(--bs-tertiary-bg, rgba(255, 255, 255, 0.03));
563
- }
564
-
565
- /* Column filter styling */
566
- .column-filters td {
567
- padding: 0.5rem;
568
- background-color: var(--bs-tertiary-bg, #f8f9fa);
569
- }
570
-
571
- /* Action column styling */
572
- .action-column {
573
- width: 100px; /* Increased from 60px to accommodate two buttons */
574
- min-width: 100px; /* Ensure minimum width */
575
- white-space: nowrap;
576
- position: sticky;
577
- left: 0;
578
- z-index: 30; /* Increased z-index to ensure it stays on top */
579
- background-color: var(--bs-body-bg, #fff); /* Use body background color */
580
- box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04);
581
- }
582
-
583
- .copy-factory-btn {
584
- padding: 0.1rem 0.4rem;
585
- width: 32px;
586
- }
587
-
588
- .copy-factory-btn:hover {
589
- opacity: 0.85;
590
- transform: translateY(-1px);
591
- }
592
-
593
- /* Ensure proper background color for actions column in dark mode */
594
- [data-bs-theme="dark"] .action-column {
595
- background-color: var(--bs-body-bg, #212529); /* Use body background in dark mode */
596
- }
597
-
598
- /* Maintain zebra striping with sticky action column */
599
- .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
600
- background-color: var(--bs-tertiary-bg, #f8f9fa);
601
- }
602
-
603
- .table-striped > tbody > tr:nth-of-type(even) > .action-column {
604
- background-color: var(--bs-body-bg, #fff);
605
- }
606
-
607
- [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
608
- background-color: var(--bs-tertiary-bg, #2b3035);
609
- }
610
-
611
- [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(even) > .action-column {
612
- background-color: var(--bs-body-bg, #212529);
613
- }
614
-
615
- .view-record-btn {
616
- padding: 0.1rem 0.4rem;
617
- width: 32px;
618
- }
619
-
620
- .view-record-btn:hover {
621
- opacity: 0.85;
622
- transform: translateY(-1px);
623
- }
624
-
625
- /* Record detail modal styling */
626
- .record-detail-table tr:first-child th,
627
- .record-detail-table tr:first-child td {
628
- border-top: none;
629
- }
630
-
631
- .record-detail-table .code-block {
632
- background-color: var(--bs-light);
633
- padding: 0.5rem;
634
- border-radius: 0.25rem;
635
- overflow-x: auto;
636
- max-height: 200px;
637
- }
638
-
639
- /* Relationships section styling */
640
- #relationshipsSection {
641
- border-top: 1px solid var(--bs-border-color);
642
- padding-top: 1rem;
643
- }
644
-
645
- #relationshipsSection h6 {
646
- color: var(--bs-emphasis-color);
647
- margin-bottom: 1rem;
648
- }
649
-
650
- [data-bs-theme="dark"] #relationshipsSection {
651
- border-top-color: #495057;
652
- }
653
-
654
- .relationships-table .btn-outline-primary {
655
- font-size: 0.75rem;
656
- padding: 0.25rem 0.5rem;
657
- }
658
-
659
- .relationships-table code {
660
- background-color: var(--bs-gray-100);
661
- padding: 0.125rem 0.25rem;
662
- border-radius: 0.125rem;
663
- font-size: 0.875rem;
664
- }
665
-
666
- [data-bs-theme="dark"] .relationships-table code {
667
- background-color: var(--bs-gray-800);
668
- color: var(--bs-gray-100);
669
- }
670
- margin-bottom: 0;
671
- }
672
-
673
- [data-bs-theme="dark"] .record-detail-table .code-block {
674
- background-color: var(--bs-dark);
675
- }
676
-
677
- /* Fullscreen table styles */
678
- .table-fullscreen {
679
- position: fixed !important;
680
- top: 0 !important;
681
- left: 0 !important;
682
- width: 100vw !important;
683
- height: 100vh !important;
684
- z-index: 9999 !important;
685
- background: var(--bs-body-bg) !important;
686
- margin: 0 !important;
687
- border-radius: 0 !important;
688
- overflow: hidden !important;
689
- display: flex !important;
690
- flex-direction: column !important;
691
- }
692
-
693
- .table-fullscreen .card-body {
694
- flex: 1 !important;
695
- overflow: hidden !important;
696
- display: flex !important;
697
- flex-direction: column !important;
698
- }
699
-
700
- .table-fullscreen .table-responsive {
701
- flex: 1 !important;
702
- overflow: auto !important;
703
- }
704
-
705
- .table-fullscreen .card-header {
706
- flex-shrink: 0 !important;
707
- position: sticky !important;
708
- top: 0 !important;
709
- z-index: 10000 !important;
710
- background: var(--bs-body-bg) !important;
711
- border-bottom: 1px solid var(--bs-border-color) !important;
712
- }
713
-
714
- /* Hide pagination in fullscreen mode */
715
- .table-fullscreen .pagination-container {
716
- display: none !important;
717
- }
718
-
719
- /* Adjust table header in fullscreen */
720
- .table-fullscreen .dbviewer-table-header {
721
- position: sticky !important;
722
- top: 0 !important;
723
- z-index: 100 !important;
724
- }
725
-
726
- /* Ensure body doesn't scroll when table is fullscreen */
727
- body.table-fullscreen-active {
728
- overflow: hidden !important;
729
- }
730
-
731
- /* Fullscreen button hover effect */
732
- #fullscreen-toggle:hover {
733
- background-color: var(--bs-secondary-bg) !important;
734
- border-color: var(--bs-secondary-border-subtle) !important;
735
- }
736
-
737
- /* Smooth transitions */
738
- #table-section {
739
- transition: all 0.3s ease-in-out;
740
- }
741
- </style>
742
-
743
- <script>
744
- document.addEventListener('DOMContentLoaded', function() {
745
- // Record Detail Modal functionality
746
- const recordDetailModal = document.getElementById('recordDetailModal');
747
- if (recordDetailModal) {
748
- recordDetailModal.addEventListener('show.bs.modal', function (event) {
749
- // Button that triggered the modal
750
- const button = event.relatedTarget;
751
-
752
- // Extract record data from button's data attribute
753
- let recordData;
754
- let foreignKeys;
755
- try {
756
- recordData = JSON.parse(button.getAttribute('data-record-data'));
757
- foreignKeys = JSON.parse(button.getAttribute('data-foreign-keys') || '[]');
758
- } catch (e) {
759
- console.error('Error parsing record data:', e);
760
- recordData = {};
761
- foreignKeys = [];
762
- }
763
-
764
- // Update the modal's title with table name
765
- const modalTitle = recordDetailModal.querySelector('.modal-title');
766
- modalTitle.textContent = '<%= @table_name %> Record Details';
767
-
768
- // Populate the table with record data
769
- const tableBody = document.getElementById('recordDetailTableBody');
770
- tableBody.innerHTML = '';
771
-
772
- // Get all columns
773
- const columns = Object.keys(recordData);
774
-
775
- // Create rows for each column
776
- columns.forEach(column => {
777
- const row = document.createElement('tr');
778
-
779
- // Create column name cell
780
- const columnNameCell = document.createElement('td');
781
- columnNameCell.className = 'fw-bold';
782
- columnNameCell.textContent = column;
783
- row.appendChild(columnNameCell);
784
-
785
- // Create value cell
786
- const valueCell = document.createElement('td');
787
- let cellValue = recordData[column];
788
-
789
- // Format value differently based on type
790
- if (cellValue === null) {
791
- valueCell.innerHTML = '<span class="text-muted">NULL</span>';
792
- } else if (typeof cellValue === 'string' && cellValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
793
- // Handle datetime values
794
- const date = new Date(cellValue);
795
- if (!isNaN(date.getTime())) {
796
- valueCell.textContent = date.toLocaleString();
797
- } else {
798
- valueCell.textContent = cellValue;
799
- }
800
- } else if (typeof cellValue === 'string' && (cellValue.startsWith('{') || cellValue.startsWith('['))) {
801
- // Handle JSON values
802
- try {
803
- const jsonValue = JSON.parse(cellValue);
804
- const formattedJSON = JSON.stringify(jsonValue, null, 2);
805
- valueCell.innerHTML = `<pre class="mb-0 code-block">${formattedJSON}</pre>`;
806
- } catch (e) {
807
- valueCell.textContent = cellValue;
808
- }
809
- } else {
810
- valueCell.textContent = cellValue;
811
- }
812
-
813
- row.appendChild(valueCell);
814
- tableBody.appendChild(row);
815
- });
816
-
817
- // Populate relationships section
818
- const relationshipsSection = document.getElementById('relationshipsSection');
819
- const relationshipsContent = document.getElementById('relationshipsContent');
820
- const reverseForeignKeys = JSON.parse(button.dataset.reverseForeignKeys || '[]');
821
-
822
- // Check if we have any relationships to show
823
- const hasRelationships = (foreignKeys && foreignKeys.length > 0) || (reverseForeignKeys && reverseForeignKeys.length > 0);
824
-
825
- if (hasRelationships) {
826
- relationshipsSection.style.display = 'block';
827
- relationshipsContent.innerHTML = '';
828
-
829
- // Handle belongs_to relationships (foreign keys from this table)
830
- if (foreignKeys && foreignKeys.length > 0) {
831
- const activeRelationships = foreignKeys.filter(fk => {
832
- const columnValue = recordData[fk.column];
833
- return columnValue !== null && columnValue !== undefined && columnValue !== '';
834
- });
835
-
836
- if (activeRelationships.length > 0) {
837
- relationshipsContent.appendChild(createRelationshipSection('Belongs To', activeRelationships, recordData, 'belongs_to'));
838
- }
839
- }
840
-
841
- // Handle has_many relationships (foreign keys from other tables pointing to this table)
842
- if (reverseForeignKeys && reverseForeignKeys.length > 0) {
843
- const primaryKeyValue = recordData[Object.keys(recordData).find(key => key === 'id') || Object.keys(recordData)[0]];
844
-
845
- if (primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== '') {
846
- const hasManySection = createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue);
847
- relationshipsContent.appendChild(hasManySection);
848
-
849
- // Fetch relationship counts asynchronously
850
- fetchRelationshipCounts('<%= @table_name %>', primaryKeyValue, reverseForeignKeys, hasManySection);
851
- }
852
- }
853
-
854
- // Show message if no active relationships
855
- if (relationshipsContent.children.length === 0) {
856
- relationshipsContent.innerHTML = `
857
- <div class="text-muted small">
858
- <i class="bi bi-info-circle me-1"></i>
859
- This record has no active relationships.
860
- </div>
861
- `;
862
- }
863
- } else {
864
- relationshipsSection.style.display = 'none';
865
- }
866
- });
867
- }
868
-
869
- // Column filter functionality
870
- const columnFilters = document.querySelectorAll('.column-filter');
871
- const operatorSelects = document.querySelectorAll('.operator-select');
872
- const filterForm = document.getElementById('column-filters-form');
873
-
874
- // Add debounce function to reduce form submissions
875
- function debounce(func, wait) {
876
- let timeout;
877
- return function() {
878
- const context = this;
879
- const args = arguments;
880
- clearTimeout(timeout);
881
- timeout = setTimeout(function() {
882
- func.apply(context, args);
883
- }, wait);
884
- };
885
- }
886
-
887
- // Function to handle operator changes for IS NULL and IS NOT NULL operators
888
- function setupNullOperators() {
889
- operatorSelects.forEach(select => {
890
- // Initial setup for existing null operators
891
- if (select.value === 'is_null' || select.value === 'is_not_null') {
892
- const columnName = select.name.match(/\[(.*?)_operator\]/)[1];
893
- const inputContainer = select.closest('.filter-input-group');
894
- // Check for display field (the visible disabled field)
895
- const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
896
- if (displayField) {
897
- displayField.classList.add('disabled-filter');
898
- }
899
-
900
- // Make sure the value field properly reflects the null operator
901
- const valueField = inputContainer.querySelector(`[data-column="${columnName}"]`);
902
- if (valueField) {
903
- valueField.value = select.value;
904
- }
905
- }
906
-
907
- // Handle operator changes
908
- select.addEventListener('change', function() {
909
- const columnName = this.name.match(/\[(.*?)_operator\]/)[1];
910
- const filterForm = this.closest('form');
911
- const inputContainer = this.closest('.filter-input-group');
912
- const hiddenField = inputContainer.querySelector(`[data-column="${columnName}"]`);
913
- const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
914
- const wasNullOperator = hiddenField && (hiddenField.value === 'is_null' || hiddenField.value === 'is_not_null');
915
- const isNullOperator = this.value === 'is_null' || this.value === 'is_not_null';
916
-
917
- if (isNullOperator) {
918
- // Configure for null operator
919
- if (hiddenField) {
920
- hiddenField.value = this.value;
921
- }
922
- // Submit immediately
923
- filterForm.submit();
924
- } else if (wasNullOperator) {
925
- // Clear value when switching from null operator to regular operator
926
- if (hiddenField) {
927
- hiddenField.value = '';
928
- }
929
- }
930
- });
931
- });
932
- }
933
-
934
- // Function to submit the form
935
- const submitForm = debounce(function() {
936
- filterForm.submit();
937
- }, 500);
938
-
939
- // Initialize the null operators handling
940
- setupNullOperators();
941
-
942
- // Add event listeners to all filter inputs
943
- columnFilters.forEach(function(filter) {
944
- // For text fields use input event
945
- filter.addEventListener('input', submitForm);
946
-
947
- // For date/time fields also use change event since they have calendar/time pickers
948
- if (filter.type === 'date' || filter.type === 'datetime-local' || filter.type === 'time') {
949
- filter.addEventListener('change', submitForm);
950
- }
951
- });
952
-
953
- // Add event listeners to operator selects
954
- operatorSelects.forEach(function(select) {
955
- select.addEventListener('change', submitForm);
956
- });
957
-
958
- // Add clear button functionality if there are any filters applied
959
- const hasActiveFilters = Array.from(columnFilters).some(input => input.value);
960
-
961
- if (hasActiveFilters) {
962
- // Add a clear filters button
963
- const paginationContainer = document.querySelector('nav[aria-label="Page navigation"]') ||
964
- document.querySelector('.table-responsive');
965
-
966
- if (paginationContainer) {
967
- const clearButton = document.createElement('div');
968
- clearButton.className = 'text-center mt-3';
969
- clearButton.innerHTML = '<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-filters">' +
970
- '<i class="bi bi-x-circle me-1"></i>Clear All Filters</button>';
971
-
972
- paginationContainer.insertAdjacentHTML('afterend', clearButton.outerHTML);
973
-
974
- document.getElementById('clear-filters').addEventListener('click', function() {
975
- // Reset all input values
976
- columnFilters.forEach(filter => filter.value = '');
977
-
978
- // Reset operator selects to their default values
979
- operatorSelects.forEach(select => {
980
- // Find the first option of the select (usually the default)
981
- if (select.options.length > 0) {
982
- select.selectedIndex = 0;
983
- }
984
- });
985
-
986
- submitForm();
987
- });
988
- }
989
- }
990
-
991
- // Load Mini ERD when modal is opened
992
- const miniErdModal = document.getElementById('miniErdModal');
993
- if (miniErdModal) {
994
- let isModalLoaded = false;
995
- let erdData = null;
996
-
997
- miniErdModal.addEventListener('show.bs.modal', function(event) {
998
- const modalContent = document.getElementById('miniErdModalContent');
999
-
1000
- // Set loading state
1001
- modalContent.innerHTML = `
1002
- <div class="modal-header">
1003
- <h5 class="modal-title">Relationships for <%= @table_name %></h5>
1004
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1005
- </div>
1006
- <div class="modal-body p-0">
1007
- <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
1008
- <div class="text-center">
1009
- <div class="spinner-border text-primary mb-3" role="status">
1010
- <span class="visually-hidden">Loading...</span>
1011
- </div>
1012
- <p class="mt-2">Loading relationships diagram...</p>
1013
- <small class="text-muted">This may take a moment for tables with many relationships</small>
1014
- </div>
1015
- </div>
1016
- </div>
1017
- `;
1018
-
1019
- // Always fetch fresh data when modal is opened
1020
- fetchErdData();
1021
- });
1022
-
1023
- // Function to fetch ERD data
1024
- function fetchErdData() {
1025
- // Add cache-busting timestamp to prevent browser caching
1026
- const cacheBuster = new Date().getTime();
1027
- const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
1028
-
1029
- fetch(fetchUrl)
1030
- .then(response => {
1031
- if (!response.ok) {
1032
- throw new Error(`Server returned ${response.status} ${response.statusText}`);
1033
- }
1034
- return response.json(); // Parse as JSON instead of text
1035
- })
1036
- .then(data => {
1037
- isModalLoaded = true;
1038
- erdData = data; // Store the data
1039
- renderMiniErd(data);
1040
- })
1041
- .catch(error => {
1042
- console.error('Error loading mini ERD:', error);
1043
- showErdError(error);
1044
- });
1045
- }
1046
-
1047
- // Function to show error modal
1048
- function showErdError(error) {
1049
- const modalContent = document.getElementById('miniErdModalContent');
1050
- modalContent.innerHTML = `
1051
- <div class="modal-header">
1052
- <h5 class="modal-title">Relationships for <%= @table_name %></h5>
1053
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1054
- </div>
1055
- <div class="modal-body p-0">
1056
- <div class="alert alert-danger m-3">
1057
- <i class="bi bi-exclamation-triangle-fill me-2"></i>
1058
- <strong>Error loading relationship diagram</strong>
1059
- <p class="mt-2 mb-0">${error.message}</p>
1060
- </div>
1061
- <div class="m-3">
1062
- <p><strong>Debug Information:</strong></p>
1063
- <code>GET <%= dbviewer.mini_erd_table_path(@table_name, format: :json) %></code> failed
1064
- <p class="mt-3">
1065
- <button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
1066
- <i class="bi bi-arrow-clockwise me-1"></i> Retry
1067
- </button>
1068
- </p>
1069
- </div>
1070
- </div>
1071
- <div class="modal-footer">
1072
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
1073
- </div>
1074
- `;
1075
- }
1076
-
1077
- // Function to render the ERD with Mermaid
1078
- function renderMiniErd(data) {
1079
- const modalContent = document.getElementById('miniErdModalContent');
1080
-
1081
- // Set up the modal content with container for ERD
1082
- modalContent.innerHTML = `
1083
- <div class="modal-header">
1084
- <h5 class="modal-title">Relationships for <%= @table_name %></h5>
1085
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1086
- </div>
1087
- <div class="modal-body p-0"> <!-- Removed padding for full width -->
1088
- <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
1089
- <div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
1090
- <div class="text-center">
1091
- <div class="spinner-border text-primary mb-3" role="status">
1092
- <span class="visually-hidden">Loading...</span>
1093
- </div>
1094
- <p>Generating Relationships Diagram...</p>
1095
- </div>
1096
- </div>
1097
- <div id="mini-erd-error" class="alert alert-danger m-3 d-none">
1098
- <h5>Error generating diagram</h5>
1099
- <p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
1100
- <pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
1101
- </div>
1102
- </div>
1103
- <div id="debug-data" class="d-none m-3 border-top pt-3">
1104
- <details>
1105
- <summary>Debug Information</summary>
1106
- <div class="alert alert-info small">
1107
- <pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
1108
- </div>
1109
- </details>
1110
- </div>
1111
- </div>
1112
- <div class="modal-footer">
1113
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
1114
- <a href="<%= dbviewer.entity_relationship_diagrams_path %>" class="btn btn-primary">View Full ERD</a>
1115
- </div>
1116
- `;
1117
-
1118
- try {
1119
- const tables = data.tables || [];
1120
- const relationships = data.relationships || [];
1121
-
1122
- // Validate data before proceeding
1123
- if (!Array.isArray(tables) || !Array.isArray(relationships)) {
1124
- showDiagramError('Invalid data format', 'The relationship data is not in the expected format.');
1125
- console.error('Invalid data format received:', data);
1126
- return;
1127
- }
1128
-
1129
- console.log(`Found ${tables.length} tables and ${relationships.length} relationships`);
1130
-
1131
- // Create the ER diagram definition in Mermaid syntax
1132
- let mermaidDefinition = 'erDiagram\n';
1133
-
1134
- // Add tables to the diagram - ensure we have at least one table
1135
- if (tables.length === 0) {
1136
- mermaidDefinition += ` <%= @table_name.gsub(/[^\w]/, '_') %> {\n`;
1137
- mermaidDefinition += ` string id PK\n`;
1138
- mermaidDefinition += ` }\n`;
1139
- } else {
1140
- tables.forEach(function(table) {
1141
- const tableName = table.name;
1142
-
1143
- if (!tableName) {
1144
- console.warn('Table with no name found:', table);
1145
- return; // Skip this table
1146
- }
1147
-
1148
- // Clean table name for mermaid (remove special characters)
1149
- const cleanTableName = tableName.replace(/[^\w]/g, '_');
1150
-
1151
- // Make the current table stand out with a different visualization
1152
- if (tableName === '<%= @table_name %>') {
1153
- mermaidDefinition += ` ${cleanTableName} {\n`;
1154
- mermaidDefinition += ` string id PK\n`;
1155
- mermaidDefinition += ` }\n`;
1156
- } else {
1157
- mermaidDefinition += ` ${cleanTableName} {\n`;
1158
- mermaidDefinition += ` string id\n`;
1159
- mermaidDefinition += ` }\n`;
1160
- }
1161
- });
1162
- }
1163
-
1164
- // Add relationships
1165
- if (relationships && relationships.length > 0) {
1166
- relationships.forEach(function(rel) {
1167
- try {
1168
- // Ensure all required properties exist
1169
- if (!rel.from_table || !rel.to_table) {
1170
- console.error('Missing table in relationship:', rel);
1171
- return; // Skip this relationship
1172
- }
1173
-
1174
- // Clean up table names for mermaid (remove special characters)
1175
- const fromTable = rel.from_table.replace(/[^\w]/g, '_');
1176
- const toTable = rel.to_table.replace(/[^\w]/g, '_');
1177
- const relationLabel = rel.from_column || '';
1178
-
1179
- // Customize the display based on direction
1180
- mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
1181
- } catch (err) {
1182
- console.error('Error processing relationship:', err, rel);
1183
- }
1184
- });
1185
- } else {
1186
- // Add a note if no relationships are found
1187
- mermaidDefinition += ' %% No relationships found for this table\n';
1188
- }
1189
-
1190
- // Log the generated mermaid definition for debugging
1191
- console.log('Mermaid Definition:', mermaidDefinition);
1192
-
1193
- // Hide the loading indicator first since render might take time
1194
- document.getElementById('mini-erd-loading').style.display = 'none';
1195
-
1196
- // Render the diagram with Mermaid
1197
- mermaid.render('mini-erd-graph', mermaidDefinition)
1198
- .then(function(result) {
1199
- console.log('Mermaid rendering successful');
1200
-
1201
- // Get the container
1202
- const container = document.getElementById('mini-erd-container');
1203
-
1204
- // Insert the rendered SVG
1205
- container.innerHTML = result.svg;
1206
-
1207
- // Style the SVG element for better fit
1208
- const svgElement = container.querySelector('svg');
1209
- if (svgElement) {
1210
- // Set size attributes for the SVG
1211
- svgElement.setAttribute('width', '100%');
1212
- svgElement.setAttribute('height', '100%');
1213
- svgElement.style.minHeight = '450px';
1214
- svgElement.style.width = '100%';
1215
- svgElement.style.height = '100%';
1216
-
1217
- // Set viewBox if not present to enable proper scaling
1218
- if (!svgElement.getAttribute('viewBox')) {
1219
- const width = svgElement.getAttribute('width') || '100%';
1220
- const height = svgElement.getAttribute('height') || '100%';
1221
- svgElement.setAttribute('viewBox', `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`);
1222
- }
1223
- }
1224
-
1225
- // Apply SVG-Pan-Zoom to make the diagram interactive
1226
- try {
1227
- const svgElement = container.querySelector('svg');
1228
- if (svgElement && typeof svgPanZoom !== 'undefined') {
1229
- // Make SVG take the full container width and ensure it has valid dimensions
1230
- svgElement.setAttribute('width', '100%');
1231
- svgElement.setAttribute('height', '100%');
1232
-
1233
- // Wait for SVG to be fully rendered with proper dimensions
1234
- setTimeout(() => {
1235
- try {
1236
- // Get dimensions to ensure they're valid before initializing pan-zoom
1237
- const clientRect = svgElement.getBoundingClientRect();
1238
-
1239
- // Only initialize if we have valid dimensions
1240
- if (clientRect.width > 0 && clientRect.height > 0) {
1241
- // Initialize SVG Pan-Zoom with more robust error handling
1242
- const panZoomInstance = svgPanZoom(svgElement, {
1243
- zoomEnabled: true,
1244
- controlIconsEnabled: true,
1245
- fit: false, // Don't automatically fit on init - can cause the matrix error
1246
- center: false, // Don't automatically center - can cause the matrix error
1247
- minZoom: 0.5,
1248
- maxZoom: 2.5,
1249
- beforeZoom: function() {
1250
- // Check if the SVG is valid for zooming
1251
- return svgElement.getBoundingClientRect().width > 0 &&
1252
- svgElement.getBoundingClientRect().height > 0;
1253
- }
1254
- });
1255
-
1256
- // Store the panZoom instance for resize handling
1257
- container.panZoomInstance = panZoomInstance;
1258
-
1259
- // Manually fit and center after a slight delay
1260
- setTimeout(() => {
1261
- try {
1262
- panZoomInstance.resize();
1263
- panZoomInstance.fit();
1264
- panZoomInstance.center();
1265
- } catch(err) {
1266
- console.warn("Error during fit/center operation:", err);
1267
- }
1268
- }, 300);
1269
-
1270
- // Setup resize observer to maintain full size
1271
- const resizeObserver = new ResizeObserver(() => {
1272
- if (container.panZoomInstance) {
1273
- try {
1274
- // Reset zoom and center when container is resized
1275
- container.panZoomInstance.resize();
1276
- // Only fit and center if the element is visible with valid dimensions
1277
- if (svgElement.getBoundingClientRect().width > 0 &&
1278
- svgElement.getBoundingClientRect().height > 0) {
1279
- container.panZoomInstance.fit();
1280
- container.panZoomInstance.center();
1281
- }
1282
- } catch(err) {
1283
- console.warn("Error during resize observer callback:", err);
1284
- }
1285
- }
1286
- });
1287
-
1288
- // Observe the container for size changes
1289
- resizeObserver.observe(container);
1290
-
1291
- // Also handle manual resize on modal resize
1292
- miniErdModal.addEventListener('resize.bs.modal', function() {
1293
- if (container.panZoomInstance) {
1294
- setTimeout(() => {
1295
- try {
1296
- container.panZoomInstance.resize();
1297
- // Only fit and center if the element is visible with valid dimensions
1298
- if (svgElement.getBoundingClientRect().width > 0 &&
1299
- svgElement.getBoundingClientRect().height > 0) {
1300
- container.panZoomInstance.fit();
1301
- container.panZoomInstance.center();
1302
- }
1303
- } catch(err) {
1304
- console.warn("Error during modal resize handler:", err);
1305
- }
1306
- }, 300);
1307
- }
1308
- });
1309
- } else {
1310
- console.warn("Cannot initialize SVG-Pan-Zoom: SVG has invalid dimensions", clientRect);
1311
- }
1312
- } catch(err) {
1313
- console.warn("Error initializing SVG-Pan-Zoom:", err);
1314
- }
1315
- }, 500); // Increased delay to ensure SVG is fully rendered with proper dimensions
1316
- }
1317
- } catch (e) {
1318
- console.warn('Failed to initialize svg-pan-zoom:', e);
1319
- // Not critical, continue without pan-zoom
1320
- }
1321
-
1322
- // Add highlighting for the current table after a delay to ensure SVG is fully processed
1323
- setTimeout(function() {
1324
- try {
1325
- const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
1326
- const currentTableElement = container.querySelector(`[id*="${cleanTableName}"]`);
1327
- if (currentTableElement) {
1328
- const rect = currentTableElement.querySelector('rect');
1329
- if (rect) {
1330
- // Highlight the current table
1331
- rect.setAttribute('fill', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2c3034' : '#e2f0ff');
1332
- rect.setAttribute('stroke', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#6ea8fe' : '#0d6efd');
1333
- rect.setAttribute('stroke-width', '2');
1334
- }
1335
- }
1336
- } catch (e) {
1337
- console.error('Error highlighting current table:', e);
1338
- }
1339
- }, 100);
1340
- })
1341
- .catch(function(error) {
1342
- console.error('Error rendering mini ERD:', error);
1343
- showDiagramError(
1344
- 'Error rendering diagram',
1345
- 'There was an error rendering the relationships diagram.',
1346
- error.message || 'Unknown error'
1347
- );
1348
-
1349
- // Show debug data when there's an error
1350
- document.getElementById('debug-data').classList.remove('d-none');
1351
- });
1352
- } catch (error) {
1353
- console.error('Exception in renderMiniErd function:', error);
1354
- showDiagramError(
1355
- 'Exception generating diagram',
1356
- 'There was an exception processing the relationships diagram.',
1357
- error.message || 'Unknown error'
1358
- );
1359
-
1360
- // Show debug data when there's an error
1361
- document.getElementById('debug-data').classList.remove('d-none');
1362
- }
1363
- }
1364
-
1365
- // Function to show diagram error
1366
- function showDiagramError(title, message, details = '') {
1367
- const errorContainer = document.getElementById('mini-erd-error');
1368
- const errorMessage = document.getElementById('mini-erd-error-message');
1369
- const errorDetails = document.getElementById('mini-erd-error-details');
1370
- const loadingIndicator = document.getElementById('mini-erd-loading');
1371
-
1372
- if (loadingIndicator) {
1373
- loadingIndicator.style.display = 'none';
1374
- }
1375
-
1376
- if (errorContainer && errorMessage) {
1377
- // Set error message
1378
- errorMessage.textContent = message;
1379
-
1380
- // Set error details if provided
1381
- if (details && errorDetails) {
1382
- errorDetails.textContent = details;
1383
- errorDetails.classList.remove('d-none');
1384
- } else if (errorDetails) {
1385
- errorDetails.classList.add('d-none');
1386
- }
1387
-
1388
- // Show the error container
1389
- errorContainer.classList.remove('d-none');
1390
- }
1391
- }
1392
-
1393
- // Handle modal shown event - adjust size after the modal is fully visible
1394
- miniErdModal.addEventListener('shown.bs.modal', function(event) {
1395
- // After modal is fully shown, resize the diagram to fit
1396
- const container = document.getElementById('mini-erd-container');
1397
- if (container && container.panZoomInstance) {
1398
- setTimeout(() => {
1399
- try {
1400
- // Check if the SVG still has valid dimensions before operating on it
1401
- const svgElement = container.querySelector('svg');
1402
- if (svgElement &&
1403
- svgElement.getBoundingClientRect().width > 0 &&
1404
- svgElement.getBoundingClientRect().height > 0) {
1405
- container.panZoomInstance.resize();
1406
- container.panZoomInstance.fit();
1407
- container.panZoomInstance.center();
1408
- } else {
1409
- console.warn("Cannot perform pan-zoom operations: SVG has invalid dimensions");
1410
- }
1411
- } catch(err) {
1412
- console.warn("Error during modal shown handler:", err);
1413
- }
1414
- }, 500); // Increased delay to ensure modal is fully transitioned and SVG is rendered
1415
- }
1416
- });
1417
-
1418
- // Handle modal close to reset state for future opens
1419
- miniErdModal.addEventListener('hidden.bs.modal', function(event) {
1420
- // Reset flags and cached data to ensure fresh fetch on next open
1421
- isModalLoaded = false;
1422
- erdData = null;
1423
- console.log('Modal closed, diagram data will be refetched on next open');
1424
- });
1425
- }
1426
-
1427
- // Function to retry loading the Mini ERD
1428
- function retryLoadingMiniERD() {
1429
- console.log('Retrying loading of mini ERD');
1430
- const modalContent = document.getElementById('miniErdModalContent');
1431
-
1432
- // Set loading state again
1433
- modalContent.innerHTML = `
1434
- <div class="modal-header">
1435
- <h5 class="modal-title">Relationships for <%= @table_name %></h5>
1436
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1437
- </div>
1438
- <div class="modal-body p-0">
1439
- <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
1440
- <div class="text-center">
1441
- <div class="spinner-border text-primary mb-3" role="status">
1442
- <span class="visually-hidden">Loading...</span>
1443
- </div>
1444
- <p>Retrying to load relationships diagram...</p>
1445
- </div>
1446
- </div>
1447
- </div>
1448
- `;
1449
-
1450
- // Reset state to ensure fresh fetch
1451
- isModalLoaded = false;
1452
- erdData = null;
1453
-
1454
- // Retry fetching data
1455
- fetchErdData();
1456
- }
1457
-
1458
- // Column sorting enhancement
1459
- const sortableColumns = document.querySelectorAll('.sortable-column');
1460
- sortableColumns.forEach(column => {
1461
- const link = column.querySelector('.column-sort-link');
1462
-
1463
- // Mouse over effects
1464
- column.addEventListener('mouseenter', () => {
1465
- const sortIcon = column.querySelector('.sort-icon');
1466
- if (sortIcon && sortIcon.classList.contains('invisible')) {
1467
- sortIcon.style.visibility = 'visible';
1468
- sortIcon.style.opacity = '0.3';
1469
- }
1470
- });
1471
-
1472
- column.addEventListener('mouseleave', () => {
1473
- const sortIcon = column.querySelector('.sort-icon');
1474
- if (sortIcon && sortIcon.classList.contains('invisible')) {
1475
- sortIcon.style.visibility = 'hidden';
1476
- sortIcon.style.opacity = '0';
1477
- }
1478
- });
1479
-
1480
- // Keyboard accessibility
1481
- if (link) {
1482
- link.addEventListener('keydown', (e) => {
1483
- if (e.key === 'Enter' || e.key === ' ') {
1484
- e.preventDefault();
1485
- link.click();
1486
- }
1487
- });
1488
- }
1489
- });
1490
-
1491
- // Table fullscreen functionality
1492
- const fullscreenToggle = document.getElementById('fullscreen-toggle');
1493
- const fullscreenIcon = document.getElementById('fullscreen-icon');
1494
- const tableSection = document.getElementById('table-section');
1495
-
1496
- if (fullscreenToggle && tableSection) {
1497
- // Key for storing fullscreen state in localStorage
1498
- const fullscreenStateKey = 'dbviewer-table-fullscreen-<%= @table_name %>';
1499
-
1500
- // Function to apply fullscreen state
1501
- function applyFullscreenState(isFullscreen) {
1502
- if (isFullscreen) {
1503
- // Enter fullscreen
1504
- tableSection.classList.add('table-fullscreen');
1505
- document.body.classList.add('table-fullscreen-active');
1506
- fullscreenIcon.classList.remove('bi-fullscreen');
1507
- fullscreenIcon.classList.add('bi-fullscreen-exit');
1508
- fullscreenToggle.setAttribute('title', 'Exit fullscreen');
1509
- } else {
1510
- // Exit fullscreen
1511
- tableSection.classList.remove('table-fullscreen');
1512
- document.body.classList.remove('table-fullscreen-active');
1513
- fullscreenIcon.classList.remove('bi-fullscreen-exit');
1514
- fullscreenIcon.classList.add('bi-fullscreen');
1515
- fullscreenToggle.setAttribute('title', 'Toggle fullscreen');
1516
- }
1517
- }
1518
-
1519
- // Restore fullscreen state from localStorage on page load
1520
- try {
1521
- const savedState = localStorage.getItem(fullscreenStateKey);
1522
- if (savedState === 'true') {
1523
- applyFullscreenState(true);
1524
- }
1525
- } catch (e) {
1526
- // Handle localStorage not available (private browsing, etc.)
1527
- console.warn('Could not restore fullscreen state:', e);
1528
- }
1529
-
1530
- fullscreenToggle.addEventListener('click', function() {
1531
- const isFullscreen = tableSection.classList.contains('table-fullscreen');
1532
- const newState = !isFullscreen;
1533
-
1534
- // Apply the new state
1535
- applyFullscreenState(newState);
1536
-
1537
- // Save state to localStorage
1538
- try {
1539
- localStorage.setItem(fullscreenStateKey, newState.toString());
1540
- } catch (e) {
1541
- // Handle localStorage not available (private browsing, etc.)
1542
- console.warn('Could not save fullscreen state:', e);
1543
- }
1544
- });
1545
-
1546
- // Exit fullscreen with Escape key
1547
- document.addEventListener('keydown', function(e) {
1548
- if (e.key === 'Escape' && tableSection.classList.contains('table-fullscreen')) {
1549
- fullscreenToggle.click();
1550
- }
1551
- });
1552
- }
1553
-
1554
- // Function to copy FactoryBot code
1555
- window.copyToJson = function(button) {
1556
- try {
1557
- // Get record data from data attribute
1558
- const recordData = JSON.parse(button.dataset.recordData);
1559
-
1560
- // Generate formatted JSON string
1561
- const jsonString = JSON.stringify(recordData, null, 2);
1562
-
1563
- // Copy to clipboard
1564
- navigator.clipboard.writeText(jsonString).then(() => {
1565
- // Show a temporary success message on the button
1566
- const originalTitle = button.getAttribute('title');
1567
- button.setAttribute('title', 'Copied!');
1568
- button.classList.remove('btn-outline-secondary');
1569
- button.classList.add('btn-success');
1570
-
1571
- // Show a toast notification
1572
- if (typeof Toastify === 'function') {
1573
- Toastify({
1574
- text: `<span class="toast-icon"><i class="bi bi-clipboard-check"></i></span> JSON data copied to clipboard!`,
1575
- className: "toast-factory-bot",
1576
- duration: 3000,
1577
- gravity: "bottom",
1578
- position: "right",
1579
- escapeMarkup: false,
1580
- style: {
1581
- animation: "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s"
1582
- },
1583
- onClick: function() { /* Dismiss toast on click */ }
1584
- }).showToast();
1585
- }
1586
-
1587
- setTimeout(() => {
1588
- button.setAttribute('title', originalTitle);
1589
- button.classList.remove('btn-success');
1590
- button.classList.add('btn-outline-secondary');
1591
- }, 2000);
1592
- }).catch(err => {
1593
- console.error('Failed to copy text: ', err);
1594
-
1595
- // Show error toast
1596
- if (typeof Toastify === 'function') {
1597
- Toastify({
1598
- text: '<span class="toast-icon"><i class="bi bi-exclamation-triangle"></i></span> Failed to copy to clipboard',
1599
- className: "bg-danger",
1600
- duration: 3000,
1601
- gravity: "bottom",
1602
- position: "right",
1603
- escapeMarkup: false,
1604
- style: {
1605
- background: "linear-gradient(135deg, #dc3545, #c82333)",
1606
- animation: "slideInRight 0.3s ease-out"
1607
- }
1608
- }).showToast();
1609
- } else {
1610
- alert('Failed to copy to clipboard. See console for details.');
1611
- }
1612
- });
1613
- } catch (error) {
1614
- console.error('Error generating JSON:', error);
1615
- alert('Error generating JSON. See console for details.');
1616
- }
1617
- };
1618
-
1619
- // Helper function to create relationship sections
1620
- // Function to fetch relationship counts from API
1621
- async function fetchRelationshipCounts(tableName, recordId, relationships, hasManySection) {
1622
- try {
1623
- const response = await fetch(`/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`);
1624
- if (!response.ok) {
1625
- throw new Error(`HTTP error! status: ${response.status}`);
1626
- }
1627
-
1628
- const data = await response.json();
1629
-
1630
- // Update each count in the UI
1631
- const countSpans = hasManySection.querySelectorAll('.relationship-count');
1632
-
1633
- relationships.forEach((relationship, index) => {
1634
- const countSpan = countSpans[index];
1635
- if (countSpan) {
1636
- const relationshipData = data.relationships.find(r =>
1637
- r.table === relationship.from_table && r.foreign_key === relationship.column
1638
- );
1639
-
1640
- if (relationshipData) {
1641
- const count = relationshipData.count;
1642
- let badgeClass = 'bg-secondary';
1643
- let badgeText = `${count} record${count !== 1 ? 's' : ''}`;
1644
-
1645
- // Use different colors based on count
1646
- if (count > 0) {
1647
- badgeClass = count > 10 ? 'bg-warning' : 'bg-success';
1648
- }
1649
-
1650
- countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
1651
- } else {
1652
- // Fallback if no data found
1653
- countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
1654
-
1655
- }
1656
- }
1657
- });
1658
-
1659
- } catch (error) {
1660
- console.error('Error fetching relationship counts:', error);
1661
-
1662
- // Show error state in UI
1663
- const countSpans = hasManySection.querySelectorAll('.relationship-count');
1664
- countSpans.forEach(span => {
1665
- span.innerHTML = '<span class="badge bg-danger">Error</span>';
1666
- });
1667
- }
1668
- }
1669
-
1670
- function createRelationshipSection(title, relationships, recordData, type, primaryKeyValue = null) {
1671
- const section = document.createElement('div');
1672
- section.className = 'relationship-section mb-4';
1673
-
1674
- // Create section header
1675
- const header = document.createElement('h6');
1676
- header.className = 'mb-3';
1677
- const icon = type === 'belongs_to' ? 'bi-arrow-up-right' : 'bi-arrow-down-left';
1678
- header.innerHTML = `<i class="bi ${icon} me-2"></i>${title}`;
1679
- section.appendChild(header);
1680
-
1681
- const tableContainer = document.createElement('div');
1682
- tableContainer.className = 'table-responsive';
1683
-
1684
- const table = document.createElement('table');
1685
- table.className = 'table table-sm table-bordered';
1686
-
1687
- // Create header based on relationship type
1688
- const thead = document.createElement('thead');
1689
- if (type === 'belongs_to') {
1690
- thead.innerHTML = `
1691
- <tr>
1692
- <th width="25%">Column</th>
1693
- <th width="25%">Value</th>
1694
- <th width="25%">References</th>
1695
- <th width="25%">Action</th>
1696
- </tr>
1697
- `;
1698
- } else {
1699
- thead.innerHTML = `
1700
- <tr>
1701
- <th width="30%">Related Table</th>
1702
- <th width="25%">Foreign Key</th>
1703
- <th width="20%">Count</th>
1704
- <th width="25%">Action</th>
1705
- </tr>
1706
- `;
1707
- }
1708
- table.appendChild(thead);
1709
-
1710
- // Create body
1711
- const tbody = document.createElement('tbody');
1712
-
1713
- relationships.forEach(fk => {
1714
- const row = document.createElement('tr');
1715
-
1716
- if (type === 'belongs_to') {
1717
- const columnValue = recordData[fk.column];
1718
- row.innerHTML = `
1719
- <td class="fw-medium">${fk.column}</td>
1720
- <td><code>${columnValue}</code></td>
1721
- <td>
1722
- <span class="text-muted">${fk.to_table}.</span><strong>${fk.primary_key}</strong>
1723
- </td>
1724
- <td>
1725
- <a href="/dbviewer/tables/${fk.to_table}?column_filters[${fk.primary_key}]=${encodeURIComponent(columnValue)}"
1726
- class="btn btn-sm btn-outline-primary"
1727
- title="View referenced record in ${fk.to_table}">
1728
- <i class="bi bi-arrow-right me-1"></i>View
1729
- </a>
1730
- </td>
1731
- `;
1732
- } else {
1733
- // For has_many relationships
1734
- row.innerHTML = `
1735
- <td class="fw-medium">${fk.from_table}</td>
1736
- <td>
1737
- <span class="text-muted">${fk.from_table}.</span><strong>${fk.column}</strong>
1738
- </td>
1739
- <td>
1740
- <span class="relationship-count">
1741
- <span class="badge bg-secondary">
1742
- <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
1743
- Loading...
1744
- </span>
1745
- </span>
1746
- </td>
1747
- <td>
1748
- <a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
1749
- class="btn btn-sm btn-outline-success"
1750
- title="View all ${fk.from_table} records that reference this record">
1751
- <i class="bi bi-list me-1"></i>View Related
1752
- </a>
1753
- </td>
1754
- `;
1755
- }
1756
-
1757
- tbody.appendChild(row);
1758
- });
1759
-
1760
- table.appendChild(tbody);
1761
- tableContainer.appendChild(table);
1762
- section.appendChild(tableContainer);
1763
-
1764
- return section;
1765
- }
1766
- });
1767
- </script>
1768
-
1769
207
  <!-- Floating Creation Filter - Only visible on desktop and on table details page -->
1770
208
  <% if has_timestamp_column?(@table_name) %>
1771
209
  <div class="floating-creation-filter d-none d-lg-block">
@@ -1909,869 +347,6 @@
1909
347
  </form>
1910
348
  </div>
1911
349
  </div>
1912
-
1913
- <style>
1914
- /* Floating creation filter button */
1915
- .floating-creation-filter {
1916
- position: fixed;
1917
- bottom: 30px;
1918
- right: 30px;
1919
- z-index: 1050;
1920
- }
1921
-
1922
- .floating-filter-btn {
1923
- width: 60px;
1924
- height: 60px;
1925
- border-radius: 50%;
1926
- display: flex;
1927
- align-items: center;
1928
- justify-content: center;
1929
- font-size: 1.2rem;
1930
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1931
- border: none;
1932
- position: relative;
1933
- background: var(--bs-primary);
1934
- color: white;
1935
- box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.4);
1936
- }
1937
-
1938
- .floating-filter-btn:hover {
1939
- transform: translateY(-3px) scale(1.05);
1940
- box-shadow: 0 8px 25px rgba(var(--bs-primary-rgb), 0.5) !important;
1941
- background: var(--bs-primary) !important;
1942
- color: white !important;
1943
- }
1944
-
1945
- .floating-filter-btn:active {
1946
- transform: translateY(-1px) scale(1.02);
1947
- transition: all 0.1s ease;
1948
- }
1949
-
1950
- .floating-filter-btn:focus {
1951
- outline: 2px solid rgba(var(--bs-primary-rgb), 0.5);
1952
- outline-offset: 2px;
1953
- }
1954
-
1955
- /* Badge for active filter indicator */
1956
- .floating-filter-btn .badge {
1957
- font-size: 0.6rem;
1958
- width: 18px;
1959
- height: 18px;
1960
- display: flex;
1961
- align-items: center;
1962
- justify-content: center;
1963
- background: var(--bs-success) !important;
1964
- animation: pulse 2s infinite;
1965
- }
1966
-
1967
- @keyframes pulse {
1968
- 0% { transform: scale(1); }
1969
- 50% { transform: scale(1.1); }
1970
- 100% { transform: scale(1); }
1971
- }
1972
-
1973
- /* Better datetime input styling for the floating filter */
1974
- #creationFilterOffcanvas .form-control {
1975
- border-radius: 6px;
1976
- border: 1px solid var(--bs-border-color);
1977
- background-color: var(--bs-body-bg);
1978
- color: var(--bs-body-color);
1979
- transition: all 0.15s ease-in-out;
1980
- }
1981
-
1982
- #creationFilterOffcanvas .form-control:focus {
1983
- border-color: var(--bs-primary);
1984
- box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1985
- background-color: var(--bs-body-bg);
1986
- }
1987
-
1988
- /* Offcanvas enhancements */
1989
- #creationFilterOffcanvas {
1990
- backdrop-filter: blur(10px);
1991
- }
1992
-
1993
- #creationFilterOffcanvas .offcanvas-header {
1994
- background: var(--bs-body-bg);
1995
- border-bottom: 1px solid var(--bs-border-color);
1996
- padding: 1.25rem;
1997
- }
1998
-
1999
- #creationFilterOffcanvas .offcanvas-body {
2000
- background: var(--bs-body-bg);
2001
- padding: 1.25rem;
2002
- }
2003
-
2004
- #creationFilterOffcanvas .offcanvas-title {
2005
- color: var(--bs-body-color);
2006
- font-weight: 600;
2007
- }
2008
-
2009
- /* Dark mode specific enhancements */
2010
- [data-bs-theme="dark"] .floating-filter-btn {
2011
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.2);
2012
- }
2013
-
2014
- [data-bs-theme="dark"] .floating-filter-btn:hover {
2015
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.3) !important;
2016
- }
2017
-
2018
- [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-header {
2019
- background: var(--bs-dark);
2020
- border-bottom-color: var(--bs-border-color-translucent);
2021
- }
2022
-
2023
- [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-body {
2024
- background: var(--bs-dark);
2025
- }
2026
-
2027
- [data-bs-theme="dark"] #creationFilterOffcanvas .form-control {
2028
- background-color: var(--bs-body-bg);
2029
- border-color: var(--bs-border-color-translucent);
2030
- }
2031
-
2032
- [data-bs-theme="dark"] #creationFilterOffcanvas .form-control:focus {
2033
- background-color: var(--bs-body-bg);
2034
- border-color: var(--bs-primary);
2035
- }
2036
-
2037
- /* Date range picker styling */
2038
- #floatingCreationFilterRange {
2039
- cursor: pointer;
2040
- background-color: var(--bs-body-bg);
2041
- color: var(--bs-body-color);
2042
- border: 1px solid var(--bs-border-color);
2043
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
2044
- }
2045
-
2046
- #floatingCreationFilterRange:focus {
2047
- border-color: var(--bs-primary);
2048
- box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2049
- background-color: var(--bs-body-bg);
2050
- }
2051
-
2052
- #floatingCreationFilterRange::placeholder {
2053
- color: var(--bs-secondary-color);
2054
- opacity: 0.7;
2055
- }
2056
-
2057
- /* Enhanced dark mode support for input */
2058
- [data-bs-theme="dark"] #floatingCreationFilterRange {
2059
- background-color: var(--bs-body-bg);
2060
- border-color: var(--bs-border-color-translucent);
2061
- color: var(--bs-body-color);
2062
- }
2063
-
2064
- [data-bs-theme="dark"] #floatingCreationFilterRange:focus {
2065
- background-color: var(--bs-body-bg);
2066
- border-color: var(--bs-primary);
2067
- box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2068
- }
2069
-
2070
- /* Preset buttons styling */
2071
- .preset-btn {
2072
- font-size: 0.8rem;
2073
- padding: 0.35rem 0.75rem;
2074
- text-align: left;
2075
- justify-content: flex-start;
2076
- border: 1px solid var(--bs-border-color);
2077
- background-color: var(--bs-body-bg);
2078
- color: var(--bs-body-color);
2079
- transition: all 0.2s ease;
2080
- position: relative;
2081
- overflow: hidden;
2082
- }
2083
-
2084
- .preset-btn::before {
2085
- content: '';
2086
- position: absolute;
2087
- top: 0;
2088
- left: -100%;
2089
- width: 100%;
2090
- height: 100%;
2091
- background: linear-gradient(90deg, transparent, rgba(var(--bs-primary-rgb), 0.1), transparent);
2092
- transition: left 0.3s ease;
2093
- }
2094
-
2095
- .preset-btn:hover {
2096
- background-color: var(--bs-primary);
2097
- color: white;
2098
- border-color: var(--bs-primary);
2099
- transform: translateY(-1px);
2100
- box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
2101
- }
2102
-
2103
- .preset-btn:hover::before {
2104
- left: 100%;
2105
- }
2106
-
2107
- .preset-btn:active {
2108
- transform: translateY(0);
2109
- box-shadow: 0 1px 2px rgba(var(--bs-primary-rgb), 0.2);
2110
- }
2111
-
2112
- .preset-btn i {
2113
- opacity: 0.8;
2114
- transition: opacity 0.2s ease;
2115
- }
2116
-
2117
- .preset-btn:hover i {
2118
- opacity: 1;
2119
- }
2120
-
2121
- /* Dark mode enhancements for preset buttons */
2122
- [data-bs-theme="dark"] .preset-btn {
2123
- background-color: var(--bs-dark);
2124
- border-color: var(--bs-border-color-translucent);
2125
- color: var(--bs-body-color);
2126
- }
2127
-
2128
- [data-bs-theme="dark"] .preset-btn:hover {
2129
- background-color: var(--bs-primary);
2130
- color: white;
2131
- border-color: var(--bs-primary);
2132
- box-shadow: 0 2px 8px rgba(var(--bs-primary-rgb), 0.3);
2133
- }
2134
-
2135
- /* Flatpickr theme adjustments */
2136
- .flatpickr-calendar {
2137
- border-radius: 8px;
2138
- box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
2139
- border: 1px solid var(--bs-border-color);
2140
- background: var(--bs-body-bg);
2141
- font-family: var(--bs-body-font-family);
2142
- }
2143
-
2144
- .flatpickr-months {
2145
- background: var(--bs-body-bg);
2146
- border-bottom: 1px solid var(--bs-border-color);
2147
- border-radius: 8px 8px 0 0;
2148
- }
2149
-
2150
- .flatpickr-month {
2151
- color: var(--bs-body-color);
2152
- fill: var(--bs-body-color);
2153
- }
2154
-
2155
- .flatpickr-current-month {
2156
- color: var(--bs-body-color);
2157
- }
2158
-
2159
- .flatpickr-current-month .flatpickr-monthDropdown-month {
2160
- background: var(--bs-body-bg);
2161
- color: var(--bs-body-color);
2162
- }
2163
-
2164
- .flatpickr-weekdays {
2165
- background: var(--bs-body-bg);
2166
- }
2167
-
2168
- .flatpickr-weekday {
2169
- color: var(--bs-secondary-color);
2170
- font-weight: 600;
2171
- font-size: 0.75rem;
2172
- }
2173
-
2174
- .flatpickr-day {
2175
- color: var(--bs-body-color);
2176
- border-radius: 4px;
2177
- transition: all 0.2s ease;
2178
- }
2179
-
2180
- .flatpickr-day:hover {
2181
- background: var(--bs-primary-bg-subtle);
2182
- color: var(--bs-primary);
2183
- border-color: var(--bs-primary-border-subtle);
2184
- }
2185
-
2186
- .flatpickr-day.selected {
2187
- background: var(--bs-primary);
2188
- color: white;
2189
- border-color: var(--bs-primary);
2190
- box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.3);
2191
- }
2192
-
2193
- .flatpickr-day.selected:hover {
2194
- background: var(--bs-primary);
2195
- color: white;
2196
- }
2197
-
2198
- .flatpickr-day.inRange {
2199
- background: var(--bs-primary-bg-subtle);
2200
- color: var(--bs-primary);
2201
- border-color: transparent;
2202
- }
2203
-
2204
- .flatpickr-day.startRange {
2205
- background: var(--bs-primary);
2206
- color: white;
2207
- border-radius: 4px 0 0 4px;
2208
- }
2209
-
2210
- .flatpickr-day.endRange {
2211
- background: var(--bs-primary);
2212
- color: white;
2213
- border-radius: 0 4px 4px 0;
2214
- }
2215
-
2216
- .flatpickr-day.startRange.endRange {
2217
- border-radius: 4px;
2218
- }
2219
-
2220
- .flatpickr-day.today {
2221
- border-color: var(--bs-primary);
2222
- color: var(--bs-primary);
2223
- font-weight: 600;
2224
- }
2225
-
2226
- .flatpickr-day.today:hover {
2227
- background: var(--bs-primary);
2228
- color: white;
2229
- }
2230
-
2231
- .flatpickr-day.disabled {
2232
- color: var(--bs-secondary-color);
2233
- opacity: 0.5;
2234
- }
2235
-
2236
- .flatpickr-time {
2237
- background: var(--bs-body-bg);
2238
- border-radius: 0 0 8px 8px;
2239
- }
2240
-
2241
- .flatpickr-time input {
2242
- background: var(--bs-body-bg);
2243
- color: var(--bs-body-color);
2244
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
2245
- }
2246
-
2247
- .flatpickr-time input:focus {
2248
- border-color: var(--bs-primary);
2249
- box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2250
- outline: 0;
2251
- }
2252
-
2253
- .flatpickr-time .flatpickr-time-separator {
2254
- color: var(--bs-body-color);
2255
- }
2256
-
2257
- .flatpickr-prev-month,
2258
- .flatpickr-next-month {
2259
- color: var(--bs-body-color);
2260
- fill: var(--bs-body-color);
2261
- transition: color 0.2s ease;
2262
- }
2263
-
2264
- .flatpickr-prev-month:hover,
2265
- .flatpickr-next-month:hover {
2266
- color: var(--bs-primary);
2267
- fill: var(--bs-primary);
2268
- }
2269
-
2270
- /* Dark mode specific enhancements */
2271
- [data-bs-theme="dark"] .flatpickr-calendar {
2272
- background: var(--bs-dark);
2273
- border-color: var(--bs-border-color-translucent);
2274
- box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
2275
- }
2276
-
2277
- [data-bs-theme="dark"] .flatpickr-months {
2278
- background: var(--bs-dark);
2279
- border-bottom-color: var(--bs-border-color-translucent);
2280
- }
2281
-
2282
- [data-bs-theme="dark"] .flatpickr-weekdays {
2283
- background: var(--bs-dark);
2284
- }
2285
-
2286
- /* Enhanced dark mode day styling for better contrast */
2287
- [data-bs-theme="dark"] .flatpickr-day {
2288
- color: #e9ecef !important;
2289
- background: transparent;
2290
- border: 1px solid transparent;
2291
- }
2292
-
2293
- [data-bs-theme="dark"] .flatpickr-day:hover {
2294
- background: rgba(var(--bs-primary-rgb), 0.25) !important;
2295
- color: #ffffff !important;
2296
- border-color: rgba(var(--bs-primary-rgb), 0.4);
2297
- }
2298
-
2299
- [data-bs-theme="dark"] .flatpickr-day.inRange {
2300
- background: rgba(var(--bs-primary-rgb), 0.2) !important;
2301
- color: #ffffff !important;
2302
- border-color: transparent;
2303
- }
2304
-
2305
- [data-bs-theme="dark"] .flatpickr-day.selected {
2306
- background: var(--bs-primary) !important;
2307
- color: #ffffff !important;
2308
- border-color: var(--bs-primary);
2309
- box-shadow: 0 2px 6px rgba(var(--bs-primary-rgb), 0.4);
2310
- }
2311
-
2312
- [data-bs-theme="dark"] .flatpickr-day.selected:hover {
2313
- background: var(--bs-primary) !important;
2314
- color: #ffffff !important;
2315
- }
2316
-
2317
- [data-bs-theme="dark"] .flatpickr-day.startRange {
2318
- background: var(--bs-primary) !important;
2319
- color: #ffffff !important;
2320
- border-radius: 4px 0 0 4px;
2321
- }
2322
-
2323
- [data-bs-theme="dark"] .flatpickr-day.endRange {
2324
- background: var(--bs-primary) !important;
2325
- color: #ffffff !important;
2326
- border-radius: 0 4px 4px 0;
2327
- }
2328
-
2329
- [data-bs-theme="dark"] .flatpickr-day.startRange.endRange {
2330
- border-radius: 4px;
2331
- }
2332
-
2333
- [data-bs-theme="dark"] .flatpickr-day.today {
2334
- border-color: var(--bs-primary) !important;
2335
- color: var(--bs-primary) !important;
2336
- font-weight: 600;
2337
- background: rgba(var(--bs-primary-rgb), 0.1);
2338
- }
2339
-
2340
- [data-bs-theme="dark"] .flatpickr-day.today:hover {
2341
- background: var(--bs-primary) !important;
2342
- color: #ffffff !important;
2343
- }
2344
-
2345
- [data-bs-theme="dark"] .flatpickr-day.disabled {
2346
- color: #6c757d !important;
2347
- opacity: 0.4;
2348
- background: transparent !important;
2349
- }
2350
-
2351
- /* Dark mode other day states */
2352
- [data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
2353
- [data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
2354
- color: #6c757d !important;
2355
- opacity: 0.6;
2356
- }
2357
-
2358
- [data-bs-theme="dark"] .flatpickr-day.nextMonthDay:hover,
2359
- [data-bs-theme="dark"] .flatpickr-day.prevMonthDay:hover {
2360
- background: rgba(var(--bs-primary-rgb), 0.15) !important;
2361
- color: #adb5bd !important;
2362
- }
2363
-
2364
- [data-bs-theme="dark"] .flatpickr-time {
2365
- background: var(--bs-dark);
2366
- border-top-color: var(--bs-border-color-translucent);
2367
- }
2368
-
2369
- [data-bs-theme="dark"] .flatpickr-time input {
2370
- background: var(--bs-body-bg);
2371
- border-color: var(--bs-border-color-translucent);
2372
- color: #e9ecef !important;
2373
- }
2374
-
2375
- .flatpickr-calendar.hasTime .flatpickr-time {
2376
- border-top: var(--bs-border-color-translucent);
2377
- }
2378
-
2379
- .flatpickr-next-month {
2380
- color: var(--bs-body-color);
2381
- fill: var(--bs-body-color);
2382
- }
2383
-
2384
- span.flatpickr-weekday {
2385
- color: var(--bs-secondary-color);
2386
- font-weight: 600;
2387
- font-size: 0.75rem;
2388
- }
2389
-
2390
- [data-bs-theme="dark"] .flatpickr-time input:focus {
2391
- border-color: var(--bs-primary);
2392
- background: var(--bs-body-bg);
2393
- color: #ffffff !important;
2394
- }
2395
-
2396
- [data-bs-theme="dark"] .flatpickr-time .flatpickr-time-separator {
2397
- color: #e9ecef !important;
2398
- }
2399
-
2400
- /* Better focus states */
2401
- .flatpickr-day:focus {
2402
- outline: 2px solid var(--bs-primary);
2403
- outline-offset: -2px;
2404
- z-index: 10;
2405
- }
2406
-
2407
- /* Animation for calendar appearance */
2408
- .flatpickr-calendar.open {
2409
- animation: flatpickr-slideDown 0.2s ease-out;
2410
- }
2411
-
2412
- @keyframes flatpickr-slideDown {
2413
- from {
2414
- opacity: 0;
2415
- transform: translateY(-10px) scale(0.98);
2416
- }
2417
- to {
2418
- opacity: 1;
2419
- transform: translateY(0) scale(1);
2420
- }
2421
- }
2422
-
2423
- /* Small alert styling */
2424
- .alert-sm {
2425
- padding: 0.5rem;
2426
- font-size: 0.875rem;
2427
- }
2428
-
2429
- /* Responsive adjustments - hide on smaller screens */
2430
- @media (max-width: 991.98px) {
2431
- .floating-creation-filter {
2432
- display: none !important;
2433
- }
2434
- }
2435
-
2436
- /* Flatpickr positioning and responsive adjustments */
2437
- .flatpickr-calendar.arrowTop:before,
2438
- .flatpickr-calendar.arrowTop:after {
2439
- border-bottom-color: var(--bs-border-color);
2440
- }
2441
-
2442
- .flatpickr-calendar.arrowBottom:before,
2443
- .flatpickr-calendar.arrowBottom:after {
2444
- border-top-color: var(--bs-border-color);
2445
- }
2446
-
2447
- [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:before,
2448
- [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:after {
2449
- border-bottom-color: var(--bs-border-color-translucent);
2450
- }
2451
-
2452
- [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:before,
2453
- [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:after {
2454
- border-top-color: var(--bs-border-color-translucent);
2455
- }
2456
-
2457
- /* Ensure calendar stays within viewport on mobile */
2458
- @media (max-width: 576px) {
2459
- .flatpickr-calendar {
2460
- max-width: calc(100vw - 20px);
2461
- font-size: 14px;
2462
- }
2463
-
2464
- .flatpickr-day {
2465
- height: 35px;
2466
- line-height: 35px;
2467
- }
2468
-
2469
- .flatpickr-time input {
2470
- font-size: 14px;
2471
- }
2472
- }
2473
-
2474
- /* Improved accessibility */
2475
- .flatpickr-calendar {
2476
- font-family: inherit;
2477
- }
2478
-
2479
- .flatpickr-day[aria-label] {
2480
- position: relative;
2481
- }
2482
-
2483
- /* Smooth scroll behavior for time inputs */
2484
- .flatpickr-time input {
2485
- scroll-behavior: smooth;
2486
- }
2487
-
2488
- /* Enhanced visual feedback for interactive elements */
2489
- .flatpickr-prev-month,
2490
- .flatpickr-next-month {
2491
- border-radius: 4px;
2492
- padding: 4px;
2493
- margin: 2px;
2494
- }
2495
-
2496
- .flatpickr-prev-month:hover,
2497
- .flatpickr-next-month:hover {
2498
- background: rgba(var(--bs-primary-rgb), 0.1);
2499
- }
2500
-
2501
- /* Relationship count styling */
2502
- .relationship-count .badge {
2503
- min-width: 80px;
2504
- display: inline-flex;
2505
- align-items: center;
2506
- justify-content: center;
2507
- }
2508
-
2509
- .relationship-count .spinner-border-sm {
2510
- width: 0.875rem;
2511
- height: 0.875rem;
2512
- }
2513
- </style>
2514
-
2515
- <script>
2516
- document.addEventListener('DOMContentLoaded', function() {
2517
- // Initialize Flatpickr date range picker
2518
- const dateRangeInput = document.getElementById('floatingCreationFilterRange');
2519
- const startHidden = document.getElementById('creation_filter_start');
2520
- const endHidden = document.getElementById('creation_filter_end');
2521
-
2522
- if (dateRangeInput && typeof flatpickr !== 'undefined') {
2523
- console.log('Flatpickr library loaded, initializing date range picker');
2524
- // Store the Flatpickr instance in a variable accessible to all handlers
2525
- let fp;
2526
-
2527
- // Function to initialize Flatpickr
2528
- function initializeFlatpickr(theme) {
2529
- // Determine theme based on current document theme or passed parameter
2530
- const currentTheme = theme || (document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
2531
-
2532
- const config = {
2533
- mode: 'range',
2534
- enableTime: true,
2535
- dateFormat: 'Y-m-d H:i',
2536
- time_24hr: true,
2537
- allowInput: false,
2538
- clickOpens: true,
2539
- theme: currentTheme,
2540
- animate: true,
2541
- position: 'auto',
2542
- static: false,
2543
- appendTo: document.body, // Ensure it renders above other elements
2544
- locale: {
2545
- rangeSeparator: ' to ',
2546
- weekdays: {
2547
- shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
2548
- longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
2549
- },
2550
- months: {
2551
- shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
2552
- longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
2553
- }
2554
- },
2555
- onOpen: function(selectedDates, dateStr, instance) {
2556
- // Add a slight delay to apply theme-specific styling after calendar opens
2557
- setTimeout(() => {
2558
- const calendar = instance.calendarContainer;
2559
- if (calendar) {
2560
- // Apply theme-specific class for additional styling control
2561
- calendar.classList.add(`flatpickr-${currentTheme}`);
2562
-
2563
- // Ensure proper z-index for offcanvas overlay
2564
- calendar.style.zIndex = '1070';
2565
-
2566
- // Add elegant entrance animation
2567
- calendar.classList.add('open');
2568
- }
2569
- }, 10);
2570
- },
2571
- onClose: function(selectedDates, dateStr, instance) {
2572
- const calendar = instance.calendarContainer;
2573
- if (calendar) {
2574
- calendar.classList.remove('open');
2575
- }
2576
- },
2577
- onChange: function(selectedDates, dateStr, instance) {
2578
- console.log('Date range changed:', selectedDates);
2579
-
2580
- if (selectedDates.length === 2) {
2581
- // Format dates for hidden inputs (Rails expects ISO format)
2582
- startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2583
- endHidden.value = selectedDates[1].toISOString().slice(0, 16);
2584
-
2585
- // Update display with elegant formatting
2586
- const formatOptions = {
2587
- year: 'numeric',
2588
- month: 'short',
2589
- day: 'numeric',
2590
- hour: '2-digit',
2591
- minute: '2-digit',
2592
- hour12: false
2593
- };
2594
-
2595
- const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2596
- const endFormatted = selectedDates[1].toLocaleDateString('en-US', formatOptions);
2597
- dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
2598
-
2599
- } else if (selectedDates.length === 1) {
2600
- startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2601
- endHidden.value = '';
2602
-
2603
- const formatOptions = {
2604
- year: 'numeric',
2605
- month: 'short',
2606
- day: 'numeric',
2607
- hour: '2-digit',
2608
- minute: '2-digit',
2609
- hour12: false
2610
- };
2611
-
2612
- const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2613
- dateRangeInput.value = `${startFormatted} (select end date)`;
2614
-
2615
- } else {
2616
- startHidden.value = '';
2617
- endHidden.value = '';
2618
- dateRangeInput.value = '';
2619
- }
2620
- }
2621
- };
2622
-
2623
- return flatpickr(dateRangeInput, config);
2624
- }
2625
-
2626
- // Initialize date range picker
2627
- fp = initializeFlatpickr();
2628
-
2629
- // Set initial values if they exist
2630
- if (startHidden.value || endHidden.value) {
2631
- const dates = [];
2632
- if (startHidden.value) {
2633
- dates.push(new Date(startHidden.value));
2634
- }
2635
- if (endHidden.value) {
2636
- dates.push(new Date(endHidden.value));
2637
- }
2638
- fp.setDate(dates);
2639
- }
2640
-
2641
- // Preset button functionality
2642
- const presetButtons = document.querySelectorAll('.preset-btn');
2643
- presetButtons.forEach(button => {
2644
- button.addEventListener('click', function(event) {
2645
- event.preventDefault(); // Prevent any form submission
2646
-
2647
- const preset = this.getAttribute('data-preset');
2648
- const now = new Date();
2649
- let startDate, endDate;
2650
-
2651
- console.log('Preset button clicked:', preset); // Debug log
2652
-
2653
- switch (preset) {
2654
- case 'lastminute':
2655
- startDate = new Date(now);
2656
- startDate.setMinutes(startDate.getMinutes() - 1);
2657
- endDate = new Date(now);
2658
- break;
2659
- case 'last5minutes':
2660
- startDate = new Date(now);
2661
- startDate.setMinutes(startDate.getMinutes() - 5);
2662
- endDate = new Date(now);
2663
- break;
2664
- case 'today':
2665
- startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
2666
- endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
2667
- break;
2668
- case 'yesterday':
2669
- const yesterday = new Date(now);
2670
- yesterday.setDate(yesterday.getDate() - 1);
2671
- startDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0);
2672
- endDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59);
2673
- break;
2674
- case 'last7days':
2675
- startDate = new Date(now);
2676
- startDate.setDate(startDate.getDate() - 7);
2677
- startDate.setHours(0, 0, 0, 0);
2678
- endDate = new Date(now);
2679
- endDate.setHours(23, 59, 59, 999);
2680
- break;
2681
- case 'last30days':
2682
- startDate = new Date(now);
2683
- startDate.setDate(startDate.getDate() - 30);
2684
- startDate.setHours(0, 0, 0, 0);
2685
- endDate = new Date(now);
2686
- endDate.setHours(23, 59, 59, 999);
2687
- break;
2688
- case 'thismonth':
2689
- startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
2690
- endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
2691
- break;
2692
- }
2693
-
2694
- if (startDate && endDate && fp) {
2695
- console.log('Setting dates:', startDate, endDate); // Debug log
2696
- fp.setDate([startDate, endDate]);
2697
-
2698
- // Also update the hidden inputs directly as a fallback
2699
- startHidden.value = startDate.toISOString().slice(0, 16);
2700
- endHidden.value = endDate.toISOString().slice(0, 16);
2701
-
2702
- // Update the display value
2703
- const formattedStart = startDate.toLocaleDateString() + ' ' + startDate.toLocaleTimeString();
2704
- const formattedEnd = endDate.toLocaleDateString() + ' ' + endDate.toLocaleTimeString();
2705
- dateRangeInput.value = formattedStart + ' to ' + formattedEnd;
2706
- } else {
2707
- console.error('Failed to set dates - startDate:', startDate, 'endDate:', endDate, 'fp:', fp);
2708
- }
2709
- });
2710
- });
2711
-
2712
- // Listen for theme changes and update Flatpickr theme
2713
- document.addEventListener('dbviewerThemeChanged', function(e) {
2714
- const newTheme = e.detail.theme === 'dark' ? 'dark' : 'light';
2715
- console.log('Theme changed to:', newTheme);
2716
-
2717
- // Destroy and recreate with new theme
2718
- if (fp) {
2719
- const currentDates = fp.selectedDates;
2720
- fp.destroy();
2721
- fp = initializeFlatpickr(newTheme);
2722
-
2723
- // Restore previous values if they existed
2724
- if (currentDates && currentDates.length > 0) {
2725
- fp.setDate(currentDates);
2726
- }
2727
- }
2728
- });
2729
-
2730
- // Also listen for direct data-bs-theme attribute changes using MutationObserver
2731
- const themeObserver = new MutationObserver(function(mutations) {
2732
- mutations.forEach(function(mutation) {
2733
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') {
2734
- const newTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light';
2735
- console.log('Theme attribute changed to:', newTheme);
2736
-
2737
- if (fp) {
2738
- const currentDates = fp.selectedDates;
2739
- fp.destroy();
2740
- fp = initializeFlatpickr(newTheme);
2741
-
2742
- // Restore previous values if they existed
2743
- if (currentDates && currentDates.length > 0) {
2744
- fp.setDate(currentDates);
2745
- }
2746
- }
2747
- }
2748
- });
2749
- });
2750
-
2751
- // Start observing theme changes
2752
- themeObserver.observe(document.documentElement, {
2753
- attributes: true,
2754
- attributeFilter: ['data-bs-theme']
2755
- });
2756
- } else {
2757
- console.error('Date range picker initialization failed:', {
2758
- dateRangeInput: !!dateRangeInput,
2759
- flatpickr: typeof flatpickr !== 'undefined'
2760
- });
2761
- }
2762
-
2763
- // Close offcanvas after form submission
2764
- const form = document.getElementById('floatingCreationFilterForm');
2765
- if (form) {
2766
- form.addEventListener('submit', function() {
2767
- const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('creationFilterOffcanvas'));
2768
- if (offcanvas) {
2769
- setTimeout(() => {
2770
- offcanvas.hide();
2771
- }, 100);
2772
- }
2773
- });
2774
- }
2775
- });
2776
- </script>
2777
350
  <% end %>
351
+ <input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>">
352
+ <input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">