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
27 changes: 27 additions & 0 deletions app/assets/stylesheets/solid_queue_web/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,33 @@ tbody tr:hover { background: var(--bg); }
color: #fff;
}

/* Period filter */
.sqd-period-filter {
display: flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
}

.sqd-period-filter a {
padding: 0.2rem 0.55rem;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
text-decoration: none;
border: 1px solid var(--border);
color: var(--muted);
background: var(--surface);
transition: all 0.1s;
}

.sqd-period-filter a:hover,
.sqd-period-filter a.active {
background: var(--muted);
border-color: var(--muted);
color: #fff;
}

/* Code / monospace */
.sqd-mono {
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
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
Expand Up @@ -2,6 +2,8 @@ module SolidQueueWeb
class ApplicationController < ActionController::Base
include Pagy::Method

PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze

before_action :authenticate!

private
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/solid_queue_web/failed_jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ def destroy
def retry_all
jobs = filtered_scope.map(&:job)
SolidQueue::FailedExecution.retry_all(jobs)
redirect_to failed_jobs_path(queue: @queue, q: @search),
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
end

def discard_all
jobs = filtered_scope.map(&:job)
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
redirect_to failed_jobs_path(queue: @queue, q: @search),
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
end

Expand All @@ -41,12 +41,14 @@ def discard_all
def set_filter_params
@queue = params[:queue].presence
@search = params[:q].presence
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
end

def filtered_scope
scope = SolidQueue::FailedExecution.includes(:job)
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
scope
end
end
Expand Down
19 changes: 12 additions & 7 deletions app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ class JobsController < ApplicationController
def index
@status = params[:status].presence_in(Job::STATUSES) || "ready"
@search = params[:q].presence
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
@jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
@jobs = @jobs.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
end

Expand All @@ -24,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), notice: "Job discarded." }
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
end
rescue ArgumentError => e
redirect_to jobs_path(status: @status), alert: e.message
redirect_to jobs_path(status: @status, period: @period), alert: e.message
rescue => e
redirect_to jobs_path(status: @status), alert: "Could not discard job: #{e.message}"
redirect_to jobs_path(status: @status, period: @period), 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),
redirect_to jobs_path(status: @status, period: @period),
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
rescue ArgumentError => e
redirect_to jobs_path(status: @status), alert: e.message
redirect_to jobs_path(status: @status, period: @period), alert: e.message
rescue => e
redirect_to jobs_path(status: @status), alert: "Could not discard jobs: #{e.message}"
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
end

private
Expand All @@ -57,10 +59,13 @@ def derive_status(job)

def set_status
@status = params[:status]
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
end

def filtered_scope(model)
model.includes(:job)
scope = model.includes(:job)
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
scope
end

def execution_model_for!(status)
Expand Down
17 changes: 12 additions & 5 deletions app/views/solid_queue_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<div class="sqd-actions">
<%= button_to "Retry All", retry_all_failed_jobs_path,
method: :post,
params: { queue: @queue, q: @search },
params: { queue: @queue, q: @search, period: @period },
class: "sqd-btn sqd-btn--primary",
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
<%= button_to "Discard All", discard_all_failed_jobs_path,
method: :post,
params: { queue: @queue, q: @search },
params: { queue: @queue, q: @search, period: @period },
class: "sqd-btn sqd-btn--danger",
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
</div>
Expand All @@ -20,13 +20,20 @@
<% if @queue.present? %>
<input type="hidden" name="queue" value="<%= @queue %>">
<% end %>
<input type="hidden" name="period" value="<%= @period %>">
<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", failed_jobs_path(queue: @queue), class: "sqd-btn sqd-btn--muted" %>
<%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
<% end %>
<div class="sqd-period-filter" role="group" aria-label="Time period">
<%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
<%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %>
<%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %>
<%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %>
</div>
</form>

<% if @pagy.last > 1 %>
Expand All @@ -53,7 +60,7 @@
<tr>
<td><%= link_to job.class_name, job_path(job) %></td>
<td>
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search),
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
class: "sqd-mono", style: "color: inherit;" %>
</td>
<td>
Expand Down Expand Up @@ -83,6 +90,6 @@
<% 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", failed_jobs_path(q: @search) %>
<%= link_to "Clear filter", failed_jobs_path(q: @search, period: @period) %>
</p>
<% end %>
23 changes: 15 additions & 8 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

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

