good_job 2.4.2 → 2.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/README.md +12 -4
- data/engine/app/assets/scripts.js +1 -0
- data/engine/app/assets/style.css +5 -0
- data/engine/app/assets/vendor/chartjs/chart.min.js +13 -0
- data/engine/app/assets/vendor/rails_ujs.js +747 -0
- data/engine/app/charts/good_job/scheduled_by_queue_chart.rb +69 -0
- data/engine/app/controllers/good_job/assets_controller.rb +8 -4
- data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
- data/engine/app/filters/good_job/base_filter.rb +18 -56
- data/engine/app/filters/good_job/executions_filter.rb +9 -8
- data/engine/app/filters/good_job/jobs_filter.rb +12 -9
- data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
- data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
- data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
- data/engine/app/views/good_job/executions/index.html.erb +2 -2
- data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +18 -6
- data/engine/app/views/good_job/jobs/index.html.erb +15 -2
- data/engine/app/views/good_job/jobs/show.html.erb +2 -2
- data/engine/app/views/good_job/shared/_chart.erb +19 -46
- data/engine/app/views/good_job/shared/_filter.erb +27 -13
- data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
- data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
- data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
- data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
- data/engine/app/views/layouts/good_job/base.html.erb +6 -4
- data/engine/config/routes.rb +17 -4
- data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +2 -0
- data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
- data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
- data/lib/good_job/active_job_job.rb +228 -0
- data/lib/good_job/configuration.rb +1 -1
- data/lib/good_job/cron_entry.rb +78 -5
- data/lib/good_job/cron_manager.rb +4 -6
- data/lib/good_job/current_thread.rb +38 -5
- data/lib/good_job/execution.rb +53 -39
- data/lib/good_job/filterable.rb +42 -0
- data/lib/good_job/notifier.rb +17 -7
- data/lib/good_job/version.rb +1 -1
- metadata +31 -21
- data/engine/app/assets/vendor/chartist/chartist.css +0 -613
- data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
- data/engine/app/models/good_job/active_job_job.rb +0 -127
- data/engine/app/views/good_job/cron_schedules/index.html.erb +0 -72
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GoodJob
|
4
|
+
class ScheduledByQueueChart
|
5
|
+
def initialize(filter)
|
6
|
+
@filter = filter
|
7
|
+
end
|
8
|
+
|
9
|
+
def data
|
10
|
+
end_time = Time.current
|
11
|
+
start_time = end_time - 1.day
|
12
|
+
|
13
|
+
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
14
|
+
SELECT *
|
15
|
+
FROM generate_series(
|
16
|
+
date_trunc('hour', $1::timestamp),
|
17
|
+
date_trunc('hour', $2::timestamp),
|
18
|
+
'1 hour'
|
19
|
+
) timestamp
|
20
|
+
LEFT JOIN (
|
21
|
+
SELECT
|
22
|
+
date_trunc('hour', scheduled_at) AS scheduled_at,
|
23
|
+
queue_name,
|
24
|
+
count(*) AS count
|
25
|
+
FROM (
|
26
|
+
#{@filter.filtered_query.except(:select, :order).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
27
|
+
) sources
|
28
|
+
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
29
|
+
) sources ON sources.scheduled_at = timestamp
|
30
|
+
ORDER BY timestamp ASC
|
31
|
+
SQL
|
32
|
+
|
33
|
+
binds = [[nil, start_time], [nil, end_time]]
|
34
|
+
executions_data = GoodJob::Execution.connection.exec_query(GoodJob::Execution.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", binds)
|
35
|
+
|
36
|
+
queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
|
37
|
+
labels = []
|
38
|
+
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
39
|
+
labels << timestamp.in_time_zone.strftime('%H:%M')
|
40
|
+
queue_names.each do |queue_name|
|
41
|
+
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
labels: labels,
|
47
|
+
datasets: queues_data.map do |queue, data|
|
48
|
+
label = queue || '(none)'
|
49
|
+
{
|
50
|
+
label: label,
|
51
|
+
data: data,
|
52
|
+
backgroundColor: string_to_hsl(label),
|
53
|
+
borderColor: string_to_hsl(label),
|
54
|
+
}
|
55
|
+
end,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def string_to_hsl(string)
|
60
|
+
hash_value = string.sum
|
61
|
+
|
62
|
+
hue = hash_value % 360
|
63
|
+
saturation = (hash_value % 50) + 50
|
64
|
+
lightness = '50'
|
65
|
+
|
66
|
+
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -15,12 +15,16 @@ module GoodJob
|
|
15
15
|
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "bootstrap", "bootstrap.bundle.min.js")
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "
|
18
|
+
def chartjs_js
|
19
|
+
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartjs", "chart.min.js")
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "
|
22
|
+
def rails_ujs_js
|
23
|
+
render file: GoodJob::Engine.root.join("app", "assets", "vendor", "rails_ujs.js")
|
24
|
+
end
|
25
|
+
|
26
|
+
def scripts_js
|
27
|
+
render file: GoodJob::Engine.root.join("app", "assets", "scripts.js")
|
24
28
|
end
|
25
29
|
|
26
30
|
def style_css
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class CronEntriesController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@cron_entries = CronEntry.all
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
@cron_entry = CronEntry.find(params[:id])
|
10
|
+
@jobs_filter = JobsFilter.new(params, @cron_entry.jobs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def enqueue
|
14
|
+
@cron_entry = CronEntry.find(params[:id])
|
15
|
+
@cron_entry.enqueue(Time.current)
|
16
|
+
redirect_back(fallback_location: cron_entries_path, notice: "Cron entry has been enqueued.")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class JobsController < GoodJob::BaseController
|
4
|
+
rescue_from GoodJob::ActiveJobJob::AdapterNotGoodJobError,
|
5
|
+
GoodJob::ActiveJobJob::ActionForStateMismatchError,
|
6
|
+
with: :redirect_on_error
|
7
|
+
|
4
8
|
def index
|
5
9
|
@filter = JobsFilter.new(params)
|
6
10
|
end
|
@@ -10,5 +14,37 @@ module GoodJob
|
|
10
14
|
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
11
15
|
redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
12
16
|
end
|
17
|
+
|
18
|
+
def discard
|
19
|
+
@job = ActiveJobJob.find(params[:id])
|
20
|
+
@job.discard_job("Discarded through dashboard")
|
21
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been discarded")
|
22
|
+
end
|
23
|
+
|
24
|
+
def reschedule
|
25
|
+
@job = ActiveJobJob.find(params[:id])
|
26
|
+
@job.reschedule_job
|
27
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been rescheduled")
|
28
|
+
end
|
29
|
+
|
30
|
+
def retry
|
31
|
+
@job = ActiveJobJob.find(params[:id])
|
32
|
+
@job.retry_job
|
33
|
+
redirect_back(fallback_location: jobs_path, notice: "Job has been retried")
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def redirect_on_error(exception)
|
39
|
+
alert = case exception
|
40
|
+
when GoodJob::ActiveJobJob::AdapterNotGoodJobError
|
41
|
+
"ActiveJob Queue Adapter must be GoodJob."
|
42
|
+
when GoodJob::ActiveJobJob::ActionForStateMismatchError
|
43
|
+
"Job is not in an appropriate state for this action."
|
44
|
+
else
|
45
|
+
exception.to_s
|
46
|
+
end
|
47
|
+
redirect_back(fallback_location: jobs_path, alert: alert)
|
48
|
+
end
|
13
49
|
end
|
14
50
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class BaseFilter
|
4
|
-
|
4
|
+
DEFAULT_LIMIT = 25
|
5
|
+
EMPTY = '[none]'
|
5
6
|
|
6
|
-
|
7
|
+
attr_accessor :params, :base_query
|
8
|
+
|
9
|
+
def initialize(params, base_query = nil)
|
7
10
|
@params = params
|
11
|
+
@base_query = base_query || default_base_query
|
8
12
|
end
|
9
13
|
|
10
14
|
def records
|
@@ -13,7 +17,7 @@ module GoodJob
|
|
13
17
|
filtered_query.display_all(
|
14
18
|
after_scheduled_at: after_scheduled_at,
|
15
19
|
after_id: params[:after_id]
|
16
|
-
).limit(params.fetch(:limit,
|
20
|
+
).limit(params.fetch(:limit, DEFAULT_LIMIT))
|
17
21
|
end
|
18
22
|
|
19
23
|
def last
|
@@ -22,13 +26,13 @@ module GoodJob
|
|
22
26
|
|
23
27
|
def job_classes
|
24
28
|
base_query.group("serialized_params->>'job_class'").count
|
25
|
-
.sort_by { |name, _count| name }
|
29
|
+
.sort_by { |name, _count| name.to_s }
|
26
30
|
.to_h
|
27
31
|
end
|
28
32
|
|
29
33
|
def queues
|
30
34
|
base_query.group(:queue_name).count
|
31
|
-
.sort_by { |name, _count| name }
|
35
|
+
.sort_by { |name, _count| name.to_s || EMPTY }
|
32
36
|
.to_h
|
33
37
|
end
|
34
38
|
|
@@ -36,65 +40,23 @@ module GoodJob
|
|
36
40
|
raise NotImplementedError
|
37
41
|
end
|
38
42
|
|
39
|
-
def to_params(override)
|
43
|
+
def to_params(override = {})
|
40
44
|
{
|
41
|
-
state: params[:state],
|
42
45
|
job_class: params[:job_class],
|
43
|
-
|
46
|
+
limit: params[:limit],
|
47
|
+
queue_name: params[:queue_name],
|
48
|
+
query: params[:query],
|
49
|
+
state: params[:state],
|
50
|
+
}.merge(override).delete_if { |_, v| v.blank? }
|
44
51
|
end
|
45
52
|
|
46
|
-
def
|
47
|
-
|
48
|
-
SELECT *
|
49
|
-
FROM generate_series(
|
50
|
-
date_trunc('hour', $1::timestamp),
|
51
|
-
date_trunc('hour', $2::timestamp),
|
52
|
-
'1 hour'
|
53
|
-
) timestamp
|
54
|
-
LEFT JOIN (
|
55
|
-
SELECT
|
56
|
-
date_trunc('hour', scheduled_at) AS scheduled_at,
|
57
|
-
queue_name,
|
58
|
-
count(*) AS count
|
59
|
-
FROM (
|
60
|
-
#{filtered_query.except(:select).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
|
61
|
-
) sources
|
62
|
-
GROUP BY date_trunc('hour', scheduled_at), queue_name
|
63
|
-
) sources ON sources.scheduled_at = timestamp
|
64
|
-
ORDER BY timestamp ASC
|
65
|
-
SQL
|
66
|
-
|
67
|
-
current_time = Time.current
|
68
|
-
binds = [[nil, current_time - 1.day], [nil, current_time]]
|
69
|
-
executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
|
70
|
-
|
71
|
-
queue_names = executions_data.map { |d| d['queue_name'] }.uniq
|
72
|
-
labels = []
|
73
|
-
queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
|
74
|
-
labels << timestamp.in_time_zone.strftime('%H:%M %z')
|
75
|
-
queue_names.each do |queue_name|
|
76
|
-
(hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
{
|
81
|
-
labels: labels,
|
82
|
-
series: queues_data.map do |queue, data|
|
83
|
-
{
|
84
|
-
name: queue,
|
85
|
-
data: data,
|
86
|
-
}
|
87
|
-
end,
|
88
|
-
}
|
53
|
+
def filtered_query
|
54
|
+
raise NotImplementedError
|
89
55
|
end
|
90
56
|
|
91
57
|
private
|
92
58
|
|
93
|
-
def
|
94
|
-
raise NotImplementedError
|
95
|
-
end
|
96
|
-
|
97
|
-
def filtered_query
|
59
|
+
def default_base_query
|
98
60
|
raise NotImplementedError
|
99
61
|
end
|
100
62
|
end
|
@@ -10,16 +10,11 @@ module GoodJob
|
|
10
10
|
}
|
11
11
|
end
|
12
12
|
|
13
|
-
private
|
14
|
-
|
15
|
-
def base_query
|
16
|
-
GoodJob::Execution.all
|
17
|
-
end
|
18
|
-
|
19
13
|
def filtered_query
|
20
14
|
query = base_query
|
21
|
-
query = query.job_class(params[:job_class]) if params[:job_class]
|
22
|
-
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
15
|
+
query = query.job_class(params[:job_class]) if params[:job_class].present?
|
16
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
|
17
|
+
query = query.search_text(params[:query]) if params[:query].present?
|
23
18
|
|
24
19
|
if params[:state]
|
25
20
|
case params[:state]
|
@@ -36,5 +31,11 @@ module GoodJob
|
|
36
31
|
|
37
32
|
query
|
38
33
|
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def default_base_query
|
38
|
+
GoodJob::Execution.all
|
39
|
+
end
|
39
40
|
end
|
40
41
|
end
|
@@ -12,16 +12,13 @@ module GoodJob
|
|
12
12
|
}
|
13
13
|
end
|
14
14
|
|
15
|
-
private
|
16
|
-
|
17
|
-
def base_query
|
18
|
-
GoodJob::ActiveJobJob.all
|
19
|
-
end
|
20
|
-
|
21
15
|
def filtered_query
|
22
|
-
query = base_query
|
23
|
-
|
24
|
-
|
16
|
+
query = base_query.includes(:executions)
|
17
|
+
.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
|
18
|
+
|
19
|
+
query = query.job_class(params[:job_class]) if params[:job_class].present?
|
20
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name].present?
|
21
|
+
query = query.search_text(params[:query]) if params[:query].present?
|
25
22
|
|
26
23
|
if params[:state]
|
27
24
|
case params[:state]
|
@@ -42,5 +39,11 @@ module GoodJob
|
|
42
39
|
|
43
40
|
query
|
44
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_base_query
|
46
|
+
GoodJob::ActiveJobJob.all
|
47
|
+
end
|
45
48
|
end
|
46
49
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<% if @cron_entries.present? %>
|
2
|
+
<div class="card my-3">
|
3
|
+
<div class="table-responsive">
|
4
|
+
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
5
|
+
<thead>
|
6
|
+
<th>Key</th>
|
7
|
+
<th>Schedule</th>
|
8
|
+
<th>
|
9
|
+
Properties
|
10
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
11
|
+
data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
|
12
|
+
aria: { expanded: false, controls: @cron_entries.map { |cron_entry| "##{dom_id(cron_entry, 'properties')}" }.join(" ") }
|
13
|
+
%>
|
14
|
+
</th>
|
15
|
+
<th>Description</th>
|
16
|
+
<th>Next scheduled</th>
|
17
|
+
<th>Last run</th>
|
18
|
+
<th>Actions</th>
|
19
|
+
</thead>
|
20
|
+
<tbody>
|
21
|
+
<% @cron_entries.each do |cron_entry| %>
|
22
|
+
<tr id="<%= dom_id(cron_entry) %>">
|
23
|
+
<td class="font-monospace"><%= cron_entry.key %></td>
|
24
|
+
<td class="font-monospace"><%= cron_entry.schedule %></td>
|
25
|
+
<td>
|
26
|
+
<%= tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
27
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
|
28
|
+
aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) %>
|
29
|
+
<%= tag.pre(JSON.pretty_generate(cron_entry.display_properties), id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties") %>
|
30
|
+
</td>
|
31
|
+
<td><%= cron_entry.description %></td>
|
32
|
+
<td><%= cron_entry.next_at %></td>
|
33
|
+
<td>
|
34
|
+
<% if cron_entry.last_job.present? %>
|
35
|
+
<%= link_to cron_entry.last_at, cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
|
36
|
+
<% end %>
|
37
|
+
</td>
|
38
|
+
<td>
|
39
|
+
<%= button_to enqueue_cron_entry_path(cron_entry.id), method: :post, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Run cron entry now" }, title: "Run cron entry now", data: { confirm: "Confirm run cron entry now" } do %>
|
40
|
+
<%= render "good_job/shared/icons/play" %>
|
41
|
+
<% end %>
|
42
|
+
</td>
|
43
|
+
</tr>
|
44
|
+
<% end %>
|
45
|
+
</tbody>
|
46
|
+
</table>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
<% else %>
|
50
|
+
<em>No cron schedules present.</em>
|
51
|
+
<% end %>
|
@@ -44,7 +44,7 @@
|
|
44
44
|
<%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
|
45
45
|
</td>
|
46
46
|
<td>
|
47
|
-
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
|
47
|
+
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution", data: { confirm: "Confirm delete" } do %>
|
48
48
|
<%= render "good_job/shared/icons/trash" %>
|
49
49
|
<% end %>
|
50
50
|
</td>
|
@@ -1,11 +1,11 @@
|
|
1
1
|
<div class="card my-3 p-6">
|
2
|
-
<%= render 'good_job/shared/chart', chart_data: @filter.
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
6
|
|
7
7
|
<% if @filter.records.present? %>
|
8
|
-
<%= render 'good_job/
|
8
|
+
<%= render 'good_job/executions/table', executions: @filter.records %>
|
9
9
|
|
10
10
|
<nav aria-label="Job pagination" class="mt-3">
|
11
11
|
<ul class="pagination">
|
@@ -22,7 +22,7 @@
|
|
22
22
|
</thead>
|
23
23
|
<tbody>
|
24
24
|
<% jobs.each do |job| %>
|
25
|
-
<tr id="<%= dom_id(job) %>">
|
25
|
+
<tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
|
26
26
|
<td>
|
27
27
|
<%= link_to job_path(job.id) do %>
|
28
28
|
<code><%= job.id %></code>
|
@@ -43,11 +43,23 @@
|
|
43
43
|
%>
|
44
44
|
<%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
|
45
45
|
</td>
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
<td>
|
47
|
+
<div class="text-nowrap">
|
48
|
+
<% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
|
49
|
+
<%= button_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_reschedulable, aria: { label: "Reschedule job" }, title: "Reschedule job", data: { confirm: "Confirm reschedule" } do %>
|
50
|
+
<%= render "good_job/shared/icons/skip_forward" %>
|
51
|
+
<% end %>
|
52
|
+
|
53
|
+
<% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
|
54
|
+
<%= button_to discard_job_path(job.id), method: :put, class: "btn btn-sm #{job_discardable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_discardable, aria: { label: "Discard job" }, title: "Discard job", data: { confirm: "Confirm discard" } do %>
|
55
|
+
<%= render "good_job/shared/icons/stop" %>
|
56
|
+
<% end %>
|
57
|
+
|
58
|
+
<%= button_to retry_job_path(job.id), method: :put, class: "btn btn-sm #{job.status == :discarded ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: job.status != :discarded, aria: { label: "Retry job" }, title: "Retry job", data: { confirm: "Confirm retry" } do %>
|
59
|
+
<%= render "good_job/shared/icons/arrow_clockwise" %>
|
60
|
+
<% end %>
|
61
|
+
</div>
|
62
|
+
</td>
|
51
63
|
</tr>
|
52
64
|
<% end %>
|
53
65
|
</tbody>
|
@@ -1,7 +1,20 @@
|
|
1
1
|
<div class="card my-3 p-6">
|
2
|
-
<%= render 'good_job/shared/chart', chart_data: @filter.
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
6
|
|
7
|
-
|
7
|
+
<% if @filter.records.present? %>
|
8
|
+
<%= render 'good_job/jobs/table', jobs: @filter.records %>
|
9
|
+
<nav aria-label="Job pagination" class="mt-3">
|
10
|
+
<ul class="pagination">
|
11
|
+
<li class="page-item">
|
12
|
+
<%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
|
13
|
+
Older jobs <span aria-hidden="true">»</span>
|
14
|
+
<% end %>
|
15
|
+
</li>
|
16
|
+
</ul>
|
17
|
+
</nav>
|
18
|
+
<% else %>
|
19
|
+
<em>No jobs present.</em>
|
20
|
+
<% end %>
|
@@ -1,3 +1,3 @@
|
|
1
|
-
<h1>ActiveJob ID: <code><%= @executions.first.id %></code></h1>
|
1
|
+
<h1 class="mb-3">ActiveJob ID: <code><%= @executions.first.id %></code></h1>
|
2
2
|
|
3
|
-
<%= render 'good_job/
|
3
|
+
<%= render 'good_job/executions/table', executions: @executions %>
|
@@ -1,52 +1,25 @@
|
|
1
|
-
<div
|
1
|
+
<div class="chart-wrapper">
|
2
|
+
<canvas id="chart"></canvas>
|
3
|
+
</div>
|
2
4
|
|
3
5
|
<%= javascript_tag nonce: true do %>
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
const chartData = <%== chart_data.to_json %>;
|
7
|
+
|
8
|
+
const ctx = document.getElementById('chart').getContext('2d');
|
9
|
+
const chart = new Chart(ctx, {
|
10
|
+
type: 'line',
|
11
|
+
data: {
|
12
|
+
labels: chartData.labels,
|
13
|
+
datasets: chartData.datasets
|
11
14
|
},
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
options: {
|
16
|
+
responsive: true,
|
17
|
+
maintainAspectRatio: false,
|
18
|
+
scales: {
|
19
|
+
y: {
|
20
|
+
beginAtZero: true
|
21
|
+
}
|
15
22
|
}
|
16
|
-
},
|
17
|
-
axisY: {
|
18
|
-
low: 0,
|
19
|
-
onlyInteger: true
|
20
23
|
}
|
21
|
-
})
|
22
|
-
|
23
|
-
// https://www.smashingmagazine.com/2014/12/chartist-js-open-source-library-responsive-charts/
|
24
|
-
const chartEl = document.getElementById('chart');
|
25
|
-
const tooltipEl = document.createElement('div')
|
26
|
-
|
27
|
-
tooltipEl.classList.add('tooltip', 'tooltip-hidden');
|
28
|
-
chartEl.appendChild(tooltipEl);
|
29
|
-
|
30
|
-
document.body.addEventListener('mouseenter', function (event) {
|
31
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
32
|
-
|
33
|
-
const seriesName = event.target.closest('.ct-series').getAttribute('ct:series-name');
|
34
|
-
const value = event.target.getAttribute('ct:value');
|
35
|
-
|
36
|
-
tooltipEl.innerText = seriesName + ': ' + value;
|
37
|
-
tooltipEl.classList.remove('tooltip-hidden');
|
38
|
-
}, true);
|
39
|
-
|
40
|
-
document.body.addEventListener('mouseleave', function (event) {
|
41
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
42
|
-
|
43
|
-
tooltipEl.classList.add('tooltip-hidden');
|
44
|
-
}, true);
|
45
|
-
|
46
|
-
document.body.addEventListener('mousemove', function(event) {
|
47
|
-
if (!(event.target.matches && event.target.matches('.ct-point'))) return;
|
48
|
-
|
49
|
-
tooltipEl.style.left = (event.offsetX || event.originalEvent.layerX) + tooltipEl.offsetWidth + 10 + 'px';
|
50
|
-
tooltipEl.style.top = (event.offsetY || event.originalEvent.layerY) + tooltipEl.offsetHeight - 20 + 'px';
|
51
|
-
}, true);
|
24
|
+
});
|
52
25
|
<% end %>
|
@@ -1,52 +1,66 @@
|
|
1
1
|
<div class='card mb-2'>
|
2
2
|
<div class='card-body d-flex flex-wrap'>
|
3
|
-
|
4
|
-
<div class='me-4'>
|
3
|
+
<div class='mb-2 me-4'>
|
5
4
|
<small>Filter by job class</small>
|
6
5
|
<br>
|
7
|
-
<%
|
6
|
+
<% filter.job_classes.each do |name, count| %>
|
8
7
|
<% if params[:job_class] == name %>
|
9
|
-
<%= link_to(
|
8
|
+
<%= link_to(filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
10
9
|
<%= name %> (<%= count %>)
|
11
10
|
<% end %>
|
12
11
|
<% else %>
|
13
|
-
<%= link_to(
|
12
|
+
<%= link_to(filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
14
13
|
<%= name %> (<%= count %>)
|
15
14
|
<% end %>
|
16
15
|
<% end %>
|
17
16
|
<% end %>
|
18
17
|
</div>
|
19
18
|
|
20
|
-
<div class='me-4'>
|
19
|
+
<div class='mb-2 me-4'>
|
21
20
|
<small>Filter by state</small>
|
22
21
|
<br>
|
23
|
-
<%
|
22
|
+
<% filter.states.each do |name, count| %>
|
24
23
|
<% if params[:state] == name %>
|
25
|
-
<%= link_to(
|
24
|
+
<%= link_to(filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
26
25
|
<%= name %> (<%= count %>)
|
27
26
|
<% end %>
|
28
27
|
<% else %>
|
29
|
-
<%= link_to(
|
28
|
+
<%= link_to(filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
30
29
|
<%= name %> (<%= count %>)
|
31
30
|
<% end %>
|
32
31
|
<% end %>
|
33
32
|
<% end %>
|
34
33
|
</div>
|
35
34
|
|
36
|
-
<div>
|
35
|
+
<div class='mb-2 me-4'>
|
37
36
|
<small>Filter by queue</small>
|
38
37
|
<br>
|
39
|
-
<%
|
38
|
+
<% filter.queues.each do |name, count| %>
|
40
39
|
<% if params[:queue_name] == name %>
|
41
|
-
<%= link_to(
|
40
|
+
<%= link_to(filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
42
41
|
<%= name %> (<%= count %>)
|
43
42
|
<% end %>
|
44
43
|
<% else %>
|
45
|
-
<%= link_to(
|
44
|
+
<%= link_to(filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
46
45
|
<%= name %> (<%= count %>)
|
47
46
|
<% end %>
|
48
47
|
<% end %>
|
49
48
|
<% end %>
|
50
49
|
</div>
|
50
|
+
|
51
|
+
<div class="mb-2">
|
52
|
+
<%= form_with(url: "", method: :get, local: true) do |form| %>
|
53
|
+
<% filter.to_params(query: nil).each do |key, value| %>
|
54
|
+
<%= form.hidden_field(key.to_sym, value: value) %>
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
<small><%= form.label :query, "Search" %></small>
|
58
|
+
<div class="input-group input-group-sm">
|
59
|
+
<%= form.search_field :query, value: params[:query], class: "form-control" %>
|
60
|
+
<%= form.button "Search", type: "submit", name: nil, class: "btn btn-sm btn-outline-secondary" %>
|
61
|
+
<%= link_to "Clear", filter.to_params(query: nil), class: "btn btn-sm btn-outline-secondary" %>
|
62
|
+
</div>
|
63
|
+
<% end %>
|
64
|
+
</div>
|
51
65
|
</div>
|
52
66
|
</div>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/arrow-clockwise/ -->
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
3
|
+
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
|
4
|
+
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
|
5
|
+
</svg>
|