solid_queue_web 0.6.0 → 0.8.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 +38 -6
- 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 +133 -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/_12_dark_mode.css +34 -0
- data/app/assets/stylesheets/solid_queue_web/application.css +1 -617
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +28 -0
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -22
- data/app/controllers/solid_queue_web/history_controller.rb +35 -0
- data/app/controllers/solid_queue_web/jobs_controller.rb +38 -23
- 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/retry_failed_jobs_controller.rb +31 -0
- data/app/controllers/solid_queue_web/search_controller.rb +1 -3
- data/app/helpers/solid_queue_web/application_helper.rb +15 -1
- data/app/javascript/solid_queue_web/application.js +2 -0
- data/app/javascript/solid_queue_web/refresh_controller.js +3 -2
- data/app/javascript/solid_queue_web/theme_controller.js +26 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +12 -7
- data/app/views/solid_queue_web/dashboard/index.html.erb +66 -6
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +2 -1
- data/app/views/solid_queue_web/history/index.html.erb +73 -0
- data/app/views/solid_queue_web/jobs/index.html.erb +11 -8
- data/app/views/solid_queue_web/processes/index.html.erb +1 -1
- data/app/views/solid_queue_web/queues/index.html.erb +15 -1
- data/app/views/solid_queue_web/search/index.html.erb +2 -2
- data/config/importmap.rb +1 -0
- data/config/routes.rb +15 -12
- data/lib/solid_queue_web/engine.rb +4 -2
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +22 -0
- metadata +31 -1
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
respond_to do |format|
|
|
14
|
+
format.html { @pagy, @jobs = pagy(scope.order(finished_at: :desc)) }
|
|
15
|
+
format.csv do
|
|
16
|
+
send_data history_csv(scope),
|
|
17
|
+
filename: "job-history-#{Date.today}.csv",
|
|
18
|
+
type: "text/csv", disposition: "attachment"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def history_csv(scope)
|
|
26
|
+
CSV.generate(headers: true) do |csv|
|
|
27
|
+
csv << %w[id class_name queue_name duration_seconds finished_at]
|
|
28
|
+
scope.order(finished_at: :desc).each do |job|
|
|
29
|
+
duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
|
|
30
|
+
csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class JobsController < ApplicationController
|
|
3
|
-
before_action :set_status, only: [
|
|
3
|
+
before_action :set_status, only: [:destroy, :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
8
|
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
scope = Job::EXECUTION_MODELS[@status].includes(:job)
|
|
10
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
11
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
12
|
+
scope = scope.order(created_at: :desc)
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { @pagy, @jobs = pagy(scope) }
|
|
16
|
+
format.csv do
|
|
17
|
+
send_data jobs_csv(scope),
|
|
18
|
+
filename: "jobs-#{@status}-#{Date.today}.csv",
|
|
19
|
+
type: "text/csv", disposition: "attachment"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
def show
|
|
@@ -21,33 +30,39 @@ module SolidQueueWeb
|
|
|
21
30
|
|
|
22
31
|
def destroy
|
|
23
32
|
model = execution_model_for!(@status)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
format
|
|
29
|
-
|
|
33
|
+
if params[:id]
|
|
34
|
+
@execution = model.find(params[:id])
|
|
35
|
+
@execution.discard
|
|
36
|
+
@remaining_count = filtered_scope(model).count
|
|
37
|
+
respond_to do |format|
|
|
38
|
+
format.turbo_stream
|
|
39
|
+
format.html { redirect_to jobs_path(status: @status, period: @period), notice: "Job discarded." }
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
jobs = filtered_scope(model).map(&:job)
|
|
43
|
+
model.discard_all_from_jobs(jobs)
|
|
44
|
+
redirect_to jobs_path(status: @status, period: @period),
|
|
45
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded."
|
|
30
46
|
end
|
|
31
47
|
rescue ArgumentError => e
|
|
32
48
|
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
33
49
|
rescue => e
|
|
34
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard job: #{e.message}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def discard_all
|
|
38
|
-
model = execution_model_for!(@status)
|
|
39
|
-
jobs = filtered_scope(model).map(&:job)
|
|
40
|
-
model.discard_all_from_jobs(jobs)
|
|
41
50
|
redirect_to jobs_path(status: @status, period: @period),
|
|
42
|
-
|
|
43
|
-
rescue ArgumentError => e
|
|
44
|
-
redirect_to jobs_path(status: @status, period: @period), alert: e.message
|
|
45
|
-
rescue => e
|
|
46
|
-
redirect_to jobs_path(status: @status, period: @period), alert: "Could not discard jobs: #{e.message}"
|
|
51
|
+
alert: "Could not discard #{params[:id] ? "job" : "jobs"}: #{e.message}"
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
private
|
|
50
55
|
|
|
56
|
+
def jobs_csv(scope)
|
|
57
|
+
CSV.generate(headers: true) do |csv|
|
|
58
|
+
csv << %w[id class_name queue_name status priority enqueued_at]
|
|
59
|
+
scope.each do |execution|
|
|
60
|
+
job = execution.job
|
|
61
|
+
csv << [job.id, job.class_name, job.queue_name, @status, job.priority, job.created_at.iso8601]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
51
66
|
def derive_status(job)
|
|
52
67
|
return "failed" if job.failed_execution.present?
|
|
53
68
|
return "claimed" if job.claimed_execution.present?
|
|
@@ -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,31 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class RetryFailedJobsController < ApplicationController
|
|
3
|
+
before_action :set_filter_params
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
executions = params[:id] ? [SolidQueue::FailedExecution.find(params[:id])] : filtered_scope.to_a
|
|
7
|
+
jobs = executions.map(&:job)
|
|
8
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
9
|
+
redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
|
|
10
|
+
notice: "#{jobs.size} #{"job".pluralize(jobs.size)} queued for retry."
|
|
11
|
+
rescue => e
|
|
12
|
+
redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_filter_params
|
|
18
|
+
@queue = params[:queue].presence
|
|
19
|
+
@search = params[:q].presence
|
|
20
|
+
@period = params[:period].presence_in(PERIOD_DURATIONS.keys)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def filtered_scope
|
|
24
|
+
scope = SolidQueue::FailedExecution.includes(:job)
|
|
25
|
+
scope = scope.references(:job).where(solid_queue_jobs: { queue_name: @queue }) if @queue.present?
|
|
26
|
+
scope = scope.references(:job).where("solid_queue_jobs.class_name LIKE ?", "%#{@search}%") if @search.present?
|
|
27
|
+
scope = scope.references(:job).where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
|
|
28
|
+
scope
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module SolidQueueWeb
|
|
2
2
|
class SearchController < ApplicationController
|
|
3
|
-
LIMIT = 25
|
|
4
|
-
|
|
5
3
|
def index
|
|
6
4
|
@query = params[:q].presence
|
|
7
5
|
@job_classes = SolidQueue::Job.distinct.order(:class_name).pluck(:class_name)
|
|
@@ -15,7 +13,7 @@ module SolidQueueWeb
|
|
|
15
13
|
.where("solid_queue_jobs.class_name LIKE ?", "%#{@query}%")
|
|
16
14
|
.order(created_at: :desc)
|
|
17
15
|
total = scope.count
|
|
18
|
-
executions = scope.limit(
|
|
16
|
+
executions = scope.limit(SolidQueueWeb.search_results_limit).to_a
|
|
19
17
|
@results[status] = { executions: executions, total: total } unless executions.empty?
|
|
20
18
|
end
|
|
21
19
|
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
|
|
@@ -3,8 +3,10 @@ import { Application } from "@hotwired/stimulus"
|
|
|
3
3
|
import SearchController from "solid_queue_web/search_controller"
|
|
4
4
|
import RefreshController from "solid_queue_web/refresh_controller"
|
|
5
5
|
import SelectionController from "solid_queue_web/selection_controller"
|
|
6
|
+
import ThemeController from "solid_queue_web/theme_controller"
|
|
6
7
|
|
|
7
8
|
const application = Application.start()
|
|
8
9
|
application.register("search", SearchController)
|
|
9
10
|
application.register("refresh", RefreshController)
|
|
10
11
|
application.register("selection", SelectionController)
|
|
12
|
+
application.register("theme", ThemeController)
|
|
@@ -23,7 +23,8 @@ export default class extends Controller {
|
|
|
23
23
|
|
|
24
24
|
async _reload() {
|
|
25
25
|
clearTimeout(this._timer)
|
|
26
|
-
|
|
26
|
+
const hasSelection = this.element.querySelector("input[type='checkbox']:checked")
|
|
27
|
+
if (!document.hidden && !hasSelection) {
|
|
27
28
|
try {
|
|
28
29
|
const response = await fetch(window.location.href, {
|
|
29
30
|
headers: { "Turbo-Frame": this.element.id, Accept: "text/html" }
|
|
@@ -44,7 +45,7 @@ export default class extends Controller {
|
|
|
44
45
|
_onVisibilityChange() {
|
|
45
46
|
if (document.hidden) {
|
|
46
47
|
clearTimeout(this._timer)
|
|
47
|
-
} else {
|
|
48
|
+
} else if (!this.element.querySelector("input[type='checkbox']:checked")) {
|
|
48
49
|
this._reload()
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["toggle"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
const saved = localStorage.getItem("sqd-theme")
|
|
8
|
+
const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
9
|
+
this.apply(saved || preferred)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toggle() {
|
|
13
|
+
const current = document.documentElement.getAttribute("data-theme") || "light"
|
|
14
|
+
const next = current === "dark" ? "light" : "dark"
|
|
15
|
+
localStorage.setItem("sqd-theme", next)
|
|
16
|
+
this.apply(next)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply(theme) {
|
|
20
|
+
document.documentElement.setAttribute("data-theme", theme)
|
|
21
|
+
if (this.hasToggleTarget) {
|
|
22
|
+
this.toggleTarget.textContent = theme === "dark" ? "☀" : "☽"
|
|
23
|
+
this.toggleTarget.setAttribute("aria-label", theme === "dark" ? "Switch to light mode" : "Switch to dark mode")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -9,23 +9,18 @@
|
|
|
9
9
|
<%= inline_styles %>
|
|
10
10
|
<%= javascript_importmap_tags "solid_queue_web" %>
|
|
11
11
|
</head>
|
|
12
|
-
<body>
|
|
12
|
+
<body data-controller="theme">
|
|
13
13
|
|
|
14
14
|
<header class="sqd-header">
|
|
15
15
|
<div class="sqd-header__inner">
|
|
16
16
|
<%= link_to "Solid Queue", root_path, class: "sqd-header__title" %>
|
|
17
|
-
<button class="sqd-nav-toggle" aria-label="Toggle navigation" aria-expanded="false"
|
|
18
|
-
onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
|
|
19
|
-
<span></span>
|
|
20
|
-
<span></span>
|
|
21
|
-
<span></span>
|
|
22
|
-
</button>
|
|
23
17
|
<div class="sqd-nav-wrapper">
|
|
24
18
|
<nav aria-label="Main">
|
|
25
19
|
<ul class="sqd-nav">
|
|
26
20
|
<li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
|
|
27
21
|
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
|
|
28
22
|
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
|
|
23
|
+
<li><%= link_to "History", history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
|
|
29
24
|
<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
25
|
<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
26
|
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
|
|
@@ -33,6 +28,16 @@
|
|
|
33
28
|
</ul>
|
|
34
29
|
</nav>
|
|
35
30
|
</div>
|
|
31
|
+
<div class="sqd-header__controls">
|
|
32
|
+
<button class="sqd-theme-toggle" aria-label="Switch to dark mode"
|
|
33
|
+
data-theme-target="toggle" data-action="theme#toggle">☽</button>
|
|
34
|
+
<button class="sqd-nav-toggle" aria-label="Toggle navigation" aria-expanded="false"
|
|
35
|
+
onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
|
|
36
|
+
<span></span>
|
|
37
|
+
<span></span>
|
|
38
|
+
<span></span>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
36
41
|
</div>
|
|
37
42
|
</header>
|
|
38
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.dashboard_refresh_interval } do %>
|
|
2
2
|
<h1 class="sqd-page-title">Dashboard</h1>
|
|
3
3
|
|
|
4
4
|
<div class="sqd-stats">
|
|
@@ -34,9 +34,47 @@
|
|
|
34
34
|
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
35
35
|
<div class="sqd-stat__label">Processes</div>
|
|
36
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 %>
|
|
37
75
|
</div>
|
|
38
76
|
|
|
39
|
-
<div style="display:grid; grid-template-columns:
|
|
77
|
+
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
|
|
40
78
|
<div class="sqd-card">
|
|
41
79
|
<div class="sqd-card__header">
|
|
42
80
|
<span class="sqd-card__title">Quick Links</span>
|
|
@@ -53,13 +91,35 @@
|
|
|
53
91
|
<% if @stats[:failed] > 0 %>
|
|
54
92
|
<div class="sqd-card">
|
|
55
93
|
<div class="sqd-card__header">
|
|
56
|
-
<span class="sqd-card__title">
|
|
94
|
+
<span class="sqd-card__title">Failed Jobs</span>
|
|
57
95
|
</div>
|
|
58
|
-
<div style="padding: 1rem;">
|
|
59
|
-
<p style="color: var(--danger);
|
|
96
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
97
|
+
<p style="color: var(--danger); font-size: 13px;">
|
|
60
98
|
<%= pluralize(@stats[:failed], "failed job") %> need attention.
|
|
61
99
|
</p>
|
|
62
|
-
<%=
|
|
100
|
+
<%= button_to "Retry All Failed", retry_all_failed_path,
|
|
101
|
+
method: :post,
|
|
102
|
+
class: "sqd-btn sqd-btn--primary",
|
|
103
|
+
data: { confirm: "Retry all #{@stats[:failed]} failed #{"job".pluralize(@stats[:failed])}?" } %>
|
|
104
|
+
<%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<% end %>
|
|
108
|
+
|
|
109
|
+
<% if @stats[:blocked] > 0 %>
|
|
110
|
+
<div class="sqd-card">
|
|
111
|
+
<div class="sqd-card__header">
|
|
112
|
+
<span class="sqd-card__title">Blocked Jobs</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
115
|
+
<p style="color: var(--warning); font-size: 13px;">
|
|
116
|
+
<%= pluralize(@stats[:blocked], "blocked job") %>.
|
|
117
|
+
</p>
|
|
118
|
+
<%= button_to "Discard All Blocked", discard_all_blocked_path,
|
|
119
|
+
method: :post,
|
|
120
|
+
class: "sqd-btn sqd-btn--danger",
|
|
121
|
+
data: { confirm: "Discard all #{@stats[:blocked]} blocked #{"job".pluralize(@stats[:blocked])}? This cannot be undone." } %>
|
|
122
|
+
<%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
|
|
63
123
|
</div>
|
|
64
124
|
</div>
|
|
65
125
|
<% end %>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<h1 class="sqd-page-title">Failed Jobs</h1>
|
|
3
3
|
<% if @failed_jobs.any? %>
|
|
4
4
|
<div class="sqd-actions">
|
|
5
|
+
<%= link_to "Export CSV", failed_jobs_path(format: :csv, queue: @queue, q: @search, period: @period),
|
|
6
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
5
7
|
<%= button_to "Retry All", retry_all_failed_jobs_path,
|
|
6
8
|
method: :post,
|
|
7
9
|
params: { queue: @queue, q: @search, period: @period },
|
|
@@ -24,7 +26,6 @@
|
|
|
24
26
|
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
25
27
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
26
28
|
data-action="input->search#filter">
|
|
27
|
-
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
28
29
|
<% if @search.present? %>
|
|
29
30
|
<%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
30
31
|
<% end %>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
2
|
+
<div class="sqd-page-header">
|
|
3
|
+
<h1 class="sqd-page-title">Job History</h1>
|
|
4
|
+
<% if @jobs.any? %>
|
|
5
|
+
<div class="sqd-actions">
|
|
6
|
+
<%= link_to "Export CSV", history_path(format: :csv, queue: @queue, q: @search, period: @period),
|
|
7
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form class="sqd-search" action="<%= history_path %>" method="get" data-controller="search">
|
|
13
|
+
<% if @queue.present? %>
|
|
14
|
+
<input type="hidden" name="queue" value="<%= @queue %>">
|
|
15
|
+
<% end %>
|
|
16
|
+
<input type="hidden" name="period" value="<%= @period %>">
|
|
17
|
+
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
18
|
+
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
19
|
+
data-action="input->search#filter">
|
|
20
|
+
<% if @search.present? %>
|
|
21
|
+
<%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<div class="sqd-period-filter" role="group" aria-label="Time period">
|
|
24
|
+
<%= link_to "All", history_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil } %>
|
|
25
|
+
<%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil } %>
|
|
26
|
+
<%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil } %>
|
|
27
|
+
<%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil } %>
|
|
28
|
+
</div>
|
|
29
|
+
</form>
|
|
30
|
+
|
|
31
|
+
<% if @queue.present? %>
|
|
32
|
+
<p style="margin-top: 0.5rem; font-size: 13px; color: var(--muted);">
|
|
33
|
+
Filtering by queue: <strong><%= @queue %></strong> —
|
|
34
|
+
<%= link_to "Clear filter", history_path(q: @search, period: @period) %>
|
|
35
|
+
</p>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<% if @jobs.any? %>
|
|
39
|
+
<div class="sqd-card">
|
|
40
|
+
<table>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th scope="col">Job Class</th>
|
|
44
|
+
<th scope="col">Queue</th>
|
|
45
|
+
<th scope="col">Duration</th>
|
|
46
|
+
<th scope="col">Finished At</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<% @jobs.each do |job| %>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
53
|
+
<td>
|
|
54
|
+
<%= link_to job.queue_name, history_path(queue: job.queue_name, q: @search, period: @period),
|
|
55
|
+
class: "sqd-mono", style: "color: inherit;" %>
|
|
56
|
+
</td>
|
|
57
|
+
<td class="sqd-mono"><%= format_duration(job.finished_at - job.created_at) %></td>
|
|
58
|
+
<td class="sqd-mono"><%= job.finished_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
59
|
+
</tr>
|
|
60
|
+
<% end %>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
<% else %>
|
|
65
|
+
<div class="sqd-card">
|
|
66
|
+
<div class="sqd-empty">No finished jobs found.</div>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<% if @pagy.last > 1 %>
|
|
71
|
+
<%= @pagy.series_nav.html_safe %>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1 class="sqd-page-title" style="margin-bottom: 1.5rem;">Jobs</h1>
|
|
2
2
|
|
|
3
|
-
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value:
|
|
3
|
+
<%= turbo_frame_tag "jobs-table", data: { turbo_action: "advance", controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
4
4
|
<% discardable = SolidQueueWeb::Job::DISCARDABLE.include?(@status) %>
|
|
5
5
|
|
|
6
6
|
<div class="sqd-page-header">
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
<%= link_to "Blocked", jobs_path(status: "blocked", q: @search, period: @period), class: @status == "blocked" ? "active" : "" %>
|
|
12
12
|
<%= link_to "Failed", jobs_path(status: "failed", q: @search, period: @period), class: @status == "failed" ? "active" : "" %>
|
|
13
13
|
</div>
|
|
14
|
-
<% if
|
|
14
|
+
<% if @jobs.any? %>
|
|
15
15
|
<div class="sqd-actions">
|
|
16
|
-
<%=
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
<%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, period: @period),
|
|
17
|
+
class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
|
|
18
|
+
<% if discardable %>
|
|
19
|
+
<%= button_to "Discard All", discard_all_jobs_path,
|
|
20
|
+
method: :post,
|
|
21
|
+
params: { status: @status, period: @period },
|
|
22
|
+
class: "sqd-btn sqd-btn--danger",
|
|
23
|
+
data: { confirm: "Discard all #{@jobs.size} #{@status} jobs? This cannot be undone." } %>
|
|
24
|
+
<% end %>
|
|
21
25
|
</div>
|
|
22
26
|
<% end %>
|
|
23
27
|
</div>
|
|
@@ -28,7 +32,6 @@
|
|
|
28
32
|
<input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
|
|
29
33
|
placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
|
|
30
34
|
data-action="input->search#filter">
|
|
31
|
-
<button type="submit" class="sqd-btn sqd-btn--muted">Search</button>
|
|
32
35
|
<% if @search.present? %>
|
|
33
36
|
<%= link_to "Clear", jobs_path(status: @status, period: @period), class: "sqd-btn sqd-btn--muted" %>
|
|
34
37
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "processes", target: "_top", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
2
2
|
<h1 class="sqd-page-title">Processes</h1>
|
|
3
3
|
|
|
4
4
|
<div class="sqd-card">
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
<th scope="col">Name</th>
|
|
11
11
|
<th scope="col">Size</th>
|
|
12
12
|
<th scope="col">Latency</th>
|
|
13
|
+
<th scope="col">Done (24h)</th>
|
|
14
|
+
<th scope="col">Failed (24h)</th>
|
|
13
15
|
<th scope="col">Status</th>
|
|
14
16
|
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
15
17
|
</tr>
|
|
@@ -19,7 +21,19 @@
|
|
|
19
21
|
<tr>
|
|
20
22
|
<td class="sqd-mono"><%= queue.name %></td>
|
|
21
23
|
<td><%= queue.size %></td>
|
|
22
|
-
<td
|
|
24
|
+
<td>
|
|
25
|
+
<% if (oldest = @oldest_ready[queue.name]) %>
|
|
26
|
+
<% age = Time.current - oldest %>
|
|
27
|
+
<% latency_color = age > 86_400 ? "var(--danger)" : age > 3_600 ? "var(--warning)" : "inherit" %>
|
|
28
|
+
<abbr title="<%= oldest.strftime("%Y-%m-%d %H:%M:%S UTC") %>">
|
|
29
|
+
<span style="color: <%= latency_color %>"><%= format_duration(age) %></span>
|
|
30
|
+
</abbr>
|
|
31
|
+
<% else %>
|
|
32
|
+
<span style="color: var(--muted)">—</span>
|
|
33
|
+
<% end %>
|
|
34
|
+
</td>
|
|
35
|
+
<td style="color: var(--success);"><%= @completed_24h[queue.name] || 0 %></td>
|
|
36
|
+
<td style="color: <%= (@failed_24h[queue.name] || 0) > 0 ? "var(--danger)" : "inherit" %>;"><%= @failed_24h[queue.name] || 0 %></td>
|
|
23
37
|
<td>
|
|
24
38
|
<% if queue.paused? %>
|
|
25
39
|
<span class="sqd-badge sqd-badge--paused">Paused</span>
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
<span class="sqd-badge sqd-badge--<%= status %>"><%= status %></span>
|
|
28
28
|
<span class="sqd-muted-text">
|
|
29
29
|
<%= pluralize(data[:total], "match", "matches") %>
|
|
30
|
-
<% if data[:total] > SolidQueueWeb
|
|
31
|
-
— showing first <%= SolidQueueWeb
|
|
30
|
+
<% if data[:total] > SolidQueueWeb.search_results_limit %>
|
|
31
|
+
— showing first <%= SolidQueueWeb.search_results_limit %>
|
|
32
32
|
<% end %>
|
|
33
33
|
</span>
|
|
34
34
|
<% if status == "failed" %>
|
data/config/importmap.rb
CHANGED
|
@@ -2,3 +2,4 @@ pin "solid_queue_web", to: "solid_queue_web/application.js"
|
|
|
2
2
|
pin "solid_queue_web/search_controller", to: "solid_queue_web/search_controller.js"
|
|
3
3
|
pin "solid_queue_web/refresh_controller", to: "solid_queue_web/refresh_controller.js"
|
|
4
4
|
pin "solid_queue_web/selection_controller", to: "solid_queue_web/selection_controller.js"
|
|
5
|
+
pin "solid_queue_web/theme_controller", to: "solid_queue_web/theme_controller.js"
|