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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -2
  3. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +8 -0
  4. data/app/assets/stylesheets/solid_stack_web/_04_table.css +1 -0
  5. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +13 -0
  6. data/app/assets/stylesheets/solid_stack_web/_08_filters.css +59 -0
  7. data/app/assets/stylesheets/solid_stack_web/_09_detail.css +85 -0
  8. data/app/controllers/solid_stack_web/application_controller.rb +5 -1
  9. data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
  10. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +28 -0
  11. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
  12. data/app/controllers/solid_stack_web/history_controller.rb +42 -0
  13. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +24 -0
  14. data/app/controllers/solid_stack_web/jobs_controller.rb +60 -21
  15. data/app/controllers/solid_stack_web/queues/pauses_controller.rb +13 -0
  16. data/app/controllers/solid_stack_web/queues_controller.rb +9 -8
  17. data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +18 -0
  18. data/app/controllers/solid_stack_web/recurring_tasks_controller.rb +7 -0
  19. data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +52 -0
  20. data/app/helpers/solid_stack_web/application_helper.rb +8 -0
  21. data/app/javascript/solid_stack_web/application.js +6 -0
  22. data/app/javascript/solid_stack_web/selection_controller.js +42 -0
  23. data/app/models/solid_stack_web/job.rb +21 -0
  24. data/app/views/layouts/solid_stack_web/application.html.erb +5 -0
  25. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +9 -0
  26. data/app/views/solid_stack_web/failed_jobs/index.html.erb +61 -30
  27. data/app/views/solid_stack_web/failed_jobs/show.html.erb +58 -0
  28. data/app/views/solid_stack_web/history/index.html.erb +73 -0
  29. data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +2 -2
  30. data/app/views/solid_stack_web/jobs/index.html.erb +138 -34
  31. data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
  32. data/app/views/solid_stack_web/queues/index.html.erb +4 -4
  33. data/app/views/solid_stack_web/queues/show.html.erb +67 -0
  34. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +67 -0
  35. data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +9 -0
  36. data/config/importmap.rb +2 -0
  37. data/config/routes.rb +23 -7
  38. data/lib/solid_stack_web/engine.rb +15 -0
  39. data/lib/solid_stack_web/version.rb +1 -1
  40. metadata +64 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ff5e759d441ff203317f09e35d4e9fb6876f81b544d4b075093c3d2cf397293
4
- data.tar.gz: 9be02459700b7da623c939241ffc49e0147a4a460fb68f85f0ff3d63b10cea7f
3
+ metadata.gz: 777352d5919952ea022a545b13608ace8f35e0f91077431c45a679e5c30bec19
4
+ data.tar.gz: d36dbf96b4234d730249df1d259099883d0473c160c62ab9bee73fe88ff133c4
5
5
  SHA512:
6
- metadata.gz: 8dcdb3c9445a05628849c51d6081b5a3f399249b690da8a254583e134b0d761b04a9d9228818614d1b0239d4226648cec75efd8da6c7b8a990f0832df57730a4
7
- data.tar.gz: ea5a60256af380188021c58aeeab6ab7e58c8eea3bd5aaf9ff03c3306d763cebb667fef58482e09fc1ee677894b110f03ed6cadb989bc254b6cb73d997909a52
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; }
@@ -28,6 +28,7 @@
28
28
  .sqw-table tbody tr:hover { background: #f9fafb; }
29
29
 
30
30
  .sqw-actions { text-align: right; white-space: nowrap; }
31
+ .sqw-actions form { display: inline; }
31
32
 
32
33
  .sqw-empty {
33
34
  background: var(--surface);
@@ -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
- scope = ::SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
5
- @pagy, @executions = pagy(scope)
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 :require_discardable, only: :destroy
4
+ before_action :set_filters, only: [:index, :destroy]
5
+ before_action :require_discardable, only: [:destroy]
14
6
 
15
7
  def index
16
- scope = EXECUTION_MODELS[@status].includes(:job).order(created_at: :desc)
17
- @pagy, @executions = pagy(scope)
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
- model = EXECUTION_MODELS[@status]
22
- execution = model.find(params[:id])
23
- execution.job.destroy!
24
- @executions_remain = model.exists?
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
- respond_to do |format|
27
- format.html { redirect_to jobs_path(status: @status) }
28
- format.turbo_stream
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(EXECUTION_MODELS.keys) || "ready"
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 :unprocessable_entity unless DISCARDABLE.include?(@status)
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 pause
17
- ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id])
18
- redirect_to queues_path
19
- end
20
-
21
- def resume
22
- ::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy
23
- redirect_to queues_path
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
@@ -0,0 +1,7 @@
1
+ module SolidStackWeb
2
+ class RecurringTasksController < ApplicationController
3
+ def index
4
+ @recurring_tasks = SolidQueue::RecurringTask.includes(:recurring_executions).order(:key)
5
+ end
6
+ end
7
+ end