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
22 changes: 9 additions & 13 deletions app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
module SolidQueueWeb
class JobsController < ApplicationController
before_action :set_status_and_queue, only: [ :destroy, :discard_all ]
before_action :set_status, only: [ :destroy, :discard_all ]

def index
@status = params[:status].presence_in(Job::STATUSES) || "ready"
@queue = params[:queue].presence
@search = params[:q].presence
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
@jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
end
Expand All @@ -28,24 +26,24 @@ def destroy
@remaining_count = filtered_scope(model).count
respond_to do |format|
format.turbo_stream
format.html { redirect_to jobs_path(status: @status, queue: @queue), notice: "Job discarded." }
format.html { redirect_to jobs_path(status: @status), notice: "Job discarded." }
end
rescue ArgumentError => e
redirect_to jobs_path(status: @status, queue: @queue), alert: e.message
redirect_to jobs_path(status: @status), alert: e.message
rescue => e
redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard job: #{e.message}"
redirect_to jobs_path(status: @status), alert: "Could not discard job: #{e.message}"
end

def discard_all
model = execution_model_for!(@status)
jobs = filtered_scope(model).map(&:job)
model.discard_all_from_jobs(jobs)
redirect_to jobs_path(status: @status, queue: @queue),
redirect_to jobs_path(status: @status),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
rescue ArgumentError => e
redirect_to jobs_path(status: @status, queue: @queue), alert: e.message
redirect_to jobs_path(status: @status), alert: e.message
rescue => e
redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard jobs: #{e.message}"
redirect_to jobs_path(status: @status), alert: "Could not discard jobs: #{e.message}"
end

private
Expand All @@ -59,14 +57,12 @@ def derive_status(job)
"finished"
end

def set_status_and_queue
def set_status
@status = params[:status]
@queue = params[:queue].presence
end

def filtered_scope(model)
scope = model.includes(:job)
@queue.present? ? scope.where(jobs: { queue_name: @queue }) : scope
model.includes(:job)
end

def execution_model_for!(status)
Expand Down
63 changes: 63 additions & 0 deletions app/controllers/solid_queue_web/queues/jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module SolidQueueWeb
module Queues
class JobsController < ApplicationController
before_action :set_queue
before_action :set_status, only: [ :destroy, :discard_all ]

def index
@status = params[:status].presence_in(Job::STATUSES) || "ready"
@search = params[:q].presence
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
.where(solid_queue_jobs: { queue_name: @queue })
@jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
end

def destroy
model = execution_model_for!(@status)
@execution = model.find(params[:id])
@execution.discard
@remaining_count = filtered_scope(model).count
respond_to do |format|
format.turbo_stream
format.html { redirect_to queue_jobs_path(queue_name: @queue, status: @status), notice: "Job discarded." }
end
rescue ArgumentError => e
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
rescue => e
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard job: #{e.message}"
end

def discard_all
model = execution_model_for!(@status)
jobs = filtered_scope(model).map(&:job)
model.discard_all_from_jobs(jobs)
redirect_to queue_jobs_path(queue_name: @queue, status: @status),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
rescue ArgumentError => e
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
rescue => e
redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard jobs: #{e.message}"
end

private

def set_queue
@queue = params[:queue_name]
end

def set_status
@status = params[:status]
end

def filtered_scope(model)
model.includes(:job).where(solid_queue_jobs: { queue_name: @queue })
end

def execution_model_for!(status)
raise ArgumentError, "Cannot discard #{status} jobs from this page." unless Job::DISCARDABLE.include?(status)
Job::EXECUTION_MODELS[status]
end
end
end
end
20 changes: 5 additions & 15 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<div class="sqd-actions">
<%= button_to "Discard All", discard_all_jobs_path,
method: :post,
params: { status: @status, queue: @queue },
params: { status: @status },
class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
</div>
Expand All @@ -24,15 +24,12 @@

<form class="sqd-search" action="<%= jobs_path %>" method="get" data-controller="search">
<input type="hidden" name="status" value="<%= @status %>">
<% if @queue.present? %>
<input type="hidden" name="queue" value="<%= @queue %>">
<% end %>
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
data-action="input->search#filter">
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
<% if @search.present? %>
<%= link_to "Clear", jobs_path(status: @status, queue: @queue), class: "sqd-btn sqd-btn--muted" %>
<%= link_to "Clear", jobs_path(status: @status), class: "sqd-btn sqd-btn--muted" %>
<% end %>
</form>

