solid_queue_web 0.4.0 → 0.5.5
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 +47 -12
- data/app/assets/stylesheets/solid_queue_web/application.css +155 -1
- data/app/controllers/solid_queue_web/dashboard_controller.rb +2 -1
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +24 -9
- data/app/controllers/solid_queue_web/jobs_controller.rb +15 -29
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +63 -0
- data/app/controllers/solid_queue_web/recurring_tasks_controller.rb +7 -0
- data/app/javascript/solid_queue_web/application.js +6 -0
- data/app/javascript/solid_queue_web/search_controller.js +11 -0
- data/app/models/solid_queue_web/job.rb +13 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +24 -12
- data/app/views/solid_queue_web/dashboard/index.html.erb +5 -0
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +38 -9
- data/app/views/solid_queue_web/jobs/index.html.erb +28 -24
- data/app/views/solid_queue_web/jobs/show.html.erb +13 -8
- data/app/views/solid_queue_web/processes/index.html.erb +7 -7
- data/app/views/solid_queue_web/queues/index.html.erb +5 -5
- data/app/views/solid_queue_web/queues/jobs/destroy.turbo_stream.erb +9 -0
- data/app/views/solid_queue_web/queues/jobs/index.html.erb +89 -0
- data/app/views/solid_queue_web/recurring_tasks/index.html.erb +68 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +6 -0
- data/lib/solid_queue_web/engine.rb +13 -0
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +1 -0
- metadata +24 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fede0428a11872f9c6d461a4c58b658c59a5dbb4549de6fe0f13d1f1bafd6ae
|
|
4
|
+
data.tar.gz: 3fd89b402ec8034cdcec4e80c260e83dedcf4c96519375283fe0678696548d88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 361aaa9eca22abd7a34f574c434f04700dcce5d0c0f29dd0bf4f929bccedf08b32db280db3c8e29c6b237325b791dc09528cced2f9d8419b8f02879e32a0aa36
|
|
7
|
+
data.tar.gz: f2d79da345dec941c24a468729f78de4b210d9f4d8bb6b8203c6e60da16577e7b5538c2b66b8a544524bbc32cf7289fc26a77e3529e27444f825b889afde582a
|
data/README.md
CHANGED
|
@@ -1,19 +1,60 @@
|
|
|
1
1
|
# SolidQueueWeb
|
|
2
2
|
|
|
3
|
-
[](https://github.com/eclectic-coding/solid_queue_web/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/solid_queue_web)
|
|
5
|
+
[](https://rubygems.org/gems/solid_queue_web)
|
|
6
|
+
[](https://rubygems.org/gems/solid_queue_web)
|
|
7
|
+
[](https://codecov.io/gh/eclectic-coding/solid_queue_web)
|
|
4
8
|
|
|
5
|
-
A
|
|
9
|
+
A monitoring and management dashboard for [Solid Queue](https://github.com/rails/solid_queue), mountable as a Rails engine in any app.
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
Solid Queue ships without a web interface. When jobs fail, queues back up, or workers go silent in production, the only options are `rails console` or raw SQL queries. SolidQueueWeb gives your team a real-time dashboard to inspect, retry, and discard jobs without leaving the browser — and without standing up any additional infrastructure.
|
|
14
|
+
|
|
15
|
+
## Why SolidQueueWeb?
|
|
16
|
+
|
|
17
|
+
- Purpose-built for Solid Queue — uses its native models directly, no adapters
|
|
18
|
+
- No external CSS framework — drops into any Rails app without asset conflicts
|
|
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
|
|
21
|
+
- Inspired by Sidekiq Web UI and the GoodJob dashboard, adapted for the Solid Queue ecosystem
|
|
22
|
+
|
|
23
|
+
## Real-world use case
|
|
24
|
+
|
|
25
|
+
A Rails app processes order confirmations, email notifications, and report generation through Solid Queue. An operations team needs to:
|
|
26
|
+
|
|
27
|
+
- Monitor queue depth before a high-traffic event
|
|
28
|
+
- Retry a batch of failed notification jobs after a third-party API outage
|
|
29
|
+
- Pause a queue while a fix is being deployed
|
|
30
|
+
- Identify blocked or long-running jobs before they impact users
|
|
31
|
+
|
|
32
|
+
SolidQueueWeb surfaces all of this in a browser UI available at any route you choose.
|
|
6
33
|
|
|
7
34
|
## Features
|
|
8
35
|
|
|
9
|
-
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues and processes
|
|
36
|
+
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes
|
|
10
37
|
- **Queues** — all queues sorted by name
|
|
11
|
-
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; discard individual or all jobs
|
|
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
|
|
12
39
|
- **Failed jobs** — list of failed executions with error details; retry or discard individually or in bulk
|
|
13
40
|
- **Job detail** — full arguments, timestamps, and error backtrace; action buttons based on job status
|
|
14
41
|
- **Queue management** — pause and resume individual queues
|
|
42
|
+
- **Recurring tasks** — all configured recurring tasks with cron schedule, next run time, last run time, and static/dynamic classification
|
|
15
43
|
- **Processes** — workers, dispatchers, and supervisors with heartbeat health status
|
|
16
|
-
|
|
44
|
+
|
|
45
|
+
## Screenshots
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
## Compatibility
|
|
50
|
+
|
|
51
|
+
| Dependency | Version |
|
|
52
|
+
|-------------|------------|
|
|
53
|
+
| Ruby | >= 3.3 |
|
|
54
|
+
| Rails | >= 8.1.3 |
|
|
55
|
+
| Solid Queue | >= 1.0 |
|
|
56
|
+
|
|
57
|
+
Tested on Ruby 3.3, 3.4, and 4.0.
|
|
17
58
|
|
|
18
59
|
## Installation
|
|
19
60
|
|
|
@@ -37,7 +78,7 @@ Add to your `config/routes.rb`:
|
|
|
37
78
|
mount SolidQueueWeb::Engine, at: "/jobs"
|
|
38
79
|
```
|
|
39
80
|
|
|
40
|
-
The dashboard will be available at `/jobs`.
|
|
81
|
+
The dashboard will be available at `/jobs`. See [Authentication](#authentication) to restrict access to admin users.
|
|
41
82
|
|
|
42
83
|
## Authentication
|
|
43
84
|
|
|
@@ -53,12 +94,6 @@ end
|
|
|
53
94
|
|
|
54
95
|
HTTP Basic authentication is used as a fallback when the block returns falsy.
|
|
55
96
|
|
|
56
|
-
## Requirements
|
|
57
|
-
|
|
58
|
-
- Ruby >= 3.3
|
|
59
|
-
- Rails >= 8.1.3
|
|
60
|
-
- solid_queue >= 1.0
|
|
61
|
-
|
|
62
97
|
## Contributing
|
|
63
98
|
|
|
64
99
|
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/solid_queue_web).
|
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
6
6
|
|
|
7
|
+
.sqd-sr-only {
|
|
8
|
+
position: absolute;
|
|
9
|
+
width: 1px;
|
|
10
|
+
height: 1px;
|
|
11
|
+
padding: 0;
|
|
12
|
+
margin: -1px;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
clip: rect(0, 0, 0, 0);
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
border: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:focus-visible {
|
|
20
|
+
outline: 2px solid var(--primary);
|
|
21
|
+
outline-offset: 2px;
|
|
22
|
+
border-radius: 2px;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
:root {
|
|
8
26
|
--bg: #f8f9fa;
|
|
9
27
|
--surface: #ffffff;
|
|
@@ -30,6 +48,11 @@ body {
|
|
|
30
48
|
.sqd-header {
|
|
31
49
|
background: var(--surface);
|
|
32
50
|
border-bottom: 1px solid var(--border);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sqd-header__inner {
|
|
54
|
+
max-width: 1200px;
|
|
55
|
+
margin: 0 auto;
|
|
33
56
|
padding: 0 1.5rem;
|
|
34
57
|
display: flex;
|
|
35
58
|
align-items: center;
|
|
@@ -67,6 +90,28 @@ body {
|
|
|
67
90
|
color: var(--text);
|
|
68
91
|
}
|
|
69
92
|
|
|
93
|
+
.sqd-nav-toggle {
|
|
94
|
+
display: none;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
gap: 5px;
|
|
98
|
+
width: 36px;
|
|
99
|
+
height: 36px;
|
|
100
|
+
padding: 6px;
|
|
101
|
+
margin-left: auto;
|
|
102
|
+
background: none;
|
|
103
|
+
border: 1px solid var(--border);
|
|
104
|
+
border-radius: 5px;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.sqd-nav-toggle span {
|
|
109
|
+
display: block;
|
|
110
|
+
height: 2px;
|
|
111
|
+
background: var(--text);
|
|
112
|
+
border-radius: 1px;
|
|
113
|
+
}
|
|
114
|
+
|
|
70
115
|
.sqd-main {
|
|
71
116
|
max-width: 1200px;
|
|
72
117
|
margin: 0 auto;
|
|
@@ -104,7 +149,7 @@ body {
|
|
|
104
149
|
/* Stat cards */
|
|
105
150
|
.sqd-stats {
|
|
106
151
|
display: grid;
|
|
107
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
152
|
+
grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
|
|
108
153
|
gap: 1rem;
|
|
109
154
|
margin-bottom: 2rem;
|
|
110
155
|
}
|
|
@@ -138,6 +183,7 @@ body {
|
|
|
138
183
|
.sqd-stat--blocked .sqd-stat__value { color: var(--warning); }
|
|
139
184
|
.sqd-stat--queues .sqd-stat__value { color: var(--purple); }
|
|
140
185
|
.sqd-stat--processes .sqd-stat__value { color: var(--muted); }
|
|
186
|
+
.sqd-stat--recurring .sqd-stat__value { color: var(--info); }
|
|
141
187
|
|
|
142
188
|
.sqd-stat--link {
|
|
143
189
|
display: block;
|
|
@@ -222,6 +268,8 @@ tbody tr:hover { background: var(--bg); }
|
|
|
222
268
|
.sqd-badge--claimed { background: #cfe2ff; color: #084298; }
|
|
223
269
|
.sqd-badge--failed { background: #f8d7da; color: #842029; }
|
|
224
270
|
.sqd-badge--blocked { background: #fff3cd; color: #664d03; }
|
|
271
|
+
.sqd-badge--static { background: #d1e7dd; color: #0f5132; }
|
|
272
|
+
.sqd-badge--dynamic { background: #e0d7f5; color: #4a2c8a; }
|
|
225
273
|
.sqd-badge--paused { background: #e2e3e5; color: #41464b; }
|
|
226
274
|
.sqd-badge--running { background: #d1e7dd; color: #0f5132; }
|
|
227
275
|
.sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; }
|
|
@@ -254,6 +302,36 @@ tbody tr:hover { background: var(--bg); }
|
|
|
254
302
|
.sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
|
|
255
303
|
.sqd-row-actions form { display: inline; margin-left: 0.25rem; }
|
|
256
304
|
|
|
305
|
+
/* Search */
|
|
306
|
+
.sqd-search {
|
|
307
|
+
display: flex;
|
|
308
|
+
gap: 0.5rem;
|
|
309
|
+
align-items: center;
|
|
310
|
+
margin-bottom: 1rem;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.sqd-search__input {
|
|
314
|
+
width: 280px;
|
|
315
|
+
padding: 0.35rem 0.75rem;
|
|
316
|
+
border: 1px solid var(--border);
|
|
317
|
+
border-radius: 5px;
|
|
318
|
+
font-size: 13px;
|
|
319
|
+
background: var(--surface);
|
|
320
|
+
color: var(--text);
|
|
321
|
+
line-height: 1.5;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sqd-search__input:focus {
|
|
325
|
+
outline: 2px solid var(--primary);
|
|
326
|
+
outline-offset: -1px;
|
|
327
|
+
border-color: var(--primary);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@media (max-width: 640px) {
|
|
331
|
+
.sqd-search { flex-wrap: wrap; }
|
|
332
|
+
.sqd-search__input { width: 100%; }
|
|
333
|
+
}
|
|
334
|
+
|
|
257
335
|
/* Filters */
|
|
258
336
|
.sqd-filters {
|
|
259
337
|
display: flex;
|
|
@@ -344,8 +422,84 @@ nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
|
|
|
344
422
|
gap: 1.5rem;
|
|
345
423
|
}
|
|
346
424
|
|
|
425
|
+
.sqd-grid-2 {
|
|
426
|
+
display: grid;
|
|
427
|
+
grid-template-columns: 1fr 1fr;
|
|
428
|
+
gap: 1rem;
|
|
429
|
+
}
|
|
430
|
+
|
|
347
431
|
@media (max-width: 768px) {
|
|
348
432
|
.sqd-detail-grid { grid-template-columns: 1fr; }
|
|
433
|
+
.sqd-grid-2 { grid-template-columns: 1fr; }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@media (max-width: 640px) {
|
|
437
|
+
.sqd-main {
|
|
438
|
+
padding: 1.5rem 1rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.sqd-page-header {
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
align-items: flex-start;
|
|
444
|
+
gap: 0.75rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.sqd-card {
|
|
448
|
+
overflow-x: auto;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.sqd-card__header {
|
|
452
|
+
flex-wrap: wrap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.sqd-stats {
|
|
456
|
+
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.sqd-truncate {
|
|
460
|
+
max-width: 160px;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@media (max-width: 576px) {
|
|
465
|
+
.sqd-header {
|
|
466
|
+
position: relative;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.sqd-header__inner {
|
|
470
|
+
padding: 0 1rem;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.sqd-nav-toggle {
|
|
474
|
+
display: flex;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.sqd-nav-wrapper {
|
|
478
|
+
display: none;
|
|
479
|
+
position: absolute;
|
|
480
|
+
top: 100%;
|
|
481
|
+
left: 0;
|
|
482
|
+
right: 0;
|
|
483
|
+
background: var(--surface);
|
|
484
|
+
border-bottom: 1px solid var(--border);
|
|
485
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
486
|
+
z-index: 50;
|
|
487
|
+
padding: 0.5rem;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.sqd-nav-wrapper.sqd-nav--open {
|
|
491
|
+
display: block;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sqd-nav {
|
|
495
|
+
flex-direction: column;
|
|
496
|
+
gap: 0.25rem;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.sqd-nav a {
|
|
500
|
+
padding: 0.5rem 0.75rem;
|
|
501
|
+
font-size: 14px;
|
|
502
|
+
}
|
|
349
503
|
}
|
|
350
504
|
|
|
351
505
|
.sqd-detail-section { padding: 1.25rem; }
|
|
@@ -8,7 +8,8 @@ module SolidQueueWeb
|
|
|
8
8
|
failed: SolidQueue::FailedExecution.count,
|
|
9
9
|
blocked: SolidQueue::BlockedExecution.count,
|
|
10
10
|
queues: SolidQueue::Job.select(:queue_name).distinct.count,
|
|
11
|
-
processes: SolidQueue::Process.count
|
|
11
|
+
processes: SolidQueue::Process.count,
|
|
12
|
+
recurring: SolidQueue::RecurringTask.count
|
|
12
13
|
}
|
|
13
14
|
end
|
|
14
15
|
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,31 @@ module SolidQueueWeb
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def retry_all
|
|
26
|
-
|
|
27
|
-
jobs = executions.map(&:job)
|
|
26
|
+
jobs = filtered_scope.map(&:job)
|
|
28
27
|
SolidQueue::FailedExecution.retry_all(jobs)
|
|
29
|
-
redirect_to failed_jobs_path,
|
|
28
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search),
|
|
29
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def discard_all
|
|
33
|
-
|
|
34
|
-
SolidQueue::FailedExecution.
|
|
35
|
-
redirect_to failed_jobs_path,
|
|
33
|
+
jobs = filtered_scope.map(&:job)
|
|
34
|
+
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
35
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search),
|
|
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
|
+
end
|
|
45
|
+
|
|
46
|
+
def filtered_scope
|
|
47
|
+
scope = SolidQueue::FailedExecution.includes(:job)
|
|
48
|
+
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
|
|
49
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
50
|
+
scope
|
|
36
51
|
end
|
|
37
52
|
end
|
|
38
53
|
end
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
|
|
4
|
-
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
5
|
-
|
|
6
|
-
before_action :set_status_and_queue, only: [ :destroy, :discard_all ]
|
|
7
|
-
|
|
8
|
-
EXECUTION_MODELS = {
|
|
9
|
-
"ready" => SolidQueue::ReadyExecution,
|
|
10
|
-
"scheduled" => SolidQueue::ScheduledExecution,
|
|
11
|
-
"claimed" => SolidQueue::ClaimedExecution,
|
|
12
|
-
"blocked" => SolidQueue::BlockedExecution,
|
|
13
|
-
"failed" => SolidQueue::FailedExecution
|
|
14
|
-
}.freeze
|
|
3
|
+
before_action :set_status, only: [ :destroy, :discard_all ]
|
|
15
4
|
|
|
16
5
|
def index
|
|
17
|
-
@status = params[:status].presence_in(STATUSES) || "ready"
|
|
18
|
-
@
|
|
19
|
-
@jobs = EXECUTION_MODELS[@status].includes(:job)
|
|
20
|
-
@jobs = @jobs.where(
|
|
6
|
+
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
7
|
+
@search = params[:q].presence
|
|
8
|
+
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
9
|
+
@jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
21
10
|
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
|
|
22
11
|
end
|
|
23
12
|
|
|
@@ -25,7 +14,6 @@ module SolidQueueWeb
|
|
|
25
14
|
@job = SolidQueue::Job
|
|
26
15
|
.includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
|
|
27
16
|
.find(params[:id])
|
|
28
|
-
@failed_execution = @job.failed_execution
|
|
29
17
|
@execution_status = derive_status(@job)
|
|
30
18
|
end
|
|
31
19
|
|
|
@@ -36,24 +24,24 @@ module SolidQueueWeb
|
|
|
36
24
|
@remaining_count = filtered_scope(model).count
|
|
37
25
|
respond_to do |format|
|
|
38
26
|
format.turbo_stream
|
|
39
|
-
format.html { redirect_to jobs_path(status: @status
|
|
27
|
+
format.html { redirect_to jobs_path(status: @status), notice: "Job discarded." }
|
|
40
28
|
end
|
|
41
29
|
rescue ArgumentError => e
|
|
42
|
-
redirect_to jobs_path(status: @status
|
|
30
|
+
redirect_to jobs_path(status: @status), alert: e.message
|
|
43
31
|
rescue => e
|
|
44
|
-
redirect_to jobs_path(status: @status
|
|
32
|
+
redirect_to jobs_path(status: @status), alert: "Could not discard job: #{e.message}"
|
|
45
33
|
end
|
|
46
34
|
|
|
47
35
|
def discard_all
|
|
48
36
|
model = execution_model_for!(@status)
|
|
49
37
|
jobs = filtered_scope(model).map(&:job)
|
|
50
38
|
model.discard_all_from_jobs(jobs)
|
|
51
|
-
redirect_to jobs_path(status: @status
|
|
39
|
+
redirect_to jobs_path(status: @status),
|
|
52
40
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
53
41
|
rescue ArgumentError => e
|
|
54
|
-
redirect_to jobs_path(status: @status
|
|
42
|
+
redirect_to jobs_path(status: @status), alert: e.message
|
|
55
43
|
rescue => e
|
|
56
|
-
redirect_to jobs_path(status: @status
|
|
44
|
+
redirect_to jobs_path(status: @status), alert: "Could not discard jobs: #{e.message}"
|
|
57
45
|
end
|
|
58
46
|
|
|
59
47
|
private
|
|
@@ -67,19 +55,17 @@ module SolidQueueWeb
|
|
|
67
55
|
"finished"
|
|
68
56
|
end
|
|
69
57
|
|
|
70
|
-
def
|
|
58
|
+
def set_status
|
|
71
59
|
@status = params[:status]
|
|
72
|
-
@queue = params[:queue].presence
|
|
73
60
|
end
|
|
74
61
|
|
|
75
62
|
def filtered_scope(model)
|
|
76
|
-
|
|
77
|
-
@queue.present? ? scope.where(jobs: { queue_name: @queue }) : scope
|
|
63
|
+
model.includes(:job)
|
|
78
64
|
end
|
|
79
65
|
|
|
80
66
|
def execution_model_for!(status)
|
|
81
|
-
raise ArgumentError, "Cannot discard #{status} jobs from this page." unless DISCARDABLE.include?(status)
|
|
82
|
-
EXECUTION_MODELS[status]
|
|
67
|
+
raise ArgumentError, "Cannot discard #{status} jobs from this page." unless Job::DISCARDABLE.include?(status)
|
|
68
|
+
Job::EXECUTION_MODELS[status]
|
|
83
69
|
end
|
|
84
70
|
end
|
|
85
71
|
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,11 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
filter({ target }) {
|
|
5
|
+
clearTimeout(this._timer)
|
|
6
|
+
const len = target.value.length
|
|
7
|
+
if (len >= 4 || len === 0) {
|
|
8
|
+
this._timer = setTimeout(() => target.form.requestSubmit(), 300)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -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
|
|
@@ -7,28 +7,40 @@
|
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
<%= inline_styles %>
|
|
10
|
+
<%= javascript_importmap_tags "solid_queue_web" %>
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
|
|
13
14
|
<header class="sqd-header">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
<div class="sqd-header__inner">
|
|
16
|
+
<%= link_to "Solid Queue", root_path, class: "sqd-header__title" %>
|
|
17
|
+
<button class="sqd-nav-toggle" aria-label="Toggle navigation" aria-expanded="false"
|
|
18
|
+
onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
|
|
19
|
+
<span></span>
|
|
20
|
+
<span></span>
|
|
21
|
+
<span></span>
|
|
22
|
+
</button>
|
|
23
|
+
<div class="sqd-nav-wrapper">
|
|
24
|
+
<nav aria-label="Main">
|
|
25
|
+
<ul class="sqd-nav">
|
|
26
|
+
<li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
|
|
27
|
+
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
|
|
28
|
+
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
|
|
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
|
+
<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
|
+
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
|
|
32
|
+
</ul>
|
|
33
|
+
</nav>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
24
36
|
</header>
|
|
25
37
|
|
|
26
38
|
<main class="sqd-main">
|
|
27
39
|
<% if notice.present? %>
|
|
28
|
-
<div class="sqd-flash sqd-flash--notice"><%= notice %></div>
|
|
40
|
+
<div class="sqd-flash sqd-flash--notice" role="status"><%= notice %></div>
|
|
29
41
|
<% end %>
|
|
30
42
|
<% if alert.present? %>
|
|
31
|
-
<div class="sqd-flash sqd-flash--alert"><%= alert %></div>
|
|
43
|
+
<div class="sqd-flash sqd-flash--alert" role="alert"><%= alert %></div>
|
|
32
44
|
<% end %>
|
|
33
45
|
|
|
34
46
|
<%= yield %>
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
<div class="sqd-stat__value"><%= @stats[:queues] %></div>
|
|
26
26
|
<div class="sqd-stat__label">Queues</div>
|
|
27
27
|
<% end %>
|
|
28
|
+
<%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %>
|
|
29
|
+
<div class="sqd-stat__value"><%= @stats[:recurring] %></div>
|
|
30
|
+
<div class="sqd-stat__label">Recurring</div>
|
|
31
|
+
<% end %>
|
|
28
32
|
<%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
|
|
29
33
|
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
30
34
|
<div class="sqd-stat__label">Processes</div>
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
<%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %>
|
|
42
46
|
<%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
43
47
|
<%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %>
|
|
48
|
+
<%= link_to "View recurring tasks", recurring_tasks_path, class: "sqd-btn sqd-btn--muted" %>
|
|
44
49
|
</div>
|
|
45
50
|
</div>
|
|
46
51
|
|
|
@@ -2,14 +2,33 @@
|
|
|
2
2
|
<h1 class="sqd-page-title">Failed Jobs</h1>
|
|
3
3
|
<% if @failed_jobs.any? %>
|
|
4
4
|
<div class="sqd-actions">
|
|
5
|
-
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
5
|
+
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
6
|
+
method: :post,
|
|
7
|
+
params: { queue: @queue, q: @search },
|
|
8
|
+
class: "sqd-btn sqd-btn--primary",
|
|
6
9
|
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
|
|
7
|
-
<%= button_to "Discard All", discard_all_failed_jobs_path,
|
|
10
|
+
<%= button_to "Discard All", discard_all_failed_jobs_path,
|
|
11
|
+
method: :post,
|
|
12
|
+
params: { queue: @queue, q: @search },
|
|
13
|
+
class: "sqd-btn sqd-btn--danger",
|
|
8
14
|
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
|
|
9
15
|
</div>
|
|
10
16
|
<% end %>
|
|
11
17
|
</div>
|
|
12
18
|
|
|
19
|
+
<form class="sqd-search" action="<%= failed_jobs_path %>" method="get" data-controller="search">
|
|
20
|
+
<% if @queue.present? %>
|
|
21
|
+
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
22
|
+
<% end %>
|
|
23
|
+
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
24
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
25
|
+
data-action="input->search#filter">
|
|
26
|
+
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
27
|
+
<% if @search.present? %>
|
|
28
|
+
<%= link_to "Clear", failed_jobs_path(queue: @queue), class: "sqd-btn sqd-btn--muted" %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</form>
|
|
31
|
+
|
|
13
32
|
<% if @pagy.last > 1 %>
|
|
14
33
|
<%= @pagy.series_nav.html_safe %>
|
|
15
34
|
<% end %>
|
|
@@ -21,11 +40,11 @@
|
|
|
21
40
|
<table>
|
|
22
41
|
<thead>
|
|
23
42
|
<tr>
|
|
24
|
-
<th>Job Class</th>
|
|
25
|
-
<th>Queue</th>
|
|
26
|
-
<th>Error</th>
|
|
27
|
-
<th>Failed At</th>
|
|
28
|
-
<th></th>
|
|
43
|
+
<th scope="col">Job Class</th>
|
|
44
|
+
<th scope="col">Queue</th>
|
|
45
|
+
<th scope="col">Error</th>
|
|
46
|
+
<th scope="col">Failed At</th>
|
|
47
|
+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
29
48
|
</tr>
|
|
30
49
|
</thead>
|
|
31
50
|
<tbody>
|
|
@@ -33,7 +52,10 @@
|
|
|
33
52
|
<% job = execution.job %>
|
|
34
53
|
<tr>
|
|
35
54
|
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
36
|
-
<td
|
|
55
|
+
<td>
|
|
56
|
+
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search),
|
|
57
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
58
|
+
</td>
|
|
37
59
|
<td>
|
|
38
60
|
<% if execution.exception_class.present? %>
|
|
39
61
|
<div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
|
|
@@ -56,4 +78,11 @@
|
|
|
56
78
|
</tbody>
|
|
57
79
|
</table>
|
|
58
80
|
<% end %>
|
|
59
|
-
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<% if @queue.present? %>
|
|
84
|
+
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
85
|
+
Filtering by queue: <strong><%= @queue %></strong> —
|
|
86
|
+
<%= link_to "Clear filter", failed_jobs_path(q: @search) %>
|
|
87
|
+
</p>
|
|
88
|
+
<% end %>
|
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>
|
|
2
2
|
|
|
3
|
-
<%= turbo_frame_tag "jobs-table" do %>
|
|
4
|
-
<% discardable = SolidQueueWeb::
|
|
3
|
+
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
|
|
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",
|
|
9
|
-
<%= link_to "Scheduled", jobs_path(status: "scheduled",
|
|
10
|
-
<%= link_to "Running", jobs_path(status: "claimed",
|
|
11
|
-
<%= link_to "Blocked", jobs_path(status: "blocked",
|
|
12
|
-
<%= link_to "Failed", jobs_path(status: "failed",
|
|
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" : "" %>
|
|
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 },
|
|
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>
|
|
22
22
|
<% end %>
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
+
<form class="sqd-search" action="<%= jobs_path %>" method="get" data-controller="search">
|
|
26
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
27
|
+
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
28
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
29
|
+
data-action="input->search#filter">
|
|
30
|
+
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
31
|
+
<% if @search.present? %>
|
|
32
|
+
<%= link_to "Clear", jobs_path(status: @status), class: "sqd-btn sqd-btn--muted" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</form>
|
|
35
|
+
|
|
25
36
|
<div class="sqd-card" id="jobs-list">
|
|
26
37
|
<% if @jobs.empty? %>
|
|
27
38
|
<div class="sqd-empty">No <%= @status %> jobs.</div>
|
|
@@ -29,12 +40,12 @@
|
|
|
29
40
|
<table>
|
|
30
41
|
<thead>
|
|
31
42
|
<tr>
|
|
32
|
-
<th>Job Class</th>
|
|
33
|
-
<th>Queue</th>
|
|
34
|
-
<th>Priority</th>
|
|
35
|
-
<th>Scheduled At</th>
|
|
36
|
-
<th>Enqueued At</th>
|
|
37
|
-
<% if discardable %><th></th><% end %>
|
|
43
|
+
<th scope="col">Job Class</th>
|
|
44
|
+
<th scope="col">Queue</th>
|
|
45
|
+
<th scope="col">Priority</th>
|
|
46
|
+
<th scope="col">Scheduled At</th>
|
|
47
|
+
<th scope="col">Enqueued At</th>
|
|
48
|
+
<% if discardable %><th scope="col"><span class="sqd-sr-only">Actions</span></th><% end %>
|
|
38
49
|
</tr>
|
|
39
50
|
</thead>
|
|
40
51
|
<tbody>
|
|
@@ -43,10 +54,10 @@
|
|
|
43
54
|
<tr id="execution_<%= execution.id %>">
|
|
44
55
|
<td>
|
|
45
56
|
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
46
|
-
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %>
|
|
57
|
+
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
47
58
|
</td>
|
|
48
59
|
<td>
|
|
49
|
-
<%= link_to job.queue_name,
|
|
60
|
+
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
|
|
50
61
|
class: "sqd-mono", style: "color: inherit;" %>
|
|
51
62
|
</td>
|
|
52
63
|
<td><%= job.priority %></td>
|
|
@@ -58,7 +69,7 @@
|
|
|
58
69
|
<td class="sqd-row-actions">
|
|
59
70
|
<%= button_to "Discard", job_path(execution),
|
|
60
71
|
method: :delete,
|
|
61
|
-
params: { status: @status
|
|
72
|
+
params: { status: @status },
|
|
62
73
|
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
63
74
|
data: { confirm: "Discard this job?" } %>
|
|
64
75
|
</td>
|
|
@@ -73,11 +84,4 @@
|
|
|
73
84
|
<% if @pagy.last > 1 %>
|
|
74
85
|
<%= @pagy.series_nav.html_safe %>
|
|
75
86
|
<% end %>
|
|
76
|
-
|
|
77
|
-
<% if @queue.present? %>
|
|
78
|
-
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
79
|
-
Filtering by queue: <strong><%= @queue %></strong> —
|
|
80
|
-
<%= link_to "Clear filter", jobs_path(status: @status) %>
|
|
81
|
-
</p>
|
|
82
|
-
<% end %>
|
|
83
87
|
<% end %>
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
<div class="sqd-actions">
|
|
10
|
-
<% if @execution_status == "failed" && @failed_execution %>
|
|
11
|
-
<%= button_to "Retry", retry_failed_job_path(@failed_execution), method: :post,
|
|
10
|
+
<% if @execution_status == "failed" && @job.failed_execution %>
|
|
11
|
+
<%= button_to "Retry", retry_failed_job_path(@job.failed_execution), method: :post,
|
|
12
12
|
class: "sqd-btn sqd-btn--primary" %>
|
|
13
|
-
<%= button_to "Discard", failed_job_path(@failed_execution), method: :delete,
|
|
13
|
+
<%= button_to "Discard", failed_job_path(@job.failed_execution), method: :delete,
|
|
14
14
|
class: "sqd-btn sqd-btn--danger",
|
|
15
15
|
data: { confirm: "Discard this job?" } %>
|
|
16
|
-
<% elsif SolidQueueWeb::
|
|
16
|
+
<% elsif SolidQueueWeb::Job::DISCARDABLE.include?(@execution_status) %>
|
|
17
17
|
<% execution = @job.public_send("#{@execution_status}_execution") %>
|
|
18
18
|
<% if execution %>
|
|
19
19
|
<%= button_to "Discard", job_path(execution),
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
<dt>Concurrency Key</dt>
|
|
46
46
|
<dd class="sqd-mono"><%= @job.concurrency_key.presence || "—" %></dd>
|
|
47
47
|
|
|
48
|
+
<% if @job.blocked_execution %>
|
|
49
|
+
<dt>Blocked Until</dt>
|
|
50
|
+
<dd class="sqd-mono"><%= @job.blocked_execution.expires_at ? @job.blocked_execution.expires_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
|
|
51
|
+
<% end %>
|
|
52
|
+
|
|
48
53
|
<dt>Enqueued At</dt>
|
|
49
54
|
<dd class="sqd-mono"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></dd>
|
|
50
55
|
|
|
@@ -62,14 +67,14 @@
|
|
|
62
67
|
</div>
|
|
63
68
|
</div>
|
|
64
69
|
|
|
65
|
-
<% if @failed_execution %>
|
|
70
|
+
<% if @job.failed_execution %>
|
|
66
71
|
<div class="sqd-card sqd-detail-section" style="margin-top: 1.5rem;">
|
|
67
72
|
<h2 class="sqd-section-title sqd-section-title--danger">Error</h2>
|
|
68
73
|
<p class="sqd-error-header">
|
|
69
|
-
<strong><%= @failed_execution.exception_class %></strong>: <%= @failed_execution.message %>
|
|
74
|
+
<strong><%= @job.failed_execution.exception_class %></strong>: <%= @job.failed_execution.message %>
|
|
70
75
|
</p>
|
|
71
|
-
<% if @failed_execution.backtrace.present? %>
|
|
72
|
-
<pre class="sqd-pre sqd-pre--muted" style="margin-top: 0.75rem;"><%= Array(@failed_execution.backtrace).join("\n") %></pre>
|
|
76
|
+
<% if @job.failed_execution.backtrace.present? %>
|
|
77
|
+
<pre class="sqd-pre sqd-pre--muted" style="margin-top: 0.75rem;"><%= Array(@job.failed_execution.backtrace).join("\n") %></pre>
|
|
73
78
|
<% end %>
|
|
74
79
|
</div>
|
|
75
80
|
<% end %>
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
<table>
|
|
8
8
|
<thead>
|
|
9
9
|
<tr>
|
|
10
|
-
<th>Kind</th>
|
|
11
|
-
<th>Name</th>
|
|
12
|
-
<th>PID</th>
|
|
13
|
-
<th>Host</th>
|
|
14
|
-
<th>Details</th>
|
|
15
|
-
<th>Last Heartbeat</th>
|
|
16
|
-
<th>Status</th>
|
|
10
|
+
<th scope="col">Kind</th>
|
|
11
|
+
<th scope="col">Name</th>
|
|
12
|
+
<th scope="col">PID</th>
|
|
13
|
+
<th scope="col">Host</th>
|
|
14
|
+
<th scope="col">Details</th>
|
|
15
|
+
<th scope="col">Last Heartbeat</th>
|
|
16
|
+
<th scope="col">Status</th>
|
|
17
17
|
</tr>
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
<table>
|
|
8
8
|
<thead>
|
|
9
9
|
<tr>
|
|
10
|
-
<th>Name</th>
|
|
11
|
-
<th>Size</th>
|
|
12
|
-
<th>Latency</th>
|
|
13
|
-
<th>Status</th>
|
|
14
|
-
<th></th>
|
|
10
|
+
<th scope="col">Name</th>
|
|
11
|
+
<th scope="col">Size</th>
|
|
12
|
+
<th scope="col">Latency</th>
|
|
13
|
+
<th scope="col">Status</th>
|
|
14
|
+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
15
15
|
</tr>
|
|
16
16
|
</thead>
|
|
17
17
|
<tbody>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if @remaining_count == 0 %>
|
|
2
|
+
<%= turbo_stream.replace "jobs-list" do %>
|
|
3
|
+
<div class="sqd-card" id="jobs-list">
|
|
4
|
+
<div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
|
|
5
|
+
</div>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% else %>
|
|
8
|
+
<%= turbo_stream.remove "execution_#{@execution.id}" %>
|
|
9
|
+
<% end %>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<div class="sqd-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="sqd-breadcrumb">
|
|
4
|
+
<%= link_to "Queues", queues_path %> › <%= @queue %>
|
|
5
|
+
</div>
|
|
6
|
+
<h1 class="sqd-page-title">Jobs</h1>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance" } do %>
|
|
11
|
+
<% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
|
|
12
|
+
|
|
13
|
+
<div class="sqd-page-header">
|
|
14
|
+
<div class="sqd-filters">
|
|
15
|
+
<%= link_to "Ready", queue_jobs_path(queue_name: @queue, status: "ready", q: @search), class: @status == "ready" ? "active" : "" %>
|
|
16
|
+
<%= link_to "Scheduled", queue_jobs_path(queue_name: @queue, status: "scheduled", q: @search), class: @status == "scheduled" ? "active" : "" %>
|
|
17
|
+
<%= link_to "Running", queue_jobs_path(queue_name: @queue, status: "claimed", q: @search), class: @status == "claimed" ? "active" : "" %>
|
|
18
|
+
<%= link_to "Blocked", queue_jobs_path(queue_name: @queue, status: "blocked", q: @search), class: @status == "blocked" ? "active" : "" %>
|
|
19
|
+
<%= link_to "Failed", queue_jobs_path(queue_name: @queue, status: "failed", q: @search), class: @status == "failed" ? "active" : "" %>
|
|
20
|
+
</div>
|
|
21
|
+
<% if discardable && @jobs.any? %>
|
|
22
|
+
<div class="sqd-actions">
|
|
23
|
+
<%= button_to "Discard All", discard_all_queue_jobs_path(queue_name: @queue),
|
|
24
|
+
method: :post,
|
|
25
|
+
params: { status: @status },
|
|
26
|
+
class: "sqd-btn sqd-btn--danger",
|
|
27
|
+
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs in #{@queue}? This cannot be undone." } %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<form class="sqd-search" action="<%= queue_jobs_path(queue_name: @queue) %>" method="get" data-controller="search">
|
|
33
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
34
|
+
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
35
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
36
|
+
data-action="input->search#filter">
|
|
37
|
+
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
38
|
+
<% if @search.present? %>
|
|
39
|
+
<%= link_to "Clear", queue_jobs_path(queue_name: @queue, status: @status), class: "sqd-btn sqd-btn--muted" %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</form>
|
|
42
|
+
|
|
43
|
+
<div class="sqd-card" id="jobs-list">
|
|
44
|
+
<% if @jobs.empty? %>
|
|
45
|
+
<div class="sqd-empty">No <%= @status %> jobs in <%= @queue %>.</div>
|
|
46
|
+
<% else %>
|
|
47
|
+
<table>
|
|
48
|
+
<thead>
|
|
49
|
+
<tr>
|
|
50
|
+
<th scope="col">Job Class</th>
|
|
51
|
+
<th scope="col">Priority</th>
|
|
52
|
+
<th scope="col">Scheduled At</th>
|
|
53
|
+
<th scope="col">Enqueued At</th>
|
|
54
|
+
<% if discardable %><th scope="col"><span class="sqd-sr-only">Actions</span></th><% end %>
|
|
55
|
+
</tr>
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
<% @jobs.each do |execution| %>
|
|
59
|
+
<% job = execution.job %>
|
|
60
|
+
<tr id="execution_<%= execution.id %>">
|
|
61
|
+
<td>
|
|
62
|
+
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
63
|
+
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
64
|
+
</td>
|
|
65
|
+
<td><%= job.priority %></td>
|
|
66
|
+
<td class="sqd-mono">
|
|
67
|
+
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
68
|
+
</td>
|
|
69
|
+
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
70
|
+
<% if discardable %>
|
|
71
|
+
<td class="sqd-row-actions">
|
|
72
|
+
<%= button_to "Discard", queue_job_path(queue_name: @queue, id: execution),
|
|
73
|
+
method: :delete,
|
|
74
|
+
params: { status: @status },
|
|
75
|
+
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
76
|
+
data: { confirm: "Discard this job?" } %>
|
|
77
|
+
</td>
|
|
78
|
+
<% end %>
|
|
79
|
+
</tr>
|
|
80
|
+
<% end %>
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<% if @pagy.last > 1 %>
|
|
87
|
+
<%= @pagy.series_nav.html_safe %>
|
|
88
|
+
<% end %>
|
|
89
|
+
<% end %>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Recurring Tasks</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-card" style="margin-top: 1.5rem;">
|
|
4
|
+
<% if @recurring_tasks.empty? %>
|
|
5
|
+
<div class="sqd-empty">No recurring tasks configured.</div>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th scope="col">Key</th>
|
|
11
|
+
<th scope="col">Schedule</th>
|
|
12
|
+
<th scope="col">Job / Command</th>
|
|
13
|
+
<th scope="col">Queue</th>
|
|
14
|
+
<th scope="col">Next Run</th>
|
|
15
|
+
<th scope="col">Last Run</th>
|
|
16
|
+
<th scope="col">Type</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @recurring_tasks.each do |task| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td class="sqd-mono"><%= task.key %></td>
|
|
23
|
+
<td class="sqd-mono"><%= task.schedule %></td>
|
|
24
|
+
<td>
|
|
25
|
+
<% if task.class_name.present? %>
|
|
26
|
+
<%= task.class_name %>
|
|
27
|
+
<% if task.arguments.present? %>
|
|
28
|
+
<div class="sqd-muted-text sqd-mono" style="font-size: 11px;">
|
|
29
|
+
<%= task.arguments.inspect %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% else %>
|
|
33
|
+
<span class="sqd-mono sqd-muted-text"><%= task.command %></span>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% if task.description.present? %>
|
|
36
|
+
<div class="sqd-muted-text" style="font-size: 12px; margin-top: 0.2rem;">
|
|
37
|
+
<%= task.description %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</td>
|
|
41
|
+
<td class="sqd-mono"><%= task.queue_name.presence || "default" %></td>
|
|
42
|
+
<td class="sqd-mono">
|
|
43
|
+
<%
|
|
44
|
+
next_run = begin
|
|
45
|
+
task.next_time.strftime("%Y-%m-%d %H:%M %Z")
|
|
46
|
+
rescue
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
%>
|
|
50
|
+
<%= next_run || "—" %>
|
|
51
|
+
</td>
|
|
52
|
+
<td class="sqd-mono">
|
|
53
|
+
<% last_run = task.last_enqueued_time %>
|
|
54
|
+
<%= last_run ? last_run.strftime("%Y-%m-%d %H:%M %Z") : "—" %>
|
|
55
|
+
</td>
|
|
56
|
+
<td>
|
|
57
|
+
<% if task.static? %>
|
|
58
|
+
<span class="sqd-badge sqd-badge--static">Static</span>
|
|
59
|
+
<% else %>
|
|
60
|
+
<span class="sqd-badge sqd-badge--dynamic">Dynamic</span>
|
|
61
|
+
<% end %>
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
<% end %>
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
<% end %>
|
|
68
|
+
</div>
|
data/config/importmap.rb
ADDED
data/config/routes.rb
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
SolidQueueWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
3
|
|
|
4
|
+
resources :recurring_tasks, only: [ :index ]
|
|
4
5
|
resources :processes, only: [ :index ]
|
|
5
6
|
resources :queues, only: [ :index ], param: :name do
|
|
6
7
|
member do
|
|
7
8
|
post :pause
|
|
8
9
|
post :resume
|
|
9
10
|
end
|
|
11
|
+
resources :jobs, path: "list", only: [ :index, :destroy ], controller: "queues/jobs" do
|
|
12
|
+
collection do
|
|
13
|
+
post :discard_all
|
|
14
|
+
end
|
|
15
|
+
end
|
|
10
16
|
end
|
|
11
17
|
resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
|
|
12
18
|
collection do
|
|
@@ -9,6 +9,19 @@ module SolidQueueWeb
|
|
|
9
9
|
|
|
10
10
|
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
11
11
|
|
|
12
|
+
initializer "solid_queue_web.assets" do |app|
|
|
13
|
+
if app.config.respond_to?(:assets)
|
|
14
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "solid_queue_web.importmap", before: "importmap" do |app|
|
|
19
|
+
if app.config.respond_to?(:importmap)
|
|
20
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
21
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
12
25
|
initializer "solid_queue_web.pagy" do
|
|
13
26
|
Pagy::OPTIONS[:limit] = 25
|
|
14
27
|
end
|
data/lib/solid_queue_web.rb
CHANGED
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.5.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: importmap-rails
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.2'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.2'
|
|
68
82
|
description: Mount SolidQueueWeb in any Rails app using Solid Queue to get a dashboard
|
|
69
83
|
for your queues, jobs by status, failed executions, and job actions (retry, discard)
|
|
70
84
|
— all without leaving your app.
|
|
@@ -83,10 +97,15 @@ files:
|
|
|
83
97
|
- app/controllers/solid_queue_web/failed_jobs_controller.rb
|
|
84
98
|
- app/controllers/solid_queue_web/jobs_controller.rb
|
|
85
99
|
- app/controllers/solid_queue_web/processes_controller.rb
|
|
100
|
+
- app/controllers/solid_queue_web/queues/jobs_controller.rb
|
|
86
101
|
- app/controllers/solid_queue_web/queues_controller.rb
|
|
102
|
+
- app/controllers/solid_queue_web/recurring_tasks_controller.rb
|
|
87
103
|
- app/helpers/solid_queue_web/application_helper.rb
|
|
104
|
+
- app/javascript/solid_queue_web/application.js
|
|
105
|
+
- app/javascript/solid_queue_web/search_controller.js
|
|
88
106
|
- app/jobs/solid_queue_web/application_job.rb
|
|
89
107
|
- app/models/solid_queue_web/application_record.rb
|
|
108
|
+
- app/models/solid_queue_web/job.rb
|
|
90
109
|
- app/views/layouts/solid_queue_web/application.html.erb
|
|
91
110
|
- app/views/solid_queue_web/dashboard/index.html.erb
|
|
92
111
|
- app/views/solid_queue_web/failed_jobs/index.html.erb
|
|
@@ -95,6 +114,10 @@ files:
|
|
|
95
114
|
- app/views/solid_queue_web/jobs/show.html.erb
|
|
96
115
|
- app/views/solid_queue_web/processes/index.html.erb
|
|
97
116
|
- app/views/solid_queue_web/queues/index.html.erb
|
|
117
|
+
- app/views/solid_queue_web/queues/jobs/destroy.turbo_stream.erb
|
|
118
|
+
- app/views/solid_queue_web/queues/jobs/index.html.erb
|
|
119
|
+
- app/views/solid_queue_web/recurring_tasks/index.html.erb
|
|
120
|
+
- config/importmap.rb
|
|
98
121
|
- config/routes.rb
|
|
99
122
|
- lib/solid_queue_web.rb
|
|
100
123
|
- lib/solid_queue_web/engine.rb
|