dbwatcher 1.1.2 → 1.1.4

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 (40) 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 +27 -17
  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/configuration.rb +11 -0
  19. data/lib/dbwatcher/logging.rb +23 -1
  20. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  21. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  22. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
  23. data/lib/dbwatcher/services/diagram_generator.rb +35 -69
  24. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
  25. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
  26. data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
  27. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
  28. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
  29. data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
  30. data/lib/dbwatcher/services/diagram_system.rb +14 -1
  31. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
  32. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
  33. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  34. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  35. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  36. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  37. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  38. data/lib/dbwatcher/version.rb +1 -1
  39. data/lib/dbwatcher.rb +1 -1
  40. metadata +13 -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,34 +6,37 @@ 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}"
11
-
12
- # Paginated, filtered changes data
13
- # Convert ActionController::Parameters to a hash before passing to service
14
- service = Dbwatcher::Services::Api::ChangesDataService.new(@session, filter_params.to_h)
9
+ def tables_data
10
+ Rails.logger.info "API::V1::SessionsController#tables_data: Getting tables for session #{@session.id}"
11
+ service = Dbwatcher::Services::Api::TablesDataService.new(@session, tables_data_params)
15
12
  render json: service.call
16
13
  end
17
14
 
18
15
  def summary_data
19
16
  Rails.logger.info "API::V1::SessionsController#summary_data: Getting summary for session #{@session.id}"
20
-
21
- # Aggregated summary statistics
22
17
  service = Dbwatcher::Services::Api::SummaryDataService.new(@session)
23
18
  render json: service.call
24
19
  end
25
20
 
26
21
  def diagram_data
27
22
  Rails.logger.info "API::V1::SessionsController#diagram_data: Getting diagram for session #{@session.id}"
28
-
29
- # Generated diagram content with caching
30
- # Convert ActionController::Parameters to a hash before passing to service
31
- diagram_params = params.to_unsafe_h
32
23
  service = Dbwatcher::Services::Api::DiagramDataService.new(@session, params[:type], diagram_params)
33
24
  result = service.call
34
25
 
35
26
  if result[:error]
36
- render json: { error: result[:error] }, status: :unprocessable_entity
27
+ render_error(result[:error])
28
+ else
29
+ render json: result
30
+ end
31
+ end
32
+
33
+ def timeline_data
34
+ Rails.logger.info "API::V1::SessionsController#timeline_data: Getting timeline for session #{@session.id}"
35
+ service = Dbwatcher::Services::TimelineDataService.new(@session)
36
+ result = service.call
37
+
38
+ if result[:errors].any?
39
+ render_error(result[:errors].first[:message])
37
40
  else
38
41
  render json: result
39
42
  end
@@ -41,7 +44,6 @@ module Dbwatcher
41
44
 
42
45
  def diagram_types
43
46
  Rails.logger.info "API::V1::SessionsController#diagram_types: Getting available diagram types"
44
-
45
47
  render json: {
46
48
  types: Dbwatcher::Services::Api::DiagramDataService.available_types_with_metadata,
47
49
  default_type: "database_tables"
@@ -52,11 +54,19 @@ module Dbwatcher
52
54
 
53
55
  def find_session
54
56
  @session = Storage.sessions.find(params[:id])
55
- render json: { error: "Session not found" }, status: :not_found unless @session
57
+ render_error("Session not found", :not_found) unless @session
58
+ end
59
+
60
+ def tables_data_params
61
+ params.permit(:id, :table, :operation, :page, :per_page, session: {}).to_h
62
+ end
63
+
64
+ def diagram_params
65
+ params.permit(:type, :format, :include_columns, :show_relationships, session: {}).to_h
56
66
  end
57
67
 
58
- def filter_params
59
- params.permit(:table, :operation, :page, :per_page)
68
+ def render_error(message, status = :unprocessable_entity)
69
+ render json: { error: message }, status: status
60
70
  end
61
71
  end
62
72
  end
@@ -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>