dbwatcher 1.0.0 → 1.1.0
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 +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,265 @@
|
|
1
|
+
<!-- Changes Content - Hybrid Tabulator Implementation with Original UI Structure -->
|
2
|
+
<div class="h-full"
|
3
|
+
x-data="changesTableHybrid({ sessionId: '<%= @session.id %>' })">
|
4
|
+
|
5
|
+
<!-- Loading State -->
|
6
|
+
<div x-show="loading" class="flex items-center justify-center h-64">
|
7
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
8
|
+
<span class="ml-2 text-gray-600">Loading changes...</span>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<!-- Error State -->
|
12
|
+
<div x-show="error" class="p-4 bg-red-50 border border-red-200 rounded">
|
13
|
+
<p class="text-red-700" x-text="error"></p>
|
14
|
+
<button @click="loadChangesData()" class="mt-2 text-red-600 underline">Retry</button>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<!-- No Data State -->
|
18
|
+
<div x-show="!loading && !error && Object.keys(tableData).length === 0"
|
19
|
+
class="flex flex-col items-center justify-center h-64">
|
20
|
+
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
21
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
22
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
23
|
+
</svg>
|
24
|
+
<p class="mt-2 text-gray-500">No changes data available</p>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<!-- Multiple Tables - Original UI Structure -->
|
28
|
+
<template x-if="!loading && !error && Object.keys(tableData).length > 0">
|
29
|
+
<div class="h-full overflow-auto">
|
30
|
+
<template x-for="[tableName, tableInfo] in Object.entries(tableData)" :key="tableName">
|
31
|
+
<div class="border-b border-gray-300" x-data="{ expanded: true }">
|
32
|
+
<!-- Table Header with Column Controls -->
|
33
|
+
<div class="bg-gray-100 px-3 py-2 flex items-center cursor-pointer"
|
34
|
+
@click="expanded = !expanded"
|
35
|
+
:id="`table-${tableName}`">
|
36
|
+
<svg class="w-3 h-3 mr-2 transition-transform"
|
37
|
+
:class="{ 'rotate-90': expanded }"
|
38
|
+
fill="currentColor" viewBox="0 0 20 20">
|
39
|
+
<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
|
+
</svg>
|
41
|
+
<h3 class="text-sm font-medium text-gray-900 flex-1" x-text="tableName"></h3>
|
42
|
+
<div class="flex gap-2 mr-4">
|
43
|
+
<template x-for="[op, count] in Object.entries(tableInfo.operations || {})" :key="op">
|
44
|
+
<span x-show="count > 0" class="badge" :class="`badge-${op.toLowerCase()}`" x-text="count"></span>
|
45
|
+
</template>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<!-- Column Visibility Button -->
|
49
|
+
<button @click.stop="toggleColumnSelector(tableName)"
|
50
|
+
class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50 relative">
|
51
|
+
Columns
|
52
|
+
</button>
|
53
|
+
</div>
|
54
|
+
|
55
|
+
<!-- Column Selector Dropdown -->
|
56
|
+
<div x-show="showColumnSelector === tableName"
|
57
|
+
x-transition
|
58
|
+
@click.away="showColumnSelector = null"
|
59
|
+
class="absolute z-50 bg-white border border-gray-300 rounded shadow-lg p-3 max-h-64 overflow-auto"
|
60
|
+
style="right: 1rem; margin-top: -2px;">
|
61
|
+
<div class="text-xs font-medium mb-2">Select Visible Columns:</div>
|
62
|
+
<div class="space-y-1 min-w-48">
|
63
|
+
<template x-for="column in tableInfo.columns" :key="column">
|
64
|
+
<label class="flex items-center text-xs hover:bg-gray-50 p-1 rounded">
|
65
|
+
<input type="checkbox"
|
66
|
+
:checked="isColumnVisible(tableName, column)"
|
67
|
+
@change="toggleColumnVisibility(tableName, column)"
|
68
|
+
class="mr-2">
|
69
|
+
<span class="flex-1" x-text="column"></span>
|
70
|
+
</label>
|
71
|
+
</template>
|
72
|
+
</div>
|
73
|
+
<div class="mt-2 pt-2 border-t border-gray-200 flex gap-1">
|
74
|
+
<button @click="selectAllColumns(tableName)"
|
75
|
+
class="text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700">All</button>
|
76
|
+
<button @click="selectNoneColumns(tableName)"
|
77
|
+
class="text-xs bg-gray-600 text-white px-2 py-1 rounded hover:bg-gray-700">None</button>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
|
81
|
+
<!-- Tabulator Container for This Table -->
|
82
|
+
<div x-show="expanded" x-collapse>
|
83
|
+
<div :id="`changes-table-${tableName}`" class="table-container"></div>
|
84
|
+
</div>
|
85
|
+
</div>
|
86
|
+
</template>
|
87
|
+
</div>
|
88
|
+
</template>
|
89
|
+
</div>
|
90
|
+
|
91
|
+
<!-- Custom Tabulator Styles -->
|
92
|
+
<style>
|
93
|
+
/* Override Tabulator styles to match current design */
|
94
|
+
.tabulator {
|
95
|
+
font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace !important;
|
96
|
+
font-size: 12px !important;
|
97
|
+
border: none;
|
98
|
+
background: white;
|
99
|
+
border-collapse: separate;
|
100
|
+
border-spacing: 0;
|
101
|
+
}
|
102
|
+
|
103
|
+
.tabulator .tabulator-header {
|
104
|
+
background: #f3f3f3 !important;
|
105
|
+
border-bottom: 2px solid #e8e8e8 !important;
|
106
|
+
font-size: 11px !important;
|
107
|
+
}
|
108
|
+
|
109
|
+
.tabulator .tabulator-header .tabulator-col {
|
110
|
+
background: #f3f3f3 !important;
|
111
|
+
border-right: 1px solid #e8e8e8 !important;
|
112
|
+
padding: 4px 8px !important;
|
113
|
+
font-weight: 500 !important;
|
114
|
+
text-transform: none !important;
|
115
|
+
height: 32px !important;
|
116
|
+
text-align: left;
|
117
|
+
position: sticky;
|
118
|
+
top: 0;
|
119
|
+
z-index: 10;
|
120
|
+
}
|
121
|
+
|
122
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row {
|
123
|
+
background: white !important;
|
124
|
+
border-bottom: 1px solid #f0f0f0 !important;
|
125
|
+
min-height: auto;
|
126
|
+
}
|
127
|
+
|
128
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row:hover {
|
129
|
+
background: #f3f4f6 !important;
|
130
|
+
}
|
131
|
+
|
132
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell {
|
133
|
+
border-right: 1px solid #f0f0f0 !important;
|
134
|
+
padding: 2px 8px !important;
|
135
|
+
overflow: hidden;
|
136
|
+
text-overflow: ellipsis;
|
137
|
+
white-space: nowrap;
|
138
|
+
vertical-align: top;
|
139
|
+
font-size: 12px !important;
|
140
|
+
height: auto;
|
141
|
+
min-height: 28px;
|
142
|
+
}
|
143
|
+
|
144
|
+
/* Override for UPDATE operation cells to allow multi-line */
|
145
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell:has(.space-y-1) {
|
146
|
+
white-space: normal;
|
147
|
+
height: auto;
|
148
|
+
padding: 4px 8px !important;
|
149
|
+
}
|
150
|
+
|
151
|
+
/* Maintain sticky columns exactly as current */
|
152
|
+
.tabulator .tabulator-header .tabulator-col.sticky-left-0 {
|
153
|
+
position: sticky !important;
|
154
|
+
left: 0 !important;
|
155
|
+
z-index: 20 !important;
|
156
|
+
background: #f3f3f3 !important;
|
157
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.1) !important;
|
158
|
+
}
|
159
|
+
|
160
|
+
.tabulator .tabulator-header .tabulator-col.sticky-left-1 {
|
161
|
+
position: sticky !important;
|
162
|
+
left: 60px !important;
|
163
|
+
z-index: 19 !important;
|
164
|
+
background: #f3f3f3 !important;
|
165
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.1) !important;
|
166
|
+
}
|
167
|
+
|
168
|
+
.tabulator .tabulator-header .tabulator-col.sticky-left-2 {
|
169
|
+
position: sticky !important;
|
170
|
+
left: 108px !important;
|
171
|
+
z-index: 18 !important;
|
172
|
+
background: #f3f3f3 !important;
|
173
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.1) !important;
|
174
|
+
}
|
175
|
+
|
176
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.sticky-left-0 {
|
177
|
+
position: sticky !important;
|
178
|
+
left: 0 !important;
|
179
|
+
background: white !important;
|
180
|
+
z-index: 5 !important;
|
181
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.05) !important;
|
182
|
+
}
|
183
|
+
|
184
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.sticky-left-1 {
|
185
|
+
position: sticky !important;
|
186
|
+
left: 60px !important;
|
187
|
+
background: white !important;
|
188
|
+
z-index: 4 !important;
|
189
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.05) !important;
|
190
|
+
}
|
191
|
+
|
192
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.sticky-left-2 {
|
193
|
+
position: sticky !important;
|
194
|
+
left: 108px !important;
|
195
|
+
background: white !important;
|
196
|
+
z-index: 3 !important;
|
197
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.05) !important;
|
198
|
+
}
|
199
|
+
|
200
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row:hover .tabulator-cell.sticky-left-0,
|
201
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row:hover .tabulator-cell.sticky-left-1,
|
202
|
+
.tabulator .tabulator-tableholder .tabulator-table .tabulator-row:hover .tabulator-cell.sticky-left-2 {
|
203
|
+
background: #f9fafb !important;
|
204
|
+
}
|
205
|
+
|
206
|
+
/* Operation badges */
|
207
|
+
.badge-insert { background: #10b981; color: white; }
|
208
|
+
.badge-update { background: #6CADDF; color: white; }
|
209
|
+
.badge-delete { background: #ef4444; color: white; }
|
210
|
+
.badge-select { background: #6b7280; color: white; }
|
211
|
+
|
212
|
+
.badge {
|
213
|
+
padding: 1px 6px;
|
214
|
+
font-size: 10px;
|
215
|
+
border-radius: 3px;
|
216
|
+
font-weight: 500;
|
217
|
+
text-transform: uppercase;
|
218
|
+
display: inline-block;
|
219
|
+
width: 18px;
|
220
|
+
height: 18px;
|
221
|
+
line-height: 18px;
|
222
|
+
text-align: center;
|
223
|
+
}
|
224
|
+
|
225
|
+
/* Row detail styling */
|
226
|
+
.row-detail {
|
227
|
+
border-top: 1px solid #e5e7eb;
|
228
|
+
background: #f9fafb;
|
229
|
+
padding: 0;
|
230
|
+
}
|
231
|
+
|
232
|
+
.row-detail td {
|
233
|
+
padding: 8px !important;
|
234
|
+
vertical-align: top !important;
|
235
|
+
border-right: 1px solid #e5e7eb !important;
|
236
|
+
}
|
237
|
+
|
238
|
+
.row-detail h4 {
|
239
|
+
font-weight: 600;
|
240
|
+
color: #374151;
|
241
|
+
margin-bottom: 8px;
|
242
|
+
}
|
243
|
+
|
244
|
+
/* Maintain sticky positioning in expanded rows - combined first 3 columns */
|
245
|
+
.row-detail .sticky-left-0 {
|
246
|
+
position: sticky !important;
|
247
|
+
left: 0 !important;
|
248
|
+
z-index: 5 !important; /* Lower z-index to prevent overlay issues */
|
249
|
+
background: #f9fafb !important; /* Match row-detail background */
|
250
|
+
box-shadow: 2px 0 4px rgba(0,0,0,0.1) !important;
|
251
|
+
width: 268px !important;
|
252
|
+
min-width: 268px !important;
|
253
|
+
}
|
254
|
+
|
255
|
+
/* Expand button styling */
|
256
|
+
.expand-btn {
|
257
|
+
padding: 2px;
|
258
|
+
border-radius: 2px;
|
259
|
+
transition: all 0.15s ease;
|
260
|
+
}
|
261
|
+
|
262
|
+
.expand-btn:hover {
|
263
|
+
background-color: #f3f4f6;
|
264
|
+
}
|
265
|
+
</style>
|
@@ -0,0 +1,166 @@
|
|
1
|
+
<!-- Diagrams Content - API-First Implementation -->
|
2
|
+
<div class="h-full"
|
3
|
+
x-data="diagrams({ sessionId: '<%= @session.id %>' })"
|
4
|
+
x-init="init()">
|
5
|
+
<!-- Diagram Controls - consistent with other tabs -->
|
6
|
+
<div class="p-3 border-b border-gray-300 bg-gray-100">
|
7
|
+
<div class="flex items-center justify-between">
|
8
|
+
<div class="flex items-center gap-2">
|
9
|
+
<div class="flex items-center gap-2">
|
10
|
+
<label class="text-xs font-medium text-gray-700">Diagram Type:</label>
|
11
|
+
<select
|
12
|
+
x-model="selectedType"
|
13
|
+
@change="loadDiagram()"
|
14
|
+
class="compact-select text-xs border border-gray-300 rounded px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
15
|
+
>
|
16
|
+
<template x-for="[type, metadata] in Object.entries(diagramTypes)" :key="type">
|
17
|
+
<option :value="type" x-text="metadata.display_name"></option>
|
18
|
+
</template>
|
19
|
+
</select>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<button
|
23
|
+
@click="loadDiagram()"
|
24
|
+
:disabled="loading"
|
25
|
+
class="<%= diagram_button_classes(:primary) %>"
|
26
|
+
>
|
27
|
+
<span x-show="!loading">Generate</span>
|
28
|
+
<span x-show="loading">Generating...</span>
|
29
|
+
</button>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<!-- Right-aligned controls -->
|
33
|
+
<div class="flex items-center gap-1">
|
34
|
+
<button
|
35
|
+
x-show="diagramContent && !loading"
|
36
|
+
@click="toggleCodeView()"
|
37
|
+
class="<%= diagram_button_classes(:secondary) %>"
|
38
|
+
>
|
39
|
+
<span x-show="!showCodeView">View Code</span>
|
40
|
+
<span x-show="showCodeView">Hide Code</span>
|
41
|
+
</button>
|
42
|
+
<button
|
43
|
+
x-show="diagramContent && !loading"
|
44
|
+
@click="downloadSVG()"
|
45
|
+
class="<%= diagram_button_classes(:primary) %>"
|
46
|
+
>
|
47
|
+
Download
|
48
|
+
</button>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<!-- Diagram Content Area -->
|
54
|
+
<div class="flex-1 overflow-hidden relative mx-1 mb-1 mt-2 rounded-lg" style="min-height: 600px; height: calc(100vh - 150px);">
|
55
|
+
<!-- Loading Types State (Initial) -->
|
56
|
+
<div
|
57
|
+
x-show="loading && Object.keys(diagramTypes).length === 0"
|
58
|
+
class="absolute inset-0 flex flex-col items-center justify-center bg-white bg-opacity-75 rounded-md border border-gray-200"
|
59
|
+
style="z-index: 10;"
|
60
|
+
>
|
61
|
+
<div class="flex flex-col items-center">
|
62
|
+
<div class="w-6 h-6 border-3 border-blue-600 border-t-transparent rounded-full animate-spin mb-2"></div>
|
63
|
+
<div class="text-xs text-gray-600">Loading diagram types...</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
|
67
|
+
<!-- Error State -->
|
68
|
+
<div
|
69
|
+
x-show="error"
|
70
|
+
class="absolute inset-0 flex flex-col items-center justify-center p-4 bg-red-50 border border-red-200 rounded-md"
|
71
|
+
style="z-index: 10;"
|
72
|
+
>
|
73
|
+
<svg class="w-10 h-10 mb-3 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
74
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
75
|
+
</svg>
|
76
|
+
<p class="text-sm font-medium mb-1 text-red-700" x-text="error"></p>
|
77
|
+
<button @click="loadDiagramTypes().then(() => loadDiagram())" class="mt-2 text-red-600 underline">Retry</button>
|
78
|
+
</div>
|
79
|
+
|
80
|
+
<!-- Empty State -->
|
81
|
+
<div
|
82
|
+
x-show="!diagramContent && !loading && !generating && !error && Object.keys(diagramTypes).length > 0"
|
83
|
+
class="absolute inset-0 flex flex-col items-center justify-center text-gray-500 p-6 border border-gray-200 rounded-md bg-gray-50"
|
84
|
+
>
|
85
|
+
<svg class="w-10 h-10 mb-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
86
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
87
|
+
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
88
|
+
</svg>
|
89
|
+
<p class="text-sm font-medium mb-1">Database Diagram</p>
|
90
|
+
<p class="text-xs text-center text-gray-400">Select a diagram type and click Generate to visualize your database structure</p>
|
91
|
+
</div>
|
92
|
+
|
93
|
+
<!-- Generating Diagram State -->
|
94
|
+
<div
|
95
|
+
x-show="generating"
|
96
|
+
class="absolute inset-0 flex flex-col items-center justify-center bg-white bg-opacity-75 rounded-md border border-gray-200"
|
97
|
+
style="z-index: 10;"
|
98
|
+
>
|
99
|
+
<div class="flex flex-col items-center">
|
100
|
+
<div class="w-6 h-6 border-3 border-blue-600 border-t-transparent rounded-full animate-spin mb-2"></div>
|
101
|
+
<div class="text-xs text-gray-600">Generating diagram...</div>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Rendered Diagram -->
|
106
|
+
<div
|
107
|
+
x-ref="diagramContainer"
|
108
|
+
x-show="diagramContent && !loading && !error && !showCodeView"
|
109
|
+
class="absolute inset-0 p-4 overflow-hidden bg-white border border-gray-200 rounded-md shadow-inner"
|
110
|
+
style="display: flex; flex-direction: column;"
|
111
|
+
>
|
112
|
+
<!-- Mermaid diagram will be rendered here -->
|
113
|
+
<div class="flex-1 bg-gray-50 rounded-md p-2" style="min-height: 400px;">
|
114
|
+
<!-- This div will contain the actual diagram -->
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
|
118
|
+
<!-- Code View -->
|
119
|
+
<div
|
120
|
+
x-show="diagramContent && !loading && !error && showCodeView"
|
121
|
+
class="absolute inset-0 p-4 overflow-hidden bg-white border border-gray-200 rounded-md shadow-inner"
|
122
|
+
style="display: flex; flex-direction: column;"
|
123
|
+
>
|
124
|
+
<div class="flex justify-between items-center mb-2">
|
125
|
+
<h3 class="text-sm font-medium text-gray-700">Mermaid Diagram Code</h3>
|
126
|
+
<button
|
127
|
+
x-ref="copyButton"
|
128
|
+
@click="copyDiagramCode()"
|
129
|
+
class="<%= diagram_button_classes(:primary) %>"
|
130
|
+
>
|
131
|
+
Copy Code
|
132
|
+
</button>
|
133
|
+
</div>
|
134
|
+
<div class="flex-1 bg-gray-50 rounded-md overflow-auto diagram-code-view" style="min-height: 400px;">
|
135
|
+
<div class="relative h-full">
|
136
|
+
<pre x-ref="codeContainer" class="text-xs font-mono p-4 whitespace-pre-wrap overflow-x-auto h-full" style="max-height: calc(100vh - 220px); overflow-y: auto;" x-text="diagramContent"></pre>
|
137
|
+
<div class="absolute top-0 right-2 bg-gray-200 text-xs text-gray-500 px-1 py-0.5 rounded-b opacity-70">
|
138
|
+
Mermaid Syntax
|
139
|
+
</div>
|
140
|
+
</div>
|
141
|
+
</div>
|
142
|
+
</div>
|
143
|
+
|
144
|
+
<!-- Error State -->
|
145
|
+
<div
|
146
|
+
x-show="error"
|
147
|
+
class="absolute inset-0 flex items-center justify-center p-6 border border-gray-200 rounded-md bg-gray-50"
|
148
|
+
>
|
149
|
+
<div class="max-w-md w-full text-center">
|
150
|
+
<div class="bg-red-50 border border-red-200 rounded p-4">
|
151
|
+
<svg class="w-10 h-10 text-red-400 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
152
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
153
|
+
</svg>
|
154
|
+
<h3 class="text-sm font-medium text-red-800 mb-2">Error Generating Diagram</h3>
|
155
|
+
<p class="text-xs text-red-600 mb-3" x-text="error"></p>
|
156
|
+
<button
|
157
|
+
@click="error = null; loadDiagram()"
|
158
|
+
class="<%= diagram_button_classes(:primary) %>"
|
159
|
+
>
|
160
|
+
Try Again
|
161
|
+
</button>
|
162
|
+
</div>
|
163
|
+
</div>
|
164
|
+
</div>
|
165
|
+
</div>
|
166
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<!-- Compact Header -->
|
2
|
+
<div class="h-10 bg-navy-dark text-white flex items-center px-4">
|
3
|
+
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
4
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
5
|
+
</svg>
|
6
|
+
<h1 class="text-sm font-medium truncate"><%= session.name %></h1>
|
7
|
+
<span class="ml-auto text-xs text-blue-light whitespace-nowrap">
|
8
|
+
<%= Time.parse(session.started_at.to_s).strftime("%Y-%m-%d %H:%M:%S") rescue session.started_at %> -
|
9
|
+
<%= Time.parse(session.ended_at.to_s).strftime("%Y-%m-%d %H:%M:%S") rescue session.ended_at %>
|
10
|
+
</span>
|
11
|
+
</div>
|
@@ -0,0 +1,88 @@
|
|
1
|
+
<!-- Summary Content - API-First Implementation -->
|
2
|
+
<div class="h-full"
|
3
|
+
x-data="summary({ sessionId: '<%= @session.id %>' })">
|
4
|
+
|
5
|
+
<!-- Loading State -->
|
6
|
+
<div x-show="loading" class="flex items-center justify-center h-64">
|
7
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-500"></div>
|
8
|
+
<span class="ml-2 text-gray-600">Loading summary...</span>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<!-- Error State -->
|
12
|
+
<div x-show="error" class="p-4 bg-red-50 border border-red-200 rounded">
|
13
|
+
<p class="text-red-700" x-text="error"></p>
|
14
|
+
<button @click="loadSummaryData()" class="mt-2 text-red-600 underline">Retry</button>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<!-- Content when data is loaded -->
|
18
|
+
<template x-if="!loading && !error">
|
19
|
+
<div class="h-full overflow-auto">
|
20
|
+
<!-- Session Overview -->
|
21
|
+
<div class="border-b border-gray-300">
|
22
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
23
|
+
<h3 class="text-sm font-medium text-gray-900">Session Overview</h3>
|
24
|
+
</div>
|
25
|
+
<div class="bg-white p-3">
|
26
|
+
<div class="grid grid-cols-4 gap-4 text-xs">
|
27
|
+
<div>
|
28
|
+
<div class="text-gray-500 font-medium mb-1">Started At</div>
|
29
|
+
<div class="text-gray-900 font-mono" x-text="formatStartTime()"></div>
|
30
|
+
</div>
|
31
|
+
<div>
|
32
|
+
<div class="text-gray-500 font-medium mb-1">End Time</div>
|
33
|
+
<div class="text-gray-900 font-mono" x-text="formatEndTime()"></div>
|
34
|
+
</div>
|
35
|
+
<div>
|
36
|
+
<div class="text-gray-500 font-medium mb-1">Duration</div>
|
37
|
+
<div class="text-gray-900 font-mono" x-text="formatDuration()"></div>
|
38
|
+
</div>
|
39
|
+
<div>
|
40
|
+
<div class="text-gray-500 font-medium mb-1">Status</div>
|
41
|
+
<div class="flex items-center">
|
42
|
+
<div class="h-2 w-2 rounded-full mr-2"
|
43
|
+
:class="formatEndTime() === 'Active' ? 'bg-green-500 animate-pulse' : 'bg-gray-500'"></div>
|
44
|
+
<span class="text-gray-900 font-medium text-xs"
|
45
|
+
x-text="formatEndTime() === 'Active' ? 'Active' : 'Completed'"></span>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
|
53
|
+
<!-- Tables Grid -->
|
54
|
+
<div x-show="summaryData.tables_breakdown && summaryData.tables_breakdown.length > 0" class="border-b border-gray-300">
|
55
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
56
|
+
<h3 class="text-sm font-medium text-gray-900">Table Activity Details</h3>
|
57
|
+
</div>
|
58
|
+
<div class="bg-white p-3">
|
59
|
+
<div class="grid grid-cols-4 gap-3">
|
60
|
+
<template x-for="table in summaryData.tables_breakdown" :key="table.table_name">
|
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}`">
|
63
|
+
<h4 class="text-xs font-medium text-gray-800 mb-2 truncate" x-text="table.table_name"></h4>
|
64
|
+
<div class="space-y-1">
|
65
|
+
<template x-for="(count, op) in table.operations" :key="op">
|
66
|
+
<div class="flex justify-between items-center" x-show="count > 0">
|
67
|
+
<span class="badge" :class="`badge-${op.toLowerCase()}`" x-text="op.charAt(0)"></span>
|
68
|
+
<span class="text-xs font-medium text-gray-900" x-text="count"></span>
|
69
|
+
</div>
|
70
|
+
</template>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</template>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
|
78
|
+
<!-- Empty State -->
|
79
|
+
<div x-show="!summaryData.tables_breakdown || summaryData.tables_breakdown.length === 0" class="bg-white p-8 text-center">
|
80
|
+
<svg class="w-8 h-8 mx-auto mb-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
81
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
82
|
+
</svg>
|
83
|
+
<p class="text-sm font-medium text-gray-500">No Database Changes</p>
|
84
|
+
<p class="text-xs text-gray-400">This session contains no tracked database operations.</p>
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
</template>
|
88
|
+
</div>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<!-- Tab Bar -->
|
2
|
+
<div class="tab-bar">
|
3
|
+
<div class="tab-item"
|
4
|
+
:class="{ active: activeTab === 'changes' }"
|
5
|
+
@click="setActiveTab('changes')">Changes</div>
|
6
|
+
<div class="tab-item"
|
7
|
+
:class="{ active: activeTab === 'summary' }"
|
8
|
+
@click="setActiveTab('summary')">Summary</div>
|
9
|
+
<div class="tab-item"
|
10
|
+
:class="{ active: activeTab === 'diagrams' }"
|
11
|
+
@click="setActiveTab('diagrams')">Diagrams</div>
|
12
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="h-full flex flex-col">
|
2
|
+
<%= render partial: 'session_header', locals: { session: @session } %>
|
3
|
+
|
4
|
+
<!-- Tab Bar with URL-based navigation -->
|
5
|
+
<div class="tab-bar">
|
6
|
+
<%= link_to changes_session_path(@session.id), class: "tab-item active" do %>
|
7
|
+
Changes
|
8
|
+
<% end %>
|
9
|
+
<%= link_to summary_session_path(@session.id), class: "tab-item" do %>
|
10
|
+
Summary
|
11
|
+
<% end %>
|
12
|
+
<%= link_to diagrams_session_path(@session.id), class: "tab-item" do %>
|
13
|
+
Diagrams
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<!-- Changes Content -->
|
18
|
+
<div class="flex-1 overflow-auto">
|
19
|
+
<%= render partial: 'changes_tab', locals: { tables_summary: @tables_summary } %>
|
20
|
+
</div>
|
21
|
+
</div>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<div class="flex flex-wrap items-center gap-4">
|
2
|
+
<div class="flex items-center gap-2">
|
3
|
+
<label for="table-filter" class="text-sm font-medium text-gray-700">Table:</label>
|
4
|
+
<select id="table-filter"
|
5
|
+
x-model="filters.table"
|
6
|
+
@change="applyFilters()"
|
7
|
+
class="compact-select">
|
8
|
+
<option value="">All Tables</option>
|
9
|
+
<% if defined?(tables_summary) && tables_summary %>
|
10
|
+
<% tables_summary.keys.each do |table_name| %>
|
11
|
+
<option value="<%= table_name %>" <%= 'selected' if filters && filters[:table] == table_name %>><%= table_name %></option>
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
14
|
+
</select>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div class="flex items-center gap-2">
|
18
|
+
<label for="operation-filter" class="text-sm font-medium text-gray-700">Operation:</label>
|
19
|
+
<select id="operation-filter"
|
20
|
+
x-model="filters.operation"
|
21
|
+
@change="applyFilters()"
|
22
|
+
class="compact-select">
|
23
|
+
<option value="">All Operations</option>
|
24
|
+
<option value="INSERT" <%= 'selected' if filters && filters[:operation] == 'INSERT' %>>INSERT</option>
|
25
|
+
<option value="UPDATE" <%= 'selected' if filters && filters[:operation] == 'UPDATE' %>>UPDATE</option>
|
26
|
+
<option value="DELETE" <%= 'selected' if filters && filters[:operation] == 'DELETE' %>>DELETE</option>
|
27
|
+
</select>
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<div class="flex items-center gap-2">
|
31
|
+
<button @click="clearFilters()"
|
32
|
+
class="text-sm text-gray-500 hover:text-gray-700 underline">
|
33
|
+
Clear Filters
|
34
|
+
</button>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<div class="ml-auto flex items-center gap-2" x-show="loading">
|
38
|
+
<svg class="animate-spin h-4 w-4 text-blue-500" fill="none" viewBox="0 0 24 24">
|
39
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
40
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
41
|
+
</svg>
|
42
|
+
<span class="text-sm text-gray-500">Loading...</span>
|
43
|
+
</div>
|
44
|
+
</div>
|