rails_error_dashboard 0.2.0 → 0.2.1

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: 7ded53b39ba42ddedec2dea52df11dc8e69c77376bb8204891e6c4f9d3fc165b
4
- data.tar.gz: c0d80a90c1a2f9dff08f82d4bd40dcc2e1f0336ffe443cc5c96420d059b42a64
3
+ metadata.gz: dfe05c8e6948bea62c2d86897a52a5e761ab2ab129cb90c62bc57a670c9f3f6f
4
+ data.tar.gz: 91ac00e2be2b1a224d1e3a9783c2e7ca458ae291b8bf84aae797bbefb8ce17cd
5
5
  SHA512:
6
- metadata.gz: fcd8b1b91f2d6a9563ff137b8ceeb50fd82cbda3738792680a3d08b418a204102be98b3527dce1da6e1495a6a0c0647cc85e0d3a073de53e0657fdb8a4d4b490
7
- data.tar.gz: 0f374fc4577b29cc3165ed5ad756243298d1d5c691a9f0e865c974411a9eec3430b544f8abf176b47d8a1e816d7f26b36eff819a9fe483bbf13798aef801f9ae
6
+ metadata.gz: aaac80a450c66391b8ed83cd761def41db706cd7fe897141fde41e9823c53130d29164449c54a835f2c2e9dce374aba5734c52ef88137ddf899d7e0690cdea71
7
+ data.tar.gz: 10b8d21fd9b386be8a33817f26b4154519439c4dd07472e9ed6bb4713b07f17dc07d700bd30aebe0b844437999ce74926be6ba4cd8f8cdcd3d87b1a7534c0ba8
data/README.md CHANGED
@@ -29,7 +29,7 @@ Experience the full dashboard with 480+ realistic Rails errors, LOTR-themed demo
29
29
  ---
30
30
 
31
31
  ### ⚠️ BETA SOFTWARE
