dbviewer 0.4.2 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbd62dc61d3960414f9f2496634d3241eadf3270c424006190d3cf2a6f909a15
4
- data.tar.gz: 83365d3965efc2b431b61c085ebc7b15d6e23564c785e584ede10f015efd143d
3
+ metadata.gz: b726fcc60badaa82692f22151b9656152810ecaa9a5c59e39e0a27e0c595add5
4
+ data.tar.gz: a88f8817f599d276ad814bce26fa2314bd79c7c4a5979123aa524457c7c0d534
5
5
  SHA512:
6
- metadata.gz: 6587f9a6cbe948f32bf45a36ba60c40ea06109e534c3fffdb78bb6a727e80c6676b7cb8027df1ff9876bb4eaab37688e565e10372f321277f377980667f53923
7
- data.tar.gz: f93fe7e8f7cb1721f65e17a01288fa17be0bd2fdee25b278f157cfe56a85e9fe6a943cd8d5f58bd8a1a5cc604fb5e5b84b541d2a76e0f1663e86a248ff6ef3d9
6
+ metadata.gz: c0a0cfc7172bf1ea0e3146bfeb3cb739e247a8412c0fd204828fdb0e2a4f261fdbd75ef070da765c1a6cde0d1ae0bd60b842346c71512a547c9d05dd592930ae
7
+ data.tar.gz: 5304c49470fe85760bf0e6aef851cb3c199963be104518c46dad24dac6e22eaae597802786c339b9a779a810a8c849f9c1b558f73672abbb7f432429bbd9c519
@@ -1,9 +1,97 @@
1
1
  module Dbviewer
2
2
  class HomeController < ApplicationController
3
3
  def index
4
- @analytics = fetch_database_analytics
5
- if Dbviewer.configuration.enable_query_logging
6
- @recent_queries = Dbviewer::Logger.instance.recent_queries(limit: 10)
4
+ # Load page immediately without heavy data
5
+ # Data will be loaded asynchronously via AJAX
6
+ end
7
+
8
+ def analytics
9
+ # This method is deprecated but kept for backward compatibility
10
+ analytics_data = fetch_database_analytics
11
+ # Remove record data which will be served by the records endpoint
12
+ analytics_data.delete(:total_records)
13
+ analytics_data.delete(:largest_tables)
14
+ analytics_data.delete(:empty_tables)
15
+ analytics_data.delete(:avg_records_per_table)
16
+
17
+ respond_to do |format|
18
+ format.json { render json: analytics_data }
19
+ end
20
+ end
21
+
22
+ def tables_count
23
+ tables = fetch_tables_with_stats(include_record_counts: false)
24
+
25
+ respond_to do |format|
26
+ format.json { render json: { total_tables: tables.size } }
27
+ end
28
+ end
29
+
30
+ def relationships_count
31
+ begin
32
+ tables = fetch_tables_with_stats(include_record_counts: false)
33
+ total_relationships = 0
34
+
35
+ tables.each do |table|
36
+ metadata = fetch_table_metadata(table[:name])
37
+ total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
38
+ end
39
+
40
+ respond_to do |format|
41
+ format.json { render json: { total_relationships: total_relationships } }
42
+ end
43
+ rescue => e
44
+ Rails.logger.error("Error calculating relationship count: #{e.message}")
45
+ respond_to do |format|
46
+ format.json { render json: { total_relationships: 0, error: e.message }, status: :internal_server_error }
47
+ end
48
+ end
49
+ end
50
+
51
+ def database_size
52
+ begin
53
+ size = calculate_schema_size
54
+
55
+ respond_to do |format|
56
+ format.json { render json: { schema_size: size } }
57
+ end
58
+ rescue => e
59
+ Rails.logger.error("Error calculating schema size: #{e.message}")
60
+ respond_to do |format|
61
+ format.json { render json: { schema_size: nil, error: e.message }, status: :internal_server_error }
62
+ end
63
+ end
64
+ end
65
+
66
+ def records
67
+ tables = fetch_tables_with_stats(include_record_counts: true)
68
+
69
+ records_data = {
70
+ total_records: tables.sum { |t| t[:record_count] },
71
+ largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
72
+ empty_tables: tables.select { |t| t[:record_count] == 0 },
73
+ avg_records_per_table: tables.any? ? (tables.sum { |t| t[:record_count] }.to_f / tables.size).round(1) : 0
74
+ }
75
+
76
+ respond_to do |format|
77
+ format.json { render json: records_data }
78
+ end
79
+ end
80
+
81
+ def recent_queries
82
+ @recent_queries = if Dbviewer.configuration.enable_query_logging
83
+ Dbviewer::Logger.instance.recent_queries(limit: 10)
84
+ else
85
+ []
86
+ end
87
+
88
+ respond_to do |format|
89
+ format.json do
90
+ render json: {
91
+ enabled: Dbviewer.configuration.enable_query_logging,
92
+ queries: @recent_queries
93
+ }
94
+ end
7
95
  end
