dbwatcher 1.1.1 → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/config/dbwatcher_manifest.js +1 -0
  4. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
  5. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  6. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  7. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  8. data/app/assets/stylesheets/dbwatcher/application.css +691 -41
  9. data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
  10. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  11. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  12. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  13. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  14. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  15. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  16. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  17. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  18. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  19. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  20. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  21. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  22. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  23. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  24. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  25. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  26. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  27. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  28. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  29. data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
  30. data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
  31. data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
  32. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  33. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  34. data/app/views/dbwatcher/sessions/show.html.erb +12 -4
  35. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  36. data/app/views/layouts/dbwatcher/application.html.erb +101 -48
  37. data/config/routes.rb +25 -7
  38. data/lib/dbwatcher/configuration.rb +18 -1
  39. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  40. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  41. data/lib/dbwatcher/services/base_service.rb +2 -0
  42. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  43. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  44. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  45. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  46. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  47. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  48. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  49. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  50. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  51. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  52. data/lib/dbwatcher/storage/session.rb +5 -0
  53. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  54. data/lib/dbwatcher/storage.rb +12 -0
  55. data/lib/dbwatcher/version.rb +1 -1
  56. data/lib/dbwatcher.rb +16 -2
  57. metadata +28 -16
  58. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  59. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  60. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  61. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  62. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  63. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  64. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  65. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  66. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  67. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  68. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  69. /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
@@ -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
-
177
- // Force Tabulator to use our custom ID field
178
- index: 'id', // Tell Tabulator to use the 'id' field as the row identifier
179
-
193
+
194
+ // Force Tabulator to use our custom rowId field
195
+ index: 'rowId', // Tell Tabulator to use the 'rowId' field as the row identifier
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,33 +221,23 @@ 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
-
211
- // Create truly unique row ID using table name and index only (more reliable)
212
- const uniqueId = `${tableName}_row_${index}`;
213
-
214
- // Clean column data to remove any 'id' field that might conflict
215
- const cleanColumnData = { ...columnData };
216
- delete cleanColumnData.id; // Remove any id field from column data
217
-
227
+
228
+ // Create truly unique row ID using table name and index only (for Tabulator internal use)
229
+ const uniqueRowId = `${tableName}_row_${index}`;
230
+
218
231
  const row = {
219
- id: uniqueId, // Force this to be the ID used by Tabulator
232
+ rowId: uniqueRowId, // Tabulator's internal row identifier
220
233
  index: index + 1, // Display index (1-based) - should maintain API order
221
234
  operation: change.operation,
222
235
  timestamp: change.timestamp,
223
236
  table_name: tableName,
224
237
  change_data: change, // Keep original change data with ID intact
225
- original_change_id: change.id, // Store original ID separately
226
- ...cleanColumnData // Use cleaned column data
238
+ ...columnData // Include all column data including actual record ID
227
239
  };
228
-
229
- // Ensure ID is correct
230
- if (row.id !== uniqueId) {
231
- row.id = uniqueId; // Force it back
232
- }
233
-
240
+
234
241
  rows.push(row);
235
242
  });
236
243
 
