sidekiq-tasks 0.1.8 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d5973e69de00ba6c53592cda231856f13bb1c18795d67938121573eb40526c2
4
- data.tar.gz: 7f58c1afbff9bc66e1e6c742cc0dc1a74dd7415201977aca021cca445a32fc5c
3
+ metadata.gz: 8fee03d24fdcc9755f8fadb9915b39127fac89ba1f4447a7d822998b9a64c102
4
+ data.tar.gz: 8a6c5940eae17706522c7f584c392779a0b2c513848fa49cab8206c2a5af5473
5
5
  SHA512:
6
- metadata.gz: b1c7bd390583a1456694330def614d4a85b26a23b0c18af9cd4bea9711fe55e13ded4617ecf60a20afd58a53e258c363d2f2c49ffaafd87af3d3662e3b596c8b
7
- data.tar.gz: 683d90b2c4fd4e0c648ada7fbd13116c27da624745783ed77f70336c52efe4519acea5daf8425e25df36cc044ab213fd04fa35e0ba2c8098cbb888e2fe0aa534
6
+ metadata.gz: b85eb95f150f99c0de282d0ac6fb1356a984f0cdc7d5a485baf1a9fdb7963346156a9e4c224d1cd22476002efd34bb8ed18bbf1252d762cbd25c8b41623557b4
7
+ data.tar.gz: 51ca1179d3232c84139c8ef380290aa5e9bd8f3b97b21e92a54165fcee0376a1a42b207ec9029510f086b35e2ec69aa091685f89d343a3eb38c31296d8600709
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## Changelog
2
2
 
3
+ ### [1.0.0] - 2026-03-13
4
+
5
+ - Add configurable `storage` option with support for custom backends (default: Redis).
6
+ - Add configurable `current_user` option to track who enqueued each task.
7
+ - Add configurable `history_limit` option (default: 10).
8
+ - Add sortable columns (`name`, `last_enqueued`) in the tasks list.
9
+
3
10
  ### [0.1.8] - 2026-03-06
4
11
 
5
12
  - Replace deprecated `webdrivers` gem with `selenium-webdriver`.
data/README.md CHANGED
@@ -227,12 +227,38 @@ end
227
227
 
228
228
  ## Execution history
229
229
 
230
- Each task keeps a history of its last 10 executions, stored in Redis. The task detail page displays for each execution:
230
+ Each task keeps a history of its last executions (default: 10, stored in Redis). The task detail page displays for each execution:
231
231
 
232
232
  - The enqueue, start, and finish timestamps
233
233
  - The arguments passed
234
234
  - The error message if the execution failed
235
235
 
236
+ You can configure the history limit:
237
+
238
+ ```ruby
239
+ Sidekiq::Tasks.configure do |config|
240
+ config.history_limit = 25
241
+ end
242
+ ```
243
+
244
+ You can also use a custom storage backend. See the [Custom Storage Guide](docs/custom_storage.md) for more details.
245
+
246
+ ## Current user
247
+
248
+ You can configure a `current_user` proc to track who enqueued each task.
249
+ The proc receives the Rack `env` and should return a Hash identifying the user:
250
+
251
+ ```ruby
252
+ Sidekiq::Tasks.configure do |config|
253
+ config.current_user = ->(env) do
254
+ user = env["warden"].user(:admin_user)
255
+ {id: user.id, email: user.email}
256
+ end
257
+ end
258
+ ```
259
+
260
+ When configured, an "Enqueued by" column appears in the task history table.
261
+
236
262
  ## Development
237
263
 
