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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +563 -50
  3. data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +190 -23
  4. data/app/controllers/roundhouse_ui/application_controller.rb +73 -0
  5. data/app/controllers/roundhouse_ui/audit_controller.rb +1 -0
  6. data/app/controllers/roundhouse_ui/busy_controller.rb +25 -5
  7. data/app/controllers/roundhouse_ui/dashboard_controller.rb +23 -4
  8. data/app/controllers/roundhouse_ui/dead_controller.rb +26 -12
  9. data/app/controllers/roundhouse_ui/errors_controller.rb +30 -8
  10. data/app/controllers/roundhouse_ui/jobs_controller.rb +5 -1
  11. data/app/controllers/roundhouse_ui/queues_controller.rb +56 -7
  12. data/app/controllers/roundhouse_ui/recurring_controller.rb +12 -0
  13. data/app/controllers/roundhouse_ui/retries_controller.rb +33 -12
  14. data/app/controllers/roundhouse_ui/scheduled_controller.rb +3 -8
  15. data/app/controllers/roundhouse_ui/settings_controller.rb +15 -0
  16. data/app/controllers/roundhouse_ui/snapshots_controller.rb +5 -6
  17. data/app/controllers/roundhouse_ui/workers_controller.rb +2 -7
  18. data/app/helpers/roundhouse_ui/application_helper.rb +253 -10
  19. data/app/helpers/roundhouse_ui/nav_helper.rb +3 -3
  20. data/app/helpers/roundhouse_ui/observability_helper.rb +113 -27
  21. data/app/helpers/roundhouse_ui/tags_helper.rb +30 -3
  22. data/app/views/layouts/roundhouse_ui/application.html.erb +1181 -92
  23. data/app/views/roundhouse_ui/busy/index.html.erb +30 -6
  24. data/app/views/roundhouse_ui/dashboard/show.html.erb +62 -17
  25. data/app/views/roundhouse_ui/dead/index.html.erb +23 -15
  26. data/app/views/roundhouse_ui/errors/index.html.erb +28 -9
  27. data/app/views/roundhouse_ui/jobs/show.html.erb +7 -3
  28. data/app/views/roundhouse_ui/metrics/show.html.erb +26 -3
  29. data/app/views/roundhouse_ui/queues/index.html.erb +53 -13
  30. data/app/views/roundhouse_ui/queues/show.html.erb +55 -0
  31. data/app/views/roundhouse_ui/recurring/index.html.erb +71 -0
  32. data/app/views/roundhouse_ui/retries/index.html.erb +17 -15
  33. data/app/views/roundhouse_ui/scheduled/index.html.erb +10 -10
  34. data/app/views/roundhouse_ui/settings/show.html.erb +91 -0
  35. data/app/views/roundhouse_ui/shared/_filter_error.html.erb +23 -0
  36. data/app/views/roundhouse_ui/shared/_pager.html.erb +2 -2
  37. data/app/views/roundhouse_ui/shared/_search_bar.html.erb +55 -0
  38. data/app/views/roundhouse_ui/shared/_search_help.html.erb +37 -0
  39. data/app/views/roundhouse_ui/shared/_tag_filter.html.erb +2 -4
  40. data/app/views/roundhouse_ui/shared/bulk_preview.html.erb +69 -0
  41. data/config/routes.rb +9 -0
  42. data/lib/roundhouse_ui/backends/sidekiq.rb +124 -5
  43. data/lib/roundhouse_ui/backends/solid_queue.rb +54 -3
  44. data/lib/roundhouse_ui/demo.rb +130 -0
  45. data/lib/roundhouse_ui/error_groups.rb +13 -3
  46. data/lib/roundhouse_ui/filter_query.rb +504 -0
  47. data/lib/roundhouse_ui/health.rb +43 -4
  48. data/lib/roundhouse_ui/history.rb +54 -0
  49. data/lib/roundhouse_ui/icons.rb +104 -0
  50. data/lib/roundhouse_ui/marks/datadog-lockup-white.svg +30 -0
  51. data/lib/roundhouse_ui/marks/datadog-lockup.svg +41 -0
  52. data/lib/roundhouse_ui/observability.rb +55 -7
  53. data/lib/roundhouse_ui/pause.rb +6 -11
  54. data/lib/roundhouse_ui/queue_summary.rb +11 -0
  55. data/lib/roundhouse_ui/recurring.rb +137 -0
  56. data/lib/roundhouse_ui/runbooks.rb +75 -0
  57. data/lib/roundhouse_ui/snapshots.rb +13 -2
  58. data/lib/roundhouse_ui/tags.rb +8 -11
  59. data/lib/roundhouse_ui/theme.rb +298 -0
  60. data/lib/roundhouse_ui/version.rb +1 -1
  61. data/lib/roundhouse_ui.rb +200 -5
  62. data/lib/tasks/roundhouse_ui_tasks.rake +84 -4
  63. metadata +29 -7
