dbviewer 0.4.1 → 0.4.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.
@@ -1,9 +1,5 @@
1
1
  <% content_for :title, "Entity Relationship Diagram" %>
2
2
 
3
- <% content_for :sidebar do %>
4
- <%= render 'dbviewer/shared/sidebar' %>
5
- <% end %>
6
-
7
3
  <div class="container-fluid h-100">
8
4
  <div class="row h-100">
9
5
  <div class="col-md-12 p-0">
@@ -1,8 +1,3 @@
1
- <%# sidebar content %>
2
- <% content_for :sidebar do %>
3
- <%= render 'dbviewer/shared/sidebar' %>
4
- <% end %>
5
-
6
1
  <div class="container-fluid px-0">
7
2
  <div class="row mb-3">
8
3
  <div class="col">
@@ -13,7 +8,7 @@
13
8
  </div>
14
9
  </div>
15
10
 
16
- <div class="row g-3 mb-4">
11
+ <div class="row g-3 mb-4" id="analytics-cards">
17
12
  <div class="col-md-3">
18
13
  <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
19
14
  <div class="card-body d-flex align-items-center">
@@ -22,7 +17,12 @@
22
17
  </div>
23
18
  <div class="text-start">
24
19
  <h5 class="mb-1">Tables</h5>
25
- <h2 class="mb-0"><%= @analytics[:total_tables] %></h2>
20
+ <h2 class="mb-0">
21
+ <span class="skeleton-loader number-loader" id="tables-loading">
22
+ &nbsp;&nbsp;
23
+ </span>
24
+ <span id="tables-count" class="d-none">0</span>
25
+ </h2>
26
26
  </div>
27
27
  </div>
28
28
  </div>
@@ -36,7 +36,12 @@
36
36
  </div>
37
37
  <div class="text-start">
38
38
  <h5 class="mb-1">Records</h5>
39
- <h2 class="mb-0"><%= number_with_delimiter(@analytics[:total_records]) %></h2>
39
+ <h2 class="mb-0">
40
+ <span class="skeleton-loader number-loader" id="records-loading">
41
+ &nbsp;&nbsp;&nbsp;&nbsp;
42
+ </span>
43
+ <span id="records-count" class="d-none">0</span>
44
+ </h2>
40
45
  </div>
41
46
  </div>
42
47
  </div>
@@ -50,7 +55,12 @@
50
55
  </div>
51
56
  <div class="text-start">
52
57
  <h5 class="mb-1">Relationships</h5>
53
- <h2 class="mb-0"><%= @analytics[:total_relationships] %></h2>
58
+ <h2 class="mb-0">
59
+ <span class="skeleton-loader number-loader" id="relationships-loading">
60
+ &nbsp;&nbsp;
61
+ </span>
62
+ <span id="relationships-count" class="d-none">0</span>
63
+ </h2>
54
64
  <small class="text-muted d-block">Foreign Key Connections</small>
55
65
  </div>
56
66
  </div>
@@ -65,7 +75,12 @@
65
75
  </div>
66
76
  <div class="text-start">
67
77
  <h5 class="mb-1">Database Size</h5>
68
- <h2 class="mb-0"><%= number_to_human_size(@analytics[:schema_size]) %></h2>
78
+ <h2 class="mb-0">
79
+ <span class="skeleton-loader number-loader" id="size-loading">
80
+ &nbsp;&nbsp;&nbsp;
81
+ </span>
82
+ <span id="size-count" class="d-none">0</span>
83
+ </h2>
69
84
  </div>
70
85
  </div>
71
86
  </div>
@@ -78,99 +93,58 @@
78
93
  <div class="card-header">
79
94
  <h5 class="card-title mb-0">Largest Tables</h5>
80
95
  </div>
81
- <div class="card-body">
82
- <% if @analytics[:largest_tables].any? %>
83
- <div class="table-responsive">
84
- <table class="table table-sm table-hover">
85
- <thead>
96
+ <div class="card-body p-0" id="largest-tables-container">
97
+ <div class="table-responsive">
98
+ <table class="table table-hover table-sm mb-0">
99
+ <thead>
100
+ <tr>
101
+ <th>Table Name</th>
102
+ <th class="text-end">Records</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>
106
+ <% 10.times do %>
86
107
  <tr>
