dbwatcher 1.1.2 → 1.1.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/dbwatcher_manifest.js +1 -0
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
  4. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  5. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  6. data/app/assets/stylesheets/dbwatcher/application.css +298 -1
  7. data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
  8. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  9. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  10. data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
  11. data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
  12. data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
  13. data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
  14. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  15. data/app/views/dbwatcher/sessions/show.html.erb +3 -1
  16. data/app/views/layouts/dbwatcher/application.html.erb +1 -0
  17. data/config/routes.rb +2 -1
  18. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  19. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  20. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  21. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  22. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  23. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  24. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  25. data/lib/dbwatcher/version.rb +1 -1
  26. data/lib/dbwatcher.rb +1 -1
  27. metadata +11 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08e110f9429a5d0a67afbea8155a063fcfb012228626cf2d7e0a42c00831db17'
4
- data.tar.gz: 5f2f2db2a7c6c2bbaeeb3a57dafb2e0d4cd841542cd05a351473555591bc3078
3
+ metadata.gz: cc4ee903407a594933016fc81eab8e7ea90b7f63fe6b046305191b18e87afc27
4
+ data.tar.gz: e1e9c3f66d04da6e5e043feddf01fb19e02680f0556799e06c5a74ef44da0143
5
5
  SHA512:
6
- metadata.gz: e51c681692cc08e1af7d8663e2f1b036ff4c309a62c7b4264b769b6d7e549d915d98fd4834ba7e0d5615f03913595e03fa6e99d43d2e2618ede24f3f46278f92
7
- data.tar.gz: d5265b28f063458470804315fbb9e4480e8669443799e345c420cc7e4c26eab1d6823464c5289f707c0c936549c926d7f850598486fb14d9817127e4892edd4d
6
+ metadata.gz: 66c026987650f546ebe5ca2f0ef513d0828f3ca615d28dfeb55174c36c33db4dd1361cea07812905f43d70eab4a369f9ddf0c483191b3337b22f89c699412b19
7
+ data.tar.gz: 95d9a01e5dd54e81d9a1f6260720fb43d2ef650a3c550012cb943d39c66e4d13616d796a39b489bdffdc9816f30d6bedbd78ad3c0537c588ba120821964c3e19
@@ -4,6 +4,7 @@
4
4
  //= link dbwatcher/alpine_registrations.js
5
5
  //= link_tree ../javascripts/dbwatcher/components
6
6
  //= link dbwatcher/components/changes_table_hybrid.js
7
+ //= link dbwatcher/components/timeline.js
7
8
  //= link_tree ../javascripts/dbwatcher/core
8
9
  //= link_tree ../javascripts/dbwatcher/services
9
10
  //= link dbwatcher/vendor/lodash.min.js
@@ -18,7 +18,8 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
18
18
  filters: {
19
19
  search: '',
20
20
  operation: '',
21
- table: ''
21
+ table: '',
22
+ selectedTables: []
22
23
  },
23
24
  showColumnSelector: null,
24
25
  expandedRows: {},
@@ -57,46 +58,55 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
57
58
 
58
59
  // Apply filters directly to Tabulator (no server reload needed)
