roundhouse_ui 0.10.0 → 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 +563 -50
- data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +190 -23
- data/app/controllers/roundhouse_ui/application_controller.rb +73 -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 +23 -4
- data/app/controllers/roundhouse_ui/dead_controller.rb +26 -12
- data/app/controllers/roundhouse_ui/errors_controller.rb +30 -8
- data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
- data/app/controllers/roundhouse_ui/queues_controller.rb +56 -7
- data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
- data/app/controllers/roundhouse_ui/retries_controller.rb +33 -12
- data/app/controllers/roundhouse_ui/scheduled_controller.rb +3 -8
- 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 +253 -10
- data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
- data/app/helpers/roundhouse_ui/observability_helper.rb +113 -27
- data/app/helpers/roundhouse_ui/tags_helper.rb +30 -3
- data/app/views/layouts/roundhouse_ui/application.html.erb +1181 -92
- 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 +23 -15
- data/app/views/roundhouse_ui/errors/index.html.erb +28 -9
- data/app/views/roundhouse_ui/jobs/show.html.erb +7 -3
- data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
- data/app/views/roundhouse_ui/queues/index.html.erb +53 -13
- 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 +17 -15
- data/app/views/roundhouse_ui/scheduled/index.html.erb +10 -10
- 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 +2 -4
- 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 +13 -3
- 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 +8 -11
- data/lib/roundhouse_ui/theme.rb +298 -0
- data/lib/roundhouse_ui/version.rb +1 -1
- data/lib/roundhouse_ui.rb +200 -5
- data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
- metadata +29 -7
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<% content_for :title, "Recurring" %>
|
|
2
|
+
|
|
3
|
+
<%# The useful view is not the crontab — it is "this says hourly and has not run
|
|
4
|
+
in three days". That comparison needs the schedule's interval, which needs a
|
|
5
|
+
cron parser; where one is unavailable, staleness is reported as unknown
|
|
6
|
+
rather than guessed. %>
|
|
7
|
+
<h2 class="rh-h2">
|
|
8
|
+
Recurring · <%= pluralize(@tasks.size, "task") %>
|
|
9
|
+
<% if @overdue.positive? %><span class="hint"><%= @overdue %> overdue</span><% end %>
|
|
10
|
+
<% if @sources.any? %><span class="hint">from <%= @sources.to_sentence %></span><% end %>
|
|
11
|
+
</h2>
|
|
12
|
+
|
|
13
|
+
<% if @tasks.empty? %>
|
|
14
|
+
<div class="rh-panel">
|
|
15
|
+
<p class="rh-empty">
|
|
16
|
+
No recurring jobs found. Roundhouse reads sidekiq-cron, sidekiq-scheduler,
|
|
17
|
+
Sidekiq Enterprise periodic loops and Solid Queue recurring tasks — configure
|
|
18
|
+
one and its schedule appears here.
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
<% else %>
|
|
22
|
+
<table class="rh-table">
|
|
23
|
+
<thead><tr>
|
|
24
|
+
<th class="rh-sort" data-sort-col="0" data-sort-type="text">Task</th>
|
|
25
|
+
<th class="rh-sort" data-sort-col="1" data-sort-type="text">Schedule</th>
|
|
26
|
+
<th>Queue</th>
|
|
27
|
+
<th class="rh-sort r" data-sort-col="3" data-sort-type="num">Last run</th>
|
|
28
|
+
<th>State</th>
|
|
29
|
+
</tr></thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<% @tasks.each do |t| %>
|
|
32
|
+
<% sev = { overdue: "crit", paused: "warn", unknown: "warn", ok: "ok" }[t.status] %>
|
|
33
|
+
<tr data-rh-sev="<%= sev %>">
|
|
34
|
+
<td data-sort-value="<%= t.name %>">
|
|
35
|
+
<div class="rh-mono"><%= t.name %></div>
|
|
36
|
+
<% if t.klass.present? && t.klass.to_s != t.name.to_s %>
|
|
37
|
+
<div class="rh-sub rh-mono"><%= t.klass %></div>
|
|
38
|
+
<% end %>
|
|
39
|
+
</td>
|
|
40
|
+
<td data-sort-value="<%= t.schedule %>"><span class="rh-pill rh-mono"><%= t.schedule %></span></td>
|
|
41
|
+
<td><%= t.queue.present? ? queue_pill(t.queue) : content_tag(:span, "—", class: "rh-sub") %></td>
|
|
42
|
+
<td class="r" data-sort-value="<%= t.last_run&.to_i || 0 %>">
|
|
43
|
+
<%= t.last_run ? job_time(t.last_run, overdue: "#{time_ago_in_words(t.last_run)} ago") : content_tag(:span, "never", class: "rh-sub") %>
|
|
44
|
+
</td>
|
|
45
|
+
<td>
|
|
46
|
+
<% case t.status %>
|
|
47
|
+
<% when :overdue %>
|
|
48
|
+
<span class="rh-fc rh-fc-growing"><%= icon(:warn) %> overdue — schedule says <%= t.schedule %></span>
|
|
49
|
+
<% when :paused %>
|
|
50
|
+
<span class="rh-pill rh-pill-warn">paused</span>
|
|
51
|
+
<% when :unknown %>
|
|
52
|
+
<span class="rh-fc rh-fc-stalled"><%= icon(:warn) %> never run</span>
|
|
53
|
+
<% else %>
|
|
54
|
+
<span class="rh-fc rh-fc-draining"><%= icon(:ok) %> on schedule</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
|
|
62
|
+
<p class="rh-note">
|
|
63
|
+
Read-only. Schedules are defined in your application; this shows what those
|
|
64
|
+
definitions are currently doing.
|
|
65
|
+
<% unless defined?(::Fugit) %>
|
|
66
|
+
Staleness cannot be judged without a cron parser — install
|
|
67
|
+
<code>fugit</code> (sidekiq-cron and sidekiq-scheduler both already depend
|
|
68
|
+
on it) and overdue tasks will be flagged.
|
|
69
|
+
<% end %>
|
|
70
|
+
</p>
|
|
71
|
+
<% end %>
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<% content_for :title, "Retries" %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<% if params[:tag].present? %><input type="hidden" name="tag" value="<%= params[:tag] %>"><% end %>
|
|
6
|
-
<% if params[:queue].present? %><input type="hidden" name="queue" value="<%= params[:queue] %>"><% end %>
|
|
7
|
-
</form>
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: retries_path,
|
|
4
|
+
noun: "class, jid, error or arguments" %>
|
|
8
5
|
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
9
7
|
<%= render "roundhouse_ui/shared/tag_filter" %>
|
|
10
8
|
|
|
11
9
|
<%= set_heading("Retries", showing: @jobs.size, total: @total, query: @query, tag: @tag) %>
|
|
@@ -16,32 +14,36 @@
|
|
|
16
14
|
<span class="rh-actionlabel" title="<%= filter_description(@query, @tag) %>">
|
|
17
15
|
All <%= number_with_delimiter @total %> <%= filter_description(@query, @tag) %>
|
|
18
16
|
</span>
|
|
19
|
-
<%=
|
|
20
|
-
<%=
|
|
17
|
+
<%= link_to "↻ Run now", preview_bulk_retries_path(**filter_params(op: "retry")), class: "rh-btn" %>
|
|
18
|
+
<%= link_to "✕ Delete", preview_bulk_retries_path(**filter_params(op: "delete")), class: "rh-btn rh-btn-danger" %>
|
|
21
19
|
</div>
|
|
22
20
|
</div>
|
|
23
21
|
<% end %>
|
|
24
22
|
|
|
23
|
+
<%= trace_legend(@jobs) %>
|
|
25
24
|
<table class="rh-table">
|
|
26
|
-
<thead><tr><th>Job</th><% if tag_column? %><th><%= tag_column_label %></th><% end %><th>Queue</th><th>Last error</th><th
|
|
25
|
+
<thead><tr><th>Job</th><% if tag_column? %><th><%= tag_column_label %></th><% end %><th>Queue</th><th>Last error</th><th>Attempt</th><th>Next try</th><th class="r">Actions</th></tr></thead>
|
|
27
26
|
<tbody>
|
|
28
27
|
<% if @jobs.empty? %>
|
|
29
|
-
<tr><td colspan="<%= tag_column? ? 7 : 6 %>" class="rh-empty"><%=
|
|
28
|
+
<tr><td colspan="<%= tag_column? ? 7 : 6 %>" class="rh-empty"><%= empty_state(@query, @tag, noun: "retries", all_clear: "Nothing waiting to retry 🎉") %></td></tr>
|
|
30
29
|
<% else %>
|
|
31
30
|
<% @jobs.each do |job| %>
|
|
32
31
|
<tr>
|
|
33
|
-
<td><%= job_identity(job
|
|
32
|
+
<td><%= job_identity(job_display_class(job), job.jid, job_path(set: "retry", jid: job.jid)) %></td>
|
|
34
33
|
<% if tag_column? %><td><%= tag_cell(tags_for(job)) %></td><% end %>
|
|
35
34
|
<td><%= queue_pill(job.queue, link: true) %></td>
|
|
36
35
|
<td>
|
|
37
36
|
<span class="rh-err"><%= job.item["error_class"] %></span>
|
|
38
37
|
<% 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 %>
|
|
39
38
|
</td>
|
|
40
|
-
<td
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
<td><%= attempt_ladder(job.item["retry_count"].to_i + 1) %></td>
|
|
40
|
+
<%# `since` is the last failure, so the ring shows how far through the
|
|
41
|
+
backoff this job is, not only when it next fires. %>
|
|
42
|
+
<td><span class="rh-next"><%= countdown(job.at, since: job.item["failed_at"] && Time.at(job.item["failed_at"])) %></span></td>
|
|
43
|
+
<td class="r rh-cell-actions">
|
|
44
|
+
<%= find_like_link("retry", job) %>
|
|
45
|
+
<%= trace_icon(klass: job_display_class(job), jid: job.jid, queue: job.queue) %>
|
|
46
|
+
<% if job_editing? %><%= link_to "Edit", edit_job_path(set: "retry", jid: job.jid), class: "rh-btn" %><% end %>
|
|
45
47
|
<%= button_to "Run now", run_retry_path(job.jid), method: :post, class: "rh-btn", form_class: "rh-inline" %>
|
|
46
48
|
<%= button_to "Delete", delete_retry_path(job.jid), method: :post, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Delete #{job.jid}?" } %>
|
|
47
49
|
</td>
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
<% content_for :title, "Scheduled" %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<% if params[:tag].present? %><input type="hidden" name="tag" value="<%= params[:tag] %>"><% end %>
|
|
6
|
-
<% if params[:queue].present? %><input type="hidden" name="queue" value="<%= params[:queue] %>"><% end %>
|
|
7
|
-
</form>
|
|
3
|
+
<%= render "roundhouse_ui/shared/search_bar", action: scheduled_path,
|
|
4
|
+
noun: "class, jid or arguments" %>
|
|
8
5
|
|
|
6
|
+
<%= render "roundhouse_ui/shared/filter_error" %>
|
|
9
7
|
<%= render "roundhouse_ui/shared/tag_filter" %>
|
|
10
8
|
|
|
11
9
|
<%= set_heading("Scheduled", showing: @jobs.size, total: @total, query: @query, tag: @tag) %>
|
|
10
|
+
<%= trace_legend(@jobs) %>
|
|
12
11
|
<table class="rh-table">
|
|
13
12
|
<thead><tr><th>Job</th><% if tag_column? %><th><%= tag_column_label %></th><% end %><th>Queue</th><th class="r">Runs</th><th class="r">Actions</th></tr></thead>
|
|
14
13
|
<tbody>
|
|
15
14
|
<% if @jobs.empty? %>
|
|
16
|
-
<tr><td colspan="<%= tag_column? ? 5 : 4 %>" class="rh-empty"><%=
|
|
15
|
+
<tr><td colspan="<%= tag_column? ? 5 : 4 %>" class="rh-empty"><%= empty_state(@query, @tag, noun: "scheduled jobs", all_clear: "Nothing scheduled") %></td></tr>
|
|
17
16
|
<% else %>
|
|
18
17
|
<% @jobs.each do |job| %>
|
|
19
18
|
<tr>
|
|
20
|
-
<td><%= job_identity(job
|
|
19
|
+
<td><%= job_identity(job_display_class(job), job.jid, job_path(set: "scheduled", jid: job.jid)) %></td>
|
|
21
20
|
<% if tag_column? %><td><%= tag_cell(tags_for(job)) %></td><% end %>
|
|
22
21
|
<td><%= queue_pill(job.queue, link: true) %></td>
|
|
23
22
|
<td class="r"><%= job_time(job.at) %></td>
|
|
24
|
-
<td class="r">
|
|
25
|
-
<%=
|
|
23
|
+
<td class="r rh-cell-actions">
|
|
24
|
+
<%= find_like_link("scheduled", job) %>
|
|
25
|
+
<%= trace_icon(klass: job_display_class(job), jid: job.jid, queue: job.queue) %>
|
|
26
26
|
<% if RoundhouseUi.allow_job_editing %><%= link_to "Edit", edit_job_path(set: "scheduled", jid: job.jid), class: "rh-btn" %><% end %>
|
|
27
|
-
|
|
27
|
+
<% if RoundhouseUi.backend.supports?(:enqueue_now) %><%= button_to "Enqueue now", enqueue_scheduled_path(job.jid), method: :post, class: "rh-btn", form_class: "rh-inline" %><% end %>
|
|
28
28
|
<%= button_to "Delete", delete_scheduled_path(job.jid), method: :post, class: "rh-btn rh-btn-danger", form_class: "rh-inline", data: { turbo_confirm: "Delete #{job.jid}?" } %>
|
|
29
29
|
</td>
|
|
30
30
|
</tr>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<% content_for :title, "Settings" %>
|
|
2
|
+
|
|
3
|
+
<h2 class="rh-h2">Settings <span class="hint">stored in this browser only</span></h2>
|
|
4
|
+
|
|
5
|
+
<div class="rh-panel rh-settings">
|
|
6
|
+
<div class="rh-setting">
|
|
7
|
+
<div class="rh-setting-label">
|
|
8
|
+
<b>Appearance</b>
|
|
9
|
+
<span class="rh-sub">Roundhouse is dark unless you say otherwise — it does not follow the operating system.</span>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="rh-setting-control">
|
|
12
|
+
<select id="rh-set-theme" class="rh-btn" aria-label="Appearance">
|
|
13
|
+
<option value="dark">Dark</option>
|
|
14
|
+
<option value="light">Light</option>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<% if @palettes.any? %>
|
|
20
|
+
<div class="rh-setting">
|
|
21
|
+
<div class="rh-setting-label">
|
|
22
|
+
<b>Palette</b>
|
|
23
|
+
<span class="rh-sub">
|
|
24
|
+
Colours only.
|
|
25
|
+
<% if @host_theme %>“Default” is whatever this app configured.<% end %>
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="rh-setting-control">
|
|
29
|
+
<select id="rh-set-palette" class="rh-btn" aria-label="Palette">
|
|
30
|
+
<option value="">Default</option>
|
|
31
|
+
<%# A palette covering only one mode is inert in the other, so say which. %>
|
|
32
|
+
<% @palettes.each do |name, tokens| %>
|
|
33
|
+
<% only = RoundhouseUi::Theme.only_mode(tokens) %>
|
|
34
|
+
<option value="<%= name %>"><%= name.to_s.titleize %><%= " — #{only} only" if only %></option>
|
|
35
|
+
<% end %>
|
|
36
|
+
</select>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<div class="rh-setting">
|
|
42
|
+
<div class="rh-setting-label">
|
|
43
|
+
<b>Content width</b>
|
|
44
|
+
<span class="rh-sub">Full width helps on the wide tables; compact is easier to read.</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="rh-setting-control">
|
|
47
|
+
<select id="rh-set-width" class="rh-btn" aria-label="Content width">
|
|
48
|
+
<option value="compact">Compact</option>
|
|
49
|
+
<option value="full">Full width</option>
|
|
50
|
+
</select>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="rh-setting">
|
|
55
|
+
<div class="rh-setting-label">
|
|
56
|
+
<b>Refresh every</b>
|
|
57
|
+
<span class="rh-sub">
|
|
58
|
+
Each refresh re-runs this app's own auth and routing, so faster is not free.
|
|
59
|
+
This app's default is <%= @host_poll %>s.
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="rh-setting-control">
|
|
63
|
+
<select id="rh-set-poll" class="rh-btn" aria-label="Refresh interval">
|
|
64
|
+
<option value="">App default (<%= @host_poll %>s)</option>
|
|
65
|
+
<option value="2">2 seconds</option>
|
|
66
|
+
<option value="5">5 seconds</option>
|
|
67
|
+
<option value="10">10 seconds</option>
|
|
68
|
+
<option value="30">30 seconds</option>
|
|
69
|
+
<option value="60">1 minute</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="rh-setting">
|
|
75
|
+
<div class="rh-setting-label">
|
|
76
|
+
<b>Reset</b>
|
|
77
|
+
<span class="rh-sub">Forget every preference stored for this browser.</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="rh-setting-control">
|
|
80
|
+
<button type="button" id="rh-set-reset" class="rh-btn rh-btn-danger">Reset to defaults</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<p class="rh-note">
|
|
86
|
+
Preferences live in this browser's local storage — they are not shared with
|
|
87
|
+
other people, other devices, or the server. A private window starts fresh.
|
|
88
|
+
<% unless RoundhouseUi.allow_theme_selection %>
|
|
89
|
+
Palette selection is disabled for this install.
|
|
90
|
+
<% end %>
|
|
91
|
+
</p>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%# Two states, both above the table where the rows they explain are.
|
|
2
|
+
|
|
3
|
+
REFUSED: the search was not understood, so it selects nothing. Unexplained,
|
|
4
|
+
that is indistinguishable from "no jobs have this problem" — the reading that
|
|
5
|
+
gets someone to close the tab satisfied.
|
|
6
|
+
|
|
7
|
+
DEGRADED: a facet was dropped and the rest applied. Browsing proceeds, because
|
|
8
|
+
a typo should not stop you looking. Bulk actions do not, because the filter
|
|
9
|
+
that survived selects a superset of what was typed — see bulk_filter_present?.
|
|
10
|
+
|
|
11
|
+
Reuses .rh-warn rather than inventing a state: the bulk cap notice already
|
|
12
|
+
means "read this before you trust the count below". %>
|
|
13
|
+
<% if @filter&.invalid? %>
|
|
14
|
+
<div class="rh-warn" role="alert">
|
|
15
|
+
<%= @filter.message %>
|
|
16
|
+
<%= link_to "Clear the search", filter_url(q: nil), class: "rh-sub" %>
|
|
17
|
+
</div>
|
|
18
|
+
<% elsif @filter&.degraded? %>
|
|
19
|
+
<div class="rh-warn" role="status">
|
|
20
|
+
<%= @filter.notes.join(" ") %>
|
|
21
|
+
Showing what the rest of the search selects; bulk actions are off until it parses.
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<% if @page > 1 || @has_next %>
|
|
2
2
|
<div class="rh-pager">
|
|
3
3
|
<% if @page > 1 %>
|
|
4
|
-
<%= link_to "← Prev",
|
|
4
|
+
<%= link_to "← Prev", filter_url(page: @page - 1), class: "rh-btn" %>
|
|
5
5
|
<% else %>
|
|
6
6
|
<span class="rh-btn" style="opacity:.4; pointer-events:none">← Prev</span>
|
|
7
7
|
<% end %>
|
|
8
8
|
<span class="rh-sub">Page <%= @page %></span>
|
|
9
9
|
<% if @has_next %>
|
|
10
|
-
<%= link_to "Next →",
|
|
10
|
+
<%= link_to "Next →", filter_url(page: @page + 1), class: "rh-btn" %>
|
|
11
11
|
<% else %>
|
|
12
12
|
<span class="rh-btn" style="opacity:.4; pointer-events:none">Next →</span>
|
|
13
13
|
<% end %>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%# One control, and the only place the filter is stated.
|
|
2
|
+
|
|
3
|
+
It replaced four: this box (free text only), the squad chip row, a "Filtered to
|
|
4
|
+
… ×" strip, and the heading saying it a third time in prose. Four renderings of
|
|
5
|
+
one value, in four vocabularies, with two different "clear"s three rows apart.
|
|
6
|
+
|
|
7
|
+
Every active facet is a pill, server-rendered from what is ACTUALLY applied —
|
|
8
|
+
so a pill cannot show a filter that is not in force. That direction matters:
|
|
9
|
+
this bar sits above "delete all matching", and a pill that appeared before its
|
|
10
|
+
filter did would be the invisible-filter bug inverted. Each × links to the
|
|
11
|
+
without-that-one URL, so removing one facet never removes another.
|
|
12
|
+
|
|
13
|
+
The visible input holds only the free text; the facets ride in the hidden `qf`
|
|
14
|
+
beside it and are rejoined at the single parse point. One field per role, no
|
|
15
|
+
field that can carry "some of the filters". %>
|
|
16
|
+
<% filter = @filter || RoundhouseUi::FilterQuery.none %>
|
|
17
|
+
<% keys = local_assigns.fetch(:keys, RoundhouseUi::FilterQuery::KEYS) %>
|
|
18
|
+
<%# The placeholder is BUILT from `keys` (see filter_placeholder), so a hand-written
|
|
19
|
+
one is not honoured. Raising beats ignoring it: five stale hand-written hints is
|
|
20
|
+
what this replaced, and a sixth that silently did nothing would be worse. %>
|
|
21
|
+
<% raise ArgumentError, "search_bar builds its own placeholder from keys; pass noun: instead" if local_assigns.key?(:placeholder) %>
|
|
22
|
+
<form method="get" action="<%= action %>" class="rh-search" role="search">
|
|
23
|
+
<%# Bar and help are one input group: flush, one shared border, one focus ring. %>
|
|
24
|
+
<div class="rh-group">
|
|
25
|
+
<div class="rh-bar">
|
|
26
|
+
<%= icon(:search) %>
|
|
27
|
+
<% filter.chips.each do |key, value| %>
|
|
28
|
+
<span class="rh-pillf">
|
|
29
|
+
<span class="k"><%= key %></span>
|
|
30
|
+
<span class="v" title="<%= value %>"><%= value %></span>
|
|
31
|
+
<%= link_to "×", filter_url(key => nil, page: nil),
|
|
32
|
+
class: "x", title: "Remove the #{key} filter",
|
|
33
|
+
aria: { label: "Remove the #{key} filter" } %>
|
|
34
|
+
</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<input type="search" name="q" value="<%= filter.invalid? ? filter.raw : filter.text %>"
|
|
37
|
+
placeholder="<%= filter.any_facets? ? "narrow further…" : filter_placeholder(keys, noun: local_assigns.fetch(:noun, "text")) %>"
|
|
38
|
+
autocomplete="off" spellcheck="false"
|
|
39
|
+
data-rh-filter-keys="<%= keys.join(",") %>"
|
|
40
|
+
data-rh-filter-values="<%= filter_vocabulary.to_json %>"
|
|
41
|
+
aria-label="Filter and search">
|
|
42
|
+
<%# The facets the pills stand for. Serialized once, from the parsed filter —
|
|
43
|
+
never enumerated per key, which is how the confirm form came to carry four
|
|
44
|
+
of five and destroy more than its dry run displayed. %>
|
|
45
|
+
<% if filter.any_facets? %>
|
|
46
|
+
<input type="hidden" name="qf" value="<%= filter.facet_parts.join(" ") %>">
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if filter.any? %>
|
|
49
|
+
<%= link_to "clear", filter_url(q: nil, page: nil), class: "rh-bar-clear",
|
|
50
|
+
title: "Clear every filter" %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
<%= render "roundhouse_ui/shared/search_help", keys: keys %>
|
|
54
|
+
</div>
|
|
55
|
+
</form>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%# How to use the box, revealed on hover — and on focus, so it is reachable by
|
|
2
|
+
keyboard and on touch, where hover does not exist.
|
|
3
|
+
|
|
4
|
+
It earns its place because the grammar REFUSES what it does not understand
|
|
5
|
+
rather than falling back to a substring search. A refusal you cannot act on is
|
|
6
|
+
a dead end, so the vocabulary has to be one hover away from the box that needs
|
|
7
|
+
it. Every example below is copy-pasteable as written.
|
|
8
|
+
|
|
9
|
+
No JS: CSS :hover / :focus-within only. A popover that needs a script is a
|
|
10
|
+
popover that stops opening the day a nonce slips. %>
|
|
11
|
+
<% keys = local_assigns.fetch(:keys, RoundhouseUi::FilterQuery::KEYS) %>
|
|
12
|
+
<div class="rh-help">
|
|
13
|
+
<button type="button" aria-label="How to search" aria-describedby="rh-help-panel">?</button>
|
|
14
|
+
<div class="rh-help-panel" id="rh-help-panel" role="tooltip">
|
|
15
|
+
<h4>Filters narrow. Text searches.</h4>
|
|
16
|
+
<dl>
|
|
17
|
+
<% if keys.include?("class") %><dt>class=</dt><dd>exact job class — <code>class=Billing::SyncWorker</code></dd><% end %>
|
|
18
|
+
<% if keys.include?("error") %><dt>error=</dt><dd>exact error class — <code>error=Timeout::Error</code></dd><% end %>
|
|
19
|
+
<% if keys.include?("queue") %><dt>queue=</dt><dd>exact queue name — <code>queue=default</code></dd><% end %>
|
|
20
|
+
<% if RoundhouseUi.job_tags && keys.include?("tag") %>
|
|
21
|
+
<dt>tag=</dt><dd>a declared tag, as <code>key:value</code> — <code>tag=squad:core</code></dd>
|
|
22
|
+
<% end %>
|
|
23
|
+
<dt>anything else</dt><dd>substring, across class, jid, error message and redacted arguments</dd>
|
|
24
|
+
</dl>
|
|
25
|
+
<dl style="margin-top:9px">
|
|
26
|
+
<dt>%</dt><dd>wildcard — <code>class=Roundhouse%</code>, <code>class=%Worker</code>, <code>class=%oundhouse%</code></dd>
|
|
27
|
+
</dl>
|
|
28
|
+
<p class="rh-help-note">
|
|
29
|
+
Combine them: <code>error=KeyError queue=ai stripe</code> means that error, on
|
|
30
|
+
that queue, mentioning “stripe”. Without a <code>%</code> a filter matches
|
|
31
|
+
<strong>exactly</strong>, so <code>queue=default</code> never also selects
|
|
32
|
+
<code>default_low</code>. <code>_</code> is a literal, not a wildcard. Quote
|
|
33
|
+
values with spaces: <code>error="Net::ReadTimeout with body"</code>. For text
|
|
34
|
+
that looks like a filter, use <code>text="account_id=1234"</code>.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -14,16 +14,14 @@
|
|
|
14
14
|
<% values = values.sort_by { |_, count| -count } if counts %>
|
|
15
15
|
<% values.each do |value, count| %>
|
|
16
16
|
<% active = @tag == [ key.to_s, value.to_s ] %>
|
|
17
|
-
<%= link_to
|
|
18
|
-
tag: (active ? nil : "#{key}:#{value}"),
|
|
19
|
-
queue: params[:queue].presence),
|
|
17
|
+
<%= link_to filter_url(page: nil, tag: (active ? nil : "#{key}:#{value}")),
|
|
20
18
|
class: "rh-qf#{' is-on' if active}", title: "#{key}: #{value}" do %>
|
|
21
19
|
<%= value %><% if count %><span class="rh-qf-n"><%= number_with_delimiter count %></span><% end %>
|
|
22
20
|
<% end %>
|
|
23
21
|
<% end %>
|
|
24
22
|
<% end %>
|
|
25
23
|
<% if @tag %>
|
|
26
|
-
<%= link_to "clear",
|
|
24
|
+
<%= link_to "clear", filter_url(tag: nil, page: nil), class: "rh-sub" %>
|
|
27
25
|
<% end %>
|
|
28
26
|
</div>
|
|
29
27
|
<% end %>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<% content_for :title, "Confirm bulk #{@op}" %>
|
|
2
|
+
|
|
3
|
+
<h2 class="rh-h2">
|
|
4
|
+
<%= @op == "delete" ? "Delete" : "Retry" %>
|
|
5
|
+
<%= number_with_delimiter @matched.entries.size %>
|
|
6
|
+
<%= @noun.pluralize(@matched.entries.size) %>?
|
|
7
|
+
<span class="hint"><%= filter_description(@query, @tag) %></span>
|
|
8
|
+
</h2>
|
|
9
|
+
|
|
10
|
+
<% if @matched.entries.empty? %>
|
|
11
|
+
<div class="rh-panel">
|
|
12
|
+
<p class="rh-empty">Nothing matches <%= filter_description(@query, @tag) %>. Nothing to do.</p>
|
|
13
|
+
</div>
|
|
14
|
+
<p><%= link_to "← Back", @back_path, class: "rh-btn" %></p>
|
|
15
|
+
<% else %>
|
|
16
|
+
<% if @matched.capped %>
|
|
17
|
+
<div class="rh-warn">
|
|
18
|
+
Stopped at the <%= RoundhouseUi::JobSetBrowsing::BULK_CAP %>-job cap. More jobs match than
|
|
19
|
+
are listed here — this action will touch the <%= number_with_delimiter @matched.entries.size %>
|
|
20
|
+
below, and you can run it again for the rest.
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<%# The confirm form carries EVERY active filter, not a hand-picked four. It
|
|
25
|
+
listed op, q, tag and queue — and not class or error — so a dry run showing
|
|
26
|
+
two jobs POSTed a request that deleted five, and reported "Deleted 5
|
|
27
|
+
matching job(s)" as though that had been approved. %>
|
|
28
|
+
<div class="rh-toolbar">
|
|
29
|
+
<%= button_to "#{@op == "delete" ? "✕ Delete" : "↻ Retry"} #{number_with_delimiter @matched.entries.size} #{@noun.pluralize(@matched.entries.size)}",
|
|
30
|
+
@confirm_path,
|
|
31
|
+
method: :post,
|
|
32
|
+
params: active_filters.merge(op: @op),
|
|
33
|
+
class: "rh-btn #{'rh-btn-danger' if @op == 'delete'}",
|
|
34
|
+
form_class: "rh-inline" %>
|
|
35
|
+
<%= link_to "Cancel", @back_path, class: "rh-btn" %>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<table class="rh-table">
|
|
39
|
+
<thead>
|
|
40
|
+
<tr>
|
|
41
|
+
<th>Job</th>
|
|
42
|
+
<% if tag_column? %><th><%= tag_column_label %></th><% end %>
|
|
43
|
+
<th>Queue</th>
|
|
44
|
+
<th>Arguments</th>
|
|
45
|
+
<th>Last error</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
<%# Only the first 100 are listed; the count above is what will be touched. %>
|
|
50
|
+
<% @matched.entries.first(100).each do |entry| %>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><%= job_identity(job_display_class(entry), entry.jid, job_path(set: @set, jid: entry.jid)) %></td>
|
|
53
|
+
<% if tag_column? %><td><%= tag_cell(tags_for(entry)) %></td><% end %>
|
|
54
|
+
<td><%= queue_pill(entry.queue) %></td>
|
|
55
|
+
<td><%= job_args_preview(entry) %></td>
|
|
56
|
+
<td><span class="rh-err"><%= entry.item["error_class"] %></span></td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
|
|
62
|
+
<% if @matched.entries.size > 100 %>
|
|
63
|
+
<p class="rh-note">
|
|
64
|
+
Showing the first 100 of <%= number_with_delimiter @matched.entries.size %>.
|
|
65
|
+
All <%= number_with_delimiter @matched.entries.size %> will be
|
|
66
|
+
<%= @op == "delete" ? "deleted" : "retried" %>.
|
|
67
|
+
</p>
|
|
68
|
+
<% end %>
|
|
69
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -17,8 +17,15 @@ RoundhouseUi::Engine.routes.draw do
|
|
|
17
17
|
post "queues/:name/pause" => "queues#pause", as: :pause_queue
|
|
18
18
|
post "queues/:name/resume" => "queues#resume", as: :resume_queue
|
|
19
19
|
post "queues/:name/snapshot" => "queues#snapshot", as: :snapshot_queue
|
|
20
|
+
# Last in the scope so it cannot shadow the POST actions above.
|
|
21
|
+
get "queues/:name" => "queues#show", as: :queue
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
get "settings" => "settings#show", as: :settings
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
get "recurring" => "recurring#index", as: :recurring
|
|
28
|
+
|
|
22
29
|
get "snapshots" => "snapshots#index", as: :snapshots
|
|
23
30
|
scope constraints: { id: /[^\/]+/ } do
|
|
24
31
|
post "snapshots/:id/restore" => "snapshots#restore", as: :restore_snapshot
|
|
@@ -30,12 +37,14 @@ RoundhouseUi::Engine.routes.draw do
|
|
|
30
37
|
post "scheduled/:jid/delete" => "scheduled#destroy", as: :delete_scheduled
|
|
31
38
|
|
|
32
39
|
get "retries" => "retries#index", as: :retries
|
|
40
|
+
get "retries/preview" => "retries#preview", as: :preview_bulk_retries
|
|
33
41
|
post "retries/bulk_all" => "retries#bulk_all", as: :bulk_all_retries
|
|
34
42
|
post "retries/:jid/run" => "retries#requeue", as: :run_retry
|
|
35
43
|
post "retries/:jid/delete" => "retries#destroy", as: :delete_retry
|
|
36
44
|
|
|
37
45
|
get "dead" => "dead#index", as: :dead_set
|
|
38
46
|
post "dead/bulk" => "dead#bulk", as: :bulk_dead
|
|
47
|
+
get "dead/preview" => "dead#preview", as: :preview_bulk_dead
|
|
39
48
|
post "dead/bulk_all" => "dead#bulk_all", as: :bulk_all_dead
|
|
40
49
|
post "dead/:jid/retry" => "dead#requeue", as: :retry_dead_job
|
|
41
50
|
post "dead/:jid/delete" => "dead#destroy", as: :delete_dead_job
|