karafka-web 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +13 -4
  4. data/CHANGELOG.md +119 -5
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +27 -24
  7. data/README.md +2 -0
  8. data/bin/rspecs +6 -0
  9. data/certs/cert_chain.pem +21 -21
  10. data/docker-compose.yml +22 -0
  11. data/karafka-web.gemspec +3 -3
  12. data/lib/karafka/web/app.rb +6 -2
  13. data/lib/karafka/web/cli.rb +51 -47
  14. data/lib/karafka/web/config.rb +33 -9
  15. data/lib/karafka/web/contracts/base.rb +32 -0
  16. data/lib/karafka/web/contracts/config.rb +63 -0
  17. data/lib/karafka/web/deserializer.rb +10 -1
  18. data/lib/karafka/web/errors.rb +29 -7
  19. data/lib/karafka/web/installer.rb +58 -148
  20. data/lib/karafka/web/management/base.rb +34 -0
  21. data/lib/karafka/web/management/clean_boot_file.rb +31 -0
  22. data/lib/karafka/web/management/create_initial_states.rb +101 -0
  23. data/lib/karafka/web/management/create_topics.rb +127 -0
  24. data/lib/karafka/web/management/delete_topics.rb +28 -0
  25. data/lib/karafka/web/management/enable.rb +82 -0
  26. data/lib/karafka/web/management/extend_boot_file.rb +37 -0
  27. data/lib/karafka/web/processing/consumer.rb +73 -17
  28. data/lib/karafka/web/processing/consumers/aggregators/base.rb +56 -0
  29. data/lib/karafka/web/processing/consumers/aggregators/metrics.rb +154 -0
  30. data/lib/karafka/web/processing/consumers/aggregators/state.rb +180 -0
  31. data/lib/karafka/web/processing/consumers/contracts/aggregated_stats.rb +32 -0
  32. data/lib/karafka/web/processing/consumers/contracts/metrics.rb +53 -0
  33. data/lib/karafka/web/processing/consumers/contracts/process.rb +19 -0
  34. data/lib/karafka/web/processing/consumers/contracts/state.rb +49 -0
  35. data/lib/karafka/web/processing/consumers/contracts/topic_stats.rb +21 -0
  36. data/lib/karafka/web/processing/consumers/metrics.rb +29 -0
  37. data/lib/karafka/web/processing/consumers/schema_manager.rb +56 -0
  38. data/lib/karafka/web/processing/consumers/state.rb +6 -9
  39. data/lib/karafka/web/processing/time_series_tracker.rb +130 -0
  40. data/lib/karafka/web/tracking/consumers/contracts/consumer_group.rb +2 -2
  41. data/lib/karafka/web/tracking/consumers/contracts/job.rb +2 -1
  42. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +14 -1
  43. data/lib/karafka/web/tracking/consumers/contracts/report.rb +10 -8
  44. data/lib/karafka/web/tracking/consumers/contracts/subscription_group.rb +2 -2
  45. data/lib/karafka/web/tracking/consumers/contracts/topic.rb +2 -2
  46. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +6 -2
  47. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +15 -1
  48. data/lib/karafka/web/tracking/consumers/reporter.rb +14 -6
  49. data/lib/karafka/web/tracking/consumers/sampler.rb +80 -39
  50. data/lib/karafka/web/tracking/contracts/error.rb +2 -1
  51. data/lib/karafka/web/ui/app.rb +20 -10
  52. data/lib/karafka/web/ui/base.rb +56 -6
  53. data/lib/karafka/web/ui/controllers/base.rb +28 -0
  54. data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
  55. data/lib/karafka/web/ui/controllers/cluster.rb +12 -6
  56. data/lib/karafka/web/ui/controllers/consumers.rb +4 -2
  57. data/lib/karafka/web/ui/controllers/dashboard.rb +32 -0
  58. data/lib/karafka/web/ui/controllers/errors.rb +19 -6
  59. data/lib/karafka/web/ui/controllers/jobs.rb +4 -2
  60. data/lib/karafka/web/ui/controllers/requests/params.rb +28 -0
  61. data/lib/karafka/web/ui/controllers/responses/redirect.rb +29 -0
  62. data/lib/karafka/web/ui/helpers/application_helper.rb +57 -14
  63. data/lib/karafka/web/ui/helpers/paths_helper.rb +48 -0
  64. data/lib/karafka/web/ui/lib/hash_proxy.rb +18 -6
  65. data/lib/karafka/web/ui/lib/paginations/base.rb +61 -0
  66. data/lib/karafka/web/ui/lib/paginations/offset_based.rb +96 -0
  67. data/lib/karafka/web/ui/lib/paginations/page_based.rb +70 -0
  68. data/lib/karafka/web/ui/lib/paginations/paginators/arrays.rb +33 -0
  69. data/lib/karafka/web/ui/lib/paginations/paginators/base.rb +23 -0
  70. data/lib/karafka/web/ui/lib/paginations/paginators/partitions.rb +52 -0
  71. data/lib/karafka/web/ui/lib/paginations/paginators/sets.rb +85 -0
  72. data/lib/karafka/web/ui/lib/paginations/watermark_offsets_based.rb +75 -0
  73. data/lib/karafka/web/ui/lib/ttl_cache.rb +82 -0
  74. data/lib/karafka/web/ui/models/cluster_info.rb +59 -0
  75. data/lib/karafka/web/ui/models/consumers_metrics.rb +46 -0
  76. data/lib/karafka/web/ui/models/{state.rb → consumers_state.rb} +6 -2
  77. data/lib/karafka/web/ui/models/health.rb +37 -7
  78. data/lib/karafka/web/ui/models/message.rb +123 -39
  79. data/lib/karafka/web/ui/models/metrics/aggregated.rb +196 -0
  80. data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +50 -0
  81. data/lib/karafka/web/ui/models/metrics/charts/topics.rb +109 -0
  82. data/lib/karafka/web/ui/models/metrics/topics.rb +101 -0
  83. data/lib/karafka/web/ui/models/partition.rb +27 -0
  84. data/lib/karafka/web/ui/models/process.rb +12 -1
  85. data/lib/karafka/web/ui/models/status.rb +110 -22
  86. data/lib/karafka/web/ui/models/visibility_filter.rb +33 -0
  87. data/lib/karafka/web/ui/pro/app.rb +87 -19
  88. data/lib/karafka/web/ui/pro/controllers/cluster.rb +11 -0
  89. data/lib/karafka/web/ui/pro/controllers/consumers.rb +13 -7
  90. data/lib/karafka/web/ui/pro/controllers/dashboard.rb +54 -0
  91. data/lib/karafka/web/ui/pro/controllers/dlq.rb +1 -2
  92. data/lib/karafka/web/ui/pro/controllers/errors.rb +46 -10
  93. data/lib/karafka/web/ui/pro/controllers/explorer.rb +145 -15
  94. data/lib/karafka/web/ui/pro/controllers/health.rb +10 -2
  95. data/lib/karafka/web/ui/pro/controllers/messages.rb +62 -0
  96. data/lib/karafka/web/ui/pro/controllers/routing.rb +44 -0
  97. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +7 -1
  98. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +1 -1
  99. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +7 -5
  100. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +3 -3
  101. data/lib/karafka/web/ui/pro/views/consumers/consumer/_metrics.erb +5 -4
  102. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +13 -4
  103. data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +3 -2
  104. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +7 -0
  105. data/lib/karafka/web/ui/pro/views/consumers/details.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/consumers/index.erb +4 -2
  107. data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +39 -0
  108. data/lib/karafka/web/ui/pro/views/dashboard/index.erb +82 -0
  109. data/lib/karafka/web/ui/pro/views/dlq/_topic.erb +1 -1
  110. data/lib/karafka/web/ui/pro/views/errors/_breadcrumbs.erb +8 -6
  111. data/lib/karafka/web/ui/pro/views/errors/_error.erb +2 -2
  112. data/lib/karafka/web/ui/pro/views/errors/_partition_option.erb +1 -1
  113. data/lib/karafka/web/ui/pro/views/errors/_table.erb +21 -0
  114. data/lib/karafka/web/ui/pro/views/errors/_title_with_select.erb +31 -0
  115. data/lib/karafka/web/ui/pro/views/errors/index.erb +9 -56
  116. data/lib/karafka/web/ui/pro/views/errors/partition.erb +17 -0
  117. data/lib/karafka/web/ui/pro/views/errors/show.erb +1 -1
  118. data/lib/karafka/web/ui/pro/views/explorer/_breadcrumbs.erb +6 -4
  119. data/lib/karafka/web/ui/pro/views/explorer/_filtered.erb +16 -0
  120. data/lib/karafka/web/ui/pro/views/explorer/_message.erb +14 -4
  121. data/lib/karafka/web/ui/pro/views/explorer/_no_topics.erb +7 -0
  122. data/lib/karafka/web/ui/pro/views/explorer/_partition_option.erb +3 -3
  123. data/lib/karafka/web/ui/pro/views/explorer/_topic.erb +1 -1
  124. data/lib/karafka/web/ui/pro/views/explorer/index.erb +12 -8
  125. data/lib/karafka/web/ui/pro/views/explorer/messages/_headers.erb +15 -0
  126. data/lib/karafka/web/ui/pro/views/explorer/messages/_key.erb +12 -0
  127. data/lib/karafka/web/ui/pro/views/explorer/partition/_details.erb +35 -0
  128. data/lib/karafka/web/ui/pro/views/explorer/partition/_messages.erb +1 -0
  129. data/lib/karafka/web/ui/pro/views/explorer/partition.erb +6 -4
  130. data/lib/karafka/web/ui/pro/views/explorer/show.erb +48 -5
  131. data/lib/karafka/web/ui/pro/views/explorer/topic/_details.erb +23 -0
  132. data/lib/karafka/web/ui/pro/views/explorer/topic/_empty.erb +3 -0
  133. data/lib/karafka/web/ui/pro/views/explorer/topic/_limited.erb +4 -0
  134. data/lib/karafka/web/ui/pro/views/explorer/topic.erb +51 -0
  135. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +16 -0
  136. data/lib/karafka/web/ui/pro/views/health/_no_data.erb +9 -0
  137. data/lib/karafka/web/ui/pro/views/health/_partition.erb +17 -15
  138. data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +40 -0
  139. data/lib/karafka/web/ui/pro/views/health/_tabs.erb +27 -0
  140. data/lib/karafka/web/ui/pro/views/health/offsets.erb +71 -0
  141. data/lib/karafka/web/ui/pro/views/health/overview.erb +68 -0
  142. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +6 -3
  143. data/lib/karafka/web/ui/pro/views/jobs/index.erb +4 -1
  144. data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +37 -0
  145. data/lib/karafka/web/ui/pro/views/routing/_detail.erb +25 -0
  146. data/lib/karafka/web/ui/pro/views/routing/_topic.erb +23 -0
  147. data/lib/karafka/web/ui/pro/views/routing/index.erb +10 -0
  148. data/lib/karafka/web/ui/pro/views/routing/show.erb +26 -0
  149. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +7 -10
  150. data/lib/karafka/web/ui/public/images/logo-gray.svg +28 -0
  151. data/lib/karafka/web/ui/public/javascripts/application.js +30 -0
  152. data/lib/karafka/web/ui/public/javascripts/chart.min.js +14 -0
  153. data/lib/karafka/web/ui/public/javascripts/charts.js +330 -0
  154. data/lib/karafka/web/ui/public/javascripts/datepicker.js +6 -0
  155. data/lib/karafka/web/ui/public/javascripts/live_poll.js +39 -12
  156. data/lib/karafka/web/ui/public/javascripts/offset_datetime.js +74 -0
  157. data/lib/karafka/web/ui/public/javascripts/tabs.js +59 -0
  158. data/lib/karafka/web/ui/public/stylesheets/application.css +11 -0
  159. data/lib/karafka/web/ui/public/stylesheets/datepicker.min.css +12 -0
  160. data/lib/karafka/web/ui/views/cluster/_no_partitions.erb +3 -0
  161. data/lib/karafka/web/ui/views/cluster/_partition.erb +20 -22
  162. data/lib/karafka/web/ui/views/cluster/index.erb +6 -1
  163. data/lib/karafka/web/ui/views/consumers/_consumer.erb +1 -1
  164. data/lib/karafka/web/ui/views/consumers/_counters.erb +6 -4
  165. data/lib/karafka/web/ui/views/consumers/_summary.erb +3 -3
  166. data/lib/karafka/web/ui/views/consumers/index.erb +3 -1
  167. data/lib/karafka/web/ui/views/dashboard/_feature_pro.erb +3 -0
  168. data/lib/karafka/web/ui/views/dashboard/_not_enough_data.erb +15 -0
  169. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -0
  170. data/lib/karafka/web/ui/views/dashboard/index.erb +95 -0
  171. data/lib/karafka/web/ui/views/errors/_detail.erb +12 -0
  172. data/lib/karafka/web/ui/views/errors/_error.erb +2 -2
  173. data/lib/karafka/web/ui/views/errors/show.erb +1 -1
  174. data/lib/karafka/web/ui/views/jobs/index.erb +3 -1
  175. data/lib/karafka/web/ui/views/layout.erb +10 -3
  176. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +8 -6
  177. data/lib/karafka/web/ui/views/routing/_detail.erb +2 -2
  178. data/lib/karafka/web/ui/views/routing/_topic.erb +1 -1
  179. data/lib/karafka/web/ui/views/routing/show.erb +1 -1
  180. data/lib/karafka/web/ui/views/shared/_brand.erb +2 -2
  181. data/lib/karafka/web/ui/views/shared/_chart.erb +14 -0
  182. data/lib/karafka/web/ui/views/shared/_content.erb +2 -2
  183. data/lib/karafka/web/ui/views/shared/_feature_pro.erb +1 -1
  184. data/lib/karafka/web/ui/views/shared/_flashes.erb +9 -0
  185. data/lib/karafka/web/ui/views/shared/_footer.erb +22 -0
  186. data/lib/karafka/web/ui/views/shared/_header.erb +15 -9
  187. data/lib/karafka/web/ui/views/shared/_live_poll.erb +7 -0
  188. data/lib/karafka/web/ui/views/shared/_navigation.erb +5 -8
  189. data/lib/karafka/web/ui/views/shared/_no_paginated_data.erb +9 -0
  190. data/lib/karafka/web/ui/views/shared/_pagination.erb +17 -13
  191. data/lib/karafka/web/ui/views/shared/_tab_nav.erb +7 -0
  192. data/lib/karafka/web/ui/views/shared/exceptions/not_found.erb +34 -32
  193. data/lib/karafka/web/ui/views/shared/exceptions/pro_only.erb +45 -43
  194. data/lib/karafka/web/ui/views/status/failures/_consumers_reports_schema_state.erb +15 -0
  195. data/lib/karafka/web/ui/views/status/failures/_enabled.erb +8 -0
  196. data/lib/karafka/web/ui/views/status/failures/_initial_consumers_metrics.erb +11 -0
  197. data/lib/karafka/web/ui/views/status/failures/{_initial_state.erb → _initial_consumers_state.erb} +3 -3
  198. data/lib/karafka/web/ui/views/status/failures/_partitions.erb +14 -6
  199. data/lib/karafka/web/ui/views/status/info/_components.erb +21 -1
  200. data/lib/karafka/web/ui/views/status/show.erb +62 -5
  201. data/lib/karafka/web/ui/views/status/successes/_enabled.erb +1 -0
  202. data/lib/karafka/web/ui/views/status/warnings/_replication.erb +19 -0
  203. data/lib/karafka/web/version.rb +1 -1
  204. data/lib/karafka/web.rb +11 -0
  205. data.tar.gz.sig +0 -0
  206. metadata +124 -39
  207. metadata.gz.sig +0 -0
  208. data/lib/karafka/web/processing/consumers/aggregator.rb +0 -130
  209. data/lib/karafka/web/tracking/contracts/base.rb +0 -34
  210. data/lib/karafka/web/ui/lib/paginate_array.rb +0 -38
  211. data/lib/karafka/web/ui/pro/views/explorer/_encryption_enabled.erb +0 -18
  212. data/lib/karafka/web/ui/pro/views/explorer/partition/_watermark_offsets.erb +0 -10
  213. data/lib/karafka/web/ui/pro/views/health/index.erb +0 -60
  214. /data/lib/karafka/web/ui/pro/views/explorer/{_detail.erb → messages/_detail.erb} +0 -0
