From 5e9d6ae7ed547adb30cfeb005a6d216c8966bb16 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 13:02:33 -0400 Subject: [PATCH 1/4] feat: global job search across all statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /jobs/search?q= which queries all five execution models (ready, scheduled, claimed, blocked, failed) for jobs whose class name matches the search term. Results are grouped by status, each with a match count, a "View all →" link to the filtered status page (or failed jobs page), and a table of the first 25 matches linking to individual job detail pages. A "Search" nav link is added to the header. The page shows no results section when the query is blank. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/application.css | 23 +++++ .../solid_queue_web/search_controller.rb | 22 +++++ .../solid_queue_web/application.html.erb | 1 + .../solid_queue_web/search/index.html.erb | 57 +++++++++++ config/routes.rb | 2 + spec/requests/solid_queue_web/search_spec.rb | 94 +++++++++++++++++++ 6 files changed, 199 insertions(+) create mode 100644 app/controllers/solid_queue_web/search_controller.rb create mode 100644 app/views/solid_queue_web/search/index.html.erb create mode 100644 spec/requests/solid_queue_web/search_spec.rb diff --git a/app/assets/stylesheets/solid_queue_web/application.css b/app/assets/stylesheets/solid_queue_web/application.css index 5a06bea..e6fa75f 100644 --- a/app/assets/stylesheets/solid_queue_web/application.css +++ b/app/assets/stylesheets/solid_queue_web/application.css @@ -332,6 +332,29 @@ tbody tr:hover { background: var(--bg); } .sqd-search__input { width: 100%; } } +.sqd-search--global { margin-bottom: 2rem; } + +.sqd-search__input--lg { + width: 420px; + font-size: 15px; + padding: 0.5rem 1rem; +} + +@media (max-width: 640px) { + .sqd-search__input--lg { width: 100%; } +} + +.sqd-search-group { + margin-bottom: 2rem; +} + +.sqd-search-group__header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + /* Filters */ .sqd-filters { display: flex; diff --git a/app/controllers/solid_queue_web/search_controller.rb b/app/controllers/solid_queue_web/search_controller.rb new file mode 100644 index 0000000..206ff32 --- /dev/null +++ b/app/controllers/solid_queue_web/search_controller.rb @@ -0,0 +1,22 @@ +module SolidQueueWeb + class SearchController < ApplicationController + LIMIT = 25 + + def index + @query = params[:q].presence + @results = {} + + return unless @query + + Job::EXECUTION_MODELS.each do |status, model| + scope = model.includes(:job) + .references(:job) + .where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%") + .order(created_at: :desc) + total = scope.count + executions = scope.limit(LIMIT).to_a + @results[status] = { executions: executions, total: total } unless executions.empty? + end + end + end +end diff --git a/app/views/layouts/solid_queue_web/application.html.erb b/app/views/layouts/solid_queue_web/application.html.erb index 59b29ff..f7ad321 100644 --- a/app/views/layouts/solid_queue_web/application.html.erb +++ b/app/views/layouts/solid_queue_web/application.html.erb @@ -29,6 +29,7 @@
  • <%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %>
  • <%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %>
  • <%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %>
  • +
  • <%= link_to "Search", search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %>
  • diff --git a/app/views/solid_queue_web/search/index.html.erb b/app/views/solid_queue_web/search/index.html.erb new file mode 100644 index 0000000..700ec4a --- /dev/null +++ b/app/views/solid_queue_web/search/index.html.erb @@ -0,0 +1,57 @@ +

    Search Jobs

    + + + +<% if @query.present? %> + <% if @results.empty? %> +
    No jobs found matching “<%= @query %>”.
    + <% else %> + <% @results.each do |status, data| %> +
    +
    + <%= status %> + + <%= pluralize(data[:total], "match", "matches") %> + <% if data[:total] > SolidQueueWeb::SearchController::LIMIT %> + — showing first <%= SolidQueueWeb::SearchController::LIMIT %> + <% end %> + + <% if status == "failed" %> + <%= link_to "View all →", failed_jobs_path(q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %> + <% else %> + <%= link_to "View all →", jobs_path(status: status, q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %> + <% end %> +
    +
    + + + + + + + + + + <% data[:executions].each do |execution| %> + <% job = execution.job %> + + + + + + <% end %> + +
    Job ClassQueueEnqueued At
    <%= link_to job.class_name, job_path(job) %><%= job.queue_name %><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
    +
    +
    + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 72dc845..4ec1be0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ SolidQueueWeb::Engine.routes.draw do root to: "dashboard#index" + get "search", to: "search#index", as: :search + resources :recurring_tasks, only: [ :index ] resources :processes, only: [ :index ] resources :queues, only: [ :index ], param: :name do diff --git a/spec/requests/solid_queue_web/search_spec.rb b/spec/requests/solid_queue_web/search_spec.rb new file mode 100644 index 0000000..43eaa6d --- /dev/null +++ b/spec/requests/solid_queue_web/search_spec.rb @@ -0,0 +1,94 @@ +require "rails_helper" + +RSpec.describe "Search", type: :request do + let!(:ready_job) do + SolidQueue::Job.create!( + queue_name: "default", + class_name: "MyWorkerJob", + arguments: {}, + active_job_id: SecureRandom.uuid + ) + end + + let!(:failed_job) do + j = SolidQueue::Job.create!( + queue_name: "mailers", + class_name: "MyMailerJob", + arguments: {}, + active_job_id: SecureRandom.uuid + ) + j.ready_execution&.destroy + SolidQueue::FailedExecution.create!( + job: j, + error: { exception_class: "RuntimeError", message: "boom", backtrace: [] } + ) + j + end + + describe "GET /jobs/search" do + it "returns HTTP success" do + get "/jobs/search" + expect(response).to have_http_status(:ok) + end + + it "renders the search form" do + get "/jobs/search" + expect(response.body).to include("Search by job class name") + end + + it "shows no results section when query is blank" do + get "/jobs/search" + expect(response.body).not_to include("MyWorkerJob") + end + end + + describe "GET /jobs/search?q=" do + it "returns matches across statuses" do + get "/jobs/search", params: { q: "My" } + expect(response.body).to include("MyWorkerJob") + expect(response.body).to include("MyMailerJob") + end + + it "groups results by status" do + get "/jobs/search", params: { q: "My" } + expect(response.body).to include("ready") + expect(response.body).to include("failed") + end + + it "is case-insensitive" do + get "/jobs/search", params: { q: "myworker" } + expect(response.body).to include("MyWorkerJob") + end + + it "shows only matching jobs" do + get "/jobs/search", params: { q: "Worker" } + expect(response.body).to include("MyWorkerJob") + expect(response.body).not_to include("MyMailerJob") + end + + it "shows empty state when nothing matches" do + get "/jobs/search", params: { q: "NoSuchJob" } + expect(response.body).to include("No jobs found") + end + + it "links to the filtered jobs index for non-failed statuses" do + get "/jobs/search", params: { q: "Worker" } + expect(response.body).to include("status=ready") + end + + it "links to the failed jobs page for failed results" do + get "/jobs/search", params: { q: "Mailer" } + expect(response.body).to include("failed_jobs") + end + + it "links each job class name to its detail page" do + get "/jobs/search", params: { q: "Worker" } + expect(response.body).to include("/jobs/list/#{ready_job.id}") + end + + it "renders a clear link when search is active" do + get "/jobs/search", params: { q: "Worker" } + expect(response.body).to include("Clear") + end + end +end From f5cc44dec3c5b568be54978823066a8483db2200 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Tue, 19 May 2026 13:07:19 -0400 Subject: [PATCH 2/4] feat: autocomplete job class names on global search via datalist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queries all distinct class names from solid_queue_jobs and renders them as a native HTML on the search page. The browser filters suggestions as the user types — no JavaScript required, fully accessible. Co-Authored-By: Claude Sonnet 4.6 --- app/controllers/solid_queue_web/search_controller.rb | 5 +++-- app/views/solid_queue_web/search/index.html.erb | 9 ++++++++- spec/requests/solid_queue_web/search_spec.rb | 12 ++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/controllers/solid_queue_web/search_controller.rb b/app/controllers/solid_queue_web/search_controller.rb index 206ff32..5b3d46e 100644 --- a/app/controllers/solid_queue_web/search_controller.rb +++ b/app/controllers/solid_queue_web/search_controller.rb @@ -3,8 +3,9 @@ class SearchController < ApplicationController LIMIT = 25 def index - @query = params[:q].presence - @results = {} + @query = params[:q].presence + @job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name) + @results = {} return unless @query diff --git a/app/views/solid_queue_web/search/index.html.erb b/app/views/solid_queue_web/search/index.html.erb index 700ec4a..b3008e4 100644 --- a/app/views/solid_queue_web/search/index.html.erb +++ b/app/views/solid_queue_web/search/index.html.erb @@ -1,9 +1,16 @@

    Search Jobs

    + + <% @job_classes.each do |klass| %> + +