dbwatcher 1.0.0 → 1.1.1
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 +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +603 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +140 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +118 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,1008 @@
|
|
1
|
+
/**
|
2
|
+
* Changes Table Hybrid Component v3.0
|
3
|
+
* Alpine.js + Tabulator.js implementation for DBWatcher changes tab
|
4
|
+
* Production version - all issues fixed, debugging removed
|
5
|
+
*/
|
6
|
+
|
7
|
+
// Register component with DBWatcher
|
8
|
+
DBWatcher.registerComponent('changesTableHybrid', function(config) {
|
9
|
+
const baseComponent = DBWatcher.BaseComponent ? DBWatcher.BaseComponent(config) : {};
|
10
|
+
|
11
|
+
return {
|
12
|
+
// Include BaseComponent
|
13
|
+
...baseComponent,
|
14
|
+
|
15
|
+
// Component-specific state
|
16
|
+
sessionId: config.sessionId || null,
|
17
|
+
tableData: {},
|
18
|
+
filters: {
|
19
|
+
search: '',
|
20
|
+
operation: '',
|
21
|
+
table: ''
|
22
|
+
},
|
23
|
+
showColumnSelector: null,
|
24
|
+
expandedRows: {},
|
25
|
+
tabulators: {}, // Multiple tabulator instances (one per table)
|
26
|
+
tableColumns: {}, // Per-table column visibility
|
27
|
+
|
28
|
+
// Alpine init hook (auto-called by Alpine.js)
|
29
|
+
init() {
|
30
|
+
// Initialize empty state
|
31
|
+
this.tableColumns = {};
|
32
|
+
this.expandedRows = {};
|
33
|
+
|
34
|
+
// Setup filtering
|
35
|
+
this.setupFiltering();
|
36
|
+
|
37
|
+
// Load data from API
|
38
|
+
this.loadChangesData();
|
39
|
+
|
40
|
+
// Setup URL state sync
|
41
|
+
this.setupURLStateSync();
|
42
|
+
|
43
|
+
// Call base init if it exists
|
44
|
+
if (baseComponent.init) {
|
45
|
+
baseComponent.init.call(this);
|
46
|
+
}
|
47
|
+
},
|
48
|
+
|
49
|
+
// Setup filtering with debouncing
|
50
|
+
setupFiltering() {
|
51
|
+
// Use library debouncing from BaseComponent
|
52
|
+
this.applyFilters = this.debounce(() => {
|
53
|
+
this.applyTabulatorFilters();
|
54
|
+
this.updateURL();
|
55
|
+
}, 300);
|
56
|
+
},
|
57
|
+
|
58
|
+
// Apply filters directly to Tabulator (no server reload needed)
|
59
|
+
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 = [];
|
67
|
+
|
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
|
+
}
|
77
|
+
|
78
|
+
// Operation filter
|
79
|
+
if (this.filters.operation) {
|
80
|
+
filters.push({
|
81
|
+
field: 'operation',
|
82
|
+
type: '=',
|
83
|
+
value: this.filters.operation
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
// Table filter
|
88
|
+
if (this.filters.table) {
|
89
|
+
filters.push({
|
90
|
+
field: 'table_name',
|
91
|
+
type: '=',
|
92
|
+
value: this.filters.table
|
93
|
+
});
|
94
|
+
}
|
95
|
+
|
96
|
+
// Apply all filters
|
97
|
+
if (filters.length > 0) {
|
98
|
+
this.tabulator.setFilter(filters);
|
99
|
+
}
|
100
|
+
},
|
101
|
+
|
102
|
+
// Load data from API
|
103
|
+
async loadChangesData() {
|
104
|
+
if (!this.sessionId) {
|
105
|
+
console.error('No session ID provided to changes table hybrid component');
|
106
|
+
this.handleError(new Error('No session ID provided'));
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
|
110
|
+
this.setLoading(true);
|
111
|
+
this.clearError();
|
112
|
+
|
113
|
+
try {
|
114
|
+
// Build query parameters from filters
|
115
|
+
const params = new URLSearchParams();
|
116
|
+
if (this.filters.table) params.append('table', this.filters.table);
|
117
|
+
if (this.filters.operation) params.append('operation', this.filters.operation);
|
118
|
+
if (this.filters.search) params.append('search', this.filters.search);
|
119
|
+
|
120
|
+
const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/changes_data?${params.toString()}`;
|
121
|
+
const data = await this.fetchData(url);
|
122
|
+
|
123
|
+
if (data.tables_summary) {
|
124
|
+
this.tableData = data.tables_summary;
|
125
|
+
this.initializeColumnVisibility();
|
126
|
+
this.initializeTabulators();
|
127
|
+
} else {
|
128
|
+
throw new Error('No changes data received');
|
129
|
+
}
|
130
|
+
} catch (error) {
|
131
|
+
this.handleError(error);
|
132
|
+
} finally {
|
133
|
+
this.setLoading(false);
|
134
|
+
}
|
135
|
+
},
|
136
|
+
|
137
|
+
// Initialize Tabulator tables (one per table)
|
138
|
+
initializeTabulators() {
|
139
|
+
this.$nextTick(() => {
|
140
|
+
// Destroy existing tables
|
141
|
+
Object.values(this.tabulators).forEach(tabulator => {
|
142
|
+
if (tabulator) tabulator.destroy();
|
143
|
+
});
|
144
|
+
this.tabulators = {};
|
145
|
+
|
146
|
+
// Create one Tabulator instance per table
|
147
|
+
Object.keys(this.tableData).forEach(tableName => {
|
148
|
+
this.initializeTableTabulator(tableName);
|
149
|
+
});
|
150
|
+
});
|
151
|
+
},
|
152
|
+
|
153
|
+
// Initialize Tabulator for a specific table
|
154
|
+
initializeTableTabulator(tableName) {
|
155
|
+
const container = document.getElementById(`changes-table-${tableName}`);
|
156
|
+
if (!container) {
|
157
|
+
console.warn(`Table container not found for ${tableName}`);
|
158
|
+
return;
|
159
|
+
}
|
160
|
+
|
161
|
+
const tableInfo = this.tableData[tableName];
|
162
|
+
if (!tableInfo || !tableInfo.changes || tableInfo.changes.length === 0) {
|
163
|
+
container.innerHTML = '<div class="p-4 text-gray-500 text-center">No changes for this table</div>';
|
164
|
+
return;
|
165
|
+
}
|
166
|
+
|
167
|
+
// Transform data for this specific table
|
168
|
+
const tabulatorData = this.transformTableDataForTabulator(tableName, tableInfo);
|
169
|
+
|
170
|
+
// Create Tabulator instance for this table
|
171
|
+
this.tabulators[tableName] = new Tabulator(container, {
|
172
|
+
data: tabulatorData,
|
173
|
+
layout: 'fitDataFill',
|
174
|
+
responsiveLayout: false,
|
175
|
+
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
|
+
|
180
|
+
// Performance optimizations - disable virtual DOM to ensure all rows render
|
181
|
+
virtualDom: false,
|
182
|
+
pagination: false, // Ensure no pagination
|
183
|
+
|
184
|
+
// Column configuration for this table
|
185
|
+
columns: this.buildColumnsForTable(tableName, tableInfo),
|
186
|
+
|
187
|
+
// Row formatting
|
188
|
+
rowFormatter: this.customRowFormatter.bind(this),
|
189
|
+
|
190
|
+
// No initial sorting - data should be in correct order from API
|
191
|
+
// initialSort: [],
|
192
|
+
|
193
|
+
// Enable header sorting
|
194
|
+
headerSortTristate: true,
|
195
|
+
|
196
|
+
// Callbacks
|
197
|
+
headerBuilt: () => this.applyHeaderClasses(tableName),
|
198
|
+
rowBuilt: (row) => this.applyRowClasses(tableName, row)
|
199
|
+
});
|
200
|
+
|
201
|
+
},
|
202
|
+
|
203
|
+
// Transform data for a specific table
|
204
|
+
transformTableDataForTabulator(tableName, tableInfo) {
|
205
|
+
const rows = [];
|
206
|
+
const changes = tableInfo.changes || [];
|
207
|
+
|
208
|
+
changes.forEach((change, index) => {
|
209
|
+
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
|
+
|
218
|
+
const row = {
|
219
|
+
id: uniqueId, // Force this to be the ID used by Tabulator
|
220
|
+
index: index + 1, // Display index (1-based) - should maintain API order
|
221
|
+
operation: change.operation,
|
222
|
+
timestamp: change.timestamp,
|
223
|
+
table_name: tableName,
|
224
|
+
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
|
227
|
+
};
|
228
|
+
|
229
|
+
// Ensure ID is correct
|
230
|
+
if (row.id !== uniqueId) {
|
231
|
+
row.id = uniqueId; // Force it back
|
232
|
+
}
|
233
|
+
|
234
|
+
rows.push(row);
|
235
|
+
});
|
236
|
+
|
237
|
+
return rows;
|
238
|
+
},
|
239
|
+
|
240
|
+
// Build columns for a specific table
|
241
|
+
buildColumnsForTable(tableName, tableInfo) {
|
242
|
+
const columns = [
|
243
|
+
{
|
244
|
+
title: '#',
|
245
|
+
field: 'index',
|
246
|
+
width: 60,
|
247
|
+
frozen: true,
|
248
|
+
headerSort: false,
|
249
|
+
cssClass: 'sticky-left-0',
|
250
|
+
titleFormatter: () => '<span class="text-xs">#</span>',
|
251
|
+
formatter: (cell) => {
|
252
|
+
const rowData = cell.getRow().getData();
|
253
|
+
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}">
|
256
|
+
<svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
257
|
+
<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
|
+
</svg>
|
259
|
+
</button>
|
260
|
+
<span class="text-xs text-gray-500">${rowData.index}</span>
|
261
|
+
</div>`;
|
262
|
+
},
|
263
|
+
cellClick: (e, _cell) => {
|
264
|
+
e.stopPropagation();
|
265
|
+
const target = e.target.closest('.expand-btn');
|
266
|
+
if (target) {
|
267
|
+
const rowId = target.getAttribute('data-row-id');
|
268
|
+
this.toggleRowExpansion(rowId);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
},
|
272
|
+
{
|
273
|
+
title: 'Op',
|
274
|
+
field: 'operation',
|
275
|
+
width: 48,
|
276
|
+
frozen: true,
|
277
|
+
headerSort: false,
|
278
|
+
cssClass: 'sticky-left-1',
|
279
|
+
titleFormatter: () => '<span class="text-xs">Op</span>',
|
280
|
+
formatter: (cell) => {
|
281
|
+
const op = cell.getValue();
|
282
|
+
return `<span class="badge badge-${op.toLowerCase()}">${op.charAt(0)}</span>`;
|
283
|
+
}
|
284
|
+
},
|
285
|
+
{
|
286
|
+
title: 'Timestamp',
|
287
|
+
field: 'timestamp',
|
288
|
+
width: 160,
|
289
|
+
frozen: true,
|
290
|
+
cssClass: 'sticky-left-2',
|
291
|
+
titleFormatter: () => '<span class="text-xs">Timestamp</span>',
|
292
|
+
sorter: 'string', // Changed from 'datetime' to 'string'
|
293
|
+
formatter: (cell) => {
|
294
|
+
return `<span class="text-xs text-gray-600">${this.formatTimestamp(cell.getValue())}</span>`;
|
295
|
+
}
|
296
|
+
}
|
297
|
+
];
|
298
|
+
|
299
|
+
// Add columns specific to this table
|
300
|
+
if (tableInfo.columns) {
|
301
|
+
tableInfo.columns.forEach(col => {
|
302
|
+
// Only add column if it's visible
|
303
|
+
if (this.isColumnVisible(tableName, col)) {
|
304
|
+
columns.push({
|
305
|
+
title: col,
|
306
|
+
field: col,
|
307
|
+
minWidth: 100,
|
308
|
+
titleFormatter: () => `<span class="text-xs">${col}</span>`,
|
309
|
+
headerSort: true,
|
310
|
+
sorter: this.getColumnSorter(col),
|
311
|
+
formatter: (cell) => {
|
312
|
+
const value = cell.getValue();
|
313
|
+
const rowData = cell.getRow().getData();
|
314
|
+
const change = rowData.change_data;
|
315
|
+
|
316
|
+
// Handle different operations with appropriate styling
|
317
|
+
if (change.operation === 'UPDATE' && change.changes) {
|
318
|
+
const columnChange = change.changes.find(c => c.column === col);
|
319
|
+
if (columnChange) {
|
320
|
+
return `<div class="text-xs space-y-1">
|
321
|
+
<div class="text-red-600 line-through">${this.formatCellValue(columnChange.old_value)}</div>
|
322
|
+
<div class="text-green-600 font-medium">${this.formatCellValue(columnChange.new_value)}</div>
|
323
|
+
</div>`;
|
324
|
+
} else {
|
325
|
+
return `<span class="text-xs text-gray-700">${this.formatCellValue(value)}</span>`;
|
326
|
+
}
|
327
|
+
} else if (change.operation === 'INSERT') {
|
328
|
+
return `<span class="text-xs text-green-600 font-medium">${this.formatCellValue(value)}</span>`;
|
329
|
+
} else if (change.operation === 'DELETE') {
|
330
|
+
return `<span class="text-xs text-red-600 line-through">${this.formatCellValue(value)}</span>`;
|
331
|
+
} else {
|
332
|
+
return `<span class="text-xs text-gray-700">${this.formatCellValue(value)}</span>`;
|
333
|
+
}
|
334
|
+
}
|
335
|
+
});
|
336
|
+
}
|
337
|
+
});
|
338
|
+
}
|
339
|
+
|
340
|
+
return columns;
|
341
|
+
},
|
342
|
+
|
343
|
+
|
344
|
+
// Extract column data from change record
|
345
|
+
extractColumnData(change, columns) {
|
346
|
+
const data = {};
|
347
|
+
|
348
|
+
columns.forEach(col => {
|
349
|
+
// Get value from record snapshot or change data
|
350
|
+
if (change.record_snapshot && change.record_snapshot[col] !== undefined) {
|
351
|
+
data[col] = change.record_snapshot[col];
|
352
|
+
} else if (change.operation === 'UPDATE' && change.changes) {
|
353
|
+
const columnChange = change.changes.find(c => c.column === col);
|
354
|
+
if (columnChange) {
|
355
|
+
data[col] = columnChange.new_value;
|
356
|
+
}
|
357
|
+
}
|
358
|
+
});
|
359
|
+
|
360
|
+
return data;
|
361
|
+
},
|
362
|
+
|
363
|
+
// Build column configuration for Tabulator
|
364
|
+
buildColumns() {
|
365
|
+
const columns = [
|
366
|
+
{
|
367
|
+
title: '#',
|
368
|
+
field: 'index',
|
369
|
+
width: 60,
|
370
|
+
frozen: true,
|
371
|
+
headerSort: false,
|
372
|
+
cssClass: 'sticky-left-0',
|
373
|
+
titleFormatter: () => '<span class="text-xs">#</span>',
|
374
|
+
formatter: (cell) => {
|
375
|
+
const rowData = cell.getRow().getData();
|
376
|
+
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}">
|
379
|
+
<svg class="w-3 h-3 transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
380
|
+
<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
|
+
</svg>
|
382
|
+
</button>
|
383
|
+
<span class="text-xs text-gray-500">${rowData.index}</span>
|
384
|
+
</div>`;
|
385
|
+
},
|
386
|
+
cellClick: (e, _cell) => {
|
387
|
+
e.stopPropagation();
|
388
|
+
const target = e.target.closest('.expand-btn');
|
389
|
+
if (target) {
|
390
|
+
const rowId = target.getAttribute('data-row-id');
|
391
|
+
this.toggleRowExpansion(rowId);
|
392
|
+
}
|
393
|
+
}
|
394
|
+
},
|
395
|
+
{
|
396
|
+
title: 'Op',
|
397
|
+
field: 'operation',
|
398
|
+
width: 48,
|
399
|
+
frozen: true,
|
400
|
+
headerSort: false,
|
401
|
+
cssClass: 'sticky-left-1',
|
402
|
+
titleFormatter: () => '<span class="text-xs">Op</span>',
|
403
|
+
formatter: (cell) => {
|
404
|
+
const op = cell.getValue();
|
405
|
+
return `<span class="badge badge-${op.toLowerCase()}">${op.charAt(0)}</span>`;
|
406
|
+
}
|
407
|
+
},
|
408
|
+
{
|
409
|
+
title: 'Timestamp',
|
410
|
+
field: 'timestamp',
|
411
|
+
width: 160,
|
412
|
+
frozen: true,
|
413
|
+
cssClass: 'sticky-left-2',
|
414
|
+
titleFormatter: () => '<span class="text-xs">Timestamp</span>',
|
415
|
+
sorter: 'string', // Changed from 'datetime' to 'string'
|
416
|
+
formatter: (cell) => {
|
417
|
+
return `<span class="text-xs text-gray-600">${this.formatTimestamp(cell.getValue())}</span>`;
|
418
|
+
}
|
419
|
+
}
|
420
|
+
];
|
421
|
+
|
422
|
+
// Add dynamic columns based on table data
|
423
|
+
const allColumns = new Set();
|
424
|
+
Object.keys(this.tableData).forEach(tableName => {
|
425
|
+
const tableInfo = this.tableData[tableName];
|
426
|
+
if (tableInfo.columns) {
|
427
|
+
tableInfo.columns.forEach(col => allColumns.add(col));
|
428
|
+
}
|
429
|
+
});
|
430
|
+
|
431
|
+
// Add each column
|
432
|
+
allColumns.forEach(col => {
|
433
|
+
columns.push({
|
434
|
+
title: col,
|
435
|
+
field: col,
|
436
|
+
minWidth: 100,
|
437
|
+
titleFormatter: () => `<span class="text-xs">${col}</span>`,
|
438
|
+
headerSort: true,
|
439
|
+
sorter: this.getColumnSorter(col),
|
440
|
+
formatter: (cell) => {
|
441
|
+
const value = cell.getValue();
|
442
|
+
const rowData = cell.getRow().getData();
|
443
|
+
const change = rowData.change_data;
|
444
|
+
|
445
|
+
// Handle different operations with appropriate styling
|
446
|
+
if (change.operation === 'UPDATE' && change.changes) {
|
447
|
+
const columnChange = change.changes.find(c => c.column === col);
|
448
|
+
if (columnChange) {
|
449
|
+
return `<div class="text-xs space-y-1">
|
450
|
+
<div class="text-red-600 line-through">${this.formatCellValue(columnChange.old_value)}</div>
|
451
|
+
<div class="text-green-600 font-medium">${this.formatCellValue(columnChange.new_value)}</div>
|
452
|
+
</div>`;
|
453
|
+
} else {
|
454
|
+
return `<span class="text-xs text-gray-700">${this.formatCellValue(value)}</span>`;
|
455
|
+
}
|
456
|
+
} else if (change.operation === 'INSERT') {
|
457
|
+
return `<span class="text-xs text-green-600 font-medium">${this.formatCellValue(value)}</span>`;
|
458
|
+
} else if (change.operation === 'DELETE') {
|
459
|
+
return `<span class="text-xs text-red-600 line-through">${this.formatCellValue(value)}</span>`;
|
460
|
+
} else {
|
461
|
+
return `<span class="text-xs text-gray-700">${this.formatCellValue(value)}</span>`;
|
462
|
+
}
|
463
|
+
}
|
464
|
+
});
|
465
|
+
});
|
466
|
+
|
467
|
+
return columns;
|
468
|
+
},
|
469
|
+
|
470
|
+
// Get appropriate sorter for column based on data type
|
471
|
+
getColumnSorter(columnName) {
|
472
|
+
// Common ID columns
|
473
|
+
if (columnName.toLowerCase().includes('id') || columnName.toLowerCase().includes('uuid')) {
|
474
|
+
return 'string';
|
475
|
+
}
|
476
|
+
|
477
|
+
// Timestamp columns - use string sorting to avoid Luxon dependency
|
478
|
+
if (columnName.toLowerCase().includes('time') ||
|
479
|
+
columnName.toLowerCase().includes('date') ||
|
480
|
+
columnName.toLowerCase().includes('created') ||
|
481
|
+
columnName.toLowerCase().includes('updated')) {
|
482
|
+
return 'string'; // Changed from 'datetime' to 'string'
|
483
|
+
}
|
484
|
+
|
485
|
+
// Numeric columns
|
486
|
+
if (columnName.toLowerCase().includes('count') ||
|
487
|
+
columnName.toLowerCase().includes('amount') ||
|
488
|
+
columnName.toLowerCase().includes('price') ||
|
489
|
+
columnName.toLowerCase().includes('quantity')) {
|
490
|
+
return 'number';
|
491
|
+
}
|
492
|
+
|
493
|
+
// Default to alphanum for mixed content
|
494
|
+
return 'alphanum';
|
495
|
+
},
|
496
|
+
|
497
|
+
// Apply header classes after Tabulator builds headers
|
498
|
+
applyHeaderClasses(tableName) {
|
499
|
+
const tabulator = this.tabulators[tableName];
|
500
|
+
if (!tabulator) return;
|
501
|
+
|
502
|
+
const headers = tabulator.getHeaderElements();
|
503
|
+
headers.forEach((header) => {
|
504
|
+
const field = header.getAttribute('tabulator-field');
|
505
|
+
|
506
|
+
if (field === 'index') {
|
507
|
+
header.classList.add('sticky-left-0');
|
508
|
+
} else if (field === 'operation') {
|
509
|
+
header.classList.add('sticky-left-1');
|
510
|
+
} else if (field === 'timestamp') {
|
511
|
+
header.classList.add('sticky-left-2');
|
512
|
+
}
|
513
|
+
});
|
514
|
+
},
|
515
|
+
|
516
|
+
// Apply row classes after Tabulator builds rows
|
517
|
+
applyRowClasses(tableName, row) {
|
518
|
+
const element = row.getElement();
|
519
|
+
const cells = element.querySelectorAll('.tabulator-cell');
|
520
|
+
|
521
|
+
cells.forEach((cell) => {
|
522
|
+
const field = cell.getAttribute('tabulator-field');
|
523
|
+
|
524
|
+
if (field === 'index') {
|
525
|
+
cell.classList.add('sticky-left-0');
|
526
|
+
} else if (field === 'operation') {
|
527
|
+
cell.classList.add('sticky-left-1');
|
528
|
+
} else if (field === 'timestamp') {
|
529
|
+
cell.classList.add('sticky-left-2');
|
530
|
+
}
|
531
|
+
});
|
532
|
+
},
|
533
|
+
|
534
|
+
// Custom row formatter
|
535
|
+
customRowFormatter(row) {
|
536
|
+
const rowData = row.getData();
|
537
|
+
const element = row.getElement();
|
538
|
+
|
539
|
+
// Add classes based on operation
|
540
|
+
const operation = rowData.operation;
|
541
|
+
if (operation) {
|
542
|
+
element.classList.add(`operation-${operation.toLowerCase()}`);
|
543
|
+
}
|
544
|
+
|
545
|
+
// Add hover effects
|
546
|
+
element.addEventListener('mouseenter', () => {
|
547
|
+
element.style.backgroundColor = '#f3f4f6';
|
548
|
+
});
|
549
|
+
|
550
|
+
element.addEventListener('mouseleave', () => {
|
551
|
+
element.style.backgroundColor = '';
|
552
|
+
});
|
553
|
+
},
|
554
|
+
|
555
|
+
|
556
|
+
// Toggle row expansion
|
557
|
+
toggleRowExpansion(rowId) {
|
558
|
+
this.expandedRows[rowId] = !this.expandedRows[rowId];
|
559
|
+
|
560
|
+
// Find the row across all tabulator instances
|
561
|
+
let targetRow = null;
|
562
|
+
let foundInTable = null;
|
563
|
+
|
564
|
+
Object.keys(this.tabulators).forEach(tableName => {
|
565
|
+
const tabulator = this.tabulators[tableName];
|
566
|
+
if (tabulator && !targetRow) {
|
567
|
+
try {
|
568
|
+
// Search by row ID directly
|
569
|
+
const row = tabulator.getRow(rowId);
|
570
|
+
if (row) {
|
571
|
+
targetRow = row;
|
572
|
+
foundInTable = tableName;
|
573
|
+
return; // Found it, stop searching
|
574
|
+
}
|
575
|
+
} catch (e) {
|
576
|
+
// Try searching through data if direct lookup fails
|
577
|
+
try {
|
578
|
+
const data = tabulator.getData();
|
579
|
+
const matchingData = data.find(d => d.id === rowId);
|
580
|
+
if (matchingData) {
|
581
|
+
targetRow = tabulator.getRow(rowId);
|
582
|
+
foundInTable = tableName;
|
583
|
+
}
|
584
|
+
} catch (e2) {
|
585
|
+
// Row not found in this tabulator, continue searching
|
586
|
+
}
|
587
|
+
}
|
588
|
+
}
|
589
|
+
});
|
590
|
+
|
591
|
+
if (targetRow) {
|
592
|
+
if (this.expandedRows[rowId]) {
|
593
|
+
this.showRowDetails(targetRow);
|
594
|
+
} else {
|
595
|
+
this.hideRowDetails(targetRow);
|
596
|
+
}
|
597
|
+
} else {
|
598
|
+
console.warn(`Row ${rowId} not found in any table`);
|
599
|
+
}
|
600
|
+
},
|
601
|
+
|
602
|
+
// Show row details
|
603
|
+
showRowDetails(row) {
|
604
|
+
const rowData = row.getData();
|
605
|
+
const element = row.getElement();
|
606
|
+
|
607
|
+
|
608
|
+
// Check if detail row already exists
|
609
|
+
const existingDetail = element.nextElementSibling;
|
610
|
+
if (existingDetail && existingDetail.classList.contains('row-detail')) {
|
611
|
+
return; // Already expanded
|
612
|
+
}
|
613
|
+
|
614
|
+
// Create detail row as a proper table row
|
615
|
+
const detailRow = document.createElement('tr');
|
616
|
+
detailRow.className = 'row-detail bg-gray-50';
|
617
|
+
detailRow.setAttribute('data-parent-id', rowData.id);
|
618
|
+
|
619
|
+
// Create full-width cell
|
620
|
+
const detailCell = document.createElement('td');
|
621
|
+
detailCell.colSpan = 1000; // Span all columns
|
622
|
+
detailCell.className = 'p-0 border-t border-gray-200';
|
623
|
+
|
624
|
+
try {
|
625
|
+
detailCell.innerHTML = this.generateExpandedContent(rowData);
|
626
|
+
detailRow.appendChild(detailCell);
|
627
|
+
|
628
|
+
// Insert after the current row
|
629
|
+
element.parentNode.insertBefore(detailRow, element.nextSibling);
|
630
|
+
|
631
|
+
// Update expand button
|
632
|
+
this.updateExpandButton(element, true);
|
633
|
+
|
634
|
+
// Dynamically increase table height when expanded
|
635
|
+
const tabulator = this.findTabulatorForRow(rowData.table_name);
|
636
|
+
if (tabulator) {
|
637
|
+
setTimeout(() => {
|
638
|
+
const currentHeight = tabulator.getElement().offsetHeight;
|
639
|
+
const expandedHeight = Math.max(currentHeight + 200, 300); // Add 200px for expanded content
|
640
|
+
tabulator.setHeight(expandedHeight);
|
641
|
+
}, 50);
|
642
|
+
}
|
643
|
+
} catch (error) {
|
644
|
+
console.error(`Error creating detail row for ${rowData.id}:`, error);
|
645
|
+
}
|
646
|
+
},
|
647
|
+
|
648
|
+
// Hide row details
|
649
|
+
hideRowDetails(row) {
|
650
|
+
const element = row.getElement();
|
651
|
+
const rowData = row.getData();
|
652
|
+
|
653
|
+
|
654
|
+
// Find and remove the detail row
|
655
|
+
const detailRow = element.parentNode.querySelector(`tr.row-detail[data-parent-id="${rowData.id}"]`);
|
656
|
+
if (detailRow) {
|
657
|
+
detailRow.remove();
|
658
|
+
} else {
|
659
|
+
}
|
660
|
+
|
661
|
+
// Update expand button
|
662
|
+
this.updateExpandButton(element, false);
|
663
|
+
|
664
|
+
// Shrink table height back when collapsed
|
665
|
+
const tabulator = this.findTabulatorForRow(rowData.table_name);
|
666
|
+
if (tabulator) {
|
667
|
+
setTimeout(() => {
|
668
|
+
const tableData = tabulator.getData();
|
669
|
+
const baseHeight = Math.max(200, Math.min(400, (tableData.length * 35) + 80));
|
670
|
+
tabulator.setHeight(baseHeight);
|
671
|
+
}, 50);
|
672
|
+
}
|
673
|
+
},
|
674
|
+
|
675
|
+
// Helper method to find tabulator instance for a table
|
676
|
+
findTabulatorForRow(tableName) {
|
677
|
+
return this.tabulators[tableName] || null;
|
678
|
+
},
|
679
|
+
|
680
|
+
// Update expand button state
|
681
|
+
updateExpandButton(rowElement, expanded) {
|
682
|
+
const expandBtn = rowElement.querySelector('.expand-btn svg');
|
683
|
+
if (expandBtn) {
|
684
|
+
expandBtn.style.transform = expanded ? 'rotate(90deg)' : 'rotate(0deg)';
|
685
|
+
}
|
686
|
+
},
|
687
|
+
|
688
|
+
// Generate expanded content for a row - inline table format
|
689
|
+
generateExpandedContent(rowData) {
|
690
|
+
const change = rowData.change_data;
|
691
|
+
const tableInfo = this.tableData[rowData.table_name];
|
692
|
+
const columns = tableInfo ? tableInfo.columns : [];
|
693
|
+
|
694
|
+
// Create a table row that matches the column structure
|
695
|
+
let content = `
|
696
|
+
<div class="bg-gray-50 border-t border-gray-200">
|
697
|
+
<table class="w-full" style="table-layout: fixed;">
|
698
|
+
<tbody>
|
699
|
+
<tr>
|
700
|
+
<!-- Combined details for first 3 columns (index + op + timestamp) -->
|
701
|
+
<td class="sticky-left-0 bg-gray-100 border-r border-gray-300 p-2 align-top" style="width: 268px; min-width: 268px;">
|
702
|
+
<div class="text-xs font-medium text-gray-600 mb-2">Change Details</div>
|
703
|
+
<div class="space-y-2 text-xs">
|
704
|
+
<div class="flex items-center justify-between">
|
705
|
+
<span class="text-gray-500">Row #${rowData.index}</span>
|
706
|
+
<span class="badge badge-${change.operation.toLowerCase()}">${change.operation}</span>
|
707
|
+
</div>
|
708
|
+
<div class="text-gray-700">
|
709
|
+
<div class="font-medium">${this.formatDate(change.timestamp)}</div>
|
710
|
+
<div class="text-gray-500 mt-1">${rowData.table_name}</div>
|
711
|
+
</div>
|
712
|
+
`;
|
713
|
+
|
714
|
+
if (change.operation === 'UPDATE' && change.changes) {
|
715
|
+
content += `<div class="text-blue-600 font-medium">${change.changes.length} columns modified</div>`;
|
716
|
+
}
|
717
|
+
|
718
|
+
if (change.record_snapshot && (change.record_snapshot.id || change.record_snapshot.uuid)) {
|
719
|
+
const fullId = change.record_snapshot.id || change.record_snapshot.uuid;
|
720
|
+
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
|
+
}
|
722
|
+
|
723
|
+
content += `
|
724
|
+
</div>
|
725
|
+
</td>
|
726
|
+
`;
|
727
|
+
|
728
|
+
// Add detail cells for each column that matches the table structure
|
729
|
+
columns.forEach(col => {
|
730
|
+
// Only show if column is visible in the main table
|
731
|
+
if (this.isColumnVisible(rowData.table_name, col)) {
|
732
|
+
content += `
|
733
|
+
<td class="border-r border-gray-300 p-2 align-top" style="min-width: 100px;">
|
734
|
+
<div class="text-xs font-medium text-gray-600 mb-1">${col}</div>
|
735
|
+
<div class="text-xs">
|
736
|
+
`;
|
737
|
+
|
738
|
+
if (change.operation === 'UPDATE' && change.changes) {
|
739
|
+
const columnChange = change.changes.find(c => c.column === col);
|
740
|
+
if (columnChange) {
|
741
|
+
// Show old -> new for changed columns in compact format
|
742
|
+
content += `
|
743
|
+
<div class="space-y-1">
|
744
|
+
<div class="text-xs text-red-700 bg-red-50 p-1 rounded border-l-2 border-red-400">
|
745
|
+
<div class="font-medium text-red-800 mb-1">Old:</div>
|
746
|
+
<div class="break-all max-h-12 overflow-auto">${this.formatDetailValue(columnChange.old_value)}</div>
|
747
|
+
</div>
|
748
|
+
<div class="text-xs text-green-700 bg-green-50 p-1 rounded border-l-2 border-green-400">
|
749
|
+
<div class="font-medium text-green-800 mb-1">New:</div>
|
750
|
+
<div class="break-all max-h-12 overflow-auto">${this.formatDetailValue(columnChange.new_value)}</div>
|
751
|
+
</div>
|
752
|
+
</div>
|
753
|
+
`;
|
754
|
+
} else {
|
755
|
+
// Show unchanged value
|
756
|
+
content += `
|
757
|
+
<div class="bg-gray-50 p-1 rounded border-l-2 border-gray-300">
|
758
|
+
<div class="text-xs text-gray-700 break-all max-h-12 overflow-auto">
|
759
|
+
${this.formatDetailValue(change.record_snapshot?.[col])}
|
760
|
+
</div>
|
761
|
+
</div>
|
762
|
+
`;
|
763
|
+
}
|
764
|
+
} else if (change.operation === 'INSERT') {
|
765
|
+
// Show new value for INSERT
|
766
|
+
content += `
|
767
|
+
<div class="bg-green-50 p-1 rounded border-l-2 border-green-400">
|
768
|
+
<div class="text-xs text-green-700 break-all max-h-12 overflow-auto">
|
769
|
+
${this.formatDetailValue(change.record_snapshot?.[col])}
|
770
|
+
</div>
|
771
|
+
</div>
|
772
|
+
`;
|
773
|
+
} else if (change.operation === 'DELETE') {
|
774
|
+
// Show deleted value for DELETE
|
775
|
+
content += `
|
776
|
+
<div class="bg-red-50 p-1 rounded border-l-2 border-red-400">
|
777
|
+
<div class="text-xs text-red-700 break-all max-h-12 overflow-auto">
|
778
|
+
${this.formatDetailValue(change.record_snapshot?.[col])}
|
779
|
+
</div>
|
780
|
+
</div>
|
781
|
+
`;
|
782
|
+
} else {
|
783
|
+
// Show value for other operations
|
784
|
+
content += `
|
785
|
+
<div class="bg-gray-50 p-1 rounded border-l-2 border-gray-300">
|
786
|
+
<div class="text-xs text-gray-700 break-all max-h-12 overflow-auto">
|
787
|
+
${this.formatDetailValue(change.record_snapshot?.[col])}
|
788
|
+
</div>
|
789
|
+
</div>
|
790
|
+
`;
|
791
|
+
}
|
792
|
+
|
793
|
+
content += `
|
794
|
+
</div>
|
795
|
+
</td>
|
796
|
+
`;
|
797
|
+
}
|
798
|
+
});
|
799
|
+
|
800
|
+
content += `
|
801
|
+
</tr>
|
802
|
+
</tbody>
|
803
|
+
</table>
|
804
|
+
</div>
|
805
|
+
`;
|
806
|
+
|
807
|
+
return content;
|
808
|
+
},
|
809
|
+
|
810
|
+
// Format value for detailed display
|
811
|
+
formatDetailValue(value) {
|
812
|
+
if (value === null || value === undefined) {
|
813
|
+
return '<span class="text-gray-400 italic">NULL</span>';
|
814
|
+
}
|
815
|
+
|
816
|
+
if (typeof value === 'string' && this.isJsonValue(value)) {
|
817
|
+
try {
|
818
|
+
const parsed = JSON.parse(value);
|
819
|
+
return JSON.stringify(parsed, null, 2);
|
820
|
+
} catch {
|
821
|
+
return String(value);
|
822
|
+
}
|
823
|
+
}
|
824
|
+
|
825
|
+
return String(value);
|
826
|
+
},
|
827
|
+
|
828
|
+
// Check if value is JSON
|
829
|
+
isJsonValue(value) {
|
830
|
+
if (typeof value !== 'string') return false;
|
831
|
+
try {
|
832
|
+
const parsed = JSON.parse(value);
|
833
|
+
return typeof parsed === 'object' && parsed !== null;
|
834
|
+
} catch {
|
835
|
+
return false;
|
836
|
+
}
|
837
|
+
},
|
838
|
+
|
839
|
+
// Format cell value for display
|
840
|
+
formatCellValue(value) {
|
841
|
+
if (value === null || value === undefined) {
|
842
|
+
return '<span class="text-gray-400">NULL</span>';
|
843
|
+
}
|
844
|
+
|
845
|
+
if (typeof value === 'string' && value.length > 50) {
|
846
|
+
return `<span title="${value}">${value.substring(0, 50)}...</span>`;
|
847
|
+
}
|
848
|
+
|
849
|
+
return String(value);
|
850
|
+
},
|
851
|
+
|
852
|
+
// Setup URL state synchronization
|
853
|
+
setupURLStateSync() {
|
854
|
+
const urlParams = new URLSearchParams(window.location.search);
|
855
|
+
const tableParam = urlParams.get('table');
|
856
|
+
const operationParam = urlParams.get('operation');
|
857
|
+
|
858
|
+
if (tableParam) this.filters.table = tableParam;
|
859
|
+
if (operationParam) this.filters.operation = operationParam;
|
860
|
+
},
|
861
|
+
|
862
|
+
// Update URL with current filters
|
863
|
+
updateURL() {
|
864
|
+
const url = new URL(window.location.href);
|
865
|
+
const params = new URLSearchParams(url.search);
|
866
|
+
|
867
|
+
if (this.filters.table) {
|
868
|
+
params.set('table', this.filters.table);
|
869
|
+
} else {
|
870
|
+
params.delete('table');
|
871
|
+
}
|
872
|
+
|
873
|
+
if (this.filters.operation) {
|
874
|
+
params.set('operation', this.filters.operation);
|
875
|
+
} else {
|
876
|
+
params.delete('operation');
|
877
|
+
}
|
878
|
+
|
879
|
+
url.search = params.toString();
|
880
|
+
window.history.replaceState({}, '', url.toString());
|
881
|
+
},
|
882
|
+
|
883
|
+
// Clear all filters
|
884
|
+
clearFilters() {
|
885
|
+
this.filters = {
|
886
|
+
search: '',
|
887
|
+
operation: '',
|
888
|
+
table: ''
|
889
|
+
};
|
890
|
+
this.applyFilters();
|
891
|
+
},
|
892
|
+
|
893
|
+
// Column management methods
|
894
|
+
toggleColumnSelector(tableName) {
|
895
|
+
this.showColumnSelector = this.showColumnSelector === tableName ? null : tableName;
|
896
|
+
},
|
897
|
+
|
898
|
+
// Initialize column visibility state (per table)
|
899
|
+
initializeColumnVisibility() {
|
900
|
+
this.tableColumns = {};
|
901
|
+
|
902
|
+
Object.keys(this.tableData).forEach(tableName => {
|
903
|
+
const tableInfo = this.tableData[tableName];
|
904
|
+
if (tableInfo && tableInfo.columns) {
|
905
|
+
this.tableColumns[tableName] = {};
|
906
|
+
tableInfo.columns.forEach(col => {
|
907
|
+
this.tableColumns[tableName][col] = true; // All columns visible by default
|
908
|
+
});
|
909
|
+
}
|
910
|
+
});
|
911
|
+
},
|
912
|
+
|
913
|
+
// Toggle column visibility for a specific table
|
914
|
+
toggleColumnVisibility(tableName, columnName) {
|
915
|
+
if (this.tableColumns[tableName] && this.tableColumns[tableName][columnName] !== undefined) {
|
916
|
+
this.tableColumns[tableName][columnName] = !this.tableColumns[tableName][columnName];
|
917
|
+
this.updateTableColumns(tableName);
|
918
|
+
}
|
919
|
+
},
|
920
|
+
|
921
|
+
// Update Tabulator column visibility for a specific table
|
922
|
+
updateTableColumns(tableName) {
|
923
|
+
const tabulator = this.tabulators[tableName];
|
924
|
+
if (!tabulator) return;
|
925
|
+
|
926
|
+
// Rebuild columns for this table
|
927
|
+
const tableInfo = this.tableData[tableName];
|
928
|
+
const newColumns = this.buildColumnsForTable(tableName, tableInfo);
|
929
|
+
|
930
|
+
// Update the tabulator columns
|
931
|
+
tabulator.setColumns(newColumns);
|
932
|
+
},
|
933
|
+
|
934
|
+
// Select all columns for a table
|
935
|
+
selectAllColumns(tableName) {
|
936
|
+
if (!this.tableColumns[tableName]) return;
|
937
|
+
|
938
|
+
Object.keys(this.tableColumns[tableName]).forEach(col => {
|
939
|
+
this.tableColumns[tableName][col] = true;
|
940
|
+
});
|
941
|
+
this.updateTableColumns(tableName);
|
942
|
+
},
|
943
|
+
|
944
|
+
// Deselect all columns for a table
|
945
|
+
selectNoneColumns(tableName) {
|
946
|
+
if (!this.tableColumns[tableName]) return;
|
947
|
+
|
948
|
+
Object.keys(this.tableColumns[tableName]).forEach(col => {
|
949
|
+
this.tableColumns[tableName][col] = false;
|
950
|
+
});
|
951
|
+
this.updateTableColumns(tableName);
|
952
|
+
},
|
953
|
+
|
954
|
+
// Check if column is visible for a specific table
|
955
|
+
isColumnVisible(tableName, columnName) {
|
956
|
+
return this.tableColumns[tableName] && this.tableColumns[tableName][columnName] === true;
|
957
|
+
},
|
958
|
+
|
959
|
+
// Format timestamp for display
|
960
|
+
formatTimestamp(timestamp) {
|
961
|
+
if (!timestamp) return '--';
|
962
|
+
const date = new Date(timestamp);
|
963
|
+
return date.toLocaleString('en-AU', {
|
964
|
+
year: '2-digit',
|
965
|
+
month: '2-digit',
|
966
|
+
day: '2-digit',
|
967
|
+
hour: '2-digit',
|
968
|
+
minute: '2-digit',
|
969
|
+
second: '2-digit',
|
970
|
+
hour12: false
|
971
|
+
}).replace(/\//g, '-');
|
972
|
+
},
|
973
|
+
|
974
|
+
// Format date for expanded view
|
975
|
+
formatDate(timestamp) {
|
976
|
+
if (!timestamp) return '--';
|
977
|
+
const date = new Date(timestamp);
|
978
|
+
return date.toLocaleString('en-AU', {
|
979
|
+
year: 'numeric',
|
980
|
+
month: '2-digit',
|
981
|
+
day: '2-digit',
|
982
|
+
hour: '2-digit',
|
983
|
+
minute: '2-digit',
|
984
|
+
second: '2-digit',
|
985
|
+
hour12: false
|
986
|
+
});
|
987
|
+
},
|
988
|
+
|
989
|
+
// Check if there are any visible changes
|
990
|
+
hasVisibleChanges() {
|
991
|
+
return Object.keys(this.tableData).some(tableName => {
|
992
|
+
const data = this.tableData[tableName];
|
993
|
+
return data.changes && data.changes.length > 0;
|
994
|
+
});
|
995
|
+
},
|
996
|
+
|
997
|
+
|
998
|
+
// Component cleanup
|
999
|
+
componentDestroy() {
|
1000
|
+
Object.values(this.tabulators).forEach(tabulator => {
|
1001
|
+
if (tabulator) {
|
1002
|
+
tabulator.destroy();
|
1003
|
+
}
|
1004
|
+
});
|
1005
|
+
this.tabulators = {};
|
1006
|
+
}
|
1007
|
+
};
|
1008
|
+
});
|