<form class="sqd-search" action="<%= jobs_path %>" method="get" data-controller="search">
<input type="hidden" name="status" value="<%= @status %>">
<input type="hidden" name="period" value="<%= @period %>">
<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), class: "sqd-btn sqd-btn--muted" %>
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqd-btn sqd-btn--muted" %>
<% end %>
<div class="sqd-period-filter" role="group" aria-label="Time period">
<%= link_to "All", jobs_path(status: @status, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
<%= link_to "1h", jobs_path(status: @status, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %>
<%= link_to "24h", jobs_path(status: @status, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %>
<%= link_to "7d", jobs_path(status: @status, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %>
</div>
</form>

<div class="sqd-card" id="jobs-list">
Expand Down Expand Up @@ -69,7 +76,7 @@
<td class="sqd-row-actions">
<%= button_to "Discard", job_path(execution),
method: :delete,
params: { status: @status },
params: { status: @status, period: @period },
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
data: { confirm: "Discard this job?" } %>
</td>
Expand Down
2 changes: 1 addition & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +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")
config.backtrace_exclusion_patterns << /\/turbo-rails\//
end
41 changes: 41 additions & 0 deletions spec/requests/solid_queue_web/failed_jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,47 @@
end
end

describe "GET /jobs/failed_jobs?period= (time-based filter)" do
let!(:old_execution) do
j = SolidQueue::Job.create!(
queue_name: "default",
class_name: "OldFailedJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
j.ready_execution&.destroy
exec = SolidQueue::FailedExecution.create!(
job: j,
error: { exception_class: "RuntimeError", message: "old", backtrace: [] }
)
j.update_columns(created_at: 2.days.ago)
exec
end

it "shows all failed jobs when no period is set" do
get "/jobs/failed_jobs"
expect(response.body).to include("TestJob")
expect(response.body).to include("OldFailedJob")
end

it "filters to jobs failed within the last hour" do
get "/jobs/failed_jobs", params: { period: "1h" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("OldFailedJob")
end

it "ignores invalid period values" do
get "/jobs/failed_jobs", params: { period: "bogus" }
expect(response).to have_http_status(:ok)
expect(response.body).to include("TestJob")
end

it "persists period across bulk action params" do
get "/jobs/failed_jobs", params: { period: "1h" }
expect(response.body).to include("period=1h")
end
end

describe "POST /jobs/failed_jobs/:id/retry" do
it "retries the job and redirects" do
post "/jobs/failed_jobs/#{execution.id}/retry"
Expand Down
49 changes: 49 additions & 0 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,55 @@
end
end

describe "GET /jobs/list?period= (time-based filter)" do
let!(:old_job) do
job = SolidQueue::Job.create!(
queue_name: "default",
class_name: "OldJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
job.update_columns(created_at: 2.days.ago)
job.ready_execution.update_columns(created_at: 2.days.ago)
job
end

it "shows all jobs when no period is set" do
get "/jobs/list"
expect(response.body).to include("TestJob")
expect(response.body).to include("OldJob")
end

it "filters to jobs enqueued within the last hour" do
get "/jobs/list", params: { period: "1h" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("OldJob")
end

it "filters to jobs enqueued within the last 24 hours" do
get "/jobs/list", params: { period: "24h" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("OldJob")
end

it "includes all jobs within 7 days" do
get "/jobs/list", params: { period: "7d" }
expect(response.body).to include("TestJob")
expect(response.body).to include("OldJob")
end

it "ignores invalid period values" do
get "/jobs/list", params: { period: "bogus" }
expect(response).to have_http_status(:ok)
expect(response.body).to include("TestJob")
end

it "persists period across status tab links" do
get "/jobs/list", params: { period: "1h" }
expect(response.body).to include("period=1h")
end
end

describe "DELETE /jobs/list/:id (discard single)" do
it "discards the job and redirects (HTML)" do
delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" }
Expand Down
Loading