Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- A Batches page (and a "Part of" card on job detail) when Solid Queue 1.7+ is installed and the optional batches migration has run.

### Fixed

- A list of one job says "1 job" (and "1 failed job") instead of the plural.

## [1.2.0] - 2026-09-04

### Added
Expand Down
44 changes: 22 additions & 22 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ GEM
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
base64 (0.3.0)
bigdecimal (4.1.2)
bigdecimal (4.1.3)
builder (3.3.0)
capybara (3.40.0)
addressable
Expand All @@ -102,24 +102,24 @@ GEM
crass (1.0.7)
date (3.5.1)
drb (2.2.3)
erb (6.0.6)
erb (6.0.7)
erubi (1.13.1)
et-orbi (1.4.0)
et-orbi (1.4.2)
tzinfo
fugit (1.13.0)
fugit (1.14.0)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.4.0)
activesupport (>= 6.1)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
io-console (0.9.4)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.21.1)
json (2.21.2)
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
logger (1.7.0)
Expand Down Expand Up @@ -177,7 +177,7 @@ GEM
nio4r (~> 2.0)
raabro (1.5.0)
racc (1.8.1)
rack (3.2.6)
rack (3.2.7)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
Expand Down Expand Up @@ -217,7 +217,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.4.2)
rbs (4.1.0)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
Expand All @@ -227,7 +227,7 @@ GEM
rbs (>= 4.0.0)
tsort
regexp_parser (2.12.0)
reline (0.6.3)
reline (0.7.0)
io-console (~> 0.5)
rexml (3.4.4)
rubocop (1.88.2)
Expand Down Expand Up @@ -267,7 +267,7 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
solid_queue (1.5.0)
solid_queue (1.7.0)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
Expand Down Expand Up @@ -306,7 +306,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.8.2)
zeitwerk (2.8.3)

PLATFORMS
aarch64-linux-gnu
Expand Down Expand Up @@ -345,7 +345,7 @@ CHECKSUMS
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bundler (4.0.16) sha256=d6ca5dd440c24f9abce9844cf44cc8e18c6a553de65a47efb4544137af92c47d
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
Expand All @@ -354,15 +354,15 @@ CHECKSUMS
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
erb (6.0.6) sha256=a9b24986700f5bf127c4f297c5403c3ca41b83b0a316c0cd09a096b56e644ae5
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc
fugit (1.13.0) sha256=a4f093fce740da52f216740a5041e2a594ea763cdb89e8b2754ca4399634ab18
et-orbi (1.4.2) sha256=bb555dae668419cb24caa2a293a170e58be6d4df1e017c51f5030bdc133cd20c
fugit (1.14.0) sha256=e311a9cc85a54bb67649df45060e721282a1e2c8ebad40e41874a3a7d576f37e
globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
Expand Down Expand Up @@ -394,7 +394,7 @@ CHECKSUMS
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
Expand All @@ -404,10 +404,10 @@ CHECKSUMS
railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rbs (4.1.0) sha256=8baba59008b0643b4ba2090e9b1d0149655b0bbd42eb2ffe42b1d0eb6923fd72
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf
rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
Expand All @@ -418,7 +418,7 @@ CHECKSUMS
rubyzip (3.4.1) sha256=0a79e745b5c25872ebd148457df5665da8530ed8626c993b01457128f173ca02
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.46.0) sha256=cb6cfa6f79eac041749df0b14cdcb0e9fe5df3715a64c77bfaa56b345b99f957
solid_queue (1.5.0) sha256=329f29caab9cc68eefe489013eb5a38ea4a66708ed2bee301384dbed91890f7f
solid_queue (1.7.0) sha256=6566b70b801d1c317c81bba7bcdd5677c019afac584a30374b4164002ca356d3
solid_queue-flightdeck (1.2.0)
sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
Expand Down Expand Up @@ -447,7 +447,7 @@ CHECKSUMS
websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5

