karafka-web 0.7.10 → 0.8.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +18 -5
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +63 -0
  6. data/Gemfile.lock +22 -22
  7. data/docker-compose.yml +3 -1
  8. data/karafka-web.gemspec +2 -2
  9. data/lib/karafka/web/config.rb +16 -3
  10. data/lib/karafka/web/contracts/config.rb +7 -2
  11. data/lib/karafka/web/errors.rb +12 -0
  12. data/lib/karafka/web/inflector.rb +33 -0
  13. data/lib/karafka/web/installer.rb +20 -11
  14. data/lib/karafka/web/management/actions/base.rb +36 -0
  15. data/lib/karafka/web/management/actions/clean_boot_file.rb +33 -0
  16. data/lib/karafka/web/management/actions/create_initial_states.rb +77 -0
  17. data/lib/karafka/web/management/actions/create_topics.rb +139 -0
  18. data/lib/karafka/web/management/actions/delete_topics.rb +30 -0
  19. data/lib/karafka/web/management/actions/enable.rb +117 -0
  20. data/lib/karafka/web/management/actions/extend_boot_file.rb +39 -0
  21. data/lib/karafka/web/management/actions/migrate_states_data.rb +18 -0
  22. data/lib/karafka/web/management/migrations/0_base.rb +58 -0
  23. data/lib/karafka/web/management/migrations/0_set_initial_consumers_metrics.rb +36 -0
  24. data/lib/karafka/web/management/migrations/0_set_initial_consumers_state.rb +43 -0
  25. data/lib/karafka/web/management/migrations/1699543515_fill_missing_received_and_sent_bytes_in_consumers_metrics.rb +26 -0
  26. data/lib/karafka/web/management/migrations/1699543515_fill_missing_received_and_sent_bytes_in_consumers_state.rb +23 -0
  27. data/lib/karafka/web/management/migrations/1700234522_introduce_waiting_in_consumers_metrics.rb +24 -0
  28. data/lib/karafka/web/management/migrations/1700234522_introduce_waiting_in_consumers_state.rb +20 -0
  29. data/lib/karafka/web/management/migrations/1700234522_remove_processing_from_consumers_metrics.rb +24 -0
  30. data/lib/karafka/web/management/migrations/1700234522_remove_processing_from_consumers_state.rb +20 -0
  31. data/lib/karafka/web/management/migrations/1704722380_split_listeners_into_active_and_paused_in_metrics.rb +36 -0
  32. data/lib/karafka/web/management/migrations/1704722380_split_listeners_into_active_and_paused_in_states.rb +32 -0
  33. data/lib/karafka/web/management/migrator.rb +117 -0
  34. data/lib/karafka/web/processing/consumer.rb +39 -38
  35. data/lib/karafka/web/processing/consumers/aggregators/metrics.rb +2 -3
  36. data/lib/karafka/web/processing/consumers/aggregators/state.rb +8 -3
  37. data/lib/karafka/web/processing/consumers/contracts/aggregated_stats.rb +5 -1
  38. data/lib/karafka/web/processing/publisher.rb +59 -0
  39. data/lib/karafka/web/tracking/consumers/contracts/job.rb +3 -2
  40. data/lib/karafka/web/tracking/consumers/contracts/partition.rb +1 -0
  41. data/lib/karafka/web/tracking/consumers/contracts/report.rb +6 -1
  42. data/lib/karafka/web/tracking/consumers/contracts/subscription_group.rb +10 -1
  43. data/lib/karafka/web/tracking/consumers/listeners/connections.rb +49 -0
  44. data/lib/karafka/web/tracking/consumers/listeners/pausing.rb +7 -4
  45. data/lib/karafka/web/tracking/consumers/listeners/processing.rb +78 -70
  46. data/lib/karafka/web/tracking/consumers/listeners/statistics.rb +40 -13
  47. data/lib/karafka/web/tracking/consumers/sampler.rb +82 -25
  48. data/lib/karafka/web/tracking/helpers/ttls/array.rb +72 -0
  49. data/lib/karafka/web/tracking/helpers/ttls/hash.rb +34 -0
  50. data/lib/karafka/web/tracking/helpers/ttls/stats.rb +49 -0
  51. data/lib/karafka/web/tracking/helpers/ttls/windows.rb +32 -0
  52. data/lib/karafka/web/tracking/reporter.rb +1 -0
  53. data/lib/karafka/web/ui/app.rb +22 -4
  54. data/lib/karafka/web/ui/base.rb +18 -2
  55. data/lib/karafka/web/ui/controllers/base.rb +34 -4
  56. data/lib/karafka/web/ui/controllers/become_pro.rb +1 -1
  57. data/lib/karafka/web/ui/controllers/cluster.rb +33 -9
  58. data/lib/karafka/web/ui/controllers/consumers.rb +8 -2
  59. data/lib/karafka/web/ui/controllers/dashboard.rb +2 -2
  60. data/lib/karafka/web/ui/controllers/errors.rb +2 -2
  61. data/lib/karafka/web/ui/controllers/jobs.rb +55 -5
  62. data/lib/karafka/web/ui/controllers/requests/params.rb +5 -0
  63. data/lib/karafka/web/ui/controllers/responses/deny.rb +15 -0
  64. data/lib/karafka/web/ui/controllers/responses/file.rb +23 -0
  65. data/lib/karafka/web/ui/controllers/responses/{data.rb → render.rb} +3 -3
  66. data/lib/karafka/web/ui/controllers/routing.rb +11 -2
  67. data/lib/karafka/web/ui/controllers/status.rb +1 -1
  68. data/lib/karafka/web/ui/helpers/application_helper.rb +70 -0
  69. data/lib/karafka/web/ui/lib/hash_proxy.rb +29 -14
  70. data/lib/karafka/web/ui/lib/sorter.rb +170 -0
  71. data/lib/karafka/web/ui/models/counters.rb +6 -0
  72. data/lib/karafka/web/ui/models/health.rb +23 -2
  73. data/lib/karafka/web/ui/models/jobs.rb +48 -0
  74. data/lib/karafka/web/ui/models/metrics/charts/aggregated.rb +33 -0
  75. data/lib/karafka/web/ui/models/metrics/charts/topics.rb +1 -1
  76. data/lib/karafka/web/ui/models/process.rb +2 -1
  77. data/lib/karafka/web/ui/models/status.rb +23 -7
  78. data/lib/karafka/web/ui/models/topic.rb +3 -1
  79. data/lib/karafka/web/ui/models/visibility_filter.rb +16 -0
  80. data/lib/karafka/web/ui/pro/app.rb +44 -6
  81. data/lib/karafka/web/ui/pro/controllers/cluster.rb +1 -0
  82. data/lib/karafka/web/ui/pro/controllers/consumers.rb +52 -6
  83. data/lib/karafka/web/ui/pro/controllers/dashboard.rb +1 -1
  84. data/lib/karafka/web/ui/pro/controllers/dlq.rb +1 -1
  85. data/lib/karafka/web/ui/pro/controllers/errors.rb +3 -3
  86. data/lib/karafka/web/ui/pro/controllers/explorer.rb +8 -8
  87. data/lib/karafka/web/ui/pro/controllers/health.rb +34 -2
  88. data/lib/karafka/web/ui/pro/controllers/jobs.rb +11 -0
  89. data/lib/karafka/web/ui/pro/controllers/messages.rb +42 -0
  90. data/lib/karafka/web/ui/pro/controllers/routing.rb +11 -2
  91. data/lib/karafka/web/ui/pro/views/consumers/_breadcrumbs.erb +8 -2
  92. data/lib/karafka/web/ui/pro/views/consumers/_consumer.erb +14 -8
  93. data/lib/karafka/web/ui/pro/views/consumers/_counters.erb +8 -6
  94. data/lib/karafka/web/ui/pro/views/consumers/consumer/_job.erb +4 -1
  95. data/lib/karafka/web/ui/pro/views/consumers/consumer/_no_jobs.erb +1 -1
  96. data/lib/karafka/web/ui/pro/views/consumers/consumer/_partition.erb +1 -3
  97. data/lib/karafka/web/ui/pro/views/consumers/consumer/_subscription_group.erb +28 -11
  98. data/lib/karafka/web/ui/pro/views/consumers/consumer/_tabs.erb +10 -3
  99. data/lib/karafka/web/ui/pro/views/consumers/index.erb +3 -3
  100. data/lib/karafka/web/ui/pro/views/consumers/pending_jobs.erb +43 -0
  101. data/lib/karafka/web/ui/pro/views/consumers/{jobs.erb → running_jobs.erb} +11 -10
  102. data/lib/karafka/web/ui/pro/views/dashboard/index.erb +7 -1
  103. data/lib/karafka/web/ui/pro/views/explorer/message/_message_actions.erb +18 -0
  104. data/lib/karafka/web/ui/pro/views/explorer/message/_metadata.erb +43 -0
  105. data/lib/karafka/web/ui/pro/views/explorer/message/_payload.erb +21 -0
  106. data/lib/karafka/web/ui/pro/views/explorer/message/_payload_actions.erb +19 -0
  107. data/lib/karafka/web/ui/pro/views/explorer/show.erb +9 -84
  108. data/lib/karafka/web/ui/pro/views/health/_breadcrumbs.erb +8 -0
  109. data/lib/karafka/web/ui/pro/views/health/_partition.erb +1 -3
  110. data/lib/karafka/web/ui/pro/views/health/_partition_offset.erb +4 -4
  111. data/lib/karafka/web/ui/pro/views/health/_partition_times.erb +32 -0
  112. data/lib/karafka/web/ui/pro/views/health/_tabs.erb +9 -0
  113. data/lib/karafka/web/ui/pro/views/health/changes.erb +66 -0
  114. data/lib/karafka/web/ui/pro/views/health/offsets.erb +14 -14
  115. data/lib/karafka/web/ui/pro/views/health/overview.erb +11 -11
  116. data/lib/karafka/web/ui/pro/views/jobs/_job.erb +1 -1
  117. data/lib/karafka/web/ui/pro/views/jobs/_no_jobs.erb +1 -1
  118. data/lib/karafka/web/ui/pro/views/jobs/pending.erb +39 -0
  119. data/lib/karafka/web/ui/pro/views/jobs/running.erb +39 -0
  120. data/lib/karafka/web/ui/pro/views/routing/_consumer_group.erb +2 -2
  121. data/lib/karafka/web/ui/pro/views/routing/_topic.erb +9 -0
  122. data/lib/karafka/web/ui/pro/views/routing/show.erb +12 -0
  123. data/lib/karafka/web/ui/pro/views/shared/_navigation.erb +1 -1
  124. data/lib/karafka/web/ui/public/javascripts/application.js +10 -0
  125. data/lib/karafka/web/ui/public/stylesheets/application.css +4 -0
  126. data/lib/karafka/web/ui/views/cluster/_breadcrumbs.erb +16 -0
  127. data/lib/karafka/web/ui/views/cluster/_tabs.erb +27 -0
  128. data/lib/karafka/web/ui/views/cluster/brokers.erb +27 -0
  129. data/lib/karafka/web/ui/views/cluster/topics.erb +35 -0
  130. data/lib/karafka/web/ui/views/consumers/_counters.erb +8 -6
  131. data/lib/karafka/web/ui/views/consumers/_summary.erb +2 -2
  132. data/lib/karafka/web/ui/views/consumers/index.erb +3 -3
  133. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +23 -7
  134. data/lib/karafka/web/ui/views/dashboard/index.erb +19 -8
  135. data/lib/karafka/web/ui/views/errors/show.erb +2 -23
  136. data/lib/karafka/web/ui/views/jobs/_breadcrumbs.erb +17 -1
  137. data/lib/karafka/web/ui/views/jobs/_job.erb +1 -1
  138. data/lib/karafka/web/ui/views/jobs/_no_jobs.erb +1 -1
  139. data/lib/karafka/web/ui/views/jobs/_tabs.erb +27 -0
  140. data/lib/karafka/web/ui/views/jobs/{index.erb → pending.erb} +9 -7
  141. data/lib/karafka/web/ui/{pro/views/jobs/index.erb → views/jobs/running.erb} +9 -11
  142. data/lib/karafka/web/ui/views/routing/_consumer_group.erb +14 -12
  143. data/lib/karafka/web/ui/views/shared/_navigation.erb +1 -1
  144. data/lib/karafka/web/ui/views/shared/_pagination.erb +1 -1
  145. data/lib/karafka/web/ui/views/shared/exceptions/not_allowed.erb +37 -0
  146. data/lib/karafka/web/ui/views/status/show.erb +17 -2
  147. data/lib/karafka/web/ui/views/status/warnings/_routing_topics_presence.erb +15 -0
  148. data/lib/karafka/web/version.rb +1 -1
  149. data/lib/karafka/web.rb +6 -2
  150. data.tar.gz.sig +0 -0
  151. metadata +61 -26
  152. metadata.gz.sig +0 -0
  153. data/lib/karafka/web/management/base.rb +0 -34
  154. data/lib/karafka/web/management/clean_boot_file.rb +0 -31
  155. data/lib/karafka/web/management/create_initial_states.rb +0 -101
  156. data/lib/karafka/web/management/create_topics.rb +0 -133
  157. data/lib/karafka/web/management/delete_topics.rb +0 -28
  158. data/lib/karafka/web/management/enable.rb +0 -102
  159. data/lib/karafka/web/management/extend_boot_file.rb +0 -37
  160. data/lib/karafka/web/tracking/ttl_array.rb +0 -59
  161. data/lib/karafka/web/tracking/ttl_hash.rb +0 -16
  162. data/lib/karafka/web/ui/pro/views/dashboard/_ranges_selector.erb +0 -39
  163. data/lib/karafka/web/ui/views/cluster/index.erb +0 -74