87
- <th>Table Name</th>
88
- <th class="text-end">Records</th>
108
+ <td><div class="skeleton-loader table-cell-loader"></div></td>
109
+ <td class="text-end"><div class="skeleton-loader records-loader"></div></td>
89
110
  </tr>
90
- </thead>
91
- <tbody>
92
- <% @analytics[:largest_tables].each do |table| %>
93
- <tr>
94
- <td>
95
- <a href="<%= dbviewer.table_path(table[:name]) %>">
96
- <%= table[:name] %>
97
- </a>
98
- </td>
99
- <td class="text-end"><%= number_with_delimiter(table[:record_count]) %></td>
100
- </tr>
101
- <% end %>
102
- </tbody>
103
- </table>
104
- </div>
105
- <% else %>
106
- <div class="text-center my-4 empty-data-message">
107
- <p>No table data available</p>
108
- </div>
109
- <% end %>
111
+ <% end %>
112
+ </tbody>
113
+ </table>
114
+ </div>
110
115
  </div>
111
116
  </div>
112
117
  </div>
113
118
 
114
- <div class="col-6 mb-4">
119
+ <div class="col-md-6 mb-4">
115
120
  <div class="card shadow-sm">
116
121
  <div class="card-header d-flex justify-content-between align-items-center">
117
122
  <h5 class="card-title mb-0">Recent SQL Queries</h5>
118
- <% if Dbviewer.configuration.enable_query_logging %>
119
- <a href="<%= dbviewer.logs_path %>" class="btn btn-sm btn-primary">View All Logs</a>
120
- <% end %>
123
+ <div id="queries-view-all-link" class="d-none">
124
+ <!-- Link will be added dynamically if query logging is enabled -->
125
+ </div>
121
126
  </div>
122
- <div class="card-body p-0">
123
- <% begin %>
124
- <% require_dependency "dbviewer/logger" %>
125
- <% if Dbviewer.configuration.enable_query_logging %>
126
- <% queries = Dbviewer::Logger.instance.recent_queries(limit: 10) %>
127
-
128
- <% if queries.any? %>
129
- <div class="table-responsive">
130
- <table class="table table-sm table-hover mb-0">
131
-
132
- <thead>
133
- <tr>
134
- <th>Query</th>
135
- <th class="text-end" style="width: 120px">Duration</th>
136
- <th class="text-end" style="width: 180px">Time</th>
137
- </tr>
138
- </thead>
139
- <tbody>
140
- <% queries.each do |query| %>
141
- <tr>
142
- <td class="text-truncate" style="max-width: 500px;">
143
- <code class="sql-query-code"><%= query[:sql] %></code>
144
- </td>
145
- <td class="text-end">
146
- <span class="<%= query[:duration_ms] > 100 ? 'query-duration-slow' : 'query-duration' %>">
147
- <%= query[:duration_ms] %> ms
148
- </span>
149
- </td>
150
- <td class="text-end query-timestamp">
151
- <small><%= query[:timestamp].strftime("%H:%M:%S") %></small>
152
- </td>
153
- </tr>
154
- <% end %>
155
- </tbody>
156
- </table>
157
- </div>
158
- <% else %>
159
- <div class="text-center my-4 empty-data-message">
160
- <p>No queries recorded yet</p>
161
- </div>
162
- <% end %>
163
- <% else %>
164
- <div class="text-center my-4 empty-data-message">
165
- <p>Query logging is disabled</p>
166
- <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
167
- </div>
168
- <% end %>
169
- <% rescue => e %>
170
- <div class="text-center my-4 empty-data-message">
171
- <p>Error loading query logs: <%= e.message %></p>
172
- </div>
173
- <% end %>
127
+ <div class="card-body p-0" id="recent-queries-container">
128
+ <div class="table-responsive">
129
+ <table class="table table-hover table-sm mb-0">
130
+ <thead>
131
+ <tr>
132
+ <th>Query</th>
133
+ <th class="text-end" style="width: 120px">Duration</th>
134
+ <th class="text-end" style="width: 180px">Time</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ <% 5.times do %>
139
+ <tr>
140
+ <td><div class="skeleton-loader query-cell-loader"></div></td>
141
+ <td class="text-end"><div class="skeleton-loader duration-cell-loader"></div></td>
142
+ <td class="text-end"><div class="skeleton-loader time-cell-loader"></div></td>
143
+ </tr>
144
+ <% end %>
145
+ </tbody>
146
+ </table>
147
+ </div>
174
148
  </div>
175
149
  </div>
176
150
  </div>
@@ -178,48 +152,294 @@
178
152
  </div>
179
153
 
180
154
  <script>
181
- document.addEventListener('DOMContentLoaded', function() {
182
- const searchInput = document.getElementById('tableSearch');
183
- const tablesList = document.getElementById('tablesList');
184
- const tables = tablesList ? tablesList.querySelectorAll('.list-group-item') : [];
155
+ document.addEventListener('DOMContentLoaded', function() {
156
+ // Helper function to format numbers with commas
157
+ function numberWithDelimiter(number) {
158
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
159
+ }
185
160
 
186
- if (searchInput) {
187
- searchInput.addEventListener('keyup', function() {
188
- const query = this.value.trim().toLowerCase();
189
-
190
- Array.from(tables).forEach(function(table) {
191
- const tableName = table.textContent.trim().toLowerCase();
192
- if (tableName.includes(query)) {
193
- table.style.display = '';
194
- } else {
195
- table.style.display = 'none';
196
- }
197
- });
198
- });
161
+ // Helper function to format file sizes
162
+ function numberToHumanSize(bytes) {
163
+ if (bytes === null || bytes === undefined) return 'N/A';
164
+ if (bytes === 0) return '0 Bytes';
165
+
166
+ const k = 1024;
167
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
168
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
169
+
170
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
171
+ }
172
+
173
+ // Function to update analytics cards
174
+ function updateAnalyticsCards(analytics) {
175
+ // Update tables count
176
+ document.getElementById('tables-loading').classList.add('d-none');
177
+ document.getElementById('tables-count').classList.remove('d-none');
178
+ document.getElementById('tables-count').textContent = analytics.total_tables || 0;
179
+
180
+ // Update records count
181
+ document.getElementById('records-loading').classList.add('d-none');
182
+ document.getElementById('records-count').classList.remove('d-none');
183
+ document.getElementById('records-count').textContent = numberWithDelimiter(analytics.total_records || 0);
184
+
185
+ // Update relationships count
186
+ document.getElementById('relationships-loading').classList.add('d-none');
187
+ document.getElementById('relationships-count').classList.remove('d-none');
188
+ document.getElementById('relationships-count').textContent = analytics.total_relationships || 0;
189
+
190
+ // Update database size
191
+ document.getElementById('size-loading').classList.add('d-none');
192
+ document.getElementById('size-count').classList.remove('d-none');
193
+ document.getElementById('size-count').textContent = numberToHumanSize(analytics.schema_size);
194
+ }
195
+
196
+ // Function to update largest tables
197
+ function updateLargestTables(analytics) {
198
+ const container = document.getElementById('largest-tables-container');
199
+
200
+ if (analytics.largest_tables && analytics.largest_tables.length > 0) {
201
+ const tableHtml = `
202
+ <div class="table-responsive">
203
+ <table class="table table-sm table-hover">
204
+ <thead>
205
+ <tr>
206
+ <th>Table Name</th>
207
+ <th class="text-end">Records</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>
211
+ ${analytics.largest_tables.map(table => `
212
+ <tr>
213
+ <td>
214
+ <a href="${window.location.origin}${window.location.pathname.replace(/\/$/, '')}/tables/${table.name}">
215
+ ${table.name}
216
+ </a>
217
+ </td>
218
+ <td class="text-end">${numberWithDelimiter(table.record_count)}</td>
219
+ </tr>
220
+ `).join('')}
221
+ </tbody>
222
+ </table>
223
+ </div>
224
+ `;
225
+ container.innerHTML = tableHtml;
226
+ } else {
227
+ container.innerHTML = `
228
+ <div class="text-center my-4 empty-data-message">
229
+ <p>No table data available</p>
230
+ </div>
231
+ `;
199
232
  }
233
+ }
234
+
235
+ // Function to update recent queries
236
+ function updateRecentQueries(data) {
237
+ const container = document.getElementById('recent-queries-container');
238
+ const linkContainer = document.getElementById('queries-view-all-link');
200
239
 
201
- // Update code blocks for the current theme
202
- function updateCodeBlockTheme() {
203
- const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
204
- const codeBlocks = document.querySelectorAll('code.sql-query-code');
240
+ if (data.enabled) {
241
+ // Show "View All Logs" link if query logging is enabled
242
+ linkContainer.innerHTML = `
243
+ <a href="${window.location.origin}${window.location.pathname.replace(/\/$/, '')}/logs" class="btn btn-sm btn-primary">View All Logs</a>
244
+ `;
245
+ linkContainer.classList.remove('d-none');
205
246
 
206
- codeBlocks.forEach(codeBlock => {
207
- if (isDarkMode) {
208
- codeBlock.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
209
- codeBlock.style.color = '#65cdff';
210
- } else {
211
- codeBlock.style.backgroundColor = 'rgba(0, 0, 0, 0.05)';
212
- codeBlock.style.color = '';
213
- }
214
- });
247
+ if (data.queries && data.queries.length > 0) {
248
+ const tableHtml = `
249
+ <div class="table-responsive">
250
+ <table class="table table-sm table-hover mb-0">
251
+ <thead>
252
+ <tr>
253
+ <th>Query</th>
254
+ <th class="text-end" style="width: 120px">Duration</th>
255
+ <th class="text-end" style="width: 180px">Time</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody>
259
+ ${data.queries.map(query => {
260
+ const duration = query.duration_ms;
261
+ const durationClass = duration > 100 ? 'query-duration-slow' : 'query-duration';
262
+ const timestamp = new Date(query.timestamp);
263
+ const timeString = timestamp.toLocaleTimeString();
264
+
265
+ return `
266
+ <tr>
267
+ <td class="text-truncate" style="max-width: 500px;">
268
+ <code class="sql-query-code">${query.sql}</code>
269
+ </td>
270
+ <td class="text-end">
271
+ <span class="${durationClass}">
272
+ ${duration} ms
273
+ </span>
274
+ </td>
275
+ <td class="text-end query-timestamp">
276
+ <small>${timeString}</small>
277
+ </td>
278
+ </tr>
279
+ `;
280
+ }).join('')}
281
+ </tbody>
282
+ </table>
283
+ </div>
284
+ `;
285
+ container.innerHTML = tableHtml;
286
+ } else {
287
+ container.innerHTML = `
288
+ <div class="text-center my-4 empty-data-message">
289
+ <p>No queries recorded yet</p>
290
+ </div>
291
+ `;
292
+ }
293
+ } else {
294
+ container.innerHTML = `
295
+ <div class="text-center my-4 empty-data-message">
296
+ <p>Query logging is disabled</p>
297
+ <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
298
+ </div>
299
+ `;
215
300
  }
216
-
217
- // Initial theme check
218
- updateCodeBlockTheme();
219
-
220
- // Listen for theme changes
221
- document.addEventListener('dbviewerThemeChanged', function(event) {
222
- updateCodeBlockTheme();
301
+ }
302
+
303
+ // Function to show error state
304
+ function showError(containerId, message) {
305
+ const container = document.getElementById(containerId);
306
+ container.innerHTML = `
307
+ <div class="text-center my-4 text-danger">
308
+ <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
309
+ <p>Error loading data</p>
310
+ <small>${message}</small>
311
+ </div>
312
+ `;
313
+ }
314
+
315
+ // Load analytics data
316
+ fetch('<%= api_analytics_path %>', {
317
+ headers: {
318
+ 'Accept': 'application/json',
319
+ 'X-Requested-With': 'XMLHttpRequest'
320
+ }
321
+ })
322
+ .then(response => {
323
+ if (!response.ok) {
324
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
325
+ }
326
+ return response.json();
327
+ })
328
+ .then(analytics => {
329
+ updateAnalyticsCards(analytics);
330
+ updateLargestTables(analytics);
331
+ })
332
+ .catch(error => {
333
+ console.error('Error loading analytics:', error);
334
+ // Update cards with error state
335
+ ['tables-loading', 'records-loading', 'relationships-loading', 'size-loading'].forEach(id => {
336
+ const loading = document.getElementById(id);
337
+ const count = document.getElementById(id.replace('-loading', '-count'));
338
+ loading.classList.add('d-none');
339
+ count.classList.remove('d-none');
340
+ count.innerHTML = '<span class="text-danger">Error</span>';
223
341
  });
342
+
343
+ showError('largest-tables-container', error.message);
224
344
  });
345
+
346
+ // Load recent queries data
347
+ fetch('<%= api_recent_queries_path %>', {
348
+ headers: {
349
+ 'Accept': 'application/json',
350
+ 'X-Requested-With': 'XMLHttpRequest'
351
+ }
352
+ })
353
+ .then(response => {
354
+ if (!response.ok) {
355
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
356
+ }
357
+ return response.json();
358
+ })
359
+ .then(data => {
360
+ updateRecentQueries(data);
361
+ })
362
+ .catch(error => {
363
+ console.error('Error loading recent queries:', error);
364
+ showError('recent-queries-container', error.message);
365
+ });
366
+ });
225
367
  </script>
368
+
369
+ <style>
370
+ .sql-query-code {
371
+ font-family: 'Courier New', Courier, monospace;
372
+ font-size: 0.85rem;
373
+ background-color: rgba(0, 0, 0, 0.05);
374
+ padding: 2px 4px;
375
+ border-radius: 3px;
376
+ }
377
+
378
+ .query-duration {
379
+ color: #28a745;
380
+ font-weight: 500;
381
+ }
382
+
383
+ .query-duration-slow {
384
+ color: #dc3545;
385
+ font-weight: 600;
386
+ }
387
+
388
+ .query-timestamp {
389
+ color: #6c757d;
390
+ }
391
+
392
+ .empty-data-message {
393
+ color: #6c757d;
394
+ }
395
+
396
+ /* Loading animations */
397
+ .spinner-border-sm {
398
+ width: 1rem;
399
+ height: 1rem;
400
+ }
401
+
402
+ /* Skeleton loader styles */
403
+ .skeleton-loader {
404
+ display: inline-block;
405
+ height: 1.2em;
406
+ width: 100%;
407
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%);
408
+ background-size: 400% 100%;
409
+ animation: skeleton-loading 1.2s ease-in-out infinite;
410
+ border-radius: 4px;
411
+ }
412
+ .number-loader {
413
+ width: 2.5em;
414
+ height: 1.5em;
415
+ margin-bottom: 0.2em;
416
+ }
417
+ .table-cell-loader {
418
+ width: 6em;
419
+ height: 1.2em;
420
+ }
421
+ .records-loader {
422
+ width: 3em;
423
+ height: 1.2em;
424
+ }
425
+ .query-cell-loader {
426
+ width: 12em;
427
+ height: 1.2em;
428
+ }
429
+ .duration-cell-loader {
430
+ width: 4em;
431
+ height: 1.2em;
432
+ }
433
+ .time-cell-loader {
434
+ width: 7em;
435
+ height: 1.2em;
436
+ }
437
+ @keyframes skeleton-loading {
438
+ 0% {
439
+ background-position: 100% 50%;
440
+ }
441
+ 100% {
442
+ background-position: 0 50%;
443
+ }
444
+ }
445
+ </style>
@@ -1,9 +1,5 @@
1
1
  <% content_for :title, "SQL Query Logs" %>
2
2
 
3
- <% content_for :sidebar do %>
4
- <%= render 'dbviewer/shared/sidebar' %>
5
- <% end %>
6
-
7
3
  <div class="container-fluid">
8
4
  <div class="d-flex justify-content-between align-items-center mb-2">
9
5
  <h1>
@@ -2,10 +2,6 @@
2
2
  Database Tables
3
3
  <% end %>
4
4
 
5
- <% content_for :sidebar do %>
6
- <%= render 'dbviewer/shared/sidebar' %>
7
- <% end %>
8
-
9
5
  <div class="d-flex justify-content-between align-items-center mb-4">
10
6
  <h1>Database Tables</h1>
11
7
  <div>
@@ -181,10 +181,6 @@
181
181
 
182
182
  <% content_for :sidebar_active do %>active<% end %>
183
183
 
184
- <% content_for :sidebar do %>
185
- <%= render 'dbviewer/shared/sidebar' %>
186
- <% end %>
187
-
188
184
  <div class="d-flex justify-content-between align-items-center mb-4">
189
185
  <h1>Query: <%= @table_name %></h1>
190
186
  <div>