solid_stack_web 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -1
- data/app/assets/stylesheets/solid_stack_web/_02_layout.css +8 -0
- data/app/controllers/solid_stack_web/application_controller.rb +1 -1
- data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +10 -4
- data/app/controllers/solid_stack_web/history_controller.rb +42 -0
- data/app/controllers/solid_stack_web/queues/pauses_controller.rb +13 -0
- data/app/controllers/solid_stack_web/queues_controller.rb +9 -8
- data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +18 -0
- data/app/controllers/solid_stack_web/recurring_tasks_controller.rb +7 -0
- data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +52 -0
- data/app/helpers/solid_stack_web/application_helper.rb +8 -0
- data/app/views/layouts/solid_stack_web/application.html.erb +4 -0
- data/app/views/solid_stack_web/history/index.html.erb +73 -0
- data/app/views/solid_stack_web/jobs/index.html.erb +22 -1
- data/app/views/solid_stack_web/queues/index.html.erb +4 -4
- data/app/views/solid_stack_web/queues/show.html.erb +67 -0
- data/app/views/solid_stack_web/recurring_tasks/index.html.erb +67 -0
- data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +9 -0
- data/config/routes.rb +13 -5
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 777352d5919952ea022a545b13608ace8f35e0f91077431c45a679e5c30bec19
|
|
4
|
+
data.tar.gz: d36dbf96b4234d730249df1d259099883d0473c160c62ab9bee73fe88ff133c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eefbb43300e8e244b6354d1fb952edc7577585c76f5721920d6fa28aba41038840d4924cae9799903a3742e0782a2e8f95d8b142502194d1726e87fbb8c35e7a
|
|
7
|
+
data.tar.gz: 254f13440a71b0dcfbeb3330f2c1be4c21a5e038e09355493bba1fe5b3767b70d314ba37982d7adced753915a971d159d06bdc29741e5c37e223e520184f34e9
|
data/README.md
CHANGED
|
@@ -10,8 +10,12 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
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
|
|
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
|
|
14
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
|
|
15
19
|
- **Solid Cache** — entry count and total byte size at a glance
|
|
16
20
|
- **Solid Cable** — active message count and distinct channel count
|
|
17
21
|
- **Turbo Stream** job discard — removes the row inline without a full page reload
|
|
@@ -82,6 +86,7 @@ The `authenticate` block is evaluated in the context of each request's controlle
|
|
|
82
86
|
- [solid_cache](https://github.com/rails/solid_cache) >= 1.0
|
|
83
87
|
- [solid_cable](https://github.com/rails/solid_cable) >= 1.0
|
|
84
88
|
- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
|
|
89
|
+
- [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
|
|
85
90
|
|
|
86
91
|
## Contributing
|
|
87
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; }
|
|
@@ -15,7 +15,7 @@ module SolidStackWeb
|
|
|
15
15
|
|
|
16
16
|
def current_section
|
|
17
17
|
case controller_name
|
|
18
|
-
when "jobs", "failed_jobs", "queues", "processes" then :queue
|
|
18
|
+
when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue
|
|
19
19
|
when "cache" then :cache
|
|
20
20
|
when "cable" then :cable
|
|
21
21
|
else :overview
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
module FailedJobs
|
|
3
3
|
class SelectionsController < ApplicationController
|
|
4
|
+
before_action :set_ids
|
|
5
|
+
|
|
4
6
|
def create
|
|
5
|
-
|
|
6
|
-
SolidQueue::FailedExecution.where(id: ids).each(&:retry)
|
|
7
|
+
SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
|
|
7
8
|
redirect_to failed_jobs_path
|
|
8
9
|
rescue => e
|
|
9
10
|
redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def destroy
|
|
13
|
-
|
|
14
|
-
job_ids = SolidQueue::FailedExecution.where(id: ids).pluck(:job_id)
|
|
14
|
+
job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
|
|
15
15
|
SolidQueue::Job.where(id: job_ids).destroy_all
|
|
16
16
|
redirect_to failed_jobs_path
|
|
17
17
|
rescue => e
|
|
18
18
|
redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def set_ids
|
|
24
|
+
@ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
|
|
25
|
+
end
|
|
20
26
|
end
|
|
21
27
|
end
|
|
22
28
|
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,13 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class Queues::PausesController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
::SolidQueue::Pause.find_or_create_by!(queue_name: params[:queue_id])
|
|
5
|
+
redirect_back_or_to queues_path
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def destroy
|
|
9
|
+
::SolidQueue::Pause.find_by(queue_name: params[:queue_id])&.destroy
|
|
10
|
+
redirect_back_or_to queues_path
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -13,14 +13,15 @@ module SolidStackWeb
|
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
def show
|
|
17
|
+
@queue_name = params[:id]
|
|
18
|
+
@paused = ::SolidQueue::Pause.exists?(queue_name: @queue_name)
|
|
19
|
+
@pagy, @executions = pagy(
|
|
20
|
+
::SolidQueue::ReadyExecution
|
|
21
|
+
.where(queue_name: @queue_name)
|
|
22
|
+
.includes(:job)
|
|
23
|
+
.order(created_at: :desc)
|
|
24
|
+
)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class RecurringTasks::RunsController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
task = SolidQueue::RecurringTask.find_by!(key: params[:recurring_task_key])
|
|
5
|
+
result = task.enqueue(at: Time.current)
|
|
6
|
+
|
|
7
|
+
if result
|
|
8
|
+
redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
|
|
9
|
+
else
|
|
10
|
+
redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
|
|
11
|
+
end
|
|
12
|
+
rescue ActiveRecord::RecordNotFound
|
|
13
|
+
redirect_to recurring_tasks_path, alert: "Recurring task not found."
|
|
14
|
+
rescue => e
|
|
15
|
+
redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module SolidStackWeb
|
|
2
|
+
class ScheduledJobsController < ApplicationController
|
|
3
|
+
def create
|
|
4
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
5
|
+
job_ids = scheduled_scope.pluck("solid_queue_jobs.id")
|
|
6
|
+
SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
7
|
+
SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
|
|
8
|
+
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
9
|
+
notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
|
|
10
|
+
rescue => e
|
|
11
|
+
redirect_to jobs_path(status: "scheduled", period: @period),
|
|
12
|
+
alert: "Could not run jobs: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
@execution = SolidQueue::ScheduledExecution.find(params[:id])
|
|
17
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
18
|
+
@run_now = params[:offset] == "now"
|
|
19
|
+
new_time = resolve_new_time(@execution, params[:offset])
|
|
20
|
+
|
|
21
|
+
@execution.update!(scheduled_at: new_time)
|
|
22
|
+
@execution.job.update!(scheduled_at: new_time)
|
|
23
|
+
|
|
24
|
+
respond_to do |format|
|
|
25
|
+
format.turbo_stream
|
|
26
|
+
format.html do
|
|
27
|
+
notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
|
|
28
|
+
redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
rescue ArgumentError => e
|
|
32
|
+
redirect_to jobs_path(status: "scheduled"), alert: e.message
|
|
33
|
+
rescue => e
|
|
34
|
+
redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def scheduled_scope
|
|
40
|
+
scope = SolidQueue::ScheduledExecution.joins(:job)
|
|
41
|
+
scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
42
|
+
scope
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resolve_new_time(execution, offset)
|
|
46
|
+
return 1.second.ago if offset == "now"
|
|
47
|
+
raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
|
|
48
|
+
|
|
49
|
+
execution.scheduled_at + PERIOD_DURATIONS[offset]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module SolidStackWeb
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def format_duration(seconds)
|
|
4
|
+
s = seconds.to_i
|
|
5
|
+
return "#{s}s" if s < 60
|
|
6
|
+
return "#{s / 60}m #{s % 60}s" if s < 3600
|
|
7
|
+
|
|
8
|
+
"#{s / 3600}h #{(s % 3600) / 60}m"
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
def inline_styles
|
|
4
12
|
dir = SolidStackWeb::Engine.root.join("app/assets/stylesheets/solid_stack_web")
|
|
5
13
|
css = dir.glob("_*.css").sort.map(&:read).join("\n")
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
|
|
34
34
|
<%= link_to "Queues", queues_path,
|
|
35
35
|
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
|
|
36
|
+
<%= link_to "Recurring", recurring_tasks_path,
|
|
37
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "recurring_tasks"}" %>
|
|
38
|
+
<%= link_to "History", history_path,
|
|
39
|
+
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %>
|
|
36
40
|
<%= link_to "Processes", processes_path,
|
|
37
41
|
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
|
|
38
42
|
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<h1 class="sqw-page-title">Job History</h1>
|
|
3
|
+
<div class="sqw-header-actions">
|
|
4
|
+
<% if @jobs&.any? %>
|
|
5
|
+
<%= link_to "Export CSV", history_path(format: :csv, queue: @queue, q: @search, period: @period),
|
|
6
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<form class="sqw-filters" action="<%= history_path %>" method="get">
|
|
12
|
+
<% if @queue.present? %>
|
|
13
|
+
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
14
|
+
<% end %>
|
|
15
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
16
|
+
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
17
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
|
|
18
|
+
<button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
|
|
19
|
+
<% if @search.present? %>
|
|
20
|
+
<%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<div class="sqw-period-filter" role="group" aria-label="Time period">
|
|
23
|
+
<%= link_to "All", history_path(queue: @queue, q: @search),
|
|
24
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
|
|
25
|
+
<%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"),
|
|
26
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
|
|
27
|
+
<%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"),
|
|
28
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
|
|
29
|
+
<%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"),
|
|
30
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
|
|
31
|
+
</div>
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<% if @queue.present? %>
|
|
35
|
+
<p class="sqw-muted" style="font-size: 13px; margin-bottom: 0.75rem;">
|
|
36
|
+
Filtering by queue: <strong><%= @queue %></strong> —
|
|
37
|
+
<%= link_to "Clear filter", history_path(q: @search, period: @period) %>
|
|
38
|
+
</p>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<% if @jobs.any? %>
|
|
42
|
+
<div class="sqw-detail-card">
|
|
43
|
+
<table class="sqw-table">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Job Class</th>
|
|
47
|
+
<th>Queue</th>
|
|
48
|
+
<th>Duration</th>
|
|
49
|
+
<th>Finished At</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<% @jobs.each do |job| %>
|
|
54
|
+
<tr>
|
|
55
|
+
<td class="sqw-monospace"><%= job.class_name %></td>
|
|
56
|
+
<td>
|
|
57
|
+
<%= link_to job.queue_name,
|
|
58
|
+
history_path(queue: job.queue_name, q: @search, period: @period),
|
|
59
|
+
class: "sqw-badge sqw-badge--queue" %>
|
|
60
|
+
</td>
|
|
61
|
+
<td class="sqw-monospace"><%= format_duration(job.finished_at - job.created_at) %></td>
|
|
62
|
+
<td class="sqw-muted"><%= job.finished_at.strftime("%b %d %H:%M:%S") %></td>
|
|
63
|
+
</tr>
|
|
64
|
+
<% end %>
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
68
|
+
</div>
|
|
69
|
+
<% else %>
|
|
70
|
+
<div class="sqw-empty">
|
|
71
|
+
<p>No finished jobs found.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
<div class="sqw-header-actions">
|
|
4
4
|
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
5
5
|
class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
|
|
6
|
+
<% if @status == "scheduled" && @executions&.any? %>
|
|
7
|
+
<%= button_to "Run All Now (#{@pagy.count})",
|
|
8
|
+
run_all_now_scheduled_jobs_path(period: @period),
|
|
9
|
+
method: :post,
|
|
10
|
+
class: "sqw-btn sqw-btn--sm",
|
|
11
|
+
data: { turbo_confirm: "Run all #{@pagy.count} scheduled jobs immediately?",
|
|
12
|
+
turbo_frame: "_top" } %>
|
|
13
|
+
<% end %>
|
|
6
14
|
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
|
|
7
15
|
<%= button_to "Discard All (#{@pagy.count})",
|
|
8
16
|
discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
@@ -111,9 +119,22 @@
|
|
|
111
119
|
<td><%= execution.job.priority %></td>
|
|
112
120
|
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
113
121
|
<% if @status == "scheduled" %>
|
|
114
|
-
<td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
|
|
122
|
+
<td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
|
|
115
123
|
<% end %>
|
|
116
124
|
<td class="sqw-actions">
|
|
125
|
+
<% if @status == "scheduled" %>
|
|
126
|
+
<%= button_to "Run Now", scheduled_job_path(execution),
|
|
127
|
+
method: :patch,
|
|
128
|
+
params: { offset: "now", period: @period },
|
|
129
|
+
class: "sqw-btn sqw-btn--sm",
|
|
130
|
+
data: { turbo_confirm: "Run this job immediately?" } %>
|
|
131
|
+
<% %w[1h 24h 7d].each do |offset| %>
|
|
132
|
+
<%= button_to "+#{offset}", scheduled_job_path(execution),
|
|
133
|
+
method: :patch,
|
|
134
|
+
params: { offset: offset, period: @period },
|
|
135
|
+
class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
136
|
+
<% end %>
|
|
137
|
+
<% end %>
|
|
117
138
|
<% if %w[ready scheduled blocked].include?(@status) %>
|
|
118
139
|
<%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
119
140
|
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
<tbody>
|
|
16
16
|
<% @queues.each do |queue| %>
|
|
17
17
|
<tr>
|
|
18
|
-
<td class="sqw-monospace"><%= queue[:name] %></td>
|
|
19
|
-
<td><%= queue[:size] %></td>
|
|
18
|
+
<td class="sqw-monospace"><%= link_to queue[:name], queue_path(queue[:name]) %></td>
|
|
19
|
+
<td><%= link_to queue[:size], queue_path(queue[:name]) %></td>
|
|
20
20
|
<td>
|
|
21
21
|
<% if queue[:paused] %>
|
|
22
22
|
<span class="sqw-badge sqw-badge--paused">Paused</span>
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
</td>
|
|
27
27
|
<td class="sqw-actions">
|
|
28
28
|
<% if queue[:paused] %>
|
|
29
|
-
<%= button_to "Resume",
|
|
29
|
+
<%= button_to "Resume", queue_pause_path(queue[:name]),
|
|
30
30
|
method: :delete, class: "sqw-btn sqw-btn--sm" %>
|
|
31
31
|
<% else %>
|
|
32
|
-
<%= button_to "Pause",
|
|
32
|
+
<%= button_to "Pause", queue_pause_path(queue[:name]),
|
|
33
33
|
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
34
34
|
<% end %>
|
|
35
35
|
</td>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div class="sqw-page-header sqw-page-header--split">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="sqw-breadcrumb">
|
|
4
|
+
<%= link_to "Queues", queues_path %> › <%= @queue_name %>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="sqw-page-title-row">
|
|
7
|
+
<h1 class="sqw-page-title sqw-monospace"><%= @queue_name %></h1>
|
|
8
|
+
<% if @paused %>
|
|
9
|
+
<span class="sqw-badge sqw-badge--paused">Paused</span>
|
|
10
|
+
<% else %>
|
|
11
|
+
<span class="sqw-badge sqw-badge--ready">Running</span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="sqw-header-actions">
|
|
16
|
+
<% if @paused %>
|
|
17
|
+
<%= button_to "Resume", queue_pause_path(@queue_name),
|
|
18
|
+
method: :delete, class: "sqw-btn sqw-btn--sm" %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= button_to "Pause", queue_pause_path(@queue_name),
|
|
21
|
+
method: :post, class: "sqw-btn sqw-btn--sm" %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% if @executions.any? %>
|
|
24
|
+
<%= button_to "Discard All Ready (#{@pagy.count})",
|
|
25
|
+
discard_all_jobs_path(status: "ready", queue: @queue_name),
|
|
26
|
+
method: :post,
|
|
27
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
28
|
+
data: { turbo_confirm: "Discard all #{@pagy.count} ready jobs in #{@queue_name}? This cannot be undone.",
|
|
29
|
+
turbo_frame: "_top" } %>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<% if @executions.any? %>
|
|
35
|
+
<table class="sqw-table">
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Job Class</th>
|
|
39
|
+
<th>Priority</th>
|
|
40
|
+
<th>Enqueued At</th>
|
|
41
|
+
<th></th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<% @executions.each do |execution| %>
|
|
46
|
+
<tr>
|
|
47
|
+
<td class="sqw-monospace">
|
|
48
|
+
<%= link_to execution.job.class_name, job_path(execution.id, status: "ready"),
|
|
49
|
+
data: { turbo_frame: "_top" } %>
|
|
50
|
+
</td>
|
|
51
|
+
<td><%= execution.job.priority %></td>
|
|
52
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
53
|
+
<td class="sqw-actions">
|
|
54
|
+
<%= button_to "Discard", job_path(execution, status: "ready", queue: @queue_name),
|
|
55
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
56
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<% end %>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
63
|
+
<% else %>
|
|
64
|
+
<div class="sqw-empty">
|
|
65
|
+
<p>No ready jobs in <strong><%= @queue_name %></strong>.</p>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div class="sqw-page-header">
|
|
2
|
+
<h1 class="sqw-page-title">Recurring Tasks</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<% if @recurring_tasks.any? %>
|
|
6
|
+
<table class="sqw-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>
|
|
9
|
+
<th>Key</th>
|
|
10
|
+
<th>Schedule</th>
|
|
11
|
+
<th>Job / Command</th>
|
|
12
|
+
<th>Queue</th>
|
|
13
|
+
<th>Next Run</th>
|
|
14
|
+
<th>Last Run</th>
|
|
15
|
+
<th>Type</th>
|
|
16
|
+
<th></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @recurring_tasks.each do |task| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td class="sqw-monospace"><%= task.key %></td>
|
|
23
|
+
<td class="sqw-monospace"><%= task.schedule %></td>
|
|
24
|
+
<td>
|
|
25
|
+
<% if task.class_name.present? %>
|
|
26
|
+
<span class="sqw-monospace"><%= task.class_name %></span>
|
|
27
|
+
<% if task.arguments.present? %>
|
|
28
|
+
<div class="sqw-muted sqw-monospace" style="font-size: 11px;"><%= task.arguments.inspect %></div>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% else %>
|
|
31
|
+
<span class="sqw-monospace sqw-muted"><%= task.command %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% if task.description.present? %>
|
|
34
|
+
<div class="sqw-muted" style="font-size: 12px; margin-top: 0.2rem;"><%= task.description %></div>
|
|
35
|
+
<% end %>
|
|
36
|
+
</td>
|
|
37
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= task.queue_name.presence || "default" %></span></td>
|
|
38
|
+
<td class="sqw-muted sqw-monospace">
|
|
39
|
+
<% next_run = begin; task.next_time.strftime("%b %d %H:%M"); rescue; nil; end %>
|
|
40
|
+
<%= next_run || "—" %>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="sqw-muted sqw-monospace">
|
|
43
|
+
<% last_run = task.last_enqueued_time %>
|
|
44
|
+
<%= last_run ? last_run.strftime("%b %d %H:%M") : "—" %>
|
|
45
|
+
</td>
|
|
46
|
+
<td>
|
|
47
|
+
<% if task.static? %>
|
|
48
|
+
<span class="sqw-badge sqw-badge--scheduled">Static</span>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="sqw-badge sqw-badge--blocked">Dynamic</span>
|
|
51
|
+
<% end %>
|
|
52
|
+
</td>
|
|
53
|
+
<td class="sqw-actions">
|
|
54
|
+
<%= button_to "Run Now", recurring_task_run_path(task.key),
|
|
55
|
+
method: :post,
|
|
56
|
+
class: "sqw-btn sqw-btn--sm",
|
|
57
|
+
data: { turbo_confirm: "Run \"#{task.key}\" immediately?" } %>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<% end %>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
<% else %>
|
|
64
|
+
<div class="sqw-empty">
|
|
65
|
+
<p>No recurring tasks configured.</p>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% if @run_now %>
|
|
2
|
+
<%= turbo_stream.remove "execution_#{@execution.id}" %>
|
|
3
|
+
<% else %>
|
|
4
|
+
<%= turbo_stream.replace "scheduled_at_#{@execution.id}" do %>
|
|
5
|
+
<td id="scheduled_at_<%= @execution.id %>" class="sqw-muted">
|
|
6
|
+
<%= @execution.scheduled_at.strftime("%b %d %H:%M") %>
|
|
7
|
+
</td>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,16 @@ SolidStackWeb::Engine.routes.draw do
|
|
|
4
4
|
resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections"
|
|
5
5
|
resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy], controller: "failed_jobs/selections"
|
|
6
6
|
|
|
7
|
+
resources :recurring_tasks, only: [:index], param: :key do
|
|
8
|
+
resource :run, only: [:create], controller: "recurring_tasks/runs"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
resources :scheduled_jobs, only: [:update] do
|
|
12
|
+
collection do
|
|
13
|
+
post :run_all_now, action: :create
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
7
17
|
resources :jobs, only: [:index, :show, :destroy] do
|
|
8
18
|
collection do
|
|
9
19
|
post :discard_all, action: :destroy
|
|
@@ -15,15 +25,13 @@ SolidStackWeb::Engine.routes.draw do
|
|
|
15
25
|
resource :arguments, only: [:update], controller: "failed_jobs/arguments"
|
|
16
26
|
end
|
|
17
27
|
|
|
18
|
-
resources :queues, only: [:index] do
|
|
19
|
-
|
|
20
|
-
post :pause
|
|
21
|
-
delete :resume
|
|
22
|
-
end
|
|
28
|
+
resources :queues, only: [:index, :show] do
|
|
29
|
+
resource :pause, only: [:create, :destroy], controller: "queues/pauses"
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
resources :processes, only: [:index]
|
|
26
33
|
|
|
34
|
+
get "history", to: "history#index", as: :history
|
|
27
35
|
get "cache", to: "cache#index", as: :cache
|
|
28
36
|
get "cable", to: "cable#index", as: :cable
|
|
29
37
|
end
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -150,10 +150,15 @@ files:
|
|
|
150
150
|
- app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
|
|
151
151
|
- app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
|
|
152
152
|
- app/controllers/solid_stack_web/failed_jobs_controller.rb
|
|
153
|
+
- app/controllers/solid_stack_web/history_controller.rb
|
|
153
154
|
- app/controllers/solid_stack_web/jobs/selections_controller.rb
|
|
154
155
|
- app/controllers/solid_stack_web/jobs_controller.rb
|
|
155
156
|
- app/controllers/solid_stack_web/processes_controller.rb
|
|
157
|
+
- app/controllers/solid_stack_web/queues/pauses_controller.rb
|
|
156
158
|
- app/controllers/solid_stack_web/queues_controller.rb
|
|
159
|
+
- app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb
|
|
160
|
+
- app/controllers/solid_stack_web/recurring_tasks_controller.rb
|
|
161
|
+
- app/controllers/solid_stack_web/scheduled_jobs_controller.rb
|
|
157
162
|
- app/helpers/solid_stack_web/application_helper.rb
|
|
158
163
|
- app/javascript/solid_stack_web/application.js
|
|
159
164
|
- app/javascript/solid_stack_web/selection_controller.js
|
|
@@ -165,12 +170,16 @@ files:
|
|
|
165
170
|
- app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
|
|
166
171
|
- app/views/solid_stack_web/failed_jobs/index.html.erb
|
|
167
172
|
- app/views/solid_stack_web/failed_jobs/show.html.erb
|
|
173
|
+
- app/views/solid_stack_web/history/index.html.erb
|
|
168
174
|
- app/views/solid_stack_web/jobs/_empty.html.erb
|
|
169
175
|
- app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
|
|
170
176
|
- app/views/solid_stack_web/jobs/index.html.erb
|
|
171
177
|
- app/views/solid_stack_web/jobs/show.html.erb
|
|
172
178
|
- app/views/solid_stack_web/processes/index.html.erb
|
|
173
179
|
- app/views/solid_stack_web/queues/index.html.erb
|
|
180
|
+
- app/views/solid_stack_web/queues/show.html.erb
|
|
181
|
+
- app/views/solid_stack_web/recurring_tasks/index.html.erb
|
|
182
|
+
- app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb
|
|
174
183
|
- config/importmap.rb
|
|
175
184
|
- config/routes.rb
|
|
176
185
|
- lib/solid_stack_web.rb
|