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
@@ -0,0 +1,325 @@
|
|
1
|
+
/**
|
2
|
+
* Dashboard Component for DBWatcher
|
3
|
+
* Handles tab switching and system info refresh functionality
|
4
|
+
*/
|
5
|
+
|
6
|
+
(function() {
|
7
|
+
'use strict';
|
8
|
+
|
9
|
+
// Configuration constants
|
10
|
+
const CONFIG = {
|
11
|
+
SELECTORS: {
|
12
|
+
container: '.dashboard-container',
|
13
|
+
tab: '.tab-item',
|
14
|
+
tabContent: '.tab-content',
|
15
|
+
refreshButton: '#refresh-system-info',
|
16
|
+
clearCacheButton: '#clear-cache-system-info',
|
17
|
+
systemInfoContent: '#system-info-content'
|
18
|
+
},
|
19
|
+
ENDPOINTS: {
|
20
|
+
refresh: '/dbwatcher/dashboard/system_info/refresh',
|
21
|
+
clearCache: '/dbwatcher/dashboard/system_info/clear_cache',
|
22
|
+
dashboard: '/dbwatcher',
|
23
|
+
systemInfo: '/dbwatcher/system_info'
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
// Dashboard Component Factory
|
28
|
+
const DashboardComponent = function(config = {}) {
|
29
|
+
// Merge configuration with defaults
|
30
|
+
const settings = {
|
31
|
+
...CONFIG.SELECTORS,
|
32
|
+
...config
|
33
|
+
};
|
34
|
+
|
35
|
+
// Component state
|
36
|
+
let isRefreshing = false;
|
37
|
+
|
38
|
+
// Initialize component
|
39
|
+
function init() {
|
40
|
+
console.log('Dashboard component init() called');
|
41
|
+
setupEventListeners();
|
42
|
+
console.log('Dashboard component initialized successfully');
|
43
|
+
}
|
44
|
+
|
45
|
+
// Setup event listeners
|
46
|
+
function setupEventListeners() {
|
47
|
+
// System info refresh
|
48
|
+
document.addEventListener('click', handleRefreshClick);
|
49
|
+
|
50
|
+
// Clear cache
|
51
|
+
document.addEventListener('click', handleClearCacheClick);
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
// Handle refresh button click
|
56
|
+
function handleRefreshClick(event) {
|
57
|
+
const target = event.target;
|
58
|
+
|
59
|
+
if (!target.matches(settings.refreshButton)) {
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
|
63
|
+
event.preventDefault();
|
64
|
+
refreshSystemInfo();
|
65
|
+
}
|
66
|
+
|
67
|
+
// Handle clear cache button click
|
68
|
+
function handleClearCacheClick(event) {
|
69
|
+
const target = event.target;
|
70
|
+
|
71
|
+
if (!target.matches(settings.clearCacheButton)) {
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
|
75
|
+
event.preventDefault();
|
76
|
+
|
77
|
+
if (confirm('Are you sure you want to clear the system information cache?')) {
|
78
|
+
clearSystemInfoCache();
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
// Refresh system information
|
83
|
+
async function refreshSystemInfo() {
|
84
|
+
if (isRefreshing) {
|
85
|
+
return;
|
86
|
+
}
|
87
|
+
|
88
|
+
isRefreshing = true;
|
89
|
+
const refreshButton = safeQuerySelector(settings.refreshButton);
|
90
|
+
|
91
|
+
try {
|
92
|
+
// Update button state
|
93
|
+
if (refreshButton) {
|
94
|
+
refreshButton.disabled = true;
|
95
|
+
refreshButton.textContent = 'Refreshing...';
|
96
|
+
}
|
97
|
+
|
98
|
+
// Make API call
|
99
|
+
const response = await fetch(CONFIG.ENDPOINTS.refresh, {
|
100
|
+
method: 'POST',
|
101
|
+
headers: utils.getApiHeaders()
|
102
|
+
});
|
103
|
+
|
104
|
+
const data = await utils.handleApiResponse(response);
|
105
|
+
|
106
|
+
if (data.success) {
|
107
|
+
// Update the system info content
|
108
|
+
await updateSystemInfoContent();
|
109
|
+
showNotification('System information refreshed successfully', 'success');
|
110
|
+
} else {
|
111
|
+
showNotification(data.error || 'Failed to refresh system information', 'error');
|
112
|
+
}
|
113
|
+
|
114
|
+
} catch (error) {
|
115
|
+
console.error('Error refreshing system info:', error);
|
116
|
+
showNotification('Failed to refresh system information', 'error');
|
117
|
+
} finally {
|
118
|
+
isRefreshing = false;
|
119
|
+
|
120
|
+
// Restore button state
|
121
|
+
if (refreshButton) {
|
122
|
+
refreshButton.disabled = false;
|
123
|
+
refreshButton.textContent = 'Refresh';
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
// Clear system information cache
|
129
|
+
async function clearSystemInfoCache() {
|
130
|
+
try {
|
131
|
+
const response = await fetch(CONFIG.ENDPOINTS.clearCache, {
|
132
|
+
method: 'DELETE',
|
133
|
+
headers: utils.getApiHeaders()
|
134
|
+
});
|
135
|
+
|
136
|
+
const data = await utils.handleApiResponse(response);
|
137
|
+
|
138
|
+
if (data.success) {
|
139
|
+
showNotification('System information cache cleared successfully', 'success');
|
140
|
+
} else {
|
141
|
+
showNotification(data.error || 'Failed to clear cache', 'error');
|
142
|
+
}
|
143
|
+
|
144
|
+
} catch (error) {
|
145
|
+
console.error('Error clearing cache:', error);
|
146
|
+
showNotification('Failed to clear cache', 'error');
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
// Update system info content
|
151
|
+
async function updateSystemInfoContent() {
|
152
|
+
const contentContainer = safeQuerySelector(settings.systemInfoContent);
|
153
|
+
|
154
|
+
if (!contentContainer) {
|
155
|
+
return;
|
156
|
+
}
|
157
|
+
|
158
|
+
try {
|
159
|
+
// Make request to get updated HTML content
|
160
|
+
const response = await fetch(CONFIG.ENDPOINTS.dashboard, {
|
161
|
+
headers: {
|
162
|
+
'Accept': 'text/html',
|
163
|
+
'X-Requested-With': 'XMLHttpRequest'
|
164
|
+
}
|
165
|
+
});
|
166
|
+
|
167
|
+
if (response.ok) {
|
168
|
+
const html = await response.text();
|
169
|
+
const parser = new DOMParser();
|
170
|
+
const doc = parser.parseFromString(html, 'text/html');
|
171
|
+
const newContent = doc.querySelector('#system-info-content');
|
172
|
+
|
173
|
+
if (newContent) {
|
174
|
+
contentContainer.innerHTML = newContent.innerHTML;
|
175
|
+
}
|
176
|
+
}
|
177
|
+
} catch (error) {
|
178
|
+
console.error('Error updating system info content:', error);
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
// Safe DOM access helper
|
183
|
+
function safeQuerySelector(selector) {
|
184
|
+
try {
|
185
|
+
return document.querySelector(selector);
|
186
|
+
} catch (e) {
|
187
|
+
console.warn('Error accessing DOM element:', selector, e);
|
188
|
+
return null;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
|
193
|
+
// Utility functions
|
194
|
+
const utils = {
|
195
|
+
// Get CSRF token
|
196
|
+
getCsrfToken() {
|
197
|
+
const metaTag = safeQuerySelector('meta[name="csrf-token"]');
|
198
|
+
return metaTag?.getAttribute('content');
|
199
|
+
},
|
200
|
+
|
201
|
+
// Common headers for API requests
|
202
|
+
getApiHeaders() {
|
203
|
+
return {
|
204
|
+
'Content-Type': 'application/json',
|
205
|
+
'X-Requested-With': 'XMLHttpRequest',
|
206
|
+
'X-CSRF-Token': this.getCsrfToken()
|
207
|
+
};
|
208
|
+
},
|
209
|
+
|
210
|
+
// Handle API response
|
211
|
+
async handleApiResponse(response) {
|
212
|
+
if (!response.ok) {
|
213
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
214
|
+
}
|
215
|
+
return await response.json();
|
216
|
+
}
|
217
|
+
};
|
218
|
+
|
219
|
+
// Show notification
|
220
|
+
function showNotification(message, type = 'info') {
|
221
|
+
// Create notification element
|
222
|
+
const notification = document.createElement('div');
|
223
|
+
notification.className = `notification notification-${type}`;
|
224
|
+
notification.style.cssText = `
|
225
|
+
position: fixed;
|
226
|
+
top: 20px;
|
227
|
+
right: 20px;
|
228
|
+
padding: 12px 16px;
|
229
|
+
border-radius: 4px;
|
230
|
+
color: white;
|
231
|
+
font-size: 14px;
|
232
|
+
z-index: 1000;
|
233
|
+
max-width: 300px;
|
234
|
+
word-wrap: break-word;
|
235
|
+
opacity: 0;
|
236
|
+
transform: translateY(-20px);
|
237
|
+
transition: all 0.3s ease;
|
238
|
+
`;
|
239
|
+
|
240
|
+
// Set background color based on type
|
241
|
+
switch (type) {
|
242
|
+
case 'success':
|
243
|
+
notification.style.backgroundColor = '#10b981';
|
244
|
+
break;
|
245
|
+
case 'error':
|
246
|
+
notification.style.backgroundColor = '#ef4444';
|
247
|
+
break;
|
248
|
+
default:
|
249
|
+
notification.style.backgroundColor = '#3b82f6';
|
250
|
+
}
|
251
|
+
|
252
|
+
notification.textContent = message;
|
253
|
+
|
254
|
+
// Add to page
|
255
|
+
if (document.body) {
|
256
|
+
document.body.appendChild(notification);
|
257
|
+
}
|
258
|
+
|
259
|
+
// Animate in
|
260
|
+
setTimeout(() => {
|
261
|
+
notification.style.opacity = '1';
|
262
|
+
notification.style.transform = 'translateY(0)';
|
263
|
+
}, 100);
|
264
|
+
|
265
|
+
// Remove after 5 seconds
|
266
|
+
setTimeout(() => {
|
267
|
+
notification.style.opacity = '0';
|
268
|
+
notification.style.transform = 'translateY(-20px)';
|
269
|
+
setTimeout(() => {
|
270
|
+
if (notification.parentNode) {
|
271
|
+
notification.parentNode.removeChild(notification);
|
272
|
+
}
|
273
|
+
}, 300);
|
274
|
+
}, 5000);
|
275
|
+
}
|
276
|
+
|
277
|
+
// Public API
|
278
|
+
return {
|
279
|
+
init,
|
280
|
+
refreshSystemInfo,
|
281
|
+
clearSystemInfoCache
|
282
|
+
};
|
283
|
+
};
|
284
|
+
|
285
|
+
// Register component with DBWatcher
|
286
|
+
if (window.DBWatcher && window.DBWatcher.register) {
|
287
|
+
window.DBWatcher.register('dashboard', DashboardComponent);
|
288
|
+
}
|
289
|
+
|
290
|
+
// Safe DOM access helper
|
291
|
+
function safeQuerySelector(selector) {
|
292
|
+
try {
|
293
|
+
return document.querySelector(selector);
|
294
|
+
} catch (e) {
|
295
|
+
console.warn('Error accessing DOM element:', selector, e);
|
296
|
+
return null;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
// Auto-initialize when DOM is ready with better error handling
|
301
|
+
function initializeDashboard() {
|
302
|
+
try {
|
303
|
+
if (safeQuerySelector('.dashboard-container') || safeQuerySelector('.tab-bar') || safeQuerySelector('#refresh-system-info') || safeQuerySelector('#clear-cache-system-info')) {
|
304
|
+
const dashboard = DashboardComponent();
|
305
|
+
dashboard.init();
|
306
|
+
}
|
307
|
+
} catch (error) {
|
308
|
+
console.error('Error initializing dashboard:', error);
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
// Multiple initialization strategies
|
313
|
+
if (document.readyState === 'loading') {
|
314
|
+
document.addEventListener('DOMContentLoaded', initializeDashboard);
|
315
|
+
} else if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
316
|
+
// DOM is already loaded, but wait a bit for Alpine to initialize
|
317
|
+
setTimeout(initializeDashboard, 100);
|
318
|
+
}
|
319
|
+
|
320
|
+
// Also listen for Alpine initialization
|
321
|
+
document.addEventListener('alpine:init', () => {
|
322
|
+
setTimeout(initializeDashboard, 50);
|
323
|
+
});
|
324
|
+
|
325
|
+
})();
|
@@ -0,0 +1,211 @@
|
|
1
|
+
/**
|
2
|
+
* Timeline Component
|
3
|
+
* Interactive timeline visualization for database operations
|
4
|
+
* API-first implementation for DBWatcher timeline tab
|
5
|
+
*/
|
6
|
+
|
7
|
+
// Register component with DBWatcher
|
8
|
+
(function() {
|
9
|
+
function registerTimeline() {
|
10
|
+
if (window.DBWatcher && window.DBWatcher.ComponentRegistry) {
|
11
|
+
DBWatcher.ComponentRegistry.register('timeline', function(config) {
|
12
|
+
return Object.assign(DBWatcher.BaseComponent(config), {
|
13
|
+
// Component-specific state
|
14
|
+
sessionId: config.sessionId,
|
15
|
+
timelineData: [],
|
16
|
+
filteredData: [],
|
17
|
+
metadata: {},
|
18
|
+
|
19
|
+
// Filter state
|
20
|
+
filters: {
|
21
|
+
tables: [],
|
22
|
+
searchText: ""
|
23
|
+
},
|
24
|
+
tableSearch: "",
|
25
|
+
|
26
|
+
// Component initialization
|
27
|
+
componentInit() {
|
28
|
+
this.loadTimelineData();
|
29
|
+
this.setupEventListeners();
|
30
|
+
},
|
31
|
+
|
32
|
+
// Component cleanup
|
33
|
+
componentDestroy() {
|
34
|
+
// Clean up any event listeners or intervals
|
35
|
+
},
|
36
|
+
|
37
|
+
// Load timeline data from API
|
38
|
+
async loadTimelineData() {
|
39
|
+
if (!this.sessionId) {
|
40
|
+
console.error('No session ID provided to timeline component');
|
41
|
+
this.handleError(new Error('No session ID provided'));
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
|
45
|
+
this.setLoading(true);
|
46
|
+
this.clearError();
|
47
|
+
|
48
|
+
try {
|
49
|
+
const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/timeline_data`;
|
50
|
+
const data = await this.fetchData(url);
|
51
|
+
|
52
|
+
if (!data.error) {
|
53
|
+
this.timelineData = data.timeline || [];
|
54
|
+
this.metadata = data.metadata || {};
|
55
|
+
this.filteredData = [...this.timelineData];
|
56
|
+
|
57
|
+
this.setupInitialView();
|
58
|
+
} else {
|
59
|
+
throw new Error(data.error || 'No timeline data received');
|
60
|
+
}
|
61
|
+
} catch (error) {
|
62
|
+
this.handleError(error);
|
63
|
+
} finally {
|
64
|
+
this.setLoading(false);
|
65
|
+
}
|
66
|
+
},
|
67
|
+
|
68
|
+
// Setup initial view after data load
|
69
|
+
setupInitialView() {
|
70
|
+
// Initialize filters based on available data
|
71
|
+
if (this.metadata.tables_affected) {
|
72
|
+
// Set up available filter options but don't apply any filters initially
|
73
|
+
}
|
74
|
+
},
|
75
|
+
|
76
|
+
// Setup event listeners
|
77
|
+
setupEventListeners() {
|
78
|
+
// Reserved for future keyboard shortcuts
|
79
|
+
},
|
80
|
+
|
81
|
+
// ==========================================
|
82
|
+
// Filtering functionality
|
83
|
+
// ==========================================
|
84
|
+
|
85
|
+
// Apply all filters to timeline data
|
86
|
+
applyFilters() {
|
87
|
+
this.filteredData = this.timelineData.filter((entry) => {
|
88
|
+
return (
|
89
|
+
this.matchesTableFilter(entry) &&
|
90
|
+
this.matchesSearchFilter(entry)
|
91
|
+
);
|
92
|
+
});
|
93
|
+
},
|
94
|
+
|
95
|
+
// Filter by table name
|
96
|
+
matchesTableFilter(entry) {
|
97
|
+
return (
|
98
|
+
this.filters.tables.length === 0 ||
|
99
|
+
this.filters.tables.includes(entry.table_name)
|
100
|
+
);
|
101
|
+
},
|
102
|
+
|
103
|
+
// Filter by search text
|
104
|
+
matchesSearchFilter(entry) {
|
105
|
+
if (!this.filters.searchText) return true;
|
106
|
+
|
107
|
+
const searchLower = this.filters.searchText.toLowerCase();
|
108
|
+
return (
|
109
|
+
entry.table_name.toLowerCase().includes(searchLower) ||
|
110
|
+
entry.operation.toLowerCase().includes(searchLower) ||
|
111
|
+
(entry.record_id && entry.record_id.toString().includes(searchLower))
|
112
|
+
);
|
113
|
+
},
|
114
|
+
|
115
|
+
// Clear all filters
|
116
|
+
clearFilters() {
|
117
|
+
this.filters = {
|
118
|
+
tables: [],
|
119
|
+
searchText: ""
|
120
|
+
};
|
121
|
+
this.applyFilters();
|
122
|
+
},
|
123
|
+
|
124
|
+
// ==========================================
|
125
|
+
// Utility methods
|
126
|
+
// ==========================================
|
127
|
+
|
128
|
+
// Format timestamp for display
|
129
|
+
formatTimestamp(timestamp) {
|
130
|
+
if (!timestamp) return 'N/A';
|
131
|
+
return this.formatDate(new Date(timestamp), 'MMM dd, yyyy HH:mm:ss');
|
132
|
+
},
|
133
|
+
|
134
|
+
// Format duration in milliseconds
|
135
|
+
formatDuration(ms) {
|
136
|
+
if (!ms || ms < 0) return '0ms';
|
137
|
+
if (ms < 1000) return `${ms}ms`;
|
138
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
139
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
140
|
+
},
|
141
|
+
|
142
|
+
// Get color for operation type
|
143
|
+
getOperationColor(operation) {
|
144
|
+
const colors = {
|
145
|
+
INSERT: "#10b981", // green
|
146
|
+
UPDATE: "#f59e0b", // amber
|
147
|
+
DELETE: "#ef4444", // red
|
148
|
+
SELECT: "#3b82f6" // blue
|
149
|
+
};
|
150
|
+
return colors[operation] || "#6b7280";
|
151
|
+
},
|
152
|
+
|
153
|
+
// Get operation icon
|
154
|
+
getOperationIcon(operation) {
|
155
|
+
const icons = {
|
156
|
+
INSERT: "plus",
|
157
|
+
UPDATE: "pencil",
|
158
|
+
DELETE: "trash",
|
159
|
+
SELECT: "eye"
|
160
|
+
};
|
161
|
+
return icons[operation] || "circle";
|
162
|
+
},
|
163
|
+
|
164
|
+
// Get available tables for filtering
|
165
|
+
getAvailableTables() {
|
166
|
+
return this.metadata.tables_affected || [];
|
167
|
+
},
|
168
|
+
|
169
|
+
// Get available operations for filtering
|
170
|
+
getAvailableOperations() {
|
171
|
+
return Object.keys(this.metadata.operation_counts || {});
|
172
|
+
},
|
173
|
+
|
174
|
+
// Get operation count for display
|
175
|
+
getOperationCount(operation) {
|
176
|
+
return this.metadata.operation_counts?.[operation] || 0;
|
177
|
+
},
|
178
|
+
|
179
|
+
// Get total filtered operations count
|
180
|
+
getTotalFilteredOperations() {
|
181
|
+
return this.filteredData.length;
|
182
|
+
},
|
183
|
+
|
184
|
+
// Get session statistics
|
185
|
+
getSessionStats() {
|
186
|
+
return {
|
187
|
+
totalOperations: this.timelineData.length,
|
188
|
+
filteredOperations: this.filteredData.length,
|
189
|
+
tablesAffected: this.getAvailableTables().length,
|
190
|
+
sessionDuration: this.metadata.session_duration || 'N/A',
|
191
|
+
timeRange: this.metadata.time_range || {}
|
192
|
+
};
|
193
|
+
},
|
194
|
+
|
195
|
+
// Format relative time from session start
|
196
|
+
formatRelativeTime(operation) {
|
197
|
+
if (!operation) return '00:00';
|
198
|
+
return operation.relative_time || '00:00';
|
199
|
+
}
|
200
|
+
}); // End of Object.assign
|
201
|
+
}); // End of registerComponent
|
202
|
+
console.log('✅ Timeline component registered successfully');
|
203
|
+
} else {
|
204
|
+
console.warn('DBWatcher ComponentRegistry not ready, retrying timeline registration...');
|
205
|
+
setTimeout(registerTimeline, 100);
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
// Register immediately without waiting for DOM
|
210
|
+
registerTimeline();
|
211
|
+
})();
|
@@ -101,6 +101,11 @@ window.DBWatcher = {
|
|
101
101
|
return factory(config);
|
102
102
|
},
|
103
103
|
|
104
|
+
// Get component (alias for createComponent for compatibility)
|
105
|
+
getComponent(name, config = {}) {
|
106
|
+
return this.createComponent(name, config);
|
107
|
+
},
|
108
|
+
|
104
109
|
// Register a component using the ComponentRegistry
|
105
110
|
register(name, factory) {
|
106
111
|
if (!this.ComponentRegistry) {
|