238
264
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,95 @@
1
+ # Custom Storage Guide
2
+
3
+ This guide walks you through implementing a custom storage backend using ActiveRecord in a Rails application.
4
+
5
+ ## Step 1: Generate the model and run the migration
6
+
7
+ ```bash
8
+ rails generate model TaskExecution \
9
+ task_name:string:index \
10
+ jid:string:index \
11
+ args:jsonb \
12
+ enqueued_at:datetime \
13
+ executed_at:datetime \
14
+ finished_at:datetime \
15
+ error:string \
16
+ user:jsonb
17
+
18
+ rails db:migrate
19
+ ```
20
+
21
+ ## Step 2: Configure the initializer
22
+
23
+ In `config/initializers/sidekiq_tasks.rb`:
24
+
25
+ ```ruby
26
+ class ActiveRecordStorage < Sidekiq::Tasks::Storage::Base
27
+ def last_enqueue_at
28
+ TaskExecution.where(task_name: task_name).order(enqueued_at: :desc).pick(:enqueued_at)
29
+ end
30
+
31
+ def history
32
+ TaskExecution
33
+ .where(task_name: task_name)
34
+ .order(enqueued_at: :desc)
35
+ .limit(history_limit)
36
+ .select(:jid, :task_name, :args, :enqueued_at, :executed_at, :finished_at, :error, :user)
37
+ .map(&:attributes)
38
+ end
39
+
40
+ def store_enqueue(jid, args, user: nil)
41
+ TaskExecution.create!(
42
+ task_name: task_name,
43
+ jid: jid,
44
+ args: args,
45
+ enqueued_at: Time.now,
46
+ user: user
47
+ )
48
+ end
49
+
50
+ def store_execution(jid, time_key)
51
+ TaskExecution.find_by(jid: jid)&.update!(time_key => Time.now)
52
+ end
53
+
54
+ def store_execution_error(jid, error)
55
+ message = truncate_message("#{error.class}: #{error.message}", ERROR_MESSAGE_MAX_LENGTH)
56
+ TaskExecution.find_by(jid: jid)&.update!(error: message)
57
+ end
58
+ end
59
+
60
+ Sidekiq::Tasks.configure do |config|
61
+ config.storage = ActiveRecordStorage
62
+ end
63
+ ```
64
+
65
+ > [!NOTE]
66
+ > The `history_limit` config is passed to each storage instance. The default Redis storage uses it to trim old entries. Custom storage implementations receive it via the `history_limit` accessor and can use it as needed (e.g. as a SQL `LIMIT`) or ignore it entirely.
67
+
68
+ ## History entry format
69
+
70
+ The `history` method must return an array of hashes with the following keys:
71
+
72
+ | Key | Type | Description |
73
+ |-----------------|---------------|--------------------------------------|
74
+ | `"jid"` | String | The Sidekiq job ID |
75
+ | `"task_name"` | String | The task name |
76
+ | `"args"` | Hash | The arguments passed to the task |
77
+ | `"enqueued_at"` | Time | When the task was enqueued |
78
+ | `"executed_at"` | Time \| nil | When the task started executing |
79
+ | `"finished_at"` | Time \| nil | When the task finished executing |
80
+ | `"error"` | String \| nil | Error message if execution failed |
81
+ | `"user"` | Hash \| nil | User who enqueued the task |
82
+
83
+ ## Migrating from Redis
84
+
85
+ After switching to a custom storage, you can clean up the old Redis history:
86
+
87
+ ```bash
88
+ redis-cli --scan --pattern "task:*" | xargs redis-cli del
89
+ ```
90
+
91
+ Or from a Rails console:
92
+
93
+ ```ruby
94
+ Sidekiq.redis { |conn| conn.keys("task:*").each { |key| conn.del(key) } }
95
+ ```
@@ -15,14 +15,21 @@ module Sidekiq
15
15
  ),
16
16
  ].freeze
17
17
 
18
+ DEFAULT_STORAGE = Sidekiq::Tasks::Storage::Redis
19
+
20
+ DEFAULT_HISTORY_LIMIT = 10
21
+
18
22
  include Sidekiq::Tasks::Validations
19
23
 
20
- attr_reader :strategies, :sidekiq_options, :authorization
24
+ attr_reader :strategies, :sidekiq_options, :authorization, :history_limit, :current_user, :storage
21
25
 
22
26
  def initialize
23
27
  @sidekiq_options = DEFAULT_SIDEKIQ_OPTIONS
24
28
  @strategies = DEFAULT_STRATEGIES
29
+ @storage = DEFAULT_STORAGE
25
30
  @authorization = ->(_env) { true }
31
+ @history_limit = DEFAULT_HISTORY_LIMIT
32
+ @current_user = nil
26
33
  end
27
34
 
28
35
  # @see https://github.com/sidekiq/sidekiq/wiki/Advanced-Options#jobs
@@ -45,11 +52,31 @@ module Sidekiq
45
52
  @strategies = strategies
46
53
  end
47
54
 
55
+ def storage=(storage_class)
56
+ validate_subclass!(storage_class, Sidekiq::Tasks::Storage::Base, "storage")
57
+
58
+ @storage = storage_class
59
+ end
60
+
61
+ def history_limit=(limit)
62
+ validate_class!(limit, [Integer], "history_limit")
63
+
64
+ raise Sidekiq::Tasks::ArgumentError, "'history_limit' must be greater than 0" if limit <= 0
65
+
66
+ @history_limit = limit
67
+ end
68
+
48
69
  def authorization=(authorization_proc)
49
70
  validate_class!(authorization_proc, [Proc], "authorization")
50
71
 
51
72
  @authorization = authorization_proc
52
73
  end
74
+
75
+ def current_user=(current_user_proc)
76
+ validate_class!(current_user_proc, [Proc], "current_user")
77
+
78
+ @current_user = current_user_proc
79
+ end
53
80
  end
54
81
  end
55
82
  end
