karafka-web 1.0.0 → 1.0.1

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile.lock +13 -13
  4. data/lib/karafka/web/management/actions/enable.rb +74 -10
  5. data/lib/karafka/web/pro/ui/app.rb +13 -0
  6. data/lib/karafka/web/pro/ui/controllers/base_controller.rb +4 -0
  7. data/lib/karafka/web/pro/ui/controllers/cluster_controller.rb +66 -4
  8. data/lib/karafka/web/pro/ui/controllers/consumers/consumers_controller.rb +9 -2
  9. data/lib/karafka/web/pro/ui/controllers/consumers/controls_controller.rb +6 -0
  10. data/lib/karafka/web/pro/ui/controllers/consumers/jobs_controller.rb +8 -2
  11. data/lib/karafka/web/pro/ui/controllers/dlq_controller.rb +6 -0
  12. data/lib/karafka/web/pro/ui/controllers/explorer/explorer_controller.rb +6 -0
  13. data/lib/karafka/web/pro/ui/controllers/health_controller.rb +19 -3
  14. data/lib/karafka/web/pro/ui/controllers/jobs_controller.rb +25 -0
  15. data/lib/karafka/web/pro/ui/controllers/recurring_tasks_controller.rb +10 -1
  16. data/lib/karafka/web/pro/ui/controllers/requests/params.rb +67 -0
  17. data/lib/karafka/web/pro/ui/controllers/routing_controller.rb +23 -1
  18. data/lib/karafka/web/pro/ui/controllers/scheduled_messages/schedules_controller.rb +6 -0
  19. data/lib/karafka/web/pro/ui/controllers/topics/configs_controller.rb +6 -1
  20. data/lib/karafka/web/pro/ui/controllers/topics/distributions_controller.rb +1 -1
  21. data/lib/karafka/web/pro/ui/controllers/topics/offsets_controller.rb +1 -1
  22. data/lib/karafka/web/pro/ui/controllers/topics/replications_controller.rb +1 -1
  23. data/lib/karafka/web/pro/ui/controllers/topics/topics_controller.rb +6 -0
  24. data/lib/karafka/web/pro/ui/helpers/filtering_helper.rb +173 -0
  25. data/lib/karafka/web/pro/ui/helpers/topics_filtering_helper.rb +80 -0
  26. data/lib/karafka/web/pro/ui/lib/filtering.rb +128 -0
  27. data/lib/karafka/web/pro/ui/lib/search/matchers/base.rb +2 -2
  28. data/lib/karafka/web/pro/ui/routes/cluster.rb +0 -2
  29. data/lib/karafka/web/pro/ui/views/cluster/_brokers_table.erb +41 -0
  30. data/lib/karafka/web/pro/ui/views/cluster/_configs_table.erb +41 -0
  31. data/lib/karafka/web/pro/ui/views/cluster/_partition.erb +2 -2
  32. data/lib/karafka/web/pro/ui/views/cluster/index.erb +12 -20
  33. data/lib/karafka/web/pro/ui/views/cluster/replication.erb +39 -0
  34. data/lib/karafka/web/pro/ui/views/cluster/show.erb +13 -21
  35. data/lib/karafka/web/pro/ui/views/consumers/consumers/_consumer_performances_table.erb +56 -0
  36. data/lib/karafka/web/pro/ui/views/consumers/consumers/_consumers_table.erb +43 -0
  37. data/lib/karafka/web/pro/ui/views/consumers/consumers/consumer/_subscription_group.erb +10 -2
  38. data/lib/karafka/web/pro/ui/views/consumers/consumers/index.erb +8 -24
  39. data/lib/karafka/web/pro/ui/views/consumers/consumers/performance.erb +8 -37
  40. data/lib/karafka/web/pro/ui/views/consumers/consumers/subscriptions.erb +20 -6
  41. data/lib/karafka/web/pro/ui/views/consumers/controls/_processes_table.erb +45 -0
  42. data/lib/karafka/web/pro/ui/views/consumers/controls/index.erb +14 -27
  43. data/lib/karafka/web/pro/ui/views/consumers/jobs/_jobs_table.erb +48 -0
  44. data/lib/karafka/web/pro/ui/views/consumers/jobs/pending.erb +8 -29
  45. data/lib/karafka/web/pro/ui/views/consumers/jobs/running.erb +8 -29
  46. data/lib/karafka/web/pro/ui/views/dlq/index.erb +9 -1
  47. data/lib/karafka/web/pro/ui/views/explorer/explorer/index.erb +9 -1
  48. data/lib/karafka/web/pro/ui/views/health/_changes_table.erb +59 -0
  49. data/lib/karafka/web/pro/ui/views/health/_cluster_lags_table.erb +43 -0
  50. data/lib/karafka/web/pro/ui/views/health/_lags_table.erb +60 -0
  51. data/lib/karafka/web/pro/ui/views/health/_no_data.erb +5 -1
  52. data/lib/karafka/web/pro/ui/views/health/_offsets_table.erb +63 -0
  53. data/lib/karafka/web/pro/ui/views/health/_overview_table.erb +83 -0
  54. data/lib/karafka/web/pro/ui/views/health/_tabs.erb +6 -0
  55. data/lib/karafka/web/pro/ui/views/health/changes.erb +13 -43
  56. data/lib/karafka/web/pro/ui/views/health/cluster_lags.erb +11 -25
  57. data/lib/karafka/web/pro/ui/views/health/lags.erb +13 -44
  58. data/lib/karafka/web/pro/ui/views/health/offsets.erb +13 -47
  59. data/lib/karafka/web/pro/ui/views/health/overview.erb +13 -67
  60. data/lib/karafka/web/pro/ui/views/jobs/_jobs_table.erb +48 -0
  61. data/lib/karafka/web/pro/ui/views/jobs/pending.erb +8 -30
  62. data/lib/karafka/web/pro/ui/views/jobs/running.erb +8 -30
  63. data/lib/karafka/web/pro/ui/views/recurring_tasks/_tasks_table.erb +43 -0
  64. data/lib/karafka/web/pro/ui/views/recurring_tasks/schedule.erb +8 -24
  65. data/lib/karafka/web/pro/ui/views/routing/_consumer_group.erb +36 -32
  66. data/lib/karafka/web/pro/ui/views/routing/_details_table.erb +43 -0
  67. data/lib/karafka/web/pro/ui/views/routing/index.erb +18 -6
  68. data/lib/karafka/web/pro/ui/views/routing/show.erb +12 -40
  69. data/lib/karafka/web/pro/ui/views/scheduled_messages/schedules/index.erb +7 -1
  70. data/lib/karafka/web/pro/ui/views/shared/_filter_box.erb +61 -0
  71. data/lib/karafka/web/pro/ui/views/topics/configs/_configs_table.erb +42 -0
  72. data/lib/karafka/web/pro/ui/views/topics/configs/index.erb +13 -22
  73. data/lib/karafka/web/pro/ui/views/topics/replications/_partition.erb +2 -2
  74. data/lib/karafka/web/pro/ui/views/topics/replications/show.erb +12 -2
  75. data/lib/karafka/web/pro/ui/views/topics/topics/index.erb +9 -1
  76. data/lib/karafka/web/tracking/consumers/listeners/errors.rb +80 -14
  77. data/lib/karafka/web/ui/base.rb +4 -0
  78. data/lib/karafka/web/ui/controllers/base_controller.rb +1 -1
  79. data/lib/karafka/web/ui/controllers/cluster_controller.rb +21 -22
  80. data/lib/karafka/web/ui/controllers/consumers_controller.rb +1 -1
  81. data/lib/karafka/web/ui/controllers/jobs_controller.rb +23 -27
  82. data/lib/karafka/web/ui/controllers/requests/hookable.rb +5 -0
  83. data/lib/karafka/web/ui/controllers/routing_controller.rb +15 -1
  84. data/lib/karafka/web/ui/helpers/application_helper.rb +3 -278
  85. data/lib/karafka/web/ui/helpers/badges_helper.rb +66 -0
  86. data/lib/karafka/web/ui/helpers/formatting_helper.rb +77 -0
  87. data/lib/karafka/web/ui/helpers/partitions_helper.rb +167 -0
  88. data/lib/karafka/web/ui/helpers/sorting_helper.rb +78 -0
  89. data/lib/karafka/web/ui/lib/filter.rb +377 -0
  90. data/lib/karafka/web/ui/lib/hash_flattener.rb +37 -0
  91. data/lib/karafka/web/ui/lib/sorter.rb +3 -1
  92. data/lib/karafka/web/ui/models/jobs.rb +3 -2
  93. data/lib/karafka/web/ui/models/process.rb +12 -0
  94. data/lib/karafka/web/ui/public/javascripts/application.min.js.gz +0 -0
  95. data/lib/karafka/web/ui/public/stylesheets/application.css +9 -0
  96. data/lib/karafka/web/ui/public/stylesheets/application.min.css +4230 -1258
  97. data/lib/karafka/web/ui/public/stylesheets/application.min.css.br +0 -0
  98. data/lib/karafka/web/ui/public/stylesheets/application.min.css.gz +0 -0
  99. data/lib/karafka/web/ui/public/stylesheets/libs/highlight_dark.min.css.gz +0 -0
  100. data/lib/karafka/web/ui/public/stylesheets/libs/highlight_light.min.css.gz +0 -0
  101. data/lib/karafka/web/ui/views/cluster/_brokers_table.erb +21 -0
  102. data/lib/karafka/web/ui/views/cluster/_partition.erb +2 -2
  103. data/lib/karafka/web/ui/views/cluster/_partitions_table.erb +31 -0
  104. data/lib/karafka/web/ui/views/cluster/brokers.erb +1 -21
  105. data/lib/karafka/web/ui/views/cluster/replication.erb +1 -23
  106. data/lib/karafka/web/ui/views/consumers/_consumers_table.erb +22 -0
  107. data/lib/karafka/web/ui/views/consumers/index.erb +1 -22
  108. data/lib/karafka/web/ui/views/errors/_table.erb +28 -0
  109. data/lib/karafka/web/ui/views/errors/index.erb +1 -28
  110. data/lib/karafka/web/ui/views/jobs/_jobs_table.erb +22 -0
  111. data/lib/karafka/web/ui/views/jobs/pending.erb +1 -22
  112. data/lib/karafka/web/ui/views/jobs/running.erb +1 -22
  113. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +18 -12
  114. data/lib/karafka/web/ui/views/routing/_details_table.erb +23 -0
  115. data/lib/karafka/web/ui/views/routing/show.erb +1 -29
  116. data/lib/karafka/web/ui/views/shared/_no_filter_results.erb +6 -0
  117. data/lib/karafka/web/version.rb +1 -1
  118. data/package-lock.json +98 -61
  119. metadata +35 -1
