karafka-web 0.10.0 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +17 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +12 -4
  7. data/certs/cert.pem +26 -0
  8. data/karafka-web.gemspec +2 -2
  9. data/lib/karafka/web/pro/ui/app.rb +75 -0
  10. data/lib/karafka/web/pro/ui/controllers/commanding_controller.rb +4 -4
  11. data/lib/karafka/web/pro/ui/controllers/recurring_tasks_controller.rb +131 -0
  12. data/lib/karafka/web/pro/ui/controllers/scheduled_messages/base_controller.rb +29 -0
  13. data/lib/karafka/web/pro/ui/controllers/scheduled_messages/explorer_controller.rb +65 -0
  14. data/lib/karafka/web/pro/ui/controllers/scheduled_messages/schedules_controller.rb +84 -0
  15. data/lib/karafka/web/pro/ui/views/consumers/_consumer_controls.erb +13 -12
  16. data/lib/karafka/web/pro/ui/views/explorer/_partition_option.erb +10 -2
  17. data/lib/karafka/web/pro/ui/views/recurring_tasks/_actions.erb +58 -0
  18. data/lib/karafka/web/pro/ui/views/recurring_tasks/_batch_actions.erb +45 -0
  19. data/lib/karafka/web/pro/ui/views/recurring_tasks/_breadcrumbs.erb +22 -0
  20. data/lib/karafka/web/pro/ui/views/recurring_tasks/_log.erb +26 -0
  21. data/lib/karafka/web/pro/ui/views/recurring_tasks/_not_active.erb +12 -0
  22. data/lib/karafka/web/pro/ui/views/recurring_tasks/_tabs.erb +17 -0
  23. data/lib/karafka/web/pro/ui/views/recurring_tasks/_task.erb +46 -0
  24. data/lib/karafka/web/pro/ui/views/recurring_tasks/logs.erb +34 -0
  25. data/lib/karafka/web/pro/ui/views/recurring_tasks/schedule.erb +43 -0
  26. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/_breadcrumbs.erb +30 -0
  27. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/_message.erb +84 -0
  28. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/_messages.erb +28 -0
  29. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/partition.erb +72 -0
  30. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/topic.erb +33 -0
  31. data/lib/karafka/web/pro/ui/views/scheduled_messages/schedules/_breadcrumbs.erb +21 -0
  32. data/lib/karafka/web/pro/ui/views/scheduled_messages/schedules/_no_groups.erb +11 -0
  33. data/lib/karafka/web/pro/ui/views/scheduled_messages/schedules/index.erb +38 -0
  34. data/lib/karafka/web/pro/ui/views/scheduled_messages/schedules/show.erb +48 -0
  35. data/lib/karafka/web/pro/ui/views/search/_breadcrumbs.erb +1 -0
  36. data/lib/karafka/web/pro/ui/views/shared/_navigation.erb +20 -0
  37. data/lib/karafka/web/pro/ui/views/topics/replication.erb +1 -1
  38. data/lib/karafka/web/tracking/consumers/listeners/errors.rb +3 -1
  39. data/lib/karafka/web/ui/base.rb +1 -1
  40. data/lib/karafka/web/ui/controllers/base_controller.rb +18 -3
  41. data/lib/karafka/web/ui/helpers/application_helper.rb +29 -8
  42. data/lib/karafka/web/ui/helpers/paths_helper.rb +18 -0
  43. data/lib/karafka/web/ui/models/recurring_tasks/log.rb +15 -0
  44. data/lib/karafka/web/ui/models/recurring_tasks/schedule.rb +75 -0
  45. data/lib/karafka/web/ui/models/recurring_tasks/task.rb +19 -0
  46. data/lib/karafka/web/ui/public/javascripts/application.js +1 -1
  47. data/lib/karafka/web/ui/public/javascripts/application.min.js +7 -7
  48. data/lib/karafka/web/ui/public/javascripts/application.min.js.br +0 -0
  49. data/lib/karafka/web/ui/public/javascripts/application.min.js.gz +0 -0
  50. data/lib/karafka/web/ui/public/javascripts/components/live_poll.js +51 -0
  51. data/lib/karafka/web/ui/public/stylesheets/application.css +5 -1
  52. data/lib/karafka/web/ui/public/stylesheets/application.min.css +2 -2
  53. data/lib/karafka/web/ui/public/stylesheets/application.min.css.br +0 -0
  54. data/lib/karafka/web/ui/public/stylesheets/application.min.css.gz +0 -0
  55. data/lib/karafka/web/ui/public/stylesheets/libs/tailwind.css +18 -6
  56. data/lib/karafka/web/ui/views/dashboard/_ranges_selector.erb +1 -1
  57. data/lib/karafka/web/ui/views/shared/_breadcrumbs.erb +1 -1
  58. data/lib/karafka/web/ui/views/shared/_navigation.erb +21 -0
  59. data/lib/karafka/web/ui/views/shared/icons/_calendar.erb +3 -0
  60. data/lib/karafka/web/ui/views/shared/icons/_check.erb +3 -0
  61. data/lib/karafka/web/ui/views/shared/icons/_play.erb +3 -0
  62. data/lib/karafka/web/ui/views/status/info/_components.erb +48 -11
  63. data/lib/karafka/web/ui/views/ux/_data_table.erb +34 -4
  64. data/lib/karafka/web/ui/views/ux/_status_rows.erb +12 -0
  65. data/lib/karafka/web/version.rb +1 -1
  66. data.tar.gz.sig +0 -0
  67. metadata +56 -27
  68. metadata.gz.sig +0 -0
  69. data/certs/cert_chain.pem +0 -26
@@ -0,0 +1,48 @@
1
+ <% view_title @schedule_name %>
2
+
3
+ <% @states.each do |partition, details| %>
4
+ <h3 class="h3">
5
+ Partition <%= partition %>
6
+ </h3>
7
+
8
+ <% if details %>
9
+ <div class="data-table-wrapper">
10
+ <table class="data-table">
11
+ <thead>
12
+ <tr>
13
+ <th>Date</th>
14
+ <th>For Dispatch</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <% details[:daily].each do |date, for_dispatch| %>
19
+ <tr>
20
+ <td><%= date %></td>
21
+ <td><%= for_dispatch %></td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
26
+
27
+ <p class="table_metadata">
28
+ Metadata:
29
+ <span class="badge">
30
+ Schema version: <%= details[:schema_version] %>
31
+ </span>
32
+
33
+ <span class="badge">
34
+ Reported at:&nbsp;
35
+ <%== relative_time(details[:dispatched_at]) %>
36
+ </span>
37
+
38
+ <span class="badge badge-<%= details[:state] == 'loaded' ? 'success' : 'warning' %>">
39
+ State: <%= details[:state] %>
40
+ </span>
41
+ </p>
42
+ </div>
43
+ <% else %>
44
+ <div class="mb-4">
45
+ <%== alert_warning 'No state information for this partition is available.' %>
46
+ </div>
47
+ <% end %>
48
+ <% end %>
@@ -0,0 +1 @@
1
+ <%== partial 'explorer/breadcrumbs' %>
@@ -62,6 +62,26 @@
62
62
  </a>
63
63
  </li>
64
64
 
65
+ <li>
66
+ <a
67
+ class="sidebar-nav-item <%= nav_class(start_with: '/recurring_tasks') %>"
68
+ href="<%= root_path('recurring_tasks') %>"
69
+ >
70
+ <%== icon(:arrow_path_rounded) %>
71
+ Cron
72
+ </a>
73
+ </li>
74
+
75
+ <li>
76
+ <a
77
+ class="sidebar-nav-item <%= nav_class(start_with: '/scheduled_messages') %>"
78
+ href="<%= root_path('scheduled_messages') %>"
79
+ >
80
+ <%== icon(:calendar) %>
81
+ Schedules
82
+ </a>
83
+ </li>
84
+
65
85
  <li>
66
86
  <a
67
87
  class="sidebar-nav-item <%= nav_class(start_with: '/errors') %>"
@@ -9,7 +9,7 @@
9
9
  <th><%== sort_link(:partition_id) %></th>
10
10
  <th><%== sort_link(:leader) %></th>
11
11
  <th><%== sort_link(:replica_count) %></th>
12
- <th><%== sort_link('In sync brokers', :in_sync_replica_brokers) %></th>
12
+ <th><%== sort_link('In Sync Brokers', :in_sync_replica_brokers) %></th>
13
13
  </tr>
14
14
  </thead>
15
15
  <tbody>
@@ -73,7 +73,9 @@ module Karafka
73
73
  partition: consumer.partition,