@@ -48,9 +48,18 @@ module Karafka
48
48
  end
49
49
  end
50
50
 
51
- r.get 'jobs' do
51
+ r.on 'jobs' do
52
52
  controller = Controllers::Jobs.new(params)
53
- controller.index
53
+
54
+ r.get 'running' do
55
+ controller.running
56
+ end
57
+
58
+ r.get 'pending' do
59
+ controller.pending
60
+ end
61
+
62
+ r.redirect root_path('jobs/running')
54
63
  end
55
64
 
56
65
  r.on 'routing' do
@@ -65,9 +74,18 @@ module Karafka
65
74
  end
66
75
  end
67
76
 
68
- r.get 'cluster' do
77
+ r.on 'cluster' do
69
78
  controller = Controllers::Cluster.new(params)
70
- controller.index
79
+
80
+ r.get 'brokers' do
81
+ controller.brokers
82
+ end
83
+
84
+ r.get 'topics' do
85
+ controller.topics
86
+ end
87
+
88
+ r.redirect root_path('cluster/brokers')
71
89
  end
72
90
 
73
91
  r.on 'errors' do
@@ -55,10 +55,16 @@ module Karafka
55
55
 
56
56
  plugin :custom_block_results
57
57
 
58
- handle_block_result Controllers::Responses::Data do |result|
58
+ handle_block_result Controllers::Responses::Render do |result|
59
59
  render_response(result)