@@ -5,31 +5,10 @@ module Karafka
5
5
  module Ui
6
6
  # Namespace for helpers used by the Web UI
7
7
  module Helpers
8
- # Main application helper
8
+ # Application-wide helpers: navigation, layout, icons, empty states and generic hash
9
+ # utilities. More specific concerns live in dedicated helpers ([[SortingHelper]],
10
+ # [[FormattingHelper]], [[BadgesHelper]], [[PartitionsHelper]]).
9
11
  module ApplicationHelper
10
- # Default attribute names mapped from the attributes themselves
11
- # It makes it easier as we do not have to declare those all the time
12
- SORT_NAMES = {
13
- id: "ID",
14
- partition_id: "Partition",
15
- memory_usage: "RSS",
16
- started_at: "Started",
17
- committed_offset: "Committed",
18
- last_offset: "Last",
19
- first_offset: "First",
20
- lo_offset: "Low",
21
- hi_offset: "High",
22
- ls_offset: "LSO",
23
- lag_hybrid: "Lag",
24
- lag_stored: "Stored",
25
- stored_offset: "Stored",
26
- fetch_state: "Fetch",
27
- poll_state: "Poll",
28
- lso_risk_state: "LSO"
29
- }.freeze
30
-
31
- private_constant :SORT_NAMES
32
-
33
12
  # Adds active class to the current location in the nav if needed
