solid_queue_web 0.7.0 → 0.8.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 +26 -12
- data/app/assets/stylesheets/solid_queue_web/_02_layout.css +29 -1
- data/app/assets/stylesheets/solid_queue_web/_12_dark_mode.css +34 -0
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +16 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -22
- data/app/controllers/solid_queue_web/history_controller.rb +20 -1
- data/app/controllers/solid_queue_web/jobs_controller.rb +38 -23
- data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +31 -0
- data/app/controllers/solid_queue_web/search_controller.rb +1 -3
- data/app/javascript/solid_queue_web/application.js +2 -0
- data/app/javascript/solid_queue_web/theme_controller.js +26 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +11 -7
- data/app/views/solid_queue_web/dashboard/index.html.erb +28 -6
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +2 -0
- data/app/views/solid_queue_web/history/index.html.erb +7 -1
- data/app/views/solid_queue_web/jobs/index.html.erb +11 -7
- data/app/views/solid_queue_web/processes/index.html.erb +1 -1
- data/app/views/solid_queue_web/search/index.html.erb +2 -2
- data/config/importmap.rb +1 -0
- data/config/routes.rb +6 -4
- data/lib/solid_queue_web/engine.rb +4 -2
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +22 -0
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6897882e9e9b87a677fe9c6af855b33395cbf4bf2d13fb55086f461d26255f7d
|
|
4
|
+
data.tar.gz: 1b6882dce3a746811ba9553d0fb2174257c3f13146d5a448fd793c8c60a76661
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d19bc85fc4350e8ecb194111c87a7b882d1e5929142ec94211bcf6fef23840810ec4eb51ae4d957a3c2288263bba9955aabca9ea1874f4d3b7f6d34590f54a28
|
|
7
|
+
data.tar.gz: 3516ec14c55cd3839bf22d721a58d6a68016b0891b09088e5463e8f2e61e611b77d9eaca461ea49c6ffc225873589880b678fb829b53a877683e32de3d75456b
|
data/README.md
CHANGED
|
@@ -44,6 +44,9 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch
|
|
|
44
44
|
- **Global search** — search across all job statuses at once by class name substring; results grouped by status with match count and direct links to filtered views; native datalist autocomplete pre-populated from all known job classes; auto-submits on selection
|
|
45
45
|
- **Targeted bulk actions** — checkboxes on the jobs and failed jobs lists for selecting individual rows; selection bar shows count and action buttons ("Discard Selected" for jobs, "Retry Selected" / "Discard Selected" for failed jobs); select-all checkbox in the table header
|
|
46
46
|
- **Job history** — browsable list of all finished jobs with class name, queue, duration, and finished timestamp; filterable by period (1h / 24h / 7d), queue, and class name search; Done (1h) / Done (24h) dashboard cards link directly to the filtered history view; auto-refreshes every 10 seconds
|
|
47
|
+
- **Dark mode** — ☽/☀ toggle in the header; preference persists to `localStorage` and defaults to the OS `prefers-color-scheme` on first visit; zero extra dependencies — implemented via CSS custom properties and a small Stimulus controller
|
|
48
|
+
- **Dashboard quick actions** — "Retry All Failed" and "Discard All Blocked" cards appear on the dashboard only when the respective count is non-zero; one-click bulk operations with confirm dialogs, keeping the dashboard clean when everything is healthy
|
|
49
|
+
- **CSV export** — "Export CSV" button on the jobs, failed jobs, and history pages downloads all records matching the current filters; columns are tailored per view
|
|
47
50
|
|
|
48
51
|
## Screenshots
|
|
49
52
|
|
|
@@ -81,13 +84,20 @@ Add to your `config/routes.rb`:
|
|
|
81
84
|
mount SolidQueueWeb::Engine, at: "/jobs"
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
The dashboard will be available at `/jobs`.
|
|
87
|
+
The dashboard will be available at `/jobs`.
|
|
85
88
|
|
|
86
|
-
##
|
|
89
|
+
## Configuration
|
|
87
90
|
|
|
88
|
-
|
|
91
|
+
All settings are optional — the dashboard works with zero configuration. Create `config/initializers/solid_queue_web.rb` to customize behavior:
|
|
89
92
|
|
|
90
93
|
```ruby
|
|
94
|
+
SolidQueueWeb.configure do |config|
|
|
95
|
+
config.page_size = 50 # rows per page across all paginated views (default: 25)
|
|
96
|
+
config.dashboard_refresh_interval = 10_000 # dashboard auto-refresh in ms (default: 5_000)
|
|
97
|
+
config.default_refresh_interval = 30_000 # jobs/processes/history auto-refresh in ms (default: 10_000)
|
|
98
|
+
config.search_results_limit = 10 # max results per status in global search (default: 25)
|
|
99
|
+
end
|
|
100
|
+
|
|
91
101
|
SolidQueueWeb.authenticate do
|
|
92
102
|
# Called in the context of ApplicationController — use any helper available there.
|
|
93
103
|
# Return a truthy value to allow access, falsy to deny (triggers HTTP Basic prompt).
|
|
@@ -95,22 +105,26 @@ SolidQueueWeb.authenticate do
|
|
|
95
105
|
end
|
|
96
106
|
```
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
No authentication is enforced by default. When the `authenticate` block returns falsy, HTTP Basic auth is used as a fallback.
|
|
99
109
|
|
|
100
110
|
## Roadmap
|
|
101
111
|
|
|
102
|
-
Planned features, roughly ordered by priority:
|
|
112
|
+
Planned features, roughly ordered by priority:
|
|
103
113
|
|
|
104
|
-
**
|
|
105
|
-
-
|
|
114
|
+
**Observability**
|
|
115
|
+
- Job failure rate chart — sparkline per queue showing failure percentage over time, mirroring the throughput chart
|
|
116
|
+
- Queue depth trend — historical queue size over time, not just the current snapshot
|
|
117
|
+
- Slow job detection — flag jobs exceeding a configurable duration threshold
|
|
106
118
|
|
|
107
|
-
**
|
|
108
|
-
-
|
|
109
|
-
-
|
|
119
|
+
**Operations**
|
|
120
|
+
- Scheduled job management — reschedule a job to run immediately, or push its `scheduled_at` forward
|
|
121
|
+
- Bulk retry with delay — retry all failed jobs with a configurable stagger to avoid thundering herd
|
|
122
|
+
- Admin audit log — record who retried or discarded which jobs and when (requires host-app user identity)
|
|
110
123
|
|
|
111
|
-
**
|
|
112
|
-
- CSV export of any filtered view (jobs, failed jobs, history)
|
|
124
|
+
**Infrastructure**
|
|
113
125
|
- Webhook / alert config — POST to a URL when the failure count exceeds a threshold
|
|
126
|
+
- Multi-database support — when Solid Queue runs on a separate database from the host app
|
|
127
|
+
- Read replica support — route dashboard queries to a replica to avoid impacting the primary
|
|
114
128
|
|
|
115
129
|
Pull requests for any of these are welcome. See [Contributing](#contributing) below.
|
|
116
130
|
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
height: 56px;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
.sqd-header__controls {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 0.5rem;
|
|
20
|
+
margin-left: auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
.sqd-header__title {
|
|
17
24
|
font-size: 16px;
|
|
18
25
|
font-weight: 600;
|
|
@@ -43,6 +50,28 @@
|
|
|
43
50
|
color: var(--text);
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
.sqd-theme-toggle {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
width: 32px;
|
|
58
|
+
height: 32px;
|
|
59
|
+
padding: 0;
|
|
60
|
+
background: none;
|
|
61
|
+
border: 1px solid var(--border);
|
|
62
|
+
border-radius: 5px;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
font-size: 16px;
|
|
65
|
+
color: var(--text);
|
|
66
|
+
line-height: 1;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
transition: background 0.1s, border-color 0.1s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sqd-theme-toggle:hover {
|
|
72
|
+
background: var(--bg);
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
.sqd-nav-toggle {
|
|
47
76
|
display: none;
|
|
48
77
|
flex-direction: column;
|
|
@@ -51,7 +80,6 @@
|
|
|
51
80
|
width: 36px;
|
|
52
81
|
height: 36px;
|
|
53
82
|
padding: 6px;
|
|
54
|
-
margin-left: auto;
|
|
55
83
|
background: none;
|
|
56
84
|
border: 1px solid var(--border);
|
|
57
85
|
border-radius: 5px;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[data-theme="dark"] {
|
|
2
|
+
--bg: #0d1117;
|
|
3
|
+
--surface: #161b22;
|
|
4
|
+
--border: #30363d;
|
|
5
|
+
--text: #e6edf3;
|
|
6
|
+
--muted: #8b949e;
|
|
7
|
+
--primary: #58a6ff;
|
|
8
|
+
--danger: #f85149;
|
|
9
|
+
--warning: #d29922;
|
|
10
|
+
--success: #3fb950;
|
|
11
|
+
--info: #39c5cf;
|
|
12
|
+
--purple: #bc8cff;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[data-theme="dark"] .sqd-badge--ready { background: #1b3a2b; color: #3fb950; }
|
|
16
|
+
[data-theme="dark"] .sqd-badge--scheduled { background: #0e2a33; color: #39c5cf; }
|
|
17
|
+
[data-theme="dark"] .sqd-badge--claimed { background: #112040; color: #58a6ff; }
|
|
18
|
+
[data-theme="dark"] .sqd-badge--failed { background: #3d1118; color: #f85149; }
|
|
19
|
+
[data-theme="dark"] .sqd-badge--blocked { background: #2d2010; color: #d29922; }
|
|
20
|
+
[data-theme="dark"] .sqd-badge--static { background: #1b3a2b; color: #3fb950; }
|
|
21
|
+
[data-theme="dark"] .sqd-badge--dynamic { background: #2c1f45; color: #bc8cff; }
|
|
22
|
+
[data-theme="dark"] .sqd-badge--paused { background: #2d2d2d; color: #8b949e; }
|
|
23
|
+
[data-theme="dark"] .sqd-badge--running { background: #1b3a2b; color: #3fb950; }
|
|
24
|
+
[data-theme="dark"] .sqd-badge--supervisor { background: #2c1f45; color: #bc8cff; }
|
|
25
|
+
[data-theme="dark"] .sqd-badge--worker { background: #1b3a2b; color: #3fb950; }
|
|
26
|
+
[data-theme="dark"] .sqd-badge--dispatcher { background: #0e2a33; color: #39c5cf; }
|
|
27
|
+
|
|
28
|
+
[data-theme="dark"] .sqd-flash--notice { background: #1b3a2b; color: #3fb950; border-color: #2d6a4f; }
|
|
29
|
+
[data-theme="dark"] .sqd-flash--alert { background: #3d1118; color: #f85149; border-color: #6a2030; }
|
|
30
|
+
|
|
31
|
+
[data-theme="dark"] .sqd-theme-toggle {
|
|
32
|
+
border-color: var(--border);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
}
|
|
@@ -24,5 +24,21 @@ module SolidQueueWeb
|
|
|
24
24
|
finished_times.count { |t| t >= from && t < to }
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
def retry_all_failed
|
|
29
|
+
jobs = SolidQueue::FailedExecution.includes(:job).map(&:job)
|
|
30
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
31
|
+
redirect_to root_path, notice: "#{jobs.size} failed #{"job".pluralize(jobs.size)} queued for retry."
|
|
32
|
+
rescue => e
|
|
33
|
+
redirect_to root_path, alert: "Could not retry failed jobs: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def discard_all_blocked
|
|
37
|
+
jobs = SolidQueue::BlockedExecution.includes(:job).map(&:job)
|
|
38
|
+
SolidQueue::BlockedExecution.discard_all_from_jobs(jobs)
|
|
39
|
+
redirect_to root_path, notice: "#{jobs.size} blocked #{"job".pluralize(jobs.size)} discarded."
|
|
40
|
+
rescue => e
|
|
41
|
+
redirect_to root_path, alert: "Could not discard blocked jobs: #{e.message}"
|
|
42
|
+
end
|
|
27
43
|
end
|
|
28
44
|
end
|
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class FailedJobsController < ApplicationController
|
|
3
|
-
before_action :set_filter_params, only: [:index, :
|
|
3
|
+
before_action :set_filter_params, only: [:index, :destroy]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
6
|
+
respond_to do |format|
|
|
7
|
+
format.html { @pagy, @failed_jobs = pagy(filtered_scope.order(created_at: :desc)) }
|
|
8
|
+
format.csv do
|
|
9
|
+
send_data failed_jobs_csv,
|
|
10
|
+
filename: "failed-jobs-#{Date.today}.csv",
|
|
11
|
+
type: "text/csv", disposition: "attachment"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def destroy
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
redirect_to failed_jobs_path, notice: "Job discarded."
|
|
17
|
+
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
18
|
+
perform_discard(executions)
|
|
21
19
|
rescue => e
|
|
22
20
|
redirect_to failed_jobs_path, alert: "Could not discard job: #{e.message}"
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def failed_jobs_csv
|
|
26
|
+
CSV.generate(headers: true) do |csv|
|
|
27
|
+
csv << %w[id class_name queue_name error_class error_message failed_at]
|
|
28
|
+
filtered_scope.order(created_at: :desc).each do |execution|
|
|
29
|
+
job = execution.job
|
|
30
|
+
error = execution.error || {}
|
|
31
|
+
csv << [job.id, job.class_name, job.queue_name,
|
|
32
|
+
error["exception_class"], error["message"],
|
|
33
|
+
execution.created_at.iso8601]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
def
|
|
33
|
-
jobs =
|
|
38
|
+
def perform_discard(executions)
|
|
39
|
+
jobs = executions.map(&:job)
|
|
34
40
|
SolidQueue::FailedExecution.discard_all_from_jobs(jobs)
|
|
35
41
|
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
36
42
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
37
43
|
end
|
|
38
44
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
45
|
def set_filter_params
|
|
42
46
|
@queue = params[:queue].presence
|
|
43
47
|
@search = params[:q].presence
|
|
@@ -10,7 +10,26 @@ module SolidQueueWeb
|
|
|
10
10
|
scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
11
|
scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
respond_to do |format|
|
|
14
|
+
format.html { @pagy, @jobs = pagy(scope.order(finished_at: :desc)) }
|
|
15
|
+
format.csv do
|
|
16
|
+
send_data history_csv(scope),
|
|
17
|
+
filename: "job-history-#{Date.today}.csv",
|
|
18
|
+
type: "text/csv", disposition: "attachment"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def history_csv(scope)
|
|
26
|
+
CSV.generate(headers: true) do |csv|
|
|
27
|
+
csv << %w[id class_name queue_name duration_seconds finished_at]
|
|
28
|
+
scope.order(finished_at: :desc).each do |job|
|
|
29
|
+
duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
|
|
30
|
+
csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
14
33
|
end
|
|
15
34
|
end
|
|
16
35
|
end
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_status, only: [:destroy, :
|
|
3
|
+
before_action :set_status, only: [:destroy, :discard_selected]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
6
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
7
7
|
@search = params[:q].presence
|
|
8
8
|
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
scope = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
10
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
|
+
scope = scope.order(created_at: :desc)
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { @pagy, @jobs = pagy(scope) }
|
|
16
|
+
format.csv do
|
|
17
|
+
send_data jobs_csv(scope),
|
|
18
|
+
filename: "jobs-#{@status}-#{Date.today}.csv",
|
|
19
|
+
type: "text/csv", disposition: "attachment"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def show
|
|
@@ -21,33 +30,39 @@ module SolidQueueWeb
|
|
|
21
30
|
|
|
22
31
|
def destroy
|
|
23
32
|
model = execution_model_for!(@status)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
format
|
|
29
|
-
|
|
33
|
+
if params[:id]
|
|
34
|
+
@execution = model.find(params[:id])
|
|
35
|
+
@execution.discard
|
|
36
|
+
@remaining_count = filtered_scope(model).count
|
|
37
|
+
respond_to do |format|
|
|
38
|
+
format.turbo_stream
|
|
39
|
+
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
jobs = filtered_scope(model).map(&:job)
|
|
43
|
+
model.discard_all_from_jobs(jobs)
|
|
44
|
+
redirect_to jobs_path(status: @status, period: @period),
|
|
45
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
30
46
|
end
|
|
31
47
|
rescue ArgumentError => e
|
|
32
48
|
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
33
49
|
rescue => e
|
|
34
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def discard_all
|
|
38
|
-
model = execution_model_for!(@status)
|
|
39
|
-
jobs = filtered_scope(model).map(&:job)
|
|
40
|
-
model.discard_all_from_jobs(jobs)
|
|
41
50
|
redirect_to jobs_path(status: @status, period: @period),
|
|
42
|
-
|
|
43
|
-
rescue ArgumentError => e
|
|
44
|
-
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
45
|
-
rescue => e
|
|
46
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
|
|
51
|
+
alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
private
|
|
50
55
|
|
|
56
|
+
def jobs_csv(scope)
|
|
57
|
+
CSV.generate(headers: true) do |csv|
|
|
58
|
+
csv << %w[id class_name queue_name status priority enqueued_at]
|
|
59
|
+
scope.each do |execution|
|
|
60
|
+
job = execution.job
|
|
61
|
+
csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
51
66
|
def derive_status(job)
|
|
52
67
|
return "failed" if job.failed_execution.present?
|
|
53
68
|
return "claimed" if job.claimed_execution.present?
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class RetryFailedJobsController < ApplicationController
|
|
3
|
+
before_action :set_filter_params
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
7
|
+
jobs = executions.map(&:job)
|
|
8
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
9
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
10
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
11
|
+
rescue => e
|
|
12
|
+
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_filter_params
|
|
18
|
+
@queue = params[:queue].presence
|
|
19
|
+
@search = params[:q].presence
|
|
20
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filtered_scope
|
|
24
|
+
scope = SolidQueue::FailedExecution.includes(:job)
|
|
25
|
+
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
|
|
26
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
27
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
28
|
+
scope
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class SearchController < ApplicationController
|
|
3
|
-
LIMIT = 25
|
|
4
|
-
|
|
5
3
|
def index
|
|
6
4
|
@query = params[:q].presence
|
|
7
5
|
@job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
|
|
@@ -15,7 +13,7 @@ module SolidQueueWeb
|
|
|
15
13
|
.where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%")
|
|
16
14
|
.order(created_at: :desc)
|
|
17
15
|
total = scope.count
|
|
18
|
-
executions = scope.limit(
|
|
16
|
+
executions = scope.limit(SolidQueueWeb.search_results_limit).to_a
|
|
19
17
|
@results[status] = { executions: executions, total: total } unless executions.empty?
|
|
20
18
|
end
|
|
21
19
|
end
|
|
@@ -3,8 +3,10 @@ import { Application } from "@hotwired/stimulus"
|
|
|
3
3
|
import SearchController from "solid_queue_web/search_controller"
|
|
4
4
|
import RefreshController from "solid_queue_web/refresh_controller"
|
|
5
5
|
import SelectionController from "solid_queue_web/selection_controller"
|
|
6
|
+
import ThemeController from "solid_queue_web/theme_controller"
|
|
6
7
|
|
|
7
8
|
const application = Application.start()
|
|
8
9
|
application.register("search", SearchController)
|
|
9
10
|
application.register("refresh", RefreshController)
|
|
10
11
|
application.register("selection", SelectionController)
|
|
12
|
+
application.register("theme", ThemeController)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["toggle"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
const saved = localStorage.getItem("sqd-theme")
|
|
8
|
+
const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
9
|
+
this.apply(saved || preferred)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toggle() {
|
|
13
|
+
const current = document.documentElement.getAttribute("data-theme") || "light"
|
|
14
|
+
const next = current === "dark" ? "light" : "dark"
|
|
15
|
+
localStorage.setItem("sqd-theme", next)
|
|
16
|
+
this.apply(next)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply(theme) {
|
|
20
|
+
document.documentElement.setAttribute("data-theme", theme)
|
|
21
|
+
if (this.hasToggleTarget) {
|
|
22
|
+
this.toggleTarget.textContent = theme === "dark" ? "☀" : "☽"
|
|
23
|
+
this.toggleTarget.setAttribute("aria-label", theme === "dark" ? "Switch to light mode" : "Switch to dark mode")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -9,17 +9,11 @@
|
|
|
9
9
|
<%= inline_styles %>
|
|
10
10
|
<%= javascript_importmap_tags "solid_queue_web" %>
|
|
11
11
|
</head>
|
|
12
|
-
<body>
|
|
12
|
+
<body data-controller="theme">
|
|
13
13
|
|
|
14
14
|
<header class="sqd-header">
|
|
15
15
|
<div class="sqd-header__inner">
|
|
16
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
17
|
<div class="sqd-nav-wrapper">
|
|
24
18
|
<nav aria-label="Main">
|
|
25
19
|
<ul class="sqd-nav">
|
|
@@ -34,6 +28,16 @@
|
|
|
34
28
|
</ul>
|
|
35
29
|
</nav>
|
|
36
30
|
</div>
|
|
31
|
+
<div class="sqd-header__controls">
|
|
32
|
+
<button class="sqd-theme-toggle" aria-label="Switch to dark mode"
|
|
33
|
+
data-theme-target="toggle" data-action="theme#toggle">☽</button>
|
|
34
|
+
<button class="sqd-nav-toggle" aria-label="Toggle navigation" aria-expanded="false"
|
|
35
|
+
onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
|
|
36
|
+
<span></span>
|
|
37
|
+
<span></span>
|
|
38
|
+
<span></span>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
37
41
|
</div>
|
|
38
42
|
</header>
|
|
39
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.dashboard_refresh_interval } do %>
|
|
2
2
|
<h1 class="sqd-page-title">Dashboard</h1>
|
|
3
3
|
|
|
4
4
|
<div class="sqd-stats">
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
<% end %>
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
|
-
<div style="display:grid; grid-template-columns:
|
|
77
|
+
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
|
|
78
78
|
<div class="sqd-card">
|
|
79
79
|
<div class="sqd-card__header">
|
|
80
80
|
<span class="sqd-card__title">Quick Links</span>
|
|
@@ -91,13 +91,35 @@
|
|
|
91
91
|
<% if @stats[:failed] > 0 %>
|
|
92
92
|
<div class="sqd-card">
|
|
93
93
|
<div class="sqd-card__header">
|
|
94
|
-
<span class="sqd-card__title">
|
|
94
|
+
<span class="sqd-card__title">Failed Jobs</span>
|
|
95
95
|
</div>
|
|
96
|
-
<div style="padding: 1rem;">
|
|
97
|
-
<p style="color: var(--danger);
|
|
96
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
97
|
+
<p style="color: var(--danger); font-size: 13px;">
|
|
98
98
|
<%= pluralize(@stats[:failed], "failed job") %> need attention.
|
|
99
99
|
</p>
|
|
100
|
-
<%=
|
|
100
|
+
<%= button_to "Retry All Failed", retry_all_failed_path,
|
|
101
|
+
method: :post,
|
|
102
|
+
class: "sqd-btn sqd-btn--primary",
|
|
103
|
+
data: { confirm: "Retry all #{@stats[:failed]} failed #{"job".pluralize(@stats[:failed])}?" } %>
|
|
104
|
+
<%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<% end %>
|
|
108
|
+
|
|
109
|
+
<% if @stats[:blocked] > 0 %>
|
|
110
|
+
<div class="sqd-card">
|
|
111
|
+
<div class="sqd-card__header">
|
|
112
|
+
<span class="sqd-card__title">Blocked Jobs</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
115
|
+
<p style="color: var(--warning); font-size: 13px;">
|
|
116
|
+
<%= pluralize(@stats[:blocked], "blocked job") %>.
|
|
117
|
+
</p>
|
|
118
|
+
<%= button_to "Discard All Blocked", discard_all_blocked_path,
|
|
119
|
+
method: :post,
|
|
120
|
+
class: "sqd-btn sqd-btn--danger",
|
|
121
|
+
data: { confirm: "Discard all #{@stats[:blocked]} blocked #{"job".pluralize(@stats[:blocked])}? This cannot be undone." } %>
|
|
122
|
+
<%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
|
|
101
123
|
</div>
|
|
102
124
|
</div>
|
|
103
125
|
<% end %>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<h1 class="sqd-page-title">Failed Jobs</h1>
|
|
3
3
|
<% if @failed_jobs.any? %>
|
|
4
4
|
<div class="sqd-actions">
|
|
5
|
+
<%= link_to "Export CSV", failed_jobs_path(format: :csv, queue: @queue, q: @search, period: @period),
|
|
6
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
5
7
|
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
6
8
|
method: :post,
|
|
7
9
|
params: { queue: @queue, q: @search, period: @period },
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
2
2
|
<div class="sqd-page-header">
|
|
3
3
|
<h1 class="sqd-page-title">Job History</h1>
|
|
4
|
+
<% if @jobs.any? %>
|
|
5
|
+
<div class="sqd-actions">
|
|
6
|
+
<%= link_to "Export CSV", history_path(format: :csv, queue: @queue, q: @search, period: @period),
|
|
7
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
4
10
|
</div>
|
|
5
11
|
|
|
6
12
|
<form class="sqd-search" action="<%= history_path %>" method="get" data-controller="search">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>
|
|
2
2
|
|
|
3
|
-
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value:
|
|
3
|
+
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
4
4
|
<% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
|
|
5
5
|
|
|
6
6
|
<div class="sqd-page-header">
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
<%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period), class: @status == "blocked" ? "active" : "" %>
|
|
12
12
|
<%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period), class: @status == "failed" ? "active" : "" %>
|
|
13
13
|
</div>
|
|
14
|
-
<% if
|
|
14
|
+
<% if @jobs.any? %>
|
|
15
15
|
<div class="sqd-actions">
|
|
16
|
-
<%=
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, period: @period),
|
|
17
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
18
|
+
<% if discardable %>
|
|
19
|
+
<%= button_to "Discard All", discard_all_jobs_path,
|
|
20
|
+
method: :post,
|
|
21
|
+
params: { status: @status, period: @period },
|
|
22
|
+
class: "sqd-btn sqd-btn--danger",
|
|
23
|
+
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
|
|
24
|
+
<% end %>
|
|
21
25
|
</div>
|
|
22
26
|
<% end %>
|
|
23
27
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
2
2
|
<h1 class="sqd-page-title">Processes</h1>
|
|
3
3
|
|
|
4
4
|
<div class="sqd-card">
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
<span class="sqd-badge sqd-badge--<%= status %>"><%= status %></span>
|
|
28
28
|
<span class="sqd-muted-text">
|
|
29
29
|
<%= pluralize(data[:total], "match", "matches") %>
|
|
30
|
-
<% if data[:total] > SolidQueueWeb
|
|
31
|
-
— showing first <%= SolidQueueWeb
|
|
30
|
+
<% if data[:total] > SolidQueueWeb.search_results_limit %>
|
|
31
|
+
— showing first <%= SolidQueueWeb.search_results_limit %>
|
|
32
32
|
<% end %>
|
|
33
33
|
</span>
|
|
34
34
|
<% if status == "failed" %>
|
data/config/importmap.rb
CHANGED
|
@@ -2,3 +2,4 @@ pin "solid_queue_web", to: "solid_queue_web/application.js"
|
|
|
2
2
|
pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js"
|
|
3
3
|
pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js"
|
|
4
4
|
pin "solid_queue_web/selection_controller", to: "solid_queue_web/selection_controller.js"
|
|
5
|
+
pin "solid_queue_web/theme_controller", to: "solid_queue_web/theme_controller.js"
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
SolidQueueWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
|
+
post "retry_all_failed", to: "dashboard#retry_all_failed", as: :retry_all_failed
|
|
4
|
+
post "discard_all_blocked", to: "dashboard#discard_all_blocked", as: :discard_all_blocked
|
|
3
5
|
|
|
4
6
|
get "search", to: "search#index", as: :search
|
|
5
7
|
get "history", to: "history#index", as: :history
|
|
@@ -23,7 +25,7 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
23
25
|
resource :job_selection, path: "list/selection", only: [:destroy], controller: "jobs/selections"
|
|
24
26
|
resources :jobs, path: "list", only: [:index, :show, :destroy] do
|
|
25
27
|
collection do
|
|
26
|
-
post :discard_all
|
|
28
|
+
post :discard_all, action: :destroy
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -31,11 +33,11 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
31
33
|
controller: "failed_jobs/selections"
|
|
32
34
|
resources :failed_jobs, only: [:index, :destroy] do
|
|
33
35
|
collection do
|
|
34
|
-
post :retry_all
|
|
35
|
-
post :discard_all
|
|
36
|
+
post :retry_all, to: "retry_failed_jobs#create"
|
|
37
|
+
post :discard_all, action: :destroy
|
|
36
38
|
end
|
|
37
39
|
member do
|
|
38
|
-
post :retry
|
|
40
|
+
post :retry, to: "retry_failed_jobs#create"
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
end
|
|
@@ -22,8 +22,10 @@ module SolidQueueWeb
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
initializer "solid_queue_web.pagy" do
|
|
26
|
-
|
|
25
|
+
initializer "solid_queue_web.pagy" do |app|
|
|
26
|
+
app.config.after_initialize do
|
|
27
|
+
Pagy::OPTIONS[:limit] = SolidQueueWeb.page_size
|
|
28
|
+
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
data/lib/solid_queue_web.rb
CHANGED
|
@@ -4,6 +4,28 @@ require "solid_queue_web/engine"
|
|
|
4
4
|
|
|
5
5
|
module SolidQueueWeb
|
|
6
6
|
class << self
|
|
7
|
+
attr_writer :page_size, :dashboard_refresh_interval, :default_refresh_interval, :search_results_limit
|
|
8
|
+
|
|
9
|
+
def page_size
|
|
10
|
+
@page_size || 25
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def dashboard_refresh_interval
|
|
14
|
+
@dashboard_refresh_interval || 5_000
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def default_refresh_interval
|
|
18
|
+
@default_refresh_interval || 10_000
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def search_results_limit
|
|
22
|
+
@search_results_limit || 25
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configure
|
|
26
|
+
yield self
|
|
27
|
+
end
|
|
28
|
+
|
|
7
29
|
def authenticate(&block)
|
|
8
30
|
@authenticate = block if block_given?
|
|
9
31
|
@authenticate
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 8.1.3
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: csv
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: solid_queue
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -102,6 +116,7 @@ files:
|
|
|
102
116
|
- app/assets/stylesheets/solid_queue_web/_09_pagination.css
|
|
103
117
|
- app/assets/stylesheets/solid_queue_web/_10_responsive.css
|
|
104
118
|
- app/assets/stylesheets/solid_queue_web/_11_throughput.css
|
|
119
|
+
- app/assets/stylesheets/solid_queue_web/_12_dark_mode.css
|
|
105
120
|
- app/assets/stylesheets/solid_queue_web/application.css
|
|
106
121
|
- app/controllers/solid_queue_web/application_controller.rb
|
|
107
122
|
- app/controllers/solid_queue_web/dashboard_controller.rb
|
|
@@ -114,12 +129,14 @@ files:
|
|
|
114
129
|
- app/controllers/solid_queue_web/queues/jobs_controller.rb
|
|
115
130
|
- app/controllers/solid_queue_web/queues_controller.rb
|
|
116
131
|
- app/controllers/solid_queue_web/recurring_tasks_controller.rb
|
|
132
|
+
- app/controllers/solid_queue_web/retry_failed_jobs_controller.rb
|
|
117
133
|
- app/controllers/solid_queue_web/search_controller.rb
|
|
118
134
|
- app/helpers/solid_queue_web/application_helper.rb
|
|
119
135
|
- app/javascript/solid_queue_web/application.js
|
|
120
136
|
- app/javascript/solid_queue_web/refresh_controller.js
|
|
121
137
|
- app/javascript/solid_queue_web/search_controller.js
|
|
122
138
|
- app/javascript/solid_queue_web/selection_controller.js
|
|
139
|
+
- app/javascript/solid_queue_web/theme_controller.js
|
|
123
140
|
- app/jobs/solid_queue_web/application_job.rb
|
|
124
141
|
- app/models/solid_queue_web/application_record.rb
|
|
125
142
|
- app/models/solid_queue_web/job.rb
|