sidekiq 7.0.9 → 7.3.10

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +263 -0
  3. data/README.md +5 -5
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +41 -14
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +170 -52
  9. data/lib/sidekiq/capsule.rb +8 -3
  10. data/lib/sidekiq/cli.rb +6 -2
  11. data/lib/sidekiq/client.rb +58 -22
  12. data/lib/sidekiq/component.rb +23 -1
  13. data/lib/sidekiq/config.rb +52 -11
  14. data/lib/sidekiq/deploy.rb +4 -2
  15. data/lib/sidekiq/embedded.rb +2 -0
  16. data/lib/sidekiq/fetch.rb +2 -1
  17. data/lib/sidekiq/iterable_job.rb +55 -0
  18. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  19. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  20. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  21. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  22. data/lib/sidekiq/job/iterable.rb +294 -0
  23. data/lib/sidekiq/job.rb +15 -8
  24. data/lib/sidekiq/job_logger.rb +7 -6
  25. data/lib/sidekiq/job_retry.rb +30 -8
  26. data/lib/sidekiq/job_util.rb +6 -2
  27. data/lib/sidekiq/launcher.rb +8 -6
  28. data/lib/sidekiq/logger.rb +1 -1
  29. data/lib/sidekiq/metrics/query.rb +6 -1
  30. data/lib/sidekiq/metrics/shared.rb +16 -5
  31. data/lib/sidekiq/metrics/tracking.rb +20 -8
  32. data/lib/sidekiq/middleware/current_attributes.rb +88 -16
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/middleware/modules.rb +2 -0
  35. data/lib/sidekiq/monitor.rb +2 -1
  36. data/lib/sidekiq/paginator.rb +8 -2
  37. data/lib/sidekiq/processor.rb +44 -33
  38. data/lib/sidekiq/rails.rb +28 -7
  39. data/lib/sidekiq/redis_client_adapter.rb +30 -31
  40. data/lib/sidekiq/redis_connection.rb +49 -9
  41. data/lib/sidekiq/ring_buffer.rb +3 -0
  42. data/lib/sidekiq/scheduled.rb +3 -3
  43. data/lib/sidekiq/systemd.rb +2 -0
  44. data/lib/sidekiq/testing.rb +32 -13
  45. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  46. data/lib/sidekiq/version.rb +5 -1
  47. data/lib/sidekiq/web/action.rb +29 -7
  48. data/lib/sidekiq/web/application.rb +64 -25
  49. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  50. data/lib/sidekiq/web/helpers.rb +95 -35
  51. data/lib/sidekiq/web/router.rb +5 -2
  52. data/lib/sidekiq/web.rb +67 -3
  53. data/lib/sidekiq.rb +5 -3
  54. data/sidekiq.gemspec +5 -13
  55. data/web/assets/javascripts/application.js +27 -0
  56. data/web/assets/javascripts/dashboard-charts.js +40 -12
  57. data/web/assets/javascripts/dashboard.js +14 -10
  58. data/web/assets/javascripts/metrics.js +34 -0
  59. data/web/assets/stylesheets/application-rtl.css +10 -0
  60. data/web/assets/stylesheets/application.css +38 -3
  61. data/web/locales/en.yml +5 -1
  62. data/web/locales/fr.yml +13 -0
  63. data/web/locales/gd.yml +0 -1
  64. data/web/locales/it.yml +32 -1
  65. data/web/locales/ja.yml +0 -1
  66. data/web/locales/pt-br.yml +20 -1
  67. data/web/locales/tr.yml +100 -0
  68. data/web/locales/uk.yml +24 -1
  69. data/web/locales/zh-cn.yml +0 -1
  70. data/web/locales/zh-tw.yml +0 -1
  71. data/web/views/_footer.erb +12 -1
  72. data/web/views/_job_info.erb +1 -1
  73. data/web/views/_metrics_period_select.erb +1 -1
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +7 -7
  76. data/web/views/dashboard.erb +29 -36
  77. data/web/views/filtering.erb +6 -0
  78. data/web/views/layout.erb +6 -6
  79. data/web/views/metrics.erb +38 -30
  80. data/web/views/metrics_for_job.erb +30 -39
  81. data/web/views/morgue.erb +2 -2
  82. data/web/views/queue.erb +1 -1
  83. data/web/views/queues.erb +6 -2
  84. metadata +52 -21
@@ -6,14 +6,57 @@ 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
- @strings ||= {}
55
+ @@strings ||= {}
13
56
 
14
57
  # Allow sidekiq-web extensions to add locale paths
15
58
  # so extensions can be localized
16
- @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
59
+ @@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
60
  find_locale_files(lang).each do |file|
18
61
  strs = YAML.safe_load(File.read(file))
19
62
  global.merge!(strs[lang])
@@ -21,6 +64,10 @@ module Sidekiq
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
@@ -30,27 +77,48 @@ module Sidekiq
30
77
  end
31
78
 
32
79
  def clear_caches
33
- @strings = nil
34
- @locale_files = nil
35
- @available_locales = nil
80
+ @@strings = nil
81
+ @@locale_files = nil
82
+ @@available_locales = nil
36
83
  end
37
84
 
38
85
  def locale_files
39
- @locale_files ||= settings.locales.flat_map { |path|
86
+ @@locale_files ||= settings.locales.flat_map { |path|
40
87
  Dir["#{path}/*.yml"]
41
88
  }
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}#{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,16 +182,10 @@ 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
185
  # sidekiq/sidekiq#3243
122
186
  def unfiltered?
123
- yield unless env["PATH_INFO"].start_with?("/filter/")
187
+ s = url_params("substr")
188
+ yield unless s && s.size > 0
124
189
  end
125
190
 
126
191
  def get_locale
@@ -245,6 +310,10 @@ module Sidekiq
245
310
  "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
246
311
  end
247
312
 
313
+ def csp_nonce
314
+ env[:csp_nonce]
315
+ end
316
+
248
317
  def to_display(arg)
249
318
  arg.inspect
250
319
  rescue
@@ -278,27 +347,17 @@ module Sidekiq
278
347
  elsif rss_kb < 10_000_000
279
348
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
280
349
  else
281
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
350
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
282
351
  end
283
352
  end
284
353
 
285
- def number_with_delimiter(number)
286
- return "" if number.nil?
287
-
288
- begin
289
- Float(number)
290
- rescue ArgumentError, TypeError
291
- return number
292
- end
293
-
294
- options = {delimiter: ",", separator: "."}
295
- parts = number.to_s.to_str.split(".")
296
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
297
- parts.join(options[:separator])
354
+ def number_with_delimiter(number, options = {})
355
+ precision = options[:precision] || 0
356
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
298
357
  end
299
358
 
300
359
  def h(text)
301
- ::Rack::Utils.escape_html(text)
360
+ ::Rack::Utils.escape_html(text.to_s)
302
361
  rescue ArgumentError => e
303
362
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
304
363
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -332,7 +391,8 @@ module Sidekiq
332
391
  end
333
392
 
334
393
  def pollable?
335
- !(current_path == "" || current_path.start_with?("metrics"))
394
+ # there's no point to refreshing the metrics pages every N seconds
395
+ !(current_path == "" || current_path.index("metrics"))
336
396
  end
337
397
 
338
398
  def retry_or_delete_or_kill(job, params)
@@ -39,10 +39,13 @@ module Sidekiq
39
39
  route(DELETE, path, &block)
40
40
  end
41
41
 
42
- def route(method, path, &block)
42
+ def route(*methods, path, &block)
43
43
  @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
44
 
45
- @routes[method] << WebRoute.new(method, path, block)
45
+ methods.each do |method|
46
+ method = method.to_s.upcase
47
+ @routes[method] << WebRoute.new(method, path, block)
48
+ end
46
49
  end
47
50
 
48
51
  def match(env)
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,7 +35,26 @@ 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
+ X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"
44
+ else
45
+ CONTENT_LANGUAGE = "content-language"
46
+ CONTENT_SECURITY_POLICY = "content-security-policy"
47
+ LOCATION = "location"
48
+ X_CASCADE = "x-cascade"
49
+ X_CONTENT_TYPE_OPTIONS = "x-content-type-options"
50
+ end
51
+
37
52
  class << self
