diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0930c46..385f438 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Retry and discard actions on individual failed jobs
- Bulk "Retry All" and "Discard All" actions for failed jobs
+- Discard action on individual ready, scheduled, and blocked jobs
+- Bulk "Discard All" action for ready, scheduled, and blocked jobs (scoped to current queue filter)
- Roadmap section added to README
### Fixed
diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb
index a67f71f..a5a5b44 100644
--- a/app/controllers/solid_queue_web/jobs_controller.rb
+++ b/app/controllers/solid_queue_web/jobs_controller.rb
@@ -1,6 +1,7 @@
module SolidQueueWeb
class JobsController < ApplicationController
STATUSES = %w[ready scheduled claimed blocked failed].freeze
+ DISCARDABLE = %w[ready scheduled blocked].freeze
def index
@status = params[:status].presence_in(STATUSES) || "ready"
@@ -17,5 +18,40 @@ def index
@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
@jobs = @jobs.order(created_at: :desc).limit(100)
end
+
+ def destroy
+ execution = execution_model_for!(params[:status]).find(params[:id])
+ execution.discard
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), notice: "Job discarded."
+ rescue ArgumentError => e
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
+ rescue => e
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard job: #{e.message}"
+ end
+
+ def discard_all
+ model = execution_model_for!(params[:status])
+ scope = model.includes(:job)
+ scope = scope.where(jobs: { queue_name: params[:queue] }) if params[:queue].present?
+ jobs = scope.map(&:job)
+ model.discard_all_from_jobs(jobs)
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]),
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
+ rescue ArgumentError => e
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
+ rescue => e
+ redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard jobs: #{e.message}"
+ end
+
+ private
+
+ def execution_model_for!(status)
+ case status
+ when "ready" then SolidQueue::ReadyExecution
+ when "scheduled" then SolidQueue::ScheduledExecution
+ when "blocked" then SolidQueue::BlockedExecution
+ else raise ArgumentError, "Cannot discard #{status} jobs from this page."
+ end
+ end
end
end
diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb
index a544d22..d4cb25f 100644
--- a/app/views/solid_queue_web/jobs/index.html.erb
+++ b/app/views/solid_queue_web/jobs/index.html.erb
@@ -1,4 +1,17 @@
-
Jobs
+<% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>
+
+
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
@@ -20,6 +33,7 @@
Priority |
Scheduled At |
Enqueued At |
+ <% if discardable %> | <% end %>
@@ -39,6 +53,15 @@
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
<%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> |
+ <% if discardable %>
+
+ <%= button_to "Discard", job_path(execution),
+ method: :delete,
+ params: { status: @status, queue: @queue },
+ class: "sqd-btn sqd-btn--danger sqd-btn--sm",
+ data: { confirm: "Discard this job?" } %>
+ |
+ <% end %>
<% end %>
@@ -51,4 +74,4 @@
Filtering by queue: <%= @queue %> —
<%= link_to "Clear filter", jobs_path(status: @status) %>
-<% end %>
+<% end %>
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index eb06eb6..c1c2828 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,7 +2,11 @@
root to: "dashboard#index"
resources :queues, only: [ :index ]
- resources :jobs, only: [ :index ]
+ resources :jobs, only: [ :index, :destroy ] do
+ collection do
+ post :discard_all
+ end
+ end
resources :failed_jobs, only: [ :index, :destroy ] do
collection do
post :retry_all
diff --git a/spec/requests/solid_queue_web/jobs_spec.rb b/spec/requests/solid_queue_web/jobs_spec.rb
new file mode 100644
index 0000000..39bb41a
--- /dev/null
+++ b/spec/requests/solid_queue_web/jobs_spec.rb
@@ -0,0 +1,71 @@
+require "rails_helper"
+
+RSpec.describe "Jobs", type: :request do
+ let!(:ready_job) do
+ SolidQueue::Job.create!(
+ queue_name: "default",
+ class_name: "TestJob",
+ arguments: {},
+ active_job_id: SecureRandom.uuid
+ )
+ end
+
+ let(:ready_execution) { ready_job.ready_execution }
+
+ describe "GET /jobs/jobs" do
+ it "returns HTTP success" do
+ get "/jobs/jobs"
+ expect(response).to have_http_status(:ok)
+ end
+
+ it "shows ready jobs by default" do
+ get "/jobs/jobs"
+ expect(response.body).to include("TestJob")
+ end
+ end
+
+ describe "DELETE /jobs/jobs/: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")
+ 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" }
+ }.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")
+ follow_redirect!
+ expect(response.body).to include("Cannot discard")
+ end
+ end
+
+ describe "POST /jobs/jobs/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")
+ follow_redirect!
+ expect(response.body).to include("discarded")
+ end
+
+ it "clears all ready executions" do
+ expect {
+ post "/jobs/jobs/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")
+ follow_redirect!
+ expect(response.body).to include("Cannot discard")
+ end
+ end
+end