solid_queue_web 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -8
  3. data/app/assets/stylesheets/solid_queue_web/application.css +69 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +2 -0
  5. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +27 -0
  6. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -9
  7. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +21 -0
  8. data/app/controllers/solid_queue_web/jobs_controller.rb +17 -27
  9. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +63 -0
  10. data/app/controllers/solid_queue_web/search_controller.rb +23 -0
  11. data/app/javascript/solid_queue_web/application.js +4 -0
  12. data/app/javascript/solid_queue_web/refresh_controller.js +51 -0
  13. data/app/javascript/solid_queue_web/search_controller.js +5 -0
  14. data/app/javascript/solid_queue_web/selection_controller.js +42 -0
  15. data/app/models/solid_queue_web/job.rb +13 -0
  16. data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
  17. data/app/views/solid_queue_web/dashboard/index.html.erb +3 -1
  18. data/app/views/solid_queue_web/failed_jobs/index.html.erb +117 -43
  19. data/app/views/solid_queue_web/jobs/index.html.erb +116 -59
  20. data/app/views/solid_queue_web/jobs/show.html.erb +13 -8
  21. data/app/views/solid_queue_web/processes/index.html.erb +3 -1
  22. data/app/views/solid_queue_web/queues/jobs/destroy.turbo_stream.erb +9 -0
  23. data/app/views/solid_queue_web/queues/jobs/index.html.erb +89 -0
  24. data/app/views/solid_queue_web/search/index.html.erb +64 -0
  25. data/config/importmap.rb +2 -0
  26. data/config/routes.rb +14 -0
  27. data/lib/solid_queue_web/version.rb +1 -1
  28. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 313999fb3e2f6307b5f5e65bd8be047935bc04894d4bc1cfb94f2b87e945f9ac
4
- data.tar.gz: d107e00893b0f8cd655d096c59778bb171b1c10c4f6716d4ec88182957ba0dfc
3
+ metadata.gz: 69555db4ec8b786eb891721bf40655ef341c3b40b84b82b64cca69f446a511b8
4
+ data.tar.gz: eafe8f169f2382cdb3e0f59b87e3fc5553b1637dadb31ad076caf50233563d9d
5
5
  SHA512:
6
- metadata.gz: 796462cdaf3a6c1e40daf0fe0f9d398f9ae3e095ff22dadd2938330d2159f6cce468770582a114a52bf68de9f5eb05ce2336c8ca333bd5e3e8effa62d3f58707
7
- data.tar.gz: 6b30554ce77a3daf354848bd7c80eea666fe8dda09b487ec246ccd7ab47adbc9ae42e43f6c82c6a73da3918a9d93f3ee48a5bcc5c126aac49079d1b5de24e47b
6
+ metadata.gz: a1b63a6625cc58a9280ca325d829c8f395130f102494f03e346d84e8d8d769e2ebbbe69740df756a2b84a8f95ff04ca27b82c82e2d8924f78a952bc2e85a12d4
7
+ data.tar.gz: 0f723d410742b889756f36c6d5ddef8d8524733e1149bc55826afbaf37f3009c54c238a593495e8e097e37a8ca67988d25b06bf8507bf3903791f06a67907fbd
data/README.md CHANGED
@@ -17,7 +17,7 @@ Solid Queue ships without a web interface. When jobs fail, queues back up, or wo
17
17
  - Purpose-built for Solid Queue — uses its native models directly, no adapters
18
18
  - No external CSS framework — drops into any Rails app without asset conflicts
19
19
  - Zero-config to start — one line in `routes.rb` and you're running
20
- - Built for Rails 8 — Turbo Frames for in-place updates, Stimulus for dynamic search, Pagy for efficient pagination
20
+ - Built for Rails 8 — Turbo Frames for in-place updates, Stimulus for dynamic search and auto-refresh, Pagy for efficient pagination
21
21
  - Inspired by Sidekiq Web UI and the GoodJob dashboard, adapted for the Solid Queue ecosystem
22
22
 
23
23
  ## Real-world use case
@@ -33,14 +33,16 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
33
33
 
34
34
  ## Features
35
35
 
