sidekiq 7.0.0 → 7.3.0

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +261 -13
  3. data/README.md +34 -27
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +204 -109
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +151 -23
  8. data/lib/sidekiq/capsule.rb +20 -0
  9. data/lib/sidekiq/cli.rb +9 -4
  10. data/lib/sidekiq/client.rb +40 -24
  11. data/lib/sidekiq/component.rb +3 -1
  12. data/lib/sidekiq/config.rb +32 -12
  13. data/lib/sidekiq/deploy.rb +5 -5
  14. data/lib/sidekiq/embedded.rb +3 -3
  15. data/lib/sidekiq/fetch.rb +3 -5
  16. data/lib/sidekiq/iterable_job.rb +53 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +22 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +231 -0
  22. data/lib/sidekiq/job.rb +17 -10
  23. data/lib/sidekiq/job_logger.rb +24 -11
  24. data/lib/sidekiq/job_retry.rb +34 -11
  25. data/lib/sidekiq/job_util.rb +51 -15
  26. data/lib/sidekiq/launcher.rb +38 -22
  27. data/lib/sidekiq/logger.rb +1 -1
  28. data/lib/sidekiq/metrics/query.rb +6 -3
  29. data/lib/sidekiq/metrics/shared.rb +4 -4
  30. data/lib/sidekiq/metrics/tracking.rb +9 -3
  31. data/lib/sidekiq/middleware/chain.rb +12 -9
  32. data/lib/sidekiq/middleware/current_attributes.rb +70 -17
  33. data/lib/sidekiq/monitor.rb +17 -4
  34. data/lib/sidekiq/paginator.rb +4 -4
  35. data/lib/sidekiq/processor.rb +41 -27
  36. data/lib/sidekiq/rails.rb +18 -8
  37. data/lib/sidekiq/redis_client_adapter.rb +31 -35
  38. data/lib/sidekiq/redis_connection.rb +29 -7
  39. data/lib/sidekiq/scheduled.rb +4 -4
  40. data/lib/sidekiq/testing.rb +27 -8
  41. data/lib/sidekiq/transaction_aware_client.rb +7 -0
  42. data/lib/sidekiq/version.rb +1 -1
  43. data/lib/sidekiq/web/action.rb +10 -4
  44. data/lib/sidekiq/web/application.rb +113 -16
  45. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  46. data/lib/sidekiq/web/helpers.rb +104 -33
  47. data/lib/sidekiq/web.rb +63 -2
  48. data/lib/sidekiq.rb +2 -1
  49. data/sidekiq.gemspec +8 -29
  50. data/web/assets/javascripts/application.js +45 -0
  51. data/web/assets/javascripts/dashboard-charts.js +38 -12
  52. data/web/assets/javascripts/dashboard.js +8 -10
  53. data/web/assets/javascripts/metrics.js +64 -2
  54. data/web/assets/stylesheets/application-dark.css +4 -0
  55. data/web/assets/stylesheets/application-rtl.css +10 -0
  56. data/web/assets/stylesheets/application.css +38 -4
  57. data/web/locales/da.yml +11 -4
  58. data/web/locales/en.yml +2 -0
  59. data/web/locales/fr.yml +14 -0
  60. data/web/locales/gd.yml +99 -0
  61. data/web/locales/ja.yml +3 -1
  62. data/web/locales/pt-br.yml +20 -0
  63. data/web/locales/tr.yml +101 -0
  64. data/web/locales/zh-cn.yml +20 -19
  65. data/web/views/_footer.erb +14 -2
  66. data/web/views/_job_info.erb +18 -2
  67. data/web/views/_metrics_period_select.erb +12 -0
  68. data/web/views/_paging.erb +2 -0
  69. data/web/views/_poll_link.erb +1 -1
  70. data/web/views/_summary.erb +7 -7
  71. data/web/views/busy.erb +46 -35
  72. data/web/views/dashboard.erb +25 -35
  73. data/web/views/filtering.erb +7 -0
  74. data/web/views/layout.erb +6 -6
  75. data/web/views/metrics.erb +42 -31
  76. data/web/views/metrics_for_job.erb +41 -51
  77. data/web/views/morgue.erb +5 -9
  78. data/web/views/queue.erb +10 -14
  79. data/web/views/queues.erb +9 -3
  80. data/web/views/retries.erb +5 -9
  81. data/web/views/scheduled.erb +12 -13
  82. metadata +37 -32
