dbwatcher 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -2
  3. data/app/assets/config/dbwatcher_manifest.js +1 -0
  4. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +196 -119
  5. data/app/assets/javascripts/dbwatcher/components/dashboard.js +325 -0
  6. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  7. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  8. data/app/assets/stylesheets/dbwatcher/application.css +691 -41
  9. data/app/assets/stylesheets/dbwatcher/application.scss +5 -0
  10. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +68 -23
  11. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +83 -26
  12. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +3 -3
  13. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +9 -0
  14. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +248 -0
  15. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  16. data/app/assets/stylesheets/dbwatcher/vendor/_tabulator_overrides.scss +37 -0
  17. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  18. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +180 -0
  19. data/app/controllers/dbwatcher/dashboard/system_info_controller.rb +64 -0
  20. data/app/controllers/dbwatcher/dashboard_controller.rb +17 -0
  21. data/app/controllers/dbwatcher/sessions_controller.rb +3 -19
  22. data/app/helpers/dbwatcher/application_helper.rb +43 -11
  23. data/app/helpers/dbwatcher/diagram_helper.rb +0 -88
  24. data/app/views/dbwatcher/dashboard/_layout.html.erb +27 -0
  25. data/app/views/dbwatcher/dashboard/_overview.html.erb +188 -0
  26. data/app/views/dbwatcher/dashboard/_system_info.html.erb +22 -0
  27. data/app/views/dbwatcher/dashboard/_system_info_content.html.erb +389 -0
  28. data/app/views/dbwatcher/dashboard/index.html.erb +8 -177
  29. data/app/views/dbwatcher/sessions/_layout.html.erb +26 -0
  30. data/app/views/dbwatcher/sessions/{_summary_tab.html.erb → _summary.html.erb} +1 -1
  31. data/app/views/dbwatcher/sessions/_tables.html.erb +170 -0
  32. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  33. data/app/views/dbwatcher/sessions/index.html.erb +107 -87
  34. data/app/views/dbwatcher/sessions/show.html.erb +12 -4
  35. data/app/views/dbwatcher/tables/index.html.erb +32 -40
  36. data/app/views/layouts/dbwatcher/application.html.erb +101 -48
  37. data/config/routes.rb +25 -7
  38. data/lib/dbwatcher/configuration.rb +18 -1
  39. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  40. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  41. data/lib/dbwatcher/services/base_service.rb +2 -0
  42. data/lib/dbwatcher/services/system_info/database_info_collector.rb +263 -0
  43. data/lib/dbwatcher/services/system_info/machine_info_collector.rb +387 -0
  44. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +328 -0
  45. data/lib/dbwatcher/services/system_info/system_info_collector.rb +114 -0
  46. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  47. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  48. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  49. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  50. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  51. data/lib/dbwatcher/storage/concerns/error_handler.rb +6 -6
  52. data/lib/dbwatcher/storage/session.rb +5 -0
  53. data/lib/dbwatcher/storage/system_info_storage.rb +242 -0
  54. data/lib/dbwatcher/storage.rb +12 -0
  55. data/lib/dbwatcher/version.rb +1 -1
  56. data/lib/dbwatcher.rb +16 -2
  57. metadata +28 -16
  58. data/app/helpers/dbwatcher/component_helper.rb +0 -29
  59. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +0 -265
  60. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +0 -12
  61. data/app/views/dbwatcher/sessions/changes.html.erb +0 -21
  62. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +0 -44
  63. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +0 -96
  64. data/app/views/dbwatcher/sessions/diagrams.html.erb +0 -21
  65. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +0 -8
  66. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +0 -35
  67. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +0 -25
  68. data/app/views/dbwatcher/sessions/summary.html.erb +0 -21
  69. /data/app/views/dbwatcher/sessions/{_diagrams_tab.html.erb → _diagrams.html.erb} +0 -0
@@ -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) {