8
96
  end
9
97
 
@@ -8,8 +8,8 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
- <div class="row g-3 mb-4">
12
- <div class="col-md-3">
11
+ <div class="row g-3 mb-4" id="analytics-cards">
12
+ <div class="col-md-4">
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">
15
15
  <div class="metric-icon me-3">
@@ -17,13 +17,18 @@
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>
24
29
  </div>
25
30
 
26
- <div class="col-md-3">
31
+ <div class="col-md-4">
27
32
  <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
28
33
  <div class="card-body d-flex align-items-center">
29
34
  <div class="metric-icon me-3">
@@ -31,28 +36,18 @@
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>
38
48
  </div>
39
49
 
40
- <div class="col-md-3">
41
- <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
42
- <div class="card-body d-flex align-items-center">
43
- <div class="metric-icon me-3">
44
- <i class="bi bi-link-45deg fs-4"></i>
45
- </div>
46
- <div class="text-start">
47
- <h5 class="mb-1">Relationships</h5>
48
- <h2 class="mb-0"><%= @analytics[:total_relationships] %></h2>
49
- <small class="text-muted d-block">Foreign Key Connections</small>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
-
55
- <div class="col-md-3">
50
+ <div class="col-md-4">
56
51
  <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
57
52
  <div class="card-body d-flex align-items-center">
58
53
  <div class="metric-icon me-3">
@@ -60,7 +55,12 @@
60
55
  </div>
61
56
  <div class="text-start">
62
57
  <h5 class="mb-1">Database Size</h5>
63
- <h2 class="mb-0"><%= number_to_human_size(@analytics[:schema_size]) %></h2>
58
+ <h2 class="mb-0">
59
+ <span class="skeleton-loader number-loader" id="size-loading">
60
+ &nbsp;&nbsp;&nbsp;
61
+ </span>
62
+ <span id="size-count" class="d-none">0</span>
63
+ </h2>
64
64
  </div>
65
65
  </div>
66
66
  </div>
@@ -73,35 +73,25 @@
73
73
  <div class="card-header">
74
74
  <h5 class="card-title mb-0">Largest Tables</h5>
75
75
  </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>
76
+ <div class="card-body p-0" id="largest-tables-container">
77
+ <div class="table-responsive">
78
+ <table class="table table-hover table-sm mb-0">
79
+ <thead>
80
+ <tr>
81
+ <th>Table Name</th>
82
+ <th class="text-end">Records</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody>
86
+ <% 10.times do %>
81
87
  <tr>
82
- <th>Table Name</th>
83
- <th class="text-end">Records</th>
88
+ <td><div class="skeleton-loader table-cell-loader"></div></td>
89
+ <td class="text-end"><div class="skeleton-loader records-loader"></div></td>
84
90
  </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 %>
91
+ <% end %>
92
+ </tbody>
93
+ </table>
94
+ </div>
105
95
  </div>
106
96
  </div>
107
97
  </div>
@@ -110,55 +100,379 @@
110
100
  <div class="card shadow-sm">
111
101
  <div class="card-header d-flex justify-content-between align-items-center">
112
102
  <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 %>
103
+ <div id="queries-view-all-link" class="d-none">
104
+ <!-- Link will be added dynamically if query logging is enabled -->
105
+ </div>
116
106
  </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>
107
+ <div class="card-body p-0" id="recent-queries-container">
108
+ <div class="table-responsive">
109
+ <table class="table table-hover table-sm mb-0">
110
+ <thead>
111
+ <tr>
112
+ <th>Query</th>
113
+ <th class="text-end" style="width: 120px">Duration</th>
114
+ <th class="text-end" style="width: 180px">Time</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ <% 5.times do %>
124
119
  <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>