53
+ # Forward compatibility with 8.0
54
+ def configure
55
+ yield self
56
+ end
57
+
38
58
  def settings
39
59
  self
40
60
  end
@@ -102,6 +122,7 @@ module Sidekiq
102
122
  end
103
123
 
104
124
  def call(env)
125
+ env[:csp_nonce] = SecureRandom.base64(16)
105
126
  app.call(env)
106
127
  end
107
128
 
@@ -126,7 +147,50 @@ module Sidekiq
126
147
  send(:"#{attribute}=", value)
127
148
  end
128
149
 
129
- def self.register(extension)
150
+ # Register a class as a Sidekiq Web UI extension. The class should
151
+ # provide one or more tabs which map to an index route. Options:
152
+ #
153
+ # @param extension [Class] Class which contains the HTTP actions, required
154
+ # @param name [String] the name of the extension, used to namespace assets
155
+ # @param tab [String | Array] labels(s) of the UI tabs
156
+ # @param index [String | Array] index route(s) for each tab
157
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
158
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
159
+ # @param cache_for [Integer] amount of time to cache assets, default one day
160
+ #
161
+ # TODO name, tab and index will be mandatory in 8.0
162
+ #
163
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
164
+ # and `views/` subdirectories.
165
+ def self.register(extension, name: nil, tab: nil, index: nil, root_dir: nil, cache_for: 86400, asset_paths: nil)
166
+ tab = Array(tab)
167
+ index = Array(index)
168
+ tab.zip(index).each do |tab, index|
169
+ tabs[tab] = index
170
+ end
171
+ if root_dir
172
+ locdir = File.join(root_dir, "locales")
173
+ locales << locdir if File.directory?(locdir)
174
+
175
+ if asset_paths && name
176
+ # if you have {root}/assets/{name}/js/scripts.js
177
+ # and {root}/assets/{name}/css/styles.css
178
+ # you would pass in:
179
+ # asset_paths: ["js", "css"]
180
+ # See script_tag and style_tag in web/helpers.rb
181
+ assdir = File.join(root_dir, "assets")
182
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
183
+ assetprops = {
184
+ urls: assurls,
185
+ root: assdir,
186
+ cascade: true
187
+ }
188
+ assetprops[:header_rules] = [[:all, {Rack::CACHE_CONTROL => "private, max-age=#{cache_for.to_i}"}]] if cache_for
189
+ middlewares << [[Rack::Static, assetprops], nil]
190
+ end
191
+ end
192
+
193
+ yield self if block_given?
130
194
  extension.registered(WebApplication)
131
195
  end
132
196
 
@@ -137,7 +201,7 @@ module Sidekiq
137
201
  m = middlewares
138
202
 
139
203
  rules = []
