sidekiq 7.1.6 → 7.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +68 -0
  3. data/README.md +2 -2
  4. data/bin/multi_queue_bench +271 -0
  5. data/lib/sidekiq/api.rb +77 -11
  6. data/lib/sidekiq/cli.rb +3 -1
  7. data/lib/sidekiq/config.rb +4 -4
  8. data/lib/sidekiq/deploy.rb +2 -2
  9. data/lib/sidekiq/job.rb +1 -1
  10. data/lib/sidekiq/job_retry.rb +3 -3
  11. data/lib/sidekiq/launcher.rb +6 -4
  12. data/lib/sidekiq/logger.rb +1 -1
  13. data/lib/sidekiq/metrics/query.rb +4 -1
  14. data/lib/sidekiq/metrics/tracking.rb +7 -3
  15. data/lib/sidekiq/middleware/current_attributes.rb +1 -1
  16. data/lib/sidekiq/paginator.rb +2 -2
  17. data/lib/sidekiq/processor.rb +1 -1
  18. data/lib/sidekiq/rails.rb +7 -3
  19. data/lib/sidekiq/redis_client_adapter.rb +16 -0
  20. data/lib/sidekiq/redis_connection.rb +3 -6
  21. data/lib/sidekiq/scheduled.rb +2 -2
  22. data/lib/sidekiq/testing.rb +9 -3
  23. data/lib/sidekiq/transaction_aware_client.rb +7 -0
  24. data/lib/sidekiq/version.rb +1 -1
  25. data/lib/sidekiq/web/action.rb +5 -0
  26. data/lib/sidekiq/web/application.rb +32 -4
  27. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  28. data/lib/sidekiq/web/helpers.rb +14 -15
  29. data/sidekiq.gemspec +1 -1
  30. data/web/assets/javascripts/application.js +21 -0
  31. data/web/assets/javascripts/dashboard-charts.js +14 -0
  32. data/web/assets/javascripts/dashboard.js +7 -9
  33. data/web/assets/javascripts/metrics.js +34 -0
  34. data/web/assets/stylesheets/application-rtl.css +10 -0
  35. data/web/assets/stylesheets/application.css +22 -0
  36. data/web/views/_footer.erb +13 -1
  37. data/web/views/_metrics_period_select.erb +1 -1
  38. data/web/views/_summary.erb +7 -7
  39. data/web/views/busy.erb +7 -7
  40. data/web/views/dashboard.erb +23 -33
  41. data/web/views/filtering.erb +4 -4
  42. data/web/views/metrics.erb +36 -27
  43. data/web/views/metrics_for_job.erb +26 -35
  44. data/web/views/queues.erb +6 -2
  45. metadata +5 -4
@@ -20,7 +20,8 @@ module Sidekiq
20
20
  end
21
21
 
22
22
  # Get metric data for all jobs from the last hour
23
- def top_jobs(minutes: 60)
23
+ # +class_filter+: return only results for classes matching filter
24
+ def top_jobs(class_filter: nil, minutes: 60)
24
25
  result = Result.new
25
26
 
26
27
  time = @time
@@ -39,6 +40,7 @@ module Sidekiq
39
40
  redis_results.each do |hash|
40
41
  hash.each do |k, v|
41
42
  kls, metric = k.split("|")
43
+ next if class_filter && !class_filter.match?(kls)
42
44
  result.job_results[kls].add_metric metric, time, v.to_i
43
45
  end
44
46
  time -= 60
@@ -117,6 +119,7 @@ module Sidekiq
117
119
 
118
120
  def total_avg(metric = "ms")
119
121
  completed = totals["p"] - totals["f"]
122
+ return 0 if completed.zero?
120
123
  totals[metric].to_f / completed
121
124
  end
122
125
 
@@ -103,12 +103,16 @@ module Sidekiq
103
103
  def reset
104
104
  @lock.synchronize {
105
105
  array = [@totals, @jobs, @grams]
106
- @totals = Hash.new(0)
107
- @jobs = Hash.new(0)
108
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
106
+ reset_instance_variables
109
107
  array
110
108
  }
111
109
  end
110
+
111
+ def reset_instance_variables
112
+ @totals = Hash.new(0)
113
+ @jobs = Hash.new(0)
114
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
115
+ end
112
116
  end
113
117
 
114
118
  class Middleware
@@ -54,7 +54,7 @@ module Sidekiq
54
54
  cattrs_to_reset << constklass
55
55
 
56
56
  job[key].each do |(attribute, value)|
57
- constklass.public_send("#{attribute}=", value)
57
+ constklass.public_send(:"#{attribute}=", value)
58
58
  end
