sidekiq 8.0.2 → 8.1.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +108 -0
  3. data/README.md +15 -0
  4. data/bin/lint-herb +13 -0
  5. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +104 -58
  6. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  7. data/lib/sidekiq/api.rb +30 -6
  8. data/lib/sidekiq/capsule.rb +4 -0
  9. data/lib/sidekiq/cli.rb +16 -4
  10. data/lib/sidekiq/client.rb +15 -1
  11. data/lib/sidekiq/component.rb +2 -1
  12. data/lib/sidekiq/config.rb +11 -6
  13. data/lib/sidekiq/fetch.rb +1 -0
  14. data/lib/sidekiq/job/iterable.rb +33 -14
  15. data/lib/sidekiq/job.rb +4 -2
  16. data/lib/sidekiq/job_logger.rb +5 -3
  17. data/lib/sidekiq/job_retry.rb +23 -8
  18. data/lib/sidekiq/launcher.rb +18 -9
  19. data/lib/sidekiq/loader.rb +57 -0
  20. data/lib/sidekiq/logger.rb +16 -9
  21. data/lib/sidekiq/metrics/tracking.rb +3 -0
  22. data/lib/sidekiq/middleware/current_attributes.rb +6 -2
  23. data/lib/sidekiq/middleware/i18n.rb +2 -0
  24. data/lib/sidekiq/monitor.rb +4 -8
  25. data/lib/sidekiq/profiler.rb +17 -3
  26. data/lib/sidekiq/rails.rb +46 -67
  27. data/lib/sidekiq/redis_connection.rb +2 -2
  28. data/lib/sidekiq/ring_buffer.rb +1 -0
  29. data/lib/sidekiq/scheduled.rb +7 -5
  30. data/lib/sidekiq/testing.rb +1 -1
  31. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  32. data/lib/sidekiq/version.rb +1 -1
  33. data/lib/sidekiq/web/action.rb +45 -2
  34. data/lib/sidekiq/web/application.rb +22 -4
  35. data/lib/sidekiq/web/config.rb +3 -3
  36. data/lib/sidekiq/web/helpers.rb +26 -29
  37. data/lib/sidekiq/web.rb +23 -3
  38. data/lib/sidekiq.rb +5 -0
  39. data/sidekiq.gemspec +5 -5
  40. data/web/assets/images/logo.png +0 -0
  41. data/web/assets/images/status.png +0 -0
  42. data/web/assets/javascripts/application.js +36 -13
  43. data/web/assets/javascripts/dashboard.js +1 -1
  44. data/web/assets/stylesheets/style.css +30 -6
  45. data/web/locales/ar.yml +1 -0
  46. data/web/locales/cs.yml +1 -0
  47. data/web/locales/da.yml +1 -0
  48. data/web/locales/de.yml +1 -0
  49. data/web/locales/el.yml +1 -0
  50. data/web/locales/en.yml +1 -0
  51. data/web/locales/es.yml +1 -0
  52. data/web/locales/fa.yml +1 -0
  53. data/web/locales/fr.yml +2 -1
  54. data/web/locales/gd.yml +1 -0
  55. data/web/locales/he.yml +1 -0
  56. data/web/locales/hi.yml +1 -0
  57. data/web/locales/it.yml +8 -0
  58. data/web/locales/ja.yml +1 -0
  59. data/web/locales/ko.yml +1 -0
  60. data/web/locales/lt.yml +1 -0
  61. data/web/locales/nb.yml +1 -0
  62. data/web/locales/nl.yml +1 -0
  63. data/web/locales/pl.yml +1 -0
  64. data/web/locales/pt-BR.yml +1 -0
  65. data/web/locales/pt.yml +1 -0
  66. data/web/locales/ru.yml +1 -0
  67. data/web/locales/sv.yml +1 -0
  68. data/web/locales/ta.yml +1 -0
  69. data/web/locales/tr.yml +1 -0
  70. data/web/locales/uk.yml +6 -5
  71. data/web/locales/ur.yml +1 -0
  72. data/web/locales/vi.yml +1 -0
  73. data/web/locales/zh-CN.yml +1 -0
  74. data/web/locales/zh-TW.yml +1 -0
  75. data/web/views/{_footer.erb → _footer.html.erb} +1 -1
  76. data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +1 -1
  77. data/web/views/{_paging.erb → _paging.html.erb} +0 -1
  78. data/web/views/_poll_link.html.erb +4 -0
  79. data/web/views/{busy.erb → busy.html.erb} +4 -8
  80. data/web/views/{dashboard.erb → dashboard.html.erb} +3 -3
  81. data/web/views/{dead.erb → dead.html.erb} +3 -3
  82. data/web/views/filtering.html.erb +6 -0
  83. data/web/views/{layout.erb → layout.html.erb} +8 -7
  84. data/web/views/{metrics.erb → metrics.html.erb} +9 -8
  85. data/web/views/{morgue.erb → morgue.html.erb} +8 -4
  86. data/web/views/{queue.erb → queue.html.erb} +2 -2
  87. data/web/views/{queues.erb → queues.html.erb} +4 -4
  88. data/web/views/{retries.erb → retries.html.erb} +9 -5
  89. data/web/views/{retry.erb → retry.html.erb} +2 -2
  90. data/web/views/{scheduled.erb → scheduled.html.erb} +9 -5
  91. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +2 -2
  92. metadata +37 -36
  93. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  94. data/web/views/_poll_link.erb +0 -4
  95. data/web/views/filtering.erb +0 -6
  96. /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
  97. /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
  98. /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
  99. /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
  100. /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
@@ -72,6 +72,7 @@ module Sidekiq
72
72
  include Sidekiq::Component
73
73
 
74
74
  INITIAL_WAIT = 10
75
+ attr_accessor :rnd
75
76
 
76
77
  def initialize(config)
77
78
  @config = config
@@ -80,6 +81,7 @@ module Sidekiq
80
81
  @done = false
81
82
  @thread = nil
82
83
  @count_calls = 0
84
+ @rnd = Random.new
83
85
  end
84
86
 
85
87
  # Shut down this instance, will pause until the thread is dead.
@@ -115,9 +117,9 @@ module Sidekiq
115
117
  private
116
118
 
117
119
  def wait
118
- @sleeper.pop(random_poll_interval)
120
+ @sleeper.pop(timeout: random_poll_interval)
119
121
  rescue Timeout::Error
120
- # expected
122
+ # TODO move to exception: false
121
123
  rescue => ex
122
124
  # if poll_interval_average hasn't been calculated yet, we can
123
125
  # raise an error trying to reach Redis.
@@ -151,11 +153,11 @@ module Sidekiq
151
153
 
152
154
  if count < 10
153
155
  # For small clusters, calculate a random interval that is ±50% the desired average.
154
- interval * rand + interval.to_f / 2
156
+ interval * @rnd.rand + interval.to_f / 2
155
157
  else
156
158
  # With 10+ processes, we should have enough randomness to get decent polling
157
159
  # across the entire timespan
158
- interval * rand
160
+ interval * @rnd.rand * 2
159
161
  end
160
162
  end
161
163
 
@@ -223,7 +225,7 @@ module Sidekiq
223
225
  total += INITIAL_WAIT unless @config[:poll_interval_average]
224
226
  total += (5 * rand)
225
227
 
226
- @sleeper.pop(total)
228
+ @sleeper.pop(timeout: total)
227
229
  rescue Timeout::Error
228
230
  ensure
229
231
  # periodically clean out the `processes` set in Redis which can collect
@@ -83,7 +83,7 @@ module Sidekiq
83
83
  class EmptyQueueError < RuntimeError; end
84
84
 
85
85
  module TestingClient
86
- def atomic_push(conn, payloads)
86
+ private def atomic_push(conn, payloads)
87
87
  if Sidekiq::Testing.fake?
88
88
  payloads.each do |job|
89
89
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
@@ -7,6 +7,12 @@ module Sidekiq
7
7
  class TransactionAwareClient
8
8
  def initialize(pool: nil, config: nil)
9
9
  @redis_client = Client.new(pool: pool, config: config)
10
+ @transaction_backend =
11
+ if ActiveRecord.version >= Gem::Version.new("7.2")
12
+ ActiveRecord.method(:after_all_transactions_commit)
13
+ else
14
+ AfterCommitEverywhere.method(:after_commit)
15
+ end
10
16
  end
11
17
 
12
18
  def batching?
@@ -20,7 +26,7 @@ module Sidekiq
20
26
  # pre-allocate the JID so we can return it immediately and
21
27
  # save it to the database as part of the transaction.
22
28
  item["jid"] ||= SecureRandom.hex(12)
23
- AfterCommitEverywhere.after_commit { @redis_client.push(item) }
29
+ @transaction_backend.call { @redis_client.push(item) }
24
30
  item["jid"]
25
31
  end
26
32
 
@@ -38,10 +44,12 @@ end
38
44
  # Use `Sidekiq.transactional_push!` in your sidekiq.rb initializer
39
45
  module Sidekiq
40
46
  def self.transactional_push!
41
- begin
42
- require "after_commit_everywhere"
43
- rescue LoadError
44
- raise %q(You need to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
47
+ if ActiveRecord.version < Gem::Version.new("7.2")
48
+ begin
49
+ require "after_commit_everywhere"
50
+ rescue LoadError
51
+ raise %q(You need ActiveRecord >= 7.2 or to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
52
+ end
45
53
  end
46
54
 
47
55
  Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "8.0.2"
4
+ VERSION = "8.1.0"
5
5
  MAJOR = 8
6
6
 
7
7
  def self.gem_version
@@ -67,14 +67,57 @@ module Sidekiq
67
67
  end
68
68
 
69
69
  def session
70
- env["rack.session"]
70
+ env["rack.session"] || fail(<<~EOM)
71
+ Sidekiq::Web needs a valid Rack session. If this is a Rails app, make
72
+ sure you mount Sidekiq::Web *inside* your application routes:
73
+
74
+
75
+ Rails.application.routes.draw do
76
+ mount Sidekiq::Web => "/sidekiq"
77
+ ....
78
+ end
79
+
80
+
81
+ If this is a Rails app in API mode, you need to enable sessions.
82
+
83
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
84
+
85
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
86
+
87
+ # first, use IRB to create a shared secret key for sessions and commit it
88
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
89
+
90
+ # now use the secret with a session cookie middleware
91
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
92
+ run Sidekiq::Web
93
+
94
+ EOM
95
+ end
96
+
97
+ def logger
98
+ Sidekiq.logger
99
+ end
100
+
101
+ # flash { "Some message to show on redirect" }
102
+ def flash
103
+ msg = yield
104
+ logger.info msg
105
+ session[:skq_flash] = msg
106
+ end
107
+
108
+ def flash?
109
+ session&.[](:skq_flash)
110
+ end
111
+
112
+ def get_flash
113
+ @flash ||= session.delete(:skq_flash)
71
114
  end
72
115
 
73
116
  def erb(content, options = {})
74
117
  if content.is_a? Symbol
75
118
  unless respond_to?(:"_erb_#{content}")
76
119
  views = options[:views] || Web.views
77
- filename = "#{views}/#{content}.erb"
120
+ filename = "#{views}/#{content}.html.erb"
78
121
  src = ERB.new(File.read(filename)).src
79
122
 
80
123
  # Need to use lineno less by 1 because erb generates a
@@ -318,6 +318,16 @@ module Sidekiq
318
318
  redirect_with_query("#{root_path}scheduled")
319
319
  end
320
320
 
321
+ post "/scheduled/all/delete" do
322
+ Sidekiq::ScheduledSet.new.clear
323
+ redirect "#{root_path}scheduled"
324
+ end
325
+
326
+ post "/scheduled/all/add_to_queue" do
327
+ Sidekiq::ScheduledSet.new.each(&:add_to_queue)
328
+ redirect "#{root_path}scheduled"
329
+ end
330
+
321
331
  get "/dashboard/stats" do
322
332
  redirect "#{root_path}stats"
323
333
  end
@@ -325,6 +335,8 @@ module Sidekiq
325
335
  get "/stats" do
326
336
  sidekiq_stats = Sidekiq::Stats.new
327
337
  redis_stats = redis_info.slice(*REDIS_KEYS)
338
+ redis_stats["store_name"] = store_name
339
+ redis_stats["store_version"] = store_version
328
340
  json(
329
341
  sidekiq: {
330
342
  processed: sidekiq_stats.processed,
@@ -360,10 +372,16 @@ module Sidekiq
360
372
  unless sid
361
373
  require "net/http"
362
374
  data = Sidekiq.redis { |c| c.hget(key, "data") }
363
- resp = Net::HTTP.post(URI(store),
364
- data,
365
- {"Accept" => "application/vnd.firefox-profiler+json;version=1.0",
366
- "User-Agent" => "Sidekiq #{Sidekiq::VERSION} job profiler"})
375
+
376
+ store_uri = URI(store)
377
+ http = Net::HTTP.new(store_uri.host, store_uri.port)
378
+ http.use_ssl = store_uri.scheme == "https"
379
+ request = Net::HTTP::Post.new(store_uri.request_uri)
380
+ request.body = data
381
+ request["Accept"] = "application/vnd.firefox-profiler+json;version=1.0"
382
+ request["User-Agent"] = "Sidekiq #{Sidekiq::VERSION} job profiler"
383
+
384
+ resp = http.request(request)
367
385
  # https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py
368
386
  rawjson = resp.body.split(".")[1].unpack1("m")
369
387
  sid = Sidekiq.load_json(rawjson)["profileToken"]
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/web/csrf_protection"
4
-
5
3
  module Sidekiq
6
4
  class Web
7
5
  ##
@@ -51,13 +49,15 @@ module Sidekiq
51
49
 
52
50
  # Adds the "Back to App" link in the header
53
51
  attr_accessor :app_url
52
+ attr_accessor :assets_path
54
53
 
55
54
  def initialize
56
55
  @options = OPTIONS.dup
57
56
  @locales = LOCALES
58
57
  @views = VIEWS
58
+ @assets_path = ASSETS
59
59
  @tabs = DEFAULT_TABS.dup
60
- @middlewares = [Sidekiq::Web::CsrfProtection]
60
+ @middlewares = []
61
61
  @custom_job_info_rows = []
62
62
  end
63
63
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
  require "yaml"
5
- require "cgi"
5
+ require "cgi/escape"
6
6
 
7
7
  module Sidekiq
8
8
  # These methods are available to pages within the Web UI and UI extensions.
@@ -122,8 +122,8 @@ module Sidekiq
122
122
  resultset
123
123
  end
124
124
 
125
- def filtering(which)
126
- erb(:filtering, locals: {which: which})
125
+ def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
126
+ erb(:filtering, locals: {which:, placeholder_key:, label_key:})
127
127
  end
128
128
 
129
129
  def filter_link(jid, within = "retries")
@@ -165,7 +165,10 @@ module Sidekiq
165
165
  text_direction == "rtl"
166
166
  end
167
167
 
168
- # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
168
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
169
+ # Returns an array of language tags ordered by their quality value
170
+ #
171
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
169
172
  def user_preferred_languages
170
173
  languages = env["HTTP_ACCEPT_LANGUAGE"]
171
174
  languages.to_s.gsub(/\s+/, "").split(",").map { |language|
@@ -180,28 +183,30 @@ module Sidekiq
180
183
 
181
184
  # Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
182
185
  # this method will try to best match the available locales to the user's preferred languages.
183
- #
184
- # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
185
186
  def locale
186
187
  # session[:locale] is set via the locale selector from the footer
187
188
  @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
188
189
  l
189
190
  else
191
+ matched_locale = nil
192
+ # Attempt to find a case-insensitive exact match first
193
+ user_preferred_languages.each do |preferred|
194
+ # We only care about the language and primary subtag
195
+ # "en-GB-oxendict" becomes "en-GB"
196
+ language_tag = preferred.split("-")[0..1].join("-")
197
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
198
+ break if matched_locale
199
+ end
190
200
 
191
- # exactly match with preferred like "pt-BR, zh-CN, zh-TW..." first
192
- matched_locale = user_preferred_languages.find { |preferred|
193
- available_locales.include?(preferred) if preferred.length == 5
194
- }
195
-
196
- matched_locale ||= user_preferred_languages.map { |preferred|
197
- preferred_language = preferred.split("-", 2).first
198
-
199
- lang_group = available_locales.select { |available|
200
- preferred_language == available.split("-", 2).first
201
- }
201
+ return matched_locale if matched_locale
202
202
 
203
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
204
- }.compact.first
203
+ # Find the first base language match
204
+ # "en-US,es-MX;q=0.9" matches "en"
205
+ user_preferred_languages.each do |preferred|
206
+ base_language = preferred.split("-", 2).first
207
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
208
+ break if matched_locale
209
+ end
205
210
 
206
211
  matched_locale || "en"
207
212
  end
@@ -251,14 +256,6 @@ module Sidekiq
251
256
  end
252
257
  end
253
258
 
254
- def busy_weights(capsule_weights)
255
- # backwards compat with 7.0.0, remove in 7.1
256
- cw = [capsule_weights].flatten
257
- cw.map { |hash|
258
- hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
259
- }.join("; ")
260
- end
261
-
262
259
  def stats
263
260
  @stats ||= Sidekiq::Stats.new
264
261
  end
@@ -332,7 +329,7 @@ module Sidekiq
332
329
  end
333
330
 
334
331
  def csrf_tag
335
- "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
332
+ ""
336
333
  end
337
334
 
338
335
  def csp_nonce
@@ -372,7 +369,7 @@ module Sidekiq
372
369
  elsif rss_kb < 10_000_000
373
370
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
374
371
  else
375
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
372
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
376
373
  end
377
374
  end
378
375
 
data/lib/sidekiq/web.rb CHANGED
@@ -13,7 +13,7 @@ module Sidekiq
13
13
  ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
14
14
  VIEWS = "#{ROOT}/views"
15
15
  LOCALES = ["#{ROOT}/locales"]
16
- LAYOUT = "#{VIEWS}/layout.erb"
16
+ LAYOUT = "#{VIEWS}/layout.html.erb"
17
17
  ASSETS = "#{ROOT}/assets"
18
18
 
19
19
  DEFAULT_TABS = {
@@ -42,6 +42,12 @@ module Sidekiq
42
42
  @@config.app_url = url
43
43
  end
44
44
 
45
+ def assets_path=(path)
46
+ @@config.assets_path = path
47
+ end
48
+
49
+ def assets_path = @@config.assets_path
50
+
45
51
  def tabs = @@config.tabs
46
52
 
47
53
  def locales = @@config.locales
@@ -87,13 +93,27 @@ module Sidekiq
87
93
  env[:web_config] = Sidekiq::Web.configure
88
94
  env[:csp_nonce] = SecureRandom.hex(8)
89
95
  env[:redis_pool] = self.class.redis_pool
90
- app.call(env)
96
+ safe_request?(env) ? app.call(env) : deny(env)
91
97
  end
92
98
 
93
99
  def app
94
100
  @app ||= build(@@config)
95
101
  end
96
102
 
103
+ def safe_methods?(env)
104
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
105
+ end
106
+
107
+ def safe_request?(env)
108
+ return true if safe_methods?(env)
109
+ env["HTTP_SEC_FETCH_SITE"] == "same-origin"
110
+ end
111
+
112
+ def deny(env)
113
+ Sidekiq.logger.warn "attack prevented by #{self.class}"
114
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
115
+ end
116
+
97
117
  private
98
118
 
99
119
  def build(cfg)
@@ -105,7 +125,7 @@ module Sidekiq
105
125
 
106
126
  ::Rack::Builder.new do
107
127
  use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
108
- root: ASSETS,
128
+ root: cfg.assets_path,
109
129
  cascade: true,
110
130
  header_rules: rules
111
131
  m.each { |middleware, block| use(*middleware, &block) }
data/lib/sidekiq.rb CHANGED
@@ -29,6 +29,7 @@ end
29
29
 
30
30
  require "sidekiq/config"
31
31
  require "sidekiq/logger"
32
+ require "sidekiq/loader"
32
33
  require "sidekiq/client"
33
34
  require "sidekiq/transaction_aware_client"
34
35
  require "sidekiq/job"
@@ -94,6 +95,10 @@ module Sidekiq
94
95
  default_configuration.logger
95
96
  end
96
97
 
98
+ def self.loader
99
+ @loader ||= Loader.new
100
+ end
101
+
97
102
  def self.configure_server(&block)
98
103
  (@config_blocks ||= []) << block
99
104
  yield default_configuration if server?
data/sidekiq.gemspec CHANGED
@@ -23,9 +23,9 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.add_dependency "redis-client", ">= 0.23.2"
27
- gem.add_dependency "connection_pool", ">= 2.5.0"
28
- gem.add_dependency "rack", ">= 3.1.0"
29
- gem.add_dependency "json", ">= 2.9.0"
30
- gem.add_dependency "logger", ">= 1.6.2"
26
+ gem.add_dependency "redis-client", ">= 0.26.0"
27
+ gem.add_dependency "connection_pool", ">= 3.0.0"
28
+ gem.add_dependency "rack", ">= 3.2.0"
29
+ gem.add_dependency "json", ">= 2.16.0"
30
+ gem.add_dependency "logger", ">= 1.7.0"
31
31
  end
File without changes
File without changes
@@ -18,19 +18,11 @@ function addListeners() {
18
18
  })
19
19
  });
20
20
 
21
- document.querySelectorAll("input[data-confirm]").forEach(node => {
22
- node.addEventListener("click", event => {
23
- if (!window.confirm(node.getAttribute("data-confirm"))) {
24
- event.preventDefault();
25
- event.stopPropagation();
26
- }
27
- })
28
- })
29
-
30
21
  document.querySelectorAll("[data-toggle]").forEach(node => {
31
22
  node.addEventListener("click", addDataToggleListeners)
32
23
  })
33
24
 
25
+ initializeBulkToggle();
34
26
  addShiftClickListeners();
35
27
  updateFuzzyTimes();
36
28
  updateNumbers();
@@ -67,11 +59,26 @@ function addPollingListeners(_event) {
67
59
 
68
60
  function addDataToggleListeners(event) {
69
61
  var source = event.target || event.srcElement;
70
- var targName = source.getAttribute("data-toggle");
62
+ var targName = source.dataset.toggle;
71
63
  var full = document.getElementById(targName);
72
64
  full.classList.toggle("is-open");
73
65
  }
74
66
 
67
+ function toggleBulkButtons() {
68
+ const checkboxes = document.querySelectorAll('.select-item-checkbox, .check-all-items');
69
+ const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
70
+ const buttons = document.querySelectorAll('.bulk-action-buttons');
71
+ buttons.forEach(btn => {
72
+ btn.style.display = anyChecked ? 'none' : 'block';
73
+ });
74
+ }
75
+
76
+ function initializeBulkToggle(){
77
+ document.querySelectorAll('.check-all-items, .select-item-checkbox').forEach(cb => {
78
+ cb.addEventListener('change', toggleBulkButtons);
79
+ });
80
+ }
81
+
75
82
  function addShiftClickListeners() {
76
83
  let checkboxes = Array.from(document.querySelectorAll(".shift_clickable"));
77
84
  let lastChecked = null;
@@ -90,7 +97,7 @@ function addShiftClickListeners() {
90
97
  }
91
98
 
92
99
  function updateFuzzyTimes() {
93
- var locale = document.body.getAttribute("data-locale");
100
+ var locale = document.body.dataset.locale;
94
101
  var parts = locale.split('-');
95
102
  if (typeof parts[1] !== 'undefined') {
96
103
  parts[1] = parts[1].toUpperCase();
@@ -105,7 +112,7 @@ function updateFuzzyTimes() {
105
112
  function updateNumbers() {
106
113
  document.querySelectorAll("[data-nwp]").forEach(node => {
107
114
  let number = parseFloat(node.textContent);
108
- let precision = parseInt(node.dataset["nwp"] || 0);
115
+ let precision = parseInt(node.dataset.nwp || 0);
109
116
  if (typeof number === "number") {
110
117
  let formatted = number.toLocaleString(undefined, {
111
118
  minimumFractionDigits: precision,
@@ -178,4 +185,20 @@ function updateLocale(event) {
178
185
 
179
186
  function updateProgressBars() {
180
187
  document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
181
- }
188
+ }
189
+
190
+ function handleConfirmDialog (event) {
191
+ const target = event.target
192
+
193
+ if (target.localName !== "input" && target.localName !== "button" ) { return }
194
+ const confirmMessage = target.dataset.confirm
195
+
196
+ if (confirmMessage === undefined) { return }
197
+
198
+ if (!window.confirm(confirmMessage)) {
199
+ event.preventDefault()
200
+ event.stopPropagation()
201
+ }
202
+ }
203
+
204
+ document.addEventListener("click", handleConfirmDialog)
@@ -11,7 +11,7 @@ var updateStatsSummary = function(data) {
11
11
  }
12
12
 
13
13
  var updateRedisStats = function(data) {
14
- document.getElementById('redis_version').innerText = data.redis_version;
14
+ document.getElementById('redis_version').innerText = data.store_version;
15
15
  document.getElementById('uptime_in_days').innerText = data.uptime_in_days;
16
16
  document.getElementById('connected_clients').innerText = data.connected_clients;
17
17
  document.getElementById('used_memory_human').innerText = data.used_memory_human;
@@ -29,8 +29,6 @@
29
29
 
30
30
  *, *::before, *::after { box-sizing: border-box; }
31
31
 
32
- ::selection { background: var(--color-selected); }
33
-
34
32
  :focus-visible {
35
33
  outline: 1px solid oklch(from var(--color-primary) l c h / 50%);
36
34
  }
@@ -69,7 +67,7 @@ body {
69
67
 
70
68
  .container {
71
69
  margin: 0 auto;
72
- max-width: 1440px;
70
+ /* max-width: 1440px; */
73
71
  padding: var(--space-2x);
74
72
  }
75
73
 
@@ -90,6 +88,7 @@ code {
90
88
  font-family: var(--font-mono);
91
89
  font-size: var(--font-size-small);
92
90
  padding: var(--space-1-2);
91
+ word-wrap: anywhere;
93
92
  }
94
93
 
95
94
  time { color: var(--color-text-light); }
@@ -437,7 +436,10 @@ article .count {
437
436
  }
438
437
 
439
438
  /* table */
440
- .table_container { overflow-x: auto; }
439
+ .table_container {
440
+ overflow-x: auto;
441
+ margin-bottom: var(--space-2x);
442
+ }
441
443
 
442
444
  .table_container + form,
443
445
  .table_container + input,
@@ -446,12 +448,14 @@ canvas + .table_container {
446
448
  }
447
449
 
448
450
  .buttons-row {
449
- margin-top: var(--space-3x);
450
451
  display: flex;
451
- flex-wrap: wrap;
452
452
  gap: 8px;
453
453
  }
454
454
 
455
+ .bulk-action-buttons.bulk-lead-button {
456
+ margin-inline-start: auto;
457
+ }
458
+
455
459
  table {
456
460
  background-color: var(--color-elevated);
457
461
  border: 1px solid var(--color-border);
@@ -748,3 +752,23 @@ body > footer .nav {
748
752
  .w-50 {
749
753
  width: 50%;
750
754
  }
755
+
756
+ .flash {
757
+ width: 100%;
758
+ text-align: center;
759
+ padding: 20px;
760
+ background: var(--color-success);
761
+ }
762
+
763
+ .args {
764
+ overflow-y: auto;
765
+ max-height: 50px;
766
+ word-break: break-word
767
+ }
768
+ .args-extended {
769
+ overflow-y: scroll;
770
+ max-height: 500px;
771
+ }
772
+
773
+ .toggle { display: none; }
774
+ .toggle.is-open { display: block; }
data/web/locales/ar.yml CHANGED
@@ -3,6 +3,7 @@ ar:
3
3
  LanguageName: العربية
4
4
  Actions: إجراءات
5
5
  AddToQueue: إضافة إلى الرتل
6
+ AddAllToQueue: إضافة الكل إلى الرتل
6
7
  AreYouSure: هل انت متأكد؟
7
8
  AreYouSureDeleteJob: هل أنت متأكد من حذف الوظيفة؟
8
9
  AreYouSureDeleteQueue: هل أنت متأكد من حذف الرتل %{queue}؟
data/web/locales/cs.yml CHANGED
@@ -3,6 +3,7 @@ cs:
3
3
  LanguageName: Čeština
4
4
  Actions: Akce
5
5
  AddToQueue: Přidat do fronty
6
+ AddAllToQueue: Přidat vše do fronty
6
7
  AreYouSure: Jste si jisti?
7
8
  AreYouSureDeleteJob: Jste si jisti, že chcete odstranit tento úkol?
8
9
  AreYouSureDeleteQueue: Jste si jisti, že chcete odstranit frontu %{queue}?
data/web/locales/da.yml CHANGED
@@ -3,6 +3,7 @@ da:
3
3
  LanguageName: Dansk
4
4
  Actions: Handlinger
5
5
  AddToQueue: Tilføj til kø
6
+ AddAllToQueue: Tilføj alle til kø
6
7
  AreYouSure: Er du sikker?
7
8
  AreYouSureDeleteJob: Er du sikker på at du vil slette dette job?
8
9
  AreYouSureDeleteQueue: Er du sikker på at du vil slette %{queue} køen?
data/web/locales/de.yml CHANGED
@@ -3,6 +3,7 @@ de:
3
3
  LanguageName: Deutsch
4
4
  Actions: Aktionen
5
5
  AddToQueue: In Warteschlange einreihen
6
+ AddAllToQueue: Alle in Warteschlange einreihen
6
7
  AreYouSure: Bist du sicher?
7
8
  AreYouSureDeleteJob: Möchtest du diesen Job wirklich löschen?
8
9
  AreYouSureDeleteQueue: Möchtest du %{queue} wirklich löschen?
data/web/locales/el.yml CHANGED
@@ -26,6 +26,7 @@ el: # <---- change this to your locale code
26
26
  ShowAll: Εμφάνιση Όλων
27
27
  Enqueued: Μπήκαν στην στοίβα
28
28
  AddToQueue: Προσθήκη στην στοίβα
29
+ AddAllToQueue: Προσθήκη όλων στην στοίβα
29
30
  AreYouSureDeleteJob: Θέλετε να διαγράψετε αυτή την εργασία;
30
31
  AreYouSureDeleteQueue: Θέλετε να διαγράψετε την στοίβα %{queue}; Αυτό θα διαγράψει όλες τις εργασίες εντός της στοίβας, θα εμφανιστεί ξανά εάν προωθήσετε περισσότερες εργασίες σε αυτήν στο μέλλον.
31
32
  Queues: Στοίβες