solid_queue_web 0.5.5 → 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.
- checksums.yaml +4 -4
- data/README.md +10 -8
- data/app/assets/stylesheets/solid_queue_web/application.css +69 -0
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +27 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +4 -2
- data/app/controllers/solid_queue_web/jobs/selections_controller.rb +21 -0
- data/app/controllers/solid_queue_web/jobs_controller.rb +13 -8
- data/app/controllers/solid_queue_web/search_controller.rb +23 -0
- data/app/javascript/solid_queue_web/application.js +4 -0
- data/app/javascript/solid_queue_web/refresh_controller.js +51 -0
- data/app/javascript/solid_queue_web/search_controller.js +5 -0
- data/app/javascript/solid_queue_web/selection_controller.js +42 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
- data/app/views/solid_queue_web/dashboard/index.html.erb +3 -1
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +93 -48
- data/app/views/solid_queue_web/jobs/index.html.erb +115 -48
- data/app/views/solid_queue_web/processes/index.html.erb +3 -1
- data/app/views/solid_queue_web/search/index.html.erb +64 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +9 -0
- data/lib/solid_queue_web/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69555db4ec8b786eb891721bf40655ef341c3b40b84b82b64cca69f446a511b8
|
|
4
|
+
data.tar.gz: eafe8f169f2382cdb3e0f59b87e3fc5553b1637dadb31ad076caf50233563d9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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;
|
|
@@ -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
|
|
@@ -25,14 +25,14 @@ module SolidQueueWeb
|
|
|
25
25
|
def retry_all
|
|
26
26
|
jobs = filtered_scope.map(&:job)
|
|
27
27
|
SolidQueue::FailedExecution.retry_all(jobs)
|
|
28
|
-
redirect_to failed_jobs_path(queue: @queue, q: @search),
|
|
28
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
29
29
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def discard_all
|
|
33
33
|
jobs = filtered_scope.map(&:job)
|
|
34
34
|
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
35
|
-
redirect_to failed_jobs_path(queue: @queue, q: @search),
|
|
35
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
36
36
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -41,12 +41,14 @@ module SolidQueueWeb
|
|
|
41
41
|
def set_filter_params
|
|
42
42
|
@queue = params[:queue].presence
|
|
43
43
|
@search = params[:q].presence
|
|
44
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def filtered_scope
|
|
47
48
|
scope = SolidQueue::FailedExecution.includes(:job)
|
|
48
49
|
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
|
|
49
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?
|
|
50
52
|
scope
|
|
51
53
|
end
|
|
52
54
|
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,12 +1,14 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_status, only: [ :destroy, :discard_all ]
|
|
3
|
+
before_action :set_status, only: [ :destroy, :discard_all, :discard_selected ]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
6
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
7
7
|
@search = params[:q].presence
|
|
8
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
8
9
|
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
9
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?
|
|
10
12
|
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
|
|
11
13
|
end
|
|
12
14
|
|
|
@@ -24,24 +26,24 @@ module SolidQueueWeb
|
|
|
24
26
|
@remaining_count = filtered_scope(model).count
|
|
25
27
|
respond_to do |format|
|
|
26
28
|
format.turbo_stream
|
|
27
|
-
format.html { redirect_to jobs_path(status: @status), notice: "Job discarded." }
|
|
29
|
+
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
|
|
28
30
|
end
|
|
29
31
|
rescue ArgumentError => e
|
|
30
|
-
redirect_to jobs_path(status: @status), alert: e.message
|
|
32
|
+
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
31
33
|
rescue => e
|
|
32
|
-
redirect_to jobs_path(status: @status), alert: "Could not discard job: #{e.message}"
|
|
34
|
+
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def discard_all
|
|
36
38
|
model = execution_model_for!(@status)
|
|
37
39
|
jobs = filtered_scope(model).map(&:job)
|
|
38
40
|
model.discard_all_from_jobs(jobs)
|
|
39
|
-
redirect_to jobs_path(status: @status),
|
|
41
|
+
redirect_to jobs_path(status: @status, period: @period),
|
|
40
42
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
41
43
|
rescue ArgumentError => e
|
|
42
|
-
redirect_to jobs_path(status: @status), alert: e.message
|
|
44
|
+
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
43
45
|
rescue => e
|
|
44
|
-
redirect_to jobs_path(status: @status), alert: "Could not discard jobs: #{e.message}"
|
|
46
|
+
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
private
|
|
@@ -57,10 +59,13 @@ module SolidQueueWeb
|
|
|
57
59
|
|
|
58
60
|
def set_status
|
|
59
61
|
@status = params[:status]
|
|
62
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def filtered_scope(model)
|
|
63
|
-
model.includes(:job)
|
|
66
|
+
scope = model.includes(:job)
|
|
67
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
68
|
+
scope
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def execution_model_for!(status)
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 %>
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
<div class="sqd-actions">
|
|
5
5
|
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
6
6
|
method: :post,
|
|
7
|
-
params: { queue: @queue, q: @search },
|
|
7
|
+
params: { queue: @queue, q: @search, period: @period },
|
|
8
8
|
class: "sqd-btn sqd-btn--primary",
|
|
9
9
|
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
|
|
10
10
|
<%= button_to "Discard All", discard_all_failed_jobs_path,
|
|
11
11
|
method: :post,
|
|
12
|
-
params: { queue: @queue, q: @search },
|
|
12
|
+
params: { queue: @queue, q: @search, period: @period },
|
|
13
13
|
class: "sqd-btn sqd-btn--danger",
|
|
14
14
|
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
|
|
15
15
|
</div>
|
|
@@ -20,69 +20,114 @@
|
|
|
20
20
|
<% if @queue.present? %>
|
|
21
21
|
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
22
22
|
<% end %>
|
|
23
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
23
24
|
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
24
25
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
25
26
|
data-action="input->search#filter">
|
|
26
27
|
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
27
28
|
<% if @search.present? %>
|
|
28
|
-
<%= link_to "Clear", failed_jobs_path(queue: @queue), class: "sqd-btn sqd-btn--muted" %>
|
|
29
|
+
<%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
29
30
|
<% end %>
|
|
31
|
+
<div class="sqd-period-filter" role="group" aria-label="Time period">
|
|
32
|
+
<%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
|
|
33
|
+
<%= 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" %>
|
|
34
|
+
<%= 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" %>
|
|
35
|
+
<%= 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" %>
|
|
36
|
+
</div>
|
|
30
37
|
</form>
|
|
31
38
|
|
|
32
39
|
<% if @pagy.last > 1 %>
|
|
33
40
|
<%= @pagy.series_nav.html_safe %>
|
|
34
41
|
<% end %>
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
<% if @failed_jobs.any? %>
|
|
44
|
+
<div data-controller="selection">
|
|
45
|
+
<%= form_tag failed_job_selection_path, method: :post, id: "retry-selection-form" do %>
|
|
46
|
+
<%= hidden_field_tag :queue, @queue %>
|
|
47
|
+
<%= hidden_field_tag :q, @search %>
|
|
48
|
+
<%= hidden_field_tag :period, @period %>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<%= form_tag failed_job_selection_path, method: :delete, id: "discard-selection-form",
|
|
52
|
+
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
|
|
53
|
+
<%= hidden_field_tag :queue, @queue %>
|
|
54
|
+
<%= hidden_field_tag :q, @search %>
|
|
55
|
+
<%= hidden_field_tag :period, @period %>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
|
|
59
|
+
<span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
|
|
60
|
+
<button type="button" class="sqd-btn sqd-btn--primary sqd-btn--sm"
|
|
61
|
+
data-action="click->selection#submit"
|
|
62
|
+
data-selection-form-id-param="retry-selection-form">Retry Selected</button>
|
|
63
|
+
<button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
|
|
64
|
+
data-action="click->selection#submit"
|
|
65
|
+
data-selection-form-id-param="discard-selection-form">Discard Selected</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="sqd-card">
|
|
69
|
+
<table>
|
|
70
|
+
<thead>
|
|
53
71
|
<tr>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<% else %>
|
|
65
|
-
<span style="color:var(--muted)">—</span>
|
|
66
|
-
<% end %>
|
|
67
|
-
</td>
|
|
68
|
-
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
69
|
-
<td class="sqd-row-actions">
|
|
70
|
-
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
|
|
71
|
-
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
72
|
-
<%= button_to "Discard", failed_job_path(execution), method: :delete,
|
|
73
|
-
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
74
|
-
data: { confirm: "Discard this job?" } %>
|
|
75
|
-
</td>
|
|
72
|
+
<th scope="col">
|
|
73
|
+
<input type="checkbox" data-selection-target="selectAll"
|
|
74
|
+
data-action="change->selection#selectAll"
|
|
75
|
+
aria-label="Select all failed jobs">
|
|
76
|
+
</th>
|
|
77
|
+
<th scope="col">Job Class</th>
|
|
78
|
+
<th scope="col">Queue</th>
|
|
79
|
+
<th scope="col">Error</th>
|
|
80
|
+
<th scope="col">Failed At</th>
|
|
81
|
+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
76
82
|
</tr>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
</thead>
|
|
84
|
+
<tbody>
|
|
85
|
+
<% @failed_jobs.each do |execution| %>
|
|
86
|
+
<% job = execution.job %>
|
|
87
|
+
<tr>
|
|
88
|
+
<td>
|
|
89
|
+
<input type="checkbox" value="<%= execution.id %>"
|
|
90
|
+
data-selection-target="checkbox"
|
|
91
|
+
data-action="change->selection#toggle"
|
|
92
|
+
aria-label="Select job <%= job.class_name %>">
|
|
93
|
+
</td>
|
|
94
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
95
|
+
<td>
|
|
96
|
+
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
|
|
97
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
98
|
+
</td>
|
|
99
|
+
<td>
|
|
100
|
+
<% if execution.exception_class.present? %>
|
|
101
|
+
<div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
|
|
102
|
+
<strong><%= execution.exception_class %></strong>: <%= execution.message %>
|
|
103
|
+
</div>
|
|
104
|
+
<% else %>
|
|
105
|
+
<span style="color:var(--muted)">—</span>
|
|
106
|
+
<% end %>
|
|
107
|
+
</td>
|
|
108
|
+
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
109
|
+
<td class="sqd-row-actions">
|
|
110
|
+
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
|
|
111
|
+
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
112
|
+
<%= button_to "Discard", failed_job_path(execution), method: :delete,
|
|
113
|
+
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
114
|
+
data: { confirm: "Discard this job?" } %>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
<% end %>
|
|
118
|
+
</tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<% else %>
|
|
123
|
+
<div class="sqd-card">
|
|
124
|
+
<div class="sqd-empty">No failed jobs. All clear!</div>
|
|
125
|
+
</div>
|
|
126
|
+
<% end %>
|
|
82
127
|
|
|
83
128
|
<% if @queue.present? %>
|
|
84
129
|
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
85
130
|
Filtering by queue: <strong><%= @queue %></strong> —
|
|
86
|
-
<%= link_to "Clear filter", failed_jobs_path(q: @search) %>
|
|
131
|
+
<%= link_to "Clear filter", failed_jobs_path(q: @search, period: @period) %>
|
|
87
132
|
</p>
|
|
88
133
|
<% end %>
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>
|
|
2
2
|
|
|
3
|
-
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
|
|
3
|
+
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: 10000 } do %>
|
|
4
4
|
<% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
|
|
5
5
|
|
|
6
6
|
<div class="sqd-page-header">
|
|
7
7
|
<div class="sqd-filters">
|
|
8
|
-
<%= link_to "Ready", jobs_path(status: "ready", q: @search), class: @status == "ready" ? "active" : "" %>
|
|
9
|
-
<%= link_to "Scheduled", jobs_path(status: "scheduled", q: @search), class: @status == "scheduled" ? "active" : "" %>
|
|
10
|
-
<%= link_to "Running", jobs_path(status: "claimed", q: @search), class: @status == "claimed" ? "active" : "" %>
|
|
11
|
-
<%= link_to "Blocked", jobs_path(status: "blocked", q: @search), class: @status == "blocked" ? "active" : "" %>
|
|
12
|
-
<%= link_to "Failed", jobs_path(status: "failed", q: @search), class: @status == "failed" ? "active" : "" %>
|
|
8
|
+
<%= link_to "Ready", jobs_path(status: "ready", q: @search, period: @period), class: @status == "ready" ? "active" : "" %>
|
|
9
|
+
<%= link_to "Scheduled", jobs_path(status: "scheduled", q: @search, period: @period), class: @status == "scheduled" ? "active" : "" %>
|
|
10
|
+
<%= link_to "Running", jobs_path(status: "claimed", q: @search, period: @period), class: @status == "claimed" ? "active" : "" %>
|
|
11
|
+
<%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period), class: @status == "blocked" ? "active" : "" %>
|
|
12
|
+
<%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period), class: @status == "failed" ? "active" : "" %>
|
|
13
13
|
</div>
|
|
14
14
|
<% if discardable && @jobs.any? %>
|
|
15
15
|
<div class="sqd-actions">
|
|
16
16
|
<%= button_to "Discard All", discard_all_jobs_path,
|
|
17
17
|
method: :post,
|
|
18
|
-
params: { status: @status },
|
|
18
|
+
params: { status: @status, period: @period },
|
|
19
19
|
class: "sqd-btn sqd-btn--danger",
|
|
20
20
|
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
|
|
21
21
|
</div>
|
|
@@ -24,62 +24,129 @@
|
|
|
24
24
|
|
|
25
25
|
<form class="sqd-search" action="<%= jobs_path %>" method="get" data-controller="search">
|
|
26
26
|
<input type="hidden" name="status" value="<%= @status %>">
|
|
27
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
27
28
|
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
28
29
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
29
30
|
data-action="input->search#filter">
|
|
30
31
|
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
31
32
|
<% if @search.present? %>
|
|
32
|
-
<%= link_to "Clear", jobs_path(status: @status), class: "sqd-btn sqd-btn--muted" %>
|
|
33
|
+
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
33
34
|
<% end %>
|
|
35
|
+
<div class="sqd-period-filter" role="group" aria-label="Time period">
|
|
36
|
+
<%= link_to "All", jobs_path(status: @status, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
|
|
37
|
+
<%= 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" %>
|
|
38
|
+
<%= 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" %>
|
|
39
|
+
<%= 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" %>
|
|
40
|
+
</div>
|
|
34
41
|
</form>
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<tr
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
<% if discardable && @jobs.any? %>
|
|
44
|
+
<div data-controller="selection">
|
|
45
|
+
<%= form_tag job_selection_path, method: :delete, id: "job-selection-form",
|
|
46
|
+
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
|
|
47
|
+
<%= hidden_field_tag :status, @status %>
|
|
48
|
+
<%= hidden_field_tag :period, @period %>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
|
|
52
|
+
<span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
|
|
53
|
+
<button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
|
|
54
|
+
data-action="click->selection#submit"
|
|
55
|
+
data-selection-form-id-param="job-selection-form">Discard Selected</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="sqd-card" id="jobs-list">
|
|
59
|
+
<table>
|
|
60
|
+
<thead>
|
|
61
|
+
<tr>
|
|
62
|
+
<th scope="col">
|
|
63
|
+
<input type="checkbox" data-selection-target="selectAll"
|
|
64
|
+
data-action="change->selection#selectAll"
|
|
65
|
+
aria-label="Select all jobs">
|
|
66
|
+
</th>
|
|
67
|
+
<th scope="col">Job Class</th>
|
|
68
|
+
<th scope="col">Queue</th>
|
|
69
|
+
<th scope="col">Priority</th>
|
|
70
|
+
<th scope="col">Scheduled At</th>
|
|
71
|
+
<th scope="col">Enqueued At</th>
|
|
72
|
+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
73
|
+
</tr>
|
|
74
|
+
</thead>
|
|
75
|
+
<tbody>
|
|
76
|
+
<% @jobs.each do |execution| %>
|
|
77
|
+
<% job = execution.job %>
|
|
78
|
+
<tr id="execution_<%= execution.id %>">
|
|
79
|
+
<td>
|
|
80
|
+
<input type="checkbox" value="<%= execution.id %>"
|
|
81
|
+
data-selection-target="checkbox"
|
|
82
|
+
data-action="change->selection#toggle"
|
|
83
|
+
aria-label="Select job <%= job.class_name %>">
|
|
84
|
+
</td>
|
|
85
|
+
<td>
|
|
86
|
+
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
87
|
+
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
88
|
+
</td>
|
|
89
|
+
<td>
|
|
90
|
+
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
|
|
91
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
92
|
+
</td>
|
|
93
|
+
<td><%= job.priority %></td>
|
|
94
|
+
<td class="sqd-mono">
|
|
95
|
+
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
96
|
+
</td>
|
|
97
|
+
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
69
98
|
<td class="sqd-row-actions">
|
|
70
99
|
<%= button_to "Discard", job_path(execution),
|
|
71
100
|
method: :delete,
|
|
72
|
-
params: { status: @status },
|
|
101
|
+
params: { status: @status, period: @period },
|
|
73
102
|
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
74
103
|
data: { confirm: "Discard this job?" } %>
|
|
75
104
|
</td>
|
|
76
|
-
|
|
105
|
+
</tr>
|
|
106
|
+
<% end %>
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<% else %>
|
|
112
|
+
<div class="sqd-card" id="jobs-list">
|
|
113
|
+
<% if @jobs.empty? %>
|
|
114
|
+
<div class="sqd-empty">No <%= @status %> jobs.</div>
|
|
115
|
+
<% else %>
|
|
116
|
+
<table>
|
|
117
|
+
<thead>
|
|
118
|
+
<tr>
|
|
119
|
+
<th scope="col">Job Class</th>
|
|
120
|
+
<th scope="col">Queue</th>
|
|
121
|
+
<th scope="col">Priority</th>
|
|
122
|
+
<th scope="col">Scheduled At</th>
|
|
123
|
+
<th scope="col">Enqueued At</th>
|
|
77
124
|
</tr>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
</thead>
|
|
126
|
+
<tbody>
|
|
127
|
+
<% @jobs.each do |execution| %>
|
|
128
|
+
<% job = execution.job %>
|
|
129
|
+
<tr id="execution_<%= execution.id %>">
|
|
130
|
+
<td>
|
|
131
|
+
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
132
|
+
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
133
|
+
</td>
|
|
134
|
+
<td>
|
|
135
|
+
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
|
|
136
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
137
|
+
</td>
|
|
138
|
+
<td><%= job.priority %></td>
|
|
139
|
+
<td class="sqd-mono">
|
|
140
|
+
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
141
|
+
</td>
|
|
142
|
+
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
143
|
+
</tr>
|
|
144
|
+
<% end %>
|
|
145
|
+
</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
149
|
+
<% end %>
|
|
83
150
|
|
|
84
151
|
<% if @pagy.last > 1 %>
|
|
85
152
|
<%= @pagy.series_nav.html_safe %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value: 10000 } do %>
|
|
1
2
|
<h1 class="sqd-page-title">Processes</h1>
|
|
2
3
|
|
|
3
4
|
<div class="sqd-card">
|
|
@@ -51,4 +52,5 @@
|
|
|
51
52
|
</tbody>
|
|
52
53
|
</table>
|
|
53
54
|
<% end %>
|
|
54
|
-
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Search Jobs</h1>
|
|
2
|
+
|
|
3
|
+
<datalist id="job-class-list">
|
|
4
|
+
<% @job_classes.each do |klass| %>
|
|
5
|
+
<option value="<%= klass %>">
|
|
6
|
+
<% end %>
|
|
7
|
+
</datalist>
|
|
8
|
+
|
|
9
|
+
<form class="sqd-search sqd-search--global" action="<%= search_path %>" method="get" data-controller="search">
|
|
10
|
+
<input class="sqd-search__input sqd-search__input--lg" type="search" name="q"
|
|
11
|
+
value="<%= @query %>" placeholder="Search by job class name…"
|
|
12
|
+
list="job-class-list" autocomplete="off"
|
|
13
|
+
aria-label="Search all jobs by class name"
|
|
14
|
+
data-action="change->search#select">
|
|
15
|
+
<% if @query.present? %>
|
|
16
|
+
<%= link_to "Clear", search_path, class: "sqd-btn sqd-btn--muted" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</form>
|
|
19
|
+
|
|
20
|
+
<% if @query.present? %>
|
|
21
|
+
<% if @results.empty? %>
|
|
22
|
+
<div class="sqd-empty" style="margin-top: 1rem;">No jobs found matching “<%= @query %>”.</div>
|
|
23
|
+
<% else %>
|
|
24
|
+
<% @results.each do |status, data| %>
|
|
25
|
+
<div class="sqd-search-group">
|
|
26
|
+
<div class="sqd-search-group__header">
|
|
27
|
+
<span class="sqd-badge sqd-badge--<%= status %>"><%= status %></span>
|
|
28
|
+
<span class="sqd-muted-text">
|
|
29
|
+
<%= pluralize(data[:total], "match", "matches") %>
|
|
30
|
+
<% if data[:total] > SolidQueueWeb::SearchController::LIMIT %>
|
|
31
|
+
— showing first <%= SolidQueueWeb::SearchController::LIMIT %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</span>
|
|
34
|
+
<% if status == "failed" %>
|
|
35
|
+
<%= link_to "View all →", failed_jobs_path(q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
|
|
36
|
+
<% else %>
|
|
37
|
+
<%= link_to "View all →", jobs_path(status: status, q: @query), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="sqd-card">
|
|
41
|
+
<table>
|
|
42
|
+
<thead>
|
|
43
|
+
<tr>
|
|
44
|
+
<th scope="col">Job Class</th>
|
|
45
|
+
<th scope="col">Queue</th>
|
|
46
|
+
<th scope="col">Enqueued At</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<% data[:executions].each do |execution| %>
|
|
51
|
+
<% job = execution.job %>
|
|
52
|
+
<tr>
|
|
53
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
54
|
+
<td class="sqd-mono"><%= job.queue_name %></td>
|
|
55
|
+
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
56
|
+
</tr>
|
|
57
|
+
<% end %>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
data/config/importmap.rb
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
pin "solid_queue_web", to: "solid_queue_web/application.js"
|
|
2
2
|
pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js"
|
|
3
|
+
pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js"
|
|
4
|
+
pin "solid_queue_web/selection_controller", to: "solid_queue_web/selection_controller.js"
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
SolidQueueWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
3
|
|
|
4
|
+
get "search", to: "search#index", as: :search
|
|
5
|
+
|
|
4
6
|
resources :recurring_tasks, only: [ :index ]
|
|
5
7
|
resources :processes, only: [ :index ]
|
|
6
8
|
resources :queues, only: [ :index ], param: :name do
|
|
@@ -14,11 +16,18 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
# Singular selection resources must be defined before the member routes of their
|
|
21
|
+
# parent resources, otherwise DELETE /list/selection matches /list/:id first.
|
|
22
|
+
resource :job_selection, path: "list/selection", only: [ :destroy ], controller: "jobs/selections"
|
|
17
23
|
resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
|
|
18
24
|
collection do
|
|
19
25
|
post :discard_all
|
|
20
26
|
end
|
|
21
27
|
end
|
|
28
|
+
|
|
29
|
+
resource :failed_job_selection, path: "failed_jobs/selection", only: [ :create, :destroy ],
|
|
30
|
+
controller: "failed_jobs/selections"
|
|
22
31
|
resources :failed_jobs, only: [ :index, :destroy ] do
|
|
23
32
|
collection do
|
|
24
33
|
post :retry_all
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -94,15 +94,20 @@ files:
|
|
|
94
94
|
- app/assets/stylesheets/solid_queue_web/application.css
|
|
95
95
|
- app/controllers/solid_queue_web/application_controller.rb
|
|
96
96
|
- app/controllers/solid_queue_web/dashboard_controller.rb
|
|
97
|
+
- app/controllers/solid_queue_web/failed_jobs/selections_controller.rb
|
|
97
98
|
- app/controllers/solid_queue_web/failed_jobs_controller.rb
|
|
99
|
+
- app/controllers/solid_queue_web/jobs/selections_controller.rb
|
|
98
100
|
- app/controllers/solid_queue_web/jobs_controller.rb
|
|
99
101
|
- app/controllers/solid_queue_web/processes_controller.rb
|
|
100
102
|
- app/controllers/solid_queue_web/queues/jobs_controller.rb
|
|
101
103
|
- app/controllers/solid_queue_web/queues_controller.rb
|
|
102
104
|
- app/controllers/solid_queue_web/recurring_tasks_controller.rb
|
|
105
|
+
- app/controllers/solid_queue_web/search_controller.rb
|
|
103
106
|
- app/helpers/solid_queue_web/application_helper.rb
|
|
104
107
|
- app/javascript/solid_queue_web/application.js
|
|
108
|
+
- app/javascript/solid_queue_web/refresh_controller.js
|
|
105
109
|
- app/javascript/solid_queue_web/search_controller.js
|
|
110
|
+
- app/javascript/solid_queue_web/selection_controller.js
|
|
106
111
|
- app/jobs/solid_queue_web/application_job.rb
|
|
107
112
|
- app/models/solid_queue_web/application_record.rb
|
|
108
113
|
- app/models/solid_queue_web/job.rb
|
|
@@ -117,6 +122,7 @@ files:
|
|
|
117
122
|
- app/views/solid_queue_web/queues/jobs/destroy.turbo_stream.erb
|
|
118
123
|
- app/views/solid_queue_web/queues/jobs/index.html.erb
|
|
119
124
|
- app/views/solid_queue_web/recurring_tasks/index.html.erb
|
|
125
|
+
- app/views/solid_queue_web/search/index.html.erb
|
|
120
126
|
- config/importmap.rb
|
|
121
127
|
- config/routes.rb
|
|
122
128
|
- lib/solid_queue_web.rb
|