sidekiq-tasks 0.1.3 → 0.1.5

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: b815ea42b8cc57ba73f3711e735fda709755b75c282e200dc18ab5287eadfdd6
4
- data.tar.gz: 3e3e652e28ae9ee632452e89bab64bf34db9d4961a6b1eb7e5ff05914cfca3e3
3
+ metadata.gz: d04a49b7d050c6c355d565b6fce32e27cf0682154596da6d3e3f0f041762b957
4
+ data.tar.gz: bb6b5b81369db48617a240463734bca35c9a396dc170578f457e7c6ddadaf842
5
5
  SHA512:
6
- metadata.gz: 53cb0abf54140641134acaf350bf51bf989f3b8d454359fe2b3f32a9ad1937d6257b06de3a000e602f3a63a217e475c4229c23712a88987731a27cab73485d29
7
- data.tar.gz: 854ef40802df6e3d3cd6eda1eae39df3aff31efc8d1e82c69ae201b1ca3576060fcd9dd3bb71e7bfc6b1c8275f6db7169c61fcaabd84da18be121b3a9c0e55d6
6
+ metadata.gz: 7ef504991cfaacc4ed522e923e5305075507d44ce0f5afd46b74e16f95cdc3915d854ca3bed6ab0fbf440d7fa5ef2672f8fcb64e2eca38a7c68c355b84b46a06
7
+ data.tar.gz: 8fda83fc3182b73e19a9a2c718366afde97667763705270ae7bdd02b9fc9a2a9cc1ad17dec341a78c204d301c583b235fe302e2c0932475aca3a5d9a0e113655
data/.rubocop.yml CHANGED
@@ -33,7 +33,7 @@ Metrics/PerceivedComplexity:
33
33
 
34
34
  Naming/FileName:
35
35
  Exclude:
36
- - lib/sidekiq/sidekiq-tasks.rb
36
+ - lib/sidekiq-tasks.rb
37
37
 
38
38
  Naming/MemoizedInstanceVariableName:
39
39
  Enabled: false
@@ -44,6 +44,9 @@ Style/Documentation:
44
44
  Style/FrozenStringLiteralComment:
45
45
  Enabled: false
46
46
 
47
+ Style/RescueStandardError:
48
+ Enabled: false
49
+
47
50
  Style/GlobalVars:
48
51
  Exclude:
49
52
  - spec/**/*
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## Changelog
2
2
 
3
+ ### [0.1.5] - 2025-05-04
4
+
5
+ - Add duration, status and error reports to history.
6
+ - Fix Code Climate report by updating CI runner from `ubuntu-20.04` to `ubuntu-22.04`.
7
+
8
+ ### [0.1.4] - 2025-03-23
9
+
10
+ - Fix gem load error by moving the entrypoint to the correct path.
11
+ - Support enabling/disabling all tasks in a namespace with a magic comment.
12
+ - Improve task search to allow more flexible and intuitive matching.
13
+ - Fix deprecation warning by avoiding direct access to `params` (Sidekiq 8 compatibility).
14
+
3
15
  ### [0.1.3] - 2025-03-22
4
16
 
5
17
  - Change required Ruby version to 3.0.0.
data/README.md CHANGED
@@ -47,7 +47,7 @@ By default, it comes with the `Sidekiq::Tasks::Strategies::RakeTask` strategy, w
47
47
  > - **`TaskFromLib`** Only tasks from the `lib` folder are loaded.
48
48
  > - **`EnableWithComment`** Only tasks explicitly enabled with a magic comment are loaded.
49
49
 
50
- Example of an available task in `lib/tasks/my_task.rake`:
50
+ Example of an enabled task in `lib/tasks/my_task.rake`:
51
51
 
52
52
  ```ruby
53
53
  # sidekiq-tasks:enable
@@ -56,7 +56,22 @@ task :my_task do
56
56
  end
57
57
  ```
58
58
 