60
60
  end
61
61
 
62
+ handle_block_result Controllers::Responses::Deny do
63
+ @error = true
64
+ response.status = 403
65
+ view 'shared/exceptions/not_allowed'
66
+ end
67
+
62
68
  # Redirect either to referer back or to the desired path
63
69
  handle_block_result Controllers::Responses::Redirect do |result|
64
70
  # Map redirect flashes (if any) to Roda flash messages
@@ -67,6 +73,12 @@ module Karafka
67
73
  response.redirect result.back? ? request.referer : root_path(result.path)
68
74
  end
69
75
 
76
+ handle_block_result Controllers::Responses::File do |result|
77
+ response.headers['Content-Type'] = 'application/octet-stream'
78
+ response.headers['Content-Disposition'] = "attachment; filename=\"#{result.file_name}\""
79
+ response.write result.content
80
+ end
81
+
70
82
  # Display appropriate error specific to a given error type
71
83
  plugin :error_handler, classes: [
72
84
  ::Rdkafka::RdkafkaError,
@@ -111,10 +123,14 @@ module Karafka
111
123
  raise Errors::Ui::NotFoundError
112
124
  end
113
125
 
114
- # Allows us to build current path with additional params
126
+ # Allows us to build current path with additional params + it merges existing params into
127
+ # the query data. Query data takes priority over request params.
115
128
  # @param query_data [Hash] query params we want to add to the current path
116
129
  path :current do |query_data = {}|
117
130
  q = query_data
131
+ .transform_values(&:to_s)
132
+ .transform_keys(&:to_s)
133
+ .then { |candidates| request.params.merge(candidates) }
118
134
  .select { |_, v| v }
119
135
  .map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }
120
136
  .join('&')
@@ -7,6 +7,13 @@ module Karafka
7
7
  module Controllers
8
8
  # Base controller from which all the controllers should inherit.
9
9
  class Base
10
+ class << self
11
+ # Attributes on which we can sort in a given controller
12
+ attr_accessor :sortable_attributes
13
+ end
14
+
15
+ self.sortable_attributes = []
16
+
10
17
  # @param params [Karafka::Web::Ui::Controllers::Requests::Params] request parameters
11
18
  def initialize(params)
12
19
  @params = params
@@ -14,10 +21,10 @@ module Karafka
14
21
 
15
22
  private
16
23
 
17
- # Builds the respond data object with assigned attributes based on instance variables.
24
+ # Builds the render data object with assigned attributes based on instance variables.
18
25
  #
19
- # @return [Responses::Data] data that should be used to render appropriate view
20
- def respond
26
+ # @return [Responses::Render] data that should be used to render appropriate view
27
+ def render
21
28
  attributes = {}
22
29
 
23
30
  scope = self.class.to_s.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase
@@ -30,7 +37,7 @@ module Karafka
30
37
  attributes[iv.to_s.delete('@').to_sym] = instance_variable_get(iv)
31
38
  end
32
39
 
33
- Responses::Data.new(
40
+ Responses::Render.new(
34
41
  "#{scope}/#{action}",
35
42
  attributes
36
43
  )
@@ -45,6 +52,29 @@ module Karafka
45
52
  Responses::Redirect.new(path, flashes)
46
53
  end
47
54
 
55
+ # Builds a file response object that will be used as a base to dispatch the file
56
+ #
57
+ # @param content [String] Payload we want to dispatch as a file
58
+ # @param file_name [String] name under which the browser is suppose to save the file
59
+ # @return [Responses::File] file response result
60
+ def file(content, file_name)
61
+ Responses::File.new(content, file_name)
62
+ end
63
+
64
+ # Builds a halt 403 response
65
+ def deny
66
+ Responses::Deny.new
67
+ end
68
+
69
+ # @param resources [Hash, Array, Lib::HashProxy] object for sorting
70
+ # @return [Hash, Array, Lib::HashProxy] sorted results
71
+ def refine(resources)
72
+ Lib::Sorter.new(
73
+ @params.sort,
74
+ allowed_attributes: self.class.sortable_attributes
75
+ ).call(resources)
76
+ end
77
+
48
78
  # Initializes the expected pagination engine and assigns expected arguments
49
79
  # @param args Any arguments accepted by the selected pagination engine
50
80
  def paginate(*args)
@@ -8,7 +8,7 @@ module Karafka
8
8
  class BecomePro < Base
9
9
  # Display a message, that a given feature is available only in Pro
10
10
  def show
11
- respond
11
+ render
12
12
  end
13
13
  end
14
14
  end
@@ -6,31 +6,55 @@ module Karafka
6
6
  module Controllers
7
7
  # Selects cluster info and topics basic info
8
8
  class Cluster < Base
9
- # List cluster info data
10
- def index
11
- # Make sure, that for the cluster view we always get the most recent cluster state
12
- @cluster_info = Models::ClusterInfo.fetch(cached: false)
9
+ self.sortable_attributes = %w[
10
+ broker_id
11
+ broker_name
12
+ broker_port
13
+ topic_name
14
+ partition_id
15
+ leader
16
+ replica_count
17
+ in_sync_replica_brokers
18
+ ].freeze
13
19
 
20
+ # Lists available brokers in the cluster
21
+ def brokers
22
+ @brokers = refine(cluster_info.brokers)
23
+
24
+ render
25
+ end
26
+
27
+ # List topics and partitions with details
28
+ def topics
14
29
  partitions_total = []
15
30
 
16
- displayable_topics(@cluster_info).each do |topic|
31
+ displayable_topics(cluster_info).each do |topic|
17
32
  topic[:partitions].each do |partition|
18
- partitions_total << partition.merge(topic: topic)
33
+ partitions_total << partition.merge(
34
+ topic: topic,
35
+ # Will allow sorting by name
36
+ topic_name: topic.fetch(:topic_name)
37
+ )
19
38
  end
20
39
  end
21
40
 
22
41
  @partitions, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
23
- partitions_total,
42
+ refine(partitions_total),
24
43
  @params.current_page
25
44
  )
26
45
 
27
46
  paginate(@params.current_page, !last_page)
28
47
 
29
- respond
48
+ render
30
49
  end
31
50
 
32
51
  private
33
52
 
53
+ # Make sure, that for the cluster view we always get the most recent cluster state
54
+ def cluster_info
55
+ @cluster_info ||= Models::ClusterInfo.fetch(cached: false)
56
+ end
57
+
34
58
  # @param cluster_info [Rdkafka::Metadata] cluster metadata
35
59
  # @return [Array<Hash>] array with topics to be displayed sorted in an alphabetical
36
60
  # order
@@ -39,7 +63,7 @@ module Karafka
39
63
  .topics
40
64
  .sort_by { |topic| topic[:topic_name] }
41
65
 
42
- return all if ::Karafka::Web.config.ui.show_internal_topics
66
+ return all if ::Karafka::Web.config.ui.visibility.internal_topics
43
67
 
44
68
  all.reject { |topic| topic[:topic_name].start_with?('__') }
45
69
  end
@@ -6,19 +6,25 @@ module Karafka
6
6
  module Controllers
7
7
  # Consumers (consuming processes - `karafka server`) processes display consumer
8
8
  class Consumers < Base
9
+ self.sortable_attributes = %w[
10
+ name
11
+ started_at
12
+ lag_stored
13
+ ].freeze
14
+
9
15
  # List page with consumers
10
16
  # @note For now we load all and paginate over the squashed data.
11
17
  def index
12
18
  @current_state = Models::ConsumersState.current!
13
19
  @counters = Models::Counters.new(@current_state)
14
20
  @processes, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
15
- Models::Processes.active(@current_state),
21
+ refine(Models::Processes.active(@current_state)),
16
22
  @params.current_page
17
23
  )
18
24
 
19
25
  paginate(@params.current_page, !last_page)
20
26
 
21
- respond
27
+ render
22
28
  end
23
29
  end
24
30
  end
@@ -20,10 +20,10 @@ module Karafka
20
20
 
21
21
  # Load only historicals for the selected range
22
22
  @aggregated_charts = Models::Metrics::Charts::Aggregated.new(
23
- @aggregated, :seconds
23
+ @aggregated, @params.current_range
24
24
  )
25
25
 
26
- respond
26
+ render
27
27
  end
28
28
  end
29
29
  end
@@ -20,7 +20,7 @@ module Karafka
20
20
  @error_messages.map(&:offset)
21
21
  )
22
22
 
23
- respond
23
+ render
24
24
  end
25
25
 
26
26
  # @param offset [Integer] given error message offset
@@ -31,7 +31,7 @@ module Karafka
31
31
  offset
32
32
  )
33
33
 
34
- respond
34
+ render
35
35
  end
36
36
 
37
37
  private
@@ -6,27 +6,77 @@ module Karafka
6
6
  module Controllers
7
7
  # Active jobs (work) reporting controller
8
8
  class Jobs < Base
9
- # Lists jobs
10
- def index
9
+ self.sortable_attributes = %w[
10
+ name
11
+ topic
12
+ consumer
13
+ type
14
+ updated_at
15
+ ].freeze
16
+
17
+ # Lists running jobs
18
+ def running
11
19
  current_state = Models::ConsumersState.current!
12
20
  processes = Models::Processes.active(current_state)
13
21
 
22
+ @jobs_counters = count_jobs_types(processes)
23
+
14
24
  # Aggregate jobs and inject the process info into them for better reporting
15
25
  jobs_total = processes.flat_map do |process|
16
- process.jobs.map do |job|
26
+ process.jobs.running.map do |job|
17
27
  job.to_h[:process] = process
18
28
  job
19
29
  end
20
30
  end
21
31
 
22
32
  @jobs, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
23
- jobs_total,
33
+ refine(jobs_total),
24
34
  @params.current_page
25
35
  )
26
36
 
27
37
  paginate(@params.current_page, !last_page)
28
38
 
29
- respond
39
+ render
40
+ end
41
+
42
+ # Lists pending jobs
43
+ def pending
44
+ current_state = Models::ConsumersState.current!
45
+ processes = Models::Processes.active(current_state)
46
+
47
+ @jobs_counters = count_jobs_types(processes)
48
+
49
+ # Aggregate jobs and inject the process info into them for better reporting
50
+ jobs_total = processes.flat_map do |process|
51
+ process.jobs.pending.map do |job|
52
+ job.to_h[:process] = process
53
+ job
54
+ end
55
+ end
56
+
57
+ @jobs, last_page = Ui::Lib::Paginations::Paginators::Arrays.call(
58
+ refine(jobs_total),
59
+ @params.current_page
60
+ )
61
+
62
+ paginate(@params.current_page, !last_page)
63
+
64
+ render
65
+ end
66
+
67
+ private
68
+
69
+ # @param processes [Array<Models::Process>]
70
+ # @return [Lib::HashProxy] particular type jobs count
71
+ def count_jobs_types(processes)
72
+ counts = { running: 0, pending: 0 }
73
+
74
+ processes.flat_map do |process|
75
+ counts[:running] += process.jobs.running.size
76
+ counts[:pending] += process.jobs.pending.size
77
+ end
78
+
79
+ Lib::HashProxy.new(counts)
30
80
  end
