dbwatcher 1.1.2 → 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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/dbwatcher_manifest.js +1 -0
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
  4. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  5. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  6. data/app/assets/stylesheets/dbwatcher/application.css +298 -1
  7. data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
  8. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  9. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
  10. data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
  11. data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
  12. data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
  13. data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
  14. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  15. data/app/views/dbwatcher/sessions/show.html.erb +3 -1
  16. data/app/views/layouts/dbwatcher/application.html.erb +1 -0
  17. data/config/routes.rb +2 -1
  18. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  19. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  20. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  21. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  22. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  23. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  24. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  25. data/lib/dbwatcher/version.rb +1 -1
  26. data/lib/dbwatcher.rb +1 -1
  27. metadata +11 -4
@@ -0,0 +1,326 @@
1
+ // Timeline Component Styles
2
+ // Provides styles for the interactive timeline visualization
3
+
4
+ .timeline-container {
5
+ @apply h-full flex flex-col;
6
+ }
7
+
8
+ .timeline-controls {
9
+ @apply p-3 border-b border-gray-300 bg-gray-50;
10
+ }
11
+
12
+ .timeline-header {
13
+ @apply flex items-center justify-between mb-3;
14
+ }
15
+
16
+ .timeline-title {
17
+ @apply text-sm font-medium text-gray-900;
18
+ }
19
+
20
+ .timeline-zoom-controls {
21
+ @apply flex items-center gap-2;
22
+ }
23
+
24
+ .timeline-filter-controls {
25
+ @apply flex items-center gap-4 text-xs flex-wrap;
26
+ }
27
+
28
+ .timeline-filter-group {
29
+ @apply flex items-center gap-2;
30
+ }
31
+
32
+ .timeline-filter-label {
33
+ @apply text-gray-700 font-medium;
34
+ }
35
+
36
+ // Timeline visualization
37
+ .timeline-visualization {
38
+ @apply flex-1 overflow-hidden;
39
+ }
40
+
41
+ .timeline-time-header {
42
+ @apply h-8 bg-gray-100 border-b border-gray-200 relative;
43
+ }
44
+
45
+ .timeline-time-scale {
46
+ @apply absolute inset-0 flex items-center px-4;
47
+ }
48
+
49
+ .timeline-content {
50
+ @apply flex-1 overflow-auto p-4 bg-white;
51
+ }
52
+
53
+ .timeline-track {
54
+ @apply relative h-16 bg-gray-50 rounded border border-gray-200 mb-4;
55
+ }
56
+
57
+ .timeline-line {
58
+ @apply absolute top-1/2 left-4 right-4 h-0.5 bg-gray-300 transform -translate-y-1/2;
59
+ }
60
+
61
+ .timeline-marker {
62
+ @apply absolute top-1/2 transform -translate-y-1/2 -translate-x-1/2 cursor-pointer;
63
+
64
+ .timeline-marker-dot {
65
+ @apply w-3 h-3 rounded-full border-2 border-white shadow-sm transition-transform;
66
+
67
+ &:hover {
68
+ @apply scale-125;
69
+ }
70
+ }
71
+ }
72
+
73
+ // Timeline statistics
74
+ .timeline-stats {
75
+ @apply grid grid-cols-2 gap-4 text-xs;
76
+
77
+ @screen md {
78
+ @apply grid-cols-4;
79
+ }
80
+ }
81
+
82
+ .timeline-stat-card {
83
+ @apply bg-gray-50 p-3 rounded;
84
+ }
85
+
86
+ .timeline-stat-label {
87
+ @apply text-gray-500 font-medium;
88
+ }
89
+
90
+ .timeline-stat-value {
91
+ @apply text-lg font-bold text-gray-900;
92
+ }
93
+
94
+ .timeline-stat-detail {
95
+ @apply text-xs text-gray-500;
96
+ }
97
+
98
+ // Operation list
99
+ .timeline-operations {
100
+ @apply mt-6;
101
+ }
102
+
103
+ .timeline-operations-title {
104
+ @apply text-sm font-medium text-gray-900 mb-3;
105
+ }
106
+
107
+ .timeline-operations-list {
108
+ @apply space-y-2 max-h-64 overflow-auto;
109
+ }
110
+
111
+ .timeline-operation-item {
112
+ @apply flex items-center justify-between p-2 bg-gray-50 rounded hover:bg-gray-100 cursor-pointer text-xs;
113
+ }
114
+
115
+ .timeline-operation-info {
116
+ @apply flex items-center gap-3;
117
+ }
118
+
119
+ .timeline-operation-marker {
120
+ @apply w-2 h-2 rounded-full;
121
+ }
122
+
123
+ .timeline-operation-type {
124
+ @apply font-medium;
125
+ }
126
+
127
+ .timeline-operation-table {
128
+ // No additional styles needed - uses default text
129
+ }
130
+
131
+ .timeline-operation-time {
132
+ @apply text-gray-500;
133
+ }
134
+
135
+ .timeline-operation-record {
136
+ @apply text-gray-500;
137
+ }
138
+
139
+ // Empty state
140
+ .timeline-empty {
141
+ @apply text-center py-8 text-gray-500;
142
+ }
143
+
144
+ .timeline-empty-icon {
145
+ @apply w-12 h-12 mx-auto mb-4 text-gray-300;
146
+ }
147
+
148
+ .timeline-empty-text {
149
+ // No additional styles needed
150
+ }
151
+
152
+ .timeline-empty-action {
153
+ @apply mt-2 text-blue-600 underline;
154
+ }
155
+
156
+ // Loading state
157
+ .timeline-loading {
158
+ @apply flex items-center justify-center h-64;
159
+ }
160
+
161
+ .timeline-loading-spinner {
162
+ @apply animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500;
163
+ }
164
+
165
+ .timeline-loading-text {
166
+ @apply ml-2 text-gray-600;
167
+ }
168
+
169
+ // Error state
170
+ .timeline-error {
171
+ @apply p-4 bg-red-50 border border-red-200 rounded m-4;
172
+ }
173
+
174
+ .timeline-error-text {
175
+ @apply text-red-700;
176
+ }
177
+
178
+ .timeline-error-retry {
179
+ @apply mt-2 text-red-600 underline;
180
+ }
181
+
182
+ // Modal styles
183
+ .timeline-modal-overlay {
184
+ @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50;
185
+ }
186
+
187
+ .timeline-modal-content {
188
+ @apply bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-96 overflow-auto;
189
+ }
190
+
191
+ .timeline-modal-header {
192
+ @apply p-4 border-b border-gray-200 flex justify-between items-center;
193
+ }
194
+
195
+ .timeline-modal-title {
196
+ @apply text-lg font-medium;
197
+ }
198
+
199
+ .timeline-modal-close {
200
+ @apply text-gray-400 hover:text-gray-600;
201
+ }
202
+
203
+ .timeline-modal-body {
204
+ @apply p-4;
205
+ }
206
+
207
+ .timeline-operation-details {
208
+ @apply grid grid-cols-1 gap-4 text-sm;
209
+
210
+ @screen md {
211
+ @apply grid-cols-2;
212
+ }
213
+ }
214
+
215
+ .timeline-operation-detail-item {
216
+ // No additional styles needed
217
+ }
218
+
219
+ .timeline-operation-detail-label {
220
+ @apply font-medium;
221
+ }
222
+
223
+ .timeline-operation-detail-value {
224
+ @apply ml-2;
225
+
226
+ &.operation-badge {
227
+ @apply px-2 py-1 rounded text-xs;
228
+ }
229
+
230
+ &.monospace {
231
+ @apply font-mono;
232
+ }
233
+
234
+ &.small {
235
+ @apply text-xs;
236
+ }
237
+ }
238
+
239
+ .timeline-changes-section {
240
+ @apply mt-4;
241
+ }
242
+
243
+ .timeline-changes-title {
244
+ @apply text-sm font-medium;
245
+ }
246
+
247
+ .timeline-changes-content {
248
+ @apply mt-2 p-3 bg-gray-50 rounded text-xs overflow-auto border max-h-32;
249
+ }
250
+
251
+ .timeline-metadata-section {
252
+ @apply mt-4;
253
+ }
254
+
255
+ .timeline-metadata-title {
256
+ @apply text-sm font-medium;
257
+ }
258
+
259
+ .timeline-metadata-grid {
260
+ @apply mt-2 grid grid-cols-2 gap-2 text-xs;
261
+ }
262
+
263
+ .timeline-metadata-item {
264
+ // No additional styles needed for items that are shown
265
+ }
266
+
267
+ .timeline-metadata-key {
268
+ @apply font-medium capitalize;
269
+ }
270
+
271
+ .timeline-metadata-value {
272
+ // No additional styles needed
273
+ }
274
+
275
+ // Responsive adjustments
276
+ @screen sm {
277
+ .timeline-filter-controls {
278
+ @apply flex-nowrap;
279
+ }
280
+
281
+ .timeline-stats {
282
+ @apply grid-cols-4;
283
+ }
284
+ }
285
+
286
+ // Operation color classes
287
+ .operation-insert {
288
+ @apply text-green-600 bg-green-100;
289
+ }
290
+
291
+ .operation-update {
292
+ @apply text-blue-600 bg-blue-100;
293
+ }
294
+
295
+ .operation-delete {
296
+ @apply text-red-600 bg-red-100;
297
+ }
298
+
299
+ .operation-select {
300
+ @apply text-purple-600 bg-purple-100;
301
+ }
302
+
303
+ // Transition classes for Alpine.js
304
+ .timeline-transition-enter {
305
+ @apply transition ease-out duration-300;
306
+ }
307
+
308
+ .timeline-transition-enter-start {
309
+ @apply opacity-0;
310
+ }
311
+
312
+ .timeline-transition-enter-end {
313
+ @apply opacity-100;
314
+ }
315
+
316
+ .timeline-transition-leave {
317
+ @apply transition ease-in duration-200;
318
+ }
319
+
320
+ .timeline-transition-leave-start {
321
+ @apply opacity-100;
322
+ }
323
+
324
+ .timeline-transition-leave-end {
325
+ @apply opacity-0;
326
+ }
@@ -6,12 +6,12 @@ module Dbwatcher
6
6
  class SessionsController < BaseController