Expand All @@ -57,10 +54,10 @@
<tr id="execution_<%= execution.id %>">
<td>
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %>
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
</td>
<td>
<%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
class: "sqd-mono", style: "color: inherit;" %>
</td>
<td><%= job.priority %></td>
Expand All @@ -72,7 +69,7 @@
<td class="sqd-row-actions">
<%= button_to "Discard", job_path(execution),
method: :delete,
params: { status: @status, queue: @queue },
params: { status: @status },
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
data: { confirm: "Discard this job?" } %>
</td>
Expand All @@ -87,11 +84,4 @@
<% 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;
<%= link_to "Clear filter", jobs_path(status: @status) %>
</p>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% if @remaining_count == 0 %>
<%= turbo_stream.replace "jobs-list" do %>
<div class="sqd-card" id="jobs-list">
<div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
</div>
<% end %>
<% else %>
<%= turbo_stream.remove "execution_#{@execution.id}" %>
<% end %>
89 changes: 89 additions & 0 deletions app/views/solid_queue_web/queues/jobs/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<div class="sqd-page-header">
<div>
<div class="sqd-breadcrumb">
<%= link_to "Queues", queues_path %> &rsaquo; <%= @queue %>
</div>
<h1 class="sqd-page-title">Jobs</h1>
</div>
</div>

<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
<% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>

<div class="sqd-page-header">
<div class="sqd-filters">
<%= link_to "Ready", queue_jobs_path(queue_name: @queue, status: "ready", q: @search), class: @status == "ready" ? "active" : "" %>
<%= link_to "Scheduled", queue_jobs_path(queue_name: @queue, status: "scheduled", q: @search), class: @status == "scheduled" ? "active" : "" %>
<%= link_to "Running", queue_jobs_path(queue_name: @queue, status: "claimed", q: @search), class: @status == "claimed" ? "active" : "" %>
<%= link_to "Blocked", queue_jobs_path(queue_name: @queue, status: "blocked", q: @search), class: @status == "blocked" ? "active" : "" %>
<%= link_to "Failed", queue_jobs_path(queue_name: @queue, status: "failed", q: @search), class: @status == "failed" ? "active" : "" %>
</div>
<% if discardable && @jobs.any? %>
<div class="sqd-actions">
<%= button_to "Discard All", discard_all_queue_jobs_path(queue_name: @queue),
method: :post,
params: { status: @status },
class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs in #{@queue}? This cannot be undone." } %>
</div>
<% end %>
</div>

<form class="sqd-search" action="<%= queue_jobs_path(queue_name: @queue) %>" method="get" data-controller="search">
<input type="hidden" name="status" value="<%= @status %>">
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
data-action="input->search#filter">
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
<% if @search.present? %>
<%= link_to "Clear", queue_jobs_path(queue_name: @queue, status: @status), class: "sqd-btn sqd-btn--muted" %>
<% end %>
</form>

<div class="sqd-card" id="jobs-list">
<% if @jobs.empty? %>
<div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
<% else %>
<table>
<thead>
<tr>
<th scope="col">Job Class</th>
<th scope="col">Priority</th>
<th scope="col">Scheduled At</th>
<th scope="col">Enqueued At</th>
<% if discardable %><th scope="col"><span class="sqd-sr-only">Actions</span></th><% end %>
</tr>
</thead>
<tbody>
<% @jobs.each do |execution| %>
<% job = execution.job %>
<tr id="execution_<%= execution.id %>">
<td>
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
</td>
<td><%= job.priority %></td>
<td class="sqd-mono">
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
</td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<% if discardable %>
<td class="sqd-row-actions">
<%= button_to "Discard", queue_job_path(queue_name: @queue, id: execution),
method: :delete,
params: { status: @status },
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
data: { confirm: "Discard this job?" } %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>

<% if @pagy.last > 1 %>
<%= @pagy.series_nav.html_safe %>
<% end %>
<% end %>
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
post :pause
post :resume
end
resources :jobs, path: "list", only: [ :index, :destroy ], controller: "queues/jobs" do
collection do
post :discard_all
end
end
end
resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
collection do
Expand Down
1 change: 1 addition & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.filter_gems_from_backtrace("turbo-rails")
end
1 change: 1 addition & 0 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
expect(response.body).to include("discarded")
end


it "responds with turbo stream when last job: replaces card with empty state" do
delete "/jobs/list/#{ready_execution.id}",
params: { status: "ready" },
Expand Down
Loading
Loading