31
81
  end
32
82
  end
@@ -24,6 +24,11 @@ module Karafka
24
24
  @request_params = request_params
25
25
  end
26
26
 
27
+ # @return [String] sort query value
28
+ def sort
29
+ @sort ||= @request_params['sort'].to_s.downcase
30
+ end
31
+
27
32
  # @return [Integer] current page for paginated views
28
33
  # @note It does basic sanitization
29
34
  def current_page
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ module Responses
8
+ # Response that will make Roda render 403 deny
9
+ class Deny
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Controllers
7
+ module Responses
8
+ # Response that tells Roda to ship the content under a file name
9
+ class File
10
+ attr_reader :content, :file_name
11
+
12
+ # @param content [String] data we want to send
13
+ # @param file_name [String] name under which we want to send it
14
+ def initialize(content, file_name)
15
+ @content = content
16
+ @file_name = file_name
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -6,10 +6,10 @@ module Karafka
6
6
  module Controllers
7
7
  # Response related components
8
8
  module Responses
9
- # Response data object. It is used to transfer attributes assigned in controllers into
10
- # views
9
+ # Response render data object. It is used to transfer attributes assigned in controllers
10
+ # into views
11
11
  # It acts as a simplification / transport layer for assigned attributes
12
- class Data
12
+ class Render
13
13
  attr_reader :path, :attributes
14
14
 
15
15
  # @param path [String] render path
@@ -6,11 +6,20 @@ module Karafka
6
6
  module Controllers
7
7
  # Routing presentation controller
8
8
  class Routing < Base
9
+ self.sortable_attributes = %w[
10
+ name
11
+ active?
12
+ ].freeze
13
+
9
14
  # Routing list
10
15
  def index
11
16
  @routes = Karafka::App.routes
12
17
 
13
- respond
18
+ @routes.each do |consumer_group|
19
+ refine(consumer_group.topics)
20
+ end
21
+
22
+ render
14
23
  end
15
24
 
16
25
  # Given route details
@@ -21,7 +30,7 @@ module Karafka
21
30
 
22
31
  @topic || raise(::Karafka::Web::Errors::Ui::NotFoundError, topic_id)
23
32
 
24
- respond
33
+ render
25
34
  end
26
35
  end
27
36
  end
@@ -15,7 +15,7 @@ module Karafka
15
15
  @status = Models::Status.new
16
16
  @sampler = Tracking::Sampler.new
17
17
 
18
- respond
18
+ render
19
19
  end
20
20
  end
21
21
  end