@@ -8,7 +8,8 @@ module Karafka
8
8
  class Cluster < Base
9
9
  # List cluster info data
10
10
  def index
11
- @cluster_info = Karafka::Admin.cluster_info
11
+ # Make sure, that for the cluster view we always get the most recent cluster state
12
+ @cluster_info = Models::ClusterInfo.fetch(cached: false)
12
13
 
13
14
  partitions_total = []
14
15
 
@@ -18,11 +19,13 @@ module Karafka
18
19
  end
19
20
  end
20
21
 
21
- @partitions, @next_page = Ui::Lib::PaginateArray.new.call(
22
+ @partitions, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
22
23
  partitions_total,
23
24
  @params.current_page
24
25
  )
25
26
 
27
+ paginate(@params.current_page, !last_page)
28
+
26
29
  respond
27
30
  end
28
31
 
@@ -32,10 +35,13 @@ module Karafka
32
35
  # @return [Array<Hash>] array with topics to be displayed sorted in an alphabetical
33
36
  # order
34
37
  def displayable_topics(cluster_info)
35
- cluster_info
36
- .topics
37
- .reject { |topic| topic[:topic_name] == '__consumer_offsets' }
38
- .sort_by { |topic| topic[:topic_name] }
38
+ all = cluster_info
39
+ .topics
40
+ .sort_by { |topic| topic[:topic_name] }
41
+
42
+ return all if ::Karafka::Web.config.ui.show_internal_topics
43
+
44
+ all.reject { |topic| topic[:topic_name].start_with?('__') }
39
45
  end
