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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +12 -0
- data/README.md +17 -2
- data/docs/task.png +0 -0
- data/lib/sidekiq/tasks/set.rb +6 -1
- data/lib/sidekiq/tasks/storage.rb +36 -20
- data/lib/sidekiq/tasks/strategies/rules/enable_with_comment.rb +34 -7
- data/lib/sidekiq/tasks/task.rb +11 -3
- data/lib/sidekiq/tasks/version.rb +1 -1
- data/lib/sidekiq/tasks/web/extension.rb +5 -3
- data/lib/sidekiq/tasks/web/helpers/application_helper.rb +12 -0
- data/lib/sidekiq/tasks/web/helpers/pagination_helper.rb +1 -12
- data/lib/sidekiq/tasks/web/helpers/tag_helper.rb +24 -0
- data/lib/sidekiq/tasks/web/helpers/task_helper.rb +26 -0
- data/lib/sidekiq/tasks/web.rb +1 -0
- data/web/assets/tasks/css/components/status_badges.css +72 -0
- data/web/assets/tasks/css/components/tables.css +19 -0
- data/web/assets/tasks/css/components/tooltips.css +42 -0
- data/web/assets/tasks/css/ext.css +2 -1
- data/web/assets/tasks/js/tooltips_manager.js +51 -0
- data/web/locales/en.yml +6 -0
- data/web/locales/fr.yml +7 -1
- data/web/views/task.erb +48 -25
- metadata +8 -7
- /data/lib/{sidekiq/sidekiq-tasks.rb → sidekiq-tasks.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d04a49b7d050c6c355d565b6fce32e27cf0682154596da6d3e3f0f041762b957
|
4
|
+
data.tar.gz: bb6b5b81369db48617a240463734bca35c9a396dc170578f457e7c6ddadaf842
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
|
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
|
data/lib/sidekiq/tasks/set.rb
CHANGED
@@ -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)
|
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
|
-
|
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
|
-
|
33
|
-
|
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.
|
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
|
-
|
56
|
-
|
57
|
-
|
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.
|
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.
|
82
|
+
[nil, ""].include?(timestamp) ? nil : Time.at(timestamp.to_f)
|
70
83
|
end
|
71
84
|
|
72
|
-
def
|
85
|
+
def update_history_entry(jid)
|
73
86
|
Sidekiq.redis do |conn|
|
74
|
-
conn.lrange(history_key, 0, -1)
|
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
|
-
|
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
|
-
|
7
|
+
file, line_number = task.locations.first.split(":")
|
8
|
+
line_number = line_number.to_i
|
8
9
|
|
9
|
-
|
10
|
+
lines = read_file_lines(file)
|
10
11
|
|
11
|
-
|
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
|
23
|
-
file
|
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
|
data/lib/sidekiq/tasks/task.rb
CHANGED
@@ -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, :
|
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
|
-
|
32
|
+
storage.store_execution(jid, "executed_at")
|
33
33
|
|
34
|
-
|
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
|
@@ -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(
|
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
|
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,
|
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
|
data/lib/sidekiq/tasks/web.rb
CHANGED
@@ -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
|
+
}
|
@@ -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: "
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
<
|
43
|
-
|
44
|
-
<
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
<
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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.
|
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-
|
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
|
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.
|
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
|