7
7
  before_action :find_session, except: [:diagram_types]
8
8
 
9
- def changes_data
10
- Rails.logger.info "API::V1::SessionsController#changes_data: Getting changes for session #{@session.id}"
9
+ def tables_data
10
+ Rails.logger.info "API::V1::SessionsController#tables_data: Getting tables for session #{@session.id}"
11
11
 
12
- # Paginated, filtered changes data
12
+ # Paginated, filtered tables data
13
13
  # Convert ActionController::Parameters to a hash before passing to service
14
- service = Dbwatcher::Services::Api::ChangesDataService.new(@session, filter_params.to_h)
14
+ service = Dbwatcher::Services::Api::TablesDataService.new(@session, filter_params.to_h)
15
15
  render json: service.call
16
16
  end
17
17
 
@@ -39,6 +39,20 @@ module Dbwatcher
39
39
  end
40
40
  end
41
41
 
42
+ def timeline_data
43
+ Rails.logger.info "API::V1::SessionsController#timeline_data: Getting timeline for session #{@session.id}"
44
+
45
+ # Timeline data processed from session changes
46
+ service = Dbwatcher::Services::TimelineDataService.new(@session)
47
+ result = service.call
48
+
49
+ if result[:errors].any?
50
+ render json: { error: result[:errors].first[:message] }, status: :unprocessable_entity
51
+ else
52
+ render json: result
53
+ end
54
+ end
55
+
42
56
  def diagram_types