40
46
  end
41
47
  end
@@ -9,13 +9,15 @@ module Karafka
9
9
  # List page with consumers
10
10
  # @note For now we load all and paginate over the squashed data.
11
11
  def index
12
- @current_state = Models::State.current!
12
+ @current_state = Models::ConsumersState.current!
13
13
  @counters = Models::Counters.new(@current_state)
14
- @processes, @next_page = Lib::PaginateArray.new.call(
14
+ @processes, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
15
15
  Models::Processes.active(@current_state),
16
16
  @params.current_page
17
17
  )
18
18
 
19
+ paginate(@params.current_page, !last_page)
20
+
19
21
  respond
20
22
  end
21
23
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ # Main Karafka Pro Web-Ui dashboard controller
8
+ class Dashboard < Ui::Controllers::Base
9
+ # View with statistics dashboard details
10
+ def index
11
+ @current_state = Models::ConsumersState.current!
12
+ @counters = Models::Counters.new(@current_state)
13
+
14
+ current_metrics = Models::ConsumersMetrics.current!
15
+
16
+ # Build the charts data using the aggregated metrics
17
+ @aggregated = Models::Metrics::Aggregated.new(
18
+ current_metrics.to_h.fetch(:aggregated)
19
+ )
20
+
21
+ # Load only historicals for the selected range
22
+ @aggregated_charts = Models::Metrics::Charts::Aggregated.new(
23
+ @aggregated, :seconds
24
+ )
25
+
26
+ respond
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -10,13 +10,15 @@ module Karafka
10
10
  class Errors < Base
