karafka-web 0.10.2 → 0.10.4

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 (72) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +17 -0
  4. data/Gemfile.lock +12 -10
  5. data/config/locales/pro_errors.yml +7 -1
  6. data/docker-compose.yml +1 -1
  7. data/karafka-web.gemspec +8 -2
  8. data/lib/karafka/web/config.rb +6 -1
  9. data/lib/karafka/web/contracts/config.rb +1 -0
  10. data/lib/karafka/web/management/actions/create_topics.rb +1 -1
  11. data/lib/karafka/web/pro/commanding/commands/{probe.rb → trace.rb} +3 -3
  12. data/lib/karafka/web/pro/commanding/listener.rb +1 -1
  13. data/lib/karafka/web/pro/commanding/manager.rb +2 -2
  14. data/lib/karafka/web/pro/commanding.rb +1 -1
  15. data/lib/karafka/web/pro/loader.rb +23 -2
  16. data/lib/karafka/web/pro/ui/app.rb +12 -2
  17. data/lib/karafka/web/pro/ui/controllers/commanding_controller.rb +4 -4
  18. data/lib/karafka/web/pro/ui/controllers/routing_controller.rb +18 -1
  19. data/lib/karafka/web/pro/ui/controllers/scheduled_messages/messages_controller.rb +60 -0
  20. data/lib/karafka/web/pro/ui/lib/branding/config.rb +40 -0
  21. data/lib/karafka/web/pro/ui/lib/branding/contracts/config.rb +56 -0
  22. data/lib/karafka/web/pro/ui/lib/branding.rb +36 -0
  23. data/lib/karafka/web/pro/ui/views/commands/_command.erb +1 -1
  24. data/lib/karafka/web/pro/ui/views/consumers/_consumer_controls.erb +3 -3
  25. data/lib/karafka/web/pro/ui/views/consumers/consumer/_commands.erb +4 -4
  26. data/lib/karafka/web/pro/ui/views/recurring_tasks/_actions.erb +2 -2
  27. data/lib/karafka/web/pro/ui/views/routing/_consumer_group.erb +2 -0
  28. data/lib/karafka/web/pro/ui/views/routing/_topic.erb +10 -0
  29. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/_key.erb +9 -0
  30. data/lib/karafka/web/pro/ui/views/scheduled_messages/explorer/_message.erb +51 -13
  31. data/lib/karafka/web/pro/ui/views/shared/_navigation.erb +20 -15
  32. data/lib/karafka/web/pro/ui/views/shared/branding/_label.erb +20 -0
  33. data/lib/karafka/web/pro/ui/views/shared/branding/_notice.erb +13 -0
  34. data/lib/karafka/web/tracking/consumers/listeners/errors.rb +38 -9
  35. data/lib/karafka/web/tracking/producers/reporter.rb +1 -8
  36. data/lib/karafka/web/ui/helpers/tailwind_helper.rb +5 -4
  37. data/lib/karafka/web/ui/public/javascripts/application.js +1 -0
  38. data/lib/karafka/web/ui/public/javascripts/application.min.js +4 -3
  39. data/lib/karafka/web/ui/public/javascripts/application.min.js.br +0 -0
  40. data/lib/karafka/web/ui/public/javascripts/application.min.js.gz +0 -0
  41. data/lib/karafka/web/ui/public/javascripts/components/btn_toggle_manager.js +31 -11
  42. data/lib/karafka/web/ui/public/stylesheets/application.css +21 -0
  43. data/lib/karafka/web/ui/public/stylesheets/application.min.css +2 -1
  44. data/lib/karafka/web/ui/public/stylesheets/application.min.css.br +0 -0
  45. data/lib/karafka/web/ui/public/stylesheets/application.min.css.gz +0 -0
  46. data/lib/karafka/web/ui/public/stylesheets/libs/tailwind.css +69 -2
  47. data/lib/karafka/web/ui/views/layout.erb +5 -2
  48. data/lib/karafka/web/ui/views/shared/_brand.erb +3 -2
  49. data/lib/karafka/web/ui/views/shared/_navigation.erb +19 -15
  50. data/lib/karafka/web/ui/views/shared/alerts/_error.erb +3 -1
  51. data/lib/karafka/web/ui/views/shared/alerts/_info.erb +1 -1
  52. data/lib/karafka/web/ui/views/shared/alerts/_primary.erb +3 -1
  53. data/lib/karafka/web/ui/views/shared/alerts/_secondary.erb +3 -1
  54. data/lib/karafka/web/ui/views/shared/alerts/_success.erb +3 -1
  55. data/lib/karafka/web/ui/views/shared/alerts/_warning.erb +3 -1
  56. data/lib/karafka/web/ui/views/shared/icons/_chevron.erb +4 -0
  57. data/lib/karafka/web/version.rb +1 -1
  58. data/lib/karafka/web.rb +6 -1
  59. data/package-lock.json +40 -35
  60. data.tar.gz.sig +0 -0
  61. metadata +13 -15
  62. metadata.gz.sig +0 -0
  63. data/.coditsu/ci.yml +0 -3
  64. data/.diffend.yml +0 -3
  65. data/.github/FUNDING.yml +0 -1
  66. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  67. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  68. data/.github/workflows/ci.yml +0 -133
  69. data/.gitignore +0 -71
  70. data/.rspec +0 -1
  71. data/.ruby-gemset +0 -1
  72. data/.ruby-version +0 -1
@@ -10,6 +10,7 @@
10
10
  <th>Subscription group</th>
11
11
  <th><%== sort_link('Topic', :name) %></th>
12
12
  <th>Type</th>
13
+ <th>Assigned</th>
13
14
  <th><%== sort_link(:active?) %></th>
14
15
  <th></th>
15
16
  </tr>
@@ -21,6 +22,7 @@
21
22
  'routing/topic',
22
23
  locals: {
23
24
  subscription_group: topic.subscription_group,
25
+ consumer_group: consumer_group.id,
24
26
  topic: topic
25
27
  }
26
28
  )
@@ -20,6 +20,16 @@
20
20
  <% end %>
21
21
  </td>
22
22
 
23
+ <td>
24
+ <% assigned = @assigned[consumer_group].include?(topic.name) %>
25
+
26
+ <% if assigned %>
27
+ <%== badge_success assigned %>
28
+ <% else %>
29
+ <%== badge_secondary assigned %>
30
+ <% end %>
31
+ </td>
32
+
23
33
  <td>
24
34
  <% if topic.active? %>
25
35
  <%== badge_success topic.active? %>
@@ -0,0 +1,9 @@
1
+ <% if @visibility_filter.key?(message) %>
2
+ <% if safe_key.success? %>
3
+ <%= safe_key.result %>
4
+ <% else %>
5
+ <span class="text-muted">[Deserialization Failed]</span>
6
+ <% end %>
7
+ <% else %>
8
+ <span class="text-muted">[Filtered]</span>
9
+ <% end %>
@@ -39,8 +39,22 @@
39
39
  </td>
40
40
 
41
41
  <% if type == 'cancel' %>
42
- <td colspan="5" class="text-center text-muted">
43
- Cancellation messages do not contain those details.
42
+ <td>
43
+ <%== relative_time(message.timestamp) %>
44
+ </td>
45
+ <td class="text-center text-muted">
46
+ N/A
47
+ </td>
48
+ <td>
49
+ <%==
50
+ partial(
51
+ 'scheduled_messages/explorer/key',
52
+ locals: { message: message, safe_key: safe_key }
53
+ )
54
+ %>
55
+ </td>
56
+ <td colspan="2" class="text-center text-muted">
57
+ N/A
44
58
  </td>
45
59
  <% else %>
46
60
  <td>
@@ -50,15 +64,12 @@
50
64
  <%== relative_time message.headers['schedule_target_epoch'].to_i %>
51
65
  </td>
52
66
  <td>
53
- <% if @visibility_filter.key?(message) %>
54
- <% if safe_key.success? %>
55
- <%= safe_key.result %>
56
- <% else %>
57
- <span class="text-muted">[Deserialization Failed]</span>
58
- <% end %>
59
- <% else %>
60
- <span class="text-muted">[Filtered]</span>
61
- <% end %>
67
+ <%==
68
+ partial(
69
+ 'scheduled_messages/explorer/key',
70
+ locals: { message: message, safe_key: safe_key }
71
+ )
72
+ %>
62
73
  </td>
63
74
  <td>
64
75
  <%= message.headers['schedule_target_topic'] %>
@@ -73,10 +84,37 @@
73
84
  <% end %>
74
85
 
75
86
  <td>
87
+ <% if type == 'schedule' %>
88
+ <%
89
+ cancel_path = root_path(
90
+ "scheduled_messages/messages",
91
+ message.topic,
92
+ message.partition,
93
+ message.offset,
94
+ 'cancel'
95
+ )
96
+ %>
97
+ <form
98
+ action="<%= cancel_path %>"
99
+ method="post"
100
+ class="inline"
101
+ >
102
+ <%== csrf_tag(cancel_path) %>
103
+ <button
104
+ type="submit"
105
+ class="btn btn-error btn-sm confirm-action"
106
+ title="Cancel dispatch of this scheduled message"
107
+ >
108
+ <%== icon(:x_circle) %>
109
+ </button>
110
+ </form>
111
+ <% end %>
112
+
76
113
  <%==
77
114
  link_button_primary_sm(
78
- 'Details',
79
- explorer_path(message.topic, message.partition, message.offset)
115
+ icon(:info_circle),
116
+ explorer_path(message.topic, message.partition, message.offset),
117
+ title: 'Display details of this message'
80
118
  )
81
119
  %>
82
120
  </td>
@@ -1,14 +1,15 @@
1
1
  <nav class="sidebar-nav">
2
2
  <%== partial 'shared/brand' %>
3
+ <%== partial 'shared/branding/label' %>
3
4
 
4
- <ul class="menu">
5
+ <ul class="menu grow">
5
6
  <li>
6
7
  <a
7
8
  class="sidebar-nav-item <%= nav_class(start_with: '/dashboard') %>"
8
9
  href="<%= root_path('dashboard') %>"
9
10
  >
10
11
  <%== icon(:home) %>
11
- Dashboard
12
+ <span class="sidebar-nav-item-label">Dashboard</span>
12
13
  </a>
13
14
  </li>
14
15
 
@@ -18,7 +19,7 @@
18
19
  href="<%= root_path('consumers') %>"
19
20
  >
20
21
  <%== icon(:cpu) %>
21
- Consumers
22
+ <span class="sidebar-nav-item-label">Consumers</span>
22
23
  </a>
23
24
  </li>
24
25
 
@@ -28,7 +29,7 @@
28
29
  href="<%= root_path('jobs/running') %>"
29
30
  >
30
31
  <%== icon(:arrow_on_squares) %>
31
- Jobs
32
+ <span class="sidebar-nav-item-label">Jobs</span>
32
33
  </a>
33
34
  </li>
34
35
 
@@ -38,7 +39,7 @@
38
39
  href="<%= root_path('health/overview') %>"
39
40
  >
40
41
  <%== icon(:heart) %>
41
- Health
42
+ <span class="sidebar-nav-item-label">Health</span>
42
43
  </a>
43
44
  </li>
44
45
 
@@ -48,7 +49,7 @@
48
49
  href="<%= root_path('routing') %>"
49
50
  >
50
51
  <%== icon(:queue_list) %>
51
- Routing
52
+ <span class="sidebar-nav-item-label">Routing</span>
52
53
  </a>
53
54
  </li>
54
55
 
@@ -58,7 +59,7 @@
58
59
  href="<%= root_path('explorer') %>"
59
60
  >
60
61
  <%== icon(:document_glass) %>
61
- Explorer
62
+ <span class="sidebar-nav-item-label">Explorer</span>
62
63
  </a>
63
64
  </li>
64
65
 
@@ -68,7 +69,7 @@
68
69
  href="<%= root_path('recurring_tasks') %>"
69
70
  >
70
71
  <%== icon(:arrow_path_rounded) %>
71
- Cron
72
+ <span class="sidebar-nav-item-label">Cron</span>
72
73
  </a>
73
74
  </li>
74
75
 
@@ -78,7 +79,7 @@
78
79
  href="<%= root_path('scheduled_messages') %>"
79
80
  >
80
81
  <%== icon(:calendar) %>
81
- Schedules
82
+ <span class="sidebar-nav-item-label">Schedules</span>
82
83
  </a>
83
84
  </li>
84
85
 
@@ -88,7 +89,7 @@
88
89
  href="<%= root_path('errors') %>"
89
90
  >
90
91
  <%== icon(:bug) %>
91
- Errors
92
+ <span class="sidebar-nav-item-label">Errors</span>
92
93
  </a>
93
94
  </li>
94
95
 
@@ -98,7 +99,7 @@
98
99
  href="<%= root_path('dlq') %>"
99
100
  >
100
101
  <%== icon(:x_circle) %>
101
- Dead
102
+ <span class="sidebar-nav-item-label">Dead</span>
102
103
  </a>
103
104
  </li>
104
105
 
@@ -108,7 +109,7 @@
108
109
  href="<%= root_path('cluster') %>"
109
110
  >
110
111
  <%== icon(:blocks) %>
111
- Cluster
112
+ <span class="sidebar-nav-item-label">Cluster</span>
112
113
  </a>
113
114
  </li>
114
115
 
@@ -118,7 +119,7 @@
118
119
  href="<%= root_path('topics') %>"
119
120
  >
120
121
  <%== icon(:list_bullets) %>
121
- Topics
122
+ <span class="sidebar-nav-item-label">Topics</span>
122
123
  </a>
123
124
  </li>
124
125
 
@@ -128,7 +129,7 @@
128
129
  href="<%= root_path('status') %>"
129
130
  >
130
131
  <%== icon(:check_badge) %>
131
- Status
132
+ <span class="sidebar-nav-item-label">Status</span>
132
133
  </a>
133
134
  </li>
134
135
 
@@ -138,8 +139,12 @@
138
139
  href="<%= root_path('support') %>"
139
140
  >
140
141
  <%== icon(:lifebuoy) %>
141
- Support
142
+ <span class="sidebar-nav-item-label">Support</span>
142
143
  </a>
143
144
  </li>
144
145
  </ul>
146
+
147
+ <button type="button" class="btn-toggle-nav-collapsed grid gap-2 grid-flow-col items-center justify-start ml-6 text-sm invisible lg:visible" data-toggle-target="drawer-side">
148
+ <%== icon(:chevron) %>
149
+ </button>
145
150
  </nav>
@@ -0,0 +1,20 @@
1
+ <% branding_cfg = Karafka::Web.config.ui.branding %>
2
+ <% if branding_cfg.label %>
3
+ <div class="branding-label text-left mt-4 ml-6">
4
+ <%==
5
+ public_send(
6
+ :"badge_#{branding_cfg.type}",
7
+ branding_cfg.label
8
+ )
9
+ %>
10
+ </div>
11
+
12
+ <div class="branding-mark text-left mt-4 ml-6">
13
+ <%==
14
+ public_send(
15
+ :"badge_#{branding_cfg.type}",
16
+ branding_cfg.label[0]
17
+ )
18
+ %>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <% branding_cfg = Karafka::Web.config.ui.branding %>
2
+ <% if branding_cfg.notice %>
3
+ <div id="branding" class="col-span-12">
4
+ <div class="mb-3">
5
+ <%==
6
+ public_send(
7
+ :"alert_#{branding_cfg.type}",
8
+ branding_cfg.notice
9
+ )
10
+ %>
11
+ </div>
12
+ </div>
13
+ <% end %>
@@ -18,17 +18,24 @@ module Karafka
18
18
  #
19
19
  # @param event [Karafka::Core::Monitoring::Event]
20
20
  def on_error_occurred(event)
21
- track do |sampler|
22
- # Collect extra info if it was a consumer related error.
23
- # Those come from user code
24
- details = if event[:caller].is_a?(Karafka::BaseConsumer)
25
- extract_consumer_info(event[:caller])
26
- else
27
- {}
28
- end
21
+ caller_ref = event[:caller]
22
+
23
+ # Collect extra info if it was a consumer related error.
24
+ # Those come from user code
25
+ details = case caller_ref
26
+ when Karafka::BaseConsumer
27
+ extract_consumer_info(caller_ref)
28
+ when Karafka::Connection::Client
29
+ extract_client_info(caller_ref)
30
+ when Karafka::Connection::Listener
31
+ extract_listener_info(caller_ref)
32
+ else
33
+ {}
34
+ end
29
35
 
30
- error_class, error_message, backtrace = extract_error_info(event[:error])
36
+ error_class, error_message, backtrace = extract_error_info(event[:error])
31
37
 