59
60
  applyTabulatorFilters() {
60
- if (!this.tabulator) return;
61
-
62
- // Clear existing filters
63
- this.tabulator.clearFilter();
64
-
65
- // Apply filters based on current state
66
- const filters = [];
61
+ // Apply filters to all tabulator instances
62
+ Object.keys(this.tabulators).forEach(tableName => {
63
+ const tabulator = this.tabulators[tableName];
64
+ if (!tabulator) return;
65
+
66
+ // Clear existing filters
67
+ tabulator.clearFilter();
68
+
69
+ // Create a combined filter function that handles all filters
70
+ const hasSearch = this.filters.search && this.filters.search.trim();
71
+ const hasOperation = this.filters.operation;
72
+ const hasTable = this.filters.table;
73
+
74
+ if (hasSearch || hasOperation || hasTable) {
75
+ const searchTerm = hasSearch ? this.filters.search.trim().toLowerCase() : '';
76
+
77
+ // Apply combined custom filter
78
+ tabulator.setFilter((data) => {
79
+ // Search filter
80
+ if (hasSearch) {
81
+ const searchableContent = [
82
+ data.table_name,
83
+ data.operation,
84
+ data.timestamp,
85
+ data.index,
86
+ ...Object.values(data).filter(val => val !== null && val !== undefined)
87
+ ].join(' ').toLowerCase();
88
+
89
+ if (!searchableContent.includes(searchTerm)) {
90
+ return false;
91
+ }
92
+ }
67
93
 
68
- // Search filter (searches across multiple fields)
69
- if (this.filters.search && this.filters.search.trim()) {
70
- const searchTerm = this.filters.search.trim().toLowerCase();
71
- filters.push({
72
- field: 'searchable_content',
73
- type: 'like',
74
- value: searchTerm
75
- });
76
- }
94
+ // Operation filter
95
+ if (hasOperation && data.operation !== this.filters.operation) {
96
+ return false;
97
+ }
77
98
 
78
- // Operation filter
79
- if (this.filters.operation) {
80
- filters.push({
81
- field: 'operation',
82
- type: '=',
83
- value: this.filters.operation
84
- });
85
- }
99
+ // Table filter
100
+ if (hasTable && data.table_name !== this.filters.table) {
101
+ return false;
102
+ }
86
103
 
87
- // Table filter
88
- if (this.filters.table) {
89
- filters.push({
90
- field: 'table_name',
91
- type: '=',
92
- value: this.filters.table
93
- });
94
- }
104
+ return true;
105
+ });
106
+ }
95
107
 
96
- // Apply all filters
97
- if (filters.length > 0) {
98
- this.tabulator.setFilter(filters);
99
- }
108
+ // Note: Multi-table filtering is handled at the template level via x-show
109
+ });
100
110
  },
101
111
 
102
112
  // Load data from API
@@ -117,11 +127,18 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
117
127
  if (this.filters.operation) params.append('operation', this.filters.operation);
118
128
  if (this.filters.search) params.append('search', this.filters.search);
119
129
 
120
- const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/changes_data?${params.toString()}`;
130
+ const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/tables_data?${params.toString()}`;
121
131
  const data = await this.fetchData(url);
122
132
 