36
- - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes
37
- - **Queues** — all queues sorted by name
38
- - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; search by job class name with dynamic auto-submit; discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search
39
- - **Failed jobs** — list of failed executions with error details; retry or discard individually or in bulk
40
- - **Job detail** — full arguments, timestamps, and error backtrace; action buttons based on job status
41
- - **Queue management** — pause and resume individual queues
36
+ - **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; auto-refreshes every 5 seconds
37
+ - **Queues** — all queues sorted by name with size, latency, and pause/resume controls
38
+ - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; search by job class name with dynamic auto-submit; time-based period filter (1 h / 24 h / 7 d); discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search; auto-refreshes every 10 seconds
39
+ - **Failed jobs** — list of failed executions with error details; search by class name; filter by queue; time-based period filter; retry or discard individually or in bulk
40
+ - **Job detail** — full arguments, timestamps, blocked-until date, and error backtrace; action buttons based on job status
41
+ - **Queue management** — pause and resume individual queues; queue-scoped job list with status filter, search, and discard
42
42
  - **Recurring tasks** — all configured recurring tasks with cron schedule, next run time, last run time, and static/dynamic classification
43
- - **Processes** — workers, dispatchers, and supervisors with heartbeat health status
43
+ - **Processes** — workers, dispatchers, and supervisors with heartbeat health status; auto-refreshes every 10 seconds
44
+ - **Global search** — search across all job statuses at once by class name substring; results grouped by status with match count and direct links to filtered views; native datalist autocomplete pre-populated from all known job classes; auto-submits on selection
45
+ - **Targeted bulk actions** — checkboxes on the jobs and failed jobs lists for selecting individual rows; selection bar shows count and action buttons ("Discard Selected" for jobs, "Retry Selected" / "Discard Selected" for failed jobs); select-all checkbox in the table header
44
46
 
45
47
  ## Screenshots
46
48
 
@@ -302,6 +302,25 @@ tbody tr:hover { background: var(--bg); }
302
302
  .sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
303
303
  .sqd-row-actions form { display: inline; margin-left: 0.25rem; }
304
304
 
305
+ /* Selection bar */
306
+ .sqd-selection-bar {
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 0.75rem;
310
+ padding: 0.5rem 1rem;
311
+ background: var(--bg);
312
+ border-bottom: 1px solid var(--border);
313
+ font-size: 13px;
314
+ }
315
+
316
+ table th input[type="checkbox"],
317
+ table td input[type="checkbox"] {
318
+ width: 15px;
319
+ height: 15px;
320
+ cursor: pointer;
321
+ accent-color: var(--primary);
322
+ }
323
+
305
324
  /* Search */
306
325
  .sqd-search {
307
326
  display: flex;
@@ -332,6 +351,29 @@ tbody tr:hover { background: var(--bg); }
332
351
  .sqd-search__input { width: 100%; }
333
352
  }
334
353
 
354
+ .sqd-search--global { margin-bottom: 2rem; }
355
+
356
+ .sqd-search__input--lg {
357
+ width: 420px;
358
+ font-size: 15px;
359
+ padding: 0.5rem 1rem;
360
+ }
361
+
362
+ @media (max-width: 640px) {
363
+ .sqd-search__input--lg { width: 100%; }
364
+ }
365
+
366
+ .sqd-search-group {
367
+ margin-bottom: 2rem;
368
+ }
369
+
370
+ .sqd-search-group__header {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 0.75rem;
374
+ margin-bottom: 0.75rem;
375
+ }
376
+
335
377
  /* Filters */
336
378
  .sqd-filters {
337
379
  display: flex;
@@ -359,6 +401,33 @@ tbody tr:hover { background: var(--bg); }
359
401
  color: #fff;
360
402
  }
361
403
 
