Module: RoundhouseUi::ApplicationHelper
- Defined in:
- app/helpers/roundhouse_ui/application_helper.rb
Constant Summary collapse
- FACET_OVERRIDES =
Override keys that name a facet, and the FilterQuery field each one sets. Anything not listed here is transport (page, op) and passes straight through.
{ class: :klass, error: :error, queue: :queue, tag: :tag }.freeze
- LIKE_PATHS =
Where "find more like this" goes, per set.
{ "dead" => :dead_set_path, "retry" => :retries_path, "scheduled" => :scheduled_path }.freeze
Instance Method Summary collapse
-
#attempt_ladder(count, max = 25) ⇒ Object
Retry attempt as a ladder rather than a number.
-
#countdown(at, since: nil, label: nil) ⇒ Object
How far through the wait a scheduled or retrying job is, as a ring.
-
#duration(seconds) ⇒ Object
Compact duration.
- #duration_ms(ms) ⇒ Object
-
#empty_state(query, tag, all_clear:, noun: "jobs") ⇒ Object
What an empty table says.
- #filter_params(overrides = {}) ⇒ Object
-
#filter_placeholder(keys = FilterQuery::KEYS, noun: "text") ⇒ Object
The placeholder, BUILT from the facets the page honours rather than written out per page.
- #filter_url(overrides = {}) ⇒ Object
-
#filter_vocabulary ⇒ Object
What the bar can complete.
-
#filtered_view?(query, tag) ⇒ Boolean
Is the page showing a SUBSET of the set? A different question from any_filter?, which asks whether a filter may authorise a bulk action β and answers no for a refused query, deliberately and correctly.
-
#find_like_link(set, job) ⇒ Object
"Find more like this": the same class, and where the set records one, the same error β the pair the Errors page already treats as one issue, so a row here and a row there mean the same thing.
-
#icon(name, extra_class: nil) ⇒ Object
An icon by name.
-
#job_args_preview(entry, length: 70) ⇒ Object
A one-line, redacted preview of a job's arguments.
-
#job_display_class(entry) ⇒ Object
The class name a row should show: the real job class, not the ActiveJob adapter's wrapper.
-
#job_editing? ⇒ Boolean
A URL that keeps every active filter, naming only what changes.
-
#job_identity(klass, jid, path) ⇒ Object
A job row's identity cell.
-
#job_time(at, overdue: "now (overdue)") ⇒ Object
Relative time answers "is this soon?", the clock time answers "does that land inside the maintenance window?".
-
#queue_pill(name, link: false) ⇒ Object
Queues carry meaning at a glance (critical vs low), so render them as a pill rather than grey text lost between two columns.
-
#runbook_link(klass, item = nil, label: "Runbook") ⇒ Object
The runbook for a job, as a link, or nil when the host declared none (#39).
-
#set_heading(label, showing:, total:, query: nil, tag: nil) ⇒ Object
Set heading that tells the truth under a filter.
Instance Method Details
#attempt_ladder(count, max = 25) ⇒ Object
Retry attempt as a ladder rather than a number. "retry 21" and "retry 3" are the same shape of text; twenty-one filled rungs going red is not. The count stays beside it, because a ladder answers "how bad" and not "how many".
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 222 def attempt_ladder(count, max = 25) filled = count.to_i.clamp(0, max) rungs = Array.new(max) do |i| state = if i >= filled then nil elsif filled > (max * 0.6) then "is-hot" else "is-on" end content_tag(:i, "", class: state) end safe_join([ content_tag(:span, safe_join(rungs), class: "rh-ladder", title: "attempt #{filled} of #{max}", aria: { hidden: true }), content_tag(:span, "#{filled}/#{max}", class: "rh-sub rh-ladder-n") ]) end |
#countdown(at, since: nil, label: nil) ⇒ Object
How far through the wait a scheduled or retrying job is, as a ring. A job about to fire should not look like one parked for six hours.
since is when the wait started; without it the ring cannot know the
fraction and renders empty rather than guessing.
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 243 def countdown(at, since: nil, label: nil) return content_tag(:span, "β", class: "rh-sub") if at.nil? remaining = at.to_f - Time.now.to_f turn = if since && (total = at.to_f - since.to_f) > 0 (1.0 - (remaining / total)).clamp(0.0, 1.0) else remaining <= 0 ? 1.0 : 0.0 end safe_join([ content_tag(:span, "", class: "rh-ring", style: "--rh-turn:#{turn.round(3)}turn", aria: { hidden: true }), content_tag(:span, label || job_time(at, overdue: "now"), class: "rh-sub") ]) end |
#duration(seconds) ⇒ Object
Compact duration. Raw seconds stop being readable somewhere around a minute and are actively useless by four figures β "10058.0s" is nearly three hours and nobody reads it as that. Two units is all anyone acts on. See #31. Both delegate to RoundhouseUi so views and lib/ cannot drift apart again.
216 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 216 def duration(seconds) = RoundhouseUi.duration(seconds) |
#duration_ms(ms) ⇒ Object
217 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 217 def duration_ms(ms) = RoundhouseUi.duration_ms(ms) |
#empty_state(query, tag, all_clear:, noun: "jobs") ⇒ Object
What an empty table says. Three cases, not two: the search was refused, the surviving filter matched nothing, or the set really is empty. Only the third gets the π β it used to get it in all three.
174 175 176 177 178 179 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 174 def empty_state(query, tag, all_clear:, noun: "jobs") return "Nothing matched β the search above was not understood." if @filter&.invalid? return "No #{noun} #{filter_description(query, tag)}." if filtered_view?(query, tag) all_clear end |
#filter_params(overrides = {}) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 37 def filter_params(overrides = {}) return overrides.compact unless respond_to?(:active_filters) # A facet override edits the QUERY, it does not add a parameter. The filter # now travels as one `?q=` string, so `filter_url(class: nil)` had to stop # meaning "send class=nil alongside q" β which would have left the class # sitting inside q, untouched, while the link claimed to clear it. Every Γ # in the console is one of these calls. query = @filter || FilterQuery.none transport = {} overrides.each do |key, value| # `q:` replaces the whole query β the one override that is not a facet edit. # Left in transport it would have emitted a second q beside the real one. next query = FilterQuery.parse(value) if key == :q field = FACET_OVERRIDES[key] field ? query = query.merge(field => value) : transport[key] = value end { q: query.to_s.presence }.compact.merge(transport.compact) end |
#filter_placeholder(keys = FilterQuery::KEYS, noun: "text") ⇒ Object
The placeholder, BUILT from the facets the page honours rather than written out
per page. Five pages had five hand-written strings β "search class, jid, error,
or arg valueβ¦", "filter by job class, error, or squadβ¦", "filter queues by
nameβ¦" β none of which mentioned that class= was a thing you could type. A
hand-written hint next to a grammar is a hint that goes stale the day the
grammar changes, and it went stale the day the grammar shipped.
119 120 121 122 123 124 125 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 119 def filter_placeholder(keys = FilterQuery::KEYS, noun: "text") facets = keys.reject { |k| k == "text" } facets -= [ "tag" ] unless RoundhouseUi. return "search #{noun}β¦" if facets.empty? "#{facets.map { |k| "#{k}=" }.join(" ")} or just type to search #{noun}" end |
#filter_url(overrides = {}) ⇒ Object
59 60 61 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 59 def filter_url(overrides = {}) url_for(filter_params(overrides).merge(only_path: true)) end |
#filter_vocabulary ⇒ Object
What the bar can complete. Keys always; tag values from the host's DECLARED vocabulary and queue names from the set the page already knows about β both free, no extra scan.
class= and error= values are deliberately absent: enumerating them means
reading every entry in the set on every render, and the whole reason browse
reads only one page is that a 50k dead set must stay cheap to open. Type them;
the funnel on each row fills them in for you.
135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 135 def filter_vocabulary vocab = {} declared = RoundhouseUi::Tags.filters if declared.present? pairs = declared.flat_map { |key, values| Array(values).map { |v| "#{key}:#{v}" } } vocab["tag"] = pairs if pairs.any? end names = Array(@queues).map { |q| q.respond_to?(:name) ? q.name.to_s : q.to_s }.reject(&:empty?) names = [ @name.to_s ] if names.empty? && @name.present? vocab["queue"] = names.uniq.sort if names.any? vocab end |
#filtered_view?(query, tag) ⇒ Boolean
Is the page showing a SUBSET of the set? A different question from any_filter?, which asks whether a filter may authorise a bulk action β and answers no for a refused query, deliberately and correctly.
Reusing the authorisation predicate for display made a refusal render as
"nothing is filtered". Every typo now refuses, where once only a 500-character
query did, so clas=Foo in the box produced the heading "Dead set Β· 24 jobs"
and the cell "Dead set is empty π" β above twenty-four dead jobs, with a
tick of congratulation. Found by looking at the running page; no test asked.
Asks the FILTER, never the bulk gate. Routing display through any_filter?
produced the same bug twice: a refused query rendered as "nothing filtered"
(heading "Dead set Β· 24 jobs" over "Dead set is empty π"), and then a degraded
one did too β tag=garbage queue=ai drops the tag, applies the queue, and
still claimed the whole set. any_filter? answers "may this authorise a bulk
action" and correctly says no in both cases; it is not a display predicate and
is no longer used as one.
164 165 166 167 168 169 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 164 def filtered_view?(query, tag) return true if @filter&.invalid? # selects nothing, which is a subset return @filter.any? if @filter # whatever SURVIVED parsing, dropped facets and all any_filter?(query, tag) end |
#find_like_link(set, job) ⇒ Object
"Find more like this": the same class, and where the set records one, the same error β the pair the Errors page already treats as one issue, so a row here and a row there mean the same thing.
Emitted as exact class=/error= facets in ?q= β the same string you could type. No %, because this button reveals "delete all matching".
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 73 def find_like_link(set, job) helper = LIKE_PATHS[set.to_s] klass = job_display_class(job) return nil if helper.nil? || klass.blank? error = job.item["error_class"].presence label = error ? "Find more #{klass} failing with #{error}" : "Find more #{klass}" # Built through from_params, not build: build does not validate, so a class # name carrying a quote would produce a link whose own q= the next request # refuses β a control that visibly does nothing. No representable filter, # no button. # Braced: from_params takes a keyword now, so a bare `class:` would be read as # one and leave the positional hash empty. filter = FilterQuery.from_params({ class: klass, error: error }) return nil if filter.invalid? query = { q: filter.to_s } # No size modifier: it takes the page scale, like every other control in the # Actions column it sits in. Asking for --sm here was picking a scale step by # hand next to 30px siblings β the scale stopped arbitrary pixels, it could # not stop me choosing the wrong one of the two it offers. link_to icon(:filter), send(helper, **query), class: "rh-btn rh-btn--icon", title: label, aria: { label: label } end |
#icon(name, extra_class: nil) ⇒ Object
An icon by name. Inline SVG by default; a class name when the host has its own icon font. Only our own SVG constants are marked html_safe β a host-supplied class name goes through content_tag and is escaped.
262 263 264 265 266 267 268 269 270 271 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 262 def icon(name, extra_class: nil) markup = RoundhouseUi::Icons.markup(name) return "".html_safe unless markup if markup.start_with?("<svg") content_tag(:span, markup.html_safe, class: [ "rh-ico", extra_class ].compact.join(" ")) else content_tag(:i, "", class: [ markup, "rh-ico", extra_class ].compact.join(" "), aria: { hidden: true }) end end |
#job_args_preview(entry, length: 70) ⇒ Object
A one-line, redacted preview of a job's arguments. Sidekiq Web shows args in its queue listing and we did not, which made a queue of one class indistinguishable row to row β the whole question being "which of these is the one I care about".
Truncated on purpose: the job page is where the full payload belongs, and a queue listing left open on a second monitor is the wrong place for a long arguments dump. Redaction still applies, but it is key-based, so a bare positional secret is not masked β same caveat as everywhere else args show.
204 205 206 207 208 209 210 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 204 def job_args_preview(entry, length: 70) args = entry.args return content_tag(:span, "β", class: "rh-sub") if args.blank? full = RoundhouseUi::Redaction.apply(args).map { |a| a.is_a?(String) ? a : a.inspect }.join(", ") content_tag(:span, truncate(full, length: length), class: "rh-sub rh-mono", title: full) end |
#job_display_class(entry) ⇒ Object
The class name a row should show: the real job class, not the ActiveJob adapter's wrapper. Resolved here rather than in each template so every surface prints the string the Errors page groups on and the APM link searches for.
7 8 9 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 7 def job_display_class(entry) RoundhouseUi.unwrapped_class(entry.klass, entry.item) end |
#job_editing? ⇒ Boolean
A URL that keeps every active filter, naming only what changes. Pass a key as nil to clear just that one.
Never enumerate filter params by hand in a view. That is what let the pager drop the class filter on page two, and the confirm form drop it between the dry run and the deletion. Editing needs BOTH the host's opt-in and a backend that can push. Six views read the config flag directly; only this reads the capability too.
29 30 31 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 29 def job_editing? RoundhouseUi.allow_job_editing && RoundhouseUi.backend.supports?(:enqueue) end |
#job_identity(klass, jid, path) ⇒ Object
A job row's identity cell. Class on the first line where the eye lands, jid on a second, dimmer line β one long line of class + hex + link is unreadable once the row also carries squad, queue and error.
14 15 16 17 18 19 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 14 def job_identity(klass, jid, path) safe_join([ link_to(klass, path, class: "rh-joblink"), content_tag(:div, jid, class: "rh-sub rh-mono rh-jid", title: jid) ]) end |
#job_time(at, overdue: "now (overdue)") ⇒ Object
Relative time answers "is this soon?", the clock time answers "does that land inside the maintenance window?". Operators need both, so show both rather than making them hover or do the arithmetic.
103 104 105 106 107 108 109 110 111 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 103 def job_time(at, overdue: "now (overdue)") return content_tag(:span, "β", class: "rh-sub") if at.nil? relative = at > Time.now ? "in #{distance_of_time_in_words(Time.now, at)}" : overdue safe_join([ content_tag(:span, relative), content_tag(:span, at.strftime("%b %-d, %H:%M"), class: "rh-sub rh-mono") ], " Β· ") end |
#queue_pill(name, link: false) ⇒ Object
Queues carry meaning at a glance (critical vs low), so render them as a
pill rather than grey text lost between two columns. On the job sets the
pill filters to that queue; link: is off where there's nothing to filter
(the Queues index itself, grouped Errors rows).
288 289 290 291 292 293 294 295 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 288 def queue_pill(name, link: false) return content_tag(:span, name, class: "rh-pill rh-mono") unless link active = (@filter&.queue) == name.to_s link_to name, filter_url(page: nil, queue: (active ? nil : name)), class: "rh-pill rh-mono rh-pill-link#{' is-on' if active}", title: active ? "Clear queue filter" : "Show only #{name}" end |
#runbook_link(klass, item = nil, label: "Runbook") ⇒ Object
The runbook for a job, as a link, or nil when the host declared none (#39). Cached per class per request like tags β the same page can ask for the same class dozens of times.
276 277 278 279 280 281 282 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 276 def runbook_link(klass, item = nil, label: "Runbook") url = RoundhouseUi::Runbooks.for(klass, item, cache: (@rh_runbook_cache ||= {})) return nil unless url link_to label, url, class: "rh-runbook", target: "_blank", rel: "noopener noreferrer", title: "Open the runbook for #{RoundhouseUi.unwrapped_class(klass, item)}" end |
#set_heading(label, showing:, total:, query: nil, tag: nil) ⇒ Object
Set heading that tells the truth under a filter. It used to always print the whole-set size, so "Dead set Β· 19 jobs" sat above four filtered rows.
183 184 185 186 187 188 189 190 191 192 193 |
# File 'app/helpers/roundhouse_ui/application_helper.rb', line 183 def set_heading(label, showing:, total:, query: nil, tag: nil) filtered = filtered_view?(query, tag) count = filtered ? "#{number_with_delimiter showing} of #{number_with_delimiter total}" : number_with_delimiter(total) # No prose restatement. The pills in the bar say which filters are on, in the # same words you would type to reproduce them; the heading's job is the count. # Saying it a second time in prose ("tagged squad: platform and failing with # KeyError") was one of the four places one filter was rendered, and the one # that could not be clicked to change it. filter_description still earns its # place on the bulk confirm, where the sentence IS the thing being approved. content_tag(:h2, "#{label} Β· #{count} jobs", class: "rh-h2") end |