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,25 +1,134 @@
|
|
|
1
1
|
module RoundhouseUi
|
|
2
|
+
# Links out to whatever APM the host configured. Four entry points — job or
|
|
3
|
+
# error, by historical call-site name — and exactly one appearance. They were
|
|
4
|
+
# four hand-rolled copies once, and only some got updated: two still emitted a
|
|
5
|
+
# literal ↗ after the rest moved to SVG. Later the split survived as two
|
|
6
|
+
# different shapes, a text link and a bordered box, which is not "one
|
|
7
|
+
# appearance" however the helper described itself.
|
|
2
8
|
module ObservabilityHelper
|
|
3
|
-
# Renders a deep-link to the configured observability tool for a job, or
|
|
4
|
-
# nothing when no adapter is configured (the default).
|
|
5
9
|
def trace_link(klass:, jid:, queue: nil)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return unless url
|
|
10
|
+
trace_affordance(job_trace_url(klass: klass, jid: jid, queue: queue), as: :mark)
|
|
11
|
+
end
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
def trace_icon(klass:, jid:, queue: nil)
|
|
14
|
+
trace_affordance(job_trace_url(klass: klass, jid: jid, queue: queue), as: :control)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
|
-
# Deep-link for a grouped error row (no single JID) — a class-wide search.
|
|
14
|
-
# respond_to? keeps older/custom adapters that lack error_url working.
|
|
15
17
|
def error_trace_link(klass:, error: nil)
|
|
18
|
+
trace_affordance(error_trace_url(klass: klass, error: error), as: :mark)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error_trace_icon(klass:, error: nil)
|
|
22
|
+
trace_affordance(error_trace_url(klass: klass, error: error), as: :control, compact: true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Says once, above a table, where every row's trace glyph goes. Takes the rows
|
|
26
|
+
# it is describing, because a legend above an empty table explains a control
|
|
27
|
+
# that is not on the page — which is exactly what "Nothing scheduled" under
|
|
28
|
+
# "Traces open in Datadog" looked like.
|
|
29
|
+
#
|
|
30
|
+
# Returns nil unless the adapter has a mark of its own, too: a plain adapter
|
|
31
|
+
# already labels every row, so there would be nothing to hoist.
|
|
32
|
+
def trace_legend(rows)
|
|
33
|
+
return nil if rows.blank?
|
|
34
|
+
|
|
35
|
+
adapter = RoundhouseUi.observability
|
|
36
|
+
return nil unless wordmark?(adapter)
|
|
37
|
+
return nil if adapter.job_url(klass: "Probe", jid: "probe").blank?
|
|
38
|
+
|
|
39
|
+
content_tag(:div, class: "rh-trace-legend") do
|
|
40
|
+
safe_join([ content_tag(:span, "Traces open in"), trace_glyph(adapter) ], " ")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# An adapter's own mark, when it supplies one. Three shapes are accepted, in
|
|
45
|
+
# descending order of how much we trust them:
|
|
46
|
+
#
|
|
47
|
+
# :name → one of Roundhouse's shipped icons
|
|
48
|
+
# "<svg …>" → the adapter's markup, rendered as-is
|
|
49
|
+
# nil → the default trace glyph
|
|
50
|
+
#
|
|
51
|
+
# The markup case is not escaped, because escaping renders the tags as text
|
|
52
|
+
# and the feature does not work at all. That is the same trust level as the
|
|
53
|
+
# adapter object itself, which can already run arbitrary code. Anything that
|
|
54
|
+
# is neither markup nor a known name falls back rather than printing stray
|
|
55
|
+
# text into a table.
|
|
56
|
+
def trace_glyph(adapter)
|
|
57
|
+
supplied = adapter.respond_to?(:icon) ? adapter.icon : nil
|
|
58
|
+
return icon(:trace_out) if supplied.blank?
|
|
59
|
+
return icon(supplied) if supplied.is_a?(Symbol)
|
|
60
|
+
|
|
61
|
+
supplied.to_s.strip.start_with?("<") ? supplied.to_s.html_safe : icon(:trace_out)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def job_trace_url(klass:, jid:, queue: nil)
|
|
67
|
+
RoundhouseUi.observability.job_url(klass: klass, jid: jid, queue: queue)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def error_trace_url(klass:, error: nil)
|
|
71
|
+
adapter = RoundhouseUi.observability
|
|
72
|
+
return nil unless adapter.respond_to?(:error_url)
|
|
73
|
+
|
|
74
|
+
adapter.error_url(klass: klass, error: error)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Two contexts, one principle: a dense list gets a control, a single item
|
|
78
|
+
# gets the vendor's mark.
|
|
79
|
+
#
|
|
80
|
+
# In a table the mark would repeat on every row — 366 copies of someone
|
|
81
|
+
# else's logo on one Retries page, and the only unboxed thing in a column of
|
|
82
|
+
# bordered buttons. So rows get a Roundhouse glyph shaped exactly like the
|
|
83
|
+
# buttons beside it, and the page says once, above the table, where that
|
|
84
|
+
# glyph goes. On a page showing one job there is nothing to tile, so the
|
|
85
|
+
# mark itself goes inline where it has room to read.
|
|
86
|
+
#
|
|
87
|
+
# This is not the old `labelled` split, which was two arbitrary shapes with
|
|
88
|
+
# no stated rule. The distinction here is repetition, and `trace_legend` is
|
|
89
|
+
# what makes the row glyph legible without restating the brand per row.
|
|
90
|
+
def trace_affordance(url, as:, compact: false)
|
|
91
|
+
return nil if url.blank?
|
|
92
|
+
|
|
16
93
|
adapter = RoundhouseUi.observability
|
|
17
|
-
return
|
|
94
|
+
return trace_control(url, adapter, compact: compact) if as == :control
|
|
18
95
|
|
|
19
|
-
|
|
20
|
-
|
|
96
|
+
link_to trace_body(adapter), url, **trace_attrs(adapter),
|
|
97
|
+
class: "rh-trace#{' rh-trace-mark' if wordmark?(adapter)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Shaped like whatever it sits beside. In an Actions column that is .rh-btn,
|
|
101
|
+
# so it lands level with Run now and Delete. Inline in a text cell it is
|
|
102
|
+
# .rh-runbook's smaller pill, because a 28px-tall button next to an 11.5px
|
|
103
|
+
# "runbook" link puts three different heights in one table cell.
|
|
104
|
+
#
|
|
105
|
+
# Never carries the vendor mark either way: boxing a logo is against most
|
|
106
|
+
# brand guidelines, Datadog's included.
|
|
107
|
+
def trace_control(url, adapter, compact: false)
|
|
108
|
+
# One class. Borrowing .rh-btn's or .rh-runbook's box was the previous fix
|
|
109
|
+
# for a 2px drift, and it still left two definitions to keep in step — the
|
|
110
|
+
# compact variant came out 2px taller than the runbook pill it sat beside.
|
|
111
|
+
# The control scale (#65) gives every control the same box from the same
|
|
112
|
+
# tokens, so there is nothing to borrow and nothing to synchronise.
|
|
113
|
+
base = compact ? "rh-trace-btn rh-btn--sm" : "rh-trace-btn"
|
|
114
|
+
link_to icon(:trace_out), url, **trace_attrs(adapter), class: base
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def trace_body(adapter)
|
|
118
|
+
glyph = trace_glyph(adapter)
|
|
119
|
+
return glyph if wordmark?(adapter)
|
|
120
|
+
|
|
121
|
+
safe_join([ glyph, content_tag(:span, adapter.label) ])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def trace_attrs(adapter)
|
|
125
|
+
{ target: "_blank", rel: "noopener noreferrer",
|
|
126
|
+
title: "Open in #{adapter.label}",
|
|
127
|
+
aria: { label: "Open in #{adapter.label}" } }
|
|
128
|
+
end
|
|
21
129
|
|
|
22
|
-
|
|
130
|
+
def wordmark?(adapter)
|
|
131
|
+
adapter.respond_to?(:wordmark?) && adapter.wordmark?
|
|
23
132
|
end
|
|
24
133
|
end
|
|
25
134
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module RoundhouseUi
|
|
2
|
+
# Renders host-defined job tags (see ADR 0002). Resolution is memoized for the
|
|
3
|
+
# life of the request: a page can render hundreds of rows, and ErrorGroups
|
|
4
|
+
# scans up to DEFAULT_SCAN_LIMIT entries, so the host's resolver must not be
|
|
5
|
+
# called once per row when it only varies by class.
|
|
6
|
+
module TagsHelper
|
|
7
|
+
# The per-request memo handed to Tags.for, shared with JobSetBrowsing so a
|
|
8
|
+
# class (or job) resolved while scanning is not resolved again when its
|
|
9
|
+
# badge renders. Tags.for chooses the key: class name normally, jid in
|
|
10
|
+
# per-job mode.
|
|
11
|
+
def tag_cache
|
|
12
|
+
@rh_tag_cache ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Tags for one job entry, as a { "key" => "value" } Hash.
|
|
16
|
+
def tags_for(entry)
|
|
17
|
+
return Tags::EMPTY unless RoundhouseUi.job_tags
|
|
18
|
+
|
|
19
|
+
Tags.for(klass: entry.klass, item: entry.item, cache: tag_cache)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Tags for a class name alone — used by grouped Errors, where every entry in
|
|
23
|
+
# a group shares a class, so a class-derived tag is constant for the group.
|
|
24
|
+
# Passes no payload, so a per-job resolver correctly declines rather than
|
|
25
|
+
# attributing one job's tags to the whole group.
|
|
26
|
+
def tags_for_class(klass)
|
|
27
|
+
return Tags::EMPTY unless RoundhouseUi.job_tags
|
|
28
|
+
|
|
29
|
+
Tags.for(klass: klass, item: {}, cache: tag_cache)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Pills for a resolved tag Hash. Renders nothing when there are no tags, so
|
|
33
|
+
# every call site can be unconditional.
|
|
34
|
+
def tag_badges(tags)
|
|
35
|
+
return if tags.blank?
|
|
36
|
+
|
|
37
|
+
safe_join(tags.map { |key, value| tag_badge(key, value) }, " ")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Tags get their own table column rather than an inline badge: class names
|
|
41
|
+
# vary wildly in length, so inline badges land at ragged x-positions and
|
|
42
|
+
# can't be scanned down. Only worth a column when a host configured tags.
|
|
43
|
+
def tag_column?
|
|
44
|
+
RoundhouseUi.job_tags.present?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Header for that column. With the usual single dimension this is the tag's
|
|
48
|
+
# own name ("squad"); with several there's no one right label.
|
|
49
|
+
def tag_column_label
|
|
50
|
+
keys = Tags.filters&.keys
|
|
51
|
+
keys&.one? ? keys.first.titleize : "Tags"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Cell contents. Stacks when a host defines more than one dimension.
|
|
55
|
+
def tag_cell(tags)
|
|
56
|
+
return content_tag(:span, "—", class: "rh-sub") if tags.blank?
|
|
57
|
+
|
|
58
|
+
tag_badges(tags)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Values offered as filter chips where no counts are available. Prefers the
|
|
62
|
+
# host's declared vocabulary, so the chips stay put as you filter; falls back
|
|
63
|
+
# to whatever the visible rows happen to carry, which at least beats nothing
|
|
64
|
+
# but shifts as you page.
|
|
65
|
+
def tag_vocabulary
|
|
66
|
+
declared = Tags.filters
|
|
67
|
+
return declared if declared.present?
|
|
68
|
+
|
|
69
|
+
seen = Hash.new { |h, k| h[k] = [] }
|
|
70
|
+
Array(@jobs).each do |job|
|
|
71
|
+
tags_for(job).each { |key, value| seen[key] << value unless seen[key].include?(value) }
|
|
72
|
+
end
|
|
73
|
+
seen.transform_values(&:sort)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Human description of the active filter set, so a bulk confirm names every
|
|
77
|
+
# constraint that will be applied — not just the text query.
|
|
78
|
+
# Every active constraint must appear here. A confirm that names only the
|
|
79
|
+
# text query while a queue or tag also narrows the set understates what is
|
|
80
|
+
# about to be destroyed.
|
|
81
|
+
# `filter` is the controller's parse, reached through the helper. One source for
|
|
82
|
+
# the prose, the predicate and the pill — a description built from a second
|
|
83
|
+
# reading of the filter is a description that can disagree with what was applied.
|
|
84
|
+
def rh_filter
|
|
85
|
+
controller.respond_to?(:filter, true) ? controller.send(:filter) : RoundhouseUi::FilterQuery.none
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def filter_description(query, tag, queue = rh_filter.queue)
|
|
89
|
+
parts = []
|
|
90
|
+
parts << "matching “#{query}”" if query.present?
|
|
91
|
+
parts << "tagged #{tag[0]}: #{tag[1]}" if tag
|
|
92
|
+
parts << "in queue #{queue}" if queue.present?
|
|
93
|
+
parts << like_description if rh_filter.klass.present? || rh_filter.error.present?
|
|
94
|
+
parts.join(" and ")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# "find more like this", said back to you. The wording has to be exact about
|
|
98
|
+
# what it selected, because the bulk Delete sitting next to it acts on
|
|
99
|
+
# precisely this set and nothing wider.
|
|
100
|
+
def like_description
|
|
101
|
+
# Either half stands on its own. "every Timeout::Error, whatever the class"
|
|
102
|
+
# is a question worth asking, so clearing the class must not take the error
|
|
103
|
+
# with it — which is what it used to do.
|
|
104
|
+
return "failing with #{rh_filter.error}" if rh_filter.klass.blank?
|
|
105
|
+
return "like #{rh_filter.klass}" if rh_filter.error.blank?
|
|
106
|
+
|
|
107
|
+
"like #{rh_filter.klass} failing with #{rh_filter.error}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Any filter active? Bulk-on-match is filter-gated, so this decides whether
|
|
111
|
+
# the bulk bar renders at all — and which empty-state copy is honest.
|
|
112
|
+
# Delegates to the concern's predicate where it is available, so the button the
|
|
113
|
+
# view offers and the scope the route enforces cannot drift apart. They did:
|
|
114
|
+
# this returned true only for the view, while bulk_all had no gate at all.
|
|
115
|
+
def any_filter?(query, tag, queue = rh_filter.queue)
|
|
116
|
+
return controller.send(:bulk_filter_present?, query, tag) if controller.respond_to?(:bulk_filter_present?, true)
|
|
117
|
+
|
|
118
|
+
query.present? || !tag.nil? || queue.present? ||
|
|
119
|
+
rh_filter.klass.present? || rh_filter.error.present?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Value only — the key is near-constant down a column, so repeating it is
|
|
123
|
+
# noise. It stays in the tooltip for hosts with more than one dimension.
|
|
124
|
+
def tag_badge(key, value)
|
|
125
|
+
content_tag(:span, value, class: "rh-pill rh-pill-tag", title: "#{key}: #{value}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|