solid_stack_web 0.1.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 +22 -2
- data/app/assets/stylesheets/solid_stack_web/_02_layout.css +8 -0
- data/app/assets/stylesheets/solid_stack_web/_04_table.css +1 -0
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +13 -0
- data/app/assets/stylesheets/solid_stack_web/_08_filters.css +59 -0
- data/app/assets/stylesheets/solid_stack_web/_09_detail.css +85 -0
- data/app/controllers/solid_stack_web/application_controller.rb +5 -1
- data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
- data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +28 -0
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
- data/app/controllers/solid_stack_web/history_controller.rb +42 -0
- data/app/controllers/solid_stack_web/jobs/selections_controller.rb +24 -0
- data/app/controllers/solid_stack_web/jobs_controller.rb +60 -21
- data/app/controllers/solid_stack_web/queues/pauses_controller.rb +13 -0
- data/app/controllers/solid_stack_web/queues_controller.rb +9 -8
- data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +18 -0
- data/app/controllers/solid_stack_web/recurring_tasks_controller.rb +7 -0
- data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +52 -0
- data/app/helpers/solid_stack_web/application_helper.rb +8 -0
- data/app/javascript/solid_stack_web/application.js +6 -0
- data/app/javascript/solid_stack_web/selection_controller.js +42 -0
- data/app/models/solid_stack_web/job.rb +21 -0
- data/app/views/layouts/solid_stack_web/application.html.erb +5 -0
- data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +9 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +61 -30
- data/app/views/solid_stack_web/failed_jobs/show.html.erb +58 -0
- data/app/views/solid_stack_web/history/index.html.erb +73 -0
- data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +2 -2
- data/app/views/solid_stack_web/jobs/index.html.erb +138 -34
- data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
- data/app/views/solid_stack_web/queues/index.html.erb +4 -4
- data/app/views/solid_stack_web/queues/show.html.erb +67 -0
- data/app/views/solid_stack_web/recurring_tasks/index.html.erb +67 -0
- data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +9 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +23 -7
- data/lib/solid_stack_web/engine.rb +15 -0
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +64 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 777352d5919952ea022a545b13608ace8f35e0f91077431c45a679e5c30bec19
|
|
4
|
+
data.tar.gz: d36dbf96b4234d730249df1d259099883d0473c160c62ab9bee73fe88ff133c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eefbb43300e8e244b6354d1fb952edc7577585c76f5721920d6fa28aba41038840d4924cae9799903a3742e0782a2e8f95d8b142502194d1726e87fbb8c35e7a
|
|
7
|
+
data.tar.gz: 254f13440a71b0dcfbeb3330f2c1be4c21a5e038e09355493bba1fe5b3767b70d314ba37982d7adced753915a971d159d06bdc29741e5c37e223e520184f34e9
|
data/README.md
CHANGED
|
@@ -9,8 +9,13 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol
|
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
- **Overview dashboard** with live counts across all three Solid Stack components
|
|
13
|
-
- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked), manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes
|
|
12
|
+
- **Overview dashboard** with live counts across all three Solid Stack components; cards are clickable and link directly to each section
|
|
13
|
+
- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters; **Per-queue browser** — click any queue name or size to drill into its ready jobs with per-row and bulk discard
|
|
14
|
+
- **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters
|
|
15
|
+
- **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" in the header back-dates all matching executions at once
|
|
16
|
+
- **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button that immediately enqueues the task
|
|
17
|
+
- **Job detail page** — drill into any job to see full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button
|
|
18
|
+
- **Failed job detail page** — drill into any failed job to see the full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
|
|
14
19
|
- **Solid Cache** — entry count and total byte size at a glance
|
|
15
20
|
- **Solid Cable** — active message count and distinct channel count
|
|
16
21
|
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
@@ -56,6 +61,19 @@ SolidStackWeb.configure do |config|
|
|
|
56
61
|
end
|
|
57
62
|
```
|
|
58
63
|
|
|
64
|
+
### Job Filtering
|
|
65
|
+
|
|
66
|
+
The jobs list supports four independent filters, all driven by query params:
|
|
67
|
+
|
|
68
|
+
| Param | Description |
|
|
69
|
+
|-------|-------------|
|
|
70
|
+
| `q` | Substring match against the job class name (e.g. `q=Report`) |
|
|
71
|
+
| `queue` | Exact queue name match; select appears only when multiple queues exist |
|
|
72
|
+
| `priority` | Exact priority value match; select appears only when multiple priorities exist |
|
|
73
|
+
| `period` | Enqueued-at window — `1h`, `24h`, `7d`, or omit for all time |
|
|
74
|
+
|
|
75
|
+
Filters are preserved when switching between status tabs (Ready / Scheduled / Running / Blocked) and when discarding a job. They can be combined freely.
|
|
76
|
+
|
|
59
77
|
### Authentication
|
|
60
78
|
|
|
61
79
|
The `authenticate` block is evaluated in the context of each request's controller instance, so any helper method available to controllers (e.g. `current_user` from Devise) works directly. If the block returns `false` or `nil`, the engine falls back to HTTP Basic authentication. If no `authenticate` block is configured, the dashboard is open.
|
|
@@ -67,6 +85,8 @@ The `authenticate` block is evaluated in the context of each request's controlle
|
|
|
67
85
|
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
|
|
68
86
|
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
69
87
|
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
88
|
+
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
89
|
+
- [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
|
|
70
90
|
|
|
71
91
|
## Contributing
|
|
72
92
|
|
|
@@ -72,12 +72,20 @@
|
|
|
72
72
|
|
|
73
73
|
.sqw-page-header { margin-bottom: 1.25rem; }
|
|
74
74
|
.sqw-page-title { font-size: 20px; font-weight: 600; }
|
|
75
|
+
.sqw-page-title-row { display: flex; align-items: center; gap: 0.5rem; }
|
|
76
|
+
|
|
77
|
+
@keyframes sqw-flash-dismiss {
|
|
78
|
+
0%, 86% { opacity: 1; max-height: 200px; margin-bottom: 1rem; }
|
|
79
|
+
100% { opacity: 0; max-height: 0; margin-bottom: 0; padding: 0; }
|
|
80
|
+
}
|
|
75
81
|
|
|
76
82
|
.sqw-flash {
|
|
77
83
|
padding: 0.75rem 1rem;
|
|
78
84
|
border-radius: var(--radius);
|
|
79
85
|
margin-bottom: 1rem;
|
|
80
86
|
font-size: 13px;
|
|
87
|
+
animation: sqw-flash-dismiss 7s ease forwards;
|
|
88
|
+
overflow: hidden;
|
|
81
89
|
}
|
|
82
90
|
.sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
|
|
83
91
|
.sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
border-radius: var(--radius);
|
|
13
13
|
box-shadow: var(--shadow);
|
|
14
14
|
overflow: hidden;
|
|
15
|
+
position: relative;
|
|
16
|
+
transition: box-shadow 0.15s;
|
|
15
17
|
}
|
|
18
|
+
.sqw-gem-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); }
|
|
16
19
|
|
|
17
20
|
.sqw-gem-card--queue { border-top-color: var(--primary); }
|
|
18
21
|
.sqw-gem-card--cache { border-top-color: var(--purple); }
|
|
@@ -41,6 +44,16 @@
|
|
|
41
44
|
}
|
|
42
45
|
.sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; }
|
|
43
46
|
|
|
47
|
+
/* Stretch the header link to cover the whole card */
|
|
48
|
+
.sqw-gem-card__link::after {
|
|
49
|
+
content: "";
|
|
50
|
+
position: absolute;
|
|
51
|
+
inset: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Stat links and other interactive elements sit above the overlay */
|
|
55
|
+
.sqw-inline-stat { position: relative; z-index: 1; }
|
|
56
|
+
|
|
44
57
|
.sqw-gem-card__body {
|
|
45
58
|
display: grid;
|
|
46
59
|
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.sqw-filters {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 0.5rem;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
margin-bottom: 1rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sqw-search-input {
|
|
10
|
+
padding: 0.35rem 0.75rem;
|
|
11
|
+
font-size: 13px;
|
|
12
|
+
border: 1px solid var(--border);
|
|
13
|
+
border-radius: var(--radius);
|
|
14
|
+
background: var(--surface);
|
|
15
|
+
color: var(--text);
|
|
16
|
+
min-width: 200px;
|
|
17
|
+
}
|
|
18
|
+
.sqw-search-input:focus {
|
|
19
|
+
outline: 2px solid var(--primary);
|
|
20
|
+
outline-offset: -1px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.sqw-select {
|
|
24
|
+
padding: 0.35rem 0.6rem;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
border: 1px solid var(--border);
|
|
27
|
+
border-radius: var(--radius);
|
|
28
|
+
background: var(--surface);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.sqw-period-filter {
|
|
34
|
+
display: flex;
|
|
35
|
+
border: 1px solid var(--border);
|
|
36
|
+
border-radius: var(--radius);
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
margin-left: auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sqw-period-btn {
|
|
42
|
+
padding: 0.35rem 0.65rem;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
background: var(--surface);
|
|
47
|
+
}
|
|
48
|
+
.sqw-period-btn + .sqw-period-btn {
|
|
49
|
+
border-left: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
.sqw-period-btn:hover:not(.sqw-period-btn--active) {
|
|
52
|
+
background: var(--bg);
|
|
53
|
+
color: var(--text);
|
|
54
|
+
text-decoration: none;
|
|
55
|
+
}
|
|
56
|
+
.sqw-period-btn--active {
|
|
57
|
+
background: var(--primary);
|
|
58
|
+
color: #fff;
|
|
59
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
.sqw-detail-card {
|
|
2
|
+
background: var(--surface);
|
|
3
|
+
border: 1px solid var(--border);
|
|
4
|
+
border-radius: var(--radius);
|
|
5
|
+
box-shadow: var(--shadow);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sqw-breadcrumb {
|
|
10
|
+
font-size: 12px;
|
|
11
|
+
color: var(--muted);
|
|
12
|
+
margin-bottom: 0.25rem;
|
|
13
|
+
}
|
|
14
|
+
.sqw-breadcrumb a { color: var(--muted); text-decoration: none; }
|
|
15
|
+
.sqw-breadcrumb a:hover { color: var(--text); }
|
|
16
|
+
|
|
17
|
+
.sqw-page-header--split {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.sqw-detail-actions,
|
|
24
|
+
.sqw-header-actions {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Two-column detail layout: Details card | Arguments card */
|
|
30
|
+
.sqw-detail-grid {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: 1fr 1fr;
|
|
33
|
+
gap: 1.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sqw-detail-section {
|
|
37
|
+
padding: 1.25rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sqw-section-title {
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: .05em;
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
margin-bottom: 0.75rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Definition list — dt naturally sized, dd takes the rest */
|
|
50
|
+
.sqw-dl {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: auto 1fr;
|
|
53
|
+
gap: 0.5rem 1.5rem;
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
}
|
|
56
|
+
.sqw-dl dt { color: var(--muted); white-space: nowrap; }
|
|
57
|
+
.sqw-dl dd { word-break: break-all; }
|
|
58
|
+
|
|
59
|
+
.sqw-code-block {
|
|
60
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
background: var(--bg);
|
|
63
|
+
border: 1px solid var(--border);
|
|
64
|
+
border-radius: var(--radius);
|
|
65
|
+
padding: 0.75rem;
|
|
66
|
+
overflow-x: auto;
|
|
67
|
+
white-space: pre-wrap;
|
|
68
|
+
word-break: break-word;
|
|
69
|
+
max-height: 400px;
|
|
70
|
+
overflow-y: auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sqw-code-input {
|
|
74
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
background: var(--bg);
|
|
77
|
+
border: 1px solid var(--border);
|
|
78
|
+
border-radius: var(--radius);
|
|
79
|
+
padding: 0.75rem;
|
|
80
|
+
width: 100%;
|
|
81
|
+
resize: vertical;
|
|
82
|
+
color: var(--text);
|
|
83
|
+
line-height: 1.5;
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
require "csv"
|
|
2
|
+
|
|
1
3
|
module SolidStackWeb
|
|
2
4
|
class ApplicationController < ActionController::Base
|
|
3
5
|
include Pagy::Method
|
|
4
6
|
|
|
7
|
+
PERIOD_DURATIONS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
|
|
8
|
+
|
|
5
9
|
before_action :authenticate!
|
|
6
10
|
around_action :with_database_connection
|
|
7
11
|
|
|
@@ -11,7 +15,7 @@ module SolidStackWeb
|
|
|
11
15
|
|
|
12
16
|
def current_section
|
|
13
17
|
case controller_name
|
|
14
|
-
when "jobs", "failed_jobs", "queues", "processes" then :queue
|
|
18
|
+
when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue
|
|
15
19
|
when "cache" then :cache
|
|
16
20
|
when "cable" then :cable
|
|
17
21
|
else :overview
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
module FailedJobs
|
|
3
|
+
class ArgumentsController < ApplicationController
|
|
4
|
+
def update
|
|
5
|
+
@execution = SolidQueue::FailedExecution.includes(:job).find(params[:failed_job_id])
|
|
6
|
+
new_arguments = JSON.parse(params[:arguments])
|
|
7
|
+
@execution.job.update!(arguments: new_arguments)
|
|
8
|
+
@execution.retry
|
|
9
|
+
redirect_to failed_jobs_path, notice: "Arguments updated and job queued for retry."
|
|
10
|
+
rescue JSON::ParserError
|
|
11
|
+
redirect_to failed_job_path(@execution), alert: "Invalid JSON — arguments were not saved."
|
|
12
|
+
rescue => e
|
|
13
|
+
redirect_to failed_jobs_path, alert: "Could not update job: #{e.message}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
module FailedJobs
|
|
3
|
+
class SelectionsController < ApplicationController
|
|
4
|
+
before_action :set_ids
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
|
|
8
|
+
redirect_to failed_jobs_path
|
|
9
|
+
rescue => e
|
|
10
|
+
redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def destroy
|
|
14
|
+
job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
|
|
15
|
+
SolidQueue::Job.where(id: job_ids).destroy_all
|
|
16
|
+
redirect_to failed_jobs_path
|
|
17
|
+
rescue => e
|
|
18
|
+
redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def set_ids
|
|
24
|
+
@ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
class FailedJobsController < ApplicationController
|
|
3
3
|
def index
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
respond_to do |format|
|
|
5
|
+
format.html do
|
|
6
|
+
scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
|
|
7
|
+
@pagy, @executions = pagy(scope)
|
|
8
|
+
end
|
|
9
|
+
format.csv do
|
|
10
|
+
send_data failed_jobs_csv,
|
|
11
|
+
filename: "failed-jobs-#{Date.today}.csv",
|
|
12
|
+
type: "text/csv", disposition: "attachment"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@execution = ::SolidQueue::FailedExecution.includes(:job).find(params[:id])
|
|
19
|
+
@arguments = JSON.pretty_generate(@execution.job.arguments) if @execution.job.arguments.present?
|
|
20
|
+
rescue JSON::GeneratorError
|
|
21
|
+
@arguments = @execution.job.arguments.to_s
|
|
6
22
|
end
|
|
7
23
|
|
|
8
24
|
def destroy
|
|
9
|
-
execution = ::SolidQueue::FailedExecution.find(params[:id])
|
|
10
|
-
execution.job.destroy!
|
|
25
|
+
@execution = ::SolidQueue::FailedExecution.find(params[:id])
|
|
26
|
+
@execution.job.destroy!
|
|
11
27
|
@executions_remain = ::SolidQueue::FailedExecution.exists?
|
|
12
28
|
|
|
13
29
|
respond_to do |format|
|
|
@@ -21,5 +37,20 @@ module SolidStackWeb
|
|
|
21
37
|
execution.retry
|
|
22
38
|
redirect_to failed_jobs_path
|
|
23
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def failed_jobs_csv
|
|
44
|
+
CSV.generate(headers: true) do |csv|
|
|
45
|
+
csv << %w[id class_name queue_name error_class error_message failed_at]
|
|
46
|
+
::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc).each do |execution|
|
|
47
|
+
job = execution.job
|
|
48
|
+
error = execution.error || {}
|
|
49
|
+
csv << [job.id, job.class_name, job.queue_name,
|
|
50
|
+
error["exception_class"], error["message"],
|
|
51
|
+
execution.created_at.iso8601]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
24
55
|
end
|
|
25
56
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class HistoryController < ApplicationController
|
|
3
|
+
before_action :set_filters
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
respond_to do |format|
|
|
7
|
+
format.html { @pagy, @jobs = pagy(filtered_scope) }
|
|
8
|
+
format.csv do
|
|
9
|
+
send_data history_csv(filtered_scope),
|
|
10
|
+
filename: "job-history-#{Date.today}.csv",
|
|
11
|
+
type: "text/csv", disposition: "attachment"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def set_filters
|
|
19
|
+
@queue = params[:queue].presence
|
|
20
|
+
@search = params[:q].presence
|
|
21
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def filtered_scope
|
|
25
|
+
scope = SolidQueue::Job.where.not(finished_at: nil).order(finished_at: :desc)
|
|
26
|
+
scope = scope.where(queue_name: @queue) if @queue.present?
|
|
27
|
+
scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
28
|
+
scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
29
|
+
scope
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def history_csv(scope)
|
|
33
|
+
CSV.generate(headers: true) do |csv|
|
|
34
|
+
csv << %w[id class_name queue_name duration_seconds finished_at]
|
|
35
|
+
scope.order(finished_at: :desc).each do |job|
|
|
36
|
+
duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
|
|
37
|
+
csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
module Jobs
|
|
3
|
+
class SelectionsController < ApplicationController
|
|
4
|
+
def destroy
|
|
5
|
+
status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
6
|
+
raise ArgumentError, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
|
|
7
|
+
|
|
8
|
+
ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
9
|
+
job_ids = Job::EXECUTION_MODELS[status].where(id: ids).pluck(:job_id)
|
|
10
|
+
SolidQueue::Job.where(id: job_ids).destroy_all
|
|
11
|
+
|
|
12
|
+
redirect_to jobs_path(
|
|
13
|
+
status: status,
|
|
14
|
+
q: params[:q].presence,
|
|
15
|
+
queue: params[:queue].presence,
|
|
16
|
+
period: params[:period].presence_in(PERIOD_DURATIONS.keys),
|
|
17
|
+
priority: params[:priority].presence
|
|
18
|
+
)
|
|
19
|
+
rescue ArgumentError => e
|
|
20
|
+
redirect_to jobs_path(status: params[:status]), alert: e.message
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,42 +1,81 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
EXECUTION_MODELS = {
|
|
4
|
-
"ready" => ::SolidQueue::ReadyExecution,
|
|
5
|
-
"scheduled" => ::SolidQueue::ScheduledExecution,
|
|
6
|
-
"claimed" => ::SolidQueue::ClaimedExecution,
|
|
7
|
-
"blocked" => ::SolidQueue::BlockedExecution
|
|
8
|
-
}.freeze
|
|
9
|
-
|
|
10
|
-
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
11
|
-
|
|
12
3
|
before_action :set_status
|
|
13
|
-
before_action :
|
|
4
|
+
before_action :set_filters, only: [:index, :destroy]
|
|
5
|
+
before_action :require_discardable, only: [:destroy]
|
|
14
6
|
|
|
15
7
|
def index
|
|
16
|
-
|
|
17
|
-
@
|
|
8
|
+
@queue_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.queue_name").sort
|
|
9
|
+
@priority_options = Job::EXECUTION_MODELS[@status].joins(:job).distinct.pluck("solid_queue_jobs.priority").sort
|
|
10
|
+
|
|
11
|
+
respond_to do |format|
|
|
12
|
+
format.html { @pagy, @executions = pagy(filtered_scope) }
|
|
13
|
+
format.csv do
|
|
14
|
+
send_data jobs_csv,
|
|
15
|
+
filename: "jobs-#{@status}-#{Date.today}.csv",
|
|
16
|
+
type: "text/csv", disposition: "attachment"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
@execution = Job::EXECUTION_MODELS[@status].includes(:job).find(params[:id])
|
|
23
|
+
@arguments = JSON.parse(@execution.job.arguments) if @execution.job.arguments.present?
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
@arguments = nil
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
def destroy
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
if params[:id]
|
|
30
|
+
@execution = Job::EXECUTION_MODELS[@status].find(params[:id])
|
|
31
|
+
@execution.job.destroy!
|
|
32
|
+
@executions_remain = Job::EXECUTION_MODELS[@status].exists?
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
respond_to do |format|
|
|
35
|
+
format.html { redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority) }
|
|
36
|
+
format.turbo_stream
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
job_ids = filtered_scope.pluck(:job_id)
|
|
40
|
+
SolidQueue::Job.where(id: job_ids).destroy_all
|
|
41
|
+
redirect_to jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority)
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
45
|
private
|
|
33
46
|
|
|
34
47
|
def set_status
|
|
35
|
-
@status = params[:status].presence_in(
|
|
48
|
+
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def set_filters
|
|
52
|
+
@search = params[:q].presence
|
|
53
|
+
@queue = params[:queue].presence
|
|
54
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
55
|
+
@priority = params[:priority].presence
|
|
36
56
|
end
|
|
37
57
|
|
|
38
58
|
def require_discardable
|
|
39
|
-
head :
|
|
59
|
+
head :unprocessable_content unless Job::DISCARDABLE.include?(@status)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def jobs_csv
|
|
63
|
+
CSV.generate(headers: true) do |csv|
|
|
64
|
+
csv << %w[id class_name queue_name status priority enqueued_at]
|
|
65
|
+
filtered_scope.each do |execution|
|
|
66
|
+
job = execution.job
|
|
67
|
+
csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def filtered_scope
|
|
73
|
+
scope = Job::EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
|
|
74
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
75
|
+
scope = scope.references(:job).where("solid_queue_jobs.queue_name = ?", @queue) if @queue.present?
|
|
76
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
77
|
+
scope = scope.references(:job).where("solid_queue_jobs.priority = ?", @priority.to_i) if @priority.present?
|
|
78
|
+
scope
|
|
40
79
|
end
|
|
41
80
|
end
|
|
42
81
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class Queues::PausesController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
::SolidQueue::Pause.find_or_create_by!(queue_name: params[:queue_id])
|
|
5
|
+
redirect_back_or_to queues_path
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def destroy
|
|
9
|
+
::SolidQueue::Pause.find_by(queue_name: params[:queue_id])&.destroy
|
|
10
|
+
redirect_back_or_to queues_path
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -13,14 +13,15 @@ module SolidStackWeb
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
def show
|
|
17
|
+
@queue_name = params[:id]
|
|
18
|
+
@paused = ::SolidQueue::Pause.exists?(queue_name: @queue_name)
|
|
19
|
+
@pagy, @executions = pagy(
|
|
20
|
+
::SolidQueue::ReadyExecution
|
|
21
|
+
.where(queue_name: @queue_name)
|
|
22
|
+
.includes(:job)
|
|
23
|
+
.order(created_at: :desc)
|
|
24
|
+
)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class RecurringTasks::RunsController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
task = SolidQueue::RecurringTask.find_by!(key: params[:recurring_task_key])
|
|
5
|
+
result = task.enqueue(at: Time.current)
|
|
6
|
+
|
|
7
|
+
if result
|
|
8
|
+
redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
|
|
9
|
+
else
|
|
10
|
+
redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
|
|
11
|
+
end
|
|
12
|
+
rescue ActiveRecord::RecordNotFound
|
|
13
|
+
redirect_to recurring_tasks_path, alert: "Recurring task not found."
|
|
14
|
+
rescue => e
|
|
15
|
+
redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|