43
57
  Rails.logger.info "API::V1::SessionsController#diagram_types: Getting available diagram types"
44
58
 
@@ -9,7 +9,7 @@ module Dbwatcher
9
9
  end
10
10
 
11
11
  def show
12
- @active_tab = params[:tab] || "changes"
12
+ @active_tab = params[:tab] || "tables"
13
13
  # Debug logging
14
14
  Rails.logger.info "SessionsController#show: Session ID: #{@session.id.inspect}, Class: #{@session.class}"
15
15
  end
@@ -5,8 +5,11 @@
5
5
 
6
6
  <!-- Tab Bar -->
7
7
  <div class="tab-bar">
8
- <%= link_to session_path(session.id, tab: 'changes'), class: "tab-item #{active_tab == 'changes' ? 'active' : ''}" do %>
9
- Changes
8
+ <%= link_to session_path(session.id, tab: 'tables'), class: "tab-item #{active_tab == 'tables' ? 'active' : ''}" do %>
9
+ Tables
10
+ <% end %>
11
+ <%= link_to session_path(session.id, tab: 'timeline'), class: "tab-item #{active_tab == 'timeline' ? 'active' : ''}" do %>
12
+ Timeline
10
13
  <% end %>
11
14
  <%= link_to session_path(session.id, tab: 'summary'), class: "tab-item #{active_tab == 'summary' ? 'active' : ''}" do %>
12
15
  Summary
@@ -59,7 +59,7 @@
59
59
  <div class="grid grid-cols-4 gap-3">
60
60
  <template x-for="table in summaryData.tables_breakdown" :key="table.table_name">
61
61
  <div class="bg-gray-50 border border-gray-200 p-2 hover:bg-gray-100 cursor-pointer transition-colors"
62
- @click="window.location.href=`/dbwatcher/sessions/${summaryData.session_id}/changes?table=${table.table_name}`">
62
+ @click="window.location.href=`/dbwatcher/sessions/${summaryData.session_id}?tab=tables`">
63
63
  <h4 class="text-xs font-medium text-gray-800 mb-2 truncate" x-text="table.table_name"></h4>
64
64
  <div class="space-y-1">
65
65
  <template x-for="(count, op) in table.operations" :key="op">
@@ -26,9 +26,82 @@
26
26
 
27
27
  <!-- Multiple Tables - Enhanced UI Structure -->
28
28
  <template x-if="!loading && !error && Object.keys(tableData).length > 0">
