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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"
gemspec

gem "puma"
gem "propshaft"

gem "sqlite3"

Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PATH
remote: .
specs:
solid_queue_web (0.4.0)
importmap-rails (>= 1.2)
pagy (>= 43.0)
rails (>= 8.1.3)
solid_queue (>= 1.0)
Expand Down Expand Up @@ -106,6 +107,10 @@ GEM
activesupport (>= 6.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
importmap-rails (2.2.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
Expand Down Expand Up @@ -156,6 +161,10 @@ GEM
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
propshaft (1.3.2)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
date
stringio
Expand Down Expand Up @@ -301,6 +310,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
propshaft
puma
rspec-rails
rubocop-rails-omakase
Expand Down Expand Up @@ -339,6 +349,7 @@ CHECKSUMS
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
Expand All @@ -363,6 +374,7 @@ CHECKSUMS
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
Expand Down
30 changes: 30 additions & 0 deletions app/assets/stylesheets/solid_queue_web/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,36 @@ tbody tr:hover { background: var(--bg); }
.sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
.sqd-row-actions form { display: inline; margin-left: 0.25rem; }

/* Search */
.sqd-search {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 1rem;
}

.sqd-search__input {
width: 280px;
padding: 0.35rem 0.75rem;
border: 1px solid var(--border);
border-radius: 5px;
font-size: 13px;
background: var(--surface);
color: var(--text);
line-height: 1.5;
}

.sqd-search__input:focus {
outline: 2px solid var(--primary);
outline-offset: -1px;
border-color: var(--primary);
}

@media (max-width: 640px) {
.sqd-search { flex-wrap: wrap; }
.sqd-search__input { width: 100%; }
}

/* Filters */
.sqd-filters {
display: flex;
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/solid_queue_web/jobs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module SolidQueueWeb
class JobsController < ApplicationController
STATUSES = %w[ready scheduled claimed blocked failed].freeze
DISCARDABLE = %w[ready scheduled blocked].freeze

before_action :set_status_and_queue, only: [ :destroy, :discard_all ]

STATUSES = %w[ready scheduled claimed blocked failed].freeze
DISCARDABLE = %w[ready scheduled blocked].freeze
EXECUTION_MODELS = {
"ready" => SolidQueue::ReadyExecution,
"scheduled" => SolidQueue::ScheduledExecution,
Expand All @@ -16,8 +15,10 @@ class JobsController < ApplicationController
def index
@status = params[:status].presence_in(STATUSES) || "ready"
@queue = params[:queue].presence
@search = params[:q].presence
@jobs = 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 Down
6 changes: 6 additions & 0 deletions app/javascript/solid_queue_web/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "@hotwired/turbo"
import { Application } from "@hotwired/stimulus"
import SearchController from "solid_queue_web/search_controller"

const application = Application.start()
application.register("search", SearchController)
11 changes: 11 additions & 0 deletions app/javascript/solid_queue_web/search_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
filter({ target }) {
clearTimeout(this._timer)
const len = target.value.length
if (len >= 4 || len === 0) {
this._timer = setTimeout(() => target.form.requestSubmit(), 300)
}
}
}
1 change: 1 addition & 0 deletions app/views/layouts/solid_queue_web/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= inline_styles %>
<%= javascript_importmap_tags "solid_queue_web" %>
</head>
<body>

Expand Down
26 changes: 20 additions & 6 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>

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

<div class="sqd-page-header">
<div class="sqd-filters">
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
<%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %>
<%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %>
<%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %>
<%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %>
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue, q: @search), class: @status == "ready" ? "active" : "" %>
<%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue, q: @search), class: @status == "scheduled" ? "active" : "" %>
<%= link_to "Running", jobs_path(status: "claimed", queue: @queue, q: @search), class: @status == "claimed" ? "active" : "" %>
<%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue, q: @search), class: @status == "blocked" ? "active" : "" %>
<%= link_to "Failed", jobs_path(status: "failed", queue: @queue, q: @search), class: @status == "failed" ? "active" : "" %>
</div>
<% if discardable && @jobs.any? %>
<div class="sqd-actions">
Expand All @@ -22,6 +22,20 @@
<% end %>
</div>

<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" %>
<% end %>
</form>

<div class="sqd-card" id="jobs-list">
<% if @jobs.empty? %>
<div class="sqd-empty">No <%= @status %> jobs.</div>
Expand Down
2 changes: 2 additions & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pin "solid_queue_web", to: "solid_queue_web/application.js"
pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js"
1 change: 1 addition & 0 deletions lib/solid_queue_web.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "solid_queue_web/version"
require "importmap-rails"
require "solid_queue_web/engine"

module SolidQueueWeb
Expand Down
13 changes: 13 additions & 0 deletions lib/solid_queue_web/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ class Engine < ::Rails::Engine

config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")

initializer "solid_queue_web.assets" do |app|
if app.config.respond_to?(:assets)
app.config.assets.paths << root.join("app/javascript")
end
end

initializer "solid_queue_web.importmap", before: "importmap" do |app|
if app.config.respond_to?(:importmap)
app.config.importmap.paths << root.join("config/importmap.rb")
app.config.importmap.cache_sweepers << root.join("app/javascript")
end
end

initializer "solid_queue_web.pagy" do
Pagy::OPTIONS[:limit] = 25
end
Expand Down
1 change: 1 addition & 0 deletions solid_queue_web.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "solid_queue", ">= 1.0"
spec.add_dependency "pagy", ">= 43.0"
spec.add_dependency "turbo-rails", ">= 2.0"
spec.add_dependency "importmap-rails", ">= 1.2"
end
2 changes: 2 additions & 0 deletions spec/dummy/config/importmap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
37 changes: 37 additions & 0 deletions spec/requests/solid_queue_web/jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@
end
end

describe "GET /jobs/list?q= (class name search)" do
let!(:other_job) do
SolidQueue::Job.create!(
queue_name: "default",
class_name: "MailerJob",
arguments: {},
active_job_id: SecureRandom.uuid
)
end

it "returns only jobs matching the search term" do
get "/jobs/list", params: { q: "Test" }
expect(response.body).to include("TestJob")
expect(response.body).not_to include("MailerJob")
end

it "is case-insensitive" do
get "/jobs/list", params: { q: "test" }
expect(response.body).to include("TestJob")
end

it "shows empty state when search matches nothing" do
get "/jobs/list", params: { q: "NoSuchJob" }
expect(response.body).to include("No ready jobs")
end

it "renders a clear link when search is active" do
get "/jobs/list", params: { q: "Test" }
expect(response.body).to include("Clear")
end

it "persists search term across status tab links" do
get "/jobs/list", params: { q: "Test" }
expect(response.body).to include("q=Test")
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