140
- rules = [[:all, {"cache-control" => "public, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
204
+ rules = [[:all, {Rack::CACHE_CONTROL => "private, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
141
205
 
142
206
  ::Rack::Builder.new do
143
207
  use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
@@ -154,7 +218,7 @@ module Sidekiq
154
218
  Sidekiq::WebApplication.helpers WebHelpers
155
219
  Sidekiq::WebApplication.helpers Sidekiq::Paginator
156
220
 
157
- Sidekiq::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
221
+ Sidekiq::WebAction.class_eval <<-RUBY, Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
158
222
  def _render
159
223
  #{ERB.new(File.read(Web::LAYOUT)).src}
160
224
  end
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
 
@@ -101,18 +102,19 @@ module Sidekiq
101
102
  def self.freeze!
102
103
  @frozen = true
103
104
  @config_blocks = nil
105
+ default_configuration.freeze!
104
106
  end
105
107
 
106
108
  # Creates a Sidekiq::Config instance that is more tuned for embedding
107
109
  # within an arbitrary Ruby process. Notably it reduces concurrency by
108
110
  # default so there is less contention for CPU time with other threads.
109
111
  #
110
- # inst = Sidekiq.configure_embed do |config|
112
+ # instance = Sidekiq.configure_embed do |config|
111
113
  # config.queues = %w[critical default low]
112
114
  # end
113
- # inst.run
115
+ # instance.run
114
116
  # sleep 10
115
- # inst.terminate
117
+ # instance.stop
116
118
  #
117
119
  # NB: it is really easy to overload a Ruby process with threads due to the GIL.
118
120
  # I do not recommend setting concurrency higher than 2-3.
data/sidekiq.gemspec CHANGED
@@ -23,17 +23,9 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.add_dependency "redis-client", ">= 0.11.0"
27
- gem.add_dependency "connection_pool", ">= 2.3.0"
28
- gem.add_dependency "rack", ">= 2.2.4"
29
- gem.add_dependency "concurrent-ruby", "< 2"
30
- gem.post_install_message = <<~EOM
31
-
32
- Welcome to Sidekiq 7.0!
33
-
34
- 1. Use `gem 'sidekiq', '<7'` in your Gemfile if you don't want this new version.
35
- 2. Read the release notes at https://github.com/sidekiq/sidekiq/blob/main/docs/7.0-Upgrade.md
36
- 3. If you have problems, search for open/closed issues at https://github.com/sidekiq/sidekiq/issues/
37
-
38
- EOM
26
+ gem.add_dependency "redis-client", ">= 0.23.0", "<1"
27
+ gem.add_dependency "connection_pool", ">= 2.3.0", "<3"
28
+ gem.add_dependency "rack", ">= 2.2.4", "<3.3"
29
+ gem.add_dependency "logger"
30
+ gem.add_dependency "base64"
39
31
  end
@@ -33,6 +33,8 @@ function addListeners() {
33
33
 
34
34
  addShiftClickListeners()
35
35
  updateFuzzyTimes();
36
+ updateNumbers();
37
+ updateProgressBars();
36
38
  setLivePollFromUrl();
37
39
 
38
40
  var buttons = document.querySelectorAll(".live-poll");
@@ -46,6 +48,8 @@ function addListeners() {
46
48
  scheduleLivePoll();
47
49
  }
48
50
  }
51
+
52
+ document.getElementById("locale-select").addEventListener("change", updateLocale);
49
53
  }
50
54
 
51
55
  function addPollingListeners(_event) {
@@ -102,6 +106,20 @@ function updateFuzzyTimes() {
102
106
  t.cancel();
103
107
  }
104
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
+
105
123
  function setLivePollFromUrl() {
106
124
  var url_params = new URL(window.location.href).searchParams
107
125
 
@@ -140,6 +158,7 @@ function checkResponse(resp) {
140
158
 
141
159
  function scheduleLivePoll() {
142
160
  let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000;
161
+ if (ti < 2000) { ti = 2000 }
143
162
  livePollTimer = setTimeout(livePollCallback, ti);
144
163
  }
145
164
 
@@ -159,3 +178,11 @@ function replacePage(text) {
159
178
  function showError(error) {
160
179
  console.error(error)
161
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
  }
@@ -81,9 +83,12 @@ class RealtimeChart extends DashboardChart {
81
83
  this.chart.data.datasets[1].data.push(failed);
82
84
  this.chart.update();
83
85
 
86
+ updateScreenReaderDashboardValues(processed, failed);
87
+
84
88
  updateStatsSummary(this.stats.sidekiq);
85
89
  updateRedisStats(this.stats.redis);
86
90
  updateFooterUTCTime(this.stats.server_utc_time);
91
+ updateNumbers();
87
92
  pulseBeacon();
88
93
 
89
94
  this.stats = stats;
@@ -105,17 +110,27 @@ class RealtimeChart extends DashboardChart {
105
110
  }
106
111
 
107
112
  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
- `;
113
+ const entry1 = this.legendEntry(dp[0]);
114
+ const entry2 = this.legendEntry(dp[1]);
115
+ const time = document.createElement("span");
116
+ time.classList.add("time");
117
+ time.innerText = dp[0].label;
118
+
119
+ this.legend.replaceChildren(entry1, entry2, time)
120
+ }
121
+
122
+ legendEntry(dp) {
123
+ const wrapper = document.createElement("span");
124
+
125
+ const swatch = document.createElement("span");
126
+ swatch.classList.add("swatch");
127
+ swatch.style.backgroundColor = dp.dataset.borderColor;
128
+ wrapper.appendChild(swatch)
129
+
130
+ const label = document.createElement("span");
131
+ label.innerText = `${dp.dataset.label}: ${dp.formattedValue}`;
132
+ wrapper.appendChild(label)
133
+ return wrapper;
119
134
  }
120
135
 
121
136
  renderCursor(dp) {
@@ -164,3 +179,16 @@ class RealtimeChart extends DashboardChart {
164
179
  };
165
180
  }
166
181
  }
182
+
183
+ var rc = document.getElementById("realtime-chart")
184
+ if (rc != null) {
185
+ var rtc = new RealtimeChart(rc, JSON.parse(rc.textContent))
186
+ rtc.registerLegend(document.getElementById("realtime-legend"))
187
+ window.realtimeChart = rtc
188
+ }
189
+
190
+ var hc = document.getElementById("history-chart")
191
+ if (hc != null) {
192
+ var htc = new DashboardChart(hc, JSON.parse(hc.textContent))
193
+ window.historyChart = htc
194
+ }
@@ -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) => {
@@ -38,6 +36,12 @@ var ready = (callback) => {
38
36
  else document.addEventListener("DOMContentLoaded", callback);
39
37
  }
40
38
 
39
+ var updateScreenReaderDashboardValues = function(processed, failed) {
40
+ let lastDashboardUpdateSpan = document.getElementById("sr-last-dashboard-update");
41
+ var updateText = document.getElementById("sr-last-dashboard-update-template").innerText;
42
+ lastDashboardUpdateSpan.innerText = updateText.replace("PROCESSED_COUNT", processed).replace("FAILED_COUNT", failed);
43
+ }
44
+
41
45
  ready(() => {
42
46
  var sldr = document.getElementById('sldr');
43
47
  if (typeof localStorage.sidekiqTimeInterval !== 'undefined') {
@@ -262,3 +262,37 @@ class HistBubbleChart extends BaseChart {
262
262
  };
263
263
  }
264
264
  }
265
+
266
+ var ch = document.getElementById("job-metrics-overview-chart");
267
+ if (ch != null) {
268
+ var jm = new JobMetricsOverviewChart(ch, JSON.parse(ch.textContent));
269
+ document.querySelectorAll(".metrics-swatch-wrapper > input[type=checkbox]").forEach((imp) => {
270
+ jm.registerSwatch(imp.id)
271
+ });
272
+ window.jobMetricsChart = jm;
273
+ }
274
+
275
+ var htc = document.getElementById("hist-totals-chart");
276
+ if (htc != null) {
277
+ var tc = new HistTotalsChart(htc, JSON.parse(htc.textContent));
278
+ window.histTotalsChart = tc
279
+ }
280
+
281
+ var hbc = document.getElementById("hist-bubble-chart");
282
+ if (hbc != null) {
283
+ var bc = new HistBubbleChart(hbc, JSON.parse(hbc.textContent));
284
+ window.histBubbleChart = bc
285
+ }
286
+
287
+ var form = document.getElementById("metrics-form")
288
+ document.querySelectorAll("#period-selector").forEach(node => {
289
+ node.addEventListener("input", debounce(() => form.submit()))
290
+ })
291
+
292
+ function debounce(func, timeout = 300) {
293
+ let timer;
294
+ return (...args) => {
295
+ clearTimeout(timer);
296
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
297
+ };
298
+ }
@@ -151,3 +151,13 @@ div.interval-slider {
151
151
  padding-left: 5px;
152
152
  }
153
153
  }
154
+
155
+ #locale-select {
156
+ float: right;
157
+ }
158
+
159
+ @media (max-width: 767px) {
160
+ #locale-select {
161
+ float: none;
162
+ }
163
+ }