123
133
  if (data.tables_summary) {
124
134
  this.tableData = data.tables_summary;
135
+
136
+ // Debug: Log the table data structure to verify model_class is included
137
+ console.log('Table data received:', Object.keys(this.tableData));
138
+ Object.entries(this.tableData).forEach(([tableName, tableInfo]) => {
139
+ console.log(`Table ${tableName} model_class:`, tableInfo.model_class);
140
+ });
141
+
125
142
  this.initializeColumnVisibility();
126
143
  this.initializeTabulators();
127
144
  } else {
@@ -166,30 +183,30 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
166
183
 
167
184
  // Transform data for this specific table
168
185
  const tabulatorData = this.transformTableDataForTabulator(tableName, tableInfo);
169
-
186
+
170
187
  // Create Tabulator instance for this table
171
188
  this.tabulators[tableName] = new Tabulator(container, {
172
189
  data: tabulatorData,
173
190
  layout: 'fitDataFill',
174
191
  responsiveLayout: false,
175
192
  height: Math.max(200, Math.min(400, (tabulatorData.length * 35) + 80)), // Minimum 200px, expand based on content
176
-
193
+
177
194
  // Force Tabulator to use our custom rowId field
178
195
  index: 'rowId', // Tell Tabulator to use the 'rowId' field as the row identifier
179
-
196
+
180
197
  // Performance optimizations - disable virtual DOM to ensure all rows render
181
198
  virtualDom: false,
182
199
  pagination: false, // Ensure no pagination
183
-
200
+
184
201
  // Column configuration for this table
185
202
  columns: this.buildColumnsForTable(tableName, tableInfo),
186
-
203
+
187
204
  // Row formatting
188
205
  rowFormatter: this.customRowFormatter.bind(this),
189
-
206
+
190
207
  // No initial sorting - data should be in correct order from API
191
208
  // initialSort: [],
192
-
209
+
193
210
  // Enable header sorting
194
211
  headerSortTristate: true,
195
212
 
@@ -204,13 +221,13 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
204
221
  transformTableDataForTabulator(tableName, tableInfo) {
205
222
  const rows = [];
206
223
  const changes = tableInfo.changes || [];
207
-
224
+
208
225
  changes.forEach((change, index) => {
209
226
  const columnData = this.extractColumnData(change, tableInfo.columns);
210
-
227
+
211
228
  // Create truly unique row ID using table name and index only (for Tabulator internal use)
212
229
  const uniqueRowId = `${tableName}_row_${index}`;
213
-
230
+
214
231
  const row = {
215
232
  rowId: uniqueRowId, // Tabulator's internal row identifier
216
233
  index: index + 1, // Display index (1-based) - should maintain API order
@@ -220,7 +237,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
220
237
  change_data: change, // Keep original change data with ID intact
221
238
  ...columnData // Include all column data including actual record ID
222
239
  };
223
-
240
+
224
241
  rows.push(row);
225
242
  });
226
243
 
@@ -241,7 +258,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
241
258
  formatter: (cell) => {
242
259
  const rowData = cell.getRow().getData();
243
260
  return `<div class="flex items-center justify-center gap-1">
244
- <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
261
+ <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
245
262
  data-row-id="${rowData.rowId}">
246
263
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
247
264
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 5.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
@@ -302,7 +319,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
302
319
  const value = cell.getValue();
303
320
  const rowData = cell.getRow().getData();
304
321
  const change = rowData.change_data;
305
-
322
+
306
323
  // Handle different operations with appropriate styling
307
324
  if (change.operation === 'UPDATE' && change.changes) {
308
325
  const columnChange = change.changes.find(c => c.column === col);
@@ -334,7 +351,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
334
351
  // Extract column data from change record
335
352
  extractColumnData(change, columns) {
336
353
  const data = {};
337
-
354
+
338
355
  columns.forEach(col => {
339
356
  // Get value from record snapshot or change data
340
357
  if (change.record_snapshot && change.record_snapshot[col] !== undefined) {
@@ -364,7 +381,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
364
381
  formatter: (cell) => {
365
382
  const rowData = cell.getRow().getData();
366
383
  return `<div class="flex items-center justify-center gap-1">
367
- <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
384
+ <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
368
385
  data-row-id="${rowData.rowId}">
369
386
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
370
387
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 5.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
@@ -431,7 +448,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
431
448
  const value = cell.getValue();
432
449
  const rowData = cell.getRow().getData();
433
450
  const change = rowData.change_data;
434
-
451
+
435
452
  // Handle different operations with appropriate styling
436
453
  if (change.operation === 'UPDATE' && change.changes) {
437
454
  const columnChange = change.changes.find(c => c.column === col);
@@ -463,15 +480,15 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
463
480
  if (columnName.toLowerCase().includes('id') || columnName.toLowerCase().includes('uuid')) {
464
481
  return 'string';
465
482
  }
466
-
483
+
467
484
  // Timestamp columns - use string sorting to avoid Luxon dependency
468
- if (columnName.toLowerCase().includes('time') ||
485
+ if (columnName.toLowerCase().includes('time') ||
469
486
  columnName.toLowerCase().includes('date') ||
470
487
  columnName.toLowerCase().includes('created') ||
471
488
  columnName.toLowerCase().includes('updated')) {
472
489
  return 'string'; // Changed from 'datetime' to 'string'
473
490
  }
474
-
491
+
475
492
  // Numeric columns
476
493
  if (columnName.toLowerCase().includes('count') ||
477
494
  columnName.toLowerCase().includes('amount') ||
@@ -479,7 +496,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
479
496
  columnName.toLowerCase().includes('quantity')) {
480
497
  return 'number';
481
498
  }
482
-
499
+
483
500
  // Default to alphanum for mixed content
484
501
  return 'alphanum';
485
502
  },
@@ -488,11 +505,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
488
505
  applyHeaderClasses(tableName) {
489
506
  const tabulator = this.tabulators[tableName];
490
507
  if (!tabulator) return;
491
-
508
+
492
509
  const headers = tabulator.getHeaderElements();
493
510
  headers.forEach((header) => {
494
511
  const field = header.getAttribute('tabulator-field');
495
-
512
+
496
513
  if (field === 'index') {
497
514
  header.classList.add('sticky-left-0');
498
515
  } else if (field === 'operation') {
@@ -507,10 +524,10 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
507
524
  applyRowClasses(tableName, row) {
508
525
  const element = row.getElement();
509
526
  const cells = element.querySelectorAll('.tabulator-cell');
510
-
527
+
511
528
  cells.forEach((cell) => {
512
529
  const field = cell.getAttribute('tabulator-field');
513
-
530
+
514
531
  if (field === 'index') {
515
532
  cell.classList.add('sticky-left-0');
516
533
  } else if (field === 'operation') {
@@ -525,7 +542,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
525
542
  customRowFormatter(row) {
526
543
  const rowData = row.getData();
527
544
  const element = row.getElement();
528
-
545
+
529
546
  // Add classes based on operation
530
547
  const operation = rowData.operation;
531
548
  if (operation) {
@@ -536,7 +553,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
536
553
  element.addEventListener('mouseenter', () => {
537
554
  element.style.backgroundColor = '#f3f4f6';
538
555
  });
539
-
556
+
540
557
  element.addEventListener('mouseleave', () => {
541
558
  element.style.backgroundColor = '';
542
559
  });
@@ -546,11 +563,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
546
563
  // Toggle row expansion
547
564
  toggleRowExpansion(rowId) {
548
565
  this.expandedRows[rowId] = !this.expandedRows[rowId];
549
-
566
+
550
567
  // Find the row across all tabulator instances
551
568
  let targetRow = null;
552
569
  let foundInTable = null;
553
-
570
+
554
571
  Object.keys(this.tabulators).forEach(tableName => {
555
572
  const tabulator = this.tabulators[tableName];
556
573
  if (tabulator && !targetRow) {
@@ -577,7 +594,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
577
594
  }
578
595
  }
579
596
  });
580
-
597
+
581
598
  if (targetRow) {
582
599
  if (this.expandedRows[rowId]) {
583
600
  this.showRowDetails(targetRow);
@@ -593,34 +610,34 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
593
610
  showRowDetails(row) {
594
611
  const rowData = row.getData();
595
612
  const element = row.getElement();
596
-
597
-
613
+
614
+
598
615
  // Check if detail row already exists
599
616
  const existingDetail = element.nextElementSibling;
600
617
  if (existingDetail && existingDetail.classList.contains('row-detail')) {
601
618
  return; // Already expanded
602
619
  }
603
-
620
+
604
621
  // Create detail row as a proper table row
605
622
  const detailRow = document.createElement('tr');
606
623
  detailRow.className = 'row-detail bg-gray-50';
607
624
  detailRow.setAttribute('data-parent-id', rowData.rowId);
608
-
625
+
609
626
  // Create full-width cell
610
627
  const detailCell = document.createElement('td');
611
628
  detailCell.colSpan = 1000; // Span all columns
612
629
  detailCell.className = 'p-0 border-t border-gray-200';
613
-
630
+
614
631
  try {
615
632
  detailCell.innerHTML = this.generateExpandedContent(rowData);
616
633
  detailRow.appendChild(detailCell);
617
-
634
+
618
635
  // Insert after the current row
619
636
  element.parentNode.insertBefore(detailRow, element.nextSibling);
620
-
637
+
621
638
  // Update expand button
622
639
  this.updateExpandButton(element, true);
623
-
640
+
624
641
  // Dynamically increase table height when expanded
625
642
  const tabulator = this.findTabulatorForRow(rowData.table_name);
626
643
  if (tabulator) {
@@ -635,22 +652,22 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
635
652
  }
636
653
  },
637
654
 
638
- // Hide row details
655
+ // Hide row details
639
656
  hideRowDetails(row) {
640
657
  const element = row.getElement();
641
658
  const rowData = row.getData();
642
-
643
-
659
+
660
+
644
661
  // Find and remove the detail row
645
662
  const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.rowId}"]`);
646
663
  if (detailRow) {
647
664
  detailRow.remove();
648
665
  } else {
649
666
  }
650
-
667
+
651
668
  // Update expand button
652
669
  this.updateExpandButton(element, false);
653
-
670
+
654
671
  // Shrink table height back when collapsed
655
672
  const tabulator = this.findTabulatorForRow(rowData.table_name);
656
673
  if (tabulator) {
@@ -675,12 +692,12 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
675
692
  }
676
693
  },
677
694
 
678
- // Generate expanded content for a row - inline table format
695
+ // Generate expanded content for a row - inline table format
679
696
  generateExpandedContent(rowData) {
680
697
  const change = rowData.change_data;
681
698
  const tableInfo = this.tableData[rowData.table_name];
682
699
  const columns = tableInfo ? tableInfo.columns : [];
683
-
700
+
684
701
  // Create a table row that matches the column structure
685
702
  let content = `
686
703
  <div class="bg-gray-50 border-t border-gray-200">
@@ -700,21 +717,21 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
700
717
  <div class="text-gray-500 mt-1">${rowData.table_name}</div>
701
718
  </div>
702
719
  `;
703
-
720
+
704
721
  if (change.operation === 'UPDATE' && change.changes) {
705
722
  content += `<div class="text-blue-600 font-medium">${change.changes.length} columns modified</div>`;
706
723
  }
707
-
724
+
708
725
  if (change.record_snapshot && (change.record_snapshot.id || change.record_snapshot.uuid)) {
709
726
  const fullId = change.record_snapshot.id || change.record_snapshot.uuid;
710
727
  content += `<div class="text-gray-500 font-mono text-xs bg-gray-200 p-1 rounded truncate" title="${fullId}">ID: ${String(fullId).substring(0, 12)}...</div>`;
711
728
  }
712
-
729
+
713
730
  content += `
714
731
  </div>
715
732
  </td>
716
733
  `;
717
-
734
+
718
735
  // Add detail cells for each column that matches the table structure
719
736
  columns.forEach(col => {
720
737
  // Only show if column is visible in the main table
@@ -724,7 +741,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
724
741
  <div class="text-xs font-medium text-gray-600 mb-1">${col}</div>
725
742
  <div class="text-xs">
726
743
  `;
727
-
744
+
728
745
  if (change.operation === 'UPDATE' && change.changes) {
729
746
  const columnChange = change.changes.find(c => c.column === col);
730
747
  if (columnChange) {
@@ -779,21 +796,21 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
779
796
  </div>
780
797
  `;
781
798
  }
782
-
799
+
783
800
  content += `
784
801
  </div>
785
802
  </td>
786
803
  `;
787
804
  }
788
805
  });
789
-
806
+
790
807
  content += `
791
808
  </tr>
792
809
  </tbody>
793
810
  </table>
794
811
  </div>
795
812
  `;
796
-
813
+
797
814
  return content;
798
815
  },
799
816
 
@@ -802,7 +819,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
802
819
  if (value === null || value === undefined) {
803
820
  return '<span class="text-gray-400 italic">NULL</span>';
804
821
  }
805
-
822
+
806
823
  if (typeof value === 'string' && this.isJsonValue(value)) {
807
824
  try {
808
825
  const parsed = JSON.parse(value);
@@ -811,7 +828,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
811
828
  return String(value);
812
829
  }
813
830
  }
814
-
831
+
815
832
  return String(value);
816
833
  },
817
834
 
@@ -831,11 +848,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
831
848
  if (value === null || value === undefined) {
832
849
  return '<span class="text-gray-400">NULL</span>';
833
850
  }
834
-
851
+
835
852
  if (typeof value === 'string' && value.length > 50) {
836
853
  return `<span title="${value}">${value.substring(0, 50)}...</span>`;
837
854
  }
838
-
855
+
839
856
  return String(value);
840
857
  },
841
858
 
@@ -888,7 +905,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
888
905
  // Initialize column visibility state (per table)
889
906
  initializeColumnVisibility() {
890
907
  this.tableColumns = {};
891
-
908
+
892
909
  Object.keys(this.tableData).forEach(tableName => {
893
910
  const tableInfo = this.tableData[tableName];
894
911
  if (tableInfo && tableInfo.columns) {
@@ -916,7 +933,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
916
933
  // Rebuild columns for this table
917
934
  const tableInfo = this.tableData[tableName];
918
935
  const newColumns = this.buildColumnsForTable(tableName, tableInfo);
919
-
936
+
920
937
  // Update the tabulator columns
921
938
  tabulator.setColumns(newColumns);
922
939
  },
@@ -984,6 +1001,76 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
984
1001
  });
985
1002
  },
986
1003
 
1004
+ // Get available operations for filtering
1005
+ getAvailableOperations() {
1006
+ const operations = new Set();
1007
+ Object.values(this.tableData).forEach(tableInfo => {
1008
+ if (tableInfo.operations) {
1009
+ Object.keys(tableInfo.operations).forEach(op => operations.add(op));
1010
+ }
1011
+ });
1012
+ return Array.from(operations).sort();
1013
+ },
1014
+
1015
+ // Get available tables for filtering
1016
+ getAvailableTables() {
1017
+ return Object.keys(this.tableData).sort();
1018
+ },
1019
+
1020
+ // Select all tables
1021
+ selectAllTables() {
1022
+ this.filters.selectedTables = this.getAvailableTables();
1023
+ this.applyFilters();
1024
+ },
1025
+
1026
+ // Clear table filters
1027
+ clearTableFilters() {
1028
+ this.filters.selectedTables = [];
1029
+ this.applyFilters();
1030
+ },
1031
+
1032
+ // Clear all filters
1033
+ clearAllFilters() {
1034
+ this.filters = {
1035
+ search: '',
1036
+ operation: '',
1037
+ table: '',
1038
+ selectedTables: []
1039
+ };
1040
+ this.applyFilters();
1041
+ },
1042
+
1043
+ // Get active filter count
1044
+ getActiveFilterCount() {
1045
+ let count = 0;
1046
+ if (this.filters.search) count++;
1047
+ if (this.filters.operation) count++;
1048
+ if (this.filters.table) count++;
1049
+ if (this.filters.selectedTables.length > 0) count++;
1050
+ return count;
1051
+ },
1052
+
1053
+ // Get filtered row count
1054
+ getFilteredRowCount() {
1055
+ let count = 0;
1056
+ Object.values(this.tabulators).forEach(tabulator => {
1057
+ if (tabulator) {
1058
+ count += tabulator.getDataCount('active');
1059
+ }
1060
+ });
1061
+ return count;
1062
+ },
1063
+
1064
+ // Get total row count
1065
+ getTotalRowCount() {
1066
+ let count = 0;
1067
+ Object.values(this.tabulators).forEach(tabulator => {
1068
+ if (tabulator) {
1069
+ count += tabulator.getDataCount();
1070
+ }
1071
+ });
1072
+ return count;
1073
+ },
987
1074
 
988
1075
  // Component cleanup
989
1076
  componentDestroy() {
@@ -995,4 +1082,4 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
995
1082
  this.tabulators = {};
996
1083
  }
997
1084
  };
998
- });
1085
+ });