@@ -6,8 +6,51 @@ require "yaml"
6
6
  require "cgi"
7
7
 
8
8
  module Sidekiq
9
- # This is not a public API
9
+ # These methods are available to pages within the Web UI and UI extensions.
10
+ # They are not public APIs for applications to use.
10
11
  module WebHelpers
12
+ def style_tag(location, **kwargs)
13
+ global = location.match?(/:\/\//)
14
+ location = root_path + location if !global && !location.start_with?(root_path)
15
+ attrs = {
16
+ type: "text/css",
17
+ media: "screen",
18
+ rel: "stylesheet",
19
+ nonce: csp_nonce,
20
+ href: location
21
+ }
22
+ html_tag(:link, attrs.merge(kwargs))
23
+ end
24
+
25
+ def script_tag(location, **kwargs)
26
+ global = location.match?(/:\/\//)
27
+ location = root_path + location if !global && !location.start_with?(root_path)
28
+ attrs = {
29
+ type: "text/javascript",
30
+ nonce: csp_nonce,
31
+ src: location
32
+ }
33
+ html_tag(:script, attrs.merge(kwargs)) {}
34
+ end
35
+
36
+ # NB: keys and values are not escaped; do not allow user input
37
+ # in the attributes
38
+ private def html_tag(tagname, attrs)
39
+ s = "<#{tagname}"
40
+ attrs.each_pair do |k, v|
41
+ next unless v
42
+ s << " #{k}=\"#{v}\""
43
+ end
44
+ if block_given?
45
+ s << ">"
46
+ yield s
47
+ s << "</#{tagname}>"
48
+ else
49
+ s << " />"
50
+ end
51
+ s
52
+ end
53
+
11
54
  def strings(lang)
12
55
  @strings ||= {}
13
56
 
@@ -15,12 +58,16 @@ module Sidekiq
15
58
  # so extensions can be localized
16
59
  @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
60
  find_locale_files(lang).each do |file|
18
- strs = YAML.safe_load(File.open(file))
61
+ strs = YAML.safe_load(File.read(file))
19
62
  global.merge!(strs[lang])
20
63
  end
21
64
  end
22
65
  end
23
66
 
67
+ def to_json(x)
68
+ Sidekiq.dump_json(x)
69
+ end
70
+
24
71
  def singularize(str, count)
25
72
  if count == 1 && str.respond_to?(:singularize) # rails
26
73
  str.singularize
@@ -42,15 +89,36 @@ module Sidekiq
42
89
  end
43
90
 
44
91
  def available_locales
45
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
92
+ @available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
46
93
  end
47
94
 
48
95
  def find_locale_files(lang)
49
96
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
50
97
  end
51
98
 
52
- # This is a hook for a Sidekiq Pro feature. Please don't touch.
53
- def filtering(*)
99
+ def search(jobset, substr)
100
+ resultset = jobset.scan(substr).to_a
101
+ @current_page = 1
102
+ @count = @total_size = resultset.size
103
+ resultset
104
+ end
105
+
106
+ def filtering(which)
107
+ erb(:filtering, locals: {which: which})
108
+ end
109
+
110
+ def filter_link(jid, within = "retries")
111
+ if within.nil?
112
+ ::Rack::Utils.escape_html(jid)
113
+ else
114
+ "<a href='#{root_path}filter/#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
115
+ end
116
+ end
117
+
118
+ def display_tags(job, within = "retries")
119
+ job.tags.map { |tag|
120
+ "<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
121
+ }.join(" ")
54
122
  end
55
123
 
56
124
  # This view helper provide ability display you html code in
@@ -96,7 +164,10 @@ module Sidekiq
96
164
  #
97
165
  # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
98
166
  def locale
99
- @locale ||= begin
167
+ # session[:locale] is set via the locale selector from the footer
168
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
+ l
170
+ else
100
171
  matched_locale = user_preferred_languages.map { |preferred|
101
172
  preferred_language = preferred.split("-", 2).first
102
173
 
@@ -111,14 +182,7 @@ module Sidekiq
111
182
  end
112
183
  end
113
184
 
114
- # within is used by Sidekiq Pro
115
- def display_tags(job, within = nil)
116
- job.tags.map { |tag|
117
- "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
118
- }.join(" ")
119
- end
120
-
121
- # mperham/sidekiq#3243
185
+ # sidekiq/sidekiq#3243
122
186
  def unfiltered?
123
187
  yield unless env["PATH_INFO"].start_with?("/filter/")
124
188
  end
@@ -137,7 +201,7 @@ module Sidekiq
137
201
  end
138
202
 
139
203
  def sort_direction_label
140
- params[:direction] == "asc" ? "&uarr;" : "&darr;"
204
+ (params[:direction] == "asc") ? "&uarr;" : "&darr;"
141
205
  end
142
206
 
143
207
  def workset
@@ -161,13 +225,21 @@ module Sidekiq
161
225
  end
162
226
  end
163
227
 
228
+ def busy_weights(capsule_weights)
229
+ # backwards compat with 7.0.0, remove in 7.1
230
+ cw = [capsule_weights].flatten
231
+ cw.map { |hash|
232
+ hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
233
+ }.join("; ")
234
+ end
235
+
164
236
  def stats
165
237
  @stats ||= Sidekiq::Stats.new
166
238
  end
167
239
 
168
240
  def redis_url
169
241
  Sidekiq.redis do |conn|
170
- conn._config.server_url
242
+ conn.config.server_url
171
243
  end
172
244
  end
173
245
 
@@ -184,7 +256,7 @@ module Sidekiq
184
256
  end
185
257
 
186
258
  def current_status
187
- workset.size == 0 ? "idle" : "active"
259
+ (workset.size == 0) ? "idle" : "active"
188
260
  end
189
261
 
190
262
  def relative_time(time)
@@ -217,7 +289,7 @@ module Sidekiq
217
289
  end
218
290
 
219
291
  def truncate(text, truncate_after_chars = 2000)
220
- truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
292
+ (truncate_after_chars && text.size > truncate_after_chars) ? "#{text[0..truncate_after_chars]}..." : text
221
293
  end
222
294
 
223
295
  def display_args(args, truncate_after_chars = 2000)
@@ -237,6 +309,10 @@ module Sidekiq
237
309
  "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
238
310
  end
239
311
 
312
+ def csp_nonce
313
+ env[:csp_nonce]
314
+ end
315
+
240
316
  def to_display(arg)
241
317
  arg.inspect
242
318
  rescue
@@ -270,27 +346,17 @@ module Sidekiq
270
346
  elsif rss_kb < 10_000_000
271
347
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
272
348
  else
273
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
349
+ "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
274
350
  end
275
351
  end
276
352
 
277
- def number_with_delimiter(number)
278
- return "" if number.nil?
279
-
280
- begin
281
- Float(number)
282
- rescue ArgumentError, TypeError
283
- return number
284
- end
285
-
286
- options = {delimiter: ",", separator: "."}
287
- parts = number.to_s.to_str.split(".")
288
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
289
- parts.join(options[:separator])
353
+ def number_with_delimiter(number, options = {})
354
+ precision = options[:precision] || 0
355
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
290
356
  end
291
357
 
292
358
  def h(text)
293
- ::Rack::Utils.escape_html(text)
359
+ ::Rack::Utils.escape_html(text.to_s)
294
360
  rescue ArgumentError => e
295
361
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
296
362
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -323,6 +389,11 @@ module Sidekiq
323
389
  Time.now.utc.strftime("%H:%M:%S UTC")
324
390
  end
325
391
 
392
+ def pollable?
393
+ # there's no point to refreshing the metrics pages every N seconds
394
+ !(current_path == "" || current_path.index("metrics"))
395
+ end
396
+
326
397
  def retry_or_delete_or_kill(job, params)
327
398
  if params["retry"]
328
399
  job.retry
data/lib/sidekiq/web.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "securerandom"
4
5
 
5
6
  require "sidekiq"
6
7
  require "sidekiq/api"
@@ -34,6 +35,18 @@ module Sidekiq
34
35
  "Metrics" => "metrics"
35
36
  }
36
37
 
38
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
39
+ CONTENT_LANGUAGE = "Content-Language"
40
+ CONTENT_SECURITY_POLICY = "Content-Security-Policy"
41
+ LOCATION = "Location"
42
+ X_CASCADE = "X-Cascade"
43
+ else
44
+ CONTENT_LANGUAGE = "content-language"
45
+ CONTENT_SECURITY_POLICY = "content-security-policy"
46
+ LOCATION = "location"
47
+ X_CASCADE = "x-cascade"
48
+ end
49
+
37
50
  class << self
38
51
  def settings
39
52
  self
@@ -48,6 +61,10 @@ module Sidekiq
48
61
  end
49
62
  alias_method :tabs, :custom_tabs
50
63
 
64
+ def custom_job_info_rows
65
+ @custom_job_info_rows ||= []
66
+ end
67
+
51
68
  def locales
52
69
  @locales ||= LOCALES
53
70
  end
@@ -98,6 +115,7 @@ module Sidekiq
98
115
  end
99
116
 
100
117
  def call(env)
118
+ env[:csp_nonce] = SecureRandom.base64(16)
101
119
  app.call(env)
102
120
  end
103
121
 
@@ -122,7 +140,50 @@ module Sidekiq
122
140
  send(:"#{attribute}=", value)
123
141
  end
124
142
 
125
- def self.register(extension)
143
+ # Register a class as a Sidekiq Web UI extension. The class should
144
+ # provide one or more tabs which map to an index route. Options:
145
+ #
146
+ # @param extension [Class] Class which contains the HTTP actions, required
147
+ # @param name [String] the name of the extension, used to namespace assets
148
+ # @param tab [String | Array] labels(s) of the UI tabs
149
+ # @param index [String | Array] index route(s) for each tab
150
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
151
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
152
+ # @param cache_for [Integer] amount of time to cache assets, default one day
153
+ #
154
+ # TODO name, tab and index will be mandatory in 8.0
155
+ #
156
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
157
+ # and `views/` subdirectories.
158
+ def self.register(extension, name: nil, tab: nil, index: nil, root_dir: nil, cache_for: 86400, asset_paths: nil)
159
+ tab = Array(tab)
160
+ index = Array(index)
161
+ tab.zip(index).each do |tab, index|
162
+ tabs[tab] = index
163
+ end
164
+ if root_dir
165
+ locdir = File.join(root_dir, "locales")
166
+ locales << locdir if File.directory?(locdir)
167
+
168
+ if asset_paths && name
169
+ # if you have {root}/assets/{name}/js/scripts.js
170
+ # and {root}/assets/{name}/css/styles.css
171
+ # you would pass in:
172
+ # asset_paths: ["js", "css"]
173
+ # See script_tag and style_tag in web/helpers.rb
174
+ assdir = File.join(root_dir, "assets")
175
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
176
+ assetprops = {
177
+ urls: assurls,
178
+ root: assdir,
179
+ cascade: true
180
+ }
181
+ assetprops[:header_rules] = [[:all, {Rack::CACHE_CONTROL => "private, max-age=#{cache_for.to_i}"}]] if cache_for
182
+ middlewares << [[Rack::Static, assetprops], nil]
183
+ end
184
+ end
185
+
186
+ yield self if block_given?
126
187
  extension.registered(WebApplication)
127
188
  end
128
189
 
@@ -133,7 +194,7 @@ module Sidekiq
133
194
  m = middlewares
134
195
 
135
196
  rules = []
136
- rules = [[:all, {"cache-control" => "public, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
197
+ rules = [[:all, {Rack::CACHE_CONTROL => "private, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
137
198
 
138
199
  ::Rack::Builder.new do
139
200
  use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
data/lib/sidekiq.rb CHANGED
@@ -32,6 +32,7 @@ require "sidekiq/logger"
32
32
  require "sidekiq/client"
33
33
  require "sidekiq/transaction_aware_client"
34
34
  require "sidekiq/job"
35
+ require "sidekiq/iterable_job"
35
36
  require "sidekiq/worker_compatibility_alias"
36
37
  require "sidekiq/redis_client_adapter"
37
38
 
@@ -112,7 +113,7 @@ module Sidekiq
112
113
  # end
113
114
  # inst.run
114
115
  # sleep 10
115
- # inst.terminate
116
+ # inst.stop
116
117
  #
117
118
  # NB: it is really easy to overload a Ruby process with threads due to the GIL.
118
119
  # I do not recommend setting concurrency higher than 2-3.
data/sidekiq.gemspec CHANGED
@@ -2,7 +2,7 @@ require_relative "lib/sidekiq/version"
2
2
 
3
3
  Gem::Specification.new do |gem|
4
4
  gem.authors = ["Mike Perham"]
5
- gem.email = ["mperham@gmail.com"]
5
+ gem.email = ["info@contribsys.com"]
6
6
  gem.summary = "Simple, efficient background processing for Ruby"
7
7
  gem.description = "Simple, efficient background processing for Ruby."
8
8
  gem.homepage = "https://sidekiq.org"
@@ -16,37 +16,16 @@ Gem::Specification.new do |gem|
16
16
 
17
17
  gem.metadata = {
18
18
  "homepage_uri" => "https://sidekiq.org",
19
- "bug_tracker_uri" => "https://github.com/mperham/sidekiq/issues",
20
- "documentation_uri" => "https://github.com/mperham/sidekiq/wiki",
21
- "changelog_uri" => "https://github.com/mperham/sidekiq/blob/main/Changes.md",
22
- "source_code_uri" => "https://github.com/mperham/sidekiq"
19
+ "bug_tracker_uri" => "https://github.com/sidekiq/sidekiq/issues",
20
+ "documentation_uri" => "https://github.com/sidekiq/sidekiq/wiki",
21
+ "changelog_uri" => "https://github.com/sidekiq/sidekiq/blob/main/Changes.md",
22
+ "source_code_uri" => "https://github.com/sidekiq/sidekiq",
23
+ "rubygems_mfa_required" => "true"
23
24
  }
24
25
 
25
- gem.add_dependency "redis-client", ">= 0.9.0"
26
+ gem.add_dependency "redis-client", ">= 0.22.2"
26
27
  gem.add_dependency "connection_pool", ">= 2.3.0"
27
28
  gem.add_dependency "rack", ">= 2.2.4"
28
29
  gem.add_dependency "concurrent-ruby", "< 2"
29
- gem.post_install_message = <<~EOM
30
-
31
- ####################################################
32
-
33
-
34
- █████████ █████ ██████████ ██████████ █████ ████ █████ ██████ ██████████ █████
35
- ███░░░░░███░░███ ░░███░░░░███ ░░███░░░░░█░░███ ███░ ░░███ ███░░░░███ ░███░░░░███ ███░░░███
36
- ░███ ░░░ ░███ ░███ ░░███ ░███ █ ░ ░███ ███ ░███ ███ ░░███ ░░░ ███ ███ ░░███
37
- ░░█████████ ░███ ░███ ░███ ░██████ ░███████ ░███ ░███ ░███ ███ ░███ ░███
38
- ░░░░░░░░███ ░███ ░███ ░███ ░███░░█ ░███░░███ ░███ ░███ ██░███ ███ ░███ ░███
39
- ███ ░███ ░███ ░███ ███ ░███ ░ █ ░███ ░░███ ░███ ░░███ ░░████ ███ ░░███ ███
40
- ░░█████████ █████ ██████████ ██████████ █████ ░░████ █████ ░░░██████░██ ███ ██ ░░░█████░
41
- ░░░░░░░░░ ░░░░░ ░░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░ ░░░░░ ░░░░░░ ░░ ░░░ ░░ ░░░░░░
42
-
43
-
44
- WARNING: This is a beta release, expect breakage!
45
-
46
- 1. Use `gem 'sidekiq', '<7'` in your Gemfile if you don't want to be a beta tester.
47
- 2. Read the release notes at https://github.com/mperham/sidekiq/blob/main/docs/7.0-Upgrade.md
48
- 3. Search for open/closed issues at https://github.com/mperham/sidekiq/issues/
49
-
50
- ####################################################
51
- EOM
30
+ gem.add_dependency "logger"
52
31
  end
@@ -31,7 +31,10 @@ function addListeners() {
31
31
  node.addEventListener("click", addDataToggleListeners)
32
32
  })
33
33
 
34
+ addShiftClickListeners()
34
35
  updateFuzzyTimes();
36
+ updateNumbers();
37
+ updateProgressBars();
35
38
  setLivePollFromUrl();
36
39
 
37
40
  var buttons = document.querySelectorAll(".live-poll");
@@ -45,6 +48,8 @@ function addListeners() {
45
48
  scheduleLivePoll();
46
49
  }
47
50
  }
51
+
52
+ document.getElementById("locale-select").addEventListener("change", updateLocale);
48
53
  }
49
54
 
50
55
  function addPollingListeners(_event) {
@@ -71,6 +76,23 @@ function addDataToggleListeners(event) {
71
76
  }
72
77
  }
73
78
 
79
+ function addShiftClickListeners() {
80
+ let checkboxes = Array.from(document.querySelectorAll(".shift_clickable"));
81
+ let lastChecked = null;
82
+ checkboxes.forEach(checkbox => {
83
+ checkbox.addEventListener("click", (e) => {
84
+ if (e.shiftKey && lastChecked) {
85
+ let myIndex = checkboxes.indexOf(checkbox);
86
+ let lastIndex = checkboxes.indexOf(lastChecked);
87
+ let [min, max] = [myIndex, lastIndex].sort();
88
+ let newState = checkbox.checked;
89
+ checkboxes.slice(min, max).forEach(c => c.checked = newState);
90
+ }
91
+ lastChecked = checkbox;
92
+ });
93
+ });
94
+ }
95
+
74
96
  function updateFuzzyTimes() {
75
97
  var locale = document.body.getAttribute("data-locale");
76
98
  var parts = locale.split('-');
@@ -84,6 +106,20 @@ function updateFuzzyTimes() {
84
106
  t.cancel();
85
107
  }
86
108
 
109
+ function updateNumbers() {
110
+ document.querySelectorAll("[data-nwp]").forEach(node => {
111
+ let number = parseFloat(node.textContent);
112
+ let precision = parseInt(node.dataset["nwp"] || 0);
113
+ if (typeof number === "number") {
114
+ let formatted = number.toLocaleString(undefined, {
115
+ minimumFractionDigits: precision,
116
+ maximumFractionDigits: precision,
117
+ });
118
+ node.textContent = formatted;
119
+ }
120
+ });
121
+ }
122
+
87
123
  function setLivePollFromUrl() {
88
124
  var url_params = new URL(window.location.href).searchParams
89
125
 
@@ -122,6 +158,7 @@ function checkResponse(resp) {
122
158
 
123
159
  function scheduleLivePoll() {
124
160
  let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000;
161
+ if (ti < 2000) { ti = 2000 }
125
162
  livePollTimer = setTimeout(livePollCallback, ti);
126
163
  }
127
164
 
@@ -141,3 +178,11 @@ function replacePage(text) {
141
178
  function showError(error) {
142
179
  console.error(error)
143
180
  }
181
+
182
+ function updateLocale(event) {
183
+ event.target.form.submit();
184
+ }
185
+
186
+ function updateProgressBars() {
187
+ document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
188
+ }
@@ -57,7 +57,9 @@ class DashboardChart extends BaseChart {
57
57
  class RealtimeChart extends DashboardChart {
58
58
  constructor(el, options) {
59
59
  super(el, options);
60
- this.delay = parseInt(localStorage.sidekiqTimeInterval) || 5000;
60
+ let d = parseInt(localStorage.sidekiqTimeInterval) || 5000;
61
+ if (d < 2000) { d = 2000; }
62
+ this.delay = d
61
63
  this.startPolling();
62
64
  document.addEventListener("interval:update", this.handleUpdate.bind(this));
63
65
  }
@@ -84,6 +86,7 @@ class RealtimeChart extends DashboardChart {
84
86
  updateStatsSummary(this.stats.sidekiq);
85
87
  updateRedisStats(this.stats.redis);
86
88
  updateFooterUTCTime(this.stats.server_utc_time);
89
+ updateNumbers();
87
90
  pulseBeacon();
88
91
 
89
92
  this.stats = stats;
@@ -105,17 +108,27 @@ class RealtimeChart extends DashboardChart {
105
108
  }
106
109
 
107
110
  renderLegend(dp) {
108
- this.legend.innerHTML = `
109
- <span>
110
- <span class="swatch" style="background-color: ${dp[0].dataset.borderColor};"></span>
111
- <span>${dp[0].dataset.label}: ${dp[0].formattedValue}</span>
112
- </span>
113
- <span>
114
- <span class="swatch" style="background-color: ${dp[1].dataset.borderColor};"></span>
115
- <span>${dp[1].dataset.label}: ${dp[1].formattedValue}</span>
116
- </span>
117
- <span class="time">${dp[0].label}</span>
118
- `;
111
+ const entry1 = this.legendEntry(dp[0]);
112
+ const entry2 = this.legendEntry(dp[1]);
113
+ const time = document.createElement("span");
114
+ time.classList.add("time");
115
+ time.innerText = dp[0].label;
116
+
117
+ this.legend.replaceChildren(entry1, entry2, time)
118
+ }
119
+
120
+ legendEntry(dp) {
121
+ const wrapper = document.createElement("span");
122
+
123
+ const swatch = document.createElement("span");
124
+ swatch.classList.add("swatch");
125
+ swatch.style.backgroundColor = dp.dataset.borderColor;
126
+ wrapper.appendChild(swatch)
127
+
128
+ const label = document.createElement("span");
129
+ label.innerText = `${dp.dataset.label}: ${dp.formattedValue}`;
130
+ wrapper.appendChild(label)
131
+ return wrapper;
119
132
  }
120
133
 
121
134
  renderCursor(dp) {
@@ -164,3 +177,16 @@ class RealtimeChart extends DashboardChart {
164
177
  };
165
178
  }
166
179
  }
180
+
181
+ var rc = document.getElementById("realtime-chart")
182
+ if (rc != null) {
183
+ var rtc = new RealtimeChart(rc, JSON.parse(rc.textContent))
184
+ rtc.registerLegend(document.getElementById("realtime-legend"))
185
+ window.realtimeChart = rtc
186
+ }
187
+
188
+ var hc = document.getElementById("history-chart")
189
+ if (hc != null) {
190
+ var htc = new DashboardChart(hc, JSON.parse(hc.textContent))
191
+ window.historyChart = htc
192
+ }
@@ -1,15 +1,13 @@
1
1
  Sidekiq = {};
2
2
 
3
- var nf = new Intl.NumberFormat();
4
-
5
3
  var updateStatsSummary = function(data) {
6
- document.getElementById("txtProcessed").innerText = nf.format(data.processed);
7
- document.getElementById("txtFailed").innerText = nf.format(data.failed);
8
- document.getElementById("txtBusy").innerText = nf.format(data.busy);
9
- document.getElementById("txtScheduled").innerText = nf.format(data.scheduled);
10
- document.getElementById("txtRetries").innerText = nf.format(data.retries);
11
- document.getElementById("txtEnqueued").innerText = nf.format(data.enqueued);
12
- document.getElementById("txtDead").innerText = nf.format(data.dead);
4
+ document.getElementById("txtProcessed").innerText = data.processed;
5
+ document.getElementById("txtFailed").innerText = data.failed;
6
+ document.getElementById("txtBusy").innerText = data.busy;
7
+ document.getElementById("txtScheduled").innerText = data.scheduled;
8
+ document.getElementById("txtRetries").innerText = data.retries;
9
+ document.getElementById("txtEnqueued").innerText = data.enqueued;
10
+ document.getElementById("txtDead").innerText = data.dead;
13
11
  }
14
12
 
15
13
  var updateRedisStats = function(data) {
@@ -30,7 +28,7 @@ var pulseBeacon = function() {
30
28
  }
31
29
 
32
30
  var setSliderLabel = function(val) {
33
- document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' sec';
31
+ document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' s';
34
32
  }
35
33
 
36
34
  var ready = (callback) => {