59
- You can also use `DisableWithComment` rule to selectively **exclude** tasks. (see [strategies configuration](#strategies-configuration))
59
+ Enable all tasks within a namespace:
60
+
61
+ ```ruby
62
+ # sidekiq-tasks:enable
63
+ namespace :my_namespace do
64
+ task :my_task do
65
+ puts "my_task"
66
+ end
67
+
68
+ task :another_task do
69
+ puts "another_task"
70
+ end
71
+ end
72
+ ```
73
+
74
+ You can also use `DisableWithComment` rule to selectively **disable** tasks. (see [strategies configuration](#strategies-configuration))
60
75
  It works similarly to `EnableWithComment`, but with inverted logic. Example of a disabled task:
61
76
 
62
77
  ```ruby
data/docs/task.png CHANGED
Binary file
@@ -9,7 +9,12 @@ module Sidekiq
9
9
 
10
10
  def self.match?(object, attributes)
11
11
  attributes.any? do |attribute, value|
12
- [nil, ""].include?(value) || object.public_send(attribute)&.match?(value)
12
+ next true if [nil, ""].include?(value)
13
+
14
+ object_value = object.public_send(attribute).to_s.downcase.gsub(/[^a-z0-9]/, "")
15
+ search_fragments = value.to_s.downcase.gsub(/[^a-z0-9\s]/, "").split
16
+
17
+ search_fragments.all? { |fragment| object_value.include?(fragment) }
13
18
  end
14
19
  end
15
20
 
@@ -3,6 +3,7 @@ module Sidekiq
3
3
  class Storage
4
4
  JID_PREFIX = "task".freeze
5
5
  HISTORY_LIMIT = 10
6
+ ERROR_MESSAGE_MAX_LENGTH = 255
6
7
 
7
8
  attr_reader :task_name
8
9
 
@@ -22,60 +23,75 @@ module Sidekiq
22
23
  stored_time("last_enqueue_at")
23
24
  end
24
25
 
25
- def last_execution_at
26
- stored_time("last_execution_at")
27
- end
28
-
29
26
  def history
30
- redis_history = Sidekiq.redis { |conn| conn.lrange(history_key, 0, -1) }&.map do |raw|
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|
31
32
  entry = Sidekiq.load_json(raw)
32
- entry["enqueued_at"] = Time.at(entry["enqueued_at"]) if entry["enqueued_at"]
33
- entry["executed_at"] = Time.at(entry["executed_at"]) if entry["executed_at"]
33
+ %w[enqueued_at executed_at finished_at].each do |key|
34
+ entry[key] = Time.at(entry[key]) if entry[key]
35
+ end
34
36
  entry
35
37
  end
36
-
37
- redis_history || []
38
38
  end
39
39
 
40
40
  def store_history(jid, task_args, time)
41
41
  Sidekiq.redis do |conn|
42
- task_trace = {jid: jid, name: task_name, args: task_args, enqueued_at: time.to_i}
42
+ task_trace = {jid: jid, name: task_name, args: task_args, enqueued_at: time.to_f}
43
43
  conn.lpush(history_key, Sidekiq.dump_json(task_trace))
44
44
  conn.ltrim(history_key, 0, HISTORY_LIMIT - 1)
45
45
  end
46
46
  end
47
47
 
48
48
  def store_enqueue(jid, args)
49
- time = Time.now
49
+ time = Time.now.to_f
50
50
  store_time(time, "last_enqueue_at")
51
51
  store_history(jid, args, time)
52
52
  end
53
53
 
54
- def store_execution(jid)
55
- time = Time.now
56
- store_time(time, "last_execution_at")
57
- store_execution_time_in_history(jid, time)
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
58
65
  end
59
66
 
60
67
  private
61
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
+
62
75
  def store_time(time, time_key)
63
- Sidekiq.redis { |conn| conn.hset(jid_key, time_key, time.to_i) }
76
+ Sidekiq.redis { |conn| conn.hset(jid_key, time_key, time.to_f) }
64
77
  end
65
78
 
66
79
  def stored_time(time_key)
67
80
  timestamp = Sidekiq.redis { |conn| conn.hget(jid_key, time_key) }
68
81
 
69
- [nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_i)
82
+ [nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_f)
70
83
  end
71
84
 
72
- def store_execution_time_in_history(jid, time)
85
+ def update_history_entry(jid)
73
86
  Sidekiq.redis do |conn|
74
- conn.lrange(history_key, 0, -1).each_with_index do |raw, index|
87
+ entries = conn.lrange(history_key, 0, -1)
88
+
89
+ entries.each_with_index do |raw, index|
75
90
  entry = Sidekiq.load_json(raw)
76
91
  next unless entry["jid"] == jid
77
92
 
78
- conn.lset(history_key, index, Sidekiq.dump_json(entry.merge("executed_at" => time.to_i)))
93
+ updated_entry = yield(entry)
94
+ conn.lset(history_key, index, Sidekiq.dump_json(updated_entry))
79
95
  break
80
96
  end
81
97
  end
@@ -4,11 +4,19 @@ module Sidekiq
4
4
  module Rules
5
5
  class EnableWithComment < Base
6
6
  def respected?(task)
7
- lines = relevant_lines(task)
7
+ file, line_number = task.locations.first.split(":")
8
+ line_number = line_number.to_i
8
9
 
9
- return false if lines.first.match?(/namespace/)
10
+ lines = read_file_lines(file)
10
11
 
11
- lines.any? { |line| line.strip.match?(magic_comment_regex) }
12
+ return false if lines.nil?
13
+
14
+ return true if task_has_magic_comment?(lines, line_number)
15
+
16
+ namespace_line_index = find_namespace_line_index(lines, task)
17
+ return false unless namespace_line_index
18
+
19
+ namespace_has_magic_comment?(lines, namespace_line_index)
12
20
  end
13
21
 
14
22
  protected
@@ -19,13 +27,32 @@ module Sidekiq
19
27
 
20
28
  private
21
29
 
22
- def relevant_lines(task)
23
- file, start_line = task.locations.first.split(":")
24
- start_line_counting_desc = start_line.to_i > 2 ? start_line.to_i - 3 : 0
25
- File.read(file).split("\n")[start_line_counting_desc..start_line_counting_desc + 1].reverse
30
+ def read_file_lines(file)
31
+ File.read(file).split("\n")
26
32
  rescue Errno::ENOENT
27
33
  raise ArgumentError, "File '#{file}' not found"
28
34
  end
35
+
36
+ def task_has_magic_comment?(lines, task_line)
37
+ context_range = (task_line - 3..task_line).to_a.select { |i| i >= 0 }
38
+ context_range.reverse.any? do |i|
39
+ lines[i]&.strip&.match?(magic_comment_regex)
40
+ end
41
+ end
42
+
43
+ def find_namespace_line_index(lines, task)
44
+ namespace = namespace_name(task)
45
+ lines.find_index { |line| line.strip.match?(/^namespace\s+:#{Regexp.escape(namespace)}/) }
46
+ end
47
+
48
+ def namespace_has_magic_comment?(lines, namespace_line_index)
49
+ comment_line = lines[namespace_line_index - 1]&.strip
50
+ comment_line&.match?(magic_comment_regex)
51
+ end
52
+
53
+ def namespace_name(task)
54
+ task.name.split(":").first
55
+ end
29
56
  end
30
57
  end
31
58
  end
@@ -7,7 +7,7 @@ module Sidekiq
7
7
  include Sidekiq::Tasks::Validations
8
8
 
9
9
  def_delegators :metadata, :name, :desc, :file, :args
10
- def_delegators :storage, :last_enqueue_at, :last_execution_at, :history
10
+ def_delegators :storage, :last_enqueue_at, :history
11
11
 
12
12
  attr_reader :metadata, :strategy
13
13
 
@@ -29,9 +29,17 @@ module Sidekiq
29
29
  end
30
30
 
31
31
  def execute(params = {}, jid: nil)
32
- strategy.execute_task(name, params)
32
+ storage.store_execution(jid, "executed_at")
33
33
 
34
- storage.store_execution(jid)
34
+ begin
35
+ strategy.execute_task(name, params)
36
+ rescue => e
37
+ storage.store_execution(jid, "finished_at")
38
+ storage.store_execution_error(jid, e)
39
+ raise
40
+ end
41
+
42
+ storage.store_execution(jid, "finished_at")
35
43
  end
36
44
 
37
45
  def storage
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Tasks
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.5"
6
6
  end
7
7
  end
@@ -1,4 +1,5 @@
1
1
  require "sidekiq/tasks/web/helpers/application_helper"
2
+ require "sidekiq/tasks/web/helpers/tag_helper"
2
3
  require "sidekiq/tasks/web/helpers/task_helper"
3
4
  require "sidekiq/tasks/web/helpers/pagination_helper"
4
5
  require "sidekiq/tasks/web/search"
@@ -11,11 +12,12 @@ module Sidekiq
11
12
  class Extension
12
13
  def self.registered(app)
13
14
  app.helpers(Sidekiq::Tasks::Web::Helpers::ApplicationHelper)
15
+ app.helpers(Sidekiq::Tasks::Web::Helpers::TagHelper)
14
16
  app.helpers(Sidekiq::Tasks::Web::Helpers::TaskHelper)
15
17
  app.helpers(Sidekiq::Tasks::Web::Helpers::PaginationHelper)
16
18
 
17
19
  app.get "/tasks" do
18
- @search = Sidekiq::Tasks::Web::Search.new(params.transform_keys(&:to_sym))
20
+ @search = Sidekiq::Tasks::Web::Search.new(fetch_params(:count, :page, :filter))
19
21
 
20
22
  erb(read_view(:tasks), locals: {search: @search})
21
23
  end
@@ -29,12 +31,12 @@ module Sidekiq
29
31
  end
30
32
 
31
33
  app.post "/tasks/:name/enqueue" do
32
- if params["env_confirmation"] != current_env
34
+ if fetch_param("env_confirmation") != current_env
33
35
  throw :halt, [400, {Rack::CONTENT_TYPE => "text/plain"}, ["Invalid confirm"]]
34
36
  end
35
37
 
36
38
  task = find_task!(env["rack.route_params"][:name])
37
- args = Sidekiq::Tasks::Web::Params.new(task, params["args"]).permit!
39
+ args = Sidekiq::Tasks::Web::Params.new(task, fetch_param("args")).permit!
38
40
 
39
41
  task.enqueue(args)
40
42
 
@@ -12,6 +12,18 @@ module Sidekiq
12
12
  def current_env
13
13
  ENV["RAILS_ENV"] || ENV["RACK_ENV"]
14
14
  end
15
+
16
+ def fetch_param(key)
17
+ if Sidekiq::Tasks::Web::SIDEKIQ_GTE_8_0_0
18
+ url_params(key.to_s)
19
+ else
20
+ params[key.to_s]
21
+ end
22
+ end
23
+
24
+ def fetch_params(*keys)
25
+ keys.to_h { |key| [key.to_sym, fetch_param(key)] }
26
+ end
15
27
  end
16
28
  end
17
29
  end
@@ -4,6 +4,7 @@ module Sidekiq
4
4
  module Helpers
5
5
  module PaginationHelper
6
6
  extend self
7
+ include Sidekiq::Tasks::Web::Helpers::TagHelper
7
8
 
8
9
  def pagination_link(root_path, link, search)
9
10
  build_tag(:li, class: "st-page-item") do
@@ -18,18 +19,6 @@ module Sidekiq
18
19
 
19
20
  private
20
21
 
21
- def build_tag(tag, content = nil, **attributes, &block)
22
- attr_string = attributes.map { |key, value| "#{key}=\"#{value}\"" }.join(" ")
23
-
24
- "<#{tag} #{attr_string}>#{block&.call || content}</#{tag}>"
25
- end
26
-
27
- def build_classes(*classes, **conditions)
28
- condition_classes = conditions.select { |_, value| value }.keys
29
-
30
- (classes + condition_classes).join(" ")
31
- end
32
-
33
22
  def pagination_url(root_path, search, page)
34
23
  "#{root_path}tasks?filter=#{ERB::Util.url_encode(search.filter)}&count=#{search.count}&page=#{page}"
35
24
  end
@@ -0,0 +1,24 @@
1
+ module Sidekiq
2
+ module Tasks
3
+ module Web
4
+ module Helpers
5
+ module TagHelper
6
+ extend self
7
+
8
+ def build_tag(tag, content = nil, **attributes, &block)
9
+ attr_string = attributes.map { |key, value| "#{key}=\"#{value}\"" }.join(" ")
10
+ attr_string = " #{attr_string}" unless attr_string.empty?
11
+
12
+ "<#{tag}#{attr_string}>#{block&.call || content}</#{tag}>"
13
+ end
14
+
15
+ def build_classes(*classes, **conditions)
16
+ condition_classes = conditions.select { |_, value| value }.keys
17
+
18
+ (classes + condition_classes).join(" ")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -4,6 +4,7 @@ module Sidekiq
4
4
  module Helpers
5
5
  module TaskHelper
6
6
  extend self
7
+ include Sidekiq::Tasks::Web::Helpers::TagHelper
7
8
 
8
9
  def parameterize_task_name(task_name)
9
10
  task_name.gsub(":", "-")
@@ -22,6 +23,31 @@ module Sidekiq
22
23
  def task_url(root_path, task)
23
24
  "#{root_path}tasks/#{parameterize_task_name(task.name)}"
24
25
  end
26
+
27
+ def task_status(jid_history)
28
+ if jid_history["error"]
29
+ :failure
30
+ elsif jid_history["finished_at"]
31
+ :success
32
+ elsif jid_history["executed_at"]
33
+ :running
34
+ else
35
+ :pending
36
+ end
37
+ end
38
+
39
+ def format_task_duration(start_time, end_time)
40
+ return "-" unless start_time && end_time
41
+
42
+ duration_in_milliseconds = ((end_time - start_time) * 1000).to_i
43
+
44
+ if duration_in_milliseconds >= 1000
45
+ duration_in_seconds = (duration_in_milliseconds / 1000.0).round
46
+ "#{[duration_in_seconds, 1].max}s"
47
+ else
48
+ "#{[duration_in_milliseconds, 1].max}ms"
49
+ end
50
+ end
25
51
  end
26
52
  end
27
53
  end
@@ -7,6 +7,7 @@ module Sidekiq
7
7
 
8
8
  ROOT = File.expand_path("../../../web", File.dirname(__FILE__))
9
9
  SIDEKIQ_GTE_7_3_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("7.3.0")
10
+ SIDEKIQ_GTE_8_0_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new("8.0.0")
10
11
  end
11
12
  end
12
13
  end
@@ -0,0 +1,72 @@
1
+ :root {
2
+ --badge-success-text: #22713d;
3
+ --badge-success-bg: #dff5e3;
4
+ --badge-success-border: #b5e2c4;
5
+
6
+ --badge-failure-text: #a82a34;
7
+ --badge-failure-bg: #fbe3e5;
8
+ --badge-failure-border: #f5b7bc;
9
+
10
+ --badge-running-text: #0b5394;
11
+ --badge-running-bg: #dceeff;
12
+ --badge-running-border: #a9d4f2;
13
+
14
+ --badge-pending-text: #8a6d1d;
15
+ --badge-pending-bg: #fff8dc;
16
+ --badge-pending-border: #f5e6ab;
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --badge-success-text: #b5e2c4;
22
+ --badge-success-bg: #1a3b28;
23
+ --badge-success-border: #2f5b3f;
24
+
25
+ --badge-failure-text: #f5b7bc;
26
+ --badge-failure-bg: #3e1b1d;
27
+ --badge-failure-border: #7a2e35;
28
+
29
+ --badge-running-text: #a9d4f2;
30
+ --badge-running-bg: #1a2c3e;
31
+ --badge-running-border: #345c7f;
32
+
33
+ --badge-pending-text: #f5e6ab;
34
+ --badge-pending-bg: #3e351a;
35
+ --badge-pending-border: #7a6a2e;
36
+ }
37
+ }
38
+
39
+ .st-status-badge {
40
+ display: inline-block;
41
+ padding: 3px 10px;
42
+ border-radius: 999px;
43
+ font-size: 11px;
44
+ font-weight: 500;
45
+ cursor: default;
46
+ border: 1px solid transparent;
47
+ white-space: nowrap;
48
+ }
49
+
50
+ .st-status-badge.success {
51
+ color: var(--badge-success-text);
52
+ background-color: var(--badge-success-bg);
53
+ border-color: var(--badge-success-border);
54
+ }
55
+
56
+ .st-status-badge.failure {
57
+ color: var(--badge-failure-text);
58
+ background-color: var(--badge-failure-bg);
59
+ border-color: var(--badge-failure-border);
60
+ }
61
+
62
+ .st-status-badge.running {
63
+ color: var(--badge-running-text);
64
+ background-color: var(--badge-running-bg);
65
+ border-color: var(--badge-running-border);
66
+ }
67
+
68
+ .st-status-badge.pending {
69
+ color: var(--badge-pending-text);
70
+ background-color: var(--badge-pending-bg);
71
+ border-color: var(--badge-pending-border);
72
+ }
@@ -5,6 +5,8 @@
5
5
  --st-table-odd-bg: #fcfcfc;
6
6
  --st-table-even-bg: transparent;
7
7
  --st-table-header-bg: transparent;
8
+ --st-table-code-bg: #fcfcfc;
9
+ --st-table-code-text: #6c6869;
8
10
  }
9
11
 
10
12
  @media (prefers-color-scheme: dark) {
@@ -18,6 +20,10 @@
18
20
  }
19
21
  }
20
22
 
23
+ .st-table-container {
24
+ overflow: auto;
25
+ }
26
+
21
27
  .st-table {
22
28
  background-color: var(--st-table-bg);
23
29
  color: var(--st-table-text);
@@ -29,8 +35,13 @@
29
35
  .st-table thead > tr > th,
30
36
  .st-table tbody > tr > th,
31
37
  .st-table tbody > tr > td {
38
+ max-width: 300px;
39
+ overflow: scroll;
40
+ display: table-cell;
41
+ vertical-align: middle;
32
42
  border: 1px solid var(--st-table-border);
33
43
  padding: 7px 10px;
44
+ white-space: nowrap;
34
45
  }
35
46
 
36
47
  .st-table tbody tr:nth-child(odd) {
@@ -46,3 +57,11 @@
46
57
  background-color: var(--st-table-header-bg);
47
58
  }
48
59
 
60
+ .st-table code {
61
+ background-color: var(--st-table-code-bg);
62
+ color: var(--st-table-code-text);
63
+ box-shadow: 0 0 0 1px var(--st-table-border);
64
+ border-radius: 2px;
65
+ padding: 0.2em 0.4em;
66
+ border-radius: 0.2em;
67
+ }
@@ -0,0 +1,42 @@
1
+ :root {
2
+ --st-tooltip-bg: #333;
3
+ --st-tooltip-text: #fff;
4
+ --st-tooltip-shadow: rgba(0, 0, 0, 0.2);
5
+ --st-tooltip-arrow: #333;
6
+ }
7
+
8
+ @media (prefers-color-scheme: dark) {
9
+ :root {
10
+ --st-tooltip-bg: #eee;
11
+ --st-tooltip-text: #111;
12
+ --st-tooltip-shadow: rgba(255, 255, 255, 0.1);
13
+ --st-tooltip-arrow: #eee;
14
+ }
15
+ }
16
+
17
+ .st-tooltip {
18
+ position: absolute;
19
+ background-color: var(--st-tooltip-bg);
20
+ color: var(--st-tooltip-text);
21
+ padding: 6px 10px;
22
+ border-radius: 4px;
23
+ font-size: 11px;
24
+ white-space: pre-wrap;
25
+ max-width: 300px;
26
+ z-index: 10000;
27
+ box-shadow: 0 0 10px var(--st-tooltip-shadow);
28
+ opacity: 0;
29
+ pointer-events: none;
30
+ transition: opacity 0.2s ease;
31
+ }
32
+
33
+ .st-tooltip::after {
34
+ content: "";
35
+ position: absolute;
36
+ top: 100%;
37
+ left: 50%;
38
+ transform: translateX(-50%);
39
+ border-width: 5px;
40
+ border-style: solid;
41
+ border-color: var(--st-tooltip-arrow) transparent transparent transparent;
42
+ }
@@ -4,4 +4,5 @@
4
4
  @import url("components/tables.css");
5
5
  @import url("components/buttons.css");
6
6
  @import url("components/pagination.css");
7
-
7
+ @import url("components/status_badges.css");
8
+ @import url("components/tooltips.css");
@@ -0,0 +1,51 @@
1
+ class TooltipsManager {
2
+ constructor(selector = '[data-tooltip]') {
3
+ this.selector = selector;
4
+ this.tooltip = null;
5
+ }
6
+
7
+ init() {
8
+ this.#createTooltipElement();
9
+
10
+ document.querySelectorAll(this.selector).forEach(element => {
11
+ element.addEventListener('mouseenter', this.#showTooltip.bind(this));
12
+ element.addEventListener('mouseleave', this.#hideTooltip.bind(this));
13
+ });
14
+ }
15
+
16
+ #createTooltipElement() {
17
+ this.tooltip = document.createElement('div');
18
+ this.tooltip.className = 'st-tooltip';
19
+ document.body.appendChild(this.tooltip);
20
+ }
21
+
22
+ #showTooltip(event) {
23
+ const target = event.currentTarget;
24
+ const text = target.getAttribute('data-tooltip');
25
+
26
+ if (!text) return;
27
+
28
+ this.tooltip.textContent = text;
29
+ this.tooltip.style.top = '0px';
30
+ this.tooltip.style.left = '-9999px';
31
+ this.tooltip.style.opacity = '1';
32
+
33
+ requestAnimationFrame(() => {
34
+ const rect = target.getBoundingClientRect();
35
+ const tooltipRect = this.tooltip.getBoundingClientRect();
36
+ const top = rect.top + window.scrollY - tooltipRect.height - 8;
37
+ const left = rect.left + window.scrollX + rect.width / 2 - tooltipRect.width / 2;
38
+
39
+ this.tooltip.style.top = `${top}px`;
40
+ this.tooltip.style.left = `${left}px`;
41
+ });
42
+ }
43
+
44
+ #hideTooltip() {
45
+ this.tooltip.style.opacity = '0';
46
+ }
47
+ }
48
+
49
+ document.addEventListener('DOMContentLoaded', () => {
50
+ new TooltipsManager().init();
51
+ });
data/web/locales/en.yml CHANGED
@@ -13,9 +13,15 @@ en:
13
13
  args: "Arguments"
14
14
  enqueued: "Enqueued"
15
15
  executed: "Executed"
16
+ duration: "Duration"
16
17
  task: "Task"
17
18
  desc: "Description"
18
19
  strategy: "Strategy"
19
20
  run_task: "Run task"
20
21
  enqueue: "Enqueue"
21
22
  env_confirmation: "Please enter '%{current_env}' to confirm."
23
+ pending: "Pending"
24
+ running: "Running"
25
+ success: "Success"
26
+ failure: "Failure"
27
+ task_time: "%m/%d/%y %H:%M:%S"
data/web/locales/fr.yml CHANGED
@@ -12,10 +12,16 @@ fr:
12
12
  jid: "JID"
13
13
  args: "Arguments"
14
14
  enqueued: "Mise en file d'attente"
15
- executed: "Exécuté"
15
+ executed: "Exécutée"
16
+ duration: "Durée"
16
17
  task: "Tâche"
17
18
  desc: "Description"
18
19
  strategy: "Stratégie"
19
20
  run_task: "Exécuter la tâche"
20
21
  enqueue: "Mettre en file d'attente"
21
22
  env_confirmation: "Veuillez saisir '%{current_env}' pour confirmer."
23
+ pending: "En attente"
24
+ running: "En cours"
25
+ success: "Succès"
26
+ failure: "Echec"
27
+ task_time: "%d/%m/%y %H:%M:%S"
data/web/views/task.erb CHANGED
@@ -35,30 +35,51 @@
35
35
  </div>
36
36
  </header>
37
37
 
38
- <% if task.history.empty? %>
39
- <p><%= t("no_history") %></p>
40
- <% else %>
41
- <table class="st-table">
42
- <thead>
43
- <tr>
44
- <th><%= t("jid") %></th>
45
- <th><%= t("args") %></th>
46
- <th><%= t("enqueued") %></th>
47
- <th><%= t("executed") %></th>
48
- </tr>
49
- </thead>
50
- <tbody>
51
- <% task.history.each do |jid_history| %>
52
- <tr>
53
- <td><%= jid_history["jid"] %></td>
54
- <td><%= jid_history["args"] %></td>
55
- <td><%= jid_history["enqueued_at"] ? relative_time(jid_history["enqueued_at"]) : "-" %></td>
56
- <td><%= jid_history["executed_at"] ? relative_time(jid_history["executed_at"]) : "-" %></td>
57
- </tr>
58
- <% end %>
59
- </tbody>
60
- </table>
61
- <% end %>
38
+ <div class="st-table-container">
39
+ <% if task.history.empty? %>
40
+ <p><%= t("no_history") %></p>
41
+ <% else %>
42
+ <table class="st-table">
43
+ <thead>
44
+ <tr>
45
+ <th><%= t("jid") %></th>
46
+ <th><%= t("args") %></th>
47
+ <th><%= t("enqueued") %></th>
48
+ <th><%= t("executed") %></th>
49
+ <th><%= t("duration") %></th>
50
+ <th><%= t("status") %></th>
51
+ </tr>
52
+ </thead>
53
+ <tbody>
54
+ <% task.history.each do |jid_history| %>
55
+ <tr>
56
+ <td><%= jid_history["jid"] %></td>
57
+ <td>
58
+ <code><%= jid_history["args"] %></code>
59
+ </td>
60
+ <td>
61
+ <%= jid_history["enqueued_at"] ? jid_history["enqueued_at"].strftime(t("task_time")) : "-" %>
62
+ </td>
63
+ <td>
64
+ <%= jid_history["executed_at"] ? jid_history["executed_at"].strftime(t("task_time")) : "-" %>
65
+ </td>
66
+ <td>
67
+ <%= format_task_duration(jid_history["enqueued_at"], jid_history["finished_at"]) %>
68
+ </td>
69
+ <td>
70
+ <%= build_tag(
71
+ :span,
72
+ t(task_status(jid_history).to_s).capitalize,
73
+ class: "st-status-badge #{task_status(jid_history)}",
74
+ "data-tooltip": jid_history["error"],
75
+ ) %>
76
+ </td>
77
+ </tr>
78
+ <% end %>
79
+ </tbody>
80
+ </table>
81
+ <% end %>
82
+ </div>
62
83
 
63
84
  <header class="">
64
85
  <div class="">
@@ -85,13 +106,15 @@
85
106
  <input type="text" id="envConfirmationInput" class="st-input" name="env_confirmation" data-current-env="<%= current_env %>" required/>
86
107
  </div>
87
108
 
88
- <button type="submit" class="st-button" id="submitBtn" disabled>
109
+ <button type="submit" class="st-button" id="submitBtn" disabled>
89
110
  <%= t("enqueue") %>
90
111
  </button>
91
112
  </form>
92
113
 
93
114
  <% if Sidekiq::Tasks::Web::SIDEKIQ_GTE_7_3_0 %>
94
115
  <%= script_tag "tasks/js/env_confirmation.js" %>
116
+ <%= script_tag "tasks/js/tooltips_manager.js" %>
95
117
  <% else %>
96
118
  <script type="text/javascript" src="<%= root_path %>tasks/js/env_confirmation.js"></script>
119
+ <script type="text/javascript" src="<%= root_path %>tasks/js/tooltips_manager.js"></script>
97
120
  <% end %>
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-tasks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-03-22 00:00:00.000000000 Z
10
+ date: 2025-05-04 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rake
@@ -238,7 +237,7 @@ files:
238
237
  - README.md
239
238
  - Rakefile
240
239
  - docs/task.png
241
- - lib/sidekiq/sidekiq-tasks.rb
240
+ - lib/sidekiq-tasks.rb
242
241
  - lib/sidekiq/tasks.rb
243
242
  - lib/sidekiq/tasks/config.rb
244
243
  - lib/sidekiq/tasks/errors.rb
@@ -261,6 +260,7 @@ files:
261
260
  - lib/sidekiq/tasks/web/extension.rb
262
261
  - lib/sidekiq/tasks/web/helpers/application_helper.rb
263
262
  - lib/sidekiq/tasks/web/helpers/pagination_helper.rb
263
+ - lib/sidekiq/tasks/web/helpers/tag_helper.rb
264
264
  - lib/sidekiq/tasks/web/helpers/task_helper.rb
265
265
  - lib/sidekiq/tasks/web/pagination.rb
266
266
  - lib/sidekiq/tasks/web/params.rb
@@ -269,11 +269,14 @@ files:
269
269
  - web/assets/tasks/css/components/buttons.css
270
270
  - web/assets/tasks/css/components/forms.css
271
271
  - web/assets/tasks/css/components/pagination.css
272
+ - web/assets/tasks/css/components/status_badges.css
272
273
  - web/assets/tasks/css/components/tables.css
274
+ - web/assets/tasks/css/components/tooltips.css
273
275
  - web/assets/tasks/css/ext.css
274
276
  - web/assets/tasks/css/layouts/header.css
275
277
  - web/assets/tasks/css/utilities.css
276
278
  - web/assets/tasks/js/env_confirmation.js
279
+ - web/assets/tasks/js/tooltips_manager.js
277
280
  - web/locales/en.yml
278
281
  - web/locales/fr.yml
279
282
  - web/views/_pagination.erb
@@ -286,7 +289,6 @@ metadata:
286
289
  homepage_uri: https://github.com/victorauthiat/sidekiq-tasks
287
290
  source_code_uri: https://github.com/victorauthiat/sidekiq-tasks/blob/master
288
291
  changelog_uri: https://github.com/victorauthiat/sidekiq-tasks/blob/master/CHANGELOG.md
289
- post_install_message:
290
292
  rdoc_options: []
291
293
  require_paths:
292
294
  - lib
@@ -301,8 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
301
303
  - !ruby/object:Gem::Version
302
304
  version: '0'
303
305
  requirements: []
304
- rubygems_version: 3.5.22
305
- signing_key:
306
+ rubygems_version: 3.6.2
306
307
  specification_version: 4
307
308
  summary: Sidekiq extension for launching tasks.
308
309
  test_files: []
File without changes