dbviewer 0.5.2 → 0.5.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -0
  3. data/app/controllers/concerns/dbviewer/database_operations.rb +11 -19
  4. data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +84 -0
  5. data/app/controllers/dbviewer/api/queries_controller.rb +1 -1
  6. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +5 -6
  7. data/app/controllers/dbviewer/logs_controller.rb +1 -1
  8. data/app/controllers/dbviewer/tables_controller.rb +2 -8
  9. data/app/helpers/dbviewer/application_helper.rb +1 -1
  10. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +217 -100
  11. data/app/views/dbviewer/tables/show.html.erb +278 -404
  12. data/config/routes.rb +7 -0
  13. data/lib/dbviewer/database/cache_manager.rb +78 -0
  14. data/lib/dbviewer/database/dynamic_model_factory.rb +62 -0
  15. data/lib/dbviewer/database/manager.rb +204 -0
  16. data/lib/dbviewer/database/metadata_manager.rb +129 -0
  17. data/lib/dbviewer/datatable/query_operations.rb +330 -0
  18. data/lib/dbviewer/datatable/query_params.rb +41 -0
  19. data/lib/dbviewer/engine.rb +1 -1
  20. data/lib/dbviewer/query/analyzer.rb +250 -0
  21. data/lib/dbviewer/query/collection.rb +39 -0
  22. data/lib/dbviewer/query/executor.rb +93 -0
  23. data/lib/dbviewer/query/logger.rb +108 -0
  24. data/lib/dbviewer/query/parser.rb +56 -0
  25. data/lib/dbviewer/storage/file_storage.rb +0 -3
  26. data/lib/dbviewer/version.rb +1 -1
  27. data/lib/dbviewer.rb +24 -7
  28. metadata +14 -14
  29. data/lib/dbviewer/cache_manager.rb +0 -78
  30. data/lib/dbviewer/database_manager.rb +0 -249
  31. data/lib/dbviewer/dynamic_model_factory.rb +0 -60
  32. data/lib/dbviewer/error_handler.rb +0 -18
  33. data/lib/dbviewer/logger.rb +0 -77
  34. data/lib/dbviewer/query_analyzer.rb +0 -239
  35. data/lib/dbviewer/query_collection.rb +0 -37
  36. data/lib/dbviewer/query_executor.rb +0 -91
  37. data/lib/dbviewer/query_parser.rb +0 -53
  38. data/lib/dbviewer/table_metadata_manager.rb +0 -136
  39. data/lib/dbviewer/table_query_operations.rb +0 -621
  40. data/lib/dbviewer/table_query_params.rb +0 -39
@@ -318,6 +318,9 @@
318
318
  <h1>Table: <%= @table_name %></h1>
319
319
  </div>
320
320
  <div class="d-flex gap-2">
321
+ <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#tableStructureModal">
322
+ <i class="bi bi-table me-1"></i> Table Structure
323
+ </button>
321
324
  <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
322
325
  <i class="bi bi-diagram-3 me-1"></i> View Relationships
323
326
  </button>
@@ -442,59 +445,6 @@
442
445
  </div>
443
446
  </div>
444
447
 
445
- <!-- Two-column layout for Timeline and Structure -->
446
- <div class="row two-column-layout">
447
- <!-- Timeline Column -->
448
- <div class="col-md-6 mb-4">
449
- <% if @timestamp_data.present? %>
450
- <div class="dbviewer-card card h-100">
451
- <div class="card-header d-flex justify-content-between align-items-center">
452
- <h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Record Creation Timeline</h5>
453
- <div>
454
- <%= time_grouping_links(@table_name, @time_grouping) %>
455
- </div>
456
- </div>
457
- <div class="card-body">
458
- <div class="chart-container">
459
- <canvas id="timestampChart"></canvas>
460
- </div>
461
- <div class="mt-3 text-center">
462
- <small class="text-muted">
463
- <i class="bi bi-info-circle"></i>
464
- Timeline shows <%= @time_grouping %> record creation patterns based on <code>created_at</code> column.
465
- </small>
466
- </div>
467
- </div>
468
- </div>
469
- <% else %>
470
- <div class="dbviewer-card card h-100">
471
- <div class="card-header">
472
- <h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Creation Timeline</h5>
473
- </div>
474
- <div class="card-body d-flex justify-content-center align-items-center text-center text-muted">
475
- <div>
476
- <i class="bi bi-calendar-x display-4 mb-3"></i>
477
- <p>No creation timestamp data available for this table.</p>
478
- <small>Timeline visualization is only available for tables with a <code>created_at</code> column.</small>
479
- </div>
480
- </div>
481
- </div>
482
- <% end %>
483
- </div>
484
-
485
- <!-- Structure Column -->
486
- <div class="col-md-6 mb-4">
487
- <div class="dbviewer-card card h-100">
488
- <div class="card-header">
489
- <h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>Table Structure</h5>
490
- </div>
491
- <div class="card-body structure-container">
492
- <%= render 'table_structure' %>
493
- </div>
494
- </div>
495
- </div>
496
- </div>
497
-
498
448
  <!-- Record Detail Modal -->