11
11
  # Lists first page of the errors
12
12
  def index
13
- @previous_page, @error_messages, @next_page, = Models::Message.page(
14
- errors_topic,
15
- 0,
16
- @params.current_page
17
- )
18
-
19
13
  @watermark_offsets = Ui::Models::WatermarkOffsets.find(errors_topic, 0)
14
+ previous_offset, @error_messages, next_offset, = current_page_data
15
+
16
+ paginate(
17
+ previous_offset,
18
+ @params.current_offset,
19
+ next_offset,
20
+ @error_messages.map(&:offset)
21
+ )
20
22
 
21
23
  respond
22
24
  end
@@ -34,6 +36,17 @@ module Karafka
34
36
 
35
37
  private
36
38
 
39
+ # @return [Array] Array with requested messages as well as pagination details and other
40
+ # obtained metadata
41
+ def current_page_data
42
+ Models::Message.offset_page(
43
+ errors_topic,
44
+ 0,
45
+ @params.current_offset,
46
+ @watermark_offsets
47
+ )
48
+ end
49
+
37
50
  # @return [String] errors topic
38
51
  def errors_topic
39
52
  ::Karafka::Web.config.topics.errors
@@ -8,7 +8,7 @@ module Karafka
8
8
  class Jobs < Base
9
9
  # Lists jobs