120
+ <td><div class="skeleton-loader query-cell-loader"></div></td>
121
+ <td class="text-end"><div class="skeleton-loader duration-cell-loader"></div></td>
122
+ <td class="text-end"><div class="skeleton-loader time-cell-loader"></div></td>
128
123
  </tr>
129
- </thead>
130
- <tbody>
131
- <% @recent_queries.each do |query| %>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <script>
135
+ document.addEventListener('DOMContentLoaded', function() {
136
+ // Helper function to format numbers with commas
137
+ function numberWithDelimiter(number) {
138
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
139
+ }
140
+
141
+ // Helper function to format file sizes
142
+ function numberToHumanSize(bytes) {
143
+ if (bytes === null || bytes === undefined) return 'N/A';
144
+ if (bytes === 0) return '0 Bytes';
145
+
146
+ const k = 1024;
147
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
148
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
149
+
150
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
151
+ }
152
+
153
+ // Function to update analytics cards
154
+ function updateTablesCount(data) {
155
+ document.getElementById('tables-loading').classList.add('d-none');
156
+ document.getElementById('tables-count').classList.remove('d-none');
157
+ document.getElementById('tables-count').textContent = data.total_tables || 0;
158
+ }
159
+
160
+ function updateRelationshipsCount(data) {
161
+ document.getElementById('relationships-loading').classList.add('d-none');
162
+ document.getElementById('relationships-count').classList.remove('d-none');
163
+ document.getElementById('relationships-count').textContent = data.total_relationships || 0;
164
+ }
165
+
166
+ function updateDatabaseSize(data) {
167
+ document.getElementById('size-loading').classList.add('d-none');
168
+ document.getElementById('size-count').classList.remove('d-none');
169
+ document.getElementById('size-count').textContent = numberToHumanSize(data.schema_size);
170
+ }
171
+
172
+ function updateRecordsData(recordsData) {
173
+ // Update records count
174
+ document.getElementById('records-loading').classList.add('d-none');
175
+ document.getElementById('records-count').classList.remove('d-none');
176
+ document.getElementById('records-count').textContent = numberWithDelimiter(recordsData.total_records || 0);
177
+
178
+ // Update largest tables
179
+ updateLargestTables(recordsData);
180
+ }
181
+
182
+ // Function to update largest tables
183
+ function updateLargestTables(data) {
184
+ const container = document.getElementById('largest-tables-container');
185
+
186
+ if (data.largest_tables && data.largest_tables.length > 0) {
187
+ const tableHtml = `
188
+ <div class="table-responsive">
189
+ <table class="table table-sm table-hover">
190
+ <thead>
191
+ <tr>
192
+ <th>Table Name</th>
193
+ <th class="text-end">Records</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ ${data.largest_tables.map(table => `
198
+ <tr>
199
+ <td>
200
+ <a href="${window.location.origin}${window.location.pathname.replace(/\/$/, '')}/tables/${table.name}">
201
+ ${table.name}
202
+ </a>
203
+ </td>
204
+ <td class="text-end">${numberWithDelimiter(table.record_count)}</td>
205
+ </tr>
206
+ `).join('')}
207
+ </tbody>
208
+ </table>
209
+ </div>
210
+ `;
211
+ container.innerHTML = tableHtml;
212
+ } else {
213
+ container.innerHTML = `
214
+ <div class="text-center my-4 empty-data-message">
215
+ <p>No table data available</p>
216
+ </div>
217
+ `;
218
+ }
219
+ }
220
+
221
+ // Function to update recent queries
222
+ function updateRecentQueries(data) {
223
+ const container = document.getElementById('recent-queries-container');
224
+ const linkContainer = document.getElementById('queries-view-all-link');
225
+
226
+ if (data.enabled) {
227
+ // Show "View All Logs" link if query logging is enabled
228
+ linkContainer.innerHTML = `
229
+ <a href="${window.location.origin}${window.location.pathname.replace(/\/$/, '')}/logs" class="btn btn-sm btn-primary">View All Logs</a>
230
+ `;
231
+ linkContainer.classList.remove('d-none');
232
+
233
+ if (data.queries && data.queries.length > 0) {
234
+ const tableHtml = `
235
+ <div class="table-responsive">
236
+ <table class="table table-sm table-hover mb-0">
237
+ <thead>
238
+ <tr>
239
+ <th>Query</th>
240
+ <th class="text-end" style="width: 120px">Duration</th>
241
+ <th class="text-end" style="width: 180px">Time</th>
242
+ </tr>
243
+ </thead>
244
+ <tbody>
245
+ ${data.queries.map(query => {
246
+ const duration = query.duration_ms;
247
+ const durationClass = duration > 100 ? 'query-duration-slow' : 'query-duration';
248
+ const timestamp = new Date(query.timestamp);
249
+ const timeString = timestamp.toLocaleTimeString();
250
+
251
+ return `
132
252
  <tr>
133
253
  <td class="text-truncate" style="max-width: 500px;">
134
- <code class="sql-query-code"><%= query[:sql] %></code>
254
+ <code class="sql-query-code">${query.sql}</code>
135
255
  </td>
136
256
  <td class="text-end">
137
- <span class="<%= query[:duration_ms] > 100 ? 'query-duration-slow' : 'query-duration' %>">
138
- <%= query[:duration_ms] %> ms
257
+ <span class="${durationClass}">
258
+ ${duration} ms
139
259
  </span>
140
260
  </td>
141
261
  <td class="text-end query-timestamp">
142
- <small><%= query[:timestamp].strftime("%H:%M:%S") %></small>
262
+ <small>${timeString}</small>
143
263
  </td>
144
264
  </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 %>
265
+ `;
266
+ }).join('')}
267
+ </tbody>
268
+ </table>
269
+ </div>
270
+ `;
271
+ container.innerHTML = tableHtml;
272
+ } else {
273
+ container.innerHTML = `
274
+ <div class="text-center my-4 empty-data-message">
275
+ <p>No queries recorded yet</p>
276
+ </div>
277
+ `;
278
+ }
279
+ } else {
280
+ container.innerHTML = `
281
+ <div class="text-center my-4 empty-data-message">
282
+ <p>Query logging is disabled</p>
283
+ <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
160
284
  </div>
285
+ `;
286
+ }
287
+ }
288
+
289
+ // Function to show error state
290
+ function showError(containerId, message) {
291
+ const container = document.getElementById(containerId);
292
+ container.innerHTML = `
293
+ <div class="text-center my-4 text-danger">
294
+ <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
295
+ <p>Error loading data</p>
296
+ <small>${message}</small>
161
297
  </div>
162
- </div>
163
- </div>
164
- </div>
298
+ `;
299
+ }
300
+
301
+ // Load tables count data
302
+ fetch('<%= api_tables_path %>', {
303
+ headers: {
304
+ 'Accept': 'application/json',
305
+ 'X-Requested-With': 'XMLHttpRequest'
306
+ }
307
+ })
308
+ .then(response => {
309
+ if (!response.ok) {
310
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
311
+ }
312
+ return response.json();
313
+ })
314
+ .then(data => {
315
+ updateTablesCount(data);
316
+ })
317
+ .catch(error => {
318
+ console.error('Error loading tables count:', error);
319
+ const loading = document.getElementById('tables-loading');
320
+ const count = document.getElementById('tables-count');
321
+ loading.classList.add('d-none');
322
+ count.classList.remove('d-none');
323
+ count.innerHTML = '<span class="text-danger">Error</span>';
324
+ });
325
+
326
+ // Load database size data
327
+ fetch('<%= api_database_size_path %>', {
328
+ headers: {
329
+ 'Accept': 'application/json',
330
+ 'X-Requested-With': 'XMLHttpRequest'
331
+ }
332
+ })
333
+ .then(response => {
334
+ if (!response.ok) {
335
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
336
+ }
337
+ return response.json();
338
+ })
339
+ .then(data => {
340
+ updateDatabaseSize(data);
341
+ })
342
+ .catch(error => {
343
+ console.error('Error loading database size:', error);
344
+ const loading = document.getElementById('size-loading');
345
+ const count = document.getElementById('size-count');
346
+ loading.classList.add('d-none');
347
+ count.classList.remove('d-none');
348
+ count.innerHTML = '<span class="text-danger">Error</span>';
349
+ });
350
+
351
+ // Load records data separately
352
+ fetch('<%= api_records_path %>', {
353
+ headers: {
354
+ 'Accept': 'application/json',
355
+ 'X-Requested-With': 'XMLHttpRequest'
356
+ }
357
+ })
358
+ .then(response => {
359
+ if (!response.ok) {
360
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
361
+ }
362
+ return response.json();
363
+ })
364
+ .then(recordsData => {
365
+ updateRecordsData(recordsData);
366
+ })
367
+ .catch(error => {
368
+ console.error('Error loading records data:', error);
369
+ // Update records-related UI with error state
370
+ const recordsLoading = document.getElementById('records-loading');
371
+ const recordsCount = document.getElementById('records-count');
372
+ recordsLoading.classList.add('d-none');
373
+ recordsCount.classList.remove('d-none');
374
+ recordsCount.innerHTML = '<span class="text-danger">Error</span>';
375
+
376
+ showError('largest-tables-container', error.message);
377
+ });
378
+
379
+ // Load recent queries data
380
+ fetch('<%= api_recent_queries_path %>', {
381
+ headers: {
382
+ 'Accept': 'application/json',
383
+ 'X-Requested-With': 'XMLHttpRequest'
384
+ }
385
+ })
386
+ .then(response => {
387
+ if (!response.ok) {
388
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
389
+ }
390
+ return response.json();
391
+ })
392
+ .then(data => {
393
+ updateRecentQueries(data);
394
+ })
395
+ .catch(error => {
396
+ console.error('Error loading recent queries:', error);
397
+ showError('recent-queries-container', error.message);
398
+ });
399
+ });
400
+ </script>
401
+
402
+ <style>
403
+ .sql-query-code {
404
+ font-family: 'Courier New', Courier, monospace;
405
+ font-size: 0.85rem;
406
+ background-color: rgba(0, 0, 0, 0.05);
407
+ padding: 2px 4px;
408
+ border-radius: 3px;
409
+ }
410
+
411
+ .query-duration {
412
+ color: #28a745;
413
+ font-weight: 500;
414
+ }
415
+
416
+ .query-duration-slow {
417
+ color: #dc3545;
418
+ font-weight: 600;
419
+ }
420
+
421
+ .query-timestamp {
422
+ color: #6c757d;
423
+ }
424
+
425
+ .empty-data-message {
426
+ color: #6c757d;
427
+ }
428
+
429
+ /* Loading animations */
430
+ .spinner-border-sm {
431
+ width: 1rem;
432
+ height: 1rem;
433
+ }
434
+
435
+ /* Skeleton loader styles */
436
+ .skeleton-loader {
437
+ display: inline-block;
438
+ height: 1.2em;
439
+ width: 100%;
440
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%);
441
+ background-size: 400% 100%;
442
+ animation: skeleton-loading 1.2s ease-in-out infinite;
443
+ border-radius: 4px;
444
+ }
445
+ .number-loader {
446
+ width: 2.5em;
447
+ height: 1.5em;
448
+ margin-bottom: 0.2em;
449
+ }
450
+ .table-cell-loader {
451
+ width: 6em;
452
+ height: 1.2em;
453
+ }
454
+ .records-loader {
455
+ width: 3em;
456
+ height: 1.2em;
457
+ }
458
+ .query-cell-loader {
459
+ width: 12em;
460
+ height: 1.2em;
461
+ }
462
+ .duration-cell-loader {
463
+ width: 4em;
464
+ height: 1.2em;
465
+ }
466
+ .time-cell-loader {
467
+ width: 7em;
468
+ height: 1.2em;
469
+ }
470
+ @keyframes skeleton-loading {
471
+ 0% {
472
+ background-position: 100% 50%;
473
+ }
474
+ 100% {
475
+ background-position: 0 50%;
476
+ }
477
+ }
478
+ </style>
data/config/routes.rb CHANGED
@@ -16,8 +16,16 @@ 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
21
 
22
+ # Analytics API endpoints
23
+ get "api/analytics", to: "home#analytics" # Legacy/combined endpoint
24
+ get "api/records", to: "home#records"
25
+ get "api/tables", to: "home#tables_count"
26
+ get "api/relationships", to: "home#relationships_count"
27
+ get "api/database_size", to: "home#database_size"
28
+ get "api/recent_queries", to: "home#recent_queries"
29
+
22
30
  root to: "home#index"
23
31
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.4"
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh