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
@@ -38,15 +38,23 @@ module Karafka
38
38
  self.sortable_attributes = %w[
39
39
  name
40
40
  active?
41
+ value
41
42
  ].freeze
42
43
 
44
+ # The list is filtered by topic/consumer group (at the view level), while the per-topic
45
+ # details page filters its flat attribute/value rows
46
+ self.filterable_attributes = {
47
+ index: %i[topic consumer_group],
48
+ show: %i[name value]
49
+ }.freeze
50
+
43
51
  # Routing list
44
52
  def index
45
53
  detect_patterns_routes
46
54
 
47
55
  @routes = Karafka::App.routes
48
56
  @routes.each do |consumer_group|
49
- refine(consumer_group.topics)
57
+ sort(consumer_group.topics)
50
58
  end
51
59
 
52
60
  current_state = Models::ConsumersState.current
@@ -80,11 +88,25 @@ module Karafka
80
88
 
81
89
  @topic || not_found!(topic_id)
82
90
 
91
+ # Present the routing settings as a flat, sortable + filterable list of name/value rows
92
+ @details = sort(filter(topic_detail_rows(@topic)))
93
+
83
94
  render
84
95
  end
85
96
 
86
97
  private
87
98
 
99
+ # @param topic [Karafka::Routing::Topic] topic we present the routing details for
100
+ # @return [Array<Hash>] `{ name:, value: }` rows for every routing setting (including
101
+ # the Pro multiplexing settings)
102
+ def topic_detail_rows(topic)
103
+ flattener = Web::Ui::Lib::HashFlattener
104
+ rows = flattener.call(topic.subscription_group.kafka, "kafka")
105
+ rows.merge!(flattener.call(topic.to_h.except(:kafka)))
106
+ rows.merge!(flattener.call(topic.subscription_group.multiplexing.to_h, "multiplexing"))
107
+ rows.map { |name, value| { name: name.to_s, value: value } }
108
+ end
109
+
88
110
  # Detect routes defined as patterns
89
111
  def detect_patterns_routes
90
112
  Lib::PatternsDetector.new.call
@@ -36,6 +36,10 @@ module Karafka
36
36
  module ScheduledMessages
37
37
  # Controller to display list of schedules (groups) and details about each
38
38
  class SchedulesController < BaseController
39
+ self.filterable_attributes = %w[
40
+ topic_name
41
+ ].freeze
42
+
39
43
  # Displays list of groups
40
44
  def index
41
45
  topics = Models::Topic.all
@@ -54,6 +58,8 @@ module Karafka
54
58
 
55
59
  @topics = topics.select { |topic| candidates.include?(topic.topic_name) }
56
60
 
61
+ @topics = filter(@topics)
62
+
57
63
  render
58
64
  end
59
65
 
@@ -44,13 +44,18 @@ module Karafka
44
44
  read_only?
45
45
  ].freeze
46
46
 
47
+ self.filterable_attributes = %i[
48
+ name
49
+ value
50
+ ].freeze
51
+
47
52
  # Displays requested topic config details
48
53
  #
49
54
  # @param topic_name [String] topic we're interested in
50
55
  def index(topic_name)
51
56
  @topic = Models::Topic.find(topic_name)
52
57
 
53
- @configs = refine(@topic.configs)
58
+ @configs = filter(sort(@topic.configs))
54
59
 
55
60
  render
56
61
  end
@@ -62,7 +62,7 @@ module Karafka
62
62
 
63
63
  @aggregated, distribution = @topic.distribution(@active_partitions)
64
64
 
65
- @distribution = refine(distribution)
65
+ @distribution = sort(distribution)
66
66
 
67
67
  next_page = @active_partitions.last < @topic.partition_count - 1
68
68
  paginate(@params.current_page, next_page)
@@ -66,7 +66,7 @@ module Karafka
66
66
  }
67
67
  end
68
68
 
69
- @offsets = refine(offsets)
69
+ @offsets = sort(offsets)
70
70
 
71
71
  next_page = @active_partitions.last < @topic.partition_count - 1
72
72
  paginate(@params.current_page, next_page)
@@ -49,7 +49,7 @@ module Karafka
49
49
  def show(topic_name)
50
50
  @topic = Models::Topic.find(topic_name)
51
51
 
52
- @partitions = refine(@topic[:partitions])
52
+ @partitions = sort(@topic[:partitions])
53
53
 
54
54
  # Extract replication factor from the first partition (same for all partitions)
55
55
  # The partitions data is an array of hashes with :replica_count key
@@ -38,6 +38,10 @@ module Karafka
38
38
  class TopicsController < BaseController
39
39
  self.sortable_attributes = [].freeze
40
40
 
41
+ self.filterable_attributes = %w[
42
+ topic_name
43
+ ].freeze
44
+
41
45
  after(:create, :delete) { cache.clear }
42
46
 
43
47
  # Displays list of topics we can work with
@@ -48,6 +52,8 @@ module Karafka
48
52
  @topics.delete_if { |topic| topic[:topic_name].start_with?("__") }
49
53
  end
50
54
 
55
+ @topics = filter(@topics)
56
+
51
57
  render
52
58
  end
53
59
 
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Karafka Pro - Source Available Commercial Software
4
+ # Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
5
+ #
6
+ # This software is NOT open source. It is source-available commercial software
7
+ # requiring a paid license for use. It is NOT covered by LGPL.
8
+ #
9
+ # The author retains all right, title, and interest in this software,
10
+ # including all copyrights, patents, and other intellectual property rights.
11
+ # No patent rights are granted under this license.
12
+ #
13
+ # PROHIBITED:
14
+ # - Use without a valid commercial license
15
+ # - Redistribution, modification, or derivative works without authorization
16
+ # - Reverse engineering, decompilation, or disassembly of this software
17
+ # - Use as training data for AI/ML models or inclusion in datasets
18
+ # - Scraping, crawling, or automated collection for any purpose
19
+ #
20
+ # PERMITTED:
21
+ # - Reading, referencing, and linking for personal or commercial use
22
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
23
+ # for the purpose of providing contextual help to Karafka users
24
+ #
25
+ # Receipt, viewing, or possession of this software does not convey or
26
+ # imply any license or right beyond those expressly stated above.
27
+ #
28
+ # License: https://karafka.io/docs/Pro-License-Comm/
29
+ # Contact: contact@karafka.io
30
+
31
+ module Karafka
32
+ module Web
33
+ module Pro
34
+ module Ui
35
+ # Namespace for the Pro-only Web UI view helpers.
36
+ module Helpers
37
+ # Helpers for rendering the keyword filtering (search) box on data tables. Companion
38
+ # to the [[SortingHelper]]: sorting reorders a listing, filtering narrows it down.
39
+ module FilteringHelper
40
+ # @return [Boolean] true if a filtering keyword is currently active. Filtering
41
+ # (search) is a Pro-only feature, so this is always false in OSS.
42
+ def filtering?
43
+ return false unless ::Karafka.pro?
44
+
45
+ !params.current_filter.empty?
46
+ end
47
+
48
+ # Renders a filtering form for the current listing.
49
+ #
50
+ # The form submits via GET to the current path, preserving every other query parameter
51
+ # (most importantly the current sort) as hidden fields while resetting pagination, so
52
+ # filtering always starts from the first page.
53
+ #
54
+ # It renders as a full-width search field with a "Search" submit button and a "Reset"
55
+ # button (always present, but disabled when there is nothing to reset, so the layout
56
+ # does not shift). When the controller exposes a `{ field => label }` map of filterable
57
+ # fields (via `filter`), a field selector is fused into the left of the field so the
58
+ # user can pick which attribute to filter on.
59
+ #
60
+ # The placeholder is intentionally a single generic default across every listing: a
61
+ # field selector always accompanies the input and already tells the user which
62
+ # attribute is being filtered, so a per-view placeholder would be redundant (and would
63
+ # look stale until the form is submitted, since it is server-rendered and does not
64
+ # follow the select live).
65
+ #
66
+ # @param labels [Hash] per-view overrides for the field selector labels, e.g.
67
+ # `labels: { id: "Task ID" }`. Anything not overridden falls back to the shared
68
+ # {FILTER_NAMES} map (and then to a humanized attribute name).
69
+ # @return [String] html of the filtering form
70
+ def filter_box(labels: {})
71
+ # Filtering (search) is a Pro-only feature, so we render nothing in OSS.
72
+ return "" unless ::Karafka.pro?
73
+
74
+ # A field is either a plain attribute name or a key-alias descriptor; the selector only
75
+ # cares about its name. We render the field selector whenever the controller exposes
76
+ # any filterable field, so every search looks consistent (even single-field listings
77
+ # show a one-option select).
78
+ fields = Array(@filterable_fields).map { |field| filter_field_name(field) }
79
+ fielded = fields.any?
80
+
81
+ partial(
82
+ "shared/filter_box",
83
+ locals: {
84
+ fields: fields,
85
+ fielded: fielded,
86
+ selected: fielded ? selected_filter_field(fields) : nil,
87
+ labels: labels,
88
+ value_name: fielded ? "filter[value]" : "filter",
89
+ keyword: params.current_filter,
90
+ hidden_params: preserved_filter_params,
91
+ active: filtering?,
92
+ clear_path: filter_clear_path
93
+ }
94
+ )
95
+ end
96
+
97
+ # Human friendly labels for the attributes we allow filtering on. Attributes not listed
98
+ # here fall back to a humanized version of their name (see {#filter_field_label}), so
99
+ # controllers only need to declare bare attribute names in `filterable_attributes`.
100
+ FILTER_NAMES = {
101
+ id: "Process ID",
102
+ subscribed_topics: "Subscriptions",
103
+ tags: "Tags",
104
+ topic: "Topic",
105
+ topic_name: "Topic",
106
+ consumer_group: "Consumer group",
107
+ subscription_group: "Subscription group",
108
+ consumer: "Consumer",
109
+ type: "Type",
110
+ name: "Name",
111
+ value: "Value",
112
+ cron: "Cron",
113
+ broker_name: "Broker"
114
+ }.freeze
115
+
116
+ private_constant :FILTER_NAMES
117
+
118
+ private
119
+
120
+ # @param field [String, Symbol, #name] a filterable field: a plain attribute name or a
121
+ # key-alias descriptor
122
+ # @return [String] the field's name
123
+ def filter_field_name(field)
124
+ field.respond_to?(:path) ? field.name.to_s : field.to_s
125
+ end
126
+
127
+ # @param attribute [String, Symbol] filterable attribute name
128
+ # @param overrides [Hash] per-view label overrides (keyed by attribute)
129
+ # @return [String] human friendly label for the attribute: a per-view override if given,
130
+ # otherwise the shared {FILTER_NAMES} entry, otherwise a humanized attribute name
131
+ def filter_field_label(attribute, overrides = {})
132
+ key = attribute.to_sym
133
+
134
+ overrides.transform_keys(&:to_sym).fetch(key) do
135
+ FILTER_NAMES.fetch(key) do
136
+ attribute.to_s.tr("_", " ").split.map(&:capitalize).join(" ")
137
+ end
138
+ end
139
+ end
140
+
141
+ # @param fields [Array<String, Symbol>] filterable attributes
142
+ # @return [String] the currently selected filter field, defaulting to the first one when
143
+ # nothing (valid) is selected
144
+ def selected_filter_field(fields)
145
+ selected = params.current_filter_field
146
+
147
+ fields.map(&:to_s).include?(selected) ? selected : fields.first.to_s
148
+ end
149
+
150
+ # Builds the path used by the "clear" control: the current path with all filtering and
151
+ # pagination parameters removed.
152
+ #
153
+ # @return [String] path without any filter/page query parameters
154
+ def filter_clear_path
155
+ query = URI.encode_www_form(preserved_filter_params)
156
+
157
+ query.empty? ? request.path : "#{request.path}?#{query}"
158
+ end
159
+
160
+ # @return [Hash] current request query parameters that we want to carry over when
161
+ # submitting the filtering form (everything except the filtering parameters themselves
162
+ # and the pagination, which we intentionally reset)
163
+ def preserved_filter_params
164
+ flatten_params("", request.params).reject do |key, _value|
165
+ key == "filter" || key.start_with?("filter[") || key == "page"
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Karafka Pro - Source Available Commercial Software
4
+ # Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
5
+ #
6
+ # This software is NOT open source. It is source-available commercial software
7
+ # requiring a paid license for use. It is NOT covered by LGPL.
8
+ #
9
+ # The author retains all right, title, and interest in this software,
10
+ # including all copyrights, patents, and other intellectual property rights.
11
+ # No patent rights are granted under this license.
12
+ #
13
+ # PROHIBITED:
14
+ # - Use without a valid commercial license
15
+ # - Redistribution, modification, or derivative works without authorization
16
+ # - Reverse engineering, decompilation, or disassembly of this software
17
+ # - Use as training data for AI/ML models or inclusion in datasets
18
+ # - Scraping, crawling, or automated collection for any purpose
19
+ #
20
+ # PERMITTED:
21
+ # - Reading, referencing, and linking for personal or commercial use
22
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
23
+ # for the purpose of providing contextual help to Karafka users
24
+ #
25
+ # Receipt, viewing, or possession of this software does not convey or
26
+ # imply any license or right beyond those expressly stated above.
27
+ #
28
+ # License: https://karafka.io/docs/Pro-License-Comm/
29
+ # Contact: contact@karafka.io
30
+
31
+ module Karafka
32
+ module Web
33
+ module Pro
34
+ module Ui
35
+ module Helpers
36
+ # View-level filtering of topic collections for the routing and per-process subscriptions
37
+ # views. Unlike the generic {FilteringHelper} (which renders the search box) and the
38
+ # [[Filter]] engine (which prunes per-request structures in place), this narrows the live
39
+ # `Karafka::App.routes` / process subscriptions non-destructively, since those must never
40
+ # be mutated.
41
+ module TopicsFilteringHelper
42
+ # Non-destructively narrows a topics collection (routing or a consumer subscription)
43
+ # down to the ones matching the current filter.
44
+ #
45
+ # It is field aware: when the `topic` field is selected only topic names are matched;
46
+ # when `consumer_group`/`subscription_group` is selected the whole collection is kept or
47
+ # dropped based on that group's name. With no explicit field (plain keyword) a topic is
48
+ # kept when its own name matches or when any of the provided group labels match.
49
+ #
50
+ # It always returns a new array and leaves the source untouched.
51
+ #
52
+ # @param topics [Enumerable] topics of a subscription/consumer group
53
+ # @param consumer_group [String, nil] the consumer group name shown as the section title
54
+ # @param subscription_group [String, nil] the subscription group name (subscriptions)
55
+ # @return [Array] all topics when no filtering is active, otherwise only matching ones
56
+ def visible_topics(topics, consumer_group: nil, subscription_group: nil)
57
+ return topics.to_a unless filtering?
58
+
59
+ keyword = params.current_filter.downcase
60
+ by_name = -> { topics.select { |topic| topic.name.to_s.downcase.include?(keyword) } }
61
+ label_match = ->(label) { label.to_s.downcase.include?(keyword) }
62
+
63
+ case params.current_filter_field
64
+ when "consumer_group"
65
+ label_match.call(consumer_group) ? topics.to_a : []
66
+ when "subscription_group"
67
+ label_match.call(subscription_group) ? topics.to_a : []
68
+ when "topic"
69
+ by_name.call
70
+ else
71
+ # Plain keyword: match the topic name or any of the group labels
72
+ [consumer_group, subscription_group].any?(&label_match) ? topics.to_a : by_name.call
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Karafka Pro - Source Available Commercial Software
4
+ # Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
5
+ #
6
+ # This software is NOT open source. It is source-available commercial software
7
+ # requiring a paid license for use. It is NOT covered by LGPL.
8
+ #
9
+ # The author retains all right, title, and interest in this software,
10
+ # including all copyrights, patents, and other intellectual property rights.
11
+ # No patent rights are granted under this license.
12
+ #
13
+ # PROHIBITED:
14
+ # - Use without a valid commercial license
15
+ # - Redistribution, modification, or derivative works without authorization
16
+ # - Reverse engineering, decompilation, or disassembly of this software
17
+ # - Use as training data for AI/ML models or inclusion in datasets
18
+ # - Scraping, crawling, or automated collection for any purpose
19
+ #
20
+ # PERMITTED:
21
+ # - Reading, referencing, and linking for personal or commercial use
22
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
23
+ # for the purpose of providing contextual help to Karafka users
24
+ #
25
+ # Receipt, viewing, or possession of this software does not convey or
26
+ # imply any license or right beyond those expressly stated above.
27
+ #
28
+ # License: https://karafka.io/docs/Pro-License-Comm/
29
+ # Contact: contact@karafka.io
30
+
31
+ module Karafka
32
+ module Web
33
+ module Pro
34
+ module Ui
35
+ module Lib
36
+ # Filtering (search) is a Pro-only feature, so all of its wiring lives in this concern and
37
+ # is mixed only into Pro controllers. OSS controllers never gain filtering.
38
+ #
39
+ # It is included by the Pro base controller (covering every standalone Pro controller)
40
+ # and, because a few Pro controllers inherit OSS controllers directly (and thus bypass
41
+ # the Pro base controller), by those explicitly.
42
+ module Filtering
43
+ # Descriptor for a filterable field that matches nested hash KEYS at a position in the
44
+ # structure, rather than an attribute on the records. Used for tree-shaped data whose
45
+ # meaningful labels are keys (e.g. the health stats, where consumer group and topic
46
+ # names are keys). See {Web::Ui::Lib::Filter} for how the engine consumes it.
47
+ #
48
+ # @!attribute name
49
+ # @return [String] the field name (shown in the selector, matched against the request)
50
+ # @!attribute path
51
+ # @return [Array] keys to descend from each container before matching that hash's keys
52
+ KeyField = Struct.new(:name, :path)
53
+
54
+ # Builds a {KeyField} descriptor for use in `filterable_attributes`.
55
+ #
56
+ # @param name [Symbol, String] field name
57
+ # @param under [Symbol, String, Array, nil] key(s) to descend before matching keys; nil
58
+ # matches the container's own keys
59
+ # @return [KeyField]
60
+ def self.key(name, under: nil)
61
+ KeyField.new(name.to_s, Array(under))
62
+ end
63
+
64
+ # @param base [Class] the controller including this concern
65
+ def self.included(base)
66
+ # Attributes on which we can filter in a given controller. Since we can filter on
67
+ # method invocations, this needs to be limited and provided on a per controller basis
68
+ # (same contract as `sortable_attributes`).
69
+ #
70
+ # It is either a flat list applied to every action, or a `{ action => fields }` hash
71
+ # with an optional `:default` entry for controllers whose actions render different
72
+ # columns (e.g. cluster brokers vs configs). See {#filterable_fields} for the
73
+ # resolution.
74
+ base.singleton_class.attr_accessor :filterable_attributes
75
+
76
+ # Expose the per-action filterable fields to the view so the filtering box (and the
77
+ # view-level filtering helpers) can render the field selector. Resolved from the
78
+ # controller's `filterable_attributes` declaration on every request, so no action has
79
+ # to wire this up by hand.
80
+ base.before { @filterable_fields = filterable_fields }
81
+ end
82
+
83
+ private
84
+
85
+ # Filters the provided resources in place based on the current filtering keyword and the
86
+ # fields allowed for filtering.
87
+ #
88
+ # The allowed fields default to `@filterable_fields`, which the before hook resolves
89
+ # from the controller's `filterable_attributes` declaration for the current action and
90
+ # exposes to the view. Pass `fields` only when the engine's match set must differ from
91
+ # the fields shown in the selector (e.g. the health views scope the selector by
92
+ # topic/consumer group but keyword-match against the leaf partition id); it overrides
93
+ # matching only and does not change the exposed selector.
94
+ #
95
+ # @param resources [Hash, Array, Web::Ui::Lib::HashProxy] object for filtering
96
+ # @param fields [Array<Symbol>, nil] explicit engine allow-list for matching, overriding
97
+ # `@filterable_fields`
98
+ # @return [Hash, Array, Web::Ui::Lib::HashProxy] filtered results
99
+ # @note It mutates the provided resources in place, so it must not be used on shared
100
+ # structures like the app routing.
101
+ def filter(resources, fields: nil)
102
+ Web::Ui::Lib::Filter.new(
103
+ @params.current_filter,
104
+ allowed_attributes: fields || @filterable_fields,
105
+ field: @params.current_filter_field
106
+ ).call(resources)
107
+ end
108
+
109
+ # Resolves the fields the current action can be filtered on from the controller's
110
+ # `filterable_attributes` declaration. The declaration is either a flat list (applied to
111
+ # every action) or a `{ action => fields }` hash with an optional `:default` entry used
112
+ # for the actions not listed explicitly.
113
+ #
114
+ # @return [Array<Symbol>, Array<String>] fields for the current action (empty when the
115
+ # action is not filterable)
116
+ def filterable_fields
117
+ attributes = self.class.filterable_attributes
118
+
119
+ return Array(attributes) unless attributes.is_a?(Hash)
120
+
121
+ Array(attributes[@current_action_name] || attributes[:default])
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -40,10 +40,10 @@ module Karafka
40
40
  # Base class for all the search matchers
41
41
  # Each matcher needs to have a class `#name` method and needs to respond to `#call`
42
42
  class Base
43
- # @param phrase [String] string phrase for search
44
43
  # @param message [Karafka::Messages::Message] message in which we search
44
+ # @param phrase [String] string phrase for search
45
45
  # @return [Boolean] true if found, otherwise false
46
- def call(phrase, message)
46
+ def call(message, phrase)
47
47
  raise NotImplementedError, "Implement in a subclass"
48
48
  end
49
49
 
@@ -40,8 +40,6 @@ module Karafka
40
40
  controller = build(Controllers::ClusterController)
41
41
 
42
42
  r.get "replication" do
43
- # We use the non-pro controller here because this action is the same
44
- controller = build(Ui::Controllers::ClusterController)
45
43
  controller.replication
46
44
  end
47
45
 
@@ -0,0 +1,41 @@
1
+ <%# Karafka Pro - Source Available Commercial Software %>
2
+ <%# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved. %>
3
+ <%# This software is NOT open source. It is source-available commercial software %>
4
+ <%# requiring a paid license for use. It is NOT covered by LGPL. %>
5
+ <%# The author retains all right, title, and interest in this software, %>
6
+ <%# including all copyrights, patents, and other intellectual property rights. %>
7
+ <%# No patent rights are granted under this license. %>
8
+ <%# PROHIBITED: Use without a valid commercial license %>
9
+ <%# PROHIBITED: Redistribution, modification, or derivative works without authorization %>
10
+ <%# PROHIBITED: Reverse engineering, decompilation, or disassembly of this software %>
11
+ <%# PROHIBITED: Use as training data for AI/ML models or inclusion in datasets %>
12
+ <%# PROHIBITED: Scraping, crawling, or automated collection for any purpose %>
13
+ <%# PERMITTED: Reading, referencing, and linking for personal or commercial use %>
14
+ <%# PERMITTED: Runtime retrieval by AI assistants, coding agents, and RAG systems %>
15
+ <%# PERMITTED: for the purpose of providing contextual help to Karafka users %>
16
+ <%# Receipt, viewing, or possession of this software does not convey or %>
17
+ <%# imply any license or right beyond those expressly stated above. %>
18
+ <%# License: https://karafka.io/docs/Pro-License-Comm/ %>
19
+ <%# Contact: contact@karafka.io %>
20
+
21
+ <div class="col-span-12 mb-3">
22
+ <div class="data-table-wrapper">
23
+ <table class="data-table">
24
+ <thead>
25
+ <tr>
26
+ <th><%== sort_link('Node ID', :id) %></th>
27
+ <th><%== sort_link(:name) %></th>
28
+ <th><%== sort_link(:port) %></th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <%==
33
+ each_partial(
34
+ @brokers,
35
+ 'cluster/broker'
36
+ )
37
+ %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ </div>
@@ -0,0 +1,41 @@
1
+ <%# Karafka Pro - Source Available Commercial Software %>
2
+ <%# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved. %>
3
+ <%# This software is NOT open source. It is source-available commercial software %>
4
+ <%# requiring a paid license for use. It is NOT covered by LGPL. %>
5
+ <%# The author retains all right, title, and interest in this software, %>
6
+ <%# including all copyrights, patents, and other intellectual property rights. %>
7
+ <%# No patent rights are granted under this license. %>
8
+ <%# PROHIBITED: Use without a valid commercial license %>
9
+ <%# PROHIBITED: Redistribution, modification, or derivative works without authorization %>
10
+ <%# PROHIBITED: Reverse engineering, decompilation, or disassembly of this software %>
11
+ <%# PROHIBITED: Use as training data for AI/ML models or inclusion in datasets %>
12
+ <%# PROHIBITED: Scraping, crawling, or automated collection for any purpose %>
13
+ <%# PERMITTED: Reading, referencing, and linking for personal or commercial use %>
14
+ <%# PERMITTED: Runtime retrieval by AI assistants, coding agents, and RAG systems %>
15
+ <%# PERMITTED: for the purpose of providing contextual help to Karafka users %>
16
+ <%# Receipt, viewing, or possession of this software does not convey or %>
17
+ <%# imply any license or right beyond those expressly stated above. %>
18
+ <%# License: https://karafka.io/docs/Pro-License-Comm/ %>
19
+ <%# Contact: contact@karafka.io %>
20
+
21
+ <div class="data-table-wrapper">
22
+ <table class="data-table">
23
+ <thead>
24
+ <tr>
25
+ <th><%== sort_link(:name) %></th>
26
+ <th><%== sort_link(:value) %></th>
27
+ <th><%== sort_link(:default?) %></th>
28
+ <th><%== sort_link(:sensitive?) %></th>
29
+ <th><%== sort_link(:read_only?) %></th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <%==
34
+ each_partial(
35
+ @configs,
36
+ 'cluster/config'
37
+ )
38
+ %>
39
+ </tbody>
40
+ </table>
41
+ </div>
@@ -35,9 +35,9 @@
35
35
  <%= partition[:leader] %>
36
36
  </td>
37
37
  <td>
38
- <%= partition[:replica_count] %>
38
+ <%== partition_replica_brokers(partition, link: true) %>
39
39
  </td>
40
40
  <td>
41
- <%= partition[:in_sync_replica_brokers] %>
41
+ <%== partition_in_sync_brokers(partition, link: true) %>
42
42
  </td>
43
43
  </tr>