@@ -0,0 +1,73 @@
1
+ module Sidekiq
2
+ module Tasks
3
+ module Storage
4
+ class Base
5
+ ERROR_MESSAGE_MAX_LENGTH = 255
6
+
7
+ attr_reader :task_name, :history_limit
8
+
9
+ def initialize(task_name, history_limit: nil)
10
+ @task_name = task_name
11
+ @history_limit = history_limit
12
+ end
13
+
14
+ # Returns the last enqueue time for the task.
15
+ #
16
+ # @abstract Subclasses must implement this method.
17
+ # @return [Time, NilClass] The last enqueue time or nil.
18
+ # @raise [NotImplementedError] If the method is not implemented in a subclass.
19
+ def last_enqueue_at
20
+ raise NotImplementedError, "Storage must implement #last_enqueue_at"
21
+ end
22
+
23
+ # Returns the execution history for the task.
24
+ #
25
+ # @abstract Subclasses must implement this method.
26
+ # @return [Array<Hash>] The execution history entries.
27
+ # @raise [NotImplementedError] If the method is not implemented in a subclass.
28
+ def history
29
+ raise NotImplementedError, "Storage must implement #history"
30
+ end
31
+
32
+ # Stores enqueue information for the task.
33
+ #
34
+ # @abstract Subclasses must implement this method.
35
+ # @param jid [String] The Sidekiq job ID.
36
+ # @param args [Hash] The arguments passed to the task.
37
+ # @param user [Hash, NilClass] The user who enqueued the task.
38
+ # @raise [NotImplementedError] If the method is not implemented in a subclass.
39
+ def store_enqueue(_jid, _args, user: nil)
40
+ raise NotImplementedError, "Storage must implement #store_enqueue"
41
+ end
42
+
43
+ # Stores execution time for a specific history entry.
44
+ #
45
+ # @abstract Subclasses must implement this method.
46
+ # @param jid [String] The Sidekiq job ID.
47
+ # @param time_key [String] The time key to store (e.g. "executed_at", "finished_at").
48
+ # @raise [NotImplementedError] If the method is not implemented in a subclass.
49
+ def store_execution(_jid, _time_key)
50
+ raise NotImplementedError, "Storage must implement #store_execution"
51
+ end
52
+
53
+ # Stores an execution error for a specific history entry.
54
+ #
55
+ # @abstract Subclasses must implement this method.
56
+ # @param jid [String] The Sidekiq job ID.
57
+ # @param error [Exception] The error that occurred during execution.
58
+ # @raise [NotImplementedError] If the method is not implemented in a subclass.
59
+ def store_execution_error(_jid, _error)
60
+ raise NotImplementedError, "Storage must implement #store_execution_error"
61
+ end
62
+
63
+ protected
64
+
65
+ def truncate_message(message, max_length)
66
+ return message if message.length <= max_length
67
+
68
+ "#{message[0...(max_length - 3)]}..."
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,89 @@
1
+ module Sidekiq
2
+ module Tasks
3
+ module Storage
4
+ class Redis < Base
5
+ JID_PREFIX = "task".freeze
6
+ def jid_key
7
+ "#{JID_PREFIX}:#{task_name}"
8
+ end
9
+
10
+ def history_key
11
+ "#{jid_key}:history"
12
+ end
13
+
14
+ def last_enqueue_at
15
+ stored_time("last_enqueue_at")
16
+ end
17
+
18
+ def history
19
+ raw_entries = Sidekiq.redis { |conn| conn.lrange(history_key, 0, -1) }
20
+
21
+ return [] unless raw_entries
22
+
23
+ raw_entries.map do |raw|
24
+ entry = Sidekiq.load_json(raw)
25
+ ["enqueued_at", "executed_at", "finished_at"].each do |key|
26
+ entry[key] = Time.at(entry[key]) if entry[key]
27
+ end
28
+ entry
29
+ end
30
+ end
31
+
32
+ def store_history(jid, task_args, time, user: nil)
33
+ Sidekiq.redis do |conn|
34
+ task_trace = {jid: jid, task_name: task_name, args: task_args, enqueued_at: time.to_f}
35
+ task_trace[:user] = user if user
36
+ conn.lpush(history_key, Sidekiq.dump_json(task_trace))
37
+ conn.ltrim(history_key, 0, history_limit - 1)
38
+ end
39
+ end
40
+
41
+ def store_enqueue(jid, args, user: nil)
42
+ time = Time.now.to_f
43
+ store_time(time, "last_enqueue_at")
44
+ store_history(jid, args, time, user: user)
45
+ end
46
+
47
+ def store_execution(jid, time_key)
48
+ update_history_entry(jid) do |entry|
49
+ entry.merge(time_key => Time.now.to_f)
50
+ end
51
+ end
52
+
53
+ def store_execution_error(jid, error)
54
+ update_history_entry(jid) do |entry|
55
+ error_message = truncate_message("#{error.class}: #{error.message}", ERROR_MESSAGE_MAX_LENGTH)
56
+ entry.merge("error" => error_message)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def store_time(time, time_key)
63
+ Sidekiq.redis { |conn| conn.hset(jid_key, time_key, time.to_f) }
64
+ end
65
+
66
+ def stored_time(time_key)
67
+ timestamp = Sidekiq.redis { |conn| conn.hget(jid_key, time_key) }
68
+
69
+ [nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_f)
70
+ end
71
+
72
+ def update_history_entry(jid)
73
+ Sidekiq.redis do |conn|
74
+ entries = conn.lrange(history_key, 0, -1)
75
+
76
+ entries.each_with_index do |raw, index|
77
+ entry = Sidekiq.load_json(raw)
78
+ next unless entry["jid"] == jid
79
+
80
+ updated_entry = yield(entry)
81
+ conn.lset(history_key, index, Sidekiq.dump_json(updated_entry))
82
+ break
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,101 +1,8 @@
1
1
  module Sidekiq
2
2
  module Tasks
3
- class Storage
4
- JID_PREFIX = "task".freeze
5
- HISTORY_LIMIT = 10
6
- ERROR_MESSAGE_MAX_LENGTH = 255
7
-
8
- attr_reader :task_name
9
-
10
- def initialize(task_name)
11
- @task_name = task_name
12
- end
13
-
14
- def jid_key
15
- "#{JID_PREFIX}:#{task_name}"
16
- end
17
-
18
- def history_key
19
- "#{jid_key}:history"
20
- end
21
-
22
- def last_enqueue_at
23
- stored_time("last_enqueue_at")
24
- end
25
-
26
- def history
27
- raw_entries = Sidekiq.redis { |conn| conn.lrange(history_key, 0, -1) }
28
-
29
- return [] unless raw_entries
30
-
31
- raw_entries.map do |raw|
32
- entry = Sidekiq.load_json(raw)
33
- %w[enqueued_at executed_at finished_at].each do |key|
34
- entry[key] = Time.at(entry[key]) if entry[key]
35
- end
36
- entry
37
- end
38
- end
39
-
40
- def store_history(jid, task_args, time)
41
- Sidekiq.redis do |conn|
42
- task_trace = {jid: jid, name: task_name, args: task_args, enqueued_at: time.to_f}
43
- conn.lpush(history_key, Sidekiq.dump_json(task_trace))
44
- conn.ltrim(history_key, 0, HISTORY_LIMIT - 1)
45
- end
46
- end
47
-
48
- def store_enqueue(jid, args)
49
- time = Time.now.to_f
50
- store_time(time, "last_enqueue_at")
51
- store_history(jid, args, time)
52
- end
53
-
54
- def store_execution(jid, time_key)
55
- update_history_entry(jid) do |entry|
56
- entry.merge(time_key => Time.now.to_f)
57
- end
58
- end
59
-
60
- def store_execution_error(jid, error)
61
- update_history_entry(jid) do |entry|
62
- error_message = truncate_message("#{error.class}: #{error.message}", ERROR_MESSAGE_MAX_LENGTH)
63
- entry.merge("error" => error_message)
64
- end
65
- end
66
-
67
- private
68
-
69
- def truncate_message(message, max_length)
70
- return message if message.length <= max_length
71
-
72
- "#{message[0...(max_length - 3)]}..."
73
- end
74
-
75
- def store_time(time, time_key)
76
- Sidekiq.redis { |conn| conn.hset(jid_key, time_key, time.to_f) }
77
- end
78
-
79
- def stored_time(time_key)
80
- timestamp = Sidekiq.redis { |conn| conn.hget(jid_key, time_key) }
81
-
82
- [nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_f)
83
- end
84
-
85
- def update_history_entry(jid)
86
- Sidekiq.redis do |conn|
87
- entries = conn.lrange(history_key, 0, -1)
88
-
89
- entries.each_with_index do |raw, index|
90
- entry = Sidekiq.load_json(raw)
91
- next unless entry["jid"] == jid
92
-
93
- updated_entry = yield(entry)
94
- conn.lset(history_key, index, Sidekiq.dump_json(updated_entry))
95
- break
96
- end
97
- end
98
- end
3
+ module Storage
4
+ autoload :Base, "sidekiq/tasks/storage/base"
5
+ autoload :Redis, "sidekiq/tasks/storage/redis"
99
6
  end
100
7
  end
101
8
  end
@@ -22,10 +22,10 @@ module Sidekiq
22
22
  validate_class!(strategy, [Sidekiq::Tasks::Strategies::Base], "strategy")
23
23
  end
24
24
 
25
- def enqueue(params = {})
25
+ def enqueue(params = {}, user: nil)
26
26
  jid = strategy.enqueue_task(name, params)
27
27
 
28
- storage.store_enqueue(jid, params)
28
+ storage.store_enqueue(jid, params, user: user)
29
29
  end
30
30
 
31
31
  def execute(params = {}, jid: nil)
@@ -33,7 +33,7 @@ module Sidekiq
33
33
 
34
34
  begin
35
35
  strategy.execute_task(name, params)
36
- rescue => e
36
+ rescue StandardError, SystemExit => e
37
37
  storage.store_execution(jid, "finished_at")
38
38
  storage.store_execution_error(jid, e)
39
39
  raise
@@ -43,7 +43,7 @@ module Sidekiq
43
43
  end
44
44
 
45
45
  def storage
46
- @_storage ||= Sidekiq::Tasks::Storage.new(name)
46
+ @_storage ||= Sidekiq::Tasks.config.storage.new(name, history_limit: Sidekiq::Tasks.config.history_limit)
47
47
  end
48
48
  end
49
49
  end
@@ -32,6 +32,14 @@ module Sidekiq
32
32
  "'#{name}' must be one of #{expected_values.map(&:inspect).join(" or ")} but received #{value.inspect}"
33
33
  end
34
34
  module_function :validate_expected_values!
35
+
36
+ def validate_subclass!(klass, base_class, name = nil)
37
+ return if klass.is_a?(Class) && klass <= base_class
38
+
39
+ raise Sidekiq::Tasks::ArgumentError,
40
+ "'#{name}' must be a class inheriting from #{base_class.name} but received #{klass.inspect}"
41
+ end
42
+ module_function :validate_subclass!
35
43
  end
36
44
  end
37
45
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Tasks
5
- VERSION = "0.1.8"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@ require "sidekiq/tasks/web/helpers/application_helper"
2
2
  require "sidekiq/tasks/web/helpers/tag_helper"
3
3
  require "sidekiq/tasks/web/helpers/task_helper"
4
4
  require "sidekiq/tasks/web/helpers/pagination_helper"
5
+ require "sidekiq/tasks/web/helpers/sort_helper"
5
6
  require "sidekiq/tasks/web/search"
6
7
  require "sidekiq/tasks/web/pagination"
7
8
  require "sidekiq/tasks/web/params"
@@ -15,11 +16,12 @@ module Sidekiq
15
16
  app.helpers(Sidekiq::Tasks::Web::Helpers::TagHelper)
16
17
  app.helpers(Sidekiq::Tasks::Web::Helpers::TaskHelper)
17
18
  app.helpers(Sidekiq::Tasks::Web::Helpers::PaginationHelper)
19
+ app.helpers(Sidekiq::Tasks::Web::Helpers::SortHelper)
18
20
 
19
21
  app.get "/tasks" do
20
22
  authorize!
21
23
 
22
- @search = Sidekiq::Tasks::Web::Search.new(fetch_params(:count, :page, :filter))
24
+ @search = Sidekiq::Tasks::Web::Search.new(fetch_params(:count, :page, :filter, :sort, :direction))
23
25
 
24
26
  erb(read_view(:tasks), locals: {search: @search})
25
27
  end
@@ -29,7 +31,22 @@ module Sidekiq
29
31
 
30
32
  @task = find_task!(env["rack.route_params"][:name])
31
33
 
32
- erb(read_view(:task), locals: {task: @task})
34
+ history = @task.history
35
+ per_page = 10
36
+ page = [fetch_param("page").to_i, 1].max
37
+ total_pages = [(history.size.to_f / per_page).ceil, 1].max
38
+ history_entries = history.slice((page - 1) * per_page, per_page) || []
39
+
40
+ erb(
41
+ read_view(:task),
42
+ locals: {
43
+ task: @task,
44
+ history_entries: history_entries,
45
+ history_page: page,
46
+ history_total_pages: total_pages,
47
+ history_total_count: history.size,
48
+ }
49
+ )
33
50
  rescue Sidekiq::Tasks::NotFoundError
34
51
  throw :halt, [404, {Rack::CONTENT_TYPE => "text/plain"}, ["Task not found"]]
35
52
  end
@@ -44,7 +61,8 @@ module Sidekiq
44
61
  task = find_task!(env["rack.route_params"][:name])
45
62
  args = Sidekiq::Tasks::Web::Params.new(task, fetch_param("args")).permit!
46
63
 
47
- task.enqueue(args)
64
+ current_user = Sidekiq::Tasks.config.current_user&.call(env)
65
+ task.enqueue(args, user: current_user)
48
66
 
49
67
  redirect(task_url(root_path, task))
50
68
  rescue Sidekiq::Tasks::ArgumentError => e
@@ -6,22 +6,29 @@ module Sidekiq
6
6
  extend self
7
7
  include Sidekiq::Tasks::Web::Helpers::TagHelper
8
8
 
9
- def pagination_link(root_path, link, search)
9
+ def pagination_base_url(search, root_path)
10
+ query_params = {
11
+ filter: search.filter.to_s,
12
+ count: search.count,
13
+ sort: search.sort,
14
+ direction: search.direction,
15
+ }
16
+
17
+ "#{root_path}tasks?#{URI.encode_www_form(query_params)}"
18
+ end
19
+
20
+ def build_pagination_link(link, base_url)
21
+ separator = base_url.include?("?") ? "&" : "?"
22
+
10
23
  build_tag(:li, class: "st-page-item") do
11
24
  build_tag(
12
25
  :a,
13
26
  link[:text],
14
27
  class: build_classes("st-page-link", disabled: link[:disabled], active: link[:active]),
15
- href: pagination_url(root_path, search, link[:page])
28
+ href: "#{base_url}#{separator}page=#{link[:page]}"
16
29
  )
17
30
  end
18
31
  end
19
-
20
- private
21
-
22
- def pagination_url(root_path, search, page)
23
- "#{root_path}tasks?filter=#{ERB::Util.url_encode(search.filter)}&count=#{search.count}&page=#{page}"
24
- end
25
32
  end
26
33
  end
27
34
  end
@@ -0,0 +1,26 @@
1
+ module Sidekiq
2
+ module Tasks
3
+ module Web
4
+ module Helpers
5
+ module SortHelper
6
+ extend self
7
+
8
+ def sort_header_url(search, root_path, column)
9
+ next_direction = search.toggle_direction(column)
10
+
11
+ query_params = {filter: search.filter.to_s, count: search.count}
12
+ query_params.merge!(sort: column, direction: next_direction) if next_direction
13
+
14
+ "#{root_path}tasks?#{URI.encode_www_form(query_params)}"
15
+ end
16
+
17
+ def sort_header_classes(search, column)
18
+ css = "st-sortable"
19
+ css += " st-sorted-#{search.direction}" if search.sorted_by?(column)
20
+ css
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -3,6 +3,8 @@ module Sidekiq
3
3
  module Web
4
4
  class Search
5
5
  DEFAULT_COUNT = 15
6
+ SORT_COLUMNS = ["name", "last_enqueued"].freeze
7
+ SORT_DIRECTIONS = ["asc", "desc"].freeze
6
8
 
7
9
  def self.count_options
8
10
  (0..3).map { |index| DEFAULT_COUNT * (2**index) }
@@ -15,7 +17,7 @@ module Sidekiq
15
17
  end
16
18
 
17
19
  def tasks
18
- @_tasks ||= filtered_collection.sort_by(&:file).slice(offset, count) || []
20
+ @_tasks ||= sorted_collection.slice(offset, count) || []
19
21
  end
20
22
 
21
23
  def filtered_collection
@@ -40,6 +42,25 @@ module Sidekiq
40
42
  requested_page.positive? ? requested_page : 1
41
43
  end
42
44
 
45
+ def sort
46
+ SORT_COLUMNS.include?(params[:sort]) ? params[:sort] : SORT_COLUMNS.first
47
+ end
48
+
49
+ def direction
50
+ SORT_DIRECTIONS.include?(params[:direction]) ? params[:direction] : SORT_DIRECTIONS.first
51
+ end
52
+
53
+ def toggle_direction(column)
54
+ return "asc" unless sort == column
55
+ return "desc" if direction == "asc"
56
+
57
+ nil
58
+ end
59
+
60
+ def sorted_by?(column)
61
+ sort == column
62
+ end
63
+
43
64
  def total_pages
44
65
  (filtered_collection.size.to_f / count).ceil
45
66
  end
@@ -47,6 +68,28 @@ module Sidekiq
47
68
  def offset
48
69
  (page - 1) * count
49
70
  end
71
+
72
+ private
73
+
74
+ def sorted_collection
75
+ sorted = filtered_collection.sort_by { |task| sort_value(task) }
76
+ direction == "desc" ? sorted.reverse : sorted
77
+ end
78
+
79
+ def sort_value(task)
80
+ case sort
81
+ when "name"
82
+ task.name.to_s.downcase
83
+ when "last_enqueued"
84
+ sort_value_for_time(task.last_enqueue_at)
85
+ end
86
+ end
87
+
88
+ def sort_value_for_time(time)
89
+ return time.to_f if time
90
+
91
+ direction == "asc" ? Float::INFINITY : -Float::INFINITY
92
+ end
50
93
  end
51
94
  end
52
95
  end
data/lib/sidekiq/tasks.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "rake"
4
4
 
5
5
  require_relative "tasks/errors"
6
+ require_relative "tasks/storage"
6
7
  require_relative "tasks/strategies"
7
8
  require_relative "tasks/validations"
8
9
  require_relative "tasks/version"
@@ -12,7 +13,6 @@ module Sidekiq
12
13
  autoload :Config, "sidekiq/tasks/config"
13
14
  autoload :Job, "sidekiq/tasks/job"
14
15
  autoload :Set, "sidekiq/tasks/set"
15
- autoload :Storage, "sidekiq/tasks/storage"
16
16
  autoload :Task, "sidekiq/tasks/task"
17
17
  autoload :TaskMetadata, "sidekiq/tasks/task_metadata"
18
18
 
@@ -22,6 +22,7 @@
22
22
 
23
23
  .st-table-container {
24
24
  overflow: auto;
25
+ max-width: 100%;
25
26
  }
26
27
 
27
28
  .st-table {
@@ -64,9 +65,42 @@
64
65
  border-radius: 2px;
65
66
  padding: 0.2em 0.4em;
66
67
  border-radius: 0.2em;
68
+ display: block;
69
+ max-width: 200px;
70
+ overflow: auto;
67
71
  }
68
72
 
69
73
  .st-table .st-desc-cell {
70
74
  white-space: pre-wrap;
71
75
  word-wrap: break-word;
72
76
  }
77
+
78
+ .st-sortable {
79
+ color: inherit;
80
+ text-decoration: none;
81
+ cursor: pointer;
82
+ }
83
+
84
+ .st-sortable:hover {
85
+ text-decoration: underline;
86
+ }
87
+
88
+ .st-sorted-asc::after,
89
+ .st-sorted-desc::after {
90
+ content: "";
91
+ display: inline-block;
92
+ margin-left: 5px;
93
+ width: 0;
94
+ height: 0;
95
+ vertical-align: middle;
96
+ border-left: 4px solid transparent;
97
+ border-right: 4px solid transparent;
98
+ }
99
+
100
+ .st-sorted-asc::after {
101
+ border-bottom: 4px solid currentColor;
102
+ }
103
+
104
+ .st-sorted-desc::after {
105
+ border-top: 4px solid currentColor;
106
+ }
data/web/locales/en.yml CHANGED
@@ -25,4 +25,5 @@ en:
25
25
  running: "Running"
26
26
  success: "Success"
27
27
  failure: "Failure"
28
+ enqueued_by: "Enqueued by"
28
29
  task_time: "%m/%d/%y %H:%M:%S"
data/web/locales/fr.yml CHANGED
@@ -25,4 +25,5 @@ fr:
25
25
  running: "En cours"
26
26
  success: "Succès"
27
27
  failure: "Echec"
28
+ enqueued_by: "Lancée par"
28
29
  task_time: "%d/%m/%y %H:%M:%S"
@@ -1,14 +1,14 @@
1
- <% if search.total_pages > 1 %>
1
+ <% if total_pages > 1 %>
2
2
  <div class="st-pagination-wrapper">
3
3
  <div>
4
4
  <small class="st-text-primary">
5
- <%= search.tasks.size %> / <%= Sidekiq::Tasks.tasks.size %>
5
+ <%= displayed_count %> / <%= total_count %>
6
6
  </small>
7
7
  </div>
8
8
 
9
9
  <ul class="st-pagination">
10
- <% Sidekiq::Tasks::Web::Pagination.new(search.page, search.total_pages).links.each do |link| %>
11
- <%= pagination_link(root_path, link, search) %>
10
+ <% Sidekiq::Tasks::Web::Pagination.new(page, total_pages).links.each do |link| %>
11
+ <%= build_pagination_link(link, base_url) %>
12
12
  <% end %>
13
13
  </ul>
14
14
  </div>
data/web/views/task.erb CHANGED
@@ -6,7 +6,7 @@
6
6
  <% end %>
7
7
  <% else %>
8
8
  <% add_to_head do %>
9
- <link href="<%= root_path %>tasks/css/ext.css" media="screen" rel="stylesheet" type="text/css"/>
9
+ <link href="<%= root_path %>tasks/css/ext.css" media="screen" rel="stylesheet" type="text/css">
10
10
  <% end %>
11
11
  <% end %>
12
12
 
@@ -42,7 +42,7 @@
42
42
  </header>
43
43
 
44
44
  <div class="st-table-container">
45
- <% if task.history.empty? %>
45
+ <% if history_entries.empty? %>
46
46
  <p><%= t("no_history") %></p>
47
47
  <% else %>
48
48
  <table class="st-table">
@@ -50,6 +50,9 @@
50
50
  <tr>
51
51
  <th><%= t("jid") %></th>
52
52
  <th><%= t("args") %></th>
53
+ <% if Sidekiq::Tasks.config.current_user %>
54
+ <th><%= t("enqueued_by") %></th>
55
+ <% end %>
53
56
  <th><%= t("enqueued") %></th>
54
57
  <th><%= t("executed") %></th>
55
58
  <th><%= t("duration") %></th>
@@ -57,12 +60,17 @@
57
60
  </tr>
58
61
  </thead>
59
62
  <tbody>
60
- <% task.history.each do |jid_history| %>
63
+ <% history_entries.each do |jid_history| %>
61
64
  <tr>
62
65
  <td><%= jid_history["jid"] %></td>
63
66
  <td>
64
67
  <code><%= jid_history["args"] %></code>
65
68
  </td>
69
+ <% if Sidekiq::Tasks.config.current_user %>
70
+ <td>
71
+ <code><%= jid_history["user"] || "-" %></code>
72
+ </td>
73
+ <% end %>
66
74
  <td>
67
75
  <%= jid_history["enqueued_at"] ? jid_history["enqueued_at"].strftime(t("task_time")) : "-" %>
68
76
  </td>
@@ -77,13 +85,24 @@
77
85
  :span,
78
86
  t(task_status(jid_history).to_s).capitalize,
79
87
  class: "st-status-badge #{task_status(jid_history)}",
80
- "data-tooltip": jid_history["error"],
88
+ "data-tooltip": jid_history["error"]
81
89
  ) %>
82
90
  </td>
83
91
  </tr>
84
92
  <% end %>
85
93
  </tbody>
86
94
  </table>
95
+
96
+ <%= erb(
97
+ read_view(:_pagination),
98
+ locals: {
99
+ displayed_count: history_entries.size,
100
+ total_count: history_total_count,
101
+ page: history_page,
102
+ total_pages: history_total_pages,
103
+ base_url: task_url(root_path, task),
104
+ }
105
+ ) %>
87
106
  <% end %>
88
107
  </div>
89
108
 
@@ -100,7 +119,7 @@
100
119
  <% task.args.each do |arg| %>
101
120
  <div class="st-form-group">
102
121
  <label for="<%= arg %>" class="st-label"><%= arg %></label>