38
+ track do |sampler|
32
39
  sampler.errors << {
33
40
  schema_version: SCHEMA_VERSION,
34
41
  type: event[:type],
@@ -70,6 +77,7 @@ module Karafka
70
77
  {
71
78
  topic: consumer.topic.name,
72
79
  consumer_group: consumer.topic.consumer_group.id,
80
+ subscription_group: consumer.topic.subscription_group.id,
73
81
  partition: consumer.partition,
74
82
  first_offset: consumer.messages.metadata.first_offset,
75
83
  last_offset: consumer.messages.metadata.last_offset,
@@ -80,6 +88,27 @@ module Karafka
80
88
  tags: consumer.tags
81
89
  }
82
90
  end
91
+
92
+ # @param client [::Karafka::Connection::Client]
93
+ # @return [Hash] hash with client specific info for details of error
94
+ def extract_client_info(client)
95
+ {
96
+ consumer_group: client.subscription_group.consumer_group.id,
97
+ subscription_group: client.subscription_group.id,
98
+ name: client.name,
99
+ id: client.id
100
+ }
101
+ end
102
+
103
+ # @param listener [::Karafka::Connection::Listener]
104
+ # @return [Hash] hash with listener specific info for details of error
105
+ def extract_listener_info(listener)
106
+ {
107
+ consumer_group: listener.subscription_group.consumer_group.id,
108
+ subscription_group: listener.subscription_group.id,
109
+ id: listener.id
110
+ }
111
+ end
83
112
  end
84
113
  end
85
114
  end
@@ -10,13 +10,6 @@ module Karafka
10
10
  # because there is no expectation on immediate status updates for producers and their
11
11
  # dispatch flow is always periodic based.
12
12
  class Reporter < Tracking::Reporter
13
- # Minimum number of messages to produce to produce them in sync mode
14
- # This acts as a small back-off not to overload the system in case we would have
15
- # extremely big number of errors happening
16
- PRODUCE_SYNC_THRESHOLD = 25
17
-
18
- private_constant :PRODUCE_SYNC_THRESHOLD
19
-
20
13
  # This mutex is shared between tracker and samplers so there is no case where metrics
21
14
  # would be collected same time tracker reports
22
15
  MUTEX = Mutex.new
@@ -82,7 +75,7 @@ module Karafka
82
75
  # normal operations we should not have that many messages to dispatch and it should not
83
76
  # slowdown any processing.
84
77
  def produce(messages)
85
- if messages.count >= PRODUCE_SYNC_THRESHOLD
78
+ if messages.count >= ::Karafka::Web.config.tracking.producers.sync_threshold
86
79
  ::Karafka::Web.producer.produce_many_sync(messages)
87
80
  else
88
81
  ::Karafka::Web.producer.produce_many_async(messages)
@@ -34,9 +34,10 @@ module Karafka
34
34
  # @param name [String] button name
35
35
  # @param path [String] path to where to go
36
36
  # @param classes [String] extra css classes
37
+ # @param title [String, nil] title (if any)
37
38
  # @return [String] button link html
38
- def link_button(name, path, classes: '')
39
- %(<a href="#{path}" class="btn #{classes}">#{name}</a>)
39
+ def link_button(name, path, classes: '', title: nil)
40
+ %(<a href="#{path}" class="btn #{classes}" title="#{title}">#{name}</a>)
40
41
  end
41
42
 
42
43
  # Defines various methods for badges and links that simplify defining them without
@@ -54,8 +55,8 @@ module Karafka
54
55
  link_button(name, path, classes: "#{classes} btn-#{type}")
55
56
  end
56
57
 
57
- define_method :"link_button_#{type}_sm" do |name, path, classes: ''|
58
- link_button(name, path, classes: "#{classes} btn-#{type} btn-sm")
58
+ define_method :"link_button_#{type}_sm" do |name, path, classes: '', title: nil|
59
+ link_button(name, path, classes: "#{classes} btn-#{type} btn-sm", title: title)
59
60
  end
60
61
 
61
62
  # @param message [String] alert message
@@ -110,6 +110,7 @@ function addListeners() {
110
110
  loadOffsetLookupDatePicker();
111
111
 
112
112
  new BtnToggleManager();
113
+ new BtnToggleManager('.btn-toggle-nav-collapsed', 'collapsed');
113
114
  new ThemeManager();
114
115
 
115
116
  refreshTitle()
@@ -51,7 +51,7 @@ function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"f
51
51
  /*! Source: lib/karafka/web/ui/public/javascripts/charts/data_formatting_utility.js */
52
52
  let DataFormattingUtils={niceBytes(e,t=2){let i=0,s=parseInt(e,10)||0;for(;1024<=s&&++i;)s/=1024;return s.toFixed(s<10&&0<i?1:t)+" "+["bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][i]},formatLabelX(e,t){return"date"!==t?e:("00"+((t=new Date(1e3*e)).getMonth()+1)).slice(-2)+"/"+("00"+t.getDate()).slice(-2)+"/"+t.getFullYear()+" "+("00"+t.getHours()).slice(-2)+":"+("00"+t.getMinutes()).slice(-2)+":"+("00"+t.getSeconds()).slice(-2)},formatTooltip(e,t){var i=t.parsed.y,s=t.dataset.label;switch(e){case"percentage":return Math.floor(i)===i?s+": "+i+" %":s+": "+Math.round(100*i)/100+" %";case"memory":return s+": "+DataFormattingUtils.niceBytes(1024*i,2);default:return t.yLabel}},formatLabelY(e,t){switch(e){case"percentage":return Math.floor(t)===t?t+"%":Math.round(100*t)/100+"%";case"memory":return DataFormattingUtils.niceBytes(1024*t,1);default:return Math.floor(t)===t?t:Math.round(100*t)/100}},isFractionalPrecision(e){return e!==Math.floor(e)}};
53
53
  /*! Source: lib/karafka/web/ui/public/javascripts/charts/dataset_state_manager.js */class DatasetStateManager{constructor(){this.storageKey="karafkaDisabledDatasets"}readAll(){var e=localStorage.getItem(this.storageKey);return e?JSON.parse(e):{}}saveAll(e){localStorage.setItem(this.storageKey,JSON.stringify(e))}saveCurrent(){var e=document.querySelectorAll(".chartjs"),t=window.location.href.split("?")[0];let i={};var s=this.readAll();e.forEach(e=>{var e=e.id,t=Chart.getChart(e);t&&t.legend&&t.legend.legendItems&&0<(t=t.legend.legendItems.map((e,t)=>e.hidden?t:null).filter(e=>null!==e)).length&&(i[e]=t)}),s[t]=i,this.saveAll(s)}getCurrentChart(e){var t=window.location.href.split("?")[0];return(this.readAll()[t]||{})[e]||[]}}
54
- /*! Source: lib/karafka/web/ui/public/javascripts/components/btn_toggle_manager.js */class BtnToggleManager{constructor(){this.init()}init(){document.querySelectorAll(".btn-toggle").forEach(t=>{let i=t.getAttribute("data-toggle-target"),s=document.getElementById(i);s&&(this.restoreVisibility(t,s),t.addEventListener("click",()=>{var e=!s.classList.contains("hidden");s.classList.toggle("hidden"),t.classList.toggle("active",!e),this.saveVisibility(i,!e)}))})}saveVisibility(e,t){localStorage.setItem(e+"_visibility",t)}restoreVisibility(e,t){var i=localStorage.getItem(t.id+"_visibility"),i=i?"true"===i:!t.classList.contains("hidden");t.classList.toggle("hidden",!i),e.classList.toggle("active",i)}}
54
+ /*! Source: lib/karafka/web/ui/public/javascripts/components/btn_toggle_manager.js */class BtnToggleManager{constructor(e=".btn-toggle",t="hidden"){this.btnClass=e,this.visibilityClass=t,this.init()}init(){document.querySelectorAll(this.btnClass).forEach(t=>{let i=t.getAttribute("data-toggle-target"),s=document.getElementById(i);s&&(this.restoreVisibility(t,s),t._isClickListenerAdded||(t.addEventListener("click",()=>{var e=!s.classList.contains(this.visibilityClass);s.classList.toggle(this.visibilityClass),t.classList.toggle("active",!e),this.saveVisibility(i,!e)}),t._isClickListenerAdded=!0))})}saveVisibility(e,t){localStorage.setItem(e+"_visibility",t)}restoreVisibility(e,t){var i=localStorage.getItem(t.id+"_visibility"),i=i?"true"===i:!t.classList.contains(this.visibilityClass);t.classList.toggle(this.visibilityClass,!i),e.classList.toggle("active",i)}}
55
55
  /*! Source: lib/karafka/web/ui/public/javascripts/components/charts.js */function refreshCharts(e){(new LineChartsManager).refreshAndRender(e,!0),(new BarChartManager).refreshAndRenderBarCharts(e,!0)}function manageCharts(){(new LineChartsManager).refreshAndRender(document,!1),(new BarChartManager).refreshAndRenderBarCharts(document,!1)}
56
56
  /*! Source: lib/karafka/web/ui/public/javascripts/components/live_poll.js */var livePollTimer=null,oldDOM=null,datePicker=null,startURL=window.location.href;let isHoveringOverClickable=!1;function initLivePolling(){document.addEventListener("mouseover",function(e){isElementClickable(e.target)&&(isHoveringOverClickable=!0)}),document.addEventListener("mouseout",function(e){isElementClickable(e.target)&&(isHoveringOverClickable=!1)}),null==localStorage.karafkaLivePoll&&(localStorage.karafkaLivePoll="enabled")}function isFormActive(){var e=document.activeElement;return["INPUT","TEXTAREA","SELECT","BUTTON","FIELDSET"].includes(e.tagName)}function isElementClickable(e){return!!("A"===e.tagName||"BUTTON"===e.tagName||"INPUT"===e.tagName&&("button"===e.type||"submit"===e.type)||e.hasAttribute("onclick")||"function"==typeof e.onclick||"SPAN"===e.tagName&&e.closest(".tab-container")||e.closest("button")||e.closest("a"))}function isUserHoveringOverClickable(){return isHoveringOverClickable}function isAnyTextSelected(){var e="";return void 0!==window.getSelection?e=window.getSelection().toString():void 0!==document.selection&&"Text"==document.selection.type&&(e=document.selection.createRange().text),""!=e}function isCollapsingHappening(){return 0<document.querySelectorAll(".collapsing").length}function isPollingPossible(e=!1){return!(isFormActive()||isUserHoveringOverClickable()||isAnyTextSelected()||isOffsetLookupCalendarVisible()||isAnyModalOpen()||isCollapsingHappening()||isTurboOperating()||e&&startURL!=window.location.href)}function isAnyModalOpen(){var e;for(e of document.querySelectorAll("dialog"))if(e.open)return!0;return!1}function bindPollingButtonClick(){var e=document.getElementById("live-poll");null!=e&&e.addEventListener("click",handleLivePollingButtonClick)}function handleLivePollingButtonClick(){toggleLivePollState(),setLivePollButton(),setPollingListener()}function toggleLivePollState(){"enabled"==localStorage.karafkaLivePoll?localStorage.karafkaLivePoll="disabled":localStorage.karafkaLivePoll="enabled"}function setLivePollButton(){null!=(selector=document.getElementById("live-poll"))&&("enabled"==localStorage.karafkaLivePoll?(selector.classList.add("text-base-content"),selector.classList.remove("text-gray-500")):(selector.classList.add("text-gray-500"),selector.classList.remove("text-base-content")))}function checkResponse(e){if(e.ok)return e;throw e}function refreshPage(e){if(!isPollingPossible())return!1;var t,e=(new DOMParser).parseFromString(e,"text/html"),i=e.getElementById("content");oldDOM!=i.innerHTML&&(t=document.querySelectorAll(".chartjs").length,0==i.querySelectorAll(".chartjs").length||0==t?(document.getElementById("content").replaceWith(i),addListeners()):(t=e.getElementById("refreshable"),document.getElementById("refreshable").replaceWith(t),refreshCharts(e)),oldDOM=i.innerHTML)}function showError(e){console.error(e)}function scheduleLivePoll(){null==oldDOM&&(oldDOM=document.getElementById("content").innerHTML);let e=parseInt(localStorage.karafkaTimeInterval)||5e3;e<1e3&&(localStorage.karafkaTimeInterval=5e3,e=5e3),livePollTimer=setTimeout(livePollCallback,e)}function livePollCallback(){clearTimeout(livePollTimer),livePollTimer=null,isPollingPossible(!1)?(startURL=window.location.href,fetch(window.location.href).then(checkResponse).then(e=>e.text()).then(refreshPage).catch(showError).finally(setPollingListener)):setPollingListener()}function setPollingListener(){var e=document.getElementById("live-poll"),t=localStorage.karafkaLivePoll;"disabled"==t||null==t||null==e?(clearTimeout(livePollTimer),livePollTimer=null):(clearTimeout(livePollTimer),scheduleLivePoll())}
57
57
  /*! Source: lib/karafka/web/ui/public/javascripts/components/offset_datetime.js */function loadOffsetLookupDatePicker(){var e={locale:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear",dateFormat:"yyyy-MM-dd",timeFormat:"HH:mm",firstDay:1},timepicker:!0,onSelect:({})=>{document.getElementById("offset-lookup-datepicker").value=""},onShow:function(){offsetLookupDatePicker.selectDate((new Date).getTime()),offsetLookupDatePicker.maxDate=new Date},onHide:function(){offsetLookupDatePicker.selectDate((new Date).getTime())},buttons:[{content(e){return"Go to offset"},onClick(e){var t=e.selectedDates[0]||new Date,i=e.$el.dataset.target;e.hide(),location.href=i+"/"+formatRedirectDateTime(t)}}]};null!=offsetLookupDatePicker&&offsetLookupDatePicker.destroy(),null!=document.getElementById("offset-lookup-datepicker")&&((offsetLookupDatePicker=new AirDatepicker("#offset-lookup-datepicker",e)).maxDate=new Date,offsetLookupDatePicker.selectDate((new Date).getTime()))}function formatRedirectDateTime(e){return e.getFullYear()+`-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}/${String(e.getHours()).padStart(2,"0")}:`+String(e.getMinutes()).padStart(2,"0")}function isOffsetLookupCalendarVisible(){return null!=offsetLookupDatePicker&&offsetLookupDatePicker.visible}
@@ -59,6 +59,7 @@ let DataFormattingUtils={niceBytes(e,t=2){let i=0,s=parseInt(e,10)||0;for(;1024<
59
59
  /*! Source: lib/karafka/web/ui/public/javascripts/components/tabs_manager.js */class TabsManager{constructor(){this.storageKey="karafkaActiveTabsv2"}readAllActiveTabs(){var e=localStorage.getItem(this.storageKey);return e?JSON.parse(e):{}}saveAllActiveTabs(e){localStorage.setItem(this.storageKey,JSON.stringify(e))}saveCurrentActiveTabs(){var e=document.querySelectorAll(".inline-tabs > .active"),t=window.location.href.split("?")[0];let i=[];var s=this.readAllActiveTabs();e.forEach(e=>{i.push(e.id)}),s[t]=i,this.saveAllActiveTabs(s)}setActiveTabs(){var e=window.location.href.split("?")[0],e=this.readAllActiveTabs()[e];(e=e||Array.from(document.querySelectorAll(".inline-tabs > .active")).map(e=>e.id))&&e.forEach(e=>{e=document.getElementById(e);e&&(e.parentElement.querySelectorAll(".custom-tab").forEach(function(e){e.classList.remove("active")}),e.classList.add("active"),document.getElementById(e.getAttribute("data-target")).classList.remove("hidden"))})}manageTabs(){this.setActiveTabs();var t=this;document.querySelectorAll(".inline-tabs > .custom-tab").forEach(function(e){e.addEventListener("click",function(e){this.parentElement.querySelectorAll(".custom-tab").forEach(function(e){e.classList.remove("active");e=e.getAttribute("data-target");document.getElementById(e).classList.add("hidden")}),this.classList.add("active"),t.saveCurrentActiveTabs(),t.setActiveTabs()})})}}
60
60
  /*! Source: lib/karafka/web/ui/public/javascripts/components/theme_manager.js */class ThemeManager{constructor(){this.themeSelectorButton=document.getElementById("theme-selector"),this.themeSelectorLight=document.getElementById("theme-selector-light"),this.themeSelectorDark=document.getElementById("theme-selector-dark"),this.init()}init(){this.lightThemeLink=document.querySelector(".highlight-light"),this.darkThemeLink=document.querySelector(".highlight-dark"),this.lightThemeLink&&this.darkThemeLink&&this.themeSelectorButton?(this.restoreTheme(),this.bindThemeSelectorButton()):console.error("Theme CSS links or theme selector button not found")}bindThemeSelectorButton(){this.themeSelectorButton&&!this.themeSelectorButton.dataset.bound&&(this.themeSelectorButton.addEventListener("click",()=>{this.toggleTheme()}),this.themeSelectorButton.dataset.bound="true")}setTheme(e){document.documentElement.setAttribute("data-theme",e),"dark"===e?(this.lightThemeLink.disabled=!0,this.darkThemeLink.disabled=!1,this.themeSelectorLight.classList.add("hidden"),this.themeSelectorDark.classList.remove("hidden")):(this.lightThemeLink.disabled=!1,this.darkThemeLink.disabled=!0,this.themeSelectorLight.classList.remove("hidden"),this.themeSelectorDark.classList.add("hidden")),localStorage.setItem("theme",e)}toggleTheme(){var e=document.documentElement.getAttribute("data-theme");this.setTheme("dark"===e?"corporate":"dark")}restoreTheme(){var e=localStorage.getItem("theme"),e=e||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"corporate");this.setTheme(e),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{localStorage.getItem("theme")||this.setTheme(e.matches?"dark":"corporate")})}}
61
61
  /*! Source: lib/karafka/web/ui/public/javascripts/components/turbo_tracker.js */let turboIsOperating=!1;function isTurboOperating(){return turboIsOperating}
62
- /*! Source: lib/karafka/web/ui/public/javascripts/application.js */function updateTimeAgo(){0!=(e=document.querySelectorAll("time")).length&&(timeago.render(e),timeago.cancel());for(var e=document.getElementsByClassName("time-title"),t=0;t<e.length;t++){var i=e[t],s=i.getAttribute("title");i.setAttribute("title",timeago.format(s))}}function refreshTitle(){var e=document.querySelectorAll(".breadcrumbs a"),e=Array.from(e).slice(1).map(e=>e.textContent.trim());0<e.length?document.title=e.join(" > ")+" - Karafka Web UI":document.title="Karafka Web UI"}function redirectToPartition(){var e=document.getElementById("current-partition");null!=e&&e.addEventListener("change",function(){Turbo.visit(this.value)})}function bindActionsConfirmations(){for(var t=document.getElementsByClassName("confirm-action"),i=0;i<t.length;i++){var s=t[i];let e="click";"FORM"===s.nodeName&&(e="submit"),s.addEventListener(e,function(e){window.confirm("Are you sure?")||e.preventDefault()})}}function bindLockableButtons(){document.querySelectorAll(".btn-lockable").forEach(function(i){i.addEventListener("click",function(e){var t=i.closest("form");t?t.addEventListener("submit",function(){i.disabled=!0,i.textContent+="...",document.querySelectorAll(".modal").forEach(function(e){e.classList.add("modal-locked")})},{once:!0}):(i.disabled=!0,i.textContent+="...")})})}function addListeners(){initLivePolling(),bindPollingButtonClick(),bindLockableButtons(),setLivePollButton(),setPollingListener(),hljs.highlightAll(),updateTimeAgo(),redirectToPartition(),(new TabsManager).manageTabs(),manageCharts(),bindActionsConfirmations(),loadOffsetLookupDatePicker(),new BtnToggleManager,new ThemeManager,refreshTitle(),new SearchMetadataVisibilityManager,new SearchModalManager}document.addEventListener("turbo:visit",function(){turboIsOperating=!0}),document.addEventListener("turbo:before-fetch-request",function(){turboIsOperating=!0}),document.addEventListener("turbo:before-fetch-response",function(){turboIsOperating=!0}),document.addEventListener("turbo:load",function(){turboIsOperating=!1}),document.addEventListener("turbo:frame-load",function(){turboIsOperating=!1}),document.addEventListener("turbo:frame-render",function(){turboIsOperating=!1}),document.addEventListener("turbo:load",addListeners),Turbo.setProgressBarDelay(100)
62
+ /*! Source: lib/karafka/web/ui/public/javascripts/application.js */function updateTimeAgo(){0!=(e=document.querySelectorAll("time")).length&&(timeago.render(e),timeago.cancel());for(var e=document.getElementsByClassName("time-title"),t=0;t<e.length;t++){var i=e[t],s=i.getAttribute("title");i.setAttribute("title",timeago.format(s))}}function refreshTitle(){var e=document.querySelectorAll(".breadcrumbs a"),e=Array.from(e).slice(1).map(e=>e.textContent.trim());0<e.length?document.title=e.join(" > ")+" - Karafka Web UI":document.title="Karafka Web UI"}function redirectToPartition(){var e=document.getElementById("current-partition");null!=e&&e.addEventListener("change",function(){Turbo.visit(this.value)})}function bindActionsConfirmations(){for(var t=document.getElementsByClassName("confirm-action"),i=0;i<t.length;i++){var s=t[i];let e="click";"FORM"===s.nodeName&&(e="submit"),s.addEventListener(e,function(e){window.confirm("Are you sure?")||e.preventDefault()})}}function bindLockableButtons(){document.querySelectorAll(".btn-lockable").forEach(function(i){i.addEventListener("click",function(e){var t=i.closest("form");t?t.addEventListener("submit",function(){i.disabled=!0,i.textContent+="...",document.querySelectorAll(".modal").forEach(function(e){e.classList.add("modal-locked")})},{once:!0}):(i.disabled=!0,i.textContent+="...")})})}function addListeners(){initLivePolling(),bindPollingButtonClick(),bindLockableButtons(),setLivePollButton(),setPollingListener(),hljs.highlightAll(),updateTimeAgo(),redirectToPartition(),(new TabsManager).manageTabs(),manageCharts(),bindActionsConfirmations(),loadOffsetLookupDatePicker(),new BtnToggleManager,new BtnToggleManager(".btn-toggle-nav-collapsed","collapsed"),new ThemeManager,refreshTitle(),new SearchMetadataVisibilityManager,new SearchModalManager}document.addEventListener("turbo:visit",function(){turboIsOperating=!0}),document.addEventListener("turbo:before-fetch-request",function(){turboIsOperating=!0}),document.addEventListener("turbo:before-fetch-response",function(){turboIsOperating=!0}),document.addEventListener("turbo:load",function(){turboIsOperating=!1}),document.addEventListener("turbo:frame-load",function(){turboIsOperating=!1}),document.addEventListener("turbo:frame-render",function(){turboIsOperating=!1}),document.addEventListener("turbo:load",addListeners),Turbo.setProgressBarDelay(100)
63
63
  /*! Source: lib/karafka/web/ui/public/javascripts/charts/types/bar.js */;class BarChartManager{constructor(){this.datasetStateManager=new DatasetStateManager}refreshAndRenderBarCharts(c,u=!1){(u?c:document).querySelectorAll(".chartjs-bar").forEach(e=>{var t=e.id,i=(u?c:document).getElementById(t),s=JSON.parse(i.dataset.datasets);let r=[],a=[],n=[];var o=i.dataset.label_type_y;let l=i.dataset.label_type_x,h=this.datasetStateManager.getCurrentChart(t);Object.entries(s).forEach(([e,t],i)=>{t.forEach(([e,t])=>{n.push(t),0===i&&r.push(DataFormattingUtils.formatLabelX(e,l))}),a.push({data:t.map(([,e])=>e),label:e,hidden:h.includes(i),borderWidth:2.5})});var i=Math.min(...n),s=Math.max(...n),i=Math.round(i-.1*i),s=Math.round(s+.005*s),d=Math.round(n.reduce((e,t)=>e+t,0)/n.length);a.push({type:"line",label:"Average",data:new Array(r.length).fill(d),borderWidth:2,fill:!1,pointRadius:0,hoverBorderWidth:3,pointHitRadius:20}),u?((d=Chart.getChart(t)).data.datasets=a,d.data.labels=r,d.options.scales.y.min=i,d.options.scales.y.max=s,d.update("none")):this.renderBarChart(e,r,a,i,s,o)})}renderBarChart(e,t,i,s,r,a){new Chart(e,{type:"bar",data:{labels:t,datasets:i},options:{responsive:!0,maintainAspectRatio:!1,aspectRatio:5,scales:{x:{display:!0},y:{beginAtZero:!1,min:s,max:r,ticks:{maxTicksLimit:8,callback:function(e,t,i){return DataFormattingUtils.formatLabelY(a,e,t,i)}}}},animation:!1,animations:{colors:!1,x:!1},transitions:{active:{animation:{duration:!1}}},plugins:{legend:{position:"hidden"},tooltip:{callbacks:{label:function(e){let t=e.dataset.label||"";return"Average"!==t?DataFormattingUtils.formatTooltip(a,e):t+=": "+e.formattedValue}}}}}})}}
64
- /*! Source: lib/karafka/web/ui/public/javascripts/charts/types/line.js */class LineChartsManager{constructor(){this.datasetStateManager=new DatasetStateManager}getLegendHeightPercentage(e){var t=e.chartArea,e=e.height,t=e-(t.bottom-t.top);return Math.round(t/e*100)}afterRenderPlugin(){let i=this;return{id:"afterRender",afterRender:function(e){var t=i.getLegendHeightPercentage(e),e=document.getElementById(e.canvas.id);50<t&&""==e.parentElement.style.height&&(e.parentElement.style.height="400px")}}}refreshAndRender(d,c=!1){(c?d:document).querySelectorAll(".chartjs-line").forEach(e=>{var t=e.id,i=(c?d:document).getElementById(t),s=JSON.parse(i.dataset.datasets);let r=[],a=[],n=0;var o=i.dataset.label_type_y;let l=i.dataset.label_type_x,h=this.datasetStateManager.getCurrentChart(t);Object.entries(s).forEach(([e,t],i)=>{t.forEach(([e,t])=>{0===i&&r.push(DataFormattingUtils.formatLabelX(e,l)),DataFormattingUtils.isFractionalPrecision(t)&&(n=2)}),a.push({data:t,label:e,hidden:h.includes(i),borderWidth:2.5,pointHitRadius:10})}),c?((i=Chart.getChart(t)).data.datasets=a,i.data.labels=r,i.update("none")):this.render(e,r,a,n,o)})}render(e,t,i,s,r){var a=null,a=10<i.length?"point":"x";new Chart(e,{type:"line",data:{labels:t,datasets:i},options:{responsive:!0,maintainAspectRatio:!1,aspectRatio:5,title:{display:!1},interaction:{mode:"nearest",axis:"x",intersect:!1},animation:!1,transitions:{active:{animation:{duration:!1}}},plugins:{legend:{position:"bottom",labels:{padding:20},onClick:(e,t,i)=>{var s=t.datasetIndex,i=i.chart;i.isDatasetVisible(s)?(i.hide(s),t.hidden=!0):(i.show(s),t.hidden=!1),this.datasetStateManager.saveCurrent()}},tooltip:{mode:a,filter:function(e,t,i){return t<10},callbacks:{label:function(e){return DataFormattingUtils.formatTooltip(r,e)}}}},scales:{x:{display:!1},y:{ticks:{precision:s,count:5,callback:function(e,t,i){return DataFormattingUtils.formatLabelY(r,e,t,i)}}}},elements:{point:{radius:0,style:!1},line:{style:"star",radius:0,spanGaps:!1}},hover:{mode:"index",intersect:!1}},plugins:[this.afterRenderPlugin()]})}}
64
+ /*! Source: lib/karafka/web/ui/public/javascripts/charts/types/line.js */class LineChartsManager{constructor(){this.datasetStateManager=new DatasetStateManager}getLegendHeightPercentage(e){var t=e.chartArea,e=e.height,t=e-(t.bottom-t.top);return Math.round(t/e*100)}afterRenderPlugin(){let i=this;return{id:"afterRender",afterRender:function(e){var t=i.getLegendHeightPercentage(e),e=document.getElementById(e.canvas.id);50<t&&""==e.parentElement.style.height&&(e.parentElement.style.height="400px")}}}refreshAndRender(d,c=!1){(c?d:document).querySelectorAll(".chartjs-line").forEach(e=>{var t=e.id,i=(c?d:document).getElementById(t),s=JSON.parse(i.dataset.datasets);let r=[],a=[],n=0;var o=i.dataset.label_type_y;let l=i.dataset.label_type_x,h=this.datasetStateManager.getCurrentChart(t);Object.entries(s).forEach(([e,t],i)=>{t.forEach(([e,t])=>{0===i&&r.push(DataFormattingUtils.formatLabelX(e,l)),DataFormattingUtils.isFractionalPrecision(t)&&(n=2)}),a.push({data:t,label:e,hidden:h.includes(i),borderWidth:2.5,pointHitRadius:10})}),c?((i=Chart.getChart(t)).data.datasets=a,i.data.labels=r,i.update("none")):this.render(e,r,a,n,o)})}render(e,t,i,s,r){var a=null,a=10<i.length?"point":"x";new Chart(e,{type:"line",data:{labels:t,datasets:i},options:{responsive:!0,maintainAspectRatio:!1,aspectRatio:5,title:{display:!1},interaction:{mode:"nearest",axis:"x",intersect:!1},animation:!1,transitions:{active:{animation:{duration:!1}}},plugins:{legend:{position:"bottom",labels:{padding:20},onClick:(e,t,i)=>{var s=t.datasetIndex,i=i.chart;i.isDatasetVisible(s)?(i.hide(s),t.hidden=!0):(i.show(s),t.hidden=!1),this.datasetStateManager.saveCurrent()}},tooltip:{mode:a,filter:function(e,t,i){return t<10},callbacks:{label:function(e){return DataFormattingUtils.formatTooltip(r,e)}}}},scales:{x:{display:!1},y:{ticks:{precision:s,count:5,callback:function(e,t,i){return DataFormattingUtils.formatLabelY(r,e,t,i)}}}},elements:{point:{radius:0,style:!1},line:{style:"star",radius:0,spanGaps:!1}},hover:{mode:"index",intersect:!1}},plugins:[this.afterRenderPlugin()]})}}
65
+ //# sourceMappingURL=application.min.js.map
@@ -1,10 +1,21 @@
1
+ /*
2
+ This class allows buttons to toggle visibility classes on target elements.
3
+ It also saves the visibility state in local storage so that it persists across page loads.
4
+
5
+ By default, it will look for elements with class 'btn-toggle' and toggle the
6
+ 'hidden' class on their target elements (defined by the data attribute `data-toggle-target`)
7
+ when clicked.
8
+ */
9
+
1
10
  class BtnToggleManager {
2
- constructor() {
11
+ constructor(btnClass = '.btn-toggle', visibilityClass = 'hidden') {
12
+ this.btnClass = btnClass;
13
+ this.visibilityClass = visibilityClass;
3
14
  this.init();
4
15
  }
5
16
 
6
17
  init() {
7
- document.querySelectorAll('.btn-toggle').forEach(button => {
18
+ document.querySelectorAll(this.btnClass).forEach(button => {
8
19
  const targetId = button.getAttribute('data-toggle-target');
9
20
  const targetElement = document.getElementById(targetId);
10
21
 
@@ -13,13 +24,22 @@ class BtnToggleManager {
13
24
  // Establish initial state from local storage or based on visibility
14
25
  this.restoreVisibility(button, targetElement);
15
26
 
16
- // Add event listener to toggle visibility
17
- button.addEventListener('click', () => {
18
- const isVisible = !targetElement.classList.contains('hidden');
19
- targetElement.classList.toggle('hidden');
20
- button.classList.toggle('active', !isVisible);
21
- this.saveVisibility(targetId, !isVisible);
22
- });
27
+ // Check if the event listener has already been added
28
+ if (!button._isClickListenerAdded) {
29
+ // Define the event handler and store the flag to prevent duplicate listeners
30
+ const handleClick = () => {
31
+ const isVisible = !targetElement.classList.contains(this.visibilityClass);
32
+ targetElement.classList.toggle(this.visibilityClass);
33
+ button.classList.toggle('active', !isVisible);
34
+ this.saveVisibility(targetId, !isVisible);
35
+ };
36
+
37
+ // Add the event listener
38
+ button.addEventListener('click', handleClick);
39
+
40
+ // Set flag to indicate the listener has been added
41
+ button._isClickListenerAdded = true;
42
+ }
23
43
  });
24
44
  }
25
45
 
@@ -29,9 +49,9 @@ class BtnToggleManager {
29
49
 
30
50
  restoreVisibility(button, targetElement) {
31
51
  const storedVisibility = localStorage.getItem(targetElement.id + '_visibility');
32
- const isVisible = storedVisibility ? (storedVisibility === 'true') : !targetElement.classList.contains('hidden');
52
+ const isVisible = storedVisibility ? (storedVisibility === 'true') : !targetElement.classList.contains(this.visibilityClass);
33
53
 
34
- targetElement.classList.toggle('hidden', !isVisible);
54
+ targetElement.classList.toggle(this.visibilityClass, !isVisible);
35
55
  button.classList.toggle('active', isVisible);
36
56
  }
37
57
  }
@@ -35,3 +35,24 @@ table td.commands {
35
35
  table td.commands-inline-3 {
36
36
  width: 180px
37
37
  }
38
+
39
+ .drawer-side .btn-toggle-nav-collapsed svg {
40
+ transform: scale(1, 1);
41
+ transition: transform 0.2s;
42
+ }
43
+
44
+ .drawer-side.collapsed .btn-toggle-nav-collapsed svg {
45
+ transform: scale(-1, -1);
46
+ }
47
+
48
+ .drawer-side .btn-toggle-nav-collapsed::after {
49
+ content: 'Collapse';
50
+ }
51
+
52
+ .drawer-side.collapsed .btn-toggle-nav-collapsed::after {
53
+ content: none;
54
+ }
55
+
56
+ .drawer-side.collapsed:hover .btn-toggle-nav-collapsed::after {
57
+ content: 'Expand';
58
+ }