29
- <div class="h-full overflow-auto p-2 bg-gray-50">
30
- <template x-for="[tableName, tableInfo] in Object.entries(tableData)" :key="tableName">
31
- <div class="mb-4 bg-white border border-gray-200 rounded shadow-sm" x-data="{ expanded: true }">
29
+ <div class="h-full flex flex-col bg-gray-50">
30
+ <!-- Filter Header -->
31
+ <div class="p-3 border-b border-gray-300 bg-gray-50">
32
+ <div class="flex items-center justify-between gap-4">
33
+ <h3 class="text-sm font-medium text-gray-900">Tables View</h3>
34
+ <div class="flex items-center gap-3 text-xs">
35
+ <!-- Search Filter -->
36
+ <input type="text"
37
+ x-model="filters.search"
38
+ @input="applyFilters()"
39
+ placeholder="Search..."
40
+ class="px-2 py-1 border border-gray-300 rounded text-xs w-32 focus:outline-none focus:ring-1 focus:ring-blue-medium">
41
+
42
+ <!-- Operation Filter -->
43
+ <select x-model="filters.operation"
44
+ @change="applyFilters()"
45
+ class="px-2 py-1 border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-medium">
46
+ <option value="">All Operations</option>
47
+ <template x-for="operation in getAvailableOperations()" :key="operation">
48
+ <option :value="operation" x-text="operation"></option>
49
+ </template>
50
+ </select>
51
+
52
+ <!-- Multi-Table Filter -->
53
+ <div class="relative" x-data="{ showTableFilter: false }">
54
+ <button @click="showTableFilter = !showTableFilter"
55
+ class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50 flex items-center gap-1">
56
+ <span x-text="filters.selectedTables.length === 0 ? 'All Tables' : `${filters.selectedTables.length} Tables`"></span>
57
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
59
+ </svg>
60
+ </button>
61
+
62
+ <div x-show="showTableFilter"
63
+ x-transition
64
+ @click.away="showTableFilter = false"
65
+ class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-50 min-w-48 max-h-48 overflow-auto">
66
+ <div class="p-2 border-b border-gray-200 flex gap-2">
67
+ <button @click="selectAllTables(); showTableFilter = false"
68
+ class="text-xs text-blue-medium hover:text-navy-dark underline">
69
+ All
70
+ </button>
71
+ <button @click="clearTableFilters()"
72
+ class="text-xs text-blue-medium hover:text-navy-dark underline">
73
+ None
74
+ </button>
75
+ </div>
76
+ <div class="p-1">
77
+ <template x-for="tableName in getAvailableTables()" :key="tableName">
78
+ <label class="flex items-center gap-2 p-1 rounded hover:bg-gray-100 cursor-pointer">
79
+ <input type="checkbox"
80
+ :value="tableName"
81
+ x-model="filters.selectedTables"
82
+ @change="applyFilters()"
83
+ class="form-checkbox h-3 w-3 text-blue-medium">
84
+ <span class="text-xs text-gray-700" x-text="tableName"></span>
85
+ </label>
86
+ </template>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Clear Filters -->
92
+ <button @click="clearAllFilters()"
93
+ class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50">
94
+ Clear
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Tables Content -->
101
+ <div class="flex-1 overflow-auto p-2">
102
+ <template x-for="[tableName, tableInfo] in Object.entries(tableData)" :key="tableName">
103
+ <div x-show="filters.selectedTables.length === 0 || filters.selectedTables.includes(tableName)"
104
+ class="mb-4 bg-white border border-gray-200 rounded shadow-sm" x-data="{ expanded: true }">
32
105
  <!-- Table Header with Column Controls -->
33
106
  <div class="bg-gray-100 px-3 py-2 flex items-center cursor-pointer border-b border-gray-200"
34
107
  @click="expanded = !expanded"
@@ -38,7 +111,12 @@
38
111
  fill="currentColor" viewBox="0 0 20 20">
39
112
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
40
113
  </svg>
41
- <h3 class="text-sm font-medium text-gray-900 flex-1" x-text="tableName"></h3>
114
+ <div class="flex-1">
115
+ <h3 class="text-sm font-medium text-gray-900" x-text="tableName"></h3>
116
+ <div class="text-xs text-gray-500 mt-1" x-show="tableInfo.model_class">
117
+ <span x-text="tableInfo.model_class"></span>
118
+ </div>
119
+ </div>
42
120
  <div class="flex gap-2 mr-4">
43
121
  <template x-for="[op, count] in Object.entries(tableInfo.operations || {})" :key="op">
44
122
  <span x-show="count > 0" class="badge changes-table-badge" :class="`badge-${op.toLowerCase()}`" x-text="count"></span>
@@ -85,7 +163,8 @@
85
163
  <div :id="`changes-table-${tableName}`" class="table-container"></div>
86
164
  </div>
87
165
  </div>
88
- </template>
166
+ </template>
167
+ </div>
89
168
  </div>
90
169
  </template>
91
170
  </div>