499
449
  <div class="modal fade" id="recordDetailModal" tabindex="-1" aria-labelledby="recordDetailModalLabel" aria-hidden="true">
500
450
  <div class="modal-dialog modal-lg modal-dialog-scrollable">
@@ -536,6 +486,194 @@
536
486
  </div>
537
487
  </div>
538
488
 
489
+ <!-- Table Structure Modal -->
490
+ <div class="modal fade" id="tableStructureModal" tabindex="-1" aria-labelledby="tableStructureModalLabel" aria-hidden="true">
491
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
492
+ <div class="modal-content">
493
+ <div class="modal-header">
494
+ <h5 class="modal-title" id="tableStructureModalLabel"><%= @table_name %> Structure</h5>
495
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
496
+ </div>
497
+ <div class="modal-body">
498
+ <%= render 'table_structure' %>
499
+ </div>
500
+ <div class="modal-footer">
501
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ </div>
506
+
507
+ <style>
508
+ /* Column filter styling */
509
+ .column-filters td {
510
+ padding: 0.5rem;
511
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
512
+ }
513
+
514
+ /* Action column styling */
515
+ .action-column {
516
+ width: 60px;
517
+ min-width: 60px; /* Ensure minimum width */
518
+ white-space: nowrap;
519
+ position: sticky;
520
+ left: 0;
521
+ z-index: 30; /* Increased z-index to ensure it stays on top */
522
+ background-color: var(--bs-body-bg, #fff); /* Use body background color */
523
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
524
+ border-right: 1px solid var(--bs-border-color);
525
+ }
526
+
527
+ /* Ensure proper background color for actions column in dark mode */
528
+ [data-bs-theme="dark"] .action-column {
529
+ background-color: var(--bs-body-bg, #212529); /* Use body background in dark mode */
530
+ }
531
+
532
+ /* Maintain zebra striping with sticky action column */
533
+ .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
534
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
535
+ }
536
+
537
+ .table-striped > tbody > tr:nth-of-type(even) > .action-column {
538
+ background-color: var(--bs-body-bg, #fff);
539
+ }
540
+
541
+ [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
542
+ background-color: var(--bs-tertiary-bg, #2b3035);
543
+ }
544
+
545
+ [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(even) > .action-column {
546
+ background-color: var(--bs-body-bg, #212529);
547
+ }
548
+
549
+ .view-record-btn {
550
+ padding: 0.1rem 0.4rem;
551
+ width: 32px;
552
+ }
553
+
554
+ .view-record-btn:hover {
555
+ opacity: 0.85;
556
+ transform: translateY(-1px);
557
+ }
558
+
559
+ /* Record detail modal styling */
560
+ .record-detail-table tr:first-child th,
561
+ .record-detail-table tr:first-child td {
562
+ border-top: none;
563
+ }
564
+
565
+ .record-detail-table .code-block {
566
+ background-color: var(--bs-light);
567
+ padding: 0.5rem;
568
+ border-radius: 0.25rem;
569
+ overflow-x: auto;
570
+ max-height: 200px;
571
+ }
572
+
573
+ /* Relationships section styling */
574
+ #relationshipsSection {
575
+ border-top: 1px solid var(--bs-border-color);
576
+ padding-top: 1rem;
577
+ }
578
+
579
+ #relationshipsSection h6 {
580
+ color: var(--bs-emphasis-color);
581
+ margin-bottom: 1rem;
582
+ }
583
+
584
+ [data-bs-theme="dark"] #relationshipsSection {
585
+ border-top-color: #495057;
586
+ }
587
+
588
+ .relationships-table .btn-outline-primary {
589
+ font-size: 0.75rem;
590
+ padding: 0.25rem 0.5rem;
591
+ }
592
+
593
+ .relationships-table code {
594
+ background-color: var(--bs-gray-100);
595
+ padding: 0.125rem 0.25rem;
596
+ border-radius: 0.125rem;
597
+ font-size: 0.875rem;
598
+ }
599
+
600
+ [data-bs-theme="dark"] .relationships-table code {
601
+ background-color: var(--bs-gray-800);
602
+ color: var(--bs-gray-100);
603
+ }
604
+ margin-bottom: 0;
605
+ }
606
+
607
+ [data-bs-theme="dark"] .record-detail-table .code-block {
608
+ background-color: var(--bs-dark);
609
+ }
610
+
611
+ /* Fullscreen table styles */
612
+ .table-fullscreen {
613
+ position: fixed !important;
614
+ top: 0 !important;
615
+ left: 0 !important;
616
+ width: 100vw !important;
617
+ height: 100vh !important;
618
+ z-index: 9999 !important;
619
+ background: var(--bs-body-bg) !important;
620
+ margin: 0 !important;
621
+ border-radius: 0 !important;
622
+ overflow: hidden !important;
623
+ display: flex !important;
624
+ flex-direction: column !important;
625
+ }
626
+
627
+ .table-fullscreen .card-body {
628
+ flex: 1 !important;
629
+ overflow: hidden !important;
630
+ display: flex !important;
631
+ flex-direction: column !important;
632
+ }
633
+
634
+ .table-fullscreen .table-responsive {
635
+ flex: 1 !important;
636
+ overflow: auto !important;
637
+ }
638
+
639
+ .table-fullscreen .card-header {
640
+ flex-shrink: 0 !important;
641
+ position: sticky !important;
642
+ top: 0 !important;
643
+ z-index: 10000 !important;
644
+ background: var(--bs-body-bg) !important;
645
+ border-bottom: 1px solid var(--bs-border-color) !important;
646
+ }
647
+
648
+ /* Hide pagination in fullscreen mode */
649
+ .table-fullscreen .pagination-container {
650
+ display: none !important;
651
+ }
652
+
653
+ /* Adjust table header in fullscreen */
654
+ .table-fullscreen .dbviewer-table-header {
655
+ position: sticky !important;
656
+ top: 0 !important;
657
+ z-index: 100 !important;
658
+ }
659
+
660
+ /* Ensure body doesn't scroll when table is fullscreen */
661
+ body.table-fullscreen-active {
662
+ overflow: hidden !important;
663
+ }
664
+
665
+ /* Fullscreen button hover effect */
666
+ #fullscreen-toggle:hover {
667
+ background-color: var(--bs-secondary-bg) !important;
668
+ border-color: var(--bs-secondary-border-subtle) !important;
669
+ }
670
+
671
+ /* Smooth transitions */
672
+ #table-section {
673
+ transition: all 0.3s ease-in-out;
674
+ }
675
+ </style>
676
+
539
677
  <script>