@@ -1,48 +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
- adapter = RoundhouseUi.observability
7
- url = adapter.job_url(klass: klass, jid: jid, queue: queue)
8
- return unless url
9
-
10
- link_to "↗ #{adapter.label}", url, target: "_blank", rel: "noopener", class: "rh-trace"
10
+ trace_affordance(job_trace_url(klass: klass, jid: jid, queue: queue), as: :mark)
11
11
  end
12
12
 
13
- # Icon-only variant for table rows. The adapter's name is worth reading once
14
- # in a header or a detail page, not once per row — there it is width spent
15
- # repeating a word the operator already knows.
16
13
  def trace_icon(klass:, jid:, queue: nil)
17
- adapter = RoundhouseUi.observability
18
- url = adapter.job_url(klass: klass, jid: jid, queue: queue)
19
- return unless url
14
+ trace_affordance(job_trace_url(klass: klass, jid: jid, queue: queue), as: :control)
15
+ end
20
16
 
21
- link_to "↗", url, target: "_blank", rel: "noopener", class: "rh-trace rh-trace-ico",
22
- title: "Open in #{adapter.label}", "aria-label": "Open in #{adapter.label}"
17
+ def error_trace_link(klass:, error: nil)
18
+ trace_affordance(error_trace_url(klass: klass, error: error), as: :mark)
23
19
  end
24
20
 
25
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
+
26
35
  adapter = RoundhouseUi.observability
27
- return unless adapter.respond_to?(:error_url)
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
28
43
 
29
- url = adapter.error_url(klass: klass, error: error)
30
- return unless url
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)
31
60
 
32
- link_to "↗", url, target: "_blank", rel: "noopener", class: "rh-trace rh-trace-ico",
33
- title: "Open in #{adapter.label}", "aria-label": "Open in #{adapter.label}"
61
+ supplied.to_s.strip.start_with?("<") ? supplied.to_s.html_safe : icon(:trace_out)
34
62
  end
35
63
 
36
- # Deep-link for a grouped error row (no single JID) — a class-wide search.
37
- # respond_to? keeps older/custom adapters that lack error_url working.
38
- def error_trace_link(klass:, error: nil)
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)
39
71
  adapter = RoundhouseUi.observability
40
- return unless adapter.respond_to?(:error_url)
72
+ return nil unless adapter.respond_to?(:error_url)
41
73
 
42
- url = adapter.error_url(klass: klass, error: error)
43
- return unless url
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
+
93
+ adapter = RoundhouseUi.observability
94
+ return trace_control(url, adapter, compact: compact) if as == :control
95
+
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
44
129
 
45
- link_to "↗ #{adapter.label}", url, target: "_blank", rel: "noopener", class: "rh-trace"
130
+ def wordmark?(adapter)
131
+ adapter.respond_to?(:wordmark?) && adapter.wordmark?
46
132
  end
47
133
  end
48
134
  end
@@ -78,18 +78,45 @@ module RoundhouseUi
78
78
  # Every active constraint must appear here. A confirm that names only the
79
79
  # text query while a queue or tag also narrows the set understates what is
80
80
  # about to be destroyed.
81
- def filter_description(query, tag, queue = @queue_filter)
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)
82
89
  parts = []
83
90
  parts << "matching “#{query}”" if query.present?
84
91
  parts << "tagged #{tag[0]}: #{tag[1]}" if tag
85
92
  parts << "in queue #{queue}" if queue.present?
93
+ parts << like_description if rh_filter.klass.present? || rh_filter.error.present?
86
94
  parts.join(" and ")
87
95
  end
88
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
+
89
110
  # Any filter active? Bulk-on-match is filter-gated, so this decides whether
90
111
  # the bulk bar renders at all — and which empty-state copy is honest.
91
- def any_filter?(query, tag, queue = @queue_filter)
92
- query.present? || !tag.nil? || queue.present?
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?
93
120
  end
94
121
 
95
122
  # Value only — the key is near-constant down a column, so repeating it is