roundhouse_ui 0.9.1 → 0.11.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 +620 -46
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +246 -12
- data/app/controllers/roundhouse_ui/application_controller.rb +74 -0
- data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
- data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
- data/app/controllers/roundhouse_ui/dashboard_controller.rb +27 -3
- data/app/controllers/roundhouse_ui/dead_controller.rb +27 -10
- data/app/controllers/roundhouse_ui/errors_controller.rb +60 -1
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +57 -6
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +34 -10
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +4 -7
- data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
- data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
- data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
- data/app/helpers/roundhouse_ui/application_helper.rb +293 -0
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +121 -12
- data/app/helpers/roundhouse_ui/tags_helper.rb +128 -0
- data/app/views/layouts/roundhouse_ui/application.html.erb +1277 -84
- data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
- data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
- data/app/views/roundhouse_ui/dead/index.html.erb +54 -32
- data/app/views/roundhouse_ui/errors/index.html.erb +51 -11
- data/app/views/roundhouse_ui/jobs/show.html.erb +8 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +54 -10
- data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
- data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
- data/app/views/roundhouse_ui/retries/index.html.erb +30 -17
- data/app/views/roundhouse_ui/scheduled/index.html.erb +17 -11
- data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
- data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
- data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
- data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
- data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
- data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +28 -0
- data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
- data/config/routes.rb +9 -0
- data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
- data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
- data/lib/roundhouse_ui/demo.rb +130 -0
- data/lib/roundhouse_ui/error_groups.rb +25 -4
- data/lib/roundhouse_ui/filter_query.rb +504 -0
- data/lib/roundhouse_ui/health.rb +43 -4
- data/lib/roundhouse_ui/history.rb +54 -0
- data/lib/roundhouse_ui/icons.rb +104 -0
- data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
- data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
- data/lib/roundhouse_ui/observability.rb +55 -7
- data/lib/roundhouse_ui/pause.rb +6 -11
- data/lib/roundhouse_ui/queue_summary.rb +11 -0
- data/lib/roundhouse_ui/recurring.rb +137 -0
- data/lib/roundhouse_ui/runbooks.rb +75 -0
- data/lib/roundhouse_ui/snapshots.rb +13 -2
- data/lib/roundhouse_ui/tags.rb +112 -0
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +235 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +32 -7
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
<% content_for :title, "Busy" %>
|
|
2
2
|
|
|
3
|
+
<%# Cancellation is inert unless the host installed the middleware or polls
|
|
4
|
+
cancelled? itself, so the column only exists where the flag says it works. %>
|
|
5
|
+
<% cancellable = RoundhouseUi.cancel_enabled && RoundhouseUi.backend.supports?(:cancel) %>
|
|
3
6
|
<h2 class="rh-h2">Busy <span class="hint"><%= @work.size %> <%= "job".pluralize(@work.size) %> running now</span></h2>
|
|
4
7
|
<table class="rh-table">
|
|
5
|
-
<thead><tr><th>Job</th><th>Queue</th><th>Worker</th><th class="r">
|
|
8
|
+
<thead><tr><th>Job</th><th>Queue</th><th>Worker</th><th>Running for</th><th class="r">vs typical</th><% if cancellable %><th class="r">Actions</th><% end %></tr></thead>
|
|
6
9
|
<tbody>
|
|
7
10
|
<% if @work.empty? %>
|
|
8
|
-
<tr><td colspan="5" class="rh-empty">No jobs running right now.</td></tr>
|
|
11
|
+
<tr><td colspan="<%= cancellable ? 6 : 5 %>" class="rh-empty">No jobs running right now.</td></tr>
|
|
9
12
|
<% else %>
|
|
10
13
|
<% @work.each do |w| %>
|
|
11
14
|
<% elapsed = (Time.now - w[:run_at]).to_i %>
|
|
12
15
|
<% long = elapsed >= @threshold %>
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
<%# Elapsed against what is normal for THIS class, where we know it.
|
|
17
|
+
A ratio answers "is this wrong" where a duration only answers "how
|
|
18
|
+
long", and those are different questions on a page about hangs. %>
|
|
19
|
+
<% typical = @typical[RoundhouseUi.unwrapped_class(w[:job].klass, w[:job].item)] %>
|
|
20
|
+
<% ratio = typical.to_f.positive? ? elapsed / typical : nil %>
|
|
21
|
+
<% sev = if ratio.nil? then (long ? "warn" : "ok")
|
|
22
|
+
elsif ratio > 2.5 then "crit"
|
|
23
|
+
elsif ratio > 1 then "warn"
|
|
24
|
+
else "ok" end %>
|
|
25
|
+
<tr data-rh-sev="<%= sev %>">
|
|
26
|
+
<td><%= job_display_class(w[:job]) %><br><span class="rh-sub"><%= w[:job].jid %></span></td>
|
|
15
27
|
<td><span class="rh-sub"><%= w[:queue] %></span></td>
|
|
16
28
|
<td><span class="rh-sub"><%= w[:process] %></span></td>
|
|
17
|
-
<td class="
|
|
18
|
-
<td class="r"
|
|
29
|
+
<td class="rh-mono"><%= duration(elapsed) %></td>
|
|
30
|
+
<td class="r">
|
|
31
|
+
<div class="rh-elapsed">
|
|
32
|
+
<div class="t" data-rh-sev="<%= sev %>">
|
|
33
|
+
<i style="width:<%= ratio ? [ ratio * 100, 100 ].min.round(1) : [ elapsed.to_f / @threshold * 100, 100 ].min.round(1) %>%"></i>
|
|
34
|
+
</div>
|
|
35
|
+
<span class="n rh-sub">
|
|
36
|
+
<%= if ratio then "×#{ratio.round(1)}"
|
|
37
|
+
elsif RoundhouseUi.collect_durations then "no baseline"
|
|
38
|
+
else "—" end %>
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
</td>
|
|
42
|
+
<% if cancellable %><td class="r"><%= button_to "Cancel", cancel_job_path(w[:job].jid), method: :post, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Request cancellation of #{w[:job].jid}? Cooperative jobs abort; others are skipped on their next attempt." } %></td><% end %>
|
|
19
43
|
</tr>
|
|
20
44
|
<% end %>
|
|
21
45
|
<% end %>
|
|
@@ -2,44 +2,55 @@
|
|
|
2
2
|
<% content_for :crumb, Rails.env %>
|
|
3
3
|
|
|
4
4
|
<% verdict = { ok: "Healthy", warn: "Degraded", crit: "Critical" }[@health.status] %>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<%# Verdict, then one tile per check. Failing checks lead — same worst-first
|
|
6
|
+
rule the Queues page uses, because the order is information. No disclosure:
|
|
7
|
+
three rows do not need collapsing, and hiding the signals hides the answer. %>
|
|
8
|
+
<% ranked = @health.ranked_signals %>
|
|
9
|
+
<% failing = @health.failing_count %>
|
|
10
|
+
<section class="rh-health is-<%= @health.status %>" aria-label="System health">
|
|
11
|
+
<div class="rh-health-top">
|
|
8
12
|
<span class="rh-health-verdict"><%= verdict %></span>
|
|
9
13
|
<span class="rh-health-reason"><%= @health.reason %></span>
|
|
10
|
-
<span class="rh-health-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<% @health.signals.each do |s| %>
|
|
14
|
-
<div class="rh-sig">
|
|
15
|
-
<span class="rh-st rh-st-<%= s.status %>"><%= s.status %></span>
|
|
16
|
-
<b><%= s.label %></b>
|
|
17
|
-
<span class="rh-sub"><%= s.detail %></span>
|
|
18
|
-
</div>
|
|
19
|
-
<% end %>
|
|
14
|
+
<span class="rh-health-meta">
|
|
15
|
+
<%= pluralize(ranked.size, "check") %><%= " · #{failing} failing" if failing.positive? %>
|
|
16
|
+
</span>
|
|
20
17
|
</div>
|
|
21
|
-
|
|
18
|
+
<% if ranked.any? %>
|
|
19
|
+
<div class="rh-health-signals">
|
|
20
|
+
<% ranked.each do |s| %>
|
|
21
|
+
<div class="rh-sig" data-rh-sev="<%= s.status %>">
|
|
22
|
+
<span class="rh-sig-name"><%= icon(s.status == :ok ? :ok : :warn) %><%= s.label %></span>
|
|
23
|
+
<span class="rh-sig-detail"><%= s.detail %></span>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</section>
|
|
22
29
|
|
|
23
30
|
<div class="rh-cards">
|
|
24
31
|
<div class="rh-card">
|
|
25
32
|
<div class="k">Processed</div>
|
|
26
33
|
<div class="v num" data-stat="processed"><%= number_with_delimiter @stats.processed %></div>
|
|
27
34
|
<div class="d"><span class="num" data-stat="rate">—</span> / min · <%= ((1 - @metrics.failure_ratio) * 100).round(1) %>% ok</div>
|
|
35
|
+
<canvas data-rh-card="processed" width="300" height="52" aria-hidden="true" title="Processed per interval — the running total only goes up"></canvas>
|
|
28
36
|
</div>
|
|
29
37
|
<div class="rh-card">
|
|
30
38
|
<div class="k">Failed · total</div>
|
|
31
39
|
<div class="v num" data-stat="failed"><%= number_with_delimiter @stats.failed %></div>
|
|
32
40
|
<div class="d bad"><span class="num" data-stat="dead"><%= number_with_delimiter @stats.dead_size %></span> dead · <%= (@metrics.failure_ratio * 100).round(1) %>% rate</div>
|
|
41
|
+
<canvas data-rh-card="failed" width="300" height="52" aria-hidden="true" title="Failures per interval — the running total only goes up"></canvas>
|
|
33
42
|
</div>
|
|
34
43
|
<div class="rh-card">
|
|
35
44
|
<div class="k">Busy threads</div>
|
|
36
45
|
<div class="v num" data-stat="busy"><%= @stats.workers_size %></div>
|
|
37
46
|
<div class="d"><%= @metrics.utilization ? "#{(@metrics.utilization * 100).round}% of #{@metrics.concurrency} busy" : "no workers reporting" %></div>
|
|
47
|
+
<canvas data-rh-card="busy" width="300" height="52" aria-hidden="true"></canvas>
|
|
38
48
|
</div>
|
|
39
49
|
<div class="rh-card">
|
|
40
50
|
<div class="k">Backlog</div>
|
|
41
51
|
<div class="v num"><%= number_with_delimiter @metrics.backlog %></div>
|
|
42
52
|
<div class="d">enqueued <span class="num" data-stat="enqueued"><%= number_with_delimiter @stats.enqueued %></span> · scheduled <span class="num" data-stat="scheduled"><%= number_with_delimiter @stats.scheduled_size %></span></div>
|
|
53
|
+
<canvas data-rh-card="backlog" width="300" height="52" aria-hidden="true"></canvas>
|
|
43
54
|
</div>
|
|
44
55
|
</div>
|
|
45
56
|
|
|
@@ -52,12 +63,15 @@
|
|
|
52
63
|
<div class="rh-insights">
|
|
53
64
|
<div class="rh-panel rh-insight">
|
|
54
65
|
<div class="rh-insight-h">Top failing job classes <%= link_to "all errors →", errors_path, class: "rh-sub" %></div>
|
|
66
|
+
<%# One legend, not one vendor lockup per row. Five stacked wordmarks read as
|
|
67
|
+
five logos; the same legend-plus-icon treatment the Errors page uses. %>
|
|
68
|
+
<div class="rh-insight-legend"><%= trace_legend(@top_errors) %></div>
|
|
55
69
|
<% if @top_errors.empty? %>
|
|
56
70
|
<div class="rh-empty">No failing jobs 🎉</div>
|
|
57
71
|
<% else %>
|
|
58
72
|
<% @top_errors.each do |g| %>
|
|
59
73
|
<div class="rh-insight-row">
|
|
60
|
-
<div class="rh-insight-main"><%= link_to g[:klass], errors_path(q: g[:klass]), class: "rh-joblink" %> <%=
|
|
74
|
+
<div class="rh-insight-main"><%= link_to g[:klass], errors_path(q: g[:klass]), class: "rh-joblink" %> <%= tag_badges(tags_for_class(g[:klass])) %> <%= error_trace_icon(klass: g[:klass], error: g[:error]) %><br><span class="rh-err"><%= g[:error] %></span></div>
|
|
61
75
|
<span class="rh-pill"><%= number_with_delimiter g[:count] %></span>
|
|
62
76
|
</div>
|
|
63
77
|
<% end %>
|
|
@@ -73,13 +87,44 @@
|
|
|
73
87
|
<% label, css = queue_state(q.latency) %>
|
|
74
88
|
<div class="rh-insight-row">
|
|
75
89
|
<div class="rh-insight-main"><%= link_to q.name, queues_path, class: "rh-joblink rh-mono" %><br><span class="rh-sub"><%= number_with_delimiter q.size %> waiting</span></div>
|
|
76
|
-
<span class="rh-st <%= css %>"><%=
|
|
90
|
+
<span class="rh-st <%= css %>"><%= duration(q.latency) %></span>
|
|
77
91
|
</div>
|
|
78
92
|
<% end %>
|
|
79
93
|
<% end %>
|
|
80
94
|
</div>
|
|
81
95
|
</div>
|
|
82
96
|
|
|
97
|
+
<%# Daily history, from the counters Sidekiq already keeps. The live throughput
|
|
98
|
+
chart above answers "what is happening"; this answers "is that normal",
|
|
99
|
+
which nothing in Roundhouse could answer before. %>
|
|
100
|
+
<% if @history.present? %>
|
|
101
|
+
<div class="rh-chart-wrap">
|
|
102
|
+
<div class="top">
|
|
103
|
+
<h3>History</h3>
|
|
104
|
+
<span class="rh-sub">daily · processed and failure rate</span>
|
|
105
|
+
<span class="rh-hist-ranges">
|
|
106
|
+
<% RoundhouseUi::History::RANGES.each do |days, label| %>
|
|
107
|
+
<%= link_to label, root_path(history: days),
|
|
108
|
+
class: "rh-qf#{' is-on' if @history_days == days.to_i}" %>
|
|
109
|
+
<% end %>
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<canvas id="rh-history"
|
|
113
|
+
width="1100" height="150"
|
|
114
|
+
data-rh-history="<%= @history.map { |d| { d: d.date, p: d.processed, f: d.failed, r: d.failure_rate.round(5) } }.to_json %>"
|
|
115
|
+
data-rh-typical="<%= @typical_failure_rate %>"
|
|
116
|
+
aria-hidden="true"></canvas>
|
|
117
|
+
<div class="rh-hist-legend">
|
|
118
|
+
<span><i class="rh-swatch is-proc"></i>processed</span>
|
|
119
|
+
<span><i class="rh-swatch is-rate"></i>failure rate</span>
|
|
120
|
+
<% if @typical_failure_rate %>
|
|
121
|
+
<span class="rh-sub">typical <%= (@typical_failure_rate * 100).round(2) %>%</span>
|
|
122
|
+
<% end %>
|
|
123
|
+
<span class="rh-sub rh-hist-hover" id="rh-hist-read"></span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<% end %>
|
|
127
|
+
|
|
83
128
|
<h2 class="rh-h2">Queues <span class="hint">live · click a queue to manage</span></h2>
|
|
84
129
|
<table class="rh-table">
|
|
85
130
|
<thead><tr><th>Queue</th><th>State</th><th class="r">Depth</th><th class="r">Oldest job</th><th></th></tr></thead>
|
|
@@ -93,7 +138,7 @@
|
|
|
93
138
|
<td class="rh-mono"><%= q.name %></td>
|
|
94
139
|
<td><span class="rh-st <%= css %>"><%= label %></span></td>
|
|
95
140
|
<td class="r rh-mono"><%= number_with_delimiter q.size %></td>
|
|
96
|
-
<td class="r rh-mono <%= "rh-lat-warn" if q.latency > 60 %>"><%=
|
|
141
|
+
<td class="r rh-mono <%= "rh-lat-warn" if q.latency > 60 %>"><%= duration(q.latency) %></td>
|
|
97
142
|
<td class="r"><%= link_to "Manage →", queues_path, class: "rh-btn" %></td>
|
|
98
143
|
</tr>
|
|
99
144
|
<% end %>
|
|
@@ -1,54 +1,76 @@
|
|
|
1
1
|
<% content_for :title, "Dead set" %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
</form>
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: dead_set_path,
|
|
4
|
+
noun: "class, jid, error or arguments" %>
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
7
|
+
<%= render "roundhouse_ui/shared/tag_filter" %>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
<div class="rh-bulkbar">
|
|
11
|
-
<%= button_to "↻ Retry all matching", bulk_all_dead_path, method: :post, params: { op: "retry", q: @query }, class: "rh-btn", form_class: "rh-inline", data: { turbo_confirm: "Retry every dead job matching “#{@query}” (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
12
|
-
<%= button_to "✕ Delete all matching", bulk_all_dead_path, method: :post, params: { op: "delete", q: @query }, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Permanently delete every dead job matching “#{@query}” (up to #{RoundhouseUi::JobSetBrowsing::BULK_CAP})?" } %>
|
|
13
|
-
<span class="rh-sub">acts on every match for “<%= @query %>”, not just this page</span>
|
|
14
|
-
</div>
|
|
15
|
-
<% end %>
|
|
9
|
+
<%= set_heading("Dead set", showing: @jobs.size, total: @total, query: @query, tag: @tag) %>
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
<%# One toolbar, two scopes. They used to be stacked bars whose captions were the
|
|
12
|
+
only thing distinguishing "the rows you ticked" from "every job that matches",
|
|
13
|
+
which is a dangerous distinction to bury. The selected-rows buttons live here
|
|
14
|
+
but submit the form wrapping the table, via the HTML form= attribute. %>
|
|
15
|
+
<div class="rh-actionbar">
|
|
16
|
+
<div class="rh-actiongroup">
|
|
17
|
+
<span class="rh-actionlabel">Selected rows</span>
|
|
18
|
+
<button type="submit" form="rh-bulk-dead" name="op" value="retry" class="rh-btn">↻ Retry</button>
|
|
19
|
+
<button type="submit" form="rh-bulk-dead" name="op" value="delete" class="rh-btn rh-btn-danger">✕ Delete</button>
|
|
22
20
|
</div>
|
|
21
|
+
|
|
22
|
+
<% if any_filter?(@query, @tag) %>
|
|
23
|
+
<div class="rh-actiongroup">
|
|
24
|
+
<span class="rh-actionlabel" title="<%= filter_description(@query, @tag) %>">
|
|
25
|
+
All <%= number_with_delimiter @total %> <%= filter_description(@query, @tag) %>
|
|
26
|
+
</span>
|
|
27
|
+
<%= link_to "↻ Retry", preview_bulk_dead_path(**filter_params(op: "retry")), class: "rh-btn" %>
|
|
28
|
+
<%= link_to "✕ Delete", preview_bulk_dead_path(**filter_params(op: "delete")), class: "rh-btn rh-btn-danger" %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<%# The form is EMPTY and sits beside the table, not around it. Wrapping the table
|
|
34
|
+
nested every row's button_to form inside this one, which is invalid HTML: the
|
|
35
|
+
parser closes the outer form at the first </form>, so the bulk form swallowed
|
|
36
|
+
the first row's authenticity_token. Two tokens in one submission and Rails
|
|
37
|
+
takes the last — a per-form token scoped to a row action, not to /dead/bulk —
|
|
38
|
+
so every bulk action failed with InvalidAuthenticityToken while carrying a
|
|
39
|
+
token that looked perfectly valid. The checkboxes join it by id instead, the
|
|
40
|
+
same HTML form= association the toolbar buttons above already use. %>
|
|
41
|
+
<%= form_with url: bulk_dead_path, method: :post, id: "rh-bulk-dead", data: { turbo: false } do %><% end %>
|
|
42
|
+
|
|
43
|
+
<%= trace_legend(@jobs) %>
|
|
23
44
|
<table class="rh-table">
|
|
24
|
-
<thead><tr><th style="width:28px"><input type="checkbox" id="rh-all" aria-label="select all"></th><th>Job</th><th>Last error</th><th class="r">Died</th></tr></thead>
|
|
45
|
+
<thead><tr><th style="width:28px"><input type="checkbox" id="rh-all" aria-label="select all"></th><th>Job</th><% if tag_column? %><th><%= tag_column_label %></th><% end %><th>Queue</th><th>Last error</th><th class="r">Died</th><th class="r">Actions</th></tr></thead>
|
|
25
46
|
<tbody>
|
|
26
47
|
<% if @jobs.empty? %>
|
|
27
|
-
<tr><td colspan="
|
|
48
|
+
<tr><td colspan="<%= tag_column? ? 7 : 6 %>" class="rh-empty"><%= empty_state(@query, @tag, noun: "dead jobs", all_clear: "Dead set is empty 🎉") %></td></tr>
|
|
28
49
|
<% else %>
|
|
29
50
|
<% @jobs.each do |job| %>
|
|
30
51
|
<tr>
|
|
31
|
-
<td><input type="checkbox" name="jids[]" value="<%= job.jid %>" class="rh-check" aria-label="select <%= job.jid %>"></td>
|
|
32
|
-
<td><%=
|
|
52
|
+
<td><input type="checkbox" form="rh-bulk-dead" name="jids[]" value="<%= job.jid %>" class="rh-check" aria-label="select <%= job.jid %>"></td>
|
|
53
|
+
<td><%= job_identity(job_display_class(job), job.jid, job_path(set: "dead", jid: job.jid)) %></td>
|
|
54
|
+
<% if tag_column? %><td><%= tag_cell(tags_for(job)) %></td><% end %>
|
|
55
|
+
<td><%= queue_pill(job.queue, link: true) %></td>
|
|
33
56
|
<td>
|
|
34
57
|
<span class="rh-err"><%= job.item["error_class"] %></span>
|
|
35
|
-
<% if job.item["error_message"].present? %><br><span class="rh-sub"><%= truncate(job.item["error_message"], length: 90) %></span><% end %>
|
|
58
|
+
<% if job.item["error_message"].present? %><br><span class="rh-sub" title="<%= job.item["error_message"] %>"><%= truncate(job.item["error_message"], length: 90) %></span><% end %>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="r"><%= job_time(job.at, overdue: "#{time_ago_in_words(job.at)} ago") %></td>
|
|
61
|
+
<td class="r rh-cell-actions">
|
|
62
|
+
<%= find_like_link("dead", job) %>
|
|
63
|
+
<%= trace_icon(klass: job_display_class(job), jid: job.jid, queue: job.queue) %>
|
|
64
|
+
<% if job_editing? %><%= link_to "Edit", edit_job_path(set: "dead", jid: job.jid), class: "rh-btn" %><% end %>
|
|
65
|
+
<%= button_to "Retry", retry_dead_job_path(job.jid), method: :post, class: "rh-btn", form_class: "rh-inline" %>
|
|
66
|
+
<%= button_to "Delete", delete_dead_job_path(job.jid), method: :post, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Delete #{job.jid}?" } %>
|
|
36
67
|
</td>
|
|
37
|
-
<td class="r"><%= time_ago_in_words(job.at) %> ago</td>
|
|
38
68
|
</tr>
|
|
39
69
|
<% end %>
|
|
40
70
|
<% end %>
|
|
41
71
|
</tbody>
|
|
42
72
|
</table>
|
|
43
|
-
<% end %>
|
|
44
73
|
<%= render "roundhouse_ui/shared/pager" %>
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
var all = document.getElementById("rh-all");
|
|
49
|
-
if (!all) return;
|
|
50
|
-
all.addEventListener("change", function () {
|
|
51
|
-
document.querySelectorAll(".rh-check").forEach(function (cb) { cb.checked = all.checked; });
|
|
52
|
-
});
|
|
53
|
-
})();
|
|
54
|
-
</script>
|
|
75
|
+
<%# The select-all handler lives in the layout head, delegated from document, so
|
|
76
|
+
it survives Turbo body swaps. Don't reintroduce a body script here. #26 %>
|
|
@@ -1,23 +1,63 @@
|
|
|
1
1
|
<% content_for :title, "Errors" %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: errors_path,
|
|
4
|
+
keys: RoundhouseUi::ErrorsController::FILTER_KEYS,
|
|
5
|
+
noun: "class, error or squad" %>
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
<%# Counts come from the same scan that built the groups, so this costs no extra
|
|
9
|
+
Redis work. Each chip narrows the list below rather than sending you elsewhere. %>
|
|
10
|
+
<%= render "roundhouse_ui/shared/tag_filter", counts: @tag_counts %>
|
|
11
|
+
|
|
12
|
+
<h2 class="rh-h2">
|
|
13
|
+
Errors · <%= pluralize(@groups.size, "issue") %>
|
|
14
|
+
<% if @tag %><span class="hint">filtered to <%= @tag[0] %>: <%= @tag[1] %></span><% end %>
|
|
15
|
+
</h2>
|
|
16
|
+
|
|
17
|
+
<%= trace_legend(@groups) %>
|
|
8
18
|
<table class="rh-table">
|
|
9
|
-
<thead
|
|
19
|
+
<thead>
|
|
20
|
+
<tr>
|
|
21
|
+
<th>Issue</th>
|
|
22
|
+
<% if tag_column? %><th><%= tag_column_label %></th><% end %>
|
|
23
|
+
<th class="r">Occurrences</th>
|
|
24
|
+
<th>In</th>
|
|
25
|
+
<th>Queues</th>
|
|
26
|
+
<th class="r">Last seen</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
10
29
|
<tbody>
|
|
11
30
|
<% if @groups.empty? %>
|
|
12
|
-
<tr><td colspan="
|
|
31
|
+
<tr><td colspan="<%= tag_column? ? 6 : 5 %>" class="rh-empty">
|
|
32
|
+
<%= empty_state(@query, @tag, noun: "issues", all_clear: "No failing jobs 🎉") %>
|
|
33
|
+
</td></tr>
|
|
13
34
|
<% else %>
|
|
35
|
+
<%# Occurrences carry magnitude as well as a figure: 340 and 2 should not
|
|
36
|
+
look the same when you are scanning for the one that matters. Severity
|
|
37
|
+
is by share of the total, since "a lot" is relative to the page. %>
|
|
38
|
+
<% worst = @groups.map { |x| x[:count].to_i }.max || 0 %>
|
|
14
39
|
<% @groups.each do |g| %>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<td
|
|
40
|
+
<% share = worst.positive? ? g[:count].to_f / worst : 0 %>
|
|
41
|
+
<tr data-rh-sev="<%= share >= 0.5 ? "crit" : share >= 0.15 ? "warn" : "ok" %>">
|
|
42
|
+
<td><%= g[:klass] %> <%= error_trace_icon(klass: g[:klass], error: g[:error]) %>
|
|
43
|
+
<%# The grouped rows are where someone lands during an incident, so the
|
|
44
|
+
runbook belongs here as much as on the individual job. %>
|
|
45
|
+
<%= runbook_link(g[:klass], label: "runbook") %>
|
|
46
|
+
<br><span class="rh-err"><%= g[:error] %></span></td>
|
|
47
|
+
<% if tag_column? %><td><%= tag_cell(@group_tags[g[:klass]]) %></td><% end %>
|
|
48
|
+
<td class="r">
|
|
49
|
+
<span class="rh-count" data-rh-sev="<%= share >= 0.5 ? "crit" : share >= 0.15 ? "warn" : "ok" %>"
|
|
50
|
+
title="<%= number_with_delimiter g[:count] %><%= "+" if @truncated %> of <%= number_with_delimiter worst %><%= "+" if @truncated %> in the worst group<%= " — counts are floors, the scan stopped at #{number_with_delimiter @scan_limit}" if @truncated %>">
|
|
51
|
+
<i style="width:<%= (share * 100).round(1) %>%"></i>
|
|
52
|
+
<%# A truncated scan yields floors, not totals: the group holds at least
|
|
53
|
+
this many. Printing an exact figure for a number we know is capped
|
|
54
|
+
is the one thing the note below the table cannot fix. %>
|
|
55
|
+
<span><%= number_with_delimiter g[:count] %><%= "+" if @truncated %></span>
|
|
56
|
+
</span>
|
|
57
|
+
</td>
|
|
18
58
|
<td><span class="rh-sub"><%= g[:sources].join(", ") %></span></td>
|
|
19
|
-
<td
|
|
20
|
-
<td class="r"><%= g[:last_at]
|
|
59
|
+
<td><%= safe_join(g[:queues].map { |q| queue_pill(q) }, " ") %></td>
|
|
60
|
+
<td class="r"><%= job_time(g[:last_at], overdue: "#{time_ago_in_words(g[:last_at])} ago" ) if g[:last_at] %><%= content_tag(:span, "—", class: "rh-sub") unless g[:last_at] %></td>
|
|
21
61
|
</tr>
|
|
22
62
|
<% end %>
|
|
23
63
|
<% end %>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
<%# item binds first: the title needs it, and reading @entry.item["class"] above
|
|
2
|
+
this line would leave the two able to disagree. %>
|
|
2
3
|
<% item = @entry.item %>
|
|
4
|
+
<% content_for :title, RoundhouseUi.unwrapped_class(item["class"], item) %>
|
|
3
5
|
|
|
4
6
|
<div class="rh-tags">
|
|
5
7
|
<span class="rh-tag"><%= @set %></span>
|
|
6
8
|
<span class="rh-tag">queue <%= @entry.queue %></span>
|
|
7
9
|
<% if item["retry_count"] %><span class="rh-tag">retry <%= item["retry_count"] %></span><% end %>
|
|
10
|
+
<%= tag_badges(tags_for(@entry)) %>
|
|
8
11
|
<span class="rh-tag rh-mono"><%= @entry.jid %></span>
|
|
12
|
+
<%# The person reading a failed job at 3am is rarely the person who wrote it. %>
|
|
13
|
+
<%= runbook_link(item["class"], item) %>
|
|
9
14
|
</div>
|
|
10
15
|
|
|
11
16
|
<div class="rh-sec">Arguments<% if RoundhouseUi.redact_args.present? %> <span class="rh-sub">— sensitive keys masked</span><% end %></div>
|
|
@@ -32,13 +37,13 @@
|
|
|
32
37
|
<% end %>
|
|
33
38
|
<% end %>
|
|
34
39
|
|
|
35
|
-
<% trace = trace_link(klass: item["class"], jid: @entry.jid, queue: @entry.queue) %>
|
|
40
|
+
<% trace = trace_link(klass: RoundhouseUi.unwrapped_class(item["class"], item), jid: @entry.jid, queue: @entry.queue) %>
|
|
36
41
|
<% if trace.present? %>
|
|
37
42
|
<div class="rh-sec">Observability</div>
|
|
38
43
|
<%= trace %>
|
|
39
44
|
<% end %>
|
|
40
45
|
|
|
41
46
|
<div class="rh-pager">
|
|
42
|
-
<% if
|
|
47
|
+
<% if job_editing? %><%= link_to "Edit", edit_job_path(set: @set, jid: @entry.jid), class: "rh-btn rh-btn-primary" %><% end %>
|
|
43
48
|
<%= link_to "← Back", { "dead" => dead_set_path, "retry" => retries_path, "scheduled" => scheduled_path }[@set] || root_path, class: "rh-btn" %>
|
|
44
49
|
</div>
|
|
@@ -46,6 +46,20 @@
|
|
|
46
46
|
<div class="v num" id="rh-m-eta">—</div>
|
|
47
47
|
<div class="d">backlog clears in, at current rate</div>
|
|
48
48
|
</div>
|
|
49
|
+
<%# Capacity is deliberately whole-fleet rather than per queue: Sidekiq counts
|
|
50
|
+
processed jobs globally, so there is no honest way to say what share of
|
|
51
|
+
throughput belongs to one queue. See #36. %>
|
|
52
|
+
<div class="rh-card">
|
|
53
|
+
<div class="k">Capacity
|
|
54
|
+
<select id="rh-m-deadline" class="rh-btn rh-cap-pick" aria-label="Clear the backlog within">
|
|
55
|
+
<option value="900">15m</option>
|
|
56
|
+
<option value="3600" selected>1h</option>
|
|
57
|
+
<option value="14400">4h</option>
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="v num" id="rh-m-capacity">—</div>
|
|
61
|
+
<div class="d" id="rh-m-capacity-note">threads needed to clear in time</div>
|
|
62
|
+
</div>
|
|
49
63
|
</div>
|
|
50
64
|
|
|
51
65
|
<% if RoundhouseUi.collect_durations %>
|
|
@@ -54,14 +68,23 @@
|
|
|
54
68
|
<div class="rh-panel"><div class="rh-empty">No durations recorded yet — jobs need to run with the collector middleware installed.</div></div>
|
|
55
69
|
<% else %>
|
|
56
70
|
<table class="rh-table">
|
|
57
|
-
<thead><tr><th>Job class</th><th class="r">Runs</th><th class="r">Avg</th><th
|
|
71
|
+
<thead><tr><th>Job class</th><th class="r">Runs</th><th class="r">Avg</th><th>Total time</th></tr></thead>
|
|
58
72
|
<tbody>
|
|
73
|
+
<%# The point of "slowest classes" is proportion — this one class is most
|
|
74
|
+
of your compute — and proportion is a length, not a figure you compare
|
|
75
|
+
by reading down a column. %>
|
|
76
|
+
<% worst = @durations.map { |d| d[:total_ms].to_f }.max || 0 %>
|
|
59
77
|
<% @durations.each do |d| %>
|
|
60
78
|
<tr>
|
|
61
79
|
<td class="rh-mono"><%= d[:klass] %></td>
|
|
62
80
|
<td class="r rh-mono"><%= number_with_delimiter d[:count] %></td>
|
|
63
|
-
<td class="r rh-mono <%= "rh-lat-warn" if d[:avg_ms] >= 1000 %>"><%=
|
|
64
|
-
<td
|
|
81
|
+
<td class="r rh-mono <%= "rh-lat-warn" if d[:avg_ms] >= 1000 %>"><%= duration_ms(d[:avg_ms]) %></td>
|
|
82
|
+
<td>
|
|
83
|
+
<div class="rh-dbar">
|
|
84
|
+
<div class="t"><i style="width:<%= worst.positive? ? (d[:total_ms].to_f / worst * 100).round(1) : 0 %>%"></i></div>
|
|
85
|
+
<span class="n"><%= duration_ms(d[:total_ms]) %></span>
|
|
86
|
+
</div>
|
|
87
|
+
</td>
|
|
65
88
|
</tr>
|
|
66
89
|
<% end %>
|
|
67
90
|
</tbody>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<% content_for :title, "Queues" %>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: queues_path,
|
|
4
|
+
keys: RoundhouseUi::QueuesController::INDEX_FILTER_KEYS,
|
|
5
|
+
noun: "queue names" %>
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
7
|
+
|
|
8
|
+
<% if job_editing? %>
|
|
4
9
|
<div class="rh-toolbar"><%= link_to "+ Enqueue job", new_job_path, class: "rh-btn rh-btn-primary" %></div>
|
|
5
10
|
<% end %>
|
|
6
11
|
|
|
@@ -11,20 +16,59 @@
|
|
|
11
16
|
</div>
|
|
12
17
|
<% end %>
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
<%# State chips. A paused queue is the single most common thing an operator is
|
|
20
|
+
hunting for on this page — it is why work is not moving. Hidden where pausing
|
|
21
|
+
is turned off: filtering by a state you cannot set is a dead end. %>
|
|
22
|
+
<% if RoundhouseUi.pause_enabled %>
|
|
23
|
+
<div class="rh-quickfilters">
|
|
24
|
+
<%= link_to filter_url(state: nil, page: nil), class: "rh-qf#{' is-on' if @state.blank?}" do %>
|
|
25
|
+
All <span class="rh-qf-n"><%= @state_counts.values.sum %></span>
|
|
26
|
+
<% end %>
|
|
27
|
+
<%= link_to filter_url(state: "active", page: nil), class: "rh-qf#{' is-on' if @state == 'active'}" do %>
|
|
28
|
+
Active <span class="rh-qf-n"><%= @state_counts["active"] %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
<%= link_to filter_url(state: "paused", page: nil), class: "rh-qf#{' is-on' if @state == 'paused'}" do %>
|
|
31
|
+
Paused <span class="rh-qf-n"><%= @state_counts["paused"] %></span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<h2 class="rh-h2">Queues · <%= number_with_delimiter @total_size %> waiting<% if @query.present? %> <span class="hint">filtered to “<%= @query %>”</span><% end %><% if @state.present? %> <span class="hint"><%= @state %> only</span><% end %></h2>
|
|
15
37
|
<table class="rh-table">
|
|
16
|
-
<thead><tr
|
|
38
|
+
<thead><tr>
|
|
39
|
+
<th class="rh-sort" data-sort-col="0" data-sort-type="text">Queue</th>
|
|
40
|
+
<th class="rh-sort" data-sort-col="1" data-sort-type="text">State</th>
|
|
41
|
+
<th class="rh-sort r" data-sort-col="2" data-sort-type="num">Size</th>
|
|
42
|
+
<th class="rh-sort r" data-sort-col="3" data-sort-type="num">Latency</th>
|
|
43
|
+
<th class="rh-trend-h">Trend</th>
|
|
44
|
+
<th class="rh-sort" data-sort-col="5" data-sort-type="num">Forecast</th>
|
|
45
|
+
<th class="r">Actions</th>
|
|
46
|
+
</tr></thead>
|
|
17
47
|
<tbody>
|
|
18
48
|
<% if @queues.empty? %>
|
|
19
|
-
<tr><td colspan="
|
|
49
|
+
<tr><td colspan="7" class="rh-empty"><%= empty_state(@query, nil, noun: "queues", all_clear: "No active queues") %></td></tr>
|
|
20
50
|
<% else %>
|
|
21
51
|
<% @queues.each do |q| %>
|
|
22
52
|
<% paused = @paused.include?(q.name) %>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<td
|
|
53
|
+
<% latency = q.latency %>
|
|
54
|
+
<%# data-rh-sev is set by the poll once a velocity exists; the server seeds
|
|
55
|
+
what it can already know so the stripe is not blank on first paint. %>
|
|
56
|
+
<tr data-rh-sev="<%= q.size.zero? ? "ok" : (latency > 600 ? "crit" : latency > 60 ? "warn" : "ok") %>">
|
|
57
|
+
<td data-sort-value="<%= q.name %>"><%= link_to queue_pill(q.name), queue_path(q.name), class: "rh-pill-link" %></td>
|
|
58
|
+
<td data-sort-value="<%= paused ? "paused" : "active" %>"><% if paused %><span class="rh-pill rh-pill-warn">paused</span><% else %><span class="rh-sub">active</span><% end %></td>
|
|
59
|
+
<td class="r" data-sort-value="<%= q.size %>"><%= number_with_delimiter q.size %></td>
|
|
60
|
+
<td class="r <%= "rh-lat-warn" if latency > 60 %>" data-sort-value="<%= latency.to_f %>"><%= duration(latency) %></td>
|
|
61
|
+
<%# Filled by the stats poll — it needs two samples for a velocity, so
|
|
62
|
+
the first paint honestly says it is still measuring. %>
|
|
63
|
+
<%# Both render; the poll reveals the canvas once it has two samples to
|
|
64
|
+
draw. Shown the other way round the canvas would be a blank gap. %>
|
|
65
|
+
<td class="rh-trend">
|
|
66
|
+
<span class="rh-trend-empty" data-rh-spark-wait="<%= q.name %>"></span>
|
|
67
|
+
<canvas width="176" height="44" data-rh-spark="<%= q.name %>" aria-hidden="true" hidden></canvas>
|
|
68
|
+
</td>
|
|
69
|
+
<%# sort-value starts unknown and is written by the poll, so an unmeasured
|
|
70
|
+
queue sorts last rather than pretending to be zero. %>
|
|
71
|
+
<td data-sort-value=""><span class="rh-fc rh-fc-wait" data-rh-forecast="<%= q.name %>"><%= q.size.zero? ? "clear" : "measuring…" %></span></td>
|
|
28
72
|
<td class="r">
|
|
29
73
|
<% if RoundhouseUi.pause_enabled %>
|
|
30
74
|
<% if paused %>
|
|
@@ -33,7 +77,7 @@
|
|
|
33
77
|
<%= button_to "Pause", pause_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
|
|
34
78
|
<% end %>
|
|
35
79
|
<% end %>
|
|
36
|
-
|
|
80
|
+
<% if RoundhouseUi.backend.supports?(:snapshots) %><%= button_to "Snapshot", snapshot_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %><% end %>
|
|
37
81
|
<%= button_to "Purge", purge_queue_path(q.name),
|
|
38
82
|
method: :post,
|
|
39
83
|
class: "rh-btn rh-btn-danger",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<% content_for :title, "Queue #{@name}" %>
|
|
2
|
+
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: queue_path(@name),
|
|
4
|
+
noun: "class, jid or arguments" %>
|
|
5
|
+
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
7
|
+
<%= render "roundhouse_ui/shared/tag_filter" %>
|
|
8
|
+
|
|
9
|
+
<h2 class="rh-h2">
|
|
10
|
+
<%= queue_pill(@name) %>
|
|
11
|
+
<span class="hint">
|
|
12
|
+
<%= number_with_delimiter @total %> waiting
|
|
13
|
+
<% if @latency %>· oldest <%= duration(@latency) %><% end %>
|
|
14
|
+
· <span class="rh-fc rh-fc-wait" data-rh-forecast="<%= @name %>">measuring…</span>
|
|
15
|
+
<% if @paused %>· <span class="rh-pill rh-pill-warn">paused</span><% end %>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="rh-spacer"></span>
|
|
18
|
+
<%= link_to "← All queues", queues_path, class: "rh-btn" %>
|
|
19
|
+
</h2>
|
|
20
|
+
|
|
21
|
+
<table class="rh-table">
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Job</th>
|
|
25
|
+
<% if tag_column? %><th><%= tag_column_label %></th><% end %>
|
|
26
|
+
<th>Arguments</th>
|
|
27
|
+
<th class="r">Enqueued</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<% if @jobs.empty? %>
|
|
32
|
+
<tr><td colspan="<%= tag_column? ? 4 : 3 %>" class="rh-empty">
|
|
33
|
+
<%= empty_state(@query, @tag, noun: "jobs on “#{@name}”", all_clear: "Nothing waiting on “#{@name}”.") %>
|
|
34
|
+
</td></tr>
|
|
35
|
+
<% else %>
|
|
36
|
+
<% @jobs.each do |job| %>
|
|
37
|
+
<tr>
|
|
38
|
+
<td>
|
|
39
|
+
<%= job_display_class(job) %>
|
|
40
|
+
<div class="rh-sub rh-mono rh-jid" title="<%= job.jid %>"><%= job.jid %></div>
|
|
41
|
+
</td>
|
|
42
|
+
<% if tag_column? %><td><%= tag_cell(tags_for(job)) %></td><% end %>
|
|
43
|
+
<td><%= job_args_preview(job) %></td>
|
|
44
|
+
<td class="r"><%= job_time(RoundhouseUi.enqueued_at(job.item), overdue: "just now") %></td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
<%= render "roundhouse_ui/shared/pager" %>
|
|
51
|
+
|
|
52
|
+
<p class="rh-note">
|
|
53
|
+
Jobs waiting to be picked up, oldest last. Queue-level actions live on the
|
|
54
|
+
<%= link_to "queues list", queues_path %>.
|
|
55
|
+
</p>
|