solid_queue_web 0.7.0 → 0.9.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 +26 -15
- data/app/assets/stylesheets/solid_queue_web/_02_layout.css +29 -1
- data/app/assets/stylesheets/solid_queue_web/_04_table.css +8 -1
- data/app/assets/stylesheets/solid_queue_web/_05_badges.css +1 -0
- data/app/assets/stylesheets/solid_queue_web/_11_throughput.css +30 -1
- data/app/assets/stylesheets/solid_queue_web/_12_dark_mode.css +34 -0
- data/app/controllers/solid_queue_web/application_controller.rb +2 -0
- data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +11 -0
- data/app/controllers/solid_queue_web/dashboard_controller.rb +1 -22
- data/app/controllers/solid_queue_web/failed_jobs_controller.rb +26 -22
- data/app/controllers/solid_queue_web/history_controller.rb +20 -1
- data/app/controllers/solid_queue_web/jobs_controller.rb +38 -23
- data/app/controllers/solid_queue_web/queues/jobs_controller.rb +15 -19
- data/app/controllers/solid_queue_web/queues/pauses_controller.rb +21 -0
- data/app/controllers/solid_queue_web/queues_controller.rb +5 -31
- 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/javascript/solid_queue_web/application.js +2 -0
- data/app/javascript/solid_queue_web/theme_controller.js +26 -0
- data/app/services/solid_queue_web/dashboard_stats.rb +47 -0
- data/app/services/solid_queue_web/queue_stats.rb +52 -0
- data/app/views/layouts/solid_queue_web/application.html.erb +11 -7
- data/app/views/solid_queue_web/dashboard/index.html.erb +89 -23
- data/app/views/solid_queue_web/failed_jobs/index.html.erb +3 -1
- data/app/views/solid_queue_web/history/index.html.erb +8 -2
- data/app/views/solid_queue_web/jobs/index.html.erb +27 -10
- data/app/views/solid_queue_web/processes/index.html.erb +1 -1
- data/app/views/solid_queue_web/queues/index.html.erb +19 -2
- data/app/views/solid_queue_web/queues/jobs/index.html.erb +1 -1
- data/app/views/solid_queue_web/search/index.html.erb +3 -3
- data/config/importmap.rb +1 -0
- data/config/routes.rb +7 -9
- data/lib/solid_queue_web/engine.rb +4 -2
- data/lib/solid_queue_web/version.rb +1 -1
- data/lib/solid_queue_web.rb +27 -0
- metadata +22 -1
|
@@ -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)
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class DashboardStats
|
|
3
|
+
attr_reader :counts, :throughput, :sparkline, :depth_sparkline, :slow_jobs_count
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@now = Time.current
|
|
7
|
+
compute
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def compute
|
|
13
|
+
@counts = {
|
|
14
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
15
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
16
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
17
|
+
failed: SolidQueue::FailedExecution.count,
|
|
18
|
+
blocked: SolidQueue::BlockedExecution.count,
|
|
19
|
+
queues: SolidQueue::Job.select(:queue_name).distinct.count,
|
|
20
|
+
processes: SolidQueue::Process.count,
|
|
21
|
+
recurring: SolidQueue::RecurringTask.count
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..@now).pluck(:finished_at)
|
|
25
|
+
@throughput = {
|
|
26
|
+
completed_1h: finished_times.count { |t| t >= 1.hour.ago },
|
|
27
|
+
completed_24h: finished_times.size
|
|
28
|
+
}
|
|
29
|
+
@sparkline = 12.times.map do |i|
|
|
30
|
+
from = (12 - i).hours.ago
|
|
31
|
+
to = i == 11 ? @now : (11 - i).hours.ago
|
|
32
|
+
finished_times.count { |t| t >= from && t < to }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
threshold = SolidQueueWeb.slow_job_threshold
|
|
36
|
+
@slow_jobs_count = threshold ? SolidQueue::ClaimedExecution.where("created_at <= ?", threshold.ago).count : 0
|
|
37
|
+
|
|
38
|
+
job_timestamps = SolidQueue::Job
|
|
39
|
+
.where("created_at >= ? OR finished_at IS NULL", 72.hours.ago)
|
|
40
|
+
.pluck(:created_at, :finished_at)
|
|
41
|
+
@depth_sparkline = 12.times.map do |i|
|
|
42
|
+
t = i == 11 ? @now : (12 - i).hours.ago
|
|
43
|
+
job_timestamps.count { |created, finished| created <= t && (finished.nil? || finished > t) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module SolidQueueWeb
|
|
2
|
+
class QueueStats
|
|
3
|
+
attr_reader :completed_24h, :failed_24h, :oldest_ready, :failure_sparklines
|
|
4
|
+
|
|
5
|
+
def initialize(queues)
|
|
6
|
+
@queues = queues
|
|
7
|
+
@now = Time.current
|
|
8
|
+
compute
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def compute
|
|
14
|
+
@completed_24h = SolidQueue::Job
|
|
15
|
+
.where(finished_at: 24.hours.ago..@now)
|
|
16
|
+
.group(:queue_name)
|
|
17
|
+
.count
|
|
18
|
+
|
|
19
|
+
@failed_24h = SolidQueue::FailedExecution
|
|
20
|
+
.joins(:job)
|
|
21
|
+
.where(created_at: 24.hours.ago..@now)
|
|
22
|
+
.group("solid_queue_jobs.queue_name")
|
|
23
|
+
.count
|
|
24
|
+
|
|
25
|
+
@oldest_ready = SolidQueue::ReadyExecution
|
|
26
|
+
.joins(:job)
|
|
27
|
+
.group("solid_queue_jobs.queue_name")
|
|
28
|
+
.minimum("solid_queue_jobs.created_at")
|
|
29
|
+
|
|
30
|
+
failed_raw = SolidQueue::FailedExecution
|
|
31
|
+
.joins(:job)
|
|
32
|
+
.where(created_at: 12.hours.ago..@now)
|
|
33
|
+
.pluck("solid_queue_jobs.queue_name", "solid_queue_failed_executions.created_at")
|
|
34
|
+
done_raw = SolidQueue::Job
|
|
35
|
+
.where(finished_at: 12.hours.ago..@now)
|
|
36
|
+
.pluck(:queue_name, :finished_at)
|
|
37
|
+
|
|
38
|
+
@failure_sparklines = @queues.each_with_object({}) do |queue, h|
|
|
39
|
+
failed_times = failed_raw.filter_map { |q, t| t if q == queue.name }
|
|
40
|
+
done_times = done_raw.filter_map { |q, t| t if q == queue.name }
|
|
41
|
+
h[queue.name] = 12.times.map do |i|
|
|
42
|
+
from = (12 - i).hours.ago
|
|
43
|
+
to = i == 11 ? @now : (11 - i).hours.ago
|
|
44
|
+
f = failed_times.count { |t| t >= from && t < to }
|
|
45
|
+
d = done_times.count { |t| t >= from && t < to }
|
|
46
|
+
total = f + d
|
|
47
|
+
total > 0 ? (f.to_f / total * 100).round : nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -9,17 +9,11 @@
|
|
|
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">
|
|
@@ -34,6 +28,16 @@
|
|
|
34
28
|
</ul>
|
|
35
29
|
</nav>
|
|
36
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>
|
|
37
41
|
</div>
|
|
38
42
|
</header>
|
|
39
43
|
|
|
@@ -1,63 +1,63 @@
|
|
|
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">
|
|
5
5
|
<%= link_to jobs_path(status: "ready"), class: "sqd-stat sqd-stat--ready sqd-stat--link" do %>
|
|
6
|
-
<div class="sqd-stat__value"><%= @stats[:ready] %></div>
|
|
6
|
+
<div class="sqd-stat__value"><%= @stats.counts[:ready] %></div>
|
|
7
7
|
<div class="sqd-stat__label">Ready</div>
|
|
8
8
|
<% end %>
|
|
9
9
|
<%= link_to jobs_path(status: "scheduled"), class: "sqd-stat sqd-stat--scheduled sqd-stat--link" do %>
|
|
10
|
-
<div class="sqd-stat__value"><%= @stats[:scheduled] %></div>
|
|
10
|
+
<div class="sqd-stat__value"><%= @stats.counts[:scheduled] %></div>
|
|
11
11
|
<div class="sqd-stat__label">Scheduled</div>
|
|
12
12
|
<% end %>
|
|
13
13
|
<%= link_to jobs_path(status: "claimed"), class: "sqd-stat sqd-stat--claimed sqd-stat--link" do %>
|
|
14
|
-
<div class="sqd-stat__value"><%= @stats[:claimed] %></div>
|
|
14
|
+
<div class="sqd-stat__value"><%= @stats.counts[:claimed] %></div>
|
|
15
15
|
<div class="sqd-stat__label">Running</div>
|
|
16
16
|
<% end %>
|
|
17
17
|
<%= link_to jobs_path(status: "blocked"), class: "sqd-stat sqd-stat--blocked sqd-stat--link" do %>
|
|
18
|
-
<div class="sqd-stat__value"><%= @stats[:blocked] %></div>
|
|
18
|
+
<div class="sqd-stat__value"><%= @stats.counts[:blocked] %></div>
|
|
19
19
|
<div class="sqd-stat__label">Blocked</div>
|
|
20
20
|
<% end %>
|
|
21
21
|
<%= link_to failed_jobs_path, class: "sqd-stat sqd-stat--failed sqd-stat--link" do %>
|
|
22
|
-
<div class="sqd-stat__value"><%= @stats[:failed] %></div>
|
|
22
|
+
<div class="sqd-stat__value"><%= @stats.counts[:failed] %></div>
|
|
23
23
|
<div class="sqd-stat__label">Failed</div>
|
|
24
24
|
<% end %>
|
|
25
25
|
<%= link_to queues_path, class: "sqd-stat sqd-stat--queues sqd-stat--link" do %>
|
|
26
|
-
<div class="sqd-stat__value"><%= @stats[:queues] %></div>
|
|
26
|
+
<div class="sqd-stat__value"><%= @stats.counts[:queues] %></div>
|
|
27
27
|
<div class="sqd-stat__label">Queues</div>
|
|
28
28
|
<% end %>
|
|
29
29
|
<%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %>
|
|
30
|
-
<div class="sqd-stat__value"><%= @stats[:recurring] %></div>
|
|
30
|
+
<div class="sqd-stat__value"><%= @stats.counts[:recurring] %></div>
|
|
31
31
|
<div class="sqd-stat__label">Recurring</div>
|
|
32
32
|
<% end %>
|
|
33
33
|
<%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
|
|
34
|
-
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
|
|
34
|
+
<div class="sqd-stat__value"><%= @stats.counts[:processes] %></div>
|
|
35
35
|
<div class="sqd-stat__label">Processes</div>
|
|
36
36
|
<% end %>
|
|
37
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>
|
|
38
|
+
<div class="sqd-stat__value"><%= @stats.throughput[:completed_1h] %></div>
|
|
39
39
|
<div class="sqd-stat__label">Done (1h)</div>
|
|
40
40
|
<% end %>
|
|
41
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>
|
|
42
|
+
<div class="sqd-stat__value"><%= @stats.throughput[:completed_24h] %></div>
|
|
43
43
|
<div class="sqd-stat__label">Done (24h)</div>
|
|
44
44
|
<% end %>
|
|
45
45
|
</div>
|
|
46
46
|
|
|
47
|
-
<% max_val = [@sparkline.max, 1].max %>
|
|
47
|
+
<% max_val = [@stats.sparkline.max, 1].max %>
|
|
48
48
|
<div class="sqd-card" style="margin-bottom: 1rem;">
|
|
49
49
|
<div class="sqd-card__header">
|
|
50
50
|
<span class="sqd-card__title">Throughput — Last 12 Hours</span>
|
|
51
51
|
<div class="sqd-throughput__summary">
|
|
52
|
-
<span>1h: <strong><%= @throughput[:completed_1h] %></strong></span>
|
|
53
|
-
<span>24h: <strong><%= @throughput[:completed_24h] %></strong></span>
|
|
52
|
+
<span>1h: <strong><%= @stats.throughput[:completed_1h] %></strong></span>
|
|
53
|
+
<span>24h: <strong><%= @stats.throughput[:completed_24h] %></strong></span>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
|
-
<% if @throughput[:completed_24h] == 0 %>
|
|
56
|
+
<% if @stats.throughput[:completed_24h] == 0 %>
|
|
57
57
|
<div class="sqd-sparkline__empty">No completed jobs in the last 24 hours</div>
|
|
58
58
|
<% else %>
|
|
59
59
|
<div class="sqd-sparkline" aria-label="Jobs completed per hour over the last 12 hours">
|
|
60
|
-
<% @sparkline.each_with_index do |count, i| %>
|
|
60
|
+
<% @stats.sparkline.each_with_index do |count, i| %>
|
|
61
61
|
<% pct = (count.to_f / max_val * 100).round %>
|
|
62
62
|
<% hour_start = (12 - i).hours.ago %>
|
|
63
63
|
<% show_tick = [0, 3, 6, 9, 11].include?(i) %>
|
|
@@ -74,7 +74,37 @@
|
|
|
74
74
|
<% end %>
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
<% current_depth = @stats.counts[:ready] + @stats.counts[:scheduled] + @stats.counts[:claimed] + @stats.counts[:blocked] + @stats.counts[:failed] %>
|
|
78
|
+
<% max_depth = [@stats.depth_sparkline.max, 1].max %>
|
|
79
|
+
<div class="sqd-card" style="margin-bottom: 1rem;">
|
|
80
|
+
<div class="sqd-card__header">
|
|
81
|
+
<span class="sqd-card__title">Queue Depth — Last 12 Hours</span>
|
|
82
|
+
<div class="sqd-throughput__summary">
|
|
83
|
+
<span>Now: <strong><%= current_depth %></strong></span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<% if @stats.depth_sparkline.all?(&:zero?) %>
|
|
87
|
+
<div class="sqd-sparkline__empty">No active jobs in the last 12 hours</div>
|
|
88
|
+
<% else %>
|
|
89
|
+
<div class="sqd-sparkline" aria-label="Queue depth over the last 12 hours">
|
|
90
|
+
<% @stats.depth_sparkline.each_with_index do |depth, i| %>
|
|
91
|
+
<% pct = (depth.to_f / max_depth * 100).round %>
|
|
92
|
+
<% t = i == 11 ? Time.current : (12 - i).hours.ago %>
|
|
93
|
+
<% show_tick = [0, 3, 6, 9, 11].include?(i) %>
|
|
94
|
+
<div class="sqd-sparkline__col">
|
|
95
|
+
<div class="sqd-sparkline__bar-wrap">
|
|
96
|
+
<div class="sqd-sparkline__bar sqd-sparkline__bar--depth"
|
|
97
|
+
style="height: <%= [pct, 3].max %>%"
|
|
98
|
+
title="<%= i == 11 ? "now" : t.strftime("%-I%p").downcase %>: <%= depth %> <%= "job".pluralize(depth) %> in queue"></div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : t.strftime("%-I%p").downcase) : "" %></div>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
</div>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
|
|
78
108
|
<div class="sqd-card">
|
|
79
109
|
<div class="sqd-card__header">
|
|
80
110
|
<span class="sqd-card__title">Quick Links</span>
|
|
@@ -88,16 +118,52 @@
|
|
|
88
118
|
</div>
|
|
89
119
|
</div>
|
|
90
120
|
|
|
91
|
-
<% if @stats[:failed] > 0 %>
|
|
121
|
+
<% if @stats.counts[:failed] > 0 %>
|
|
122
|
+
<div class="sqd-card">
|
|
123
|
+
<div class="sqd-card__header">
|
|
124
|
+
<span class="sqd-card__title">Failed Jobs</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
127
|
+
<p style="color: var(--danger); font-size: 13px;">
|
|
128
|
+
<%= pluralize(@stats.counts[:failed], "failed job") %> need attention.
|
|
129
|
+
</p>
|
|
130
|
+
<%= button_to "Retry All Failed", retry_all_failed_jobs_path,
|
|
131
|
+
method: :post,
|
|
132
|
+
class: "sqd-btn sqd-btn--primary",
|
|
133
|
+
data: { confirm: "Retry all #{@stats.counts[:failed]} failed #{"job".pluralize(@stats.counts[:failed])}?" } %>
|
|
134
|
+
<%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<% end %>
|
|
138
|
+
|
|
139
|
+
<% if SolidQueueWeb.slow_job_threshold && @stats.slow_jobs_count > 0 %>
|
|
140
|
+
<div class="sqd-card">
|
|
141
|
+
<div class="sqd-card__header">
|
|
142
|
+
<span class="sqd-card__title">Slow Jobs</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
145
|
+
<p style="color: var(--warning); font-size: 13px;">
|
|
146
|
+
<%= pluralize(@stats.slow_jobs_count, "job") %> running longer than <%= distance_of_time_in_words(SolidQueueWeb.slow_job_threshold) %>.
|
|
147
|
+
</p>
|
|
148
|
+
<%= link_to "Review →", jobs_path(status: "claimed"), class: "sqd-btn sqd-btn--muted" %>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<% end %>
|
|
152
|
+
|
|
153
|
+
<% if @stats.counts[:blocked] > 0 %>
|
|
92
154
|
<div class="sqd-card">
|
|
93
155
|
<div class="sqd-card__header">
|
|
94
|
-
<span class="sqd-card__title">
|
|
156
|
+
<span class="sqd-card__title">Blocked Jobs</span>
|
|
95
157
|
</div>
|
|
96
|
-
<div style="padding: 1rem;">
|
|
97
|
-
<p style="color: var(--
|
|
98
|
-
<%= pluralize(@stats[:
|
|
158
|
+
<div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
|
159
|
+
<p style="color: var(--warning); font-size: 13px;">
|
|
160
|
+
<%= pluralize(@stats.counts[:blocked], "blocked job") %>.
|
|
99
161
|
</p>
|
|
100
|
-
<%=
|
|
162
|
+
<%= button_to "Discard All Blocked", blocked_jobs_path,
|
|
163
|
+
method: :delete,
|
|
164
|
+
class: "sqd-btn sqd-btn--danger",
|
|
165
|
+
data: { confirm: "Discard all #{@stats.counts[:blocked]} blocked #{"job".pluralize(@stats.counts[:blocked])}? This cannot be undone." } %>
|
|
166
|
+
<%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
|
|
101
167
|
</div>
|
|
102
168
|
</div>
|
|
103
169
|
<% 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 },
|
|
@@ -90,7 +92,7 @@
|
|
|
90
92
|
data-action="change->selection#toggle"
|
|
91
93
|
aria-label="Select job <%= job.class_name %>">
|
|
92
94
|
</td>
|
|
93
|
-
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
95
|
+
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
|
|
94
96
|
<td>
|
|
95
97
|
<%= link_to job.queue_name, failed_jobs_path(queue: job.queue_name, q: @search, period: @period),
|
|
96
98
|
class: "sqd-mono", style: "color: inherit;" %>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value:
|
|
1
|
+
<%= turbo_frame_tag "history-table", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.default_refresh_interval } do %>
|
|
2
2
|
<div class="sqd-page-header">
|
|
3
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 %>
|
|
4
10
|
</div>
|
|
5
11
|
|
|
6
12
|
<form class="sqd-search" action="<%= history_path %>" method="get" data-controller="search">
|
|
@@ -43,7 +49,7 @@
|
|
|
43
49
|
<tbody>
|
|
44
50
|
<% @jobs.each do |job| %>
|
|
45
51
|
<tr>
|
|
46
|
-
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
52
|
+
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
|
|
47
53
|
<td>
|
|
48
54
|
<%= link_to job.queue_name, history_path(queue: job.queue_name, q: @search, period: @period),
|
|
49
55
|
class: "sqd-mono", style: "color: inherit;" %>
|
|
@@ -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>
|
|
@@ -83,7 +87,7 @@
|
|
|
83
87
|
</td>
|
|
84
88
|
<td>
|
|
85
89
|
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
86
|
-
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
90
|
+
<%= link_to job.class_name, job_path(job), class: "sqd-table-link", style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
87
91
|
</td>
|
|
88
92
|
<td>
|
|
89
93
|
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
|
|
@@ -112,6 +116,7 @@
|
|
|
112
116
|
<% if @jobs.empty? %>
|
|
113
117
|
<div class="sqd-empty">No <%= @status %> jobs.</div>
|
|
114
118
|
<% else %>
|
|
119
|
+
<% slow_threshold = @status == "claimed" ? SolidQueueWeb.slow_job_threshold : nil %>
|
|
115
120
|
<table>
|
|
116
121
|
<thead>
|
|
117
122
|
<tr>
|
|
@@ -120,15 +125,22 @@
|
|
|
120
125
|
<th scope="col">Priority</th>
|
|
121
126
|
<th scope="col">Scheduled At</th>
|
|
122
127
|
<th scope="col">Enqueued At</th>
|
|
128
|
+
<% if @status == "claimed" %>
|
|
129
|
+
<th scope="col">Running For</th>
|
|
130
|
+
<% end %>
|
|
123
131
|
</tr>
|
|
124
132
|
</thead>
|
|
125
133
|
<tbody>
|
|
126
134
|
<% @jobs.each do |execution| %>
|
|
127
135
|
<% job = execution.job %>
|
|
128
|
-
|
|
136
|
+
<% slow = slow_threshold && execution.created_at <= slow_threshold.ago %>
|
|
137
|
+
<tr id="execution_<%= execution.id %>"<%= slow ? ' class="sqd-row--slow"'.html_safe : "" %>>
|
|
129
138
|
<td>
|
|
130
139
|
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
131
|
-
|
|
140
|
+
<% if slow %>
|
|
141
|
+
<span class="sqd-badge sqd-badge--slow">slow</span>
|
|
142
|
+
<% end %>
|
|
143
|
+
<%= link_to job.class_name, job_path(job), class: "sqd-table-link", style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
132
144
|
</td>
|
|
133
145
|
<td>
|
|
134
146
|
<%= link_to job.queue_name, queue_jobs_path(queue_name: job.queue_name, status: @status),
|
|
@@ -139,6 +151,11 @@
|
|
|
139
151
|
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
|
|
140
152
|
</td>
|
|
141
153
|
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
154
|
+
<% if @status == "claimed" %>
|
|
155
|
+
<td class="sqd-mono<%= slow ? " sqd-slow-duration" : "" %>">
|
|
156
|
+
<%= time_ago_in_words(execution.created_at) %>
|
|
157
|
+
</td>
|
|
158
|
+
<% end %>
|
|
142
159
|
</tr>
|
|
143
160
|
<% end %>
|
|
144
161
|
</tbody>
|
|
@@ -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">
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<th scope="col">Latency</th>
|
|
13
13
|
<th scope="col">Done (24h)</th>
|
|
14
14
|
<th scope="col">Failed (24h)</th>
|
|
15
|
+
<th scope="col">Failure Rate (12h)</th>
|
|
15
16
|
<th scope="col">Status</th>
|
|
16
17
|
<th scope="col"><span class="sqd-sr-only">Actions</span></th>
|
|
17
18
|
</tr>
|
|
@@ -34,6 +35,22 @@
|
|
|
34
35
|
</td>
|
|
35
36
|
<td style="color: var(--success);"><%= @completed_24h[queue.name] || 0 %></td>
|
|
36
37
|
<td style="color: <%= (@failed_24h[queue.name] || 0) > 0 ? "var(--danger)" : "inherit" %>;"><%= @failed_24h[queue.name] || 0 %></td>
|
|
38
|
+
<td>
|
|
39
|
+
<% sparkline = @failure_sparklines[queue.name] %>
|
|
40
|
+
<% if sparkline.any? %>
|
|
41
|
+
<div class="sqd-mini-sparkline" aria-label="Failure rate last 12 hours for <%= queue.name %>">
|
|
42
|
+
<% sparkline.each_with_index do |rate, i| %>
|
|
43
|
+
<% pct = rate || 0 %>
|
|
44
|
+
<% hour_label = (12 - i).hours.ago.strftime("%-I%p").downcase %>
|
|
45
|
+
<div class="sqd-mini-sparkline__bar sqd-mini-sparkline__bar--<%= rate ? "data" : "empty" %>"
|
|
46
|
+
style="height: <%= [pct, 2].max %>%"
|
|
47
|
+
title="<%= hour_label %>: <%= rate ? "#{rate}% failure rate" : "no data" %>"></div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% else %>
|
|
51
|
+
<span style="color: var(--muted)">—</span>
|
|
52
|
+
<% end %>
|
|
53
|
+
</td>
|
|
37
54
|
<td>
|
|
38
55
|
<% if queue.paused? %>
|
|
39
56
|
<span class="sqd-badge sqd-badge--paused">Paused</span>
|
|
@@ -43,10 +60,10 @@
|
|
|
43
60
|
</td>
|
|
44
61
|
<td class="sqd-row-actions">
|
|
45
62
|
<% if queue.paused? %>
|
|
46
|
-
<%= button_to "Resume",
|
|
63
|
+
<%= button_to "Resume", queue_pause_path(queue.name), method: :delete,
|
|
47
64
|
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
|
|
48
65
|
<% else %>
|
|
49
|
-
<%= button_to "Pause",
|
|
66
|
+
<%= button_to "Pause", queue_pause_path(queue.name), method: :post,
|
|
50
67
|
class: "sqd-btn sqd-btn--muted sqd-btn--sm",
|
|
51
68
|
data: { confirm: "Pause queue \"#{queue.name}\"?" } %>
|
|
52
69
|
<% end %>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<tr id="execution_<%= execution.id %>">
|
|
61
61
|
<td>
|
|
62
62
|
<span class="sqd-badge sqd-badge--<%= @status %>"><%= @status %></span>
|
|
63
|
-
<%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
63
|
+
<%= link_to job.class_name, job_path(job), class: "sqd-table-link", style: "margin-left: 0.5rem;", data: { turbo_frame: "_top" } %>
|
|
64
64
|
</td>
|
|
65
65
|
<td><%= job.priority %></td>
|
|
66
66
|
<td class="sqd-mono">
|
|
@@ -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" %>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<% data[:executions].each do |execution| %>
|
|
51
51
|
<% job = execution.job %>
|
|
52
52
|
<tr>
|
|
53
|
-
<td><%= link_to job.class_name, job_path(job) %></td>
|
|
53
|
+
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
|
|
54
54
|
<td class="sqd-mono"><%= job.queue_name %></td>
|
|
55
55
|
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
56
56
|
</tr>
|
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"
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
SolidQueueWeb::Engine.routes.draw do
|
|
2
2
|
root to: "dashboard#index"
|
|
3
|
+
resource :blocked_jobs, only: [:destroy]
|
|
3
4
|
|
|
4
5
|
get "search", to: "search#index", as: :search
|
|
5
6
|
get "history", to: "history#index", as: :history
|
|
@@ -7,13 +8,10 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
7
8
|
resources :recurring_tasks, only: [:index]
|
|
8
9
|
resources :processes, only: [:index]
|
|
9
10
|
resources :queues, only: [:index], param: :name do
|
|
10
|
-
|
|
11
|
-
post :pause
|
|
12
|
-
post :resume
|
|
13
|
-
end
|
|
11
|
+
resource :pause, only: [:create, :destroy], controller: "queues/pauses"
|
|
14
12
|
resources :jobs, path: "list", only: [:index, :destroy], controller: "queues/jobs" do
|
|
15
13
|
collection do
|
|
16
|
-
post :discard_all
|
|
14
|
+
post :discard_all, action: :destroy
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
17
|
end
|
|
@@ -23,7 +21,7 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
23
21
|
resource :job_selection, path: "list/selection", only: [:destroy], controller: "jobs/selections"
|
|
24
22
|
resources :jobs, path: "list", only: [:index, :show, :destroy] do
|
|
25
23
|
collection do
|
|
26
|
-
post :discard_all
|
|
24
|
+
post :discard_all, action: :destroy
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
|
|
@@ -31,11 +29,11 @@ SolidQueueWeb::Engine.routes.draw do
|
|
|
31
29
|
controller: "failed_jobs/selections"
|
|
32
30
|
resources :failed_jobs, only: [:index, :destroy] do
|
|
33
31
|
collection do
|
|
34
|
-
post :retry_all
|
|
35
|
-
post :discard_all
|
|
32
|
+
post :retry_all, to: "retry_failed_jobs#create"
|
|
33
|
+
post :discard_all, action: :destroy
|
|
36
34
|
end
|
|
37
35
|
member do
|
|
38
|
-
post :retry
|
|
36
|
+
post :retry, to: "retry_failed_jobs#create"
|
|
39
37
|
end
|
|
40
38
|
end
|
|
41
39
|
end
|
|
@@ -22,8 +22,10 @@ module SolidQueueWeb
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
initializer "solid_queue_web.pagy" do
|
|
26
|
-
|
|
25
|
+
initializer "solid_queue_web.pagy" do |app|
|
|
26
|
+
app.config.after_initialize do
|
|
27
|
+
Pagy::OPTIONS[:limit] = SolidQueueWeb.page_size
|
|
28
|
+
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|