diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1a119..536a37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 08de6ee..bcae1e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: solid_queue_web (0.2.0) + pagy (>= 43.0) rails (>= 8.1.3) solid_queue (>= 1.0) @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/app/controllers/solid_queue_web/application_controller.rb b/app/controllers/solid_queue_web/application_controller.rb index 9f93815..8b8e5ba 100644 --- a/app/controllers/solid_queue_web/application_controller.rb +++ b/app/controllers/solid_queue_web/application_controller.rb @@ -1,5 +1,7 @@ module SolidQueueWeb class ApplicationController < ActionController::Base + include Pagy::Method + before_action :authenticate! private diff --git a/app/controllers/solid_queue_web/failed_jobs_controller.rb b/app/controllers/solid_queue_web/failed_jobs_controller.rb index 05242b4..67dc89e 100644 --- a/app/controllers/solid_queue_web/failed_jobs_controller.rb +++ b/app/controllers/solid_queue_web/failed_jobs_controller.rb @@ -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 diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb index 3b7f468..8b34186 100644 --- a/app/controllers/solid_queue_web/jobs_controller.rb +++ b/app/controllers/solid_queue_web/jobs_controller.rb @@ -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 diff --git a/app/views/solid_queue_web/failed_jobs/index.html.erb b/app/views/solid_queue_web/failed_jobs/index.html.erb index 8b97231..59ab09d 100644 --- a/app/views/solid_queue_web/failed_jobs/index.html.erb +++ b/app/views/solid_queue_web/failed_jobs/index.html.erb @@ -10,6 +10,10 @@ <% end %> +<% if @pagy.last > 1 %> + <%= @pagy.series_nav.html_safe %> +<% end %> +
Filtering by queue: <%= @queue %> — diff --git a/config/routes.rb b/config/routes.rb index 6922aa4..95df0f0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/solid_queue_web/engine.rb b/lib/solid_queue_web/engine.rb index 821738c..82a6bfa 100644 --- a/lib/solid_queue_web/engine.rb +++ b/lib/solid_queue_web/engine.rb @@ -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 diff --git a/solid_queue_web.gemspec b/solid_queue_web.gemspec index fb9f4cb..7870f79 100644 --- a/solid_queue_web.gemspec +++ b/solid_queue_web.gemspec @@ -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 @@ -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 diff --git a/spec/requests/solid_queue_web/jobs_spec.rb b/spec/requests/solid_queue_web/jobs_spec.rb index ee6362f..d9440ca 100644 --- a/spec/requests/solid_queue_web/jobs_spec.rb +++ b/spec/requests/solid_queue_web/jobs_spec.rb @@ -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 @@ -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