@@ -251,8 +258,8 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
251
258
  formatter: (cell) => {
252
259
  const rowData = cell.getRow().getData();
253
260
  return `<div class="flex items-center justify-center gap-1">
254
- <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
255
- data-row-id="${rowData.id}">
261
+ <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
262
+ data-row-id="${rowData.rowId}">
256
263
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
257
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"/>
258
265
  </svg>
@@ -312,7 +319,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
312
319
  const value = cell.getValue();
313
320
  const rowData = cell.getRow().getData();
314
321
  const change = rowData.change_data;
315
-
322
+
316
323
  // Handle different operations with appropriate styling
317
324
  if (change.operation === 'UPDATE' && change.changes) {
318
325
  const columnChange = change.changes.find(c => c.column === col);
@@ -344,7 +351,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
344
351
  // Extract column data from change record
345
352
  extractColumnData(change, columns) {
346
353
  const data = {};
347
-
354
+
348
355
  columns.forEach(col => {
349
356
  // Get value from record snapshot or change data
350
357
  if (change.record_snapshot && change.record_snapshot[col] !== undefined) {
@@ -374,8 +381,8 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
374
381
  formatter: (cell) => {
375
382
  const rowData = cell.getRow().getData();
376
383
  return `<div class="flex items-center justify-center gap-1">
377
- <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
378
- data-row-id="${rowData.id}">
384
+ <button class="expand-btn text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
385
+ data-row-id="${rowData.rowId}">
379
386
  <svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
380
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"/>
381
388
  </svg>
@@ -441,7 +448,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
441
448
  const value = cell.getValue();
442
449
  const rowData = cell.getRow().getData();
443
450
  const change = rowData.change_data;
444
-
451
+
445
452
  // Handle different operations with appropriate styling
446
453
  if (change.operation === 'UPDATE' && change.changes) {
447
454
  const columnChange = change.changes.find(c => c.column === col);
@@ -473,15 +480,15 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
473
480
  if (columnName.toLowerCase().includes('id') || columnName.toLowerCase().includes('uuid')) {
474
481
  return 'string';
475
482
  }
476
-
483
+
477
484
  // Timestamp columns - use string sorting to avoid Luxon dependency
478
- if (columnName.toLowerCase().includes('time') ||
485
+ if (columnName.toLowerCase().includes('time') ||
479
486
  columnName.toLowerCase().includes('date') ||
480
487
  columnName.toLowerCase().includes('created') ||
481
488
  columnName.toLowerCase().includes('updated')) {
482
489
  return 'string'; // Changed from 'datetime' to 'string'
483
490
  }
484
-
491
+
485
492
  // Numeric columns
486
493
  if (columnName.toLowerCase().includes('count') ||
487
494
  columnName.toLowerCase().includes('amount') ||
@@ -489,7 +496,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
489
496
  columnName.toLowerCase().includes('quantity')) {
490
497
  return 'number';
491
498
  }
492
-
499
+
493
500
  // Default to alphanum for mixed content
494
501
  return 'alphanum';
495
502
  },
@@ -498,11 +505,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
498
505
  applyHeaderClasses(tableName) {
499
506
  const tabulator = this.tabulators[tableName];
500
507
  if (!tabulator) return;
501
-
508
+
502
509
  const headers = tabulator.getHeaderElements();
503
510
  headers.forEach((header) => {
504
511
  const field = header.getAttribute('tabulator-field');
505
-
512
+
506
513
  if (field === 'index') {
507
514
  header.classList.add('sticky-left-0');
508
515
  } else if (field === 'operation') {
@@ -517,10 +524,10 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
517
524
  applyRowClasses(tableName, row) {
518
525
  const element = row.getElement();
519
526
  const cells = element.querySelectorAll('.tabulator-cell');
520
-
527
+
521
528
  cells.forEach((cell) => {
522
529
  const field = cell.getAttribute('tabulator-field');
523
-
530
+
524
531
  if (field === 'index') {
525
532
  cell.classList.add('sticky-left-0');
526
533
  } else if (field === 'operation') {
@@ -535,7 +542,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
535
542
  customRowFormatter(row) {
536
543
  const rowData = row.getData();
537
544
  const element = row.getElement();
538
-
545
+
539
546
  // Add classes based on operation
540
547
  const operation = rowData.operation;
541
548
  if (operation) {
@@ -546,7 +553,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
546
553
  element.addEventListener('mouseenter', () => {
547
554
  element.style.backgroundColor = '#f3f4f6';
548
555
  });
549
-
556
+
550
557
  element.addEventListener('mouseleave', () => {
551
558
  element.style.backgroundColor = '';
552
559
  });
@@ -556,11 +563,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
556
563
  // Toggle row expansion
557
564
  toggleRowExpansion(rowId) {
558
565
  this.expandedRows[rowId] = !this.expandedRows[rowId];
559
-
566
+
560
567
  // Find the row across all tabulator instances
561
568
  let targetRow = null;
562
569
  let foundInTable = null;
563
-
570
+
564
571
  Object.keys(this.tabulators).forEach(tableName => {
565
572
  const tabulator = this.tabulators[tableName];
566
573
  if (tabulator && !targetRow) {
@@ -576,7 +583,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
576
583
  // Try searching through data if direct lookup fails
577
584
  try {
578
585
  const data = tabulator.getData();
579
- const matchingData = data.find(d => d.id === rowId);
586
+ const matchingData = data.find(d => d.rowId === rowId);
580
587
  if (matchingData) {
581
588
  targetRow = tabulator.getRow(rowId);
582
589
  foundInTable = tableName;
@@ -587,7 +594,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
587
594
  }
588
595
  }
589
596
  });
590
-
597
+
591
598
  if (targetRow) {
592
599
  if (this.expandedRows[rowId]) {
593
600
  this.showRowDetails(targetRow);
@@ -603,34 +610,34 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
603
610
  showRowDetails(row) {
604
611
  const rowData = row.getData();
605
612
  const element = row.getElement();
606
-
607
-
613
+
614
+
608
615
  // Check if detail row already exists
609
616
  const existingDetail = element.nextElementSibling;
610
617
  if (existingDetail && existingDetail.classList.contains('row-detail')) {
611
618
  return; // Already expanded
612
619
  }
613
-
620
+
614
621
  // Create detail row as a proper table row
615
622
  const detailRow = document.createElement('tr');
616
623
  detailRow.className = 'row-detail bg-gray-50';
617
- detailRow.setAttribute('data-parent-id', rowData.id);
618
-
624
+ detailRow.setAttribute('data-parent-id', rowData.rowId);
625
+
619
626
  // Create full-width cell
620
627
  const detailCell = document.createElement('td');
621
628
  detailCell.colSpan = 1000; // Span all columns
622
629
  detailCell.className = 'p-0 border-t border-gray-200';
623
-
630
+
624
631
  try {
625
632
  detailCell.innerHTML = this.generateExpandedContent(rowData);
626
633
  detailRow.appendChild(detailCell);
627
-
634
+
628
635
  // Insert after the current row
629
636
  element.parentNode.insertBefore(detailRow, element.nextSibling);
630
-
637
+
631
638
  // Update expand button
632
639
  this.updateExpandButton(element, true);
633
-
640
+
634
641
  // Dynamically increase table height when expanded
635
642
  const tabulator = this.findTabulatorForRow(rowData.table_name);
636
643
  if (tabulator) {
@@ -641,26 +648,26 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
641
648
  }, 50);
642
649
  }
643
650
  } catch (error) {
644
- console.error(`Error creating detail row for ${rowData.id}:`, error);
651
+ console.error(`Error creating detail row for ${rowData.rowId}:`, error);
645
652
  }
646
653
  },
647
654
 
648
- // Hide row details
655
+ // Hide row details
649
656
  hideRowDetails(row) {
650
657
  const element = row.getElement();
651
658
  const rowData = row.getData();
652
-
653
-
659
+
660
+
654
661
  // Find and remove the detail row
655
- const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.id}"]`);
662
+ const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.rowId}"]`);
656
663
  if (detailRow) {
657
664
  detailRow.remove();
658
665
  } else {
659
666
  }
660
-
667
+
661
668
  // Update expand button
662
669
  this.updateExpandButton(element, false);
663
-
670
+
664
671
  // Shrink table height back when collapsed
665
672
  const tabulator = this.findTabulatorForRow(rowData.table_name);
666
673
  if (tabulator) {
@@ -685,12 +692,12 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
685
692
  }
686
693
  },
687
694
 
688
- // Generate expanded content for a row - inline table format
695
+ // Generate expanded content for a row - inline table format
689
696
  generateExpandedContent(rowData) {
690
697
  const change = rowData.change_data;
691
698
  const tableInfo = this.tableData[rowData.table_name];
692
699
  const columns = tableInfo ? tableInfo.columns : [];
693
-
700
+
694
701
  // Create a table row that matches the column structure
695
702
  let content = `
696
703
  <div class="bg-gray-50 border-t border-gray-200">
@@ -710,21 +717,21 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
710
717
  <div class="text-gray-500 mt-1">${rowData.table_name}</div>
711
718
  </div>
712
719
  `;
713
-
720
+
714
721
  if (change.operation === 'UPDATE' && change.changes) {
715
722
  content += `<div class="text-blue-600 font-medium">${change.changes.length} columns modified</div>`;
716
723
  }
717
-
724
+
718
725
  if (change.record_snapshot && (change.record_snapshot.id || change.record_snapshot.uuid)) {
719
726
  const fullId = change.record_snapshot.id || change.record_snapshot.uuid;
720
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>`;
721
728
  }
722
-
729
+
723
730
  content += `
724
731
  </div>
725
732
  </td>
726
733
  `;
727
-
734
+
728
735
  // Add detail cells for each column that matches the table structure
729
736
  columns.forEach(col => {
730
737
  // Only show if column is visible in the main table
@@ -734,7 +741,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
734
741
  <div class="text-xs font-medium text-gray-600 mb-1">${col}</div>
735
742
  <div class="text-xs">
736
743
  `;
737
-
744
+
738
745
  if (change.operation === 'UPDATE' && change.changes) {
739
746
  const columnChange = change.changes.find(c => c.column === col);
740
747
  if (columnChange) {
@@ -789,21 +796,21 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
789
796
  </div>
790
797
  `;
791
798
  }
792
-
799
+
793
800
  content += `
794
801
  </div>
795
802
  </td>
796
803
  `;
797
804
  }
798
805
  });
799
-
806
+
800
807
  content += `
801
808
  </tr>
802
809
  </tbody>
803
810
  </table>
804
811
  </div>
805
812
  `;
806
-
813
+
807
814
  return content;
808
815
  },
809
816
 
@@ -812,7 +819,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
812
819
  if (value === null || value === undefined) {
813
820
  return '<span class="text-gray-400 italic">NULL</span>';
814
821
  }
815
-
822
+
816
823
  if (typeof value === 'string' && this.isJsonValue(value)) {
817
824
  try {
818
825
  const parsed = JSON.parse(value);
@@ -821,7 +828,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
821
828
  return String(value);
822
829
  }
823
830
  }
824
-
831
+
825
832
  return String(value);
826
833
  },
827
834
 
@@ -841,11 +848,11 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
841
848
  if (value === null || value === undefined) {
842
849
  return '<span class="text-gray-400">NULL</span>';
843
850
  }
844
-
851
+
845
852
  if (typeof value === 'string' && value.length > 50) {
846
853
  return `<span title="${value}">${value.substring(0, 50)}...</span>`;
847
854
  }
848
-
855
+
849
856
  return String(value);
850
857
  },
851
858
 
@@ -898,7 +905,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
898
905
  // Initialize column visibility state (per table)
899
906
  initializeColumnVisibility() {
900
907
  this.tableColumns = {};
901
-
908
+
902
909
  Object.keys(this.tableData).forEach(tableName => {
903
910
  const tableInfo = this.tableData[tableName];
904
911
  if (tableInfo && tableInfo.columns) {
@@ -926,7 +933,7 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
926
933
  // Rebuild columns for this table
927
934
  const tableInfo = this.tableData[tableName];
928
935
  const newColumns = this.buildColumnsForTable(tableName, tableInfo);
929
-
936
+
930
937
  // Update the tabulator columns
931
938
  tabulator.setColumns(newColumns);
932
939
  },
@@ -994,6 +1001,76 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
994
1001
  });
995
1002
  },
996
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
+ },
997
1074
 
998
1075
  // Component cleanup
999
1076
  componentDestroy() {
@@ -1005,4 +1082,4 @@ DBWatcher.registerComponent('changesTableHybrid', function(config) {
1005
1082
  this.tabulators = {};
1006
1083
  }
1007
1084
  };
1008
- });
1085
+ });