10
10
  def index
11
- current_state = Models::State.current!
11
+ current_state = Models::ConsumersState.current!
12
12
  processes = Models::Processes.active(current_state)
13
13
 
14
14
  # Aggregate jobs and inject the process info into them for better reporting
@@ -19,11 +19,13 @@ module Karafka
19
19
  end
20
20
  end
21
21
 
22
- @jobs, @next_page = Ui::Lib::PaginateArray.new.call(
22
+ @jobs, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
23
23
  jobs_total,
24
24
  @params.current_page
25
25
  )
26
26
 
27
+ paginate(@params.current_page, !last_page)
28
+
27
29
  respond
28
30
  end
29
31
  end
@@ -8,6 +8,17 @@ module Karafka
8
8
  module Requests
9
9
  # Internal representation of params with sane sanitization
10
10
  class Params
11
+ # What ranges we support for charts
12
+ # Anything else will be rejected
13
+ ALLOWED_RANGES = %w[
14
+ seconds
15
+ minutes
16
+ hours
17
+ days
18
+ ].freeze
19
+
20
+ private_constant :ALLOWED_RANGES
21
+
11
22
  # @param request_params [Hash] raw hash with params
12
23
  def initialize(request_params)
13
24
  @request_params = request_params
@@ -22,6 +33,23 @@ module Karafka
22
33
  page.positive? ? page : 1
23
34
  end
24
35
  end
36
+
37
+ # @return [String] Range type for charts we want to fetch
38
+ def current_range
39
+ candidate = @request_params.fetch('range', 'seconds')
40
+ candidate = ALLOWED_RANGES.first unless ALLOWED_RANGES.include?(candidate)
41
+ candidate.to_sym
42
+ end
43
+
44
+ # @return [Integer] offset from which we want to start. `-1` indicates, that we want
45
+ # to show the first page discovered based on the high watermark offset. If no offset
46
+ # is provided, we go with the high offset first page approach
47
+ def current_offset
48
+ @current_offset ||= begin
49
+ offset = @request_params.fetch('offset', -1).to_i
50
+ offset < -1 ? -1 : offset
51
+ end
52
+ end
25
53
  end
26
54
  end
27
55
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ module Responses
8
+ # Representation of a redirect response with optional flash messages
9
+ class Redirect
10
+ attr_reader :path, :flashes
11
+
12
+ # @param path [String, Symbol] relative (without root path) path where we want to be
13
+ # redirected or `:back` to use referer back
14
+ # @param flashes [Hash] hash where key is the flash type and value is the message
15
+ def initialize(path = :back, flashes = {})
16
+ @path = path
17
+ @flashes = flashes
18
+ end
19
+
20
+ # @return [Boolean] are we going back via referer and not explicit path
21
+ def back?
22
+ @path == :back
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -8,18 +8,6 @@ module Karafka
8
8
  module Helpers
9
9
  # Main application helper
10
10
  module ApplicationHelper