540
678
  document.addEventListener('DOMContentLoaded', function() {
541
679
  // Record Detail Modal functionality
@@ -768,19 +906,7 @@
768
906
  const cacheBuster = new Date().getTime();
769
907
  const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
770
908
 
771
- // Log loading message
772
- console.log('Loading fresh Mini ERD data from:', fetchUrl);
773
-
774
- // Set a timeout to handle long-running requests
775
- const timeoutPromise = new Promise((_, reject) =>
776
- setTimeout(() => reject(new Error('Request timeout after 10 seconds')), 10000)
777
- );
778
-
779
- // Race the fetch against a timeout
780
- Promise.race([
781
- fetch(fetchUrl),
782
- timeoutPromise
783
- ])
909
+ fetch(fetchUrl)
784
910
  .then(response => {
785
911
  if (!response.ok) {
786
912
  throw new Error(`Server returned ${response.status} ${response.statusText}`);
@@ -1208,288 +1334,102 @@
1208
1334
  // Retry fetching data
1209
1335
  fetchErdData();
1210
1336
  }
1211
- });
1212
- </script>
1337
+
1338
+ // Column sorting enhancement
1339
+ const sortableColumns = document.querySelectorAll('.sortable-column');
1340
+ sortableColumns.forEach(column => {
1341
+ const link = column.querySelector('.column-sort-link');
1342
+
1343
+ // Mouse over effects
1344
+ column.addEventListener('mouseenter', () => {
1345
+ const sortIcon = column.querySelector('.sort-icon');
1346
+ if (sortIcon && sortIcon.classList.contains('invisible')) {
1347
+ sortIcon.style.visibility = 'visible';
1348
+ sortIcon.style.opacity = '0.3';
1349
+ }
1350
+ });
1351
+
1352
+ column.addEventListener('mouseleave', () => {
1353
+ const sortIcon = column.querySelector('.sort-icon');
1354
+ if (sortIcon && sortIcon.classList.contains('invisible')) {
1355
+ sortIcon.style.visibility = 'hidden';
1356
+ sortIcon.style.opacity = '0';
1357
+ }
1358
+ });
1359
+
1360
+ // Keyboard accessibility
1361
+ if (link) {
1362
+ link.addEventListener('keydown', (e) => {
1363
+ if (e.key === 'Enter' || e.key === ' ') {
1364
+ e.preventDefault();
1365
+ link.click();
1366
+ }
1367
+ });
1368
+ }
1369
+ });
1213
1370
 
1214
- <style>
1215
- /* Column filter styling */
1216
- .column-filters td {
1217
- padding: 0.5rem;
1218
- background-color: var(--bs-tertiary-bg, #f8f9fa);
1219
- }
1220
-
1221
- /* Action column styling */
1222
- .action-column {
1223
- width: 60px;
1224
- min-width: 60px; /* Ensure minimum width */
1225
- white-space: nowrap;
1226
- position: sticky;
1227
- left: 0;
1228
- z-index: 30; /* Increased z-index to ensure it stays on top */
1229
- background-color: var(--bs-body-bg, #fff); /* Use body background color */
1230
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
1231
- border-right: 1px solid var(--bs-border-color);
1232
- }
1233
-
1234
- /* Ensure proper background color for actions column in dark mode */
1235
- [data-bs-theme="dark"] .action-column {
1236
- background-color: var(--bs-body-bg, #212529); /* Use body background in dark mode */
1237
- }
1238
-
1239
- /* Maintain zebra striping with sticky action column */
1240
- .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
1241
- background-color: var(--bs-tertiary-bg, #f8f9fa);
1242
- }
1243
-
1244
- .table-striped > tbody > tr:nth-of-type(even) > .action-column {
1245
- background-color: var(--bs-body-bg, #fff);
1246
- }
1247
-
1248
- [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
1249
- background-color: var(--bs-tertiary-bg, #2b3035);
1250
- }
1251
-
1252
- [data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(even) > .action-column {
1253
- background-color: var(--bs-body-bg, #212529);
1254
- }
1255
-
1256
- .view-record-btn {
1257
- padding: 0.1rem 0.4rem;
1258
- width: 32px;
1259
- }
1260
-
1261
- .view-record-btn:hover {
1262
- opacity: 0.85;
1263
- transform: translateY(-1px);
1264
- }
1265
-
1266
- /* Record detail modal styling */
1267
- .record-detail-table tr:first-child th,
1268
- .record-detail-table tr:first-child td {
1269
- border-top: none;
1270
- }
1271
-
1272
- .record-detail-table .code-block {
1273
- background-color: var(--bs-light);
1274
- padding: 0.5rem;
1275
- border-radius: 0.25rem;
1276
- overflow-x: auto;
1277
- max-height: 200px;
1278
- }
1279
-
1280
- /* Relationships section styling */
1281
- #relationshipsSection {
1282
- border-top: 1px solid var(--bs-border-color);
1283
- padding-top: 1rem;
1284
- }
1285
-
1286
- #relationshipsSection h6 {
1287
- color: var(--bs-emphasis-color);
1288
- margin-bottom: 1rem;
1289
- }
1290
-
1291
- [data-bs-theme="dark"] #relationshipsSection {
1292
- border-top-color: #495057;
1293
- }
1294
-
1295
- .relationships-table .btn-outline-primary {
1296
- font-size: 0.75rem;
1297
- padding: 0.25rem 0.5rem;
1298
- }
1299
-
1300
- .relationships-table code {
1301
- background-color: var(--bs-gray-100);
1302
- padding: 0.125rem 0.25rem;
1303
- border-radius: 0.125rem;
1304
- font-size: 0.875rem;
1305
- }
1306
-
1307
- [data-bs-theme="dark"] .relationships-table code {
1308
- background-color: var(--bs-gray-800);
1309
- color: var(--bs-gray-100);
1310
- }
1311
- margin-bottom: 0;
1312
- }
1313
-
1314
- [data-bs-theme="dark"] .record-detail-table .code-block {
1315
- background-color: var(--bs-dark);
1316
- }
1317
-
1318
- /* Fullscreen table styles */
1319
- .table-fullscreen {
1320
- position: fixed !important;
1321
- top: 0 !important;
1322
- left: 0 !important;
1323
- width: 100vw !important;
1324
- height: 100vh !important;
1325
- z-index: 9999 !important;
1326
- background: var(--bs-body-bg) !important;
1327
- margin: 0 !important;
1328
- border-radius: 0 !important;
1329
- overflow: hidden !important;
1330
- display: flex !important;
1331
- flex-direction: column !important;
1332
- }
1333
-
1334
- .table-fullscreen .card-body {
1335
- flex: 1 !important;
1336
- overflow: hidden !important;
1337
- display: flex !important;
1338
- flex-direction: column !important;
1339
- }
1340
-
1341
- .table-fullscreen .table-responsive {
1342
- flex: 1 !important;
1343
- overflow: auto !important;
1344
- }
1345
-
1346
- .table-fullscreen .card-header {
1347
- flex-shrink: 0 !important;
1348
- position: sticky !important;
1349
- top: 0 !important;
1350
- z-index: 10000 !important;
1351
- background: var(--bs-body-bg) !important;
1352
- border-bottom: 1px solid var(--bs-border-color) !important;
1353
- }
1354
-
1355
- /* Hide pagination in fullscreen mode */
1356
- .table-fullscreen .pagination-container {
1357
- display: none !important;
1358
- }
1359
-
1360
- /* Adjust table header in fullscreen */
1361
- .table-fullscreen .dbviewer-table-header {
1362
- position: sticky !important;
1363
- top: 0 !important;
1364
- z-index: 100 !important;
1365
- }
1366
-
1367
- /* Ensure body doesn't scroll when table is fullscreen */
1368
- body.table-fullscreen-active {
1369
- overflow: hidden !important;
1370
- }
1371
-
1372
- /* Fullscreen button hover effect */
1373
- #fullscreen-toggle:hover {
1374
- background-color: var(--bs-secondary-bg) !important;
1375
- border-color: var(--bs-secondary-border-subtle) !important;
1376
- }
1377
-
1378
- /* Smooth transitions */
1379
- #table-section {
1380
- transition: all 0.3s ease-in-out;
1381
- }
1382
- </style>
1383
-
1384
- <% if @timestamp_data.present? %>
1385
- <script>
1386
- document.addEventListener('DOMContentLoaded', function() {
1387
- const timeGrouping = '<%= @time_grouping %>';
1388
- const chartData = <%= raw @timestamp_data.to_json %>;
1389
-
1390
- // Reverse the data so it's chronological
1391
- const labels = chartData.map(item => item.label).reverse();
1392
- const values = chartData.map(item => item.value).reverse();
1393
-
1394
- // Chart colors based on time grouping
1395
- let chartColor;
1396
- let chartTitle;
1397
-
1398
- switch(timeGrouping) {
1399
- case 'hourly':
1400
- chartColor = 'rgba(75, 192, 192, 0.7)';
1401
- chartTitle = 'Hourly Record Creation';
1402
- break;
1403
- case 'weekly':
1404
- chartColor = 'rgba(153, 102, 255, 0.7)';
1405
- chartTitle = 'Weekly Record Creation';
1406
- break;
1407
- default:
1408
- chartColor = 'rgba(54, 162, 235, 0.7)';
1409
- chartTitle = 'Daily Record Creation';
1410
- }
1371
+ // Table fullscreen functionality
1372
+ const fullscreenToggle = document.getElementById('fullscreen-toggle');
1373
+ const fullscreenIcon = document.getElementById('fullscreen-icon');
1374
+ const tableSection = document.getElementById('table-section');
1411
1375
 
1412
- const ctx = document.getElementById('timestampChart').getContext('2d');
1413
- new Chart(ctx, {
1414
- type: 'bar',
1415
- data: {
1416
- labels: labels,
1417
- datasets: [{
1418
- label: 'Records Created',
1419
- data: values,
1420
- backgroundColor: chartColor,
1421
- borderColor: chartColor.replace('0.7', '1.0'),
1422
- borderWidth: 1
1423
- }]
1424
- },
1425
- options: {
1426
- responsive: true,
1427
- maintainAspectRatio: false,
1428
- plugins: {
1429
- legend: {
1430
- display: false
1431
- },
1432
- title: {
1433
- display: true,
1434
- text: chartTitle,
1435
- font: {
1436
- size: 16
1437
- }
1438
- }
1439
- },
1440
- scales: {
1441
- y: {
1442
- beginAtZero: true,
1443
- title: {
1444
- display: true,
1445
- text: 'Number of Records'
1446
- },
1447
- ticks: {
1448
- precision: 0
1449
- }
1450
- },
1451
- x: {
1452
- title: {
1453
- display: true,
1454
- text: timeGrouping.charAt(0).toUpperCase() + timeGrouping.slice(1)
1455
- }
1456
- }
1376
+ if (fullscreenToggle && tableSection) {
1377
+ // Key for storing fullscreen state in localStorage
1378
+ const fullscreenStateKey = 'dbviewer-table-fullscreen-<%= @table_name %>';
1379
+
1380
+ // Function to apply fullscreen state
1381
+ function applyFullscreenState(isFullscreen) {
1382
+ if (isFullscreen) {
1383
+ // Enter fullscreen
1384
+ tableSection.classList.add('table-fullscreen');
1385
+ document.body.classList.add('table-fullscreen-active');
1386
+ fullscreenIcon.classList.remove('bi-fullscreen');
1387
+ fullscreenIcon.classList.add('bi-fullscreen-exit');
1388
+ fullscreenToggle.setAttribute('title', 'Exit fullscreen');
1389
+ } else {
1390
+ // Exit fullscreen
1391
+ tableSection.classList.remove('table-fullscreen');
1392
+ document.body.classList.remove('table-fullscreen-active');
1393
+ fullscreenIcon.classList.remove('bi-fullscreen-exit');
1394
+ fullscreenIcon.classList.add('bi-fullscreen');
1395
+ fullscreenToggle.setAttribute('title', 'Toggle fullscreen');
1457
1396
  }
1458
1397
  }
1459
- });
1460
-
1461
- // Column sorting enhancement
1462
- const sortableColumns = document.querySelectorAll('.sortable-column');
1463
- sortableColumns.forEach(column => {
1464
- const link = column.querySelector('.column-sort-link');
1465
1398
 
1466
- // Mouse over effects
1467
- column.addEventListener('mouseenter', () => {
1468
- const sortIcon = column.querySelector('.sort-icon');
1469
- if (sortIcon && sortIcon.classList.contains('invisible')) {
1470
- sortIcon.style.visibility = 'visible';
1471
- sortIcon.style.opacity = '0.3';
1399
+ // Restore fullscreen state from localStorage on page load
1400
+ try {
1401
+ const savedState = localStorage.getItem(fullscreenStateKey);
1402
+ if (savedState === 'true') {
1403
+ applyFullscreenState(true);
1472
1404
  }
1473
- });
1405
+ } catch (e) {
1406
+ // Handle localStorage not available (private browsing, etc.)
1407
+ console.warn('Could not restore fullscreen state:', e);
1408
+ }
1474
1409
 
1475
- column.addEventListener('mouseleave', () => {
1476
- const sortIcon = column.querySelector('.sort-icon');
1477
- if (sortIcon && sortIcon.classList.contains('invisible')) {
1478
- sortIcon.style.visibility = 'hidden';
1479
- sortIcon.style.opacity = '0';
1410
+ fullscreenToggle.addEventListener('click', function() {
1411
+ const isFullscreen = tableSection.classList.contains('table-fullscreen');
1412
+ const newState = !isFullscreen;
1413
+
1414
+ // Apply the new state
1415
+ applyFullscreenState(newState);
1416
+
1417
+ // Save state to localStorage
1418
+ try {
1419
+ localStorage.setItem(fullscreenStateKey, newState.toString());
1420
+ } catch (e) {
1421
+ // Handle localStorage not available (private browsing, etc.)
1422
+ console.warn('Could not save fullscreen state:', e);
1480
1423
  }
1481
1424
  });
1482
1425
 
1483
- // Keyboard accessibility
1484
- if (link) {
1485
- link.addEventListener('keydown', (e) => {
1486
- if (e.key === 'Enter' || e.key === ' ') {
1487
- e.preventDefault();
1488
- link.click();
1489
- }
1490
- });
1491
- }
1492
- });
1426
+ // Exit fullscreen with Escape key
1427
+ document.addEventListener('keydown', function(e) {
1428
+ if (e.key === 'Escape' && tableSection.classList.contains('table-fullscreen')) {
1429
+ fullscreenToggle.click();
1430
+ }
1431
+ });
1432
+ }
1493
1433
  });
1494
1434
 
1495
1435
  // Helper function to create relationship sections
@@ -1584,70 +1524,4 @@
1584
1524
 
1585
1525
  return section;
1586
1526
  }
1587
-
1588
- // Table fullscreen functionality
1589
- document.addEventListener('DOMContentLoaded', function() {
1590
- const fullscreenToggle = document.getElementById('fullscreen-toggle');
1591
- const fullscreenIcon = document.getElementById('fullscreen-icon');
1592
- const tableSection = document.getElementById('table-section');
1593
-
1594
- if (fullscreenToggle && tableSection) {
1595
- // Key for storing fullscreen state in localStorage
1596
- const fullscreenStateKey = 'dbviewer-table-fullscreen-<%= @table_name %>';
1597
-
1598
- // Function to apply fullscreen state
1599
- function applyFullscreenState(isFullscreen) {
1600
- if (isFullscreen) {
1601
- // Enter fullscreen
1602
- tableSection.classList.add('table-fullscreen');
1603
- document.body.classList.add('table-fullscreen-active');
1604
- fullscreenIcon.classList.remove('bi-fullscreen');
1605
- fullscreenIcon.classList.add('bi-fullscreen-exit');
1606
- fullscreenToggle.setAttribute('title', 'Exit fullscreen');
1607
- } else {
1608
- // Exit fullscreen
1609
- tableSection.classList.remove('table-fullscreen');
1610
- document.body.classList.remove('table-fullscreen-active');
1611
- fullscreenIcon.classList.remove('bi-fullscreen-exit');
1612
- fullscreenIcon.classList.add('bi-fullscreen');
1613
- fullscreenToggle.setAttribute('title', 'Toggle fullscreen');
1614
- }
1615
- }
1616
-
1617
- // Restore fullscreen state from localStorage on page load
1618
- try {
1619
- const savedState = localStorage.getItem(fullscreenStateKey);
1620
- if (savedState === 'true') {
1621
- applyFullscreenState(true);
1622
- }
1623
- } catch (e) {
1624
- // Handle localStorage not available (private browsing, etc.)
1625
- console.warn('Could not restore fullscreen state:', e);
1626
- }
1627
-
1628
- fullscreenToggle.addEventListener('click', function() {
1629
- const isFullscreen = tableSection.classList.contains('table-fullscreen');
1630
- const newState = !isFullscreen;
1631
-
1632
- // Apply the new state
1633
- applyFullscreenState(newState);
1634
-
1635
- // Save state to localStorage
1636
- try {
1637
- localStorage.setItem(fullscreenStateKey, newState.toString());
1638
- } catch (e) {
1639
- // Handle localStorage not available (private browsing, etc.)
1640
- console.warn('Could not save fullscreen state:', e);
1641
- }
1642
- });
1643
-
1644
- // Exit fullscreen with Escape key
1645
- document.addEventListener('keydown', function(e) {
1646
- if (e.key === 'Escape' && tableSection.classList.contains('table-fullscreen')) {
1647
- fullscreenToggle.click();
1648
- }
1649
- });
1650
- }
1651
- });
1652
1527
  </script>
1653
- <% end %>