74
74
  first_offset: consumer.messages.metadata.first_offset,
75
75
  last_offset: consumer.messages.metadata.last_offset,
76
- committed_offset: consumer.coordinator.seek_offset - 1,
76
+ # We set it to -1000 if non-existent because after subtracting one, we will end up
77
+ # with -1001, which is "N/A" offset position for all the offsets here
78
+ committed_offset: (consumer.coordinator.seek_offset || -1_000) - 1,
77
79
  consumer: consumer.class.to_s,
78
80
  tags: consumer.tags
79
81
  }
@@ -133,7 +133,7 @@ module Karafka
133
133
  # @param query_data [Hash] query params we want to add to the current path
134
134
  path :current do |query_data = {}|
135
135
  # Merge existing request parameters with new query data
136
- merged_params = request.params.deep_merge(query_data)
136
+ merged_params = deep_merge(request.params, query_data)
137
137
 
138
138
  # Flatten the merged parameters
139
139
  flattened_params = flatten_params('', merged_params)
@@ -28,13 +28,28 @@ module Karafka
28
28
 
29
29
  # Builds the render data object with assigned attributes based on instance variables.
30
30
  #
31
+ # @param attributes [Hash] attributes coming from the outside (in case of rebind)
31
32
  # @return [Responses::Render] data that should be used to render appropriate view
32
- def render
33
- attributes = {}
33
+ def render(attributes: {})
34
+ attributes = attributes.dup
35
+
36
+ full_parts = self.class.to_s.split('::')
37
+ separator = full_parts.index('Controllers')
38
+ base = full_parts[(separator + 1)..]
39
+
40
+ base.map!.with_index do |path_part, index|
41
+ if index == (base.size - 1)
42
+ path_part.gsub(/(.)([A-Z])/, '\1_\2').downcase.gsub('_controller', '')
43
+ else
44
+ path_part.gsub(/(.)([A-Z])/, '\1_\2').downcase
45
+ end
46
+ end
34
47
 
35
- scope = self.class.to_s.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase.gsub('_controller', '')
48
+ scope = base.join('/')
36
49
  action = caller_locations(1, 1)[0].label.split('#').last
37
50
 
51
+ attributes[:breadcrums_scope] = scope
52
+
38
53
  instance_variables.each do |iv|
39
54
  next if iv.to_s.start_with?('@_')
40
55
  next if iv.to_s.start_with?('@params')
@@ -50,13 +50,6 @@ module Karafka
50
50
  object.to_s.include?('#<') ? object.class.to_s : object.to_s
51
51
  end
52
52
 
53
- # Renders per scope breadcrumbs
54
- def render_breadcrumbs
55
- scope = request.path.delete_prefix(root_path).split('/')[0]
56
-
57
- render "#{scope}/_breadcrumbs"
58
- end
59
-
60
53
  # Takes a status and recommends background style color
61
54
  #
62
55
  # @param status [String] status
@@ -309,7 +302,14 @@ module Karafka
309
302
  def sort_link(name, attribute = nil, rev: false)
310
303
  unless attribute
311
304
  attribute = name
312
- name = SORT_NAMES[attribute] || attribute.to_s.tr('_', ' ').tr('?', '').capitalize
305
+
306
+ if SORT_NAMES[attribute]
307
+ name = SORT_NAMES[attribute]
308
+ else
309
+ name = attribute.to_s.tr('_', ' ').tr('?', '')
310
+ # Always capitalize the name
311
+ name = name.split(' ').map(&:capitalize).join(' ')
312
+ end
313
313
  end
314
314
 
315
315
  arrow_both = '&#x21D5;'
@@ -369,6 +369,27 @@ module Karafka
369
369
  def icon(name)
370
370
  render "shared/icons/_#{name}"
371
371
  end
372
+
373
+ # Merges two hashes deeply, combining nested hashes recursively.
374
+ #
375
+ # @param hash1 [Hash] The first hash to merge.
376
+ # @param hash2 [Hash] The second hash to merge.
377
+ # @return [Hash] A new hash that is the result of a deep merge of the two provided hashes.
378
+ def deep_merge(hash1, hash2)
379
+ merged_hash = hash1.dup
380
+
381
+ hash2.each_pair do |k, v|
382
+ tv = merged_hash[k]
383
+
384
+ merged_hash[k] = if tv.is_a?(Hash) && v.is_a?(Hash)
385
+ deep_merge(tv, v)
386
+ else
387
+ v
388
+ end
389
+ end
390
+
391
+ merged_hash
392
+ end
372
393
  end
373
394
  end
374
395
  end
@@ -65,6 +65,24 @@ module Karafka
65
65
  def explorer_path(topic_name = nil, partition_id = nil, offset = nil, action = nil)
66
66
  root_path(*['explorer', topic_name, partition_id, offset, action].compact)
67
67
  end
68
+
69
+ # Helps build scheduled messages paths.
70
+ # Similar to the explorer helper one
71
+ # @param topic_name [String]
72
+ # @param partition_id [Integer, nil]
73
+ # @param offset [Integer, nil]
74
+ # @param action [String, nil]
75
+ # @return [String] path to the expected location
76
+ def scheduled_messages_explorer_path(
77
+ topic_name = nil,
78
+ partition_id = nil,
79
+ offset = nil,
80
+ action = nil
81
+ )
82
+ root_path(
83
+ *['scheduled_messages', 'explorer', topic_name, partition_id, offset, action].compact
84
+ )
85
+ end
68
86
  end
69
87
  end
70
88
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ module RecurringTasks
8
+ # Single log entry for recurring tasks execution
9
+ class Log < Lib::HashProxy
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ # Namespace for models representing recurring tasks related components taken from Kafka
8
+ module RecurringTasks
9
+ # Karafka schedule representation
10
+ class Schedule < Web::Ui::Lib::HashProxy
11
+ # Rdkafka errors we expect and handle gracefully
12
+ EXPECTED_RDKAFKA_ERRORS = %i[
13
+ unknown_topic
14
+ unknown_partition
15
+ unknown_topic_or_part
16
+ ].freeze
17
+
18
+ private_constant :EXPECTED_RDKAFKA_ERRORS
19
+
20
+ class << self
21
+ # @return [Schedule, false] current schedule or false if it was not possible to
22
+ # get it because requested topic/partition does not exist or nothing was present
23
+ def current
24
+ messages = Karafka::Admin.read_topic(
25
+ config.topics.schedules,
26
+ 0,
27
+ # We work here with the assumption that users won't click so fast to load
28
+ # more than 20 commands prior to a state flush. If that happens, this will
29
+ # return false. This is a known and expected limitation.
30
+ 20
31
+ )
32
+
33
+ # Out of those messages we pick the most recent persisted schedule
34
+ candidate = messages
35
+ .reverse
36
+ .find { |message| message.key == 'state:schedule' }
37
+
38
+ # If there is a schedule message we use its data to build schedule, if not false
39
+ return false unless candidate
40
+
41
+ # If the deserializer is not our dedicated recurring tasks deserializer, it means
42
+ # that routing for recurring tasks was not loaded, so recurring tasks are not
43
+ # active
44
+ #
45
+ # User might have used recurring tasks previously and disabled them, but still may
46
+ # navigate to them and then we should not show anything because without the
47
+ # correct deserializer it will crash anyhow
48
+ return false unless candidate.metadata.deserializers.payload == config.deserializer
49
+
50
+ new(candidate.payload)
51
+ rescue Rdkafka::RdkafkaError => e
52
+ # If any of "topic missing" is raised, we return false but other errors we re-raise
53
+ raise(e) unless EXPECTED_RDKAFKA_ERRORS.any? { |code| e.code == code }
54
+
55
+ false
56
+ end
57
+
58
+ private
59
+
60
+ # @return [Karafka::Core::Configurable::Node]
61
+ def config
62
+ Karafka::App.config.recurring_tasks
63
+ end
64
+ end
65
+
66
+ # @return [Array<Task>] tasks of the current schedule
67
+ def tasks
68
+ @tasks ||= super.values.map { |task_hash| Task.new(task_hash) }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Web
5
+ module Ui
6
+ module Models
7
+ module RecurringTasks
8
+ # Represents a single recurring task
9
+ class Task < Lib::HashProxy
10
+ # @return [Boolean] true if this task is enabled, otherwise false
11
+ def enabled?
12
+ enabled
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -119,4 +119,4 @@ function addListeners() {
119
119
 
120
120
  document.addEventListener('turbo:load', addListeners);
121
121
 
122
- Turbo.setProgressBarDelay(250)
122
+ Turbo.setProgressBarDelay(100)