solid_stack_web 0.1.0 → 0.2.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 +17 -2
- 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 +4 -0
- data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
- data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +22 -0
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
- 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/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 +1 -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/jobs/destroy.turbo_stream.erb +2 -2
- data/app/views/solid_stack_web/jobs/index.html.erb +117 -34
- data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +10 -2
- data/lib/solid_stack_web/engine.rb +15 -0
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +55 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0adcafbdcb32ea6e96f982efc8d44502ea31e75e9f25138558ba58a8952719c8
|
|
4
|
+
data.tar.gz: a0e5b5695d620e4d616535e5f7b394df10c756ee712584a7de7c7ef489ded759
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42c74a1cbc74acf3485ce0649e7f894bbbc1fdb4172037dbac4edb6e6805c8571c3731efb5acaa774763ae979ae8bb632e81e16f7f36e51faed447c8eb3bd81d
|
|
7
|
+
data.tar.gz: e35fb81b13d2f4cfa2d43f2e9ee223ef32c9605a69c98cf38e94f193d037f284efae651922ace035e04ad0ac1129361825345d5a64b97d91809caad1201351b9
|
data/README.md
CHANGED
|
@@ -9,8 +9,9 @@ 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
|
|
14
|
+
- **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
|
|
14
15
|
- **Solid Cache** — entry count and total byte size at a glance
|
|
15
16
|
- **Solid Cable** — active message count and distinct channel count
|
|
16
17
|
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
@@ -56,6 +57,19 @@ SolidStackWeb.configure do |config|
|
|
|
56
57
|
end
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
### Job Filtering
|
|
61
|
+
|
|
62
|
+
The jobs list supports four independent filters, all driven by query params:
|
|
63
|
+
|
|
64
|
+
| Param | Description |
|
|
65
|
+
|-------|-------------|
|
|
66
|
+
| `q` | Substring match against the job class name (e.g. `q=Report`) |
|
|
67
|
+
| `queue` | Exact queue name match; select appears only when multiple queues exist |
|
|
68
|
+
| `priority` | Exact priority value match; select appears only when multiple priorities exist |
|
|
69
|
+
| `period` | Enqueued-at window — `1h`, `24h`, `7d`, or omit for all time |
|
|
70
|
+
|
|
71
|
+
Filters are preserved when switching between status tabs (Ready / Scheduled / Running / Blocked) and when discarding a job. They can be combined freely.
|
|
72
|
+
|
|
59
73
|
### Authentication
|
|
60
74
|
|
|
61
75
|
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 +81,7 @@ The `authenticate` block is evaluated in the context of each request's controlle
|
|
|
67
81
|
- [solid_queue](https://github.com/rails/solid_queue) >= 1.0
|
|
68
82
|
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
69
83
|
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
84
|
+
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
70
85
|
|
|
71
86
|
## Contributing
|
|
72
87
|
|
|
@@ -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
|
|
|
@@ -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,22 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
module FailedJobs
|
|
3
|
+
class SelectionsController < ApplicationController
|
|
4
|
+
def create
|
|
5
|
+
ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
6
|
+
SolidQueue::FailedExecution.where(id: ids).each(&:retry)
|
|
7
|
+
redirect_to failed_jobs_path
|
|
8
|
+
rescue => e
|
|
9
|
+
redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def destroy
|
|
13
|
+
ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
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
|
+
end
|
|
21
|
+
end
|
|
22
|
+
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,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,42 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["checkbox", "selectAll", "bar", "count"]
|
|
5
|
+
|
|
6
|
+
toggle() {
|
|
7
|
+
this._update()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
selectAll({ target }) {
|
|
11
|
+
this.checkboxTargets.forEach(cb => cb.checked = target.checked)
|
|
12
|
+
this._update()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
submit({ params: { formId } }) {
|
|
16
|
+
const form = document.getElementById(formId)
|
|
17
|
+
if (!form) return
|
|
18
|
+
form.querySelectorAll("[data-injected-id]").forEach(el => el.remove())
|
|
19
|
+
this.checkboxTargets
|
|
20
|
+
.filter(cb => cb.checked)
|
|
21
|
+
.forEach(cb => {
|
|
22
|
+
const input = document.createElement("input")
|
|
23
|
+
input.type = "hidden"
|
|
24
|
+
input.name = "job_ids[]"
|
|
25
|
+
input.value = cb.value
|
|
26
|
+
input.dataset.injectedId = true
|
|
27
|
+
form.appendChild(input)
|
|
28
|
+
})
|
|
29
|
+
form.requestSubmit()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_update() {
|
|
33
|
+
const checked = this.checkboxTargets.filter(cb => cb.checked).length
|
|
34
|
+
const total = this.checkboxTargets.length
|
|
35
|
+
if (this.hasBarTarget) this.barTarget.style.display = checked > 0 ? "" : "none"
|
|
36
|
+
if (this.hasCountTarget) this.countTarget.textContent = checked
|
|
37
|
+
if (this.hasSelectAllTarget) {
|
|
38
|
+
this.selectAllTarget.indeterminate = checked > 0 && checked < total
|
|
39
|
+
this.selectAllTarget.checked = total > 0 && checked === total
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class Job
|
|
3
|
+
STATUSES = %w[ready scheduled claimed blocked].freeze
|
|
4
|
+
|
|
5
|
+
EXECUTION_MODELS = {
|
|
6
|
+
"ready" => SolidQueue::ReadyExecution,
|
|
7
|
+
"scheduled" => SolidQueue::ScheduledExecution,
|
|
8
|
+
"claimed" => SolidQueue::ClaimedExecution,
|
|
9
|
+
"blocked" => SolidQueue::BlockedExecution
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
DISCARDABLE = %w[ready scheduled blocked].freeze
|
|
13
|
+
|
|
14
|
+
TAB_LABELS = {
|
|
15
|
+
"ready" => "Ready",
|
|
16
|
+
"scheduled" => "Scheduled",
|
|
17
|
+
"claimed" => "Running",
|
|
18
|
+
"blocked" => "Blocked"
|
|
19
|
+
}.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,41 +1,72 @@
|
|
|
1
|
-
<div class="sqw-page-header">
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
2
|
<h1 class="sqw-page-title">Failed Jobs</h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<%= link_to "Export CSV", failed_jobs_path(format: :csv),
|
|
5
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
6
|
+
</div>
|
|
3
7
|
</div>
|
|
4
8
|
|
|
5
9
|
<div id="sqw-jobs-table">
|
|
6
10
|
<% if @executions.any? %>
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
<div data-controller="selection">
|
|
12
|
+
<%= form_with url: failed_job_selection_path, method: :post, id: "retry-selection-form" do |f| %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= form_with url: failed_job_selection_path, method: :delete, id: "discard-selection-form",
|
|
16
|
+
data: { turbo_confirm: "Discard selected failed jobs? This cannot be undone." } do |f| %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
|
|
20
|
+
<span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
|
|
21
|
+
<button type="button" class="sqw-btn sqw-btn--sm"
|
|
22
|
+
data-action="click->selection#submit"
|
|
23
|
+
data-selection-form-id-param="retry-selection-form">Retry Selected</button>
|
|
24
|
+
<button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
|
|
25
|
+
data-action="click->selection#submit"
|
|
26
|
+
data-selection-form-id-param="discard-selection-form">Discard Selected</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<table class="sqw-table">
|
|
30
|
+
<thead>
|
|
31
|
+
<tr>
|
|
32
|
+
<th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
|
|
33
|
+
data-selection-target="selectAll"
|
|
34
|
+
data-action="change->selection#selectAll"></th>
|
|
35
|
+
<th>Job Class</th>
|
|
36
|
+
<th>Queue</th>
|
|
37
|
+
<th>Error</th>
|
|
38
|
+
<th>Failed At</th>
|
|
39
|
+
<th></th>
|
|
31
40
|
</tr>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
</thead>
|
|
42
|
+
<tbody>
|
|
43
|
+
<% @executions.each do |execution| %>
|
|
44
|
+
<tr id="execution_<%= execution.id %>">
|
|
45
|
+
<td><input type="checkbox" value="<%= execution.id %>"
|
|
46
|
+
class="sqw-checkbox"
|
|
47
|
+
aria-label="Select <%= execution.job.class_name %>"
|
|
48
|
+
data-selection-target="checkbox"
|
|
49
|
+
data-action="change->selection#toggle"></td>
|
|
50
|
+
<td class="sqw-monospace"><%= link_to execution.job.class_name, failed_job_path(execution) %></td>
|
|
51
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
|
|
52
|
+
<td class="sqw-muted sqw-truncate" title="<%= execution.exception_class %>: <%= execution.message %>"><%= execution.exception_class %></td>
|
|
53
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
54
|
+
<td class="sqw-actions">
|
|
55
|
+
<%= button_to "Retry", retry_failed_job_path(execution),
|
|
56
|
+
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
57
|
+
<%= button_to "Discard", failed_job_path(execution),
|
|
58
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
59
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
<% end %>
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
66
|
+
</div>
|
|
36
67
|
<% else %>
|
|
37
68
|
<div class="sqw-empty">
|
|
38
69
|
<p>No failed jobs.</p>
|
|
39
70
|
</div>
|
|
40
71
|
<% end %>
|
|
41
|
-
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="sqw-breadcrumb">
|
|
4
|
+
<%= link_to "Failed Jobs", failed_jobs_path %> › Detail
|
|
5
|
+
</div>
|
|
6
|
+
<h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="sqw-detail-actions">
|
|
10
|
+
<%= button_to "Retry", retry_failed_job_path(@execution),
|
|
11
|
+
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
12
|
+
<%= button_to "Discard", failed_job_path(@execution),
|
|
13
|
+
method: :delete, class: "sqw-btn sqw-btn--danger",
|
|
14
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="sqw-detail-grid">
|
|
19
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
20
|
+
<h2 class="sqw-section-title">Details</h2>
|
|
21
|
+
<dl class="sqw-dl">
|
|
22
|
+
<dt>Queue</dt>
|
|
23
|
+
<dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>
|
|
24
|
+
|
|
25
|
+
<dt>Priority</dt>
|
|
26
|
+
<dd><%= @execution.job.priority %></dd>
|
|
27
|
+
|
|
28
|
+
<dt>Active Job ID</dt>
|
|
29
|
+
<dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
|
|
30
|
+
|
|
31
|
+
<dt>Failed At</dt>
|
|
32
|
+
<dd class="sqw-monospace"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
|
|
33
|
+
|
|
34
|
+
<dt>Error</dt>
|
|
35
|
+
<dd class="sqw-monospace"><%= @execution.exception_class %></dd>
|
|
36
|
+
|
|
37
|
+
<dt>Message</dt>
|
|
38
|
+
<dd class="sqw-muted"><%= @execution.message %></dd>
|
|
39
|
+
</dl>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
43
|
+
<h2 class="sqw-section-title">Backtrace</h2>
|
|
44
|
+
<pre class="sqw-code-block"><%= Array(@execution.backtrace).first(10).join("\n").presence || "—" %></pre>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="sqw-detail-card sqw-detail-section" style="margin-top: 1.5rem;">
|
|
49
|
+
<h2 class="sqw-section-title">Arguments</h2>
|
|
50
|
+
<%= form_with url: failed_job_arguments_path(@execution), method: :patch do |f| %>
|
|
51
|
+
<%= f.text_area :arguments, value: @arguments, rows: 12,
|
|
52
|
+
class: "sqw-code-input", aria: { label: "Job arguments JSON" },
|
|
53
|
+
spellcheck: false %>
|
|
54
|
+
<div style="margin-top: 0.75rem;">
|
|
55
|
+
<%= f.submit "Update & Retry".html_safe, class: "sqw-btn sqw-btn--sm" %>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% if @executions_remain %>
|
|
2
|
-
<%= turbo_stream.remove "execution_#{
|
|
2
|
+
<%= turbo_stream.remove "execution_#{@execution.id}" %>
|
|
3
3
|
<% else %>
|
|
4
4
|
<%= turbo_stream.replace "sqw-jobs-table" do %>
|
|
5
5
|
<%= render "empty" %>
|
|
6
6
|
<% end %>
|
|
7
|
-
<% end %>
|
|
7
|
+
<% end %>
|
|
@@ -1,50 +1,133 @@
|
|
|
1
|
-
<div class="sqw-page-header">
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
2
|
<h1 class="sqw-page-title">Jobs</h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
5
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
6
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
|
|
7
|
+
<%= button_to "Discard All (#{@pagy.count})",
|
|
8
|
+
discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
9
|
+
method: :post,
|
|
10
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
11
|
+
data: { turbo_confirm: "Discard all #{@pagy.count} jobs? This cannot be undone.",
|
|
12
|
+
turbo_frame: "_top" } %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
3
15
|
</div>
|
|
4
16
|
|
|
5
17
|
<div class="sqw-tabs">
|
|
6
|
-
<%
|
|
7
|
-
<%= link_to label, jobs_path(status: status),
|
|
18
|
+
<% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
|
|
19
|
+
<%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
8
20
|
class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
|
|
9
21
|
<% end %>
|
|
10
22
|
</div>
|
|
11
23
|
|
|
24
|
+
<turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
|
|
25
|
+
<form class="sqw-filters" action="<%= jobs_path %>" method="get">
|
|
26
|
+
<%= hidden_field_tag :status, @status %>
|
|
27
|
+
<%= hidden_field_tag :period, @period %>
|
|
28
|
+
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
29
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
|
|
30
|
+
<% if @queue_options.size > 1 %>
|
|
31
|
+
<select name="queue" class="sqw-select" aria-label="Filter by queue" onchange="this.form.requestSubmit()">
|
|
32
|
+
<option value="">All queues</option>
|
|
33
|
+
<% @queue_options.each do |q| %>
|
|
34
|
+
<option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
|
|
35
|
+
<% end %>
|
|
36
|
+
</select>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% if @priority_options.size > 1 %>
|
|
39
|
+
<select name="priority" class="sqw-select" aria-label="Filter by priority" onchange="this.form.requestSubmit()">
|
|
40
|
+
<option value="">All priorities</option>
|
|
41
|
+
<% @priority_options.each do |p| %>
|
|
42
|
+
<option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
|
|
43
|
+
<% end %>
|
|
44
|
+
</select>
|
|
45
|
+
<% end %>
|
|
46
|
+
<button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
|
|
47
|
+
<% if @search.present? || @queue.present? || @priority.present? %>
|
|
48
|
+
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<div class="sqw-period-filter" role="group" aria-label="Time period">
|
|
51
|
+
<%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
|
|
52
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
|
|
53
|
+
<%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
|
|
54
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
|
|
55
|
+
<%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
|
|
56
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
|
|
57
|
+
<%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
|
|
58
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
|
|
59
|
+
</div>
|
|
60
|
+
</form>
|
|
61
|
+
|
|
12
62
|
<div id="sqw-jobs-table">
|
|
13
63
|
<% if @executions.any? %>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
<div data-controller="<%= "selection" if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>">
|
|
65
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
66
|
+
<%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
|
|
67
|
+
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
|
|
68
|
+
<%= f.hidden_field :status, value: @status %>
|
|
69
|
+
<%= f.hidden_field :q, value: @search %>
|
|
70
|
+
<%= f.hidden_field :queue, value: @queue %>
|
|
71
|
+
<%= f.hidden_field :period, value: @period %>
|
|
72
|
+
<%= f.hidden_field :priority, value: @priority %>
|
|
73
|
+
<% end %>
|
|
74
|
+
|
|
75
|
+
<div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
|
|
76
|
+
<span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
|
|
77
|
+
<button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
|
|
78
|
+
data-action="click->selection#submit"
|
|
79
|
+
data-selection-form-id-param="job-selection-form">Discard Selected</button>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
82
|
+
|
|
83
|
+
<table class="sqw-table">
|
|
84
|
+
<thead>
|
|
85
|
+
<tr>
|
|
86
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
87
|
+
<th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
|
|
88
|
+
data-selection-target="selectAll"
|
|
89
|
+
data-action="change->selection#selectAll"></th>
|
|
34
90
|
<% end %>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</td>
|
|
91
|
+
<th>Job Class</th>
|
|
92
|
+
<th>Queue</th>
|
|
93
|
+
<th>Priority</th>
|
|
94
|
+
<th>Enqueued At</th>
|
|
95
|
+
<% if @status == "scheduled" %><th>Scheduled At</th><% end %>
|
|
96
|
+
<th></th>
|
|
42
97
|
</tr>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
98
|
+
</thead>
|
|
99
|
+
<tbody>
|
|
100
|
+
<% @executions.each do |execution| %>
|
|
101
|
+
<tr id="execution_<%= execution.id %>">
|
|
102
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
103
|
+
<td><input type="checkbox" value="<%= execution.id %>"
|
|
104
|
+
class="sqw-checkbox"
|
|
105
|
+
aria-label="Select <%= execution.job.class_name %>"
|
|
106
|
+
data-selection-target="checkbox"
|
|
107
|
+
data-action="change->selection#toggle"></td>
|
|
108
|
+
<% end %>
|
|
109
|
+
<td class="sqw-monospace"><%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %></td>
|
|
110
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
|
|
111
|
+
<td><%= execution.job.priority %></td>
|
|
112
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
113
|
+
<% if @status == "scheduled" %>
|
|
114
|
+
<td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
|
|
115
|
+
<% end %>
|
|
116
|
+
<td class="sqw-actions">
|
|
117
|
+
<% if %w[ready scheduled blocked].include?(@status) %>
|
|
118
|
+
<%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
119
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
120
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
121
|
+
<% end %>
|
|
122
|
+
</td>
|
|
123
|
+
</tr>
|
|
124
|
+
<% end %>
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
128
|
+
</div>
|
|
47
129
|
<% else %>
|
|
48
130
|
<%= render "empty" %>
|
|
49
131
|
<% end %>
|
|
50
132
|
</div>
|
|
133
|
+
</turbo-frame>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="sqw-breadcrumb">
|
|
4
|
+
<%= link_to "Jobs", jobs_path(status: params[:status]) %> › Detail
|
|
5
|
+
</div>
|
|
6
|
+
<h1 class="sqw-page-title sqw-monospace"><%= @execution.job.class_name %></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
10
|
+
<div class="sqw-detail-actions">
|
|
11
|
+
<%= button_to "Discard Job", job_path(@execution.id, status: @status),
|
|
12
|
+
method: :delete, class: "sqw-btn sqw-btn--danger",
|
|
13
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="sqw-detail-grid">
|
|
19
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
20
|
+
<h2 class="sqw-section-title">Details</h2>
|
|
21
|
+
<dl class="sqw-dl">
|
|
22
|
+
<dt>Status</dt>
|
|
23
|
+
<dd><span class="sqw-badge sqw-badge--<%= @status %>"><%= SolidStackWeb::Job::TAB_LABELS[@status] %></span></dd>
|
|
24
|
+
|
|
25
|
+
<dt>Queue</dt>
|
|
26
|
+
<dd class="sqw-monospace"><%= @execution.job.queue_name %></dd>
|
|
27
|
+
|
|
28
|
+
<dt>Priority</dt>
|
|
29
|
+
<dd><%= @execution.job.priority %></dd>
|
|
30
|
+
|
|
31
|
+
<dt>Active Job ID</dt>
|
|
32
|
+
<dd class="sqw-monospace sqw-truncate" title="<%= @execution.job.active_job_id %>"><%= @execution.job.active_job_id.presence || "—" %></dd>
|
|
33
|
+
|
|
34
|
+
<dt>Concurrency Key</dt>
|
|
35
|
+
<dd class="sqw-monospace"><%= @execution.job.concurrency_key.presence || "—" %></dd>
|
|
36
|
+
|
|
37
|
+
<% if @status == "blocked" %>
|
|
38
|
+
<dt>Blocked Until</dt>
|
|
39
|
+
<dd class="sqw-monospace"><%= @execution.expires_at ? @execution.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<dt>Enqueued At</dt>
|
|
43
|
+
<dd class="sqw-monospace"><%= @execution.job.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></dd>
|
|
44
|
+
|
|
45
|
+
<dt>Scheduled At</dt>
|
|
46
|
+
<dd class="sqw-monospace"><%= @execution.job.scheduled_at ? @execution.job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
|
|
47
|
+
|
|
48
|
+
<dt>Finished At</dt>
|
|
49
|
+
<dd class="sqw-monospace"><%= @execution.job.finished_at ? @execution.job.finished_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %></dd>
|
|
50
|
+
</dl>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="sqw-detail-card sqw-detail-section">
|
|
54
|
+
<h2 class="sqw-section-title">Arguments</h2>
|
|
55
|
+
<pre class="sqw-code-block"><%= @arguments ? JSON.pretty_generate(@arguments) : (@execution.job.arguments || "—") %></pre>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
data/config/importmap.rb
ADDED
data/config/routes.rb
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
SolidStackWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections"
|
|
5
|
+
resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy], controller: "failed_jobs/selections"
|
|
5
6
|
|
|
6
|
-
resources :
|
|
7
|
+
resources :jobs, only: [:index, :show, :destroy] do
|
|
8
|
+
collection do
|
|
9
|
+
post :discard_all, action: :destroy
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
resources :failed_jobs, only: [:index, :show, :destroy] do
|
|
7
14
|
member { post :retry }
|
|
15
|
+
resource :arguments, only: [:update], controller: "failed_jobs/arguments"
|
|
8
16
|
end
|
|
9
17
|
|
|
10
18
|
resources :queues, only: [:index] do
|
|
@@ -3,6 +3,8 @@ require "pagy/toolbox/paginators/method"
|
|
|
3
3
|
require "solid_queue"
|
|
4
4
|
require "solid_cache"
|
|
5
5
|
require "solid_cable"
|
|
6
|
+
require "turbo-rails"
|
|
7
|
+
require "importmap-rails"
|
|
6
8
|
|
|
7
9
|
module SolidStackWeb
|
|
8
10
|
class Engine < ::Rails::Engine
|
|
@@ -10,6 +12,19 @@ module SolidStackWeb
|
|
|
10
12
|
|
|
11
13
|
config.i18n.load_path += Gem.find_files("pagy/locales/en.yml")
|
|
12
14
|
|
|
15
|
+
initializer "solid_stack_web.assets" do |app|
|
|
16
|
+
if app.config.respond_to?(:assets)
|
|
17
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "solid_stack_web.importmap", before: "importmap" do |app|
|
|
22
|
+
if app.config.respond_to?(:importmap)
|
|
23
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
24
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
13
28
|
initializer "solid_stack_web.pagy" do |app|
|
|
14
29
|
app.config.after_initialize do
|
|
15
30
|
Pagy::OPTIONS[:limit] = SolidStackWeb.page_size
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_stack_web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -79,6 +79,48 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: turbo-rails
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '2.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: importmap-rails
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.2'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.2'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: csv
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.0'
|
|
82
124
|
description: Mount SolidStackWeb in any Rails app using the Solid Stack to get a single
|
|
83
125
|
dashboard covering Solid Queue job monitoring, Solid Cache statistics, and Solid
|
|
84
126
|
Cable connection observability — all without leaving your app.
|
|
@@ -98,26 +140,38 @@ files:
|
|
|
98
140
|
- app/assets/stylesheets/solid_stack_web/_05_badges.css
|
|
99
141
|
- app/assets/stylesheets/solid_stack_web/_06_buttons.css
|
|
100
142
|
- app/assets/stylesheets/solid_stack_web/_07_dashboard.css
|
|
143
|
+
- app/assets/stylesheets/solid_stack_web/_08_filters.css
|
|
144
|
+
- app/assets/stylesheets/solid_stack_web/_09_detail.css
|
|
101
145
|
- app/assets/stylesheets/solid_stack_web/application.css
|
|
102
146
|
- app/controllers/solid_stack_web/application_controller.rb
|
|
103
147
|
- app/controllers/solid_stack_web/cable_controller.rb
|
|
104
148
|
- app/controllers/solid_stack_web/cache_controller.rb
|
|
105
149
|
- app/controllers/solid_stack_web/dashboard_controller.rb
|
|
150
|
+
- app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
|
|
151
|
+
- app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
|
|
106
152
|
- app/controllers/solid_stack_web/failed_jobs_controller.rb
|
|
153
|
+
- app/controllers/solid_stack_web/jobs/selections_controller.rb
|
|
107
154
|
- app/controllers/solid_stack_web/jobs_controller.rb
|
|
108
155
|
- app/controllers/solid_stack_web/processes_controller.rb
|
|
109
156
|
- app/controllers/solid_stack_web/queues_controller.rb
|
|
110
157
|
- app/helpers/solid_stack_web/application_helper.rb
|
|
158
|
+
- app/javascript/solid_stack_web/application.js
|
|
159
|
+
- app/javascript/solid_stack_web/selection_controller.js
|
|
160
|
+
- app/models/solid_stack_web/job.rb
|
|
111
161
|
- app/views/layouts/solid_stack_web/application.html.erb
|
|
112
162
|
- app/views/solid_stack_web/cable/index.html.erb
|
|
113
163
|
- app/views/solid_stack_web/cache/index.html.erb
|
|
114
164
|
- app/views/solid_stack_web/dashboard/index.html.erb
|
|
165
|
+
- app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
|
|
115
166
|
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
167
|
+
- app/views/solid_stack_web/failed_jobs/show.html.erb
|
|
116
168
|
- app/views/solid_stack_web/jobs/_empty.html.erb
|
|
117
169
|
- app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
|
|
118
170
|
- app/views/solid_stack_web/jobs/index.html.erb
|
|
171
|
+
- app/views/solid_stack_web/jobs/show.html.erb
|
|
119
172
|
- app/views/solid_stack_web/processes/index.html.erb
|
|
120
173
|
- app/views/solid_stack_web/queues/index.html.erb
|
|
174
|
+
- config/importmap.rb
|
|
121
175
|
- config/routes.rb
|
|
122
176
|
- lib/solid_stack_web.rb
|
|
123
177
|
- lib/solid_stack_web/engine.rb
|