BUNDLED WITH
4.0.16
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ gem "solid_queue-flightdeck"
```

No migrations, no asset configuration. Requires Ruby >= 3.1, Rails >= 7.1 and
Solid Queue >= 1.0.
Solid Queue >= 1.0. The Batches page appears when the host is on Solid Queue
1.7+ **and** has run the optional batches migration (`bin/rails solid_queue:update`).
Hosts on older Solid Queue, or that have not migrated yet, see the rest of the
dashboard unchanged.

## Mounting

Expand Down
3 changes: 0 additions & 3 deletions app/assets/flightdeck/flightdeck-38ba36531cee.css

This file was deleted.

3 changes: 3 additions & 0 deletions app/assets/flightdeck/flightdeck-f31d22011660.css

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions app/assets/flightdeck/manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"flightdeck.css": {
"file": "flightdeck-38ba36531cee.css",
"digest": "38ba36531cee",
"sha256": "38ba36531cee83452b312ce04f858f83eeb262b498b6614c775e1947ba2303da",
"file": "flightdeck-f31d22011660.css",
"digest": "f31d22011660",
"sha256": "f31d22011660f83a4b07b3ee983ecc0380d0bc448b0e85c72f2e4e910260f950",
"content_type": "text/css; charset=utf-8",
"size": 31759
"size": 32428
},
"flightdeck.js": {
"file": "flightdeck-30004819780c.js",
Expand Down
37 changes: 37 additions & 0 deletions app/controllers/flightdeck/batches_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Flightdeck
class BatchesController < ApplicationController
rescue_from BatchesQuery::InvalidState, with: :unknown_state
rescue_from ActiveRecord::RecordNotFound, with: :batch_gone

before_action :require_batches!

def index
@query = BatchesQuery.new(state: params[:state], before_id: params[:before_id])
@rows = @query.rows
@state_counts = BatchesQuery.state_counts
end

def show
@batch = BatchDetail.find(params[:id])
@jobs_query = JobsQuery.new(batch_id: @batch.id, before_id: params[:before_id])
@rows = @jobs_query.rows
end

private
def require_batches!
return if BatchesQuery.available?

redirect_to root_path, alert: "Batches are not available on this Solid Queue installation."
end

def unknown_state
redirect_to batches_path, alert: "Unknown batch state."
end

def batch_gone
redirect_to batches_path, alert: "That batch no longer exists."
end
end
end
9 changes: 9 additions & 0 deletions app/helpers/flightdeck/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ def fd_nav_recurring_count
nav_count("recurring") { SolidQueue::RecurringTask.count }
end

def fd_nav_batch_count
return "—" unless Flightdeck::BatchesQuery.available?

query = Flightdeck::BatchesQuery.new
fd_count(query.count, capped: query.count_capped?)
rescue ActiveRecord::ActiveRecordError
"—"
end

def fd_nav_any_dead_processes?
nav_count("dead_processes") do
SolidQueue::Process.where(last_heartbeat_at: ..SolidQueue.process_alive_threshold.ago).exists?
Expand Down
55 changes: 55 additions & 0 deletions app/helpers/flightdeck/batches_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Flightdeck
module BatchesHelper
STATE_LABELS = {
all: "All",
unfinished: "Unfinished",
completed: "Completed",
failed: "Failed",
pending: "Pending",
enqueued: "Enqueued"
}.freeze

PILL_CLASSES = {
pending: "scheduled",
enqueued: "progress",
completed: "finished",
failed: "failed"
}.freeze

def fd_batch_state_label(state) = STATE_LABELS.fetch(state.to_sym, state.to_s.humanize)

def fd_batch_state_pill(state)
tag.span(fd_batch_state_label(state).upcase,
class: "fd-pill #{PILL_CLASSES.fetch(state.to_sym, "scheduled")}")
end

def fd_batch_progress_bar(row)
tag.svg(class: "fd-batch-bar", viewBox: "0 0 100 4", preserveAspectRatio: "none", "aria-hidden": true) do
safe_join([
tag.rect(class: "fd-depth-track", width: 100, height: 4),
(tag.rect(class: "fd-batch-fill-ok", width: row.completed_share, height: 4) if row.completed_share.positive?),
(tag.rect(class: "fd-batch-fill-bad", x: row.completed_share, width: row.failed_share, height: 4) if row.failed_share.positive?)
])
end
end

def fd_batch_progress_counts(row)
parts = [ "#{number_with_delimiter(row.completed_jobs)} completed" ]
parts << "#{number_with_delimiter(row.failed_jobs)} failed" if row.failed_jobs.positive?
parts << "#{number_with_delimiter(row.pending_jobs)} pending" if row.pending_jobs.positive?
"#{parts.join(" · ")} of #{number_with_delimiter(row.total_jobs)}"
end

def fd_current_batches_url
batches_path(request.query_parameters)
end

def fd_current_batch_url(batch)
batch_path(batch.id, request.query_parameters.slice("before_id"))
end

def dom_id_for_batch(id) = "fd-batch-#{id}"
end
end
4 changes: 4 additions & 0 deletions app/helpers/flightdeck/jobs_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def fd_current_list_url
jobs_path(request.query_parameters)
end

def fd_jobs_list_path(**extra)
jobs_path(list_filters.merge(state: params[:state].presence, **extra).compact)
end

def dom_id_for_job(id) = "fd-job-#{id}"

# Under an exception-class group header, repeating the class on every row is
Expand Down
47 changes: 47 additions & 0 deletions app/models/flightdeck/batch_detail.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module Flightdeck
# Everything the batch show page needs. Callback payloads are treated as
# JSON — they are never handed to ActiveJob to deserialize.
class BatchDetail < BatchRow
def self.find(id)
raise ActiveRecord::RecordNotFound, "Batches are not available" unless BatchesQuery.available?

batch = SolidQueue::Batch.find(id)
new(batch: batch, **BatchesQuery.progress_for([ batch ]).fetch(batch.id))
end

def metadata
batch.metadata
rescue StandardError
{}
end

def metadata? = metadata.present?

def metadata_json
JSON.pretty_generate(metadata)
rescue StandardError
metadata.to_s
end

def callbacks
[
[ "On finish", :on_finish ],
[ "On success", :on_success ],
[ "On failure", :on_failure ]
].filter_map do |label, name|
job_class = callback_class(name)
[ label, job_class ] if job_class.present?
end
end

private
def callback_class(name)
value = batch.public_send(name)
value["job_class"].presence if value.is_a?(Hash)
rescue StandardError
nil
end
end
end
Loading