Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Pagination for jobs and failed jobs lists via pagy (25 per page)
- Jobs URL segment renamed from `/jobs/jobs` to `/jobs/list`
- Job detail page showing status, queue, priority, arguments (pretty-printed JSON), and full error backtrace for failed jobs
- Retry/Discard action buttons on the detail page based on job status
- Job class names on the jobs and failed jobs index pages link to the detail page
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PATH
remote: .
specs:
solid_queue_web (0.2.0)
pagy (>= 43.0)
rails (>= 8.1.3)
solid_queue (>= 1.0)

Expand Down Expand Up @@ -144,6 +145,10 @@ GEM
racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
pagy (43.5.4)
json
uri
yaml
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
Expand Down Expand Up @@ -284,6 +289,7 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
yaml (0.4.0)
zeitwerk (2.7.5)

PLATFORMS
Expand Down Expand Up @@ -348,6 +354,7 @@ CHECKSUMS
nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639
nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
Expand Down Expand Up @@ -401,6 +408,7 @@ CHECKSUMS
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd

BUNDLED WITH
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/solid_queue_web/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module SolidQueueWeb
class ApplicationController < ActionController::Base
include Pagy::Method

before_action :authenticate!

private
Expand Down
7 changes: 3 additions & 4 deletions app/controllers/solid_queue_web/failed_jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module SolidQueueWeb
class FailedJobsController < ApplicationController
def index
@failed_jobs = SolidQueue::FailedExecution
.includes(:job)
.order(created_at: :desc)
.limit(100)
@pagy, @failed_jobs = pagy(
SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
)
end

def retry
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def index
end

@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
@jobs = @jobs.order(created_at: :desc).limit(100)
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
end

def show
Expand Down
4 changes: 4 additions & 0 deletions app/views/solid_queue_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<% end %>
</div>

<% if @pagy.last > 1 %>
<%= @pagy.series_nav.html_safe %>
<% end %>

<div class="sqd-card">
<% if @failed_jobs.empty? %>
<div class="sqd-empty">No failed jobs. All clear!</div>
Expand Down
4 changes: 4 additions & 0 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
<% end %>
</div>

<% if @pagy.last > 1 %>
<%= @pagy.series_nav.html_safe %>
<% end %>

<% if @queue.present? %>
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
Filtering by queue: <strong><%= @queue %></strong> &mdash;
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
root to: "dashboard#index"

resources :queues, only: [ :index ]
resources :jobs, only: [ :index, :show, :destroy ] do
resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
collection do
post :discard_all
end
Expand Down
8 changes: 8 additions & 0 deletions lib/solid_queue_web/engine.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
require "solid_queue"
require "pagy"
require "pagy/toolbox/paginators/method"

module SolidQueueWeb
class Engine < ::Rails::Engine
isolate_namespace SolidQueueWeb

config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")

initializer "solid_queue_web.pagy" do
Pagy::OPTIONS[:limit] = 25
end
end
end
6 changes: 4 additions & 2 deletions solid_queue_web.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ Gem::Specification.new do |spec|
spec.authors = [ "Chuck Smith" ]
spec.email = [ "eclectic-coding@users.noreply.github.com" ]
spec.homepage = "https://github.com/eclectic-coding/solid_queue_web"
spec.summary = "A read-only Rails engine dashboard for monitoring Solid Queue jobs."
spec.summary = "A Rails engine dashboard for monitoring and managing Solid Queue jobs."
spec.description = "Mount SolidQueueWeb in any Rails app using Solid Queue to get a " \
"real-time read-only view of your queues, jobs by status, and failed executions."
"dashboard for your queues, jobs by status, failed executions, and job actions " \
"(retry, discard) — all without leaving your app."
spec.license = "MIT"

spec.metadata["homepage_uri"] = spec.homepage
Expand All @@ -24,4 +25,5 @@ Gem::Specification.new do |spec|

spec.add_dependency "rails", ">= 8.1.3"
spec.add_dependency "solid_queue", ">= 1.0"
spec.add_dependency "pagy", ">= 43.0"
end
38 changes: 19 additions & 19 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@

let(:ready_execution) { ready_job.ready_execution }

describe "GET /jobs/jobs/:id (detail)" do
describe "GET /jobs/list/:id (detail)" do
it "returns HTTP success" do
get "/jobs/jobs/#{ready_job.id}"
get "/jobs/list/#{ready_job.id}"
expect(response).to have_http_status(:ok)
end

it "displays job class name and details" do
get "/jobs/jobs/#{ready_job.id}"
get "/jobs/list/#{ready_job.id}"
expect(response.body).to include("TestJob")
expect(response.body).to include("default")
end
Expand All @@ -30,64 +30,64 @@
job: ready_job,
error: { exception_class: "RuntimeError", message: "boom", backtrace: [ "app/jobs/test_job.rb:1" ] }
)
get "/jobs/jobs/#{ready_job.id}"
get "/jobs/list/#{ready_job.id}"
expect(response.body).to include("RuntimeError")
expect(response.body).to include("app/jobs/test_job.rb:1")
end
end

describe "GET /jobs/jobs" do
describe "GET /jobs/list" do
it "returns HTTP success" do
get "/jobs/jobs"
get "/jobs/list"
expect(response).to have_http_status(:ok)
end

it "shows ready jobs by default" do
get "/jobs/jobs"
get "/jobs/list"
expect(response.body).to include("TestJob")
end
end

describe "DELETE /jobs/jobs/:id (discard single)" do
describe "DELETE /jobs/list/:id (discard single)" do
it "discards the job and redirects" do
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "ready" }
expect(response).to redirect_to("/jobs/jobs?status=ready")
delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" }
expect(response).to redirect_to("/jobs/list?status=ready")
follow_redirect!
expect(response.body).to include("discarded")
end

it "removes the execution and job" do
expect {
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "ready" }
delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" }
}.to change(SolidQueue::ReadyExecution, :count).by(-1)
.and change(SolidQueue::Job, :count).by(-1)
end

it "rejects discard for claimed status" do
delete "/jobs/jobs/#{ready_execution.id}", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/jobs?status=claimed")
delete "/jobs/list/#{ready_execution.id}", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/list?status=claimed")
follow_redirect!
expect(response.body).to include("Cannot discard")
end
end

describe "POST /jobs/jobs/discard_all" do
describe "POST /jobs/list/discard_all" do
it "discards all ready jobs and redirects" do
post "/jobs/jobs/discard_all", params: { status: "ready" }
expect(response).to redirect_to("/jobs/jobs?status=ready")
post "/jobs/list/discard_all", params: { status: "ready" }
expect(response).to redirect_to("/jobs/list?status=ready")
follow_redirect!
expect(response.body).to include("discarded")
end

it "clears all ready executions" do
expect {
post "/jobs/jobs/discard_all", params: { status: "ready" }
post "/jobs/list/discard_all", params: { status: "ready" }
}.to change(SolidQueue::ReadyExecution, :count).to(0)
end

it "rejects discard_all for claimed status" do
post "/jobs/jobs/discard_all", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/jobs?status=claimed")
post "/jobs/list/discard_all", params: { status: "claimed" }
expect(response).to redirect_to("/jobs/list?status=claimed")
follow_redirect!
expect(response.body).to include("Cannot discard")
end
Expand Down