solid_queue_web 0.5.5 → 0.7.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 +28 -8
- data/Rakefile +2 -2
- data/app/assets/stylesheets/solid_queue_web/_01_base.css +41 -0
- data/app/assets/stylesheets/solid_queue_web/_02_layout.css +105 -0
- data/app/assets/stylesheets/solid_queue_web/_03_stats.css +49 -0
- data/app/assets/stylesheets/solid_queue_web/_04_table.css +52 -0
- data/app/assets/stylesheets/solid_queue_web/_05_badges.css +27 -0
- data/app/assets/stylesheets/solid_queue_web/_06_buttons.css +38 -0
- data/app/assets/stylesheets/solid_queue_web/_07_forms.css +103 -0
- data/app/assets/stylesheets/solid_queue_web/_08_detail.css +84 -0
- data/app/assets/stylesheets/solid_queue_web/_09_pagination.css +27 -0
- data/app/assets/stylesheets/solid_queue_web/_10_responsive.css +73 -0
- data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +68 -0
- data/app/assets/stylesheets/solid_queue_web/application.css +1 -548
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +12 -0
- data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +27 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +5 -3
- data/app/controllers/solid_queue_web/history_controller.rb +16 -0
- data/app/controllers/solid_queue_web/jobs/selections_controller.rb +21 -0
- data/app/controllers/solid_queue_web/jobs_controller.rb +13 -8
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +1 -1
- data/app/controllers/solid_queue_web/queues_controller.rb +15 -0
- data/app/controllers/solid_queue_web/search_controller.rb +23 -0
- data/app/helpers/solid_queue_web/application_helper.rb +15 -1
- data/app/javascript/solid_queue_web/application.js +4 -0
- data/app/javascript/solid_queue_web/refresh_controller.js +52 -0
- data/app/javascript/solid_queue_web/search_controller.js +5 -0
- data/app/javascript/solid_queue_web/selection_controller.js +42 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +2 -0
- data/app/views/solid_queue_web/dashboard/index.html.erb +41 -1
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +93 -49
- data/app/views/solid_queue_web/history/index.html.erb +67 -0
- data/app/views/solid_queue_web/jobs/index.html.erb +115 -49
- data/app/views/solid_queue_web/processes/index.html.erb +3 -1
- data/app/views/solid_queue_web/queues/index.html.erb +15 -1
- data/app/views/solid_queue_web/search/index.html.erb +64 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +16 -6
- data/lib/solid_queue_web/version.rb +1 -1
- metadata +20 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class HistoryController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@queue = params[:queue].presence
|
|
5
|
+
@search = params[:q].presence
|
|
6
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
7
|
+
|
|
8
|
+
scope = SolidQueue::Job.where.not(finished_at: nil)
|
|
9
|
+
scope = scope.where(queue_name: @queue) if @queue.present?
|
|
10
|
+
scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
|
+
scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
|
+
|
|
13
|
+
@pagy, @jobs = pagy(scope.order(finished_at: :desc))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
module Jobs
|
|
3
|
+
class SelectionsController < ApplicationController
|
|
4
|
+
def destroy
|
|
5
|
+
status = params[:status]
|
|
6
|
+
period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
7
|
+
raise ArgumentError, "Cannot discard #{status} jobs." unless Job::DISCARDABLE.include?(status)
|
|
8
|
+
model = Job::EXECUTION_MODELS[status]
|
|
9
|
+
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
|
10
|
+
jobs = model.where(id: ids).includes(:job).map(&:job)
|
|
11
|
+
model.discard_all_from_jobs(jobs)
|
|
12
|
+
redirect_to jobs_path(status: status, period: period),
|
|
13
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
14
|
+
rescue ArgumentError => e
|
|
15
|
+
redirect_to jobs_path(status: status), alert: e.message
|
|
16
|
+
rescue => e
|
|
17
|
+
redirect_to jobs_path(status: status), alert: "Could not discard jobs: #{e.message}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_status, only: [
|
|
3
|
+
before_action :set_status, only: [:destroy, :discard_all, :discard_selected]
|
|
4
4
|
|
|
5
5
|
def index
|
|
6
6
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
7
7
|
@search = params[:q].presence
|
|
8
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
8
9
|
@jobs = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
9
10
|
@jobs = @jobs.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
|
+
@jobs = @jobs.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
10
12
|
@pagy, @jobs = pagy(@jobs.order(created_at: :desc))
|
|
11
13
|
end
|
|
12
14
|
|
|
@@ -24,24 +26,24 @@ module SolidQueueWeb
|
|
|
24
26
|
@remaining_count = filtered_scope(model).count
|
|
25
27
|
respond_to do |format|
|
|
26
28
|
format.turbo_stream
|
|
27
|
-
format.html { redirect_to jobs_path(status: @status), notice: "Job discarded." }
|
|
29
|
+
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
|
|
28
30
|
end
|
|
29
31
|
rescue ArgumentError => e
|
|
30
|
-
redirect_to jobs_path(status: @status), alert: e.message
|
|
32
|
+
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
31
33
|
rescue => e
|
|
32
|
-
redirect_to jobs_path(status: @status), alert: "Could not discard job: #{e.message}"
|
|
34
|
+
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def discard_all
|
|
36
38
|
model = execution_model_for!(@status)
|
|
37
39
|
jobs = filtered_scope(model).map(&:job)
|
|
38
40
|
model.discard_all_from_jobs(jobs)
|
|
39
|
-
redirect_to jobs_path(status: @status),
|
|
41
|
+
redirect_to jobs_path(status: @status, period: @period),
|
|
40
42
|
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
41
43
|
rescue ArgumentError => e
|
|
42
|
-
redirect_to jobs_path(status: @status), alert: e.message
|
|
44
|
+
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
43
45
|
rescue => e
|
|
44
|
-
redirect_to jobs_path(status: @status), alert: "Could not discard jobs: #{e.message}"
|
|
46
|
+
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
private
|
|
@@ -57,10 +59,13 @@ module SolidQueueWeb
|
|
|
57
59
|
|
|
58
60
|
def set_status
|
|
59
61
|
@status = params[:status]
|
|
62
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def filtered_scope(model)
|
|
63
|
-
model.includes(:job)
|
|
66
|
+
scope = model.includes(:job)
|
|
67
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
68
|
+
scope
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def execution_model_for!(status)
|
|
@@ -2,7 +2,7 @@ module SolidQueueWeb
|
|
|
2
2
|
module Queues
|
|
3
3
|
class JobsController < ApplicationController
|
|
4
4
|
before_action :set_queue
|
|
5
|
-
before_action :set_status, only: [
|
|
5
|
+
before_action :set_status, only: [:destroy, :discard_all]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@status = params[:status].presence_in(Job::STATUSES) || "ready"
|
|
@@ -2,6 +2,21 @@ module SolidQueueWeb
|
|
|
2
2
|
class QueuesController < ApplicationController
|
|
3
3
|
def index
|
|
4
4
|
@queues = SolidQueue::Queue.all.sort_by(&:name)
|
|
5
|
+
|
|
6
|
+
now = Time.current
|
|
7
|
+
@completed_24h = SolidQueue::Job
|
|
8
|
+
.where(finished_at: 24.hours.ago..now)
|
|
9
|
+
.group(:queue_name)
|
|
10
|
+
.count
|
|
11
|
+
@failed_24h = SolidQueue::FailedExecution
|
|
12
|
+
.joins(:job)
|
|
13
|
+
.where(created_at: 24.hours.ago..now)
|
|
14
|
+
.group("solid_queue_jobs.queue_name")
|
|
15
|
+
.count
|
|
16
|
+
@oldest_ready = SolidQueue::ReadyExecution
|
|
17
|
+
.joins(:job)
|
|
18
|
+
.group("solid_queue_jobs.queue_name")
|
|
19
|
+
.minimum("solid_queue_jobs.created_at")
|
|
5
20
|
end
|
|
6
21
|
|
|
7
22
|
def pause
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class SearchController < ApplicationController
|
|
3
|
+
LIMIT = 25
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@query = params[:q].presence
|
|
7
|
+
@job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
|
|
8
|
+
@results = {}
|
|
9
|
+
|
|
10
|
+
return unless @query
|
|
11
|
+
|
|
12
|
+
Job::EXECUTION_MODELS.each do |status, model|
|
|
13
|
+
scope = model.includes(:job)
|
|
14
|
+
.references(:job)
|
|
15
|
+
.where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%")
|
|
16
|
+
.order(created_at: :desc)
|
|
17
|
+
total = scope.count
|
|
18
|
+
executions = scope.limit(LIMIT).to_a
|
|
19
|
+
@results[status] = { executions: executions, total: total } unless executions.empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
module ApplicationHelper
|
|
3
3
|
def inline_styles
|
|
4
|
-
|
|
4
|
+
dir = SolidQueueWeb::Engine.root.join("app/assets/stylesheets/solid_queue_web")
|
|
5
|
+
css = dir.glob("_*.css").sort.map(&:read).join("\n")
|
|
5
6
|
content_tag(:style, css.html_safe)
|
|
6
7
|
end
|
|
8
|
+
|
|
9
|
+
def format_duration(seconds)
|
|
10
|
+
s = seconds.to_i
|
|
11
|
+
return "< 1s" if s < 1
|
|
12
|
+
|
|
13
|
+
if s < 60
|
|
14
|
+
"#{s}s"
|
|
15
|
+
elsif s < 3600
|
|
16
|
+
"#{s / 60}m #{s % 60}s"
|
|
17
|
+
else
|
|
18
|
+
"#{s / 3600}h #{(s % 3600) / 60}m"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
7
21
|
end
|
|
8
22
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import "@hotwired/turbo"
|
|
2
2
|
import { Application } from "@hotwired/stimulus"
|
|
3
3
|
import SearchController from "solid_queue_web/search_controller"
|
|
4
|
+
import RefreshController from "solid_queue_web/refresh_controller"
|
|
5
|
+
import SelectionController from "solid_queue_web/selection_controller"
|
|
4
6
|
|
|
5
7
|
const application = Application.start()
|
|
6
8
|
application.register("search", SearchController)
|
|
9
|
+
application.register("refresh", RefreshController)
|
|
10
|
+
application.register("selection", SelectionController)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = { interval: { type: Number, default: 5000 } }
|
|
5
|
+
|
|
6
|
+
initialize() {
|
|
7
|
+
this._onVisibilityChange = this._onVisibilityChange.bind(this)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
document.addEventListener("visibilitychange", this._onVisibilityChange)
|
|
12
|
+
this._schedule()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
disconnect() {
|
|
16
|
+
clearTimeout(this._timer)
|
|
17
|
+
document.removeEventListener("visibilitychange", this._onVisibilityChange)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_schedule() {
|
|
21
|
+
this._timer = setTimeout(() => this._reload(), this.intervalValue)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async _reload() {
|
|
25
|
+
clearTimeout(this._timer)
|
|
26
|
+
const hasSelection = this.element.querySelector("input[type='checkbox']:checked")
|
|
27
|
+
if (!document.hidden && !hasSelection) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(window.location.href, {
|
|
30
|
+
headers: { "Turbo-Frame": this.element.id, Accept: "text/html" }
|
|
31
|
+
})
|
|
32
|
+
if (response.ok) {
|
|
33
|
+
const html = await response.text()
|
|
34
|
+
const doc = new DOMParser().parseFromString(html, "text/html")
|
|
35
|
+
const frame = doc.querySelector(`turbo-frame#${this.element.id}`)
|
|
36
|
+
if (frame && this.element.isConnected) this.element.innerHTML = frame.innerHTML
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// network error — skip this tick
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (this.element.isConnected) this._schedule()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_onVisibilityChange() {
|
|
46
|
+
if (document.hidden) {
|
|
47
|
+
clearTimeout(this._timer)
|
|
48
|
+
} else if (!this.element.querySelector("input[type='checkbox']:checked")) {
|
|
49
|
+
this._reload()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -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 = "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
|
+
}
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
<li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
|
|
27
27
|
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
|
|
28
28
|
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
|
|
29
|
+
<li><%= link_to "History", history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
|
|
29
30
|
<li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
|
|
30
31
|
<li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
|
|
31
32
|
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
|
|
33
|
+
<li><%= link_to "Search", search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %></li>
|
|
32
34
|
</ul>
|
|
33
35
|
</nav>
|
|
34
36
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: 5000 } do %>
|
|
1
2
|
<h1 class="sqd-page-title">Dashboard</h1>
|
|
2
3
|
|
|
3
4
|
<div class="sqd-stats">
|
|
@@ -33,6 +34,44 @@
|
|
|
33
34
|
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
34
35
|
<div class="sqd-stat__label">Processes</div>
|
|
35
36
|
<% end %>
|
|
37
|
+
<%= link_to history_path(period: "1h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
|
|
38
|
+
<div class="sqd-stat__value"><%= @throughput[:completed_1h] %></div>
|
|
39
|
+
<div class="sqd-stat__label">Done (1h)</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
<%= link_to history_path(period: "24h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
|
|
42
|
+
<div class="sqd-stat__value"><%= @throughput[:completed_24h] %></div>
|
|
43
|
+
<div class="sqd-stat__label">Done (24h)</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<% max_val = [@sparkline.max, 1].max %>
|
|
48
|
+
<div class="sqd-card" style="margin-bottom: 1rem;">
|
|
49
|
+
<div class="sqd-card__header">
|
|
50
|
+
<span class="sqd-card__title">Throughput — Last 12 Hours</span>
|
|
51
|
+
<div class="sqd-throughput__summary">
|
|
52
|
+
<span>1h: <strong><%= @throughput[:completed_1h] %></strong></span>
|
|
53
|
+
<span>24h: <strong><%= @throughput[:completed_24h] %></strong></span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<% if @throughput[:completed_24h] == 0 %>
|
|
57
|
+
<div class="sqd-sparkline__empty">No completed jobs in the last 24 hours</div>
|
|
58
|
+
<% else %>
|
|
59
|
+
<div class="sqd-sparkline" aria-label="Jobs completed per hour over the last 12 hours">
|
|
60
|
+
<% @sparkline.each_with_index do |count, i| %>
|
|
61
|
+
<% pct = (count.to_f / max_val * 100).round %>
|
|
62
|
+
<% hour_start = (12 - i).hours.ago %>
|
|
63
|
+
<% show_tick = [0, 3, 6, 9, 11].include?(i) %>
|
|
64
|
+
<div class="sqd-sparkline__col">
|
|
65
|
+
<div class="sqd-sparkline__bar-wrap">
|
|
66
|
+
<div class="sqd-sparkline__bar"
|
|
67
|
+
style="height: <%= [pct, 3].max %>%"
|
|
68
|
+
title="<%= hour_start.strftime('%-I%p').downcase %>: <%= count %> <%= "job".pluralize(count) %>"></div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : hour_start.strftime("%-I%p").downcase) : "" %></div>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
36
75
|
</div>
|
|
37
76
|
|
|
38
77
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
|
@@ -62,4 +101,5 @@
|
|
|
62
101
|
</div>
|
|
63
102
|
</div>
|
|
64
103
|
<% end %>
|
|
65
|
-
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
<div class="sqd-actions">
|
|
5
5
|
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
6
6
|
method: :post,
|
|
7
|
-
params: { queue: @queue, q: @search },
|
|
7
|
+
params: { queue: @queue, q: @search, period: @period },
|
|
8
8
|
class: "sqd-btn sqd-btn--primary",
|
|
9
9
|
data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
|
|
10
10
|
<%= button_to "Discard All", discard_all_failed_jobs_path,
|
|
11
11
|
method: :post,
|
|
12
|
-
params: { queue: @queue, q: @search },
|
|
12
|
+
params: { queue: @queue, q: @search, period: @period },
|
|
13
13
|
class: "sqd-btn sqd-btn--danger",
|
|
14
14
|
data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
|
|
15
15
|
</div>
|
|
@@ -20,69 +20,113 @@
|
|
|
20
20
|
<% if @queue.present? %>
|
|
21
21
|
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
22
22
|
<% end %>
|
|
23
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
23
24
|
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
24
25
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
25
26
|
data-action="input->search#filter">
|
|
26
|
-
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
27
27
|
<% if @search.present? %>
|
|
28
|
-
<%= link_to "Clear", failed_jobs_path(queue: @queue), class: "sqd-btn sqd-btn--muted" %>
|
|
28
|
+
<%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
29
29
|
<% end %>
|
|
30
|
+
<div class="sqd-period-filter" role="group" aria-label="Time period">
|
|
31
|
+
<%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time" %>
|
|
32
|
+
<%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour" %>
|
|
33
|
+
<%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours" %>
|
|
34
|
+
<%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days" %>
|
|
35
|
+
</div>
|
|
30
36
|
</form>
|
|
31
37
|
|
|
32
38
|
<% if @pagy.last > 1 %>
|
|
33
39
|
<%= @pagy.series_nav.html_safe %>
|
|
34
40
|
<% end %>
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
<% if @failed_jobs.any? %>
|
|
43
|
+
<div data-controller="selection">
|
|
44
|
+
<%= form_tag failed_job_selection_path, method: :post, id: "retry-selection-form" do %>
|
|
45
|
+
<%= hidden_field_tag :queue, @queue %>
|
|
46
|
+
<%= hidden_field_tag :q, @search %>
|
|
47
|
+
<%= hidden_field_tag :period, @period %>
|
|
48
|
+
<% end %>
|
|
49
|
+
|
|
50
|
+
<%= form_tag failed_job_selection_path, method: :delete, id: "discard-selection-form",
|
|
51
|
+
data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
|
|
52
|
+
<%= hidden_field_tag :queue, @queue %>
|
|
53
|
+
<%= hidden_field_tag :q, @search %>
|
|
54
|
+
<%= hidden_field_tag :period, @period %>
|
|
55
|
+
<% end %>
|
|
56
|
+
|
|
57
|
+
<div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
|
|
58
|
+
<span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
|
|
59
|
+
<button type="button" class="sqd-btn sqd-btn--primary sqd-btn--sm"
|
|
60
|
+
data-action="click->selection#submit"
|
|
61
|
+
data-selection-form-id-param="retry-selection-form">Retry Selected</button>
|
|
62
|
+
<button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
|
|
63
|
+
data-action="click->selection#submit"
|
|
64
|
+
data-selection-form-id-param="discard-selection-form">Discard Selected</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="sqd-card">
|
|
68
|
+
<table>
|
|
69
|
+
<thead>
|
|
53
70
|
<tr>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<% else %>
|
|
65
|
-
<span style="color:var(--muted)">—</span>
|
|
66
|
-
<% end %>
|
|
67
|
-
</td>
|
|
68
|
-
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
69
|
-
<td class="sqd-row-actions">
|
|
70
|
-
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
|
|
71
|
-
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
72
|
-
<%= button_to "Discard", failed_job_path(execution), method: :delete,
|
|
73
|
-
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
74
|
-
data: { confirm: "Discard this job?" } %>
|
|
75
|
-
</td>
|
|
71
|
+
<th scope="col">
|
|
72
|
+
<input type="checkbox" data-selection-target="selectAll"
|
|
73
|
+
data-action="change->selection#selectAll"
|
|
74
|
+
aria-label="Select all failed jobs">
|
|
75
|
+
</th>
|
|
76
|
+
<th scope="col">Job Class</th>
|
|
77
|
+
<th scope="col">Queue</th>
|
|
78
|
+
<th scope="col">Error</th>
|
|
79
|
+
<th scope="col">Failed At</th>
|
|
80
|
+
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
76
81
|
</tr>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
<% @failed_jobs.each do |execution| %>
|
|
85
|
+
<% job = execution.job %>
|
|
86
|
+
<tr>
|
|
87
|
+
<td>
|
|
88
|
+
<input type="checkbox" value="<%= execution.id %>"
|
|
89
|
+
data-selection-target="checkbox"
|
|
90
|
+
data-action="change->selection#toggle"
|
|
91
|
+
aria-label="Select job <%= job.class_name %>">
|
|
92
|
+
</td>
|
|
93
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
94
|
+
<td>
|
|
95
|
+
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
|
|
96
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
97
|
+
</td>
|
|
98
|
+
<td>
|
|
99
|
+
<% if execution.exception_class.present? %>
|
|
100
|
+
<div class="sqd-error-msg sqd-truncate" title="<%= execution.exception_class %>: <%= execution.message %>">
|
|
101
|
+
<strong><%= execution.exception_class %></strong>: <%= execution.message %>
|
|
102
|
+
</div>
|
|
103
|
+
<% else %>
|
|
104
|
+
<span style="color:var(--muted)">—</span>
|
|
105
|
+
<% end %>
|
|
106
|
+
</td>
|
|
107
|
+
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
108
|
+
<td class="sqd-row-actions">
|
|
109
|
+
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
|
|
110
|
+
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
111
|
+
<%= button_to "Discard", failed_job_path(execution), method: :delete,
|
|
112
|
+
class: "sqd-btn sqd-btn--danger sqd-btn--sm",
|
|
113
|
+
data: { confirm: "Discard this job?" } %>
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<% end %>
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<% else %>
|
|
122
|
+
<div class="sqd-card">
|
|
123
|
+
<div class="sqd-empty">No failed jobs. All clear!</div>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
82
126
|
|
|
83
127
|
<% if @queue.present? %>
|
|
84
128
|
<p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
|
|
85
129
|
Filtering by queue: <strong><%= @queue %></strong> —
|
|
86
|
-
<%= link_to "Clear filter", failed_jobs_path(q: @search) %>
|
|
130
|
+
<%= link_to "Clear filter", failed_jobs_path(q: @search, period: @period) %>
|
|
87
131
|
</p>
|
|
88
132
|
<% end %>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value: 10000 } do %>
|
|
2
|
+
<div class="sqd-page-header">
|
|
3
|
+
<h1 class="sqd-page-title">Job History</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<form class="sqd-search" action="<%= history_path %>" method="get" data-controller="search">
|
|
7
|
+
<% if @queue.present? %>
|
|
8
|
+
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
9
|
+
<% end %>
|
|
10
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
11
|
+
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
12
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
13
|
+
data-action="input->search#filter">
|
|
14
|
+
<% if @search.present? %>
|
|
15
|
+
<%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<div class="sqd-period-filter" role="group" aria-label="Time period">
|
|
18
|
+
<%= link_to "All", history_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil } %>
|
|
19
|
+
<%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil } %>
|
|
20
|
+
<%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil } %>
|
|
21
|
+
<%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil } %>
|
|
22
|
+
</div>
|
|
23
|
+
</form>
|
|
24
|
+
|
|
25
|
+
<% if @queue.present? %>
|
|
26
|
+
<p style="margin-top: 0.5rem; font-size: 13px; color: var(--muted);">
|
|
27
|
+
Filtering by queue: <strong><%= @queue %></strong> —
|
|
28
|
+
<%= link_to "Clear filter", history_path(q: @search, period: @period) %>
|
|
29
|
+
</p>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<% if @jobs.any? %>
|
|
33
|
+
<div class="sqd-card">
|
|
34
|
+
<table>
|
|
35
|
+
<thead>
|
|
36
|
+
<tr>
|
|
37
|
+
<th scope="col">Job Class</th>
|
|
38
|
+
<th scope="col">Queue</th>
|
|
39
|
+
<th scope="col">Duration</th>
|
|
40
|
+
<th scope="col">Finished At</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% @jobs.each do |job| %>
|
|
45
|
+
<tr>
|
|
46
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
47
|
+
<td>
|
|
48
|
+
<%= link_to job.queue_name, history_path(queue: job.queue_name, q: @search, period: @period),
|
|
49
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
50
|
+
</td>
|
|
51
|
+
<td class="sqd-mono"><%= format_duration(job.finished_at - job.created_at) %></td>
|
|
52
|
+
<td class="sqd-mono"><%= job.finished_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
<% else %>
|
|
59
|
+
<div class="sqd-card">
|
|
60
|
+
<div class="sqd-empty">No finished jobs found.</div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<% if @pagy.last > 1 %>
|
|
65
|
+
<%= @pagy.series_nav.html_safe %>
|
|
66
|
+
<% end %>
|
|
67
|
+
<% end %>
|