32
- This Rails Engine is in beta and under active development. While functional and tested (1,800+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
32
+ This Rails Engine is in beta and under active development. While functional and tested (1,895+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
33
33
 
34
34
  **Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
35
35
 
@@ -950,6 +950,7 @@ Thank you to everyone who has contributed to Rails Error Dashboard!
950
950
  Special thanks to:
951
951
  - [@bonniesimon](https://github.com/bonniesimon) - Turbo helpers production fix
952
952
  - [@gundestrup](https://github.com/gundestrup) - Security fixes, dependency updates, CI/CD improvements
953
+ - [@midwire](https://github.com/midwire) - Backtrace line numbers, loading states & skeleton screens
953
954
 
954
955
  See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the complete list of contributors and their contributions.
955
956
 
@@ -1320,6 +1320,65 @@ body.dark-mode .timeline-item:hover .timeline-content {
1320
1320
  #sidebarToggle:hover i {
1321
1321
  transform: scale(1.1);
1322
1322
  }
1323
+
1324
+ /* Skeleton Loading Animations */
1325
+ @keyframes shimmer {
1326
+ 0% { background-position: -200% 0; }
1327
+ 100% { background-position: 200% 0; }
1328
+ }
1329
+
1330
+ .skeleton {
1331
+ background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
1332
+ background-size: 200% 100%;
1333
+ animation: shimmer 1.5s ease-in-out infinite;
1334
+ border-radius: 4px;
1335
+ }
1336
+
1337
+ body.dark-mode .skeleton {
1338
+ background: linear-gradient(90deg, #313244 25%, #45475a 50%, #313244 75%);
1339
+ background-size: 200% 100%;
1340
+ }
1341
+
1342
+ .skeleton-text {
1343
+ height: 1em;
1344
+ width: 60%;
1345
+ margin-bottom: 0.5em;
1346
+ }
1347
+
1348
+ .skeleton-text-short {
1349
+ width: 40%;
1350
+ }
1351
+
1352
+ .skeleton-card {
1353
+ height: 80px;
1354
+ }
1355
+
1356
+ .skeleton-row {
1357
+ height: 48px;
1358
+ margin-bottom: 2px;
1359
+ }
1360
+
1361
+ .skeleton-chart {
1362
+ height: 250px;
1363
+ }
1364
+
1365
+ /* Hide skeleton containers by default */
1366
+ .loading-skeleton {
1367
+ display: none;
1368
+ }
1369
+
1370
+ /* Loading spinner for buttons */
1371
+ .btn .loading-spinner {
1372
+ display: inline-block;
1373
+ width: 1em;
1374
+ height: 1em;
1375
+ border: 2px solid currentColor;
1376
+ border-right-color: transparent;
1377
+ border-radius: 50%;
1378
+ animation: spinner-border 0.75s linear infinite;
1379
+ vertical-align: middle;
1380
+ margin-right: 0.25em;
1381
+ }
1323
1382
  </style>
1324
1383
  </head>
1325
1384
 
@@ -1566,6 +1625,98 @@ body.dark-mode .timeline-item:hover .timeline-content {
1566
1625
  <!-- Bootstrap JS -->
1567
1626
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1568
1627
 
1628
+ <!-- Stimulus (for loading state management) -->
1629
+ <script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.min.js"></script>
1630
+
1631
+ <!-- Loading State Controller -->
1632
+ <script>
1633
+ (function() {
1634
+ 'use strict';
1635
+ if (typeof Stimulus === 'undefined') return;
1636
+
1637
+ var application = Stimulus.Application.start();
1638
+
1639
+ application.register('loading', class extends Stimulus.Controller {
1640
+ static get targets() {
1641
+ return ['skeleton', 'content', 'submitButton'];
1642
+ }
1643
+
1644
+ connect() {
1645
+ this._boundHideSkeletons = this.hideSkeletons.bind(this);
1646
+ document.addEventListener('turbo:before-stream-render', this._boundHideSkeletons);
1647
+ document.addEventListener('turbo:load', this._boundHideSkeletons);
1648
+ }
1649
+
1650
+ disconnect() {
1651
+ document.removeEventListener('turbo:before-stream-render', this._boundHideSkeletons);
1652
+ document.removeEventListener('turbo:load', this._boundHideSkeletons);
1653
+ if (this._safetyTimeout) clearTimeout(this._safetyTimeout);
1654
+ }
1655
+
1656
+ // Called on filter form submit
1657
+ submit() {
1658
+ this.showSkeletons();
1659
+ this.disableSubmitButtons();
1660
+ this.startSafetyTimeout();
1661
+ }
1662
+
1663
+ // Called on async action button click
1664
+ click(event) {
1665
+ var button = event.currentTarget;
1666
+ if (button.disabled) return;
1667
+
1668
+ button.disabled = true;
1669
+ var originalHTML = button.dataset.loadingOriginalHtml;
1670
+ if (!originalHTML) {
1671
+ button.dataset.loadingOriginalHtml = button.innerHTML;
1672
+ }
1673
+ var spinnerHTML = '<span class="loading-spinner"></span>';
1674
+ var buttonText = button.textContent.trim();
1675
+ button.innerHTML = spinnerHTML + ' ' + buttonText;
1676
+ }
1677
+
1678
+ showSkeletons() {
1679
+ this.skeletonTargets.forEach(function(el) { el.style.display = ''; });
1680
+ this.contentTargets.forEach(function(el) { el.style.display = 'none'; });
1681
+ }
1682
+
1683
+ hideSkeletons() {
1684
+ this.skeletonTargets.forEach(function(el) { el.style.display = 'none'; });
1685
+ this.contentTargets.forEach(function(el) { el.style.display = ''; });
1686
+ this.enableSubmitButtons();
1687
+ if (this._safetyTimeout) clearTimeout(this._safetyTimeout);
1688
+ }
1689
+
1690
+ disableSubmitButtons() {
1691
+ this.submitButtonTargets.forEach(function(btn) {
1692
+ btn.disabled = true;
1693
+ if (!btn.dataset.loadingOriginalHtml) {
1694
+ btn.dataset.loadingOriginalHtml = btn.innerHTML;
1695
+ }
1696
+ var spinnerHTML = '<span class="loading-spinner"></span>';
1697
+ btn.innerHTML = spinnerHTML + ' Loading...';
1698
+ });
1699
+ }
1700
+
1701
+ enableSubmitButtons() {
1702
+ this.submitButtonTargets.forEach(function(btn) {
1703
+ btn.disabled = false;
1704
+ if (btn.dataset.loadingOriginalHtml) {
1705
+ btn.innerHTML = btn.dataset.loadingOriginalHtml;
1706
+ delete btn.dataset.loadingOriginalHtml;
1707
+ }
1708
+ });
1709
+ }
1710
+
1711
+ startSafetyTimeout() {
1712
+ var self = this;
1713
+ this._safetyTimeout = setTimeout(function() {
1714
+ self.hideSkeletons();
1715
+ }, 10000);
1716
+ }
1717
+ });
1718
+ })();
1719
+ </script>
1569
1720
 
1570
1721
  <!-- Dashboard JavaScript (inline for production compatibility) -->
1571
1722
 
@@ -5,7 +5,7 @@
5
5
  <%= turbo_stream_from "error_list" %>
6
6
  <% end %>
7
7
 
8
- <div class="py-4">
8
+ <div class="py-4" data-controller="loading">
9
9
  <div class="d-flex justify-content-between align-items-center mb-4">
10
10
  <h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
11
11
  <div class="text-muted">
@@ -20,7 +20,23 @@
20
20
 
21
21
  <!-- Stats Cards -->
22
22
  <div id="dashboard_stats" class="mb-4">
23
- <%= render "stats", stats: @stats %>
23
+ <div data-loading-target="content">
24
+ <%= render "stats", stats: @stats %>
25
+ </div>
26
+ <div class="loading-skeleton" data-loading-target="skeleton">
27
+ <div class="row g-4">
28
+ <% 4.times do %>
29
+ <div class="col-md-3">
30
+ <div class="card stat-card">
31
+ <div class="card-body">
32
+ <div class="skeleton skeleton-text skeleton-text-short mb-2"></div>
33
+ <div class="skeleton skeleton-card"></div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+ </div>
24
40
  </div>
25
41
 
26
42
  <!-- Top Error Types (Only show if there are errors) -->
@@ -77,7 +93,7 @@
77
93
  <!-- 7-Day Error Trend (requires chartkick gem) -->
78
94
  <% if @stats[:errors_trend_7d]&.any? && respond_to?(:line_chart) %>
79
95
  <div class="row g-4 mb-4">
80
- <div class="col-md-8">
96
+ <div class="col-md-8" data-loading-target="content">
81
97
  <div class="card">
82
98
  <div class="card-header bg-white d-flex justify-content-between align-items-center">
83
99
  <h5 class="mb-0"><i class="bi bi-graph-up"></i> 7-Day Error Trend</h5>
@@ -105,7 +121,7 @@
105
121
  </div>
106
122
  </div>
107
123
  </div>
108
- <div class="col-md-4">
124
+ <div class="col-md-4" data-loading-target="content">
109
125
  <div class="card">
110
126
  <div class="card-header bg-white">
111
127
  <h5 class="mb-0"><i class="bi bi-pie-chart"></i> By Severity (7d)</h5>
@@ -120,6 +136,31 @@
120
136
  </div>
121
137
  </div>
122
138
  </div>
139
+
140
+ <div class="loading-skeleton" data-loading-target="skeleton">
141
+ <div class="row g-4 mb-4">
142
+ <div class="col-md-8">
143
+ <div class="card">
144
+ <div class="card-header bg-white">
145
+ <div class="skeleton skeleton-text" style="width: 200px;"></div>
146
+ </div>
147
+ <div class="card-body">
148
+ <div class="skeleton skeleton-chart"></div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ <div class="col-md-4">
153
+ <div class="card">
154
+ <div class="card-header bg-white">
155
+ <div class="skeleton skeleton-text" style="width: 150px;"></div>
156
+ </div>
157
+ <div class="card-body">
158
+ <div class="skeleton skeleton-chart"></div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
123
164
  <% end %>
124
165
 
125
166
  <!-- Active Filters Pills -->
@@ -193,7 +234,10 @@
193
234
  class: "btn btn-sm #{params[:reopened] == 'true' ? 'btn-primary' : 'btn-outline-primary'}" %>
194
235
  </div>
195
236
 
196
- <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
237
+ <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false, action: "submit->loading#submit" } do %>
238
+ <% if params[:reopened].present? %>
239
+ <%= hidden_field_tag :reopened, params[:reopened] %>
240
+ <% end %>
197
241
  <div class="col-md-4">
198
242
  <%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
199
243
  </div>
@@ -320,7 +364,7 @@
320
364
  </div>
321
365
 
322
366
  <div class="col-12 mt-3">
323
- <%= submit_tag "Apply Filters", class: "btn btn-primary" %>
367
+ <%= submit_tag "Apply Filters", class: "btn btn-primary", data: { loading_target: "submitButton" } %>
324
368
  <%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
325
369
  </div>
326
370
  <% end %>
@@ -362,42 +406,52 @@
362
406
 
363
407
  <div class="card-body p-0">
364
408
  <% if @errors.any? %>
365
- <div class="table-responsive">
366
- <table class="table table-hover mb-0">
367
- <thead class="table-light">
368
- <tr>
369
- <th style="width: 40px;">
370
- <input type="checkbox" id="select-all" class="form-check-input">
371
- </th>
372
- <th><%= sortable_header("Severity", "severity") %></th>
373
- <th><%= sortable_header("Error Type", "error_type") %></th>
374
- <th>Message</th>
375
- <th><%= sortable_header("Occurrences", "occurrence_count") %></th>
376
- <th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
377
-
378
- <!-- Show App column only when viewing all apps -->
379
- <% if @applications.size > 1 && params[:application_id].blank? %>
380
- <th><%= sortable_header("Application", "application_id") %></th>
409
+ <div data-loading-target="content">
410
+ <div class="table-responsive">
411
+ <table class="table table-hover mb-0">
412
+ <thead class="table-light">
413
+ <tr>
414
+ <th style="width: 40px;">
415
+ <input type="checkbox" id="select-all" class="form-check-input">
416
+ </th>
417
+ <th><%= sortable_header("Severity", "severity") %></th>
418
+ <th><%= sortable_header("Error Type", "error_type") %></th>
419
+ <th>Message</th>
420
+ <th><%= sortable_header("Occurrences", "occurrence_count") %></th>
421
+ <th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
422
+
423
+ <!-- Show App column only when viewing all apps -->
424
+ <% if @applications.size > 1 && params[:application_id].blank? %>
425
+ <th><%= sortable_header("Application", "application_id") %></th>
426
+ <% end %>
427
+
428
+ <% if @platforms.size > 1 %>
429
+ <th><%= sortable_header("Platform", "platform") %></th>
430
+ <% end %>
431
+ <th>Status</th>
432
+ <th></th>
433
+ </tr>
434
+ </thead>
435
+ <tbody id="error_list">
436
+ <% @errors.each do |error| %>
437
+ <%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
381
438
  <% end %>
439
+ </tbody>
440
+ </table>
441
+ </div>
382
442
 
383
- <% if @platforms.size > 1 %>
384
- <th><%= sortable_header("Platform", "platform") %></th>
385
- <% end %>
386
- <th>Status</th>
387
- <th></th>
388
- </tr>
389
- </thead>
390
- <tbody id="error_list">
391
- <% @errors.each do |error| %>
392
- <%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
393
- <% end %>
394
- </tbody>
395
- </table>
443
+ <!-- Pagination -->
444
+ <div class="p-3">
445
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
446
+ </div>
396
447
  </div>
397
448
 
398
- <!-- Pagination -->
399
- <div class="p-3">
400
- <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
449
+ <div class="loading-skeleton" data-loading-target="skeleton">
450
+ <div class="p-3">
451
+ <% 5.times do %>
452
+ <div class="skeleton skeleton-row mb-2"></div>
453
+ <% end %>
454
+ </div>
401
455
  </div>
402
456
  <% else %>
403
457
  <div class="text-center py-5">
@@ -57,7 +57,7 @@
57
57
  }
58
58
  </script>
59
59
 
60
- <div class="py-4">
60
+ <div class="py-4" data-controller="loading">
61
61
  <!-- Breadcrumbs -->
62
62
  <nav aria-label="breadcrumb" class="mb-3">
63
63
  <ol class="breadcrumb">
@@ -1214,7 +1214,7 @@
1214
1214
  </div>
1215
1215
  <div class="modal-footer">
1216
1216
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1217
- <%= submit_tag "Mark as Resolved", class: "btn btn-success" %>
1217
+ <%= submit_tag "Mark as Resolved", class: "btn btn-success", data: { action: "click->loading#click" } %>
1218
1218
  </div>
1219
1219
  <% end %>
1220
1220
  </div>
@@ -1241,7 +1241,7 @@
1241
1241
  </div>
1242
1242
  <div class="modal-footer">
1243
1243
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1244
- <%= submit_tag "Assign", class: "btn btn-primary" %>
1244
+ <%= submit_tag "Assign", class: "btn btn-primary", data: { action: "click->loading#click" } %>
1245
1245
  </div>
1246
1246
  <% end %>
1247
1247
  </div>
@@ -1273,7 +1273,7 @@
1273
1273
  </div>
1274
1274
  <div class="modal-footer">
1275
1275
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1276
- <%= submit_tag "Update Priority", class: "btn btn-warning" %>
1276
+ <%= submit_tag "Update Priority", class: "btn btn-warning", data: { action: "click->loading#click" } %>
1277
1277
  </div>
1278
1278
  <% end %>
1279
1279
  </div>
@@ -1315,7 +1315,7 @@
1315
1315
  </div>
1316
1316
  <div class="modal-footer">
1317
1317
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1318
- <%= submit_tag "Snooze", class: "btn btn-warning" %>
1318
+ <%= submit_tag "Snooze", class: "btn btn-warning", data: { action: "click->loading#click" } %>
1319
1319
  </div>
1320
1320
  <% end %>
1321
1321
  </div>
@@ -13,6 +13,8 @@
13
13
  #
14
14
  # All PostgreSQL-specific — gracefully skipped on SQLite/MySQL.
15
15
  class AddTimeSeriesIndexesToErrorLogs < ActiveRecord::Migration[7.0]
16
+ disable_ddl_transaction!
17
+
16
18
  def up
17
19
  return unless postgresql?
18
20
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -403,7 +403,7 @@ metadata:
403
403
  documentation_uri: https://AnjanJ.github.io/rails_error_dashboard
404
404
  bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
405
405
  post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
406
- \ Rails Error Dashboard v0.2.0\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
406
+ \ Rails Error Dashboard v0.2.1\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
407
407
  First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
408
408
  db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
409
409
  => '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n