103
- <input type="text" id="<%= arg %>" class="st-input" name="args[<%= arg %>]"/>
122
+ <input type="text" id="<%= arg %>" class="st-input" name="args[<%= arg %>]" autocomplete="off">
104
123
  </div>
105
124
  <% end %>
106
125
  </div>
@@ -109,7 +128,15 @@
109
128
  <label for="envConfirmationInput" class="st-label">
110
129
  <%= t("env_confirmation", current_env: current_env) %>
111
130
  </label>
112
- <input type="text" id="envConfirmationInput" class="st-input" name="env_confirmation" data-current-env="<%= current_env %>" required/>
131
+ <input
132
+ type="text"
133
+ id="envConfirmationInput"
134
+ class="st-input"
135
+ name="env_confirmation"
136
+ data-current-env="<%= current_env %>"
137
+ autocomplete="off"
138
+ required
139
+ >
113
140
  </div>
114
141
 
115
142
  <button type="submit" class="st-button" id="submitBtn" disabled>
data/web/views/tasks.erb CHANGED
@@ -6,18 +6,22 @@
6
6
  <% end %>
7
7
  <% else %>
8
8
  <% add_to_head do %>
9
- <link href="<%= root_path %>tasks/css/ext.css" media="screen" rel="stylesheet" type="text/css"/>
9
+ <link href="<%= root_path %>tasks/css/ext.css" media="screen" rel="stylesheet" type="text/css">
10
10
  <% end %>
11
11
  <% end %>
12
12
 
13
13
  <header class="st-header">
14
14
  <form action="<%= root_path %>tasks" method="get" class="st-search-form">
15
+ <input type="hidden" name="sort" value="<%= @search.sort %>">
16
+ <input type="hidden" name="direction" value="<%= @search.direction %>">
17
+
15
18
  <input
16
19
  name="filter"
17
20
  class="st-input"
18
21
  type="text"
19
22
  value="<%= Rack::Utils.escape_html(@search.filter.to_s) %>"
20
23
  placeholder="<%= t("search") %>"
24
+ autocomplete="off"
21
25
  >
22
26
 
23
27
  <select name="count" class="st-select">
@@ -41,9 +45,23 @@
41
45
  <table class="st-table">
42
46
  <thead>
43
47
  <tr>
44
- <th><%= t("name") %></th>
48
+ <th>
49
+ <a
50
+ href="<%= sort_header_url(@search, root_path, "name") %>"
51
+ class="<%= sort_header_classes(@search, "name") %>"
52
+ >
53
+ <%= t("name") %>
54
+ </a>
55
+ </th>
45
56
  <th><%= t("description") %></th>
46
- <th><%= t("last_enqueued") %></th>
57
+ <th>
58
+ <a
59
+ href="<%= sort_header_url(@search, root_path, "last_enqueued") %>"
60
+ class="<%= sort_header_classes(@search, "last_enqueued") %>"
61
+ >
62
+ <%= t("last_enqueued") %>
63
+ </a>
64
+ </th>
47
65
  </tr>
48
66
  </thead>
49
67
  <tbody>
@@ -62,6 +80,15 @@
62
80
  </tbody>
63
81
  </table>
64
82
 
65
- <%= erb(read_view(:_pagination), locals: {search: @search}) %>
83
+ <%= erb(
84
+ read_view(:_pagination),
85
+ locals: {
86
+ displayed_count: @search.tasks.size,
87
+ total_count: Sidekiq::Tasks.tasks.size,
88
+ page: @search.page,
89
+ total_pages: @search.total_pages,
90
+ base_url: pagination_base_url(@search, root_path),
91
+ }
92
+ ) %>
66
93
  <% end %>
67
94
  </div>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor
@@ -236,6 +236,7 @@ files:
236
236
  - LICENSE.txt
237
237
  - README.md
238
238
  - Rakefile
239
+ - docs/custom_storage.md
239
240
  - docs/task.png
240
241
  - lib/sidekiq-tasks.rb
241
242
  - lib/sidekiq/tasks.rb
@@ -244,6 +245,8 @@ files:
244
245
  - lib/sidekiq/tasks/job.rb
245
246
  - lib/sidekiq/tasks/set.rb
246
247
  - lib/sidekiq/tasks/storage.rb
248
+ - lib/sidekiq/tasks/storage/base.rb
249
+ - lib/sidekiq/tasks/storage/redis.rb
247
250
  - lib/sidekiq/tasks/strategies.rb
248
251
  - lib/sidekiq/tasks/strategies/base.rb
249
252
  - lib/sidekiq/tasks/strategies/rake_task.rb
@@ -260,6 +263,7 @@ files:
260
263
  - lib/sidekiq/tasks/web/extension.rb
261
264
  - lib/sidekiq/tasks/web/helpers/application_helper.rb
262
265
  - lib/sidekiq/tasks/web/helpers/pagination_helper.rb
266
+ - lib/sidekiq/tasks/web/helpers/sort_helper.rb
263
267
  - lib/sidekiq/tasks/web/helpers/tag_helper.rb
264
268
  - lib/sidekiq/tasks/web/helpers/task_helper.rb
265
269
  - lib/sidekiq/tasks/web/pagination.rb