59
59
  end
60
60
  end
@@ -19,9 +19,9 @@ module Sidekiq
19
19
  total_size, items = conn.multi { |transaction|
20
20
  transaction.zcard(key)
21
21
  if rev
22
- transaction.zrange(key, starting, ending, "REV", withscores: true)
22
+ transaction.zrange(key, starting, ending, "REV", "withscores")
23
23
  else
24
- transaction.zrange(key, starting, ending, withscores: true)
24
+ transaction.zrange(key, starting, ending, "withscores")
25
25
  end
26
26
  }
27
27
  [current_page, total_size, items]
@@ -187,7 +187,7 @@ module Sidekiq
187
187
  # we didn't properly finish it.
188
188
  rescue Sidekiq::JobRetry::Handled => h
189
189
  # this is the common case: job raised error and Sidekiq::JobRetry::Handled
190
- # signals that we created a retry successfully. We can acknowlege the job.
190
+ # signals that we created a retry successfully. We can acknowledge the job.
191
191
  ack = true
192
192
  e = h.cause || h
193
193
  handle_exception(e, {context: "Job raised exception", job: job_hash})
data/lib/sidekiq/rails.rb CHANGED
@@ -20,6 +20,10 @@ module Sidekiq
20
20
  def inspect
21
21
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
22
22
  end
23
+
24
+ def to_hash
25
+ {app: @app.class.name}
26
+ end
23
27
  end
24
28
 
25
29
  # By including the Options module, we allow AJs to directly control sidekiq features
@@ -56,10 +60,10 @@ module Sidekiq
56
60
  # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
57
61
  # it will appear in the Sidekiq console with all of the job context.
58
62
  unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
59
- if ::Rails::VERSION::STRING < "7.1"
60
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
61
- else
63
+ if ::Rails.logger.respond_to?(:broadcast_to)
62
64
  ::Rails.logger.broadcast_to(config.logger)
65
+ else
66
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
63
67
  end
64
68
  end
65
69
  end
@@ -21,6 +21,22 @@ module Sidekiq
21
21
  @client.call("EVALSHA", sha, keys.size, *keys, *argv)
22
22
  end
23
23
 
24
+ # this is the set of Redis commands used by Sidekiq. Not guaranteed
25
+ # to be comprehensive, we use this as a performance enhancement to
26
+ # avoid calling method_missing on most commands
27
+ USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
28
+ get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
29
+ lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
30
+ publish rpop rpush sadd scard script set sismember smembers
31
+ srem ttl type unlink zadd zcard zincrby zrange zrem
32
+ zremrangebyrank zremrangebyscore]
33
+
34
+ USED_COMMANDS.each do |name|
35
+ define_method(name) do |*args, **kwargs|
36
+ @client.call(name, *args, **kwargs)
37
+ end
38
+ end
39
+
24
40
  private
25
41
 
26
42
  # this allows us to use methods like `conn.hmset(...)` instead of having to use
@@ -39,9 +39,8 @@ module Sidekiq
39
39
  uri.password = redacted
40
40
  scrubbed_options[:url] = uri.to_s
41
41
  end
42
- if scrubbed_options[:password]
43
- scrubbed_options[:password] = redacted
44
- end
42
+ scrubbed_options[:password] = redacted if scrubbed_options[:password]
43
+ scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
45
44
  scrubbed_options[:sentinels]&.each do |sentinel|
46
45
  sentinel[:password] = redacted if sentinel[:password]
47
46
  end
@@ -67,9 +66,7 @@ module Sidekiq
67
66
  EOM
68
67
  end
69
68
 
70
- ENV[
71
- p || "REDIS_URL"
72
- ]
69
+ ENV[p.to_s] || ENV["REDIS_URL"]
73
70
  end
74
71
  end
75
72
  end
@@ -144,7 +144,7 @@ module Sidekiq
144
144
  # In the example above, each process should schedule every 10 seconds on average. We special
145
145
  # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
146
146
  # As we run more processes, the scheduling interval average will approach an even spread
147
- # between 0 and poll interval so we don't need this artifical boost.
147
+ # between 0 and poll interval so we don't need this artificial boost.
148
148
  #
149
149
  count = process_count
150
150
  interval = poll_interval_average(count)
@@ -193,7 +193,7 @@ module Sidekiq
193
193
  # should never depend on sidekiq/api.
194
194
  def cleanup
195
195
  # dont run cleanup more than once per minute
196
- return 0 unless redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
196
+ return 0 unless redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
197
197
 
198
198
  count = 0
199
199
  redis do |conn|
@@ -5,6 +5,7 @@ require "sidekiq"
5
5
 
6
6
  module Sidekiq
7
7
  class Testing
8
+ class TestModeAlreadySetError < RuntimeError; end
8
9
  class << self
9
10
  attr_accessor :__global_test_mode
10
11
 
@@ -12,8 +13,13 @@ module Sidekiq
12
13
  # all threads. Calling with a block only affects the current Thread.
13
14
  def __set_test_mode(mode)
14
15
  if block_given?
16
+ # Reentrant testing modes will lead to a rat's nest of code which is
17
+ # hard to reason about. You can set the testing mode once globally and
18
+ # you can override that global setting once per-thread.
19
+ raise TestModeAlreadySetError, "Nesting test modes is not supported" if __local_test_mode
20
+
21
+ self.__local_test_mode = mode
15
22
  begin
16
- self.__local_test_mode = mode
17
23
  yield
18
24
  ensure
19
25
  self.__local_test_mode = nil
@@ -106,7 +112,7 @@ module Sidekiq
106
112
  # The Queues class is only for testing the fake queue implementation.
107
113
  # There are 2 data structures involved in tandem. This is due to the
108
114
  # Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
109
- # to the array. Because the array was dervied from a filter of the total
115
+ # to the array. Because the array was derived from a filter of the total
110
116
  # jobs enqueued, it appeared as though the array didn't change.
111
117
  #
112
118
  # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
@@ -272,7 +278,7 @@ module Sidekiq
272
278
  def perform_one
273
279
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
274
280
  next_job = jobs.first
275
- Queues.delete_for(next_job["jid"], queue, to_s)
281
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
276
282
  process_job(next_job)
277
283
  end
278
284
 
@@ -9,7 +9,14 @@ module Sidekiq
9
9
  @redis_client = Client.new(pool: pool, config: config)
10
10
  end
11
11
 
12
+ def batching?
13
+ Thread.current[:sidekiq_batch]
14
+ end
15
+
12
16
  def push(item)
17
+ # 6160 we can't support both Sidekiq::Batch and transactions.
18
+ return @redis_client.push(item) if batching?
19
+
13
20
  # pre-allocate the JID so we can return it immediately and
14
21
  # save it to the database as part of the transaction.
15
22
  item["jid"] ||= SecureRandom.hex(12)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.1.6"
4
+ VERSION = "7.2.4"
5
5
  MAJOR = 7
6
6
  end
@@ -22,6 +22,11 @@ module Sidekiq
22
22
  throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
23
  end
24
24
 
25
+ def reload_page
26
+ current_location = request.referer.gsub(request.base_url, "")
27
+ redirect current_location
28
+ end
29
+
25
30
  def params
26
31
  indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
27
32
 
@@ -15,7 +15,7 @@ module Sidekiq
15
15
  "manifest-src 'self'",
16
16
  "media-src 'self'",
17
17
  "object-src 'none'",
18
- "script-src 'self' https: http: 'unsafe-inline'",
18
+ "script-src 'self' https: http:",
19
19
  "style-src 'self' https: http: 'unsafe-inline'",
20
20
  "worker-src 'self'",
21
21
  "base-uri 'self'"
@@ -49,9 +49,9 @@ module Sidekiq
49
49
 
50
50
  head "/" do
51
51
  # HEAD / is the cheapest heartbeat possible,
52
- # it hits Redis to ensure connectivity
53
- Sidekiq.redis { |c| c.llen("queue:default") }
54
- ""
52
+ # it hits Redis to ensure connectivity and returns
53
+ # the size of the default queue
54
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
55
55
  end
56
56
 
57
57
  get "/" do
@@ -330,6 +330,22 @@ module Sidekiq
330
330
 
331
331
  ########
332
332
  # Filtering
333
+
334
+ get "/filter/metrics" do
335
+ redirect "#{root_path}metrics"
336
+ end
337
+
338
+ post "/filter/metrics" do
339
+ x = params[:substr]
340
+ q = Sidekiq::Metrics::Query.new
341
+ @period = h((params[:period] || "")[0..1])
342
+ @periods = METRICS_PERIODS
343
+ minutes = @periods.fetch(@period, @periods.values.first)
344
+ @query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
345
+
346
+ erb :metrics
347
+ end
348
+
333
349
  get "/filter/retries" do
334
350
  x = params[:substr]
335
351
  return redirect "#{root_path}retries" unless x && x != ""
@@ -378,6 +394,18 @@ module Sidekiq
378
394
  erb :morgue
379
395
  end
380
396
 
397
+ post "/change_locale" do
398
+ locale = params["locale"]
399
+
400
+ match = available_locales.find { |available|
401
+ locale == available
402
+ }
403
+
404
+ session[:locale] = match if match
405
+
406
+ reload_page
407
+ end
408
+
381
409
  def call(env)
382
410
  action = self.class.match(env)
383
411
  return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
@@ -27,7 +27,6 @@
27
27
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
 
29
29
  require "securerandom"
30
- require "base64"
31
30
  require "rack/request"
32
31
 
33
32
  module Sidekiq
@@ -57,7 +56,7 @@ module Sidekiq
57
56
  end
58
57
 
59
58
  def logger(env)
60
- @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
61
60
  end
62
61
 
63
62
  def deny(env)
@@ -116,7 +115,7 @@ module Sidekiq
116
115
  sess = session(env)
117
116
  localtoken = sess[:csrf]
118
117
 
119
- # Checks that Rack::Session::Cookie actualy contains the csrf toekn
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
120
119
  return false if localtoken.nil?
121
120
 
122
121
  # Rotate the session token after every use
@@ -143,7 +142,7 @@ module Sidekiq
143
142
  one_time_pad = SecureRandom.random_bytes(token.length)
144
143
  encrypted_token = xor_byte_strings(one_time_pad, token)
145
144
  masked_token = one_time_pad + encrypted_token
146
- Base64.urlsafe_encode64(masked_token)
145
+ encode_token(masked_token)
147
146
  end
148
147
 
149
148
  # Essentially the inverse of +mask_token+.
@@ -168,8 +167,12 @@ module Sidekiq
168
167
  ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
168
  end
170
169
 
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
171
174
  def decode_token(token)
172
- Base64.urlsafe_decode64(token)
175
+ token.tr("-_", "+/").unpack1("m0")
173
176
  end
174
177
 
175
178
  def xor_byte_strings(s1, s2)
@@ -21,6 +21,10 @@ module Sidekiq
21
21
  end
22
22
  end
23
23
 
24
+ def to_json(x)
25
+ Sidekiq.dump_json(x)
26
+ end
27
+
24
28
  def singularize(str, count)
25
29
  if count == 1 && str.respond_to?(:singularize) # rails
26
30
  str.singularize
@@ -117,6 +121,10 @@ module Sidekiq
117
121
  #
118
122
  # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
119
123
  def locale
124
+ # session[:locale] is set via the locale selector from the footer
125
+ # defined?(session) && session are used to avoid exceptions when running tests
126
+ return session[:locale] if defined?(session) && session&.[](:locale)
127
+
120
128
  @locale ||= begin
121
129
  matched_locale = user_preferred_languages.map { |preferred|
122
130
  preferred_language = preferred.split("-", 2).first
@@ -292,23 +300,13 @@ module Sidekiq
292
300
  elsif rss_kb < 10_000_000
293
301
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
294
302
  else
295
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
303
+ "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
296
304
  end
297
305
  end
298
306
 
299
- def number_with_delimiter(number)
300
- return "" if number.nil?
301
-
302
- begin
303
- Float(number)
304
- rescue ArgumentError, TypeError
305
- return number
306
- end
307
-
308
- options = {delimiter: ",", separator: "."}
309
- parts = number.to_s.to_str.split(".")
310
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
311
- parts.join(options[:separator])
307
+ def number_with_delimiter(number, options = {})
308
+ precision = options[:precision] || 0
309
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
312
310
  end
313
311
 
314
312
  def h(text)
@@ -346,7 +344,8 @@ module Sidekiq
346
344
  end
347
345
 
348
346
  def pollable?
349
- !(current_path == "" || current_path.start_with?("metrics"))
347
+ # there's no point to refreshing the metrics pages every N seconds
348
+ !(current_path == "" || current_path.index("metrics"))
350
349
  end
351
350
 
352
351
  def retry_or_delete_or_kill(job, params)
data/sidekiq.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.add_dependency "redis-client", ">= 0.14.0"
26
+ gem.add_dependency "redis-client", ">= 0.19.0"
27
27
  gem.add_dependency "connection_pool", ">= 2.3.0"
28
28
  gem.add_dependency "rack", ">= 2.2.4"
29
29
  gem.add_dependency "concurrent-ruby", "< 2"
@@ -33,6 +33,7 @@ function addListeners() {
33
33
 
34
34
  addShiftClickListeners()
35
35
  updateFuzzyTimes();
36
+ updateNumbers();
36
37
  setLivePollFromUrl();
37
38
 
38
39
  var buttons = document.querySelectorAll(".live-poll");
@@ -46,6 +47,8 @@ function addListeners() {
46
47
  scheduleLivePoll();
47
48
  }
48
49
  }
50
+
51
+ document.getElementById("locale-select").addEventListener("change", updateLocale);
49
52
  }