34
13
  # @param location [Hash]
35
14
  def nav_class(location)
@@ -39,167 +18,6 @@ module Karafka
39
18
  local_location.public_send(:"#{comparator}?", value) ? "active" : ""
40
19
  end
41
20
 
42
- # Converts object into a string and for objects that would anyhow return their
43
- # stringified instance value, it replaces it with the class name instead.
44
- # Useful for deserializers, etc presentation.
45
- #
46
- # @param object [Object]
47
- # @return [String]
48
- def object_value_to_s(object)
49
- object.to_s.include?("#<") ? object.class.to_s : object.to_s
50
- end
51
-
52
- # Takes a status and recommends background style color
53
- #
54
- # @param status [String] status
55
- # @return [String] background style
56
- def status_badge(status)
57
- case status
58
- when "initialized" then "badge-success"
59
- when "supervising" then "badge-success"
60
- when "running" then "badge-success"
61
- when "quieting" then "badge-warning"
62
- when "quiet" then "badge-warning"
63
- when "stopping" then "badge-warning"
64
- when "stopped" then "badge-error"
65
- when "terminated" then "badge-error"
66
- else
67
- raise ::Karafka::Errors::UnsupportedCaseError, status
68
- end
69
- end
70
-
71
- # Takes the lag trend and gives it appropriate background style color for badge
72
- #
73
- # @param trend [Numeric] lag trend
74
- # @return [String] bg classes
75
- def lag_trend_badge(trend)
76
- bg = "badge-success" if trend.negative?
77
- bg ||= "badge-warning" if trend.positive?
78
- bg ||= "badge-secondary"
79
- bg
80
- end
81
-
82
- # Renders tags one after another
83
- #
84
- # @param tags_array [Array<String>]
85
- # @return [String] tags badges
86
- def tags(tags_array)
87
- tags_array
88
- .map { |tag| %(<span class="badge badge-info">#{tag}</span>) }
89
- .join(" ")
90
- end
91
-
92
- # Takes a kafka report state and recommends background style color
93
- # @param state [String] state
94
- # @return [String] background style
95
- def kafka_state_badge(state)
96
- case state
97
- when "up" then "badge-success"
98
- when "active" then "badge-success"
99
- when "steady" then "badge-success"
100
- else
101
- "badge-warning"
102
- end
103
- end
104
-
105
- # @param mem_kb [Integer] memory used in KB
106
- # @return [String] formatted memory usage
107
- def format_memory(mem_kb)
108
- return "0" if !mem_kb || mem_kb.zero?
109
-
110
- if mem_kb < 10_240
111
- "#{number_with_delimiter(mem_kb.round(4))} KB"
112
- elsif mem_kb < 1_000_000
113
- "#{number_with_delimiter((mem_kb / 1024.0).to_i)} MB"
114
- else
115
- "#{number_with_delimiter((mem_kb / (1024.0 * 1024.0)).round(1))} GB"
116
- end
117
- end
118
-
119
- # Converts number to a more friendly delimiter based version
120
- # @param number [Numeric]
121
- # @param delimiter [String] delimiter (comma by default)
122
- # @return [String] number with delimiter
123
- def number_with_delimiter(number, delimiter = ",")
124
- return "" unless number
125
-
126
- parts = number.to_s.to_str.split(".")
127
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
128
- parts.join(".")
129
- end
130
-
131
- # @param lag [Integer] lag
132
- # @return [String] lag if correct or `N/A` with labeled explanation
133
- # @see #offset_with_label
134
- def lag_with_label(lag)
135
- if lag.negative?
136
- title = "Not available until first offset commit"
137
- %(<span class="badge badge-secondary" title="#{title}">N/A</span>)
138
- else
139
- lag.to_s
140
- end
141
- end
142
-
143
- # @param topic_name [String] name of the topic for explorer path
144
- # @param partition_id [Integer] partition for the explorer path
145
- # @param offset [Integer] offset
146
- # @param explore [Boolean] should we generate (when allowed) a link to message explorer
147
- # @return [String] offset if correct or `N/A` with labeled explanation for offsets
148
- # that are less than 0. Offset with less than 0 indicates, that the offset was not
149
- # yet committed and there is no value we know of
150
- def offset_with_label(topic_name, partition_id, offset, explore: false)
151
- if offset.negative?
152
- title = "Not available until first offset commit"
153
- %(<span class="badge badge-secondary" title="#{title}">N/A</span>)
154
- elsif explore
155
- path = explorer_topics_path(topic_name, partition_id, offset)
156
- %(<a href="#{path}">#{offset}</a>)
157
- else
158
- offset.to_s
159
- end
160
- end
161
-
162
- # Normalizes the metric value for display. Negative values coming from statistics usually
163
- # mean, that the value is not (yet) available.
164
- #
165
- # @param value [Integer]
166
- # @return [String] input value if not negative or N/A
167
- def normalized_metric(value)
168
- value.negative? ? "N/A" : value.to_s
169
- end
170
-
171
- # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
172
- # lso risk state info
173
- # @return [String] background classes for row marking
174
- def lso_risk_state_bg(details)
175
- case details.lso_risk_state
176
- when :active
177
- ""
178
- when :at_risk
179
- "bg-warning bg-opacity-25"
180
- when :stopped
181
- "bg-error bg-opacity-25"
182
- else
183
- raise ::Karafka::Errors::UnsupportedCaseError
184
- end
185
- end
186
-
187
- # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
188
- # lso risk state info
189
- # @return [String] background classes for row marking
190
- def lso_risk_state_badge(details)
191
- case details.lso_risk_state
192
- when :active
193
- ""
194
- when :at_risk
195
- "badge-warning"
196
- when :stopped
197
- "badge-error"
198
- else
199
- raise ::Karafka::Errors::UnsupportedCaseError
200
- end
201
- end
202
-
203
21
  # Sets the particular page title
204
22
  #
205
23
  # @param title [String] page title
@@ -208,99 +26,6 @@ module Karafka
208
26
  content_for(:title) { title }
209
27
  end
210
28
 
211
- # @param hash [Hash] we want to flatten
212
- # @param parent_key [String] key for recursion
213
- # @param result [Hash] result for recursion
214
- # @return [Hash]
215
- def flat_hash(hash, parent_key = nil, result = {})
216
- hash.each do |key, value|
217
- current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
218
- if value.is_a?(Hash)
219
- flat_hash(value, current_key, result)
220
- elsif value.is_a?(Array)
221
- value.each_with_index do |item, index|
222
- flat_hash({ index => item }, current_key, result)
223
- end
224
- else
225
- result[current_key] = value
226
- end
227
- end
228
-
229
- result
230
- end
231
-
232
- # @param name [String] link value
233
- # @param attribute [Symbol, nil] sorting attribute or nil if we provide only symbol name
234
- # @param rev [Boolean] when set to true, arrows will be in the reverse position. This is
235
- # used when the description in the link is reverse to data we sort. For example we have
236
- # order on when processes were started and we display "x hours" ago but we sort on
237
- # their age, meaning that it looks like it is the other way around. This flag allows
238
- # us to reverse just he arrow making it look consistent with the presented data order
239
- # @return [String] html link for sorting with arrow when attribute sort enabled
240
- def sort_link(name, attribute = nil, rev: false)
241
- unless attribute
242
- attribute = name
243
-
244
- if SORT_NAMES[attribute]
245
- name = SORT_NAMES[attribute]
246
- else
247
- name = attribute.to_s.tr("_", " ").tr("?", "")
248
- # Always capitalize the name
249
- name = name.split.map(&:capitalize).join(" ")
250
- end
251
- end
252
-
253
- arrow_both = "&#x21D5;"
254
- arrow_down = "&#9662;"
255
- arrow_up = "&#9652;"
256
-
257
- desc = "#{attribute} desc"
258
- asc = "#{attribute} asc"
259
- path = current_path(sort: desc)
260
- full_name = "#{name}&nbsp;#{arrow_both}"
261
-
262
- if params.current_sort == desc
263
- path = current_path(sort: asc)
264
- full_name = "#{name}&nbsp;#{rev ? arrow_up : arrow_down}"
265
- end
266
-
267
- if params.current_sort == asc
268
- path = current_path(sort: desc)
269
- full_name = "#{name}&nbsp;#{rev ? arrow_down : arrow_up}"
270
- end
271
-
272
- "<a class=\"sort\" href=\"#{path}\">#{full_name}</a>"
273
- end
274
-
275
- # Truncates given text if it is too long and wraps it with a title with full text.
276
- # Can use a middle-based strategy that keeps beginning and ending of a string instead of
277
- # keeping just the beginning.
278
- #
279
- # The `:middle` strategy is useful when we have strings such as really long process names
280
- # that have important beginning and end but middle can be removed without risk of not
281
- # allowing user to recognize the content.
282
- #
283
- # @param string [String] string we want to truncate
284
- # @param length [Integer] max length of the final string that we accept before truncating
285
- # @param omission [String] truncation omission
286
- # @param strategy [Symbol] `:default` or `:middle` how should we truncate
287
- # @return [String] HTML span tag with truncated content and full content title
288
- def truncate(string, length: 50, omission: "...", strategy: :default)
289
- return string if string.length <= length
290
-
291
- case strategy
292
- when :default
293
- truncated = string[0...(length - omission.length)] + omission
294
- when :middle
295
- part_length = (length - omission.length) / 2
296
- truncated = string[0...part_length] + omission + string[-part_length..]
297
- else
298
- raise Karafka::Errors::UnsupportedCaseError, "Unknown strategy: #{strategy}"
299
- end
300
-
301
- %(<span title="#{string}">#{truncated}</span>)
302
- end
303
-
304
29
  # Renders the svg icon out of our icon set
305
30
  # @param name [String, Symbol] name of the icon
306
31
  # @param size [Integer, nil] optional size override (defaults to the icon's own size)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Helpers mapping domain states (process status, kafka state, lag trend) to badge styles
8
+ # and rendering simple badge collections
9
+ module BadgesHelper
10
+ # Takes a status and recommends background style color
11
+ #
12
+ # @param status [String] status
13
+ # @return [String] background style
14
+ def status_badge(status)
15
+ case status
16
+ when "initialized" then "badge-success"
17
+ when "supervising" then "badge-success"
18
+ when "running" then "badge-success"
19
+ when "quieting" then "badge-warning"
20
+ when "quiet" then "badge-warning"
21
+ when "stopping" then "badge-warning"
22
+ when "stopped" then "badge-error"
23
+ when "terminated" then "badge-error"
24
+ else
25
+ raise ::Karafka::Errors::UnsupportedCaseError, status
26
+ end
27
+ end
28
+
29
+ # Takes the lag trend and gives it appropriate background style color for badge
30
+ #
31
+ # @param trend [Numeric] lag trend
32
+ # @return [String] bg classes
33
+ def lag_trend_badge(trend)
34
+ bg = "badge-success" if trend.negative?
35
+ bg ||= "badge-warning" if trend.positive?
36
+ bg ||= "badge-secondary"
37
+ bg
38
+ end
39
+
40
+ # Renders tags one after another
41
+ #
42
+ # @param tags_array [Array<String>]
43
+ # @return [String] tags badges
44
+ def tags(tags_array)
45
+ tags_array
46
+ .map { |tag| %(<span class="badge badge-info">#{tag}</span>) }
47
+ .join(" ")
48
+ end
49
+
50
+ # Takes a kafka report state and recommends background style color
51
+ # @param state [String] state
52
+ # @return [String] background style
53
+ def kafka_state_badge(state)
54
+ case state
55
+ when "up" then "badge-success"
56
+ when "active" then "badge-success"
57
+ when "steady" then "badge-success"
58
+ else
59
+ "badge-warning"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Generic value, number and text formatting helpers used across the Web UI
8
+ module FormattingHelper
9
+ # Converts object into a string and for objects that would anyhow return their
10
+ # stringified instance value, it replaces it with the class name instead.
11
+ # Useful for deserializers, etc presentation.
12
+ #
13
+ # @param object [Object]
14
+ # @return [String]
15
+ def object_value_to_s(object)
16
+ object.to_s.include?("#<") ? object.class.to_s : object.to_s
17
+ end
18
+
19
+ # @param mem_kb [Integer] memory used in KB
20
+ # @return [String] formatted memory usage
21
+ def format_memory(mem_kb)
22
+ return "0" if !mem_kb || mem_kb.zero?
23
+
24
+ if mem_kb < 10_240
25
+ "#{number_with_delimiter(mem_kb.round(4))} KB"
26
+ elsif mem_kb < 1_000_000
27
+ "#{number_with_delimiter((mem_kb / 1024.0).to_i)} MB"
28
+ else
29
+ "#{number_with_delimiter((mem_kb / (1024.0 * 1024.0)).round(1))} GB"
30
+ end
31
+ end
32
+
33
+ # Converts number to a more friendly delimiter based version
34
+ # @param number [Numeric]
35
+ # @param delimiter [String] delimiter (comma by default)
36
+ # @return [String] number with delimiter
37
+ def number_with_delimiter(number, delimiter = ",")
38
+ return "" unless number
39
+
40
+ parts = number.to_s.to_str.split(".")
41
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
42
+ parts.join(".")
43
+ end
44
+
45
+ # Truncates given text if it is too long and wraps it with a title with full text.
46
+ # Can use a middle-based strategy that keeps beginning and ending of a string instead of
47
+ # keeping just the beginning.
48
+ #
49
+ # The `:middle` strategy is useful when we have strings such as really long process names
50
+ # that have important beginning and end but middle can be removed without risk of not
51
+ # allowing user to recognize the content.
52
+ #
53
+ # @param string [String] string we want to truncate
54
+ # @param length [Integer] max length of the final string that we accept before truncating
55
+ # @param omission [String] truncation omission
56
+ # @param strategy [Symbol] `:default` or `:middle` how should we truncate
57
+ # @return [String] HTML span tag with truncated content and full content title
58
+ def truncate(string, length: 50, omission: "...", strategy: :default)
59
+ return string if string.length <= length
60
+
61
+ case strategy
62
+ when :default
63
+ truncated = string[0...(length - omission.length)] + omission
64
+ when :middle
65
+ part_length = (length - omission.length) / 2
66
+ truncated = string[0...part_length] + omission + string[-part_length..]
67
+ else
68
+ raise Karafka::Errors::UnsupportedCaseError, "Unknown strategy: #{strategy}"
69
+ end
70
+
71
+ %(<span title="#{string}">#{truncated}</span>)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Kafka partition presentation helpers: replica/ISR broker ids, LSO risk state and
8
+ # offset/lag labels
9
+ module PartitionsHelper
10
+ # Renders the assigned replica broker ids for a partition as badges. The leader is
11
+ # emphasized and any replica that is not part of the in-sync set (ISR) is highlighted
12
+ # as a warning, turning the replication view into an at-a-glance health signal.
13
+ #
14
+ # @param partition [Hash, Ui::Models::Partition] partition metadata
15
+ # @param link [Boolean] when true, each broker badge links to its broker details page
16
+ # (Pro-only; OSS has no per-broker view so it renders plain badges)
17
+ # @return [String] replica broker ids as badges with a count subtext
18
+ def partition_replica_brokers(partition, link: false)
19
+ replicas = partition[:replicas]
20
+
21
+ return %(<span class="text-muted">&mdash;</span>) if replicas.empty?
22
+
23
+ leader = partition[:leader]
24
+ isrs = partition[:isrs]
25
+
26
+ badges = replicas.map do |broker_id|
27
+ css = if broker_id == leader
28
+ "badge-primary"
29
+ elsif isrs.include?(broker_id)
30
+ "badge-info"
31
+ else
32
+ "badge-warning"
33
+ end
34
+
35
+ broker_id_badge(broker_id, css, link: link)
36
+ end.join(" ")
37
+
38
+ "#{badges}#{broker_count_note(replicas.size, "replicas")}"
39
+ end
40
+
41
+ # Renders the in-sync (ISR) broker ids for a partition as badges, with the leader
42
+ # emphasized. An empty ISR set (fully under-replicated partition) renders a dash.
43
+ #
44
+ # @param partition [Hash, Ui::Models::Partition] partition metadata
45
+ # @param link [Boolean] when true, each broker badge links to its broker details page
46
+ # (Pro-only; OSS has no per-broker view so it renders plain badges)
47
+ # @return [String] in-sync broker ids as badges with a count subtext
48
+ def partition_in_sync_brokers(partition, link: false)
49
+ isrs = partition[:isrs]
50
+
51
+ return %(<span class="text-muted">&mdash;</span>) if isrs.empty?
52
+
53
+ leader = partition[:leader]
54
+
55
+ badges = isrs.map do |broker_id|
56
+ css = (broker_id == leader) ? "badge-primary" : "badge-success"
57
+
58
+ broker_id_badge(broker_id, css, link: link)
59
+ end.join(" ")
60
+
61
+ "#{badges}#{broker_count_note(isrs.size, "in-sync")}"
62
+ end
63
+
64
+ # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
65
+ # lso risk state info
66
+ # @return [String] background classes for row marking
67
+ def lso_risk_state_bg(details)
68
+ case details.lso_risk_state
69
+ when :active
70
+ ""
71
+ when :at_risk
72
+ "bg-warning bg-opacity-25"
73
+ when :stopped
74
+ "bg-error bg-opacity-25"
75
+ else
76
+ raise ::Karafka::Errors::UnsupportedCaseError
77
+ end
78
+ end
79
+
80
+ # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
81
+ # lso risk state info
82
+ # @return [String] background classes for row marking
83
+ def lso_risk_state_badge(details)
84
+ case details.lso_risk_state
85
+ when :active
86
+ ""
87
+ when :at_risk
88
+ "badge-warning"
89
+ when :stopped
90
+ "badge-error"
91
+ else
92
+ raise ::Karafka::Errors::UnsupportedCaseError
93
+ end
94
+ end
95
+
96
+ # @param lag [Integer] lag
97
+ # @return [String] lag if correct or `N/A` with labeled explanation
98
+ # @see #offset_with_label
99
+ def lag_with_label(lag)
100
+ if lag.negative?
101
+ title = "Not available until first offset commit"
102
+ %(<span class="badge badge-secondary" title="#{title}">N/A</span>)
103
+ else
104
+ lag.to_s
105
+ end
106
+ end
107
+
108
+ # @param topic_name [String] name of the topic for explorer path
109
+ # @param partition_id [Integer] partition for the explorer path
110
+ # @param offset [Integer] offset
111
+ # @param explore [Boolean] should we generate (when allowed) a link to message explorer
112
+ # @return [String] offset if correct or `N/A` with labeled explanation for offsets
113
+ # that are less than 0. Offset with less than 0 indicates, that the offset was not
114
+ # yet committed and there is no value we know of
115
+ def offset_with_label(topic_name, partition_id, offset, explore: false)
116
+ if offset.negative?
117
+ title = "Not available until first offset commit"
118
+ %(<span class="badge badge-secondary" title="#{title}">N/A</span>)
119
+ elsif explore
120
+ path = explorer_topics_path(topic_name, partition_id, offset)
121
+ %(<a href="#{path}">#{offset}</a>)
122
+ else
123
+ offset.to_s
124
+ end
125
+ end
126
+
127
+ # Normalizes the metric value for display. Negative values coming from statistics usually
128
+ # mean, that the value is not (yet) available.
129
+ #
130
+ # @param value [Integer]
131
+ # @return [String] input value if not negative or N/A
132
+ def normalized_metric(value)
133
+ value.negative? ? "N/A" : value.to_s
134
+ end
135
+
136
+ private
137
+
138
+ # Renders the small muted "N replicas" / "N in-sync" summary shown under the broker
139
+ # badges. Kept next to the badge rendering so the count is never duplicated with the
140
+ # numeric fallback used for older karafka-rdkafka.
141
+ #
142
+ # @param count [Integer] number of brokers
143
+ # @param label [String] summary label (e.g. "replicas", "in-sync")
144
+ # @return [String] count subtext html
145
+ def broker_count_note(count, label)
146
+ %(<div><small class="text-muted">#{count} #{label}</small></div>)
147
+ end
148
+
149
+ # Renders a single broker id badge, optionally wrapped in a link to the broker details
150
+ # page. Linking is Pro-only (OSS has no per-broker view), so it is opt-in via `link`.
151
+ #
152
+ # @param broker_id [Integer] broker id
153
+ # @param css [String] badge style class
154
+ # @param link [Boolean] whether to link the badge to the broker details page
155
+ # @return [String] badge html (optionally linked)
156
+ def broker_id_badge(broker_id, css, link:)
157
+ badge = %(<span class="badge #{css}">#{broker_id}</span>)
158
+
159
+ return badge unless link
160
+
161
+ %(<a href="#{root_path("cluster", broker_id)}" title="Broker #{broker_id} details">#{badge}</a>)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Helpers for rendering sortable column links in data tables
8
+ module SortingHelper
9
+ # Default attribute names mapped from the attributes themselves
10
+ # It makes it easier as we do not have to declare those all the time
11
+ SORT_NAMES = {
12
+ id: "ID",
13
+ partition_id: "Partition",
14
+ memory_usage: "RSS",
15
+ started_at: "Started",
16
+ committed_offset: "Committed",
17
+ last_offset: "Last",
18
+ first_offset: "First",
19
+ lo_offset: "Low",
20
+ hi_offset: "High",
21
+ ls_offset: "LSO",
22
+ lag_hybrid: "Lag",
23
+ lag_stored: "Stored",
24
+ stored_offset: "Stored",
25
+ fetch_state: "Fetch",
26
+ poll_state: "Poll",
27
+ lso_risk_state: "LSO"
28
+ }.freeze
29
+
30
+ private_constant :SORT_NAMES
31
+
32
+ # @param name [String] link value
33
+ # @param attribute [Symbol, nil] sorting attribute or nil if we provide only symbol name
34
+ # @param rev [Boolean] when set to true, arrows will be in the reverse position. This is
35
+ # used when the description in the link is reverse to data we sort. For example we have
36
+ # order on when processes were started and we display "x hours" ago but we sort on
37
+ # their age, meaning that it looks like it is the other way around. This flag allows
38
+ # us to reverse just he arrow making it look consistent with the presented data order
39
+ # @return [String] html link for sorting with arrow when attribute sort enabled
40
+ def sort_link(name, attribute = nil, rev: false)
41
+ unless attribute
42
+ attribute = name
43
+
44
+ if SORT_NAMES[attribute]
45
+ name = SORT_NAMES[attribute]
46
+ else
47
+ name = attribute.to_s.tr("_", " ").tr("?", "")
48
+ # Always capitalize the name
49
+ name = name.split.map(&:capitalize).join(" ")
50
+ end
51
+ end
52
+
53
+ arrow_both = "&#x21D5;"
54
+ arrow_down = "&#9662;"
55
+ arrow_up = "&#9652;"
56
+
57
+ desc = "#{attribute} desc"
58
+ asc = "#{attribute} asc"
59
+ path = current_path(sort: desc)
60
+ full_name = "#{name}&nbsp;#{arrow_both}"
61
+
62
+ if params.current_sort == desc
63
+ path = current_path(sort: asc)
64
+ full_name = "#{name}&nbsp;#{rev ? arrow_up : arrow_down}"
65
+ end
66
+
67
+ if params.current_sort == asc
68
+ path = current_path(sort: desc)
69
+ full_name = "#{name}&nbsp;#{rev ? arrow_down : arrow_up}"
70
+ end
71
+
72
+ "<a class=\"sort\" href=\"#{path}\">#{full_name}</a>"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end