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.
- checksums.yaml +4 -4
- data/app/assets/config/dbwatcher_manifest.js +1 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
- 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 +298 -1
- data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
- data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +18 -4
- data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
- data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
- data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
- data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
- data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -1
- data/app/views/layouts/dbwatcher/application.html.erb +1 -0
- data/config/routes.rb +2 -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/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/version.rb +1 -1
- data/lib/dbwatcher.rb +1 -1
- 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
|
10
|
-
Rails.logger.info "API::V1::SessionsController#
|
9
|
+
def tables_data
|
10
|
+
Rails.logger.info "API::V1::SessionsController#tables_data: Getting tables for session #{@session.id}"
|
11
11
|
|
12
|
-
# Paginated, filtered
|
12
|
+
# Paginated, filtered tables data
|
13
13
|
# Convert ActionController::Parameters to a hash before passing to service
|
14
|
-
service = Dbwatcher::Services::Api::
|
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
|
|
@@ -5,8 +5,11 @@
|
|
5
5
|
|
6
6
|
<!-- Tab Bar -->
|
7
7
|
<div class="tab-bar">
|
8
|
-
<%= link_to session_path(session.id, tab: '
|
9
|
-
|
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}
|
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
|
30
|
-
|
31
|
-
|
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
|
-
<
|
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
|
-
|
166
|
+
</template>
|
167
|
+
</div>
|
89
168
|
</div>
|
90
169
|
</template>
|
91
170
|
</div>
|