sidekiq-status 0.5.0 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yaml +53 -0
- data/.gitignore +4 -1
- data/.gitlab-ci.yml +17 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +41 -0
- data/README.md +148 -41
- data/Rakefile +2 -0
- data/gemfiles/sidekiq_6.1.gemfile +7 -0
- data/gemfiles/sidekiq_6.x.gemfile +7 -0
- data/gemfiles/sidekiq_7.x.gemfile +7 -0
- data/lib/sidekiq-status/client_middleware.rb +54 -1
- data/lib/sidekiq-status/redis_adapter.rb +18 -0
- data/lib/sidekiq-status/redis_client_adapter.rb +14 -0
- data/lib/sidekiq-status/server_middleware.rb +76 -17
- data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
- data/lib/sidekiq-status/storage.rb +26 -13
- data/lib/sidekiq-status/testing/inline.rb +10 -0
- data/lib/sidekiq-status/version.rb +1 -1
- data/lib/sidekiq-status/web.rb +158 -10
- data/lib/sidekiq-status/worker.rb +12 -5
- data/lib/sidekiq-status.rb +43 -10
- data/sidekiq-status.gemspec +9 -4
- data/spec/environment.rb +1 -0
- data/spec/lib/sidekiq-status/client_middleware_spec.rb +30 -13
- data/spec/lib/sidekiq-status/server_middleware_spec.rb +102 -30
- data/spec/lib/sidekiq-status/web_spec.rb +84 -0
- data/spec/lib/sidekiq-status/worker_spec.rb +21 -2
- data/spec/lib/sidekiq-status_spec.rb +158 -77
- data/spec/spec_helper.rb +104 -24
- data/spec/support/test_jobs.rb +84 -7
- data/web/sidekiq-status-single-web.png +0 -0
- data/web/sidekiq-status-web.png +0 -0
- data/web/views/status.erb +118 -0
- data/web/views/status_not_found.erb +6 -0
- data/web/views/statuses.erb +135 -17
- metadata +102 -16
- data/.travis.yml +0 -2
- data/CHANGELOG +0 -2
data/spec/spec_helper.rb
CHANGED
@@ -1,69 +1,149 @@
|
|
1
1
|
require "rspec"
|
2
|
-
|
3
|
-
require 'celluloid'
|
2
|
+
require 'colorize'
|
4
3
|
require 'sidekiq'
|
4
|
+
|
5
|
+
# Celluloid should only be manually required before Sidekiq versions 4.+
|
6
|
+
require 'sidekiq/version'
|
7
|
+
require 'celluloid' if Gem::Version.new(Sidekiq::VERSION) < Gem::Version.new('4.0')
|
8
|
+
|
5
9
|
require 'sidekiq/processor'
|
6
10
|
require 'sidekiq/manager'
|
7
11
|
require 'sidekiq-status'
|
8
12
|
|
13
|
+
# Clears jobs before every test
|
14
|
+
RSpec.configure do |config|
|
15
|
+
config.before(:each) do
|
16
|
+
Sidekiq.redis { |conn| conn.flushall }
|
17
|
+
client_middleware
|
18
|
+
sleep 0.05
|
19
|
+
end
|
20
|
+
end
|
9
21
|
|
10
22
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
11
23
|
|
12
|
-
|
13
|
-
|
14
|
-
|
24
|
+
# Configures client middleware
|
25
|
+
def client_middleware client_middleware_options = {}
|
26
|
+
Sidekiq.configure_client do |config|
|
27
|
+
Sidekiq::Status.configure_client_middleware config, client_middleware_options
|
15
28
|
end
|
16
29
|
end
|
17
30
|
|
18
|
-
def
|
31
|
+
def redis_thread messages_limit, *channels
|
32
|
+
|
19
33
|
parent = Thread.current
|
20
34
|
thread = Thread.new {
|
21
|
-
|
35
|
+
messages = []
|
22
36
|
Sidekiq.redis do |conn|
|
23
|
-
|
37
|
+
puts "Subscribing to #{channels} for #{messages_limit.to_s.bold} messages".cyan if ENV['DEBUG']
|
38
|
+
conn.subscribe_with_timeout 60, *channels do |on|
|
24
39
|
on.subscribe do |ch, subscriptions|
|
40
|
+
puts "Subscribed to #{ch}".cyan if ENV['DEBUG']
|
25
41
|
if subscriptions == channels.size
|
26
42
|
sleep 0.1 while parent.status != "sleep"
|
27
43
|
parent.run
|
28
44
|
end
|
29
45
|
end
|
30
46
|
on.message do |ch, msg|
|
31
|
-
|
32
|
-
|
47
|
+
puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
|
48
|
+
messages << msg
|
49
|
+
conn.unsubscribe if messages.length >= messages_limit
|
33
50
|
end
|
34
51
|
end
|
35
52
|
end
|
36
|
-
|
53
|
+
puts "Returing from thread".cyan if ENV['DEBUG']
|
54
|
+
messages
|
37
55
|
}
|
56
|
+
|
38
57
|
Thread.stop
|
39
58
|
yield if block_given?
|
40
59
|
thread
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def redis_client_thread message_limit, *channels
|
64
|
+
thread = Thread.new {
|
65
|
+
messages = []
|
66
|
+
Sidekiq.redis do |conn|
|
67
|
+
puts "Subscribing to #{channels} for #{message_limit.to_s.bold} messages".cyan if ENV['DEBUG']
|
68
|
+
pubsub = conn.pubsub
|
69
|
+
pubsub.call("SUBSCRIBE", *channels)
|
70
|
+
|
71
|
+
timeouts = 0
|
72
|
+
loop do
|
73
|
+
type, ch, msg = pubsub.next_event(2)
|
74
|
+
next if type == "subscribe"
|
75
|
+
if msg
|
76
|
+
puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
|
77
|
+
messages << msg
|
78
|
+
break if messages.length >= message_limit
|
79
|
+
else
|
80
|
+
# no new message was received in the allocated timeout
|
81
|
+
timeouts += 1
|
82
|
+
break if timeouts >= 30
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
puts "Returing from thread".cyan if ENV['DEBUG']
|
87
|
+
messages
|
88
|
+
}
|
89
|
+
sleep 0.1
|
90
|
+
yield if block_given?
|
91
|
+
thread.join
|
92
|
+
end
|
93
|
+
|
94
|
+
def branched_redis_thread n, *channels, &block
|
95
|
+
if Sidekiq.major_version < 7
|
96
|
+
redis_thread(n, *channels, &block)
|
97
|
+
else
|
98
|
+
redis_client_thread(n, *channels, &block)
|
99
|
+
end
|
41
100
|
end
|
42
101
|
|
43
|
-
def capture_status_updates
|
44
|
-
|
102
|
+
def capture_status_updates n, &block
|
103
|
+
branched_redis_thread(n, "status_updates", &block).value
|
45
104
|
end
|
46
105
|
|
47
|
-
|
106
|
+
# Configures server middleware and launches a sidekiq server
|
107
|
+
def start_server server_middleware_options = {}
|
108
|
+
|
109
|
+
# Creates a process for the Sidekiq server
|
48
110
|
pid = Process.fork do
|
49
|
-
|
50
|
-
|
111
|
+
|
112
|
+
# Redirect the server's outputs
|
113
|
+
$stdout.reopen File::NULL, 'w' unless ENV['DEBUG']
|
114
|
+
$stderr.reopen File::NULL, 'w' unless ENV['DEBUG']
|
115
|
+
|
116
|
+
# Load and configure server options
|
51
117
|
require 'sidekiq/cli'
|
52
|
-
|
118
|
+
|
119
|
+
# Add the server middleware
|
53
120
|
Sidekiq.configure_server do |config|
|
54
|
-
config.
|
55
|
-
config.
|
56
|
-
|
57
|
-
end
|
121
|
+
config.concurrency = 5
|
122
|
+
config.redis = Sidekiq::RedisConnection.create if Sidekiq.major_version < 7
|
123
|
+
Sidekiq::Status.configure_server_middleware config, server_middleware_options
|
58
124
|
end
|
59
|
-
|
125
|
+
|
126
|
+
# Launch
|
127
|
+
puts "Server starting".yellow if ENV['DEBUG']
|
128
|
+
instance = Sidekiq::CLI.instance
|
129
|
+
instance.parse(['-r', File.expand_path('environment.rb', File.dirname(__FILE__))])
|
130
|
+
instance.run
|
131
|
+
|
60
132
|
end
|
61
133
|
|
134
|
+
# Run the client-side code
|
62
135
|
yield
|
63
136
|
|
64
|
-
|
137
|
+
# Pause to ensure all jobs are picked up & started before TERM is sent
|
138
|
+
sleep 0.2
|
139
|
+
|
140
|
+
# Attempt to shut down the server normally
|
65
141
|
Process.kill 'TERM', pid
|
66
|
-
|
142
|
+
Process.wait pid
|
143
|
+
|
67
144
|
ensure
|
145
|
+
|
146
|
+
# Ensure the server is actually dead
|
68
147
|
Process.kill 'KILL', pid rescue "OK" # it's OK if the process is gone already
|
148
|
+
|
69
149
|
end
|
data/spec/support/test_jobs.rb
CHANGED
@@ -1,16 +1,34 @@
|
|
1
|
+
require 'sidekiq-status'
|
2
|
+
|
1
3
|
class StubJob
|
2
4
|
include Sidekiq::Worker
|
3
5
|
include Sidekiq::Status::Worker
|
4
6
|
|
5
|
-
sidekiq_options 'retry' =>
|
7
|
+
sidekiq_options 'retry' => false
|
8
|
+
|
9
|
+
def perform(*args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class StubNoStatusJob
|
14
|
+
include Sidekiq::Worker
|
15
|
+
|
16
|
+
sidekiq_options 'retry' => false
|
6
17
|
|
7
18
|
def perform(*args)
|
8
19
|
end
|
9
20
|
end
|
10
21
|
|
22
|
+
|
23
|
+
class ExpiryJob < StubJob
|
24
|
+
def expiration
|
25
|
+
15
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
11
29
|
class LongJob < StubJob
|
12
30
|
def perform(*args)
|
13
|
-
sleep args[0] ||
|
31
|
+
sleep args[0] || 0.25
|
14
32
|
end
|
15
33
|
end
|
16
34
|
|
@@ -23,6 +41,13 @@ class DataJob < StubJob
|
|
23
41
|
end
|
24
42
|
end
|
25
43
|
|
44
|
+
class CustomDataJob < StubJob
|
45
|
+
def perform
|
46
|
+
store({mister_cat: 'meow'})
|
47
|
+
sleep 0.5
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
26
51
|
class ProgressJob < StubJob
|
27
52
|
def perform
|
28
53
|
total 500
|
@@ -34,7 +59,7 @@ end
|
|
34
59
|
class ConfirmationJob < StubJob
|
35
60
|
def perform(*args)
|
36
61
|
Sidekiq.redis do |conn|
|
37
|
-
conn.publish "job_messages_#{jid}", "while in #perform, status = #{conn.hget jid, :status}"
|
62
|
+
conn.publish "job_messages_#{jid}", "while in #perform, status = #{conn.hget "sidekiq:status:#{jid}", :status}"
|
38
63
|
end
|
39
64
|
end
|
40
65
|
end
|
@@ -54,13 +79,65 @@ class FailingJob < StubJob
|
|
54
79
|
end
|
55
80
|
end
|
56
81
|
|
82
|
+
class FailingNoStatusJob < StubNoStatusJob
|
83
|
+
def perform
|
84
|
+
raise StandardError
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class RetryAndFailJob < StubJob
|
89
|
+
sidekiq_options retry: 1
|
90
|
+
|
91
|
+
def perform
|
92
|
+
raise StandardError
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class FailingHardJob < StubJob
|
97
|
+
def perform
|
98
|
+
raise Exception
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class FailingHardNoStatusJob < StubNoStatusJob
|
103
|
+
def perform
|
104
|
+
raise Exception
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class ExitedJob < StubJob
|
109
|
+
def perform
|
110
|
+
raise SystemExit
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class ExitedNoStatusJob < StubNoStatusJob
|
115
|
+
def perform
|
116
|
+
raise SystemExit
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class InterruptedJob < StubJob
|
121
|
+
def perform
|
122
|
+
raise Interrupt
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class InterruptedNoStatusJob < StubNoStatusJob
|
127
|
+
def perform
|
128
|
+
raise Interrupt
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
57
132
|
class RetriedJob < StubJob
|
58
|
-
|
59
|
-
|
133
|
+
|
134
|
+
sidekiq_options retry: true
|
135
|
+
sidekiq_retry_in do |count| 3 end # 3 second delay > job timeout in test suite
|
136
|
+
|
137
|
+
def perform
|
60
138
|
Sidekiq.redis do |conn|
|
61
139
|
key = "RetriedJob_#{jid}"
|
62
|
-
|
63
|
-
unless conn.exists key
|
140
|
+
if [0, false].include? conn.exists(key)
|
64
141
|
conn.set key, 'tried'
|
65
142
|
raise StandardError
|
66
143
|
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,118 @@
|
|
1
|
+
<% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
|
2
|
+
<style>
|
3
|
+
.progress {
|
4
|
+
background-color: #C8E1ED;
|
5
|
+
}
|
6
|
+
.progress-percentage {
|
7
|
+
margin: 5px;
|
8
|
+
padding-left: 4px;
|
9
|
+
color: #333;
|
10
|
+
text-align: left;
|
11
|
+
text-shadow: 0 0 5px white;
|
12
|
+
font-weight: bold;
|
13
|
+
font-size: 14px;
|
14
|
+
}
|
15
|
+
</style>
|
16
|
+
|
17
|
+
<h3>
|
18
|
+
Job Status: <%= h @status["jid"] %>
|
19
|
+
<span class='label label-<%= h @status["label"] %>'>
|
20
|
+
<%= h @status["status"] %>
|
21
|
+
</span>
|
22
|
+
</h3>
|
23
|
+
|
24
|
+
<div class="progress" style="height: 30px;">
|
25
|
+
<div class="progress-bar" role="progressbar" aria-valuenow="<%= @status["pct_complete"].to_i %>" aria-valuemin="0" aria-valuemax="100" style="width: <%= @status["pct_complete"].to_i %>%">
|
26
|
+
<div class="progress-percentage">
|
27
|
+
<%= @status["pct_complete"].to_i %>%
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="panel panel-default" style="background-color: inherit">
|
33
|
+
<div class="panel-body">
|
34
|
+
<h4><%= h @status["worker"] %></h4>
|
35
|
+
|
36
|
+
<div class="row">
|
37
|
+
<div class="col-sm-2">
|
38
|
+
<strong>Arguments</strong>
|
39
|
+
</div>
|
40
|
+
<div class="col-sm-10">
|
41
|
+
<p><%= @status["args"].empty? ? "<i>none</i>" : h(@status["args"]) %></p>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div class="row">
|
46
|
+
<div class="col-sm-2">
|
47
|
+
<strong>Message</strong>
|
48
|
+
</div>
|
49
|
+
<div class="col-sm-10">
|
50
|
+
<p><%= h(@status["message"]) || "<i>none</i>" %></p>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div class="row">
|
55
|
+
<div class="col-sm-2">
|
56
|
+
<strong>Last Updated</strong>
|
57
|
+
</div>
|
58
|
+
<div class="col-sm-10">
|
59
|
+
<p>
|
60
|
+
<% secs = Time.now.to_i - @status["update_time"].to_i %>
|
61
|
+
<% if secs > 0 %>
|
62
|
+
<%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
|
63
|
+
<% else %>
|
64
|
+
Now
|
65
|
+
<% end %>
|
66
|
+
</p>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
|
70
|
+
<div class="row">
|
71
|
+
<div class="col-sm-2">
|
72
|
+
<strong>Elapsed Time</strong>
|
73
|
+
</div>
|
74
|
+
<div class="col-sm-10">
|
75
|
+
<p>
|
76
|
+
<% if @status["elapsed"] %>
|
77
|
+
<%= ChronicDuration.output(@status["elapsed"].to_i, :weeks => true, :units => 2) %>
|
78
|
+
<% end %>
|
79
|
+
</p>
|
80
|
+
</div>
|
81
|
+
</div>
|
82
|
+
|
83
|
+
<div class="row">
|
84
|
+
<div class="col-sm-2">
|
85
|
+
<strong>ETA</strong>
|
86
|
+
</div>
|
87
|
+
<div class="col-sm-10">
|
88
|
+
<p>
|
89
|
+
<% if @status["eta"] %>
|
90
|
+
<%= ChronicDuration.output(@status["eta"].to_i, :weeks => true, :units => 2) %>
|
91
|
+
<% end %>
|
92
|
+
</p>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
|
96
|
+
<% if @status["custom"].any? %>
|
97
|
+
<hr>
|
98
|
+
<% @status["custom"].each do |key, val| %>
|
99
|
+
<div class="row">
|
100
|
+
<div class="col-sm-2">
|
101
|
+
<strong><%= key %></strong>
|
102
|
+
</div>
|
103
|
+
<div class="col-sm-10">
|
104
|
+
<% if val && val.include?("\n") %>
|
105
|
+
<pre><%= h val %></pre>
|
106
|
+
<% else %>
|
107
|
+
<p><%= h(val) || "<i>none</i>" %></p>
|
108
|
+
<% end %>
|
109
|
+
</div>
|
110
|
+
</div>
|
111
|
+
<% end %>
|
112
|
+
<% end %>
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
|
116
|
+
<a href="<%= root_path %>statuses">
|
117
|
+
<small>← back to all statuses</small>
|
118
|
+
</a>
|
data/web/views/statuses.erb
CHANGED
@@ -1,31 +1,149 @@
|
|
1
|
-
|
1
|
+
<% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
|
2
|
+
<style>
|
3
|
+
.progress {
|
4
|
+
background-color: #C8E1ED;
|
5
|
+
}
|
6
|
+
.bar {
|
7
|
+
background-color: #2897cb;
|
8
|
+
color: white;
|
9
|
+
text-shadow: 0 0 0;
|
10
|
+
}
|
11
|
+
.message {
|
12
|
+
text-shadow: 0 0 5px white;
|
13
|
+
font-weight: bold; padding-left: 4px;
|
14
|
+
color: #333;
|
15
|
+
}
|
16
|
+
.actions {
|
17
|
+
text-align: center;
|
18
|
+
}
|
19
|
+
.header {
|
20
|
+
text-align: center;
|
21
|
+
}
|
22
|
+
.header_update_time {
|
23
|
+
width: 10%;
|
24
|
+
}
|
25
|
+
.header_pct_complete {
|
26
|
+
width: 45%;
|
27
|
+
}
|
28
|
+
.btn-warning {
|
29
|
+
background-image: linear-gradient(#f0ad4e, #eea236)
|
30
|
+
}
|
31
|
+
.nav-container {
|
32
|
+
display: flex;
|
33
|
+
line-height: 45px;
|
34
|
+
}
|
35
|
+
.nav-container .pull-right {
|
36
|
+
float: none !important;
|
37
|
+
}
|
38
|
+
.nav-container .pagination {
|
39
|
+
display: flex;
|
40
|
+
align-items: center;
|
41
|
+
}
|
42
|
+
.nav-container .per-page, .filter-status {
|
43
|
+
display: flex;
|
44
|
+
align-items: center;
|
45
|
+
margin: 20px 0 20px 10px;
|
46
|
+
white-space: nowrap;
|
47
|
+
}
|
48
|
+
.nav-container .per-page SELECT {
|
49
|
+
margin: 0 0 0 5px;
|
50
|
+
}
|
51
|
+
</style>
|
52
|
+
<script>
|
53
|
+
function setPerPage(select){
|
54
|
+
window.location = select.options[select.selectedIndex].getAttribute('data-url')
|
55
|
+
}
|
56
|
+
</script>
|
57
|
+
<div style="display: flex; justify-content: space-between;">
|
58
|
+
<h3 class="wi">Recent job statuses</h3>
|
59
|
+
<div class="nav-container">
|
60
|
+
<%= erb :_paging, locals: { url: "#{root_path}statuses" } %>
|
61
|
+
<div class="filter-status">
|
62
|
+
Filter Status:
|
63
|
+
<select class="form-control" onchange="setPerPage(this)">
|
64
|
+
<% (['all', 'complete', 'failed', 'interrupted', 'queued', 'retrying', 'stopped', 'working']).each do |status| %>
|
65
|
+
<option data-url="?<%= qparams(status: status)%>" value="<%= status %>" <%= 'selected="selected"' if status == (params[:status]) %>><%= status %></option>
|
66
|
+
<% end %>
|
67
|
+
</select>
|
68
|
+
</div>
|
2
69
|
|
3
|
-
|
4
|
-
|
70
|
+
<div class="per-page">
|
71
|
+
Per page:
|
72
|
+
<select class="form-control" onchange="setPerPage(this)">
|
73
|
+
<% (Sidekiq::Status::Web.per_page_opts + ['all']).each do |num| %>
|
74
|
+
<option data-url="?<%= qparams(page: 1, per_page: num)%>" value="<%= num %>" <%= 'selected="selected"' if num.to_s == (params[:per_page] || @count) %>><%= num %></option>
|
75
|
+
<% end %>
|
76
|
+
</select>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
<table class="table table-hover table-bordered table-striped">
|
5
81
|
<tr>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
82
|
+
<% @headers.each do |hdr| %>
|
83
|
+
<th class="header <%= h hdr[:class] %> header_<%= h hdr[:id] %>">
|
84
|
+
<a href="<%= h hdr[:url] %>"><%= h hdr[:name] %></a>
|
85
|
+
</th>
|
86
|
+
<% end %>
|
87
|
+
<th class="header">
|
88
|
+
Actions
|
89
|
+
</th>
|
11
90
|
</tr>
|
12
91
|
<% @statuses.each do |container| %>
|
13
92
|
<tr>
|
14
93
|
<td>
|
15
|
-
|
16
|
-
|
17
|
-
|
94
|
+
<div title='<%= h container["jid"] %>'><a href="<%= root_path %>statuses/<%= h container["jid"] %>"><%= h container["worker"] %></a></div>
|
95
|
+
</td>
|
96
|
+
<td>
|
97
|
+
<div class='args' title='<%= h container["jid"] %>'><%= h container["args"] %></div>
|
98
|
+
</td>
|
99
|
+
<td style='text-align: center;'>
|
100
|
+
<div class='label label-<%= h container["label"] %>'><%= h container["status"] %></div>
|
101
|
+
</td>
|
102
|
+
<% secs = Time.now.to_i - container["update_time"].to_i %>
|
103
|
+
<td style='text-align: center; white-space: nowrap;' title="<%= Time.at(container["update_time"].to_i) %>">
|
104
|
+
<% if secs > 0 %>
|
105
|
+
<%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
|
106
|
+
<% else %>
|
107
|
+
Now
|
108
|
+
<% end %>
|
18
109
|
</td>
|
19
|
-
<td><%= container.status %></td>
|
20
|
-
<td><%= Time.at(container.update_time.to_i) %></td>
|
21
110
|
<td>
|
22
111
|
<div class="progress progress-striped" style="margin-bottom: 0">
|
23
|
-
<div class=
|
24
|
-
|
25
|
-
<%= container.pct_complete %>%
|
112
|
+
<div class='message' style='text-align:right; padding-right:0.5em; background-color: transparent; float:right;'>
|
113
|
+
<%= h container["message"] %>
|
26
114
|
</div>
|
115
|
+
<% if container["pct_complete"].to_i > 0 %>
|
116
|
+
<div class="bar message" style="width: <%= h container["pct_complete"] %>%;">
|
117
|
+
<%= h container["pct_complete"] %>%
|
118
|
+
</div>
|
119
|
+
<% end %>
|
27
120
|
</div>
|
28
|
-
|
121
|
+
</td>
|
122
|
+
<td style='text-align: center; white-space: nowrap;'>
|
123
|
+
<% if container["elapsed"] %>
|
124
|
+
<%= ChronicDuration.output(container["elapsed"].to_i, :weeks => true, :units => 2) %>
|
125
|
+
<% end %>
|
126
|
+
</td>
|
127
|
+
<td style='text-align: center; white-space: nowrap;'>
|
128
|
+
<% if container["eta"] %>
|
129
|
+
<%= ChronicDuration.output(container["eta"].to_i, :weeks => true, :units => 2) %>
|
130
|
+
<% end %>
|
131
|
+
</td>
|
132
|
+
<td>
|
133
|
+
<div class="actions">
|
134
|
+
<form action="statuses" method="post">
|
135
|
+
<input type="hidden" name="jid" value="<%= h container["jid"] %>" />
|
136
|
+
<%= csrf_tag %>
|
137
|
+
<% if container["status"] == "complete" %>
|
138
|
+
<input type="hidden" name="_method" value="delete" />
|
139
|
+
<input type="submit" class="btn btn-danger btn-xs" value="Remove" />
|
140
|
+
<% elsif container["status"] == "failed" %>
|
141
|
+
<input type="hidden" name="_method" value="put" />
|
142
|
+
<input type="submit" class="btn btn-warning btn-xs" value="Retry Now" />
|
143
|
+
<% end %>
|
144
|
+
</form>
|
145
|
+
</div>
|
146
|
+
</td>
|
29
147
|
</tr>
|
30
148
|
<% end %>
|
31
149
|
<% if @statuses.empty? %>
|