Skip to content

Commit 7bb6cbb

Browse files
Merge pull request #85 from eclectic-coding/feat/custom-dashboard-cards
feat: custom dashboard cards and nav links
2 parents 84c45a5 + 5fb292c commit 7bb6cbb

11 files changed

Lines changed: 221 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- i18n support
13+
- Custom dashboard cards — `config.dashboard_cards` accepts an array of `{ title:, stats:, link: }` hashes; each card is rendered after the built-in queue stat cards on the dashboard; `stats:` is a lambda returning a `{ label => value }` hash evaluated at render time; `link:` is an optional `{ label:, url: }` header link
14+
- Custom nav links — `config.nav_links` accepts an array of `{ label:, url: }` hashes appended to the main navigation bar after the built-in links
1315

1416
### Changed
1517

Gemfile.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,6 @@ CHECKSUMS
342342
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
343343
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
344344
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
345-
bundler (4.0.13) sha256=19f08be7f27022cf0b89f27da0b044ae075e8270a9ef44ad248a932614e1ca3b
346345
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
347346
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
348347
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ A monitoring and management dashboard for [Solid Queue](https://github.com/rails
3535
- [Read replica support](#read-replica-support)
3636
- [i18n](#i18n)
3737
- [Adding a custom locale](#adding-a-custom-locale)
38+
- [Extensibility](#extensibility)
39+
- [Custom dashboard cards](#custom-dashboard-cards)
40+
- [Custom nav links](#custom-nav-links)
3841
- [Roadmap](#roadmap)
3942
- [Contributing](#contributing)
4043
- [License](#license)
@@ -102,6 +105,8 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
102105
- **Error frequency report**`GET /jobs/failed_jobs/errors` groups all failed jobs by error class and message prefix, shows a count per group, and surfaces a sample backtrace in an expandable row; sorted by count descending so the most common errors appear first; accessible via the "Error Summary" button on the Failed Jobs page
103106
- **Metrics / health endpoint**`GET /jobs/metrics.json` returns a machine-readable JSON document with job counts, throughput, per-queue depth and pause state, and process health summary; suitable for Prometheus scraping, uptime monitors, or external dashboards; `slow_jobs` count included when `slow_job_threshold` is configured
104107
- **i18n** — all UI strings (page titles, table headers, buttons, empty states, flash messages) are backed by `config/locales/en.yml`; locale switching via `?locale=` param or session; add a custom locale by supplying a YAML file in your host app and registering it with `config.available_locales`
108+
- **Custom dashboard cards**`config.dashboard_cards` accepts an array of `{ title:, stats:, link: }` hashes rendered after the built-in queue stat cards; `stats:` is a lambda returning a `{ label => value }` hash evaluated at render time; `link:` is an optional header link
109+
- **Custom nav links**`config.nav_links` accepts an array of `{ label:, url: }` hashes appended to the main navigation bar after the built-in links
105110

106111
[↑ Back to top](#table-of-contents)
107112

@@ -174,6 +179,8 @@ SolidQueueWeb.configure do |config|
174179
config.connects_to = { reading: :reading, writing: :writing } # read replica (default: nil)
175180
config.time_zone = "America/New_York" # display timezone for all timestamps (default: nil = UTC)
176181
config.available_locales = [:en, :fr] # locales available for switching (default: [:en])
182+
config.nav_links = [{ label: "Admin", url: "/admin" }] # extra nav links (default: [])
183+
config.dashboard_cards = [{ title: "My App", stats: -> { { "Users" => User.count } } }] # custom stat cards (default: [])
177184
end
178185

179186
SolidQueueWeb.authenticate do
@@ -451,6 +458,51 @@ Rails will pick up the file automatically via its standard `config.i18n.load_pat
451458

452459
---
453460

461+
## Extensibility
462+
463+
### Custom dashboard cards
464+
465+
`config.dashboard_cards` adds custom stat cards to the dashboard after the built-in queue cards. Each card accepts three keys:
466+
467+
| Key | Type | Description |
468+
|-----|------|-------------|
469+
| `title` | String | Card heading (required) |
470+
| `link` | `{ label:, url: }` | Optional header link rendered top-right |
471+
| `stats` | Lambda | Optional — called at render time; must return a `{ label => value }` hash |
472+
473+
```ruby
474+
SolidQueueWeb.configure do |config|
475+
config.dashboard_cards = [
476+
{
477+
title: "My App",
478+
link: { label: "View Admin", url: "/admin" },
479+
stats: -> { { "Users" => User.count, "Premium" => User.premium.count } }
480+
}
481+
]
482+
end
483+
```
484+
485+
The `stats` lambda runs on every dashboard render, so keep it fast. Defaults to `[]` — no custom cards appear when unconfigured.
486+
487+
### Custom nav links
488+
489+
`config.nav_links` appends extra links to the main navigation bar after the built-in links. Use it to link back to your host application's admin pages or related tools.
490+
491+
```ruby
492+
SolidQueueWeb.configure do |config|
493+
config.nav_links = [
494+
{ label: "Back to App", url: "/" },
495+
{ label: "Admin", url: "/admin" }
496+
]
497+
end
498+
```
499+
500+
Defaults to `[]` — no extra links appear when unconfigured.
501+
502+
[↑ Back to top](#table-of-contents)
503+
504+
---
505+
454506
## Roadmap
455507

456508
See [ROADMAP.md](ROADMAP.md) for the full post-1.0 feature plan, organized by release milestone.

ROADMAP.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,4 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib
66

77
---
88

9-
## v1.6 — Extensibility
10-
11-
*Breaking changes or large architectural additions — planned only if community demand warrants it.*
12-
13-
| Feature | Notes |
14-
|---|---|
15-
| **Custom dashboard cards** | Registration hook so host apps can add their own stat cards alongside queue stats. |
16-
| **Custom nav links** | `config.nav_links = [{ label: "Admin", url: "/admin" }]` to integrate the dashboard into the host app's navigation. |
9+
No items currently planned. Open an issue to suggest a feature.

app/assets/stylesheets/solid_queue_web/_04_table.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,34 @@
1919
font-weight: 600;
2020
}
2121

22+
.sqd-card__body {
23+
padding: 0.75rem 1rem;
24+
display: flex;
25+
flex-direction: column;
26+
}
27+
28+
.sqd-custom-stat {
29+
display: flex;
30+
justify-content: space-between;
31+
align-items: baseline;
32+
font-size: 13px;
33+
padding: 0.375rem 0;
34+
border-bottom: 1px solid var(--border);
35+
}
36+
37+
.sqd-custom-stat:last-child {
38+
border-bottom: none;
39+
}
40+
41+
.sqd-custom-stat__label {
42+
color: var(--muted);
43+
}
44+
45+
.sqd-custom-stat__value {
46+
font-weight: 600;
47+
font-variant-numeric: tabular-nums;
48+
}
49+
2250
table {
2351
width: 100%;
2452
border-collapse: collapse;

app/views/layouts/solid_queue_web/application.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<li><%= link_to t("solid_queue_web.layout.nav.processes"), processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
2828
<li><%= link_to t("solid_queue_web.layout.nav.search"), search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %></li>
2929
<li><%= link_to t("solid_queue_web.layout.nav.audit"), audit_path, class: current_page?(audit_path) ? "active" : "", aria: { current: current_page?(audit_path) ? "page" : nil } %></li>
30+
<% SolidQueueWeb.nav_links.each do |link| %>
31+
<li><%= link_to link[:label], link[:url] %></li>
32+
<% end %>
3033
</ul>
3134
</nav>
3235
</div>

app/views/solid_queue_web/dashboard/index.html.erb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,26 @@
196196
</div>
197197
</div>
198198
<% end %>
199+
200+
<% SolidQueueWeb.dashboard_cards.each do |card| %>
201+
<div class="sqd-card">
202+
<div class="sqd-card__header">
203+
<span class="sqd-card__title"><%= card[:title] %></span>
204+
<% if card[:link] %>
205+
<%= link_to card[:link][:label], card[:link][:url], class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
206+
<% end %>
207+
</div>
208+
<% if card[:stats] %>
209+
<div class="sqd-card__body">
210+
<% card[:stats].call.each do |label, value| %>
211+
<div class="sqd-custom-stat">
212+
<span class="sqd-custom-stat__label"><%= label %></span>
213+
<span class="sqd-custom-stat__value"><%= value %></span>
214+
</div>
215+
<% end %>
216+
</div>
217+
<% end %>
218+
</div>
219+
<% end %>
199220
</div>
200221
<% end %>

lib/solid_queue_web.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class << self
77
attr_writer :page_size, :dashboard_refresh_interval, :default_refresh_interval, :search_results_limit,
88
:slow_job_threshold, :alert_webhook_url, :alert_failure_threshold, :alert_webhook_cooldown,
99
:alert_queue_thresholds, :alert_slow_job_count_threshold, :alert_stale_process_threshold,
10-
:connects_to, :time_zone, :available_locales
10+
:connects_to, :time_zone, :available_locales, :nav_links, :dashboard_cards
1111

1212
def page_size
1313
@page_size || 25
@@ -65,6 +65,14 @@ def available_locales
6565
@available_locales || %i[en]
6666
end
6767

68+
def nav_links
69+
@nav_links || []
70+
end
71+
72+
def dashboard_cards
73+
@dashboard_cards || []
74+
end
75+
6876
def configure
6977
yield self
7078
end

spec/controllers/solid_queue_web/application_controller_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ def index
4646
end
4747
end
4848

49+
describe "#record_audit" do
50+
controller(SolidQueueWeb::ApplicationController) do
51+
skip_before_action :authenticate!
52+
53+
def create
54+
record_audit("test_action")
55+
render plain: "ok"
56+
end
57+
end
58+
59+
it "logs an error and does not raise when AuditEvent.create! fails" do
60+
allow(SolidQueueWeb::AuditEvent).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
61+
expect(Rails.logger).to receive(:error).with(/Audit log failed/)
62+
post :create
63+
expect(response).to have_http_status(:ok)
64+
end
65+
end
66+
67+
describe "#resolve_current_actor" do
68+
after { SolidQueueWeb.instance_variable_set(:@current_actor, nil) }
69+
70+
it "logs an error and returns nil when the current_actor block raises" do
71+
SolidQueueWeb.current_actor { raise "identity error" }
72+
expect(Rails.logger).to receive(:error).with(/current_actor block failed/)
73+
result = controller.send(:resolve_current_actor)
74+
expect(result).to be_nil
75+
end
76+
end
77+
4978
describe "#replica_configured?" do
5079
it "returns false when only role is configured" do
5180
expect(controller.send(:replica_configured?, { role: :writing })).to be false
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
require "rails_helper"
2+
require "rails/generators"
3+
require "rails/generators/testing/assertions"
4+
require "rails/generators/testing/setup_and_teardown"
5+
require "rails/generators/testing/behavior"
6+
require "generators/solid_queue_web/install/migrations_generator"
7+
8+
RSpec.describe SolidQueueWeb::Install::MigrationsGenerator do
9+
include Rails::Generators::Testing::Behavior
10+
include Rails::Generators::Testing::Assertions
11+
include FileUtils
12+
13+
tests SolidQueueWeb::Install::MigrationsGenerator
14+
destination File.expand_path("../../../tmp/generator_test", __dir__)
15+
16+
before { prepare_destination }
17+
18+
describe "#create_migration_file" do
19+
before { run_generator }
20+
21+
it "creates the audit events migration file" do
22+
assert_migration "db/migrate/create_solid_queue_web_audit_events.rb"
23+
end
24+
25+
it "defines the solid_queue_web_audit_events table in the migration" do
26+
assert_migration "db/migrate/create_solid_queue_web_audit_events.rb",
27+
/create_table :solid_queue_web_audit_events/
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)