@@ -126,6 +126,40 @@ module Karafka
126
126
  %(<span title="#{stamp}">#{time}</span>)
127
127
  end
128
128
 
129
+ # @param state [String] poll state
130
+ # @param state_ch [Integer] time until next change of the poll state
131
+ # (from paused to active)
132
+ # @return [String] span tag with label and title with change time if present
133
+ def poll_state_with_change_time_label(state, state_ch)
134
+ year_in_seconds = 131_556_926
135
+ state_ch_in_seconds = state_ch / 1_000.0
136
+
137
+ # If state is active, there is no date of change
138
+ if state == 'active'
139
+ %(
140
+ <span class="badge #{kafka_state_bg(state)} mt-1 mb-1">#{state}</span>
141
+ )
142
+ elsif state_ch_in_seconds > year_in_seconds
143
+ %(
144
+ <span
145
+ class="badge #{kafka_state_bg(state)} mt-1 mb-1"
146
+ title="until manual resume"
147
+ >
148
+ #{state}
149
+ </span>
150
+ )
151
+ else
152
+ %(
153
+ <span
154
+ class="badge #{kafka_state_bg(state)} time-title mt-1 mb-1"
155
+ title="#{Time.now + state_ch_in_seconds}"
156
+ >
157
+ #{state}
158
+ </span>
159
+ )
160
+ end
161
+ end
162
+
129
163
  # @param lag [Integer] lag
130
164
  # @return [String] lag if correct or `N/A` with labeled explanation
131
165
  # @see #offset_with_label
@@ -212,6 +246,42 @@ module Karafka
212
246
 
213
247
  result
214
248
  end
249
+
250
+ # @param name [String] link value
251
+ # @param attribute [Symbol, nil] sorting attribute or nil if we provide only symbol name
252
+ # @param rev [Boolean] when set to true, arrows will be in the reverse position. This is
253
+ # used when the description in the link is reverse to data we sort. For example we have
254
+ # order on when processes were started and we display "x hours" ago but we sort on
255
+ # their age, meaning that it looks like it is the other way around. This flag allows
256
+ # us to reverse just he arrow making it look consistent with the presented data order
257
+ # @return [String] html link for sorting with arrow when attribute sort enabled
258
+ def sort_link(name, attribute = nil, rev: false)
259
+ unless attribute
260
+ attribute = name
261
+ name = attribute.to_s.tr('_', ' ').capitalize
262
+ end
263
+
264
+ arrow_both = '&#x21D5;'
265
+ arrow_down = '&#9662;'
266
+ arrow_up = '&#9652;'
267
+
268
+ desc = "#{attribute} desc"
269
+ asc = "#{attribute} asc"
270
+ path = current_path(sort: desc)
271
+ full_name = "#{name}&nbsp;#{arrow_both}"
272
+
273
+ if params.sort == desc
274
+ path = current_path(sort: asc)
275
+ full_name = "#{name}&nbsp;#{rev ? arrow_up : arrow_down}"
276
+ end
277
+
278
+ if params.sort == asc
279
+ path = current_path(sort: desc)
280
+ full_name = "#{name}&nbsp;#{rev ? arrow_down : arrow_up}"
281
+ end
282
+
283
+ "<a class=\"sort\" href=\"#{path}\">#{full_name}</a>"
284
+ end
215
285
  end
216
286
  end
217
287
  end
@@ -17,12 +17,17 @@ module Karafka
17
17
  class HashProxy
18
18
  extend Forwardable
19
19
 
20
- def_delegators :@hash, :[], :[]=, :key?, :each, :find
20
+ def_delegators :@hash, :[], :[]=, :key?, :each, :find, :values, :keys, :select
21
21
 
22
22
  # @param hash [Hash] hash we want to convert to a proxy
23
23
  def initialize(hash)
24
24
  @hash = hash
25
- @visited = []
25
+ # Nodes we already visited in the context of a given attribute lookup
26
+ # We cache them not to look for them over and over again if they are used more than
27
+ # once
28
+ @visited = Hash.new { |h, k| h[k] = {} }
29
+ # Methods invocations cache
30
+ @results = {}
26
31
  end
27
32
 
28
33
  # @return [Original hash]
@@ -34,22 +39,32 @@ module Karafka
34
39
  # @param args [Object] all the args of the method
35
40
  # @param block [Proc] block for the method
36
41
  def method_missing(method_name, *args, &block)
37
- return super unless args.empty? && block.nil?
42
+ method_name = method_name.to_sym
38
43
 
39
- @visited.clear
44
+ return super unless args.empty? && block.nil?
45
+ return @results[method_name] if @results.key?(method_name)
40
46
 
41
- result = deep_find(@hash, method_name.to_sym)
47
+ result = deep_find(@hash, method_name)
42
48
 
43
- @visited.clear
49
+ return super if result.nil?
44
50
 
45
- result.nil? ? super : result
51
+ @results[method_name] = result
46
52
  end
47
53
 
48
54
  # @param method_name [String] method name
49
55
  # @param include_private [Boolean]
50
56
  def respond_to_missing?(method_name, include_private = false)
51
- result = deep_find(@hash, method_name.to_sym)
52
- result.nil? ? super : true
57
+ method_name = method_name.to_sym
58
+
59
+ return true if @results.key?(method_name)
60
+
61
+ result = deep_find(@hash, method_name)
62
+
63
+ return super if result.nil?
64
+
65
+ @results[method_name] = result
66
+
67
+ true
53
68
  end
54
69
 
55
70
  private
@@ -59,16 +74,16 @@ module Karafka
59
74
  def deep_find(obj, key)
60
75
  # Prevent circular dependency lookups by making sure we do not check the same object
61
76
  # multiple times
62
- return nil if @visited.include?(obj)
77
+ return nil if @visited[key].key?(obj)
63
78
 
64
- @visited << obj
79
+ @visited[key][obj] = nil
65
80
 
66
81
  if obj.respond_to?(:key?) && obj.key?(key)
67
82
  obj[key]
68
83
  elsif obj.respond_to?(:each)
69
- r = nil
70
- obj.find { |*a| r = deep_find(a.last, key) }
71
- r
84
+ result = nil
85
+ obj.find { |*a| result = deep_find(a.last, key) }
86
+ result
72
87
  end
73
88
  end
74
89
  end