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.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/app/assets/config/dbwatcher_manifest.js +1 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
- data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
- data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
- data/app/assets/stylesheets/dbwatcher/application.css +691 -41
- data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
- data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
- data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
- data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
- data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
- data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
- data/app/helpers/dbwatcher/application_helper.rb +43 -11
- data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
- data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
- data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
- data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
- data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
- data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
- data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
- data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
- data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
- data/app/views/dbwatcher/sessions/index.html.erb +107 -87
- data/app/views/dbwatcher/sessions/show.html.erb +12 -4
- data/app/views/dbwatcher/tables/index.html.erb +32 -40
- data/app/views/layouts/dbwatcher/application.html.erb +101 -48
- data/config/routes.rb +25 -7
- data/lib/dbwatcher/configuration.rb +18 -1
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
- data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
- data/lib/dbwatcher/services/base_service.rb +2 -0
- data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
- data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
- data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
- data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
- data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
- data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
- data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
- data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
- data/lib/dbwatcher/storage/session.rb +5 -0
- data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
- data/lib/dbwatcher/storage.rb +12 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +16 -2
- metadata +28 -16
- data/app/helpers/dbwatcher/component_helper.rb +0 -29
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
- data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
- data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
- data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
- /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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
field: 'table_name',
|
91
|
-
type: '=',
|
92
|
-
value: this.filters.table
|
93
|
-
});
|
94
|
-
}
|
104
|
+
return true;
|
105
|
+
});
|
106
|
+
}
|
95
107
|
|
96
|
-
|
97
|
-
|
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}/
|
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
|
178
|
-
index: '
|
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 (
|
212
|
-
const
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
+
});
|