11
- # Generates a full path with the root path out of the provided arguments
12
- #
13
- # @param args [Array<String, Numeric>] arguments that will make the path
14
- # @return [String] path from the root
15
- #
16
- # @note This needs to be done that way with the `#root_path` because the web UI can be
17
- # mounted in a sub-path and we need to make sure our all paths are relative to "our"
18
- # root, not the root of the app in which it was mounted.
19
- def root_path(*args)
20
- "#{env.fetch('SCRIPT_NAME')}/#{args.join('/')}"
21
- end
22
-
23
11
  # Adds active class to the current location in the nav if needed
24
12
  # @param location [Hash]
25
13
  def nav_class(location)
@@ -41,7 +29,7 @@ module Karafka
41
29
 
42
30
  # Renders per scope breadcrumbs
43
31
  def render_breadcrumbs
44
- scope = request.path.gsub(root_path, '').split('/')[0]
32
+ scope = request.path.delete_prefix(root_path).split('/')[0]
45
33
 
46
34
  render "#{scope}/_breadcrumbs"
47
35
  end
@@ -137,19 +125,53 @@ module Karafka
137
125
  %(<span title="#{stamp}">#{time}</span>)
138
126
  end
139
127
 
128
+ # @param lag [Integer] lag
129
+ # @return [String] lag if correct or `N/A` with labeled explanation
130
+ # @see #offset_with_label
131
+ def lag_with_label(lag)
132
+ if lag.negative?
133
+ title = 'Not available until first offset commit'
134
+ %(<span class="badge bg-secondary" title="#{title}">N/A</span>)
135
+ else
136
+ lag.to_s
137
+ end
138
+ end
139
+
140
+ # @param topic_name [String] name of the topic for explorer path
141
+ # @param partition_id [Integer] partition for the explorer path
140
142
  # @param offset [Integer] offset
143
+ # @param explore [Boolean] should we generate (when allowed) a link to message explorer
141
144
  # @return [String] offset if correct or `N/A` with labeled explanation for offsets
142
145
  # that are less than 0. Offset with less than 0 indicates, that the offset was not
143
146
  # yet committed and there is no value we know of
144
- def offset_with_label(offset)
147
+ def offset_with_label(topic_name, partition_id, offset, explore: false)
145
148
  if offset.negative?
146
149
  title = 'Not available until first offset commit'
147
150
  %(<span class="badge bg-secondary" title="#{title}">N/A</span>)
151
+ elsif explore
152
+ path = explorer_path(topic_name, partition_id, offset)
153
+ %(<a href="#{path}">#{offset}</a>)
148
154
  else
149
155
  offset.to_s
150
156
  end
151
157
  end
152
158
 
159
+ # @param details [::Karafka::Web::Ui::Models::Partition] partition information with
160
+ # lso risk state info
161
+ # @return [String] background classes for row marking
162
+ def lso_risk_state_bg(details)
163
+ case details.lso_risk_state
164
+ when :active
165
+ ''
166
+ when :at_risk
167
+ 'bg-warning bg-opacity-25'
168
+ when :stopped
169
+ 'bg-danger bg-opacity-25'
170
+ else
171
+ raise ::Karafka::Errors::UnsupportedCaseError
172
+ end
173
+ end
174
+
153
175
  # Returns the view title html code
154
176
  #
155
177
  # @param title [String] page title
@@ -168,6 +190,27 @@ module Karafka
168
190
  </div>
169
191
  HTML
170
192
  end
193
+
194
+ # @param hash [Hash] we want to flatten
195
+ # @param parent_key [String] key for recursion
196
+ # @param result [Hash] result for recursion
197
+ # @return [Hash]
198
+ def flat_hash(hash, parent_key = nil, result = {})
199
+ hash.each do |key, value|
200
+ current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
201
+ if value.is_a?(Hash)
202
+ flat_hash(value, current_key, result)
203
+ elsif value.is_a?(Array)
204
+ value.each_with_index do |item, index|
205
+ flat_hash({ index => item }, current_key, result)
206
+ end
207
+ else
208
+ result[current_key] = value
209
+ end
210
+ end
211
+
212
+ result
213
+ end
171
214
  end
172
215
  end
173
216
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Helpers
7
+ # Helper for web ui paths builders
8
+ module PathsHelper
9
+ # Generates a full path with the root path out of the provided arguments
10
+ #
11
+ # @param args [Array<String, Numeric>] arguments that will make the path
12
+ # @return [String] path from the root
13
+ #
14
+ # @note This needs to be done that way with the `#root_path` because the web UI can be
15
+ # mounted in a sub-path and we need to make sure our all paths are relative to "our"
16
+ # root, not the root of the app in which it was mounted.
17
+ def root_path(*args)
18
+ "#{env.fetch('SCRIPT_NAME')}/#{args.join('/')}"
19
+ end
20
+
21
+ # Generates a full path to any asset with our web-ui version. We ship all assets with
22
+ # the version in the url to prevent those assets from being used after update. After
23
+ # each web-ui update, assets are going to be re-fetched as the url will change
24
+ #
25
+ # @param local_path [String] local path to the asset
26
+ # @return [String] full path to the asst including correct root path
27
+ def asset_path(local_path)
28
+ root_path("assets/#{Karafka::Web::VERSION}/#{local_path}")
29
+ end
30
+
31
+ # Helps build explorer paths. We often link offsets to proper messages, etc so this
32
+ # allows us to short-track this
33
+ # @param topic_name [String, nil] name of the topic where we want to go within the
34
+ # explorer or nil if we want to just go to the explorer root
35
+ # @param partition_id [Integer, nil] partition we want to display in the explorer or nil
36
+ # if we want to go to the topic root
37
+ # @param offset [Integer, nil] offset of particular message or nil of we want to just go
38
+ # to the partition root
39
+ # @param action [String, nil] specific routed action or nil
40
+ # @return [String] path to the expected location
41
+ def explorer_path(topic_name = nil, partition_id = nil, offset = nil, action = nil)
42
+ root_path(*['explorer', topic_name, partition_id, offset, action].compact)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -12,16 +12,17 @@ module Karafka
12
12
  # It is mostly used for flat hashes.
13
13
  #
14
14
  # It is in a way similar to openstruct but has abilities to dive deep into objects
15
+ #
16
+ # It is not super fast but it is enough for the UI and how deep structures we have.
15
17
  class HashProxy
18
+ extend Forwardable
19
+
20
+ def_delegators :@hash, :[], :[]=, :key?, :each, :find
21
+
16
22
  # @param hash [Hash] hash we want to convert to a proxy
17
23
  def initialize(hash)
18
24
  @hash = hash
19
- end
20
-
21
- # @param key [Object] hash key
22
- # @return [Object] key content or nil if missing
23
- def [](key)
24
- @hash[key]
25
+ @visited = []
25
26
  end
26
27
 
27
28
  # @return [Original hash]
@@ -35,7 +36,12 @@ module Karafka
35
36
  def method_missing(method_name, *args, &block)
36
37
  return super unless args.empty? && block.nil?
37
38
 
39
+ @visited.clear
40
+
38
41
  result = deep_find(@hash, method_name.to_sym)
42
+
43
+ @visited.clear
44
+
39
45
  result.nil? ? super : result
40
46
  end
41
47
 
@@ -51,6 +57,12 @@ module Karafka
51
57
  # @param obj [Object] local scope of iterating
52
58
  # @param key [Symbol, String] key we are looking for
53
59
  def deep_find(obj, key)
60
+ # Prevent circular dependency lookups by making sure we do not check the same object
61
+ # multiple times
62
+ return nil if @visited.include?(obj)
63
+
64
+ @visited << obj
65
+
54
66
  if obj.respond_to?(:key?) && obj.key?(key)
55
67
  obj[key]
56
68
  elsif obj.respond_to?(:each)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Lib
7
+ # Namespace for all the types of pagination engines we want to support
8
+ module Paginations
9
+ # Abstraction on top of pagination, so we can alter pagination key and other things
10
+ # for non-standard pagination views (non page based, etc)
11
+ #
12
+ # @note We do not use `_page` explicitly to indicate, that the page scope may not operate
13
+ # on numerable pages (1,2,3,4) but can operate on offsets or times, etc. `_offset` is
14
+ # more general and may refer to many types of pagination.
15
+ class Base
16
+ attr_reader :previous_offset, :current_offset, :next_offset
17
+
18
+ # @return [Boolean] Should we show pagination at all
19
+ def paginate?
20
+ raise NotImplementedError, 'Implement in a subclass'
21
+ end
22
+
23
+ # @return [Boolean] Should first offset link be active. If false, the first offset link
24
+ # will be disabled
25
+ def first_offset?
26
+ raise NotImplementedError, 'Implement in a subclass'
27
+ end
28
+
29
+ # @return [String] first offset url value
30
+ def first_offset
31
+ raise NotImplementedError, 'Implement in a subclass'
32
+ end
33
+
34
+ # @return [Boolean] Should previous offset link be active. If false, the previous
35
+ # offset link will be disabled
36
+ def previous_offset?
37
+ raise NotImplementedError, 'Implement in a subclass'
38
+ end
39
+
40
+ # @return [Boolean] Should we show current offset. If false, the current offset link
41
+ # will not be visible at all. Useful for non-linear pagination.
42
+ def current_offset?
43
+ raise NotImplementedError, 'Implement in a subclass'
44
+ end
45
+
46
+ # @return [Boolean] Should we show next offset pagination. If false, next offset link
47
+ # will be disabled.
48
+ def next_offset?
49
+ raise NotImplementedError, 'Implement in a subclass'
50
+ end
51
+
52
+ # @return [String] the url offset key
53
+ def offset_key
54
+ raise NotImplementedError, 'Implement in a subclass'
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Lib
7
+ module Paginations
8
+ # Kafka offset based pagination backend
9
+ #
10
+ # Allows us to support paginating over offsets
11
+ class OffsetBased < Base
12
+ # @param previous_offset [Integer, false] previous offset or false if should not be
13
+ # presented
14
+ # @param current_offset [Integer] current offset
15
+ # @param next_offset [Integer, Boolean] should we show next offset page button. If
16
+ # false it will not be presented.
17
+ # @param visible_offsets [Array<Integer>] offsets that are visible in the paginated
18
+ # view. It is needed for the current page label
19
+ def initialize(
20
+ previous_offset,
21
+ current_offset,
22
+ next_offset,
23
+ visible_offsets
24
+ )
25
+ @previous_offset = previous_offset
26
+ @current_offset = current_offset
27
+ @next_offset = next_offset
28
+ @visible_offsets = visible_offsets
29
+ super()
30
+ end
31
+
32
+ # Show pagination only when there is more than one page of results to be presented
33
+ #
34
+ # @return [Boolean]
35
+ def paginate?
36
+ @current_offset && (!!@previous_offset || !!@next_offset)
37
+ end
38
+
39
+ # @return [Boolean] active only when we are not on the first page. First page is always
40
+ # indicated by the current offset being -1. If there is someone that sets up the
41
+ # current offset to a value equal to the last message in the topic partition, we do
42
+ # not consider it as a first page and we allow to "reset" to -1 via the first page
43
+ # button
44
+ def first_offset?
45
+ @current_offset != -1
46
+ end
47
+
48
+ # @return [Boolean] first page offset is always nothing because we use the default -1
49
+ # for the offset.
50
+ def first_offset
51
+ false
52
+ end
53
+
54
+ # @return [Boolean] Active previous page link when it is not the first page
55
+ def previous_offset?
56
+ !!@previous_offset
57
+ end
58
+
59
+ # @return [Boolean] We show current label with offsets that are present on the given
60
+ # page
61
+ def current_offset?
62
+ true
63
+ end
64
+
65
+ # @return [Boolean] move to the next page if not false. False indicates, that there is
66
+ # no next page to move to
67
+ def next_offset?
68
+ !!@next_offset
69
+ end
70
+
71
+ # If there is no next offset, we point to 0 as there should be no smaller offset than
72
+ # that in Kafka ever
73
+ # @return [Integer]
74
+ def next_offset
75
+ next_offset? ? @next_offset : 0
76
+ end
77
+
78
+ # @return [String] label of the current page. It is combined out of the first and
79
+ # the last offsets to show the range where we are. It will be empty if no offsets
80
+ # but this is not a problem as then we should not display pagination at all
81
+ def current_label
82
+ first = @visible_offsets.first
83
+ last = @visible_offsets.last
84
+ [first, last].compact.uniq.join(' - ').to_s
85
+ end
86
+
87
+ # @return [String] for offset based pagination we use the offset param name
88
+ def offset_key
89
+ 'offset'
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Lib
7
+ module Paginations
8
+ # Regular page-based pagination engine
9
+ class PageBased < Base
10
+ # @param current_offset [Integer] current page
11
+ # @param show_next_offset [Boolean] should we show next page
12
+ # (value is computed automatically)
13
+ def initialize(
14
+ current_offset,
15
+ show_next_offset
16
+ )
17
+ @previous_offset = current_offset - 1
18
+ @current_offset = current_offset
19
+ @next_offset = show_next_offset ? current_offset + 1 : false
20
+ super()
21
+ end
22
+
23
+ # Show pagination only when there is more than one page
24
+ # @return [Boolean]
25
+ def paginate?
26
+ @current_offset && (@current_offset > 1 || !!@next_offset)
27
+ end
28
+
29
+ # @return [Boolean] active the first page link when we are not on the first page
30
+ def first_offset?
31
+ @current_offset > 1
32
+ end
33
+
34
+ # @return [Boolean] first page for page based pagination is always empty as it moves us
35
+ # to the initial page so we do not include any page info
36
+ def first_offset
37
+ false
38
+ end
39
+
40
+ # @return [Boolean] Active previous page link when it is not the first page
41
+ def previous_offset?
42
+ @current_offset > 1
43
+ end
44
+
45
+ # @return [Boolean] always show current offset pagination value
46
+ def current_offset?
47
+ true
48
+ end
49
+
50
+ # @return [String] label of the current page
51
+ def current_label
52
+ @current_offset.to_s
53
+ end
54
+
55
+ # @return [Boolean] move to the next page if not false. False indicates, that there is
56
+ # no next page to move to
57
+ def next_offset?
58
+ @next_offset
59
+ end
60
+
61
+ # @return [String] for page pages pagination, always use page as the url value
62
+ def offset_key
63
+ 'page'
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end