404
+ /* Period filter */
405
+ .sqd-period-filter {
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 0.25rem;
409
+ margin-left: auto;
410
+ }
411
+
412
+ .sqd-period-filter a {
413
+ padding: 0.2rem 0.55rem;
414
+ border-radius: 4px;
415
+ font-size: 11px;
416
+ font-weight: 500;
417
+ text-decoration: none;
418
+ border: 1px solid var(--border);
419
+ color: var(--muted);
420
+ background: var(--surface);
421
+ transition: all 0.1s;
422
+ }
423
+
424
+ .sqd-period-filter a:hover,
425
+ .sqd-period-filter a.active {
426
+ background: var(--muted);
427
+ border-color: var(--muted);
428
+ color: #fff;
429
+ }
430
+
362
431
  /* Code / monospace */
363
432
  .sqd-mono {
364
433
  font-family: "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
@@ -2,6 +2,8 @@ module SolidQueueWeb
2
2
  class ApplicationController < ActionController::Base
3
3
  include Pagy::Method
4
4
 
5
+ PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
6
+
5
7
  before_action :authenticate!
6
8
 
7
9
  private
@@ -0,0 +1,27 @@
1
+ module SolidQueueWeb
2
+ module FailedJobs
3
+ class SelectionsController < ApplicationController
4
+ def create
5
+ ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
6
+ executions = SolidQueue::FailedExecution.where(id: ids)
7
+ jobs = executions.includes(:job).map(&:job)
8
+ SolidQueue::FailedExecution.retry_all(jobs)
9
+ redirect_to failed_jobs_path,
10
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
11
+ rescue => e
12
+ redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
13
+ end
14
+
15
+ def destroy
16
+ ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
17
+ executions = SolidQueue::FailedExecution.where(id: ids)
18
+ jobs = executions.includes(:job).map(&:job)
19
+ SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
20
+ redirect_to failed_jobs_path,
21
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
22
+ rescue => e
23
+ redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,9 +1,9 @@
1
1
  module SolidQueueWeb
2
2
  class FailedJobsController < ApplicationController
3
+ before_action :set_filter_params, only: [ :index, :retry_all, :discard_all ]
4
+
3
5
  def index
4
- @pagy, @failed_jobs = pagy(
5
- SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
6
- )
6
+ @pagy, @failed_jobs = pagy(filtered_scope.order(created_at: :desc))
7
7
  end
8
8
 
9
9
  def retry
@@ -23,16 +23,33 @@ module SolidQueueWeb
23
23
  end
24
24
 
25
25
  def retry_all
26
- executions = SolidQueue::FailedExecution.includes(:job).to_a
27
- jobs = executions.map(&:job)
26
+ jobs = filtered_scope.map(&:job)
28
27
  SolidQueue::FailedExecution.retry_all(jobs)
29
- redirect_to failed_jobs_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
28
+ redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
29
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
30
30
  end
31
31
 
32
32
  def discard_all
33
- count = SolidQueue::FailedExecution.count
34
- SolidQueue::FailedExecution.discard_all_in_batches
35
- redirect_to failed_jobs_path, notice: "#{count} #{"job".pluralize(count)} discarded."
33
+ jobs = filtered_scope.map(&:job)
34
+ SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
35
+ redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
36
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
37
+ end
38
+
39
+ private
40
+
41
+ def set_filter_params
42
+ @queue = params[:queue].presence
43
+ @search = params[:q].presence
44
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
45
+ end
46
+
47
+ def filtered_scope
48
+ scope = SolidQueue::FailedExecution.includes(:job)
49
+ scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
50
+ scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
51
+ scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
52
+ scope
36
53
  end
37
54
  end
38
55
  end
@@ -0,0 +1,21 @@
1
+ module SolidQueueWeb
2
+ module Jobs
3
+ class SelectionsController < ApplicationController
4
+ def destroy
5
+ status = params[:status]
6
+ period = params[:period].presence_in(PERIOD_DURATIONS.keys)
7
+ raise ArgumentError, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
8
+ model = Job::EXECUTION_MODELS[status]
9
+ ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
10
+ jobs = model.where(id: ids).includes(:job).map(&:job)
11
+ model.discard_all_from_jobs(jobs)
12
+ redirect_to jobs_path(status: status, period: period),
13
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
14
+ rescue ArgumentError => e
15
+ redirect_to jobs_path(status: status), alert: e.message
16
+ rescue => e
17
+ redirect_to jobs_path(status: status), alert: "Could not discard jobs: #{e.message}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,24 +1,14 @@
1
1
  module SolidQueueWeb
2
2
  class JobsController < ApplicationController
3
- before_action :set_status_and_queue, only: [ :destroy, :discard_all ]
4
-
5
- STATUSES = %w[ready scheduled claimed blocked failed].freeze
6
- DISCARDABLE = %w[ready scheduled blocked].freeze
7
- EXECUTION_MODELS = {
8
- "ready" => SolidQueue::ReadyExecution,
9
- "scheduled" => SolidQueue::ScheduledExecution,
10
- "claimed" => SolidQueue::ClaimedExecution,
11
- "blocked" => SolidQueue::BlockedExecution,
12
- "failed" => SolidQueue::FailedExecution
13
- }.freeze
3
+ before_action :set_status, only: [ :destroy, :discard_all, :discard_selected ]
14
4
 
15
5
  def index
16
- @status = params[:status].presence_in(STATUSES) || "ready"
17
- @queue = params[:queue].presence
6
+ @status = params[:status].presence_in(Job::STATUSES) || "ready"
18
7
  @search = params[:q].presence
19
- @jobs = EXECUTION_MODELS[@status].includes(:job)
20
- @jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
8
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
9
+ @jobs = Job::EXECUTION_MODELS[@status].includes(:job)
21
10
  @jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
11
+ @jobs = @jobs.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
22
12
  @pagy, @jobs = pagy(@jobs.order(created_at: :desc))
23
13
  end
24
14
 
@@ -26,7 +16,6 @@ module SolidQueueWeb
26
16
  @job = SolidQueue::Job
27
17
  .includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
28
18
  .find(params[:id])
29
- @failed_execution = @job.failed_execution
30
19
  @execution_status = derive_status(@job)
31
20
  end
32
21
 
@@ -37,24 +26,24 @@ module SolidQueueWeb
37
26
  @remaining_count = filtered_scope(model).count
38
27
  respond_to do |format|
39
28
  format.turbo_stream
40
- format.html { redirect_to jobs_path(status: @status, queue: @queue), notice: "Job discarded." }
29
+ format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
41
30
  end
42
31
  rescue ArgumentError => e
43
- redirect_to jobs_path(status: @status, queue: @queue), alert: e.message
32
+ redirect_to jobs_path(status: @status, period: @period), alert: e.message
44
33
  rescue => e
45
- redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard job: #{e.message}"
34
+ redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
46
35
  end
47
36
 
48
37
  def discard_all
49
38
  model = execution_model_for!(@status)
50
39
  jobs = filtered_scope(model).map(&:job)
51
40
  model.discard_all_from_jobs(jobs)
52
- redirect_to jobs_path(status: @status, queue: @queue),
41
+ redirect_to jobs_path(status: @status, period: @period),
53
42
  notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
54
43
  rescue ArgumentError => e
55
- redirect_to jobs_path(status: @status, queue: @queue), alert: e.message
44
+ redirect_to jobs_path(status: @status, period: @period), alert: e.message
56
45
  rescue => e
57
- redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard jobs: #{e.message}"
46
+ redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
58
47
  end
59
48
 
60
49
  private
@@ -68,19 +57,20 @@ module SolidQueueWeb
68
57
  "finished"
69
58
  end
70
59
 
71
- def set_status_and_queue
60
+ def set_status
72
61
  @status = params[:status]
73
- @queue = params[:queue].presence
62
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
74
63
  end
75
64
 
76
65
  def filtered_scope(model)
77
66
  scope = model.includes(:job)
78
- @queue.present? ? scope.where(jobs: { queue_name: @queue }) : scope
67
+ scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
68
+ scope
79
69
  end
80
70
 
81
71
  def execution_model_for!(status)
82
- raise ArgumentError, "Cannot discard #{status} jobs from this page." unless DISCARDABLE.include?(status)
83
- EXECUTION_MODELS[status]
72
+ raise ArgumentError, "Cannot discard #{status} jobs from this page." unless Job::DISCARDABLE.include?(status)
73
+ Job::EXECUTION_MODELS[status]
84
74
  end
85
75
  end
86
76
  end
@@ -0,0 +1,63 @@
1
+ module SolidQueueWeb
2
+ module Queues
3
+ class JobsController < ApplicationController
4
+ before_action :set_queue
5
+ before_action :set_status, only: [ :destroy, :discard_all ]
6
+
7
+ def index
8
+ @status = params[:status].presence_in(Job::STATUSES) || "ready"
9
+ @search = params[:q].presence
10
+ @jobs = Job::EXECUTION_MODELS[@status].includes(:job)
11
+ .where(solid_queue_jobs: { queue_name: @queue })
12
+ @jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
13
+ @pagy, @jobs = pagy(@jobs.order(created_at: :desc))
14
+ end
15
+
16
+ def destroy
17
+ model = execution_model_for!(@status)
18
+ @execution = model.find(params[:id])
19
+ @execution.discard
20
+ @remaining_count = filtered_scope(model).count
21
+ respond_to do |format|
22
+ format.turbo_stream
23
+ format.html { redirect_to queue_jobs_path(queue_name: @queue, status: @status), notice: "Job discarded." }
24
+ end
25
+ rescue ArgumentError => e
26
+ redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
27
+ rescue => e
28
+ redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard job: #{e.message}"
29
+ end
30
+
31
+ def discard_all
32
+ model = execution_model_for!(@status)
33
+ jobs = filtered_scope(model).map(&:job)
34
+ model.discard_all_from_jobs(jobs)
35
+ redirect_to queue_jobs_path(queue_name: @queue, status: @status),
36
+ notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
37
+ rescue ArgumentError => e
38
+ redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: e.message
39
+ rescue => e
40
+ redirect_to queue_jobs_path(queue_name: @queue, status: @status), alert: "Could not discard jobs: #{e.message}"
41
+ end
42
+
43
+ private
44
+
45
+ def set_queue
46
+ @queue = params[:queue_name]
47
+ end
48
+
49
+ def set_status
50
+ @status = params[:status]
51
+ end
52
+
53
+ def filtered_scope(model)
54
+ model.includes(:job).where(solid_queue_jobs: { queue_name: @queue })
55
+ end
56
+
57
+ def execution_model_for!(status)
58
+ raise ArgumentError, "Cannot discard #{status} jobs from this page." unless Job::DISCARDABLE.include?(status)
59
+ Job::EXECUTION_MODELS[status]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,23 @@
1
+ module SolidQueueWeb
2
+ class SearchController < ApplicationController
3
+ LIMIT = 25
4
+
5
+ def index
6
+ @query = params[:q].presence
7
+ @job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
8
+ @results = {}
9
+
10
+ return unless @query
11
+
12
+ Job::EXECUTION_MODELS.each do |status, model|
13
+ scope = model.includes(:job)
14
+ .references(:job)
15
+ .where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%")
16
+ .order(created_at: :desc)
17
+ total = scope.count
18
+ executions = scope.limit(LIMIT).to_a
19
+ @results[status] = { executions: executions, total: total } unless executions.empty?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,6 +1,10 @@
1
1
  import "@hotwired/turbo"
2
2
  import { Application } from "@hotwired/stimulus"
3
3
  import SearchController from "solid_queue_web/search_controller"
4
+ import RefreshController from "solid_queue_web/refresh_controller"
5
+ import SelectionController from "solid_queue_web/selection_controller"
4
6
 
5
7
  const application = Application.start()
6
8
  application.register("search", SearchController)
9
+ application.register("refresh", RefreshController)
10
+ application.register("selection", SelectionController)
@@ -0,0 +1,51 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { interval: { type: Number, default: 5000 } }
5
+
6
+ initialize() {
7
+ this._onVisibilityChange = this._onVisibilityChange.bind(this)
8
+ }
9
+
10
+ connect() {
11
+ document.addEventListener("visibilitychange", this._onVisibilityChange)
12
+ this._schedule()
13
+ }
14
+
15
+ disconnect() {
16
+ clearTimeout(this._timer)
17
+ document.removeEventListener("visibilitychange", this._onVisibilityChange)
18
+ }
19
+
20
+ _schedule() {
21
+ this._timer = setTimeout(() => this._reload(), this.intervalValue)
22
+ }
23
+
24
+ async _reload() {
25
+ clearTimeout(this._timer)
26
+ if (!document.hidden) {
27
+ try {
28
+ const response = await fetch(window.location.href, {
29
+ headers: { "Turbo-Frame": this.element.id, Accept: "text/html" }
30
+ })
31
+ if (response.ok) {
32
+ const html = await response.text()
33
+ const doc = new DOMParser().parseFromString(html, "text/html")
34
+ const frame = doc.querySelector(`turbo-frame#${this.element.id}`)
35
+ if (frame && this.element.isConnected) this.element.innerHTML = frame.innerHTML
36
+ }
37
+ } catch {
38
+ // network error — skip this tick
39
+ }
40
+ }
41
+ if (this.element.isConnected) this._schedule()
42
+ }
43
+
44
+ _onVisibilityChange() {
45
+ if (document.hidden) {
46
+ clearTimeout(this._timer)
47
+ } else {
48
+ this._reload()
49
+ }
50
+ }
51
+ }
@@ -8,4 +8,9 @@ export default class extends Controller {
8
8
  this._timer = setTimeout(() => target.form.requestSubmit(), 300)
9
9
  }
