solid_stack_web 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +22 -2
- data/app/assets/stylesheets/solid_stack_web/_02_layout.css +8 -0
- data/app/assets/stylesheets/solid_stack_web/_04_table.css +1 -0
- data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +13 -0
- data/app/assets/stylesheets/solid_stack_web/_08_filters.css +59 -0
- data/app/assets/stylesheets/solid_stack_web/_09_detail.css +85 -0
- data/app/controllers/solid_stack_web/application_controller.rb +5 -1
- data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +17 -0
- data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +28 -0
- data/app/controllers/solid_stack_web/failed_jobs_controller.rb +35 -4
- data/app/controllers/solid_stack_web/history_controller.rb +42 -0
- data/app/controllers/solid_stack_web/jobs/selections_controller.rb +24 -0
- data/app/controllers/solid_stack_web/jobs_controller.rb +60 -21
- data/app/controllers/solid_stack_web/queues/pauses_controller.rb +13 -0
- data/app/controllers/solid_stack_web/queues_controller.rb +9 -8
- data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +18 -0
- data/app/controllers/solid_stack_web/recurring_tasks_controller.rb +7 -0
- data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +52 -0
- data/app/helpers/solid_stack_web/application_helper.rb +8 -0
- data/app/javascript/solid_stack_web/application.js +6 -0
- data/app/javascript/solid_stack_web/selection_controller.js +42 -0
- data/app/models/solid_stack_web/job.rb +21 -0
- data/app/views/layouts/solid_stack_web/application.html.erb +5 -0
- data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +9 -0
- data/app/views/solid_stack_web/failed_jobs/index.html.erb +61 -30
- data/app/views/solid_stack_web/failed_jobs/show.html.erb +58 -0
- data/app/views/solid_stack_web/history/index.html.erb +73 -0
- data/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +2 -2
- data/app/views/solid_stack_web/jobs/index.html.erb +138 -34
- data/app/views/solid_stack_web/jobs/show.html.erb +57 -0
- data/app/views/solid_stack_web/queues/index.html.erb +4 -4
- data/app/views/solid_stack_web/queues/show.html.erb +67 -0
- data/app/views/solid_stack_web/recurring_tasks/index.html.erb +67 -0
- data/app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb +9 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +23 -7
- data/lib/solid_stack_web/engine.rb +15 -0
- data/lib/solid_stack_web/version.rb +1 -1
- metadata +64 -1
|
@@ -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")
|
|
@@ -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
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
<%= inline_styles %>
|
|
10
|
+
<%= javascript_importmap_tags "solid_stack_web" %>
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
<header class="sqw-header">
|
|
@@ -32,6 +33,10 @@
|
|
|
32
33
|
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
|
|
33
34
|
<%= link_to "Queues", queues_path,
|
|
34
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"}" %>
|
|
35
40
|
<%= link_to "Processes", processes_path,
|
|
36
41
|
class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
|
|
37
42
|
</div>
|
|
@@ -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>
|
|
@@ -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 %>
|
|
@@ -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,154 @@
|
|
|
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 @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 %>
|
|
14
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
|
|
15
|
+
<%= button_to "Discard All (#{@pagy.count})",
|
|
16
|
+
discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
17
|
+
method: :post,
|
|
18
|
+
class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
19
|
+
data: { turbo_confirm: "Discard all #{@pagy.count} jobs? This cannot be undone.",
|
|
20
|
+
turbo_frame: "_top" } %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
3
23
|
</div>
|
|
4
24
|
|
|
5
25
|
<div class="sqw-tabs">
|
|
6
|
-
<%
|
|
7
|
-
<%= link_to label, jobs_path(status: status),
|
|
26
|
+
<% SolidStackWeb::Job::TAB_LABELS.each do |status, label| %>
|
|
27
|
+
<%= link_to label, jobs_path(status: status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
8
28
|
class: "sqw-tab #{"sqw-tab--active" if @status == status}" %>
|
|
9
29
|
<% end %>
|
|
10
30
|
</div>
|
|
11
31
|
|
|
32
|
+
<turbo-frame id="sqw-jobs-filter" data-turbo-action="advance">
|
|
33
|
+
<form class="sqw-filters" action="<%= jobs_path %>" method="get">
|
|
34
|
+
<%= hidden_field_tag :status, @status %>
|
|
35
|
+
<%= hidden_field_tag :period, @period %>
|
|
36
|
+
<input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
|
|
37
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
|
|
38
|
+
<% if @queue_options.size > 1 %>
|
|
39
|
+
<select name="queue" class="sqw-select" aria-label="Filter by queue" onchange="this.form.requestSubmit()">
|
|
40
|
+
<option value="">All queues</option>
|
|
41
|
+
<% @queue_options.each do |q| %>
|
|
42
|
+
<option value="<%= q %>" <%= "selected" if @queue == q %>><%= q %></option>
|
|
43
|
+
<% end %>
|
|
44
|
+
</select>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% if @priority_options.size > 1 %>
|
|
47
|
+
<select name="priority" class="sqw-select" aria-label="Filter by priority" onchange="this.form.requestSubmit()">
|
|
48
|
+
<option value="">All priorities</option>
|
|
49
|
+
<% @priority_options.each do |p| %>
|
|
50
|
+
<option value="<%= p %>" <%= "selected" if @priority.to_s == p.to_s %>>Priority <%= p %></option>
|
|
51
|
+
<% end %>
|
|
52
|
+
</select>
|
|
53
|
+
<% end %>
|
|
54
|
+
<button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
|
|
55
|
+
<% if @search.present? || @queue.present? || @priority.present? %>
|
|
56
|
+
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
|
|
57
|
+
<% end %>
|
|
58
|
+
<div class="sqw-period-filter" role="group" aria-label="Time period">
|
|
59
|
+
<%= link_to "All", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority),
|
|
60
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
|
|
61
|
+
<%= link_to "1h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "1h"),
|
|
62
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
|
|
63
|
+
<%= link_to "24h", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "24h"),
|
|
64
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
|
|
65
|
+
<%= link_to "7d", jobs_path(status: @status, q: @search, queue: @queue, priority: @priority, period: "7d"),
|
|
66
|
+
class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
|
|
67
|
+
</div>
|
|
68
|
+
</form>
|
|
69
|
+
|
|
12
70
|
<div id="sqw-jobs-table">
|
|
13
71
|
<% if @executions.any? %>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
72
|
+
<div data-controller="<%= "selection" if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>">
|
|
73
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
74
|
+
<%= form_with url: job_selection_path, method: :delete, id: "job-selection-form",
|
|
75
|
+
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %>
|
|
76
|
+
<%= f.hidden_field :status, value: @status %>
|
|
77
|
+
<%= f.hidden_field :q, value: @search %>
|
|
78
|
+
<%= f.hidden_field :queue, value: @queue %>
|
|
79
|
+
<%= f.hidden_field :period, value: @period %>
|
|
80
|
+
<%= f.hidden_field :priority, value: @priority %>
|
|
81
|
+
<% end %>
|
|
82
|
+
|
|
83
|
+
<div class="sqw-selection-bar" data-selection-target="bar" style="display: none;">
|
|
84
|
+
<span class="sqw-muted"><span data-selection-target="count">0</span> selected</span>
|
|
85
|
+
<button type="button" class="sqw-btn sqw-btn--danger sqw-btn--sm"
|
|
86
|
+
data-action="click->selection#submit"
|
|
87
|
+
data-selection-form-id-param="job-selection-form">Discard Selected</button>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
|
|
91
|
+
<table class="sqw-table">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr>
|
|
94
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
95
|
+
<th><input type="checkbox" class="sqw-checkbox" aria-label="Select all"
|
|
96
|
+
data-selection-target="selectAll"
|
|
97
|
+
data-action="change->selection#selectAll"></th>
|
|
34
98
|
<% end %>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</td>
|
|
99
|
+
<th>Job Class</th>
|
|
100
|
+
<th>Queue</th>
|
|
101
|
+
<th>Priority</th>
|
|
102
|
+
<th>Enqueued At</th>
|
|
103
|
+
<% if @status == "scheduled" %><th>Scheduled At</th><% end %>
|
|
104
|
+
<th></th>
|
|
42
105
|
</tr>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
106
|
+
</thead>
|
|
107
|
+
<tbody>
|
|
108
|
+
<% @executions.each do |execution| %>
|
|
109
|
+
<tr id="execution_<%= execution.id %>">
|
|
110
|
+
<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %>
|
|
111
|
+
<td><input type="checkbox" value="<%= execution.id %>"
|
|
112
|
+
class="sqw-checkbox"
|
|
113
|
+
aria-label="Select <%= execution.job.class_name %>"
|
|
114
|
+
data-selection-target="checkbox"
|
|
115
|
+
data-action="change->selection#toggle"></td>
|
|
116
|
+
<% end %>
|
|
117
|
+
<td class="sqw-monospace"><%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %></td>
|
|
118
|
+
<td><span class="sqw-badge sqw-badge--queue"><%= execution.job.queue_name %></span></td>
|
|
119
|
+
<td><%= execution.job.priority %></td>
|
|
120
|
+
<td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
|
|
121
|
+
<% if @status == "scheduled" %>
|
|
122
|
+
<td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
|
|
123
|
+
<% end %>
|
|
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 %>
|
|
138
|
+
<% if %w[ready scheduled blocked].include?(@status) %>
|
|
139
|
+
<%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
|
|
140
|
+
method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
|
|
141
|
+
data: { turbo_confirm: "Discard this job?" } %>
|
|
142
|
+
<% end %>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
<% end %>
|
|
146
|
+
</tbody>
|
|
147
|
+
</table>
|
|
148
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
|
149
|
+
</div>
|
|
47
150
|
<% else %>
|
|
48
151
|
<%= render "empty" %>
|
|
49
152
|
<% end %>
|
|
50
153
|
</div>
|
|
154
|
+
</turbo-frame>
|