solid_queue_web 0.2.0 → 0.3.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 +6 -3
- data/app/assets/stylesheets/solid_queue_web/application.css +93 -3
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +32 -4
- data/app/controllers/solid_queue_web/jobs_controller.rb +54 -1
- data/app/controllers/solid_queue_web/processes_controller.rb +7 -0
- data/app/controllers/solid_queue_web/queues_controller.rb +16 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +1 -0
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +25 -3
- data/app/views/solid_queue_web/jobs/index.html.erb +30 -3
- data/app/views/solid_queue_web/jobs/show.html.erb +75 -0
- data/app/views/solid_queue_web/processes/index.html.erb +54 -0
- data/app/views/solid_queue_web/queues/index.html.erb +11 -0
- data/config/routes.rb +21 -3
- data/lib/solid_queue_web/engine.rb +8 -0
- data/lib/solid_queue_web/version.rb +1 -1
- metadata +22 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7de0f3d542cd6cd6d162d4069eb5ca373e5ee6fca1738937c2a00ac3f9560956
|
|
4
|
+
data.tar.gz: f8bb5e17912f2e87287aba8a5e6a057eb64adafbea6934aecac8b2559bc17290
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad6b13a2fe53124e78d5365d112eeb1e8811db4a5ba2be56e0e1d5fb4ac7da13c6f503cc0b71c68e4c295119764d3b4211767609a10efeca2ea8abc472dc51ef
|
|
7
|
+
data.tar.gz: 6d046fa5771188fe639ad72960ec4d3ab55916fc4e8d702e68d21b838ee5bdf9b25e6eda641b051ed6d10ecfec69dcdb4720841f170ba02b834d23e2113d516a
|
data/README.md
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/solid_queue_web)
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A Rails engine that mounts a monitoring dashboard for [Solid Queue](https://github.com/rails/solid_queue). View queues, inspect jobs by status, browse failed executions, and take action — all without leaving your app.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues and processes
|
|
10
10
|
- **Queues** — all queues sorted by name
|
|
11
|
-
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue
|
|
12
|
-
- **Failed jobs** — list of failed executions with error details
|
|
11
|
+
- **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; discard individual or all jobs
|
|
12
|
+
- **Failed jobs** — list of failed executions with error details; retry or discard individually or in bulk
|
|
13
|
+
- **Job detail** — full arguments, timestamps, and error backtrace; action buttons based on job status
|
|
14
|
+
- **Queue management** — pause and resume individual queues
|
|
15
|
+
- **Processes** — workers, dispatchers, and supervisors with heartbeat health status
|
|
13
16
|
- No external CSS framework — works out of the box
|
|
14
17
|
|
|
15
18
|
## Installation
|
|
@@ -76,9 +76,21 @@ body {
|
|
|
76
76
|
.sqd-page-title {
|
|
77
77
|
font-size: 20px;
|
|
78
78
|
font-weight: 600;
|
|
79
|
+
margin-bottom: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.sqd-page-header {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: space-between;
|
|
79
86
|
margin-bottom: 1.5rem;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
.sqd-actions {
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: 0.5rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/* Flash notices */
|
|
83
95
|
.sqd-flash {
|
|
84
96
|
padding: 0.75rem 1rem;
|
|
@@ -198,8 +210,15 @@ tbody tr:hover { background: var(--bg); }
|
|
|
198
210
|
.sqd-badge--claimed { background: #cfe2ff; color: #084298; }
|
|
199
211
|
.sqd-badge--failed { background: #f8d7da; color: #842029; }
|
|
200
212
|
.sqd-badge--blocked { background: #fff3cd; color: #664d03; }
|
|
201
|
-
.sqd-badge--paused
|
|
202
|
-
.sqd-badge--running
|
|
213
|
+
.sqd-badge--paused { background: #e2e3e5; color: #41464b; }
|
|
214
|
+
.sqd-badge--running { background: #d1e7dd; color: #0f5132; }
|
|
215
|
+
.sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; }
|
|
216
|
+
.sqd-badge--worker { background: #d1e7dd; color: #0f5132; }
|
|
217
|
+
.sqd-badge--dispatcher { background: #cff4fc; color: #055160; }
|
|
218
|
+
|
|
219
|
+
.sqd-process-meta { font-size: 12px; color: var(--muted); }
|
|
220
|
+
.sqd-process-meta span + span::before { content: " · "; }
|
|
221
|
+
.sqd-muted-text { color: var(--muted); font-size: 13px; }
|
|
203
222
|
|
|
204
223
|
/* Buttons */
|
|
205
224
|
.sqd-btn {
|
|
@@ -218,6 +237,10 @@ tbody tr:hover { background: var(--bg); }
|
|
|
218
237
|
.sqd-btn--primary { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
219
238
|
.sqd-btn--danger { background: var(--danger); color: #fff; border-color: var(--danger); }
|
|
220
239
|
.sqd-btn--muted { background: var(--surface); color: var(--text); border-color: var(--border); }
|
|
240
|
+
.sqd-btn--sm { padding: 0.2rem 0.55rem; font-size: 11px; }
|
|
241
|
+
|
|
242
|
+
.sqd-row-actions { white-space: nowrap; text-align: right; width: 1%; }
|
|
243
|
+
.sqd-row-actions form { display: inline; margin-left: 0.25rem; }
|
|
221
244
|
|
|
222
245
|
/* Filters */
|
|
223
246
|
.sqd-filters {
|
|
@@ -291,4 +314,71 @@ nav.pagy a {
|
|
|
291
314
|
nav.pagy a:hover:not([aria-disabled="true"]) { background: var(--bg); }
|
|
292
315
|
nav.pagy a[aria-current="page"] { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
293
316
|
nav.pagy a[role="separator"],
|
|
294
|
-
nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
|
|
317
|
+
nav.pagy a[aria-disabled="true"] { color: var(--muted); cursor: default; }
|
|
318
|
+
|
|
319
|
+
/* Job detail page */
|
|
320
|
+
.sqd-breadcrumb {
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
color: var(--muted);
|
|
323
|
+
margin-bottom: 0.25rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.sqd-breadcrumb a { color: var(--muted); text-decoration: none; }
|
|
327
|
+
.sqd-breadcrumb a:hover { color: var(--text); }
|
|
328
|
+
|
|
329
|
+
.sqd-detail-grid {
|
|
330
|
+
display: grid;
|
|
331
|
+
grid-template-columns: 1fr 1fr;
|
|
332
|
+
gap: 1.5rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@media (max-width: 768px) {
|
|
336
|
+
.sqd-detail-grid { grid-template-columns: 1fr; }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.sqd-detail-section { padding: 1.25rem; }
|
|
340
|
+
|
|
341
|
+
.sqd-section-title {
|
|
342
|
+
font-size: 13px;
|
|
343
|
+
font-weight: 600;
|
|
344
|
+
text-transform: uppercase;
|
|
345
|
+
letter-spacing: 0.05em;
|
|
346
|
+
color: var(--muted);
|
|
347
|
+
margin-bottom: 1rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.sqd-section-title--danger { color: var(--danger); }
|
|
351
|
+
|
|
352
|
+
.sqd-dl {
|
|
353
|
+
display: grid;
|
|
354
|
+
grid-template-columns: auto 1fr;
|
|
355
|
+
gap: 0.5rem 1.5rem;
|
|
356
|
+
font-size: 13px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.sqd-dl dt { color: var(--muted); white-space: nowrap; }
|
|
360
|
+
.sqd-dl dd { word-break: break-all; }
|
|
361
|
+
|
|
362
|
+
.sqd-pre {
|
|
363
|
+
font-family: monospace;
|
|
364
|
+
font-size: 12px;
|
|
365
|
+
background: var(--bg);
|
|
366
|
+
border: 1px solid var(--border);
|
|
367
|
+
border-radius: 5px;
|
|
368
|
+
padding: 0.75rem;
|
|
369
|
+
overflow-x: auto;
|
|
370
|
+
white-space: pre-wrap;
|
|
371
|
+
word-break: break-word;
|
|
372
|
+
max-height: 400px;
|
|
373
|
+
overflow-y: auto;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.sqd-pre--muted { color: var(--muted); }
|
|
377
|
+
|
|
378
|
+
.sqd-error-header {
|
|
379
|
+
font-size: 13px;
|
|
380
|
+
padding: 0.5rem 0.75rem;
|
|
381
|
+
background: #f8d7da;
|
|
382
|
+
color: #842029;
|
|
383
|
+
border-radius: 5px;
|
|
384
|
+
}
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class FailedJobsController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
@failed_jobs =
|
|
5
|
-
.includes(:job)
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
@pagy, @failed_jobs = pagy(
|
|
5
|
+
SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
|
|
6
|
+
)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def retry
|
|
10
|
+
execution = SolidQueue::FailedExecution.find(params[:id])
|
|
11
|
+
execution.retry
|
|
12
|
+
redirect_to failed_jobs_path, notice: "Job queued for retry."
|
|
13
|
+
rescue => e
|
|
14
|
+
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def destroy
|
|
18
|
+
execution = SolidQueue::FailedExecution.find(params[:id])
|
|
19
|
+
execution.discard
|
|
20
|
+
redirect_to failed_jobs_path, notice: "Job discarded."
|
|
21
|
+
rescue => e
|
|
22
|
+
redirect_to failed_jobs_path, alert: "Could not discard job: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retry_all
|
|
26
|
+
executions = SolidQueue::FailedExecution.includes(:job).to_a
|
|
27
|
+
jobs = executions.map(&:job)
|
|
28
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
29
|
+
redirect_to failed_jobs_path, notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def discard_all
|
|
33
|
+
count = SolidQueue::FailedExecution.count
|
|
34
|
+
SolidQueue::FailedExecution.discard_all_in_batches
|
|
35
|
+
redirect_to failed_jobs_path, notice: "#{count} #{"job".pluralize(count)} discarded."
|
|
8
36
|
end
|
|
9
37
|
end
|
|
10
38
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
3
|
STATUSES = %w[ready scheduled claimed blocked failed].freeze
|
|
4
|
+
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
4
5
|
|
|
5
6
|
def index
|
|
6
7
|
@status = params[:status].presence_in(STATUSES) || "ready"
|
|
@@ -15,7 +16,59 @@ module SolidQueueWeb
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
@jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present?
|
|
18
|
-
@jobs = @jobs.order(created_at: :desc)
|
|
19
|
+
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
@job = SolidQueue::Job
|
|
24
|
+
.includes(:ready_execution, :scheduled_execution, :claimed_execution, :blocked_execution, :failed_execution)
|
|
25
|
+
.find(params[:id])
|
|
26
|
+
@failed_execution = @job.failed_execution
|
|
27
|
+
@execution_status = derive_status(@job)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def destroy
|
|
31
|
+
execution = execution_model_for!(params[:status]).find(params[:id])
|
|
32
|
+
execution.discard
|
|
33
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]), notice: "Job discarded."
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
|
|
36
|
+
rescue => e
|
|
37
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard job: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def discard_all
|
|
41
|
+
model = execution_model_for!(params[:status])
|
|
42
|
+
scope = model.includes(:job)
|
|
43
|
+
scope = scope.where(jobs: { queue_name: params[:queue] }) if params[:queue].present?
|
|
44
|
+
jobs = scope.map(&:job)
|
|
45
|
+
model.discard_all_from_jobs(jobs)
|
|
46
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]),
|
|
47
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
48
|
+
rescue ArgumentError => e
|
|
49
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message
|
|
50
|
+
rescue => e
|
|
51
|
+
redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard jobs: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def derive_status(job)
|
|
57
|
+
return "failed" if job.failed_execution.present?
|
|
58
|
+
return "claimed" if job.claimed_execution.present?
|
|
59
|
+
return "blocked" if job.blocked_execution.present?
|
|
60
|
+
return "ready" if job.ready_execution.present?
|
|
61
|
+
return "scheduled" if job.scheduled_execution.present?
|
|
62
|
+
"finished"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def execution_model_for!(status)
|
|
66
|
+
case status
|
|
67
|
+
when "ready" then SolidQueue::ReadyExecution
|
|
68
|
+
when "scheduled" then SolidQueue::ScheduledExecution
|
|
69
|
+
when "blocked" then SolidQueue::BlockedExecution
|
|
70
|
+
else raise ArgumentError, "Cannot discard #{status} jobs from this page."
|
|
71
|
+
end
|
|
19
72
|
end
|
|
20
73
|
end
|
|
21
74
|
end
|
|
@@ -3,5 +3,21 @@ module SolidQueueWeb
|
|
|
3
3
|
def index
|
|
4
4
|
@queues = SolidQueue::Queue.all.sort_by(&:name)
|
|
5
5
|
end
|
|
6
|
+
|
|
7
|
+
def pause
|
|
8
|
+
queue = SolidQueue::Queue.find_by_name(params[:name])
|
|
9
|
+
queue.pause
|
|
10
|
+
redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
|
|
11
|
+
rescue => e
|
|
12
|
+
redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resume
|
|
16
|
+
queue = SolidQueue::Queue.find_by_name(params[:name])
|
|
17
|
+
queue.resume
|
|
18
|
+
redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
|
|
19
|
+
rescue => e
|
|
20
|
+
redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
|
|
21
|
+
end
|
|
6
22
|
end
|
|
7
23
|
end
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "" %></li>
|
|
19
19
|
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "" %></li>
|
|
20
20
|
<li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "" %></li>
|
|
21
|
+
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "" %></li>
|
|
21
22
|
</ul>
|
|
22
23
|
</nav>
|
|
23
24
|
</header>
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div class="sqd-page-header">
|
|
2
|
+
<h1 class="sqd-page-title">Failed Jobs</h1>
|
|
3
|
+
<% if @failed_jobs.any? %>
|
|
4
|
+
<div class="sqd-actions">
|
|
5
|
+
<%= button_to "Retry All", retry_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--primary",
|
|
6
|
+
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
|
|
7
|
+
<%= button_to "Discard All", discard_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--danger",
|
|
8
|
+
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<% if @pagy.last > 1 %>
|
|
14
|
+
<%= @pagy.series_nav.html_safe %>
|
|
15
|
+
<% end %>
|
|
2
16
|
|
|
3
17
|
<div class="sqd-card">
|
|
4
18
|
<% if @failed_jobs.empty? %>
|
|
@@ -11,13 +25,14 @@
|
|
|
11
25
|
<th>Queue</th>
|
|
12
26
|
<th>Error</th>
|
|
13
27
|
<th>Failed At</th>
|
|
28
|
+
<th></th>
|
|
14
29
|
</tr>
|
|
15
30
|
</thead>
|
|
16
31
|
<tbody>
|
|
17
32
|
<% @failed_jobs.each do |execution| %>
|
|
18
33
|
<% job = execution.job %>
|
|
19
34
|
<tr>
|
|
20
|
-
<td><%= job.class_name %></td>
|
|
35
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
21
36
|
<td class="sqd-mono"><%= job.queue_name %></td>
|
|
22
37
|
<td>
|
|
23
38
|
<% if execution.exception_class.present? %>
|
|
@@ -25,10 +40,17 @@
|
|
|
25
40
|
<strong><%= execution.exception_class %></strong>: <%= execution.message %>
|
|
26
41
|
</div>
|
|
27
42
|
<% else %>
|
|
28
|
-
<span
|
|
43
|
+
<span style="color:var(--muted)">—</span>
|
|
29
44
|
<% end %>
|
|
30
45
|
</td>
|
|
31
46
|
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
47
|
+
<td class="sqd-row-actions">
|
|
48
|
+
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
|
|
49
|
+
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
50
|
+
<%= button_to "Discard", failed_job_path(execution), method: :delete,
|
|
51
|
+
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
52
|
+
data: { confirm: "Discard this job?" } %>
|
|
53
|
+
</td>
|
|
32
54
|
</tr>
|
|
33
55
|
<% end %>
|
|
34
56
|
</tbody>
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-page-header">
|
|
4
|
+
<h1 class="sqd-page-title">Jobs</h1>
|
|
5
|
+
<% if discardable && @jobs.any? %>
|
|
6
|
+
<div class="sqd-actions">
|
|
7
|
+
<%= button_to "Discard All", discard_all_jobs_path,
|
|
8
|
+
method: :post,
|
|
9
|
+
params: { status: @status, queue: @queue },
|
|
10
|
+
class: "sqd-btn sqd-btn--danger",
|
|
11
|
+
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
2
15
|
|
|
3
16
|
<div class="sqd-filters">
|
|
4
17
|
<%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %>
|
|
@@ -20,6 +33,7 @@
|
|
|
20
33
|
<th>Priority</th>
|
|
21
34
|
<th>Scheduled At</th>
|
|
22
35
|
<th>Enqueued At</th>
|
|
36
|
+
<% if discardable %><th></th><% end %>
|
|
23
37
|
</tr>
|
|
24
38
|
</thead>
|
|
25
39
|
<tbody>
|
|
@@ -28,7 +42,7 @@
|
|
|
28
42
|
<tr>
|
|
29
43
|
<td>
|
|
30
44
|
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
31
|
-
|
|
45
|
+
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %>
|
|
32
46
|
</td>
|
|
33
47
|
<td>
|
|
34
48
|
<%= link_to job.queue_name, jobs_path(status: @status, queue: job.queue_name),
|
|
@@ -39,6 +53,15 @@
|
|
|
39
53
|
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
40
54
|
</td>
|
|
41
55
|
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
56
|
+
<% if discardable %>
|
|
57
|
+
<td class="sqd-row-actions">
|
|
58
|
+
<%= button_to "Discard", job_path(execution),
|
|
59
|
+
method: :delete,
|
|
60
|
+
params: { status: @status, queue: @queue },
|
|
61
|
+
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
62
|
+
data: { confirm: "Discard this job?" } %>
|
|
63
|
+
</td>
|
|
64
|
+
<% end %>
|
|
42
65
|
</tr>
|
|
43
66
|
<% end %>
|
|
44
67
|
</tbody>
|
|
@@ -46,9 +69,13 @@
|
|
|
46
69
|
<% end %>
|
|
47
70
|
</div>
|
|
48
71
|
|
|
72
|
+
<% if @pagy.last > 1 %>
|
|
73
|
+
<%= @pagy.series_nav.html_safe %>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
49
76
|
<% if @queue.present? %>
|
|
50
77
|
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
51
78
|
Filtering by queue: <strong><%= @queue %></strong> —
|
|
52
79
|
<%= link_to "Clear filter", jobs_path(status: @status) %>
|
|
53
80
|
</p>
|
|
54
|
-
<% end %>
|
|
81
|
+
<% end %>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div class="sqd-page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="sqd-breadcrumb">
|
|
4
|
+
<%= link_to "Jobs", jobs_path(status: @execution_status) %> › Detail
|
|
5
|
+
</div>
|
|
6
|
+
<h1 class="sqd-page-title"><%= @job.class_name %></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="sqd-actions">
|
|
10
|
+
<% if @execution_status == "failed" && @failed_execution %>
|
|
11
|
+
<%= button_to "Retry", retry_failed_job_path(@failed_execution), method: :post,
|
|
12
|
+
class: "sqd-btn sqd-btn--primary" %>
|
|
13
|
+
<%= button_to "Discard", failed_job_path(@failed_execution), method: :delete,
|
|
14
|
+
class: "sqd-btn sqd-btn--danger",
|
|
15
|
+
data: { confirm: "Discard this job?" } %>
|
|
16
|
+
<% elsif SolidQueueWeb::JobsController::DISCARDABLE.include?(@execution_status) %>
|
|
17
|
+
<% execution = @job.public_send("#{@execution_status}_execution") %>
|
|
18
|
+
<% if execution %>
|
|
19
|
+
<%= button_to "Discard", job_path(execution),
|
|
20
|
+
method: :delete,
|
|
21
|
+
params: { status: @execution_status },
|
|
22
|
+
class: "sqd-btn sqd-btn--danger",
|
|
23
|
+
data: { confirm: "Discard this job?" } %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="sqd-detail-grid">
|
|
30
|
+
<div class="sqd-card sqd-detail-section">
|
|
31
|
+
<h2 class="sqd-section-title">Details</h2>
|
|
32
|
+
<dl class="sqd-dl">
|
|
33
|
+
<dt>Status</dt>
|
|
34
|
+
<dd><span class="sqd-badge sqd-badge--<%= @execution_status %>"><%= @execution_status %></span></dd>
|
|
35
|
+
|
|
36
|
+
<dt>Queue</dt>
|
|
37
|
+
<dd class="sqd-mono"><%= @job.queue_name %></dd>
|
|
38
|
+
|
|
39
|
+
<dt>Priority</dt>
|
|
40
|
+
<dd><%= @job.priority %></dd>
|
|
41
|
+
|
|
42
|
+
<dt>Active Job ID</dt>
|
|
43
|
+
<dd class="sqd-mono sqd-truncate" title="<%= @job.active_job_id %>"><%= @job.active_job_id %></dd>
|
|
44
|
+
|
|
45
|
+
<dt>Concurrency Key</dt>
|
|
46
|
+
<dd class="sqd-mono"><%= @job.concurrency_key.presence || "—" %></dd>
|
|
47
|
+
|
|
48
|
+
<dt>Enqueued At</dt>
|
|
49
|
+
<dd class="sqd-mono"><%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></dd>
|
|
50
|
+
|
|
51
|
+
<dt>Scheduled At</dt>
|
|
52
|
+
<dd class="sqd-mono"><%= @job.scheduled_at ? @job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
|
|
53
|
+
|
|
54
|
+
<dt>Finished At</dt>
|
|
55
|
+
<dd class="sqd-mono"><%= @job.finished_at ? @job.finished_at.strftime("%Y-%m-%d %H:%M:%S %Z") : "—" %></dd>
|
|
56
|
+
</dl>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="sqd-card sqd-detail-section">
|
|
60
|
+
<h2 class="sqd-section-title">Arguments</h2>
|
|
61
|
+
<pre class="sqd-pre"><%= JSON.pretty_generate(@job.arguments) rescue @job.arguments.inspect %></pre>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<% if @failed_execution %>
|
|
66
|
+
<div class="sqd-card sqd-detail-section" style="margin-top: 1.5rem;">
|
|
67
|
+
<h2 class="sqd-section-title sqd-section-title--danger">Error</h2>
|
|
68
|
+
<p class="sqd-error-header">
|
|
69
|
+
<strong><%= @failed_execution.exception_class %></strong>: <%= @failed_execution.message %>
|
|
70
|
+
</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>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
<% end %>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<h1 class="sqd-page-title">Processes</h1>
|
|
2
|
+
|
|
3
|
+
<div class="sqd-card">
|
|
4
|
+
<% if @processes.empty? %>
|
|
5
|
+
<div class="sqd-empty">No processes registered.</div>
|
|
6
|
+
<% else %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
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>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @processes.each do |process| %>
|
|
21
|
+
<% stale = process.last_heartbeat_at < SolidQueue.process_alive_threshold.ago %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td><span class="sqd-badge sqd-badge--<%= process.kind.downcase %>"><%= process.kind %></span></td>
|
|
24
|
+
<td class="sqd-mono"><%= process.name %></td>
|
|
25
|
+
<td class="sqd-mono"><%= process.pid %></td>
|
|
26
|
+
<td class="sqd-mono"><%= process.hostname %></td>
|
|
27
|
+
<td class="sqd-process-meta">
|
|
28
|
+
<% meta = process.metadata || {} %>
|
|
29
|
+
<% if meta["queues"].present? %>
|
|
30
|
+
<span title="Queues"><%= Array(meta["queues"]).join(", ") %></span>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% if meta["thread_count"].present? %>
|
|
33
|
+
<span class="sqd-muted-text"><%= meta["thread_count"] %> threads</span>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% if meta["polling_interval"].present? %>
|
|
36
|
+
<span class="sqd-muted-text">every <%= meta["polling_interval"] %>s</span>
|
|
37
|
+
<% end %>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="sqd-mono sqd-muted-text" title="<%= process.last_heartbeat_at %>">
|
|
40
|
+
<%= time_ago_in_words(process.last_heartbeat_at) %> ago
|
|
41
|
+
</td>
|
|
42
|
+
<td>
|
|
43
|
+
<% if stale %>
|
|
44
|
+
<span class="sqd-badge sqd-badge--failed">Stale</span>
|
|
45
|
+
<% else %>
|
|
46
|
+
<span class="sqd-badge sqd-badge--running">Healthy</span>
|
|
47
|
+
<% end %>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<th>Size</th>
|
|
12
12
|
<th>Latency</th>
|
|
13
13
|
<th>Status</th>
|
|
14
|
+
<th></th>
|
|
14
15
|
</tr>
|
|
15
16
|
</thead>
|
|
16
17
|
<tbody>
|
|
@@ -26,6 +27,16 @@
|
|
|
26
27
|
<span class="sqd-badge sqd-badge--running">Running</span>
|
|
27
28
|
<% end %>
|
|
28
29
|
</td>
|
|
30
|
+
<td class="sqd-row-actions">
|
|
31
|
+
<% if queue.paused? %>
|
|
32
|
+
<%= button_to "Resume", resume_queue_path(queue.name), method: :post,
|
|
33
|
+
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<%= button_to "Pause", pause_queue_path(queue.name), method: :post,
|
|
36
|
+
class: "sqd-btn sqd-btn--muted sqd-btn--sm",
|
|
37
|
+
data: { confirm: "Pause queue \"#{queue.name}\"?" } %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</td>
|
|
29
40
|
</tr>
|
|
30
41
|
<% end %>
|
|
31
42
|
</tbody>
|
data/config/routes.rb
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
SolidQueueWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
3
|
|
|
4
|
-
resources :
|
|
5
|
-
resources :
|
|
6
|
-
|
|
4
|
+
resources :processes, only: [ :index ]
|
|
5
|
+
resources :queues, only: [ :index ], param: :name do
|
|
6
|
+
member do
|
|
7
|
+
post :pause
|
|
8
|
+
post :resume
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
resources :jobs, path: "list", only: [ :index, :show, :destroy ] do
|
|
12
|
+
collection do
|
|
13
|
+
post :discard_all
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
resources :failed_jobs, only: [ :index, :destroy ] do
|
|
17
|
+
collection do
|
|
18
|
+
post :retry_all
|
|
19
|
+
post :discard_all
|
|
20
|
+
end
|
|
21
|
+
member do
|
|
22
|
+
post :retry
|
|
23
|
+
end
|
|
24
|
+
end
|
|
7
25
|
end
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
require "solid_queue"
|
|
2
|
+
require "pagy"
|
|
3
|
+
require "pagy/toolbox/paginators/method"
|
|
2
4
|
|
|
3
5
|
module SolidQueueWeb
|
|
4
6
|
class Engine < ::Rails::Engine
|
|
5
7
|
isolate_namespace SolidQueueWeb
|
|
8
|
+
|
|
9
|
+
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
10
|
+
|
|
11
|
+
initializer "solid_queue_web.pagy" do
|
|
12
|
+
Pagy::OPTIONS[:limit] = 25
|
|
13
|
+
end
|
|
6
14
|
end
|
|
7
15
|
end
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -37,8 +37,23 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.0'
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pagy
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '43.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '43.0'
|
|
54
|
+
description: Mount SolidQueueWeb in any Rails app using Solid Queue to get a dashboard
|
|
55
|
+
for your queues, jobs by status, failed executions, and job actions (retry, discard)
|
|
56
|
+
— all without leaving your app.
|
|
42
57
|
email:
|
|
43
58
|
- eclectic-coding@users.noreply.github.com
|
|
44
59
|
executables: []
|
|
@@ -53,6 +68,7 @@ files:
|
|
|
53
68
|
- app/controllers/solid_queue_web/dashboard_controller.rb
|
|
54
69
|
- app/controllers/solid_queue_web/failed_jobs_controller.rb
|
|
55
70
|
- app/controllers/solid_queue_web/jobs_controller.rb
|
|
71
|
+
- app/controllers/solid_queue_web/processes_controller.rb
|
|
56
72
|
- app/controllers/solid_queue_web/queues_controller.rb
|
|
57
73
|
- app/helpers/solid_queue_web/application_helper.rb
|
|
58
74
|
- app/jobs/solid_queue_web/application_job.rb
|
|
@@ -61,6 +77,8 @@ files:
|
|
|
61
77
|
- app/views/solid_queue_web/dashboard/index.html.erb
|
|
62
78
|
- app/views/solid_queue_web/failed_jobs/index.html.erb
|
|
63
79
|
- app/views/solid_queue_web/jobs/index.html.erb
|
|
80
|
+
- app/views/solid_queue_web/jobs/show.html.erb
|
|
81
|
+
- app/views/solid_queue_web/processes/index.html.erb
|
|
64
82
|
- app/views/solid_queue_web/queues/index.html.erb
|
|
65
83
|
- config/routes.rb
|
|
66
84
|
- lib/solid_queue_web.rb
|
|
@@ -90,5 +108,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
108
|
requirements: []
|
|
91
109
|
rubygems_version: 4.0.10
|
|
92
110
|
specification_version: 4
|
|
93
|
-
summary: A
|
|
111
|
+
summary: A Rails engine dashboard for monitoring and managing Solid Queue jobs.
|
|
94
112
|
test_files: []
|