50
53
 
51
54
  function addPollingListeners(_event) {
@@ -102,6 +105,20 @@ function updateFuzzyTimes() {
102
105
  t.cancel();
103
106
  }
104
107
 
108
+ function updateNumbers() {
109
+ document.querySelectorAll("[data-nwp]").forEach(node => {
110
+ let number = parseFloat(node.textContent);
111
+ let precision = parseInt(node.dataset["nwp"] || 0);
112
+ if (typeof number === "number") {
113
+ let formatted = number.toLocaleString(undefined, {
114
+ minimumFractionDigits: precision,
115
+ maximumFractionDigits: precision,
116
+ });
117
+ node.textContent = formatted;
118
+ }
119
+ });
120
+ }
121
+
105
122
  function setLivePollFromUrl() {
106
123
  var url_params = new URL(window.location.href).searchParams
107
124
 
@@ -160,3 +177,7 @@ function replacePage(text) {
160
177
  function showError(error) {
161
178
  console.error(error)
162
179
  }
180
+
181
+ function updateLocale(event) {
182
+ event.target.form.submit();
183
+ };
@@ -86,6 +86,7 @@ class RealtimeChart extends DashboardChart {
86
86
  updateStatsSummary(this.stats.sidekiq);
87
87
  updateRedisStats(this.stats.redis);
88
88
  updateFooterUTCTime(this.stats.server_utc_time);
89
+ updateNumbers();
89
90
  pulseBeacon();
90
91
 
91
92
  this.stats = stats;
@@ -166,3 +167,16 @@ class RealtimeChart extends DashboardChart {
166
167
  };
167
168
  }
168
169
  }
170
+
171
+ var rc = document.getElementById("realtime-chart")
172
+ if (rc != null) {
173
+ var rtc = new RealtimeChart(rc, JSON.parse(rc.textContent))
174
+ rtc.registerLegend(document.getElementById("realtime-legend"))
175
+ window.realtimeChart = rtc
176
+ }
177
+
178
+ var hc = document.getElementById("history-chart")
179
+ if (hc != null) {
180
+ var htc = new DashboardChart(hc, JSON.parse(hc.textContent))
181
+ window.historyChart = htc
182
+ }
@@ -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) {
@@ -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
+ }
@@ -370,6 +370,15 @@ img.smallogo {
370
370
  .stat p {
371
371
  font-size: 0.9em;
372
372
  }
373
+
374
+ .num {
375
+ font-family: monospace;
376
+ }
377
+
378
+ td.num {
379
+ text-align: right;
380
+ }
381
+
373
382
  @media (max-width: 767px) {
374
383
  .stats-container {
375
384
  display: block;
@@ -722,3 +731,16 @@ div.interval-slider input {
722
731
  canvas {
723
732
  margin: 20px 0 30px;
724
733
  }
734
+
735
+ #locale-select {
736
+ float: left;
737
+ margin: 8px 15px;
738
+ }
739
+
740
+ @media (max-width: 767px) {
741
+ #locale-select {
742
+ float: none;
743
+ width: auto;
744
+ margin: 15px auto;
745
+ }
746
+ }
@@ -15,7 +15,19 @@
15
15
  <p class="navbar-text"><a rel=help href="https://github.com/sidekiq/sidekiq/wiki">docs</a></p>
16
16
  </li>
17
17
  <li>
18
- <p class="navbar-text"><a rel=external href="https://github.com/sidekiq/sidekiq/tree/main/web/locales"><%= locale %></a></p>
18
+ <form id="locale-form" class="form-inline" action="<%= root_path %>change_locale" method="post">
19
+ <%= csrf_tag %>
20
+ <label class="sr-only" for="locale">Language</label>
21
+ <select id="locale-select" class="form-control" name="locale">
22
+ <% available_locales.each do |locale_option| %>
23
+ <% if locale_option == locale %>
24
+ <option selected value="<%= locale_option %>"><%= locale_option %></option>
25
+ <% else %>
26
+ <option value="<%= locale_option %>"><%= locale_option %></option>
27
+ <% end %>
28
+ <% end %>
29
+ </select>
30
+ </form>
19
31
  </li>
20
32
  </ul>
21
33
  </div>
@@ -1,5 +1,5 @@
1
1
  <div>
2
- <select class="form-control" onchange="window.location.href = '<%= path %>?period=' + event.target.value">
2
+ <select class="form-control" data-metric-period="<%= path %>">
3
3
  <% periods.each_key do |code| %>
4
4
 
5
5
  <% if code == period %>