10
10
  }
11
+
12
+ select({ target }) {
13
+ clearTimeout(this._timer)
14
+ target.form.requestSubmit()
15
+ }
11
16
  }
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox", "selectAll", "bar", "count"]
5
+
6
+ toggle() {
7
+ this._update()
8
+ }
9
+
10
+ selectAll({ target }) {
11
+ this.checkboxTargets.forEach(cb => cb.checked = target.checked)
12
+ this._update()
13
+ }
14
+
15
+ submit({ params: { formId } }) {
16
+ const form = document.getElementById(formId)
17
+ if (!form) return
18
+ form.querySelectorAll("[data-injected-id]").forEach(el => el.remove())
19
+ this.checkboxTargets
20
+ .filter(cb => cb.checked)
21
+ .forEach(cb => {
22
+ const input = document.createElement("input")
23
+ input.type = "hidden"
24
+ input.name = "ids[]"
25
+ input.value = cb.value
26
+ input.dataset.injectedId = true
27
+ form.appendChild(input)
28
+ })
29
+ form.requestSubmit()
30
+ }
31
+
32
+ _update() {
33
+ const checked = this.checkboxTargets.filter(cb => cb.checked).length
34
+ const total = this.checkboxTargets.length
35
+ if (this.hasBarTarget) this.barTarget.style.display = checked > 0 ? "" : "none"
36
+ if (this.hasCountTarget) this.countTarget.textContent = checked
37
+ if (this.hasSelectAllTarget) {
38
+ this.selectAllTarget.indeterminate = checked > 0 && checked < total
39
+ this.selectAllTarget.checked = total > 0 && checked === total
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,13 @@
1
+ module SolidQueueWeb
2
+ class Job
3
+ STATUSES = %w[ready scheduled claimed blocked failed].freeze
4
+ DISCARDABLE = %w[ready scheduled blocked].freeze
5
+ EXECUTION_MODELS = {
6
+ "ready" => SolidQueue::ReadyExecution,
7
+ "scheduled" => SolidQueue::ScheduledExecution,
8
+ "claimed" => SolidQueue::ClaimedExecution,
9
+ "blocked" => SolidQueue::BlockedExecution,
10
+ "failed" => SolidQueue::FailedExecution
11
+ }.freeze
12
+ end
13
+ end
@@ -29,6 +29,7 @@
29
29
  <li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
30
30
  <li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
31
31
  <li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
32
+ <li><%= link_to "Search", search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %></li>
32
33
  </ul>
33
34
  </nav>
34
35
  </div>
@@ -1,3 +1,4 @@
1
+ <%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: 5000 } do %>
1
2
  <h1 class="sqd-page-title">Dashboard</h1>
2
3
 
3
4
  <div class="sqd-stats">
@@ -62,4 +63,5 @@
62
63
  </div>
63
64
  </div>
64
65
  <% end %>
65
- </div>
66
+ </div>
67
+ <% end %>