dbviewer 0.4.2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbd62dc61d3960414f9f2496634d3241eadf3270c424006190d3cf2a6f909a15
4
- data.tar.gz: 83365d3965efc2b431b61c085ebc7b15d6e23564c785e584ede10f015efd143d
3
+ metadata.gz: 7cc641a4845b5267ef95a4fd7b5c368a9f4145dd0f60322eb753e8b4a270414a
4
+ data.tar.gz: 37ee52e24a3578c61f939b14d74cea8d125e4f737ed98abafa109c9a3e71e627
5
5
  SHA512:
6
- metadata.gz: 6587f9a6cbe948f32bf45a36ba60c40ea06109e534c3fffdb78bb6a727e80c6676b7cb8027df1ff9876bb4eaab37688e565e10372f321277f377980667f53923
7
- data.tar.gz: f93fe7e8f7cb1721f65e17a01288fa17be0bd2fdee25b278f157cfe56a85e9fe6a943cd8d5f58bd8a1a5cc604fb5e5b84b541d2a76e0f1663e86a248ff6ef3d9
6
+ metadata.gz: 4253de79918ecd100eeb6f8e17b4e64346060295defd167cb709227db1eec0cd6716e230c1c56ae251dac5361643eaf8abd529fc566a31cad27edfdcc72ad424
7
+ data.tar.gz: af7b679d9ec6116487322ce9309ca4390e02e22fdaf8d873dff8c71f83ab8142a8bd7d08f34e9fd127d2a233c963b9903bf82bfd75972f8f6452f67c5994ab1e
@@ -3,7 +3,7 @@ module Dbviewer
3
3
  include Dbviewer::DatabaseOperations
4
4
  include Dbviewer::ErrorHandling
5
5
 
6
- before_action :authenticate_with_basic_auth
6
+ # before_action :authenticate_with_basic_auth
7
7
  before_action :set_tables
8
8
 
9
9
  private
@@ -1,9 +1,32 @@
1
1
  module Dbviewer
2
2
  class HomeController < ApplicationController
3
3
  def index
4
+ # Load page immediately without heavy data
5
+ # Data will be loaded asynchronously via AJAX
6
+ end
7
+
8
+ def analytics
4
9
  @analytics = fetch_database_analytics
5
- if Dbviewer.configuration.enable_query_logging
6
- @recent_queries = Dbviewer::Logger.instance.recent_queries(limit: 10)
10
+
11
+ respond_to do |format|
12
+ format.json { render json: @analytics }
13
+ end
14
+ end
15
+
16
+ def recent_queries
17
+ @recent_queries = if Dbviewer.configuration.enable_query_logging
18
+ Dbviewer::Logger.instance.recent_queries(limit: 10)
19
+ else
20
+ []
21
+ end
22
+
23
+ respond_to do |format|
24
+ format.json do
25
+ render json: {
26
+ enabled: Dbviewer.configuration.enable_query_logging,
27
+ queries: @recent_queries
28
+ }
29
+ end
7
30
  end
8
31
  end
9
32
 
@@ -8,7 +8,7 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
- <div class="row g-3 mb-4">
11
+ <div class="row g-3 mb-4" id="analytics-cards">
12
12
  <div class="col-md-3">
13
13
  <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
14
14
  <div class="card-body d-flex align-items-center">
@@ -17,7 +17,12 @@
17
17
  </div>
18
18
  <div class="text-start">
19
19
  <h5 class="mb-1">Tables</h5>
20
- <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>
21
26
  </div>
22
27
  </div>
23
28
  </div>
@@ -31,7 +36,12 @@
31
36
  </div>
32
37
  <div class="text-start">
33
38
  <h5 class="mb-1">Records</h5>
34
- <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>
35
45
  </div>
36
46
  </div>
37
47
  </div>
@@ -45,7 +55,12 @@
45
55
  </div>
46
56
  <div class="text-start">
47
57
  <h5 class="mb-1">Relationships</h5>
48
- <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>
49
64
  <small class="text-muted d-block">Foreign Key Connections</small>
50
65
  </div>
51
66
  </div>
@@ -60,7 +75,12 @@
60
75
  </div>
61
76
  <div class="text-start">
62
77
  <h5 class="mb-1">Database Size</h5>
63
- <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>
64
84
  </div>
65
85
  </div>
66
86
  </div>
@@ -73,35 +93,25 @@
73
93
  <div class="card-header">
74
94
  <h5 class="card-title mb-0">Largest Tables</h5>
75
95
  </div>
76
- <div class="card-body p-0">
77
- <% if @analytics[:largest_tables].any? %>
78
- <div class="table-responsive">
79
- <table class="table table-sm table-hover">
80
- <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 %>
81
107
  <tr>
82
- <th>Table Name</th>
83
- <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>
84
110
  </tr>
85
- </thead>
86
- <tbody>
87
- <% @analytics[:largest_tables].each do |table| %>
88
- <tr>
89
- <td>
90
- <a href="<%= dbviewer.table_path(table[:name]) %>">
91
- <%= table[:name] %>
92
- </a>
93
- </td>
94
- <td class="text-end"><%= number_with_delimiter(table[:record_count]) %></td>
95
- </tr>
96
- <% end %>
97
- </tbody>
98
- </table>
99
- </div>
100
- <% else %>
101
- <div class="text-center my-4 empty-data-message">
102
- <p>No table data available</p>
103
- </div>
104
- <% end %>
111
+ <% end %>
112
+ </tbody>
113
+ </table>
114
+ </div>
105
115
  </div>
106
116
  </div>
107
117
  </div>
@@ -110,55 +120,326 @@
110
120
  <div class="card shadow-sm">
111
121
  <div class="card-header d-flex justify-content-between align-items-center">
112
122
  <h5 class="card-title mb-0">Recent SQL Queries</h5>
113
- <% if Dbviewer.configuration.enable_query_logging %>
114
- <a href="<%= dbviewer.logs_path %>" class="btn btn-sm btn-primary">View All Logs</a>
115
- <% end %>
123
+ <div id="queries-view-all-link" class="d-none">
124
+ <!-- Link will be added dynamically if query logging is enabled -->
125
+ </div>
116
126
  </div>
117
- <div class="card-body p-0">
118
- <% if Dbviewer.configuration.enable_query_logging %>
119
- <% if @recent_queries.any? %>
120
- <div class="table-responsive">
121
- <table class="table table-sm table-hover mb-0">
122
-
123
- <thead>
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 %>
124
139
  <tr>
125
- <th>Query</th>
126
- <th class="text-end" style="width: 120px">Duration</th>
127
- <th class="text-end" style="width: 180px">Time</th>
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>
128
143
  </tr>
129
- </thead>
130
- <tbody>
131
- <% @recent_queries.each do |query| %>
144
+ <% end %>
145
+ </tbody>
146
+ </table>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <script>
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
+ }
160
+
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
+ `;
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');
239
+
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');
246
+
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 `
132
266
  <tr>
133
267
  <td class="text-truncate" style="max-width: 500px;">
134
- <code class="sql-query-code"><%= query[:sql] %></code>
268
+ <code class="sql-query-code">${query.sql}</code>
135
269
  </td>
136
270
  <td class="text-end">
137
- <span class="<%= query[:duration_ms] > 100 ? 'query-duration-slow' : 'query-duration' %>">
138
- <%= query[:duration_ms] %> ms
271
+ <span class="${durationClass}">
272
+ ${duration} ms
139
273
  </span>
140
274
  </td>
141
275
  <td class="text-end query-timestamp">
142
- <small><%= query[:timestamp].strftime("%H:%M:%S") %></small>
276
+ <small>${timeString}</small>
143
277
  </td>
144
278
  </tr>
145
- <% end %>
146
- </tbody>
147
- </table>
148
- </div>
149
- <% else %>
150
- <div class="text-center my-4 empty-data-message">
151
- <p>No queries recorded yet</p>
152
- </div>
153
- <% end %>
154
- <% else %>
155
- <div class="text-center my-4 empty-data-message">
156
- <p>Query logging is disabled</p>
157
- <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
158
- </div>
159
- <% end %>
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>
160
298
  </div>
299
+ `;
300
+ }
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>
161
311
  </div>
162
- </div>
163
- </div>
164
- </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>';
341
+ });
342
+
343
+ showError('largest-tables-container', error.message);
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
+ });
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>
data/config/routes.rb CHANGED
@@ -16,8 +16,10 @@ Dbviewer::Engine.routes.draw do
16
16
  end
17
17
  end
18
18
 
19
- # Homepage
19
+ # Homepage and API endpoints
20
20
  get "dashboard", to: "home#index", as: :dashboard
21
+ get "api/analytics", to: "home#analytics"
22
+ get "api/recent_queries", to: "home#recent_queries"
21
23
 
22
24
  root to: "home#index"
23
25
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh