good_job 2.1.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -1
- data/README.md +32 -0
- data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +2 -2
- data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +2 -2
- data/engine/app/controllers/good_job/cron_schedules_controller.rb +9 -0
- data/engine/app/controllers/good_job/executions_controller.rb +14 -0
- data/engine/app/controllers/good_job/jobs_controller.rb +8 -4
- data/engine/app/filters/good_job/base_filter.rb +101 -0
- data/engine/app/filters/good_job/executions_filter.rb +40 -0
- data/engine/app/filters/good_job/jobs_filter.rb +46 -0
- data/engine/app/helpers/good_job/application_helper.rb +4 -0
- data/engine/app/models/good_job/active_job_job.rb +127 -0
- data/engine/app/views/good_job/cron_schedules/index.html.erb +50 -0
- data/engine/app/views/good_job/executions/index.html.erb +21 -0
- data/engine/app/views/good_job/jobs/index.html.erb +7 -0
- data/engine/app/views/good_job/jobs/show.html.erb +3 -0
- data/engine/app/views/good_job/shared/_executions_table.erb +56 -0
- data/engine/app/views/good_job/shared/_filter.erb +52 -0
- data/engine/app/views/good_job/shared/_jobs_table.erb +19 -11
- data/engine/app/views/layouts/good_job/base.html.erb +13 -4
- data/engine/config/routes.rb +4 -3
- data/lib/good_job/active_job_extensions/concurrency.rb +6 -6
- data/lib/good_job/adapter.rb +10 -10
- data/lib/good_job/cron_manager.rb +3 -3
- data/lib/good_job/{current_execution.rb → current_thread.rb} +8 -8
- data/lib/good_job/execution.rb +308 -0
- data/lib/good_job/job.rb +6 -294
- data/lib/good_job/job_performer.rb +2 -2
- data/lib/good_job/log_subscriber.rb +4 -4
- data/lib/good_job/notifier.rb +3 -3
- data/lib/good_job/railtie.rb +2 -2
- data/lib/good_job/scheduler.rb +3 -3
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +2 -2
- metadata +16 -7
- data/engine/app/controllers/good_job/active_jobs_controller.rb +0 -9
- data/engine/app/controllers/good_job/dashboards_controller.rb +0 -106
- data/engine/app/views/good_job/active_jobs/show.html.erb +0 -1
- data/engine/app/views/good_job/dashboards/index.html.erb +0 -54
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class ExecutionsController < GoodJob::BaseController
|
4
|
+
def index
|
5
|
+
@filter = ExecutionsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def destroy
|
9
|
+
deleted_count = GoodJob::Execution.where(id: params[:id]).delete_all
|
10
|
+
message = deleted_count.positive? ? { notice: "Job execution deleted" } : { alert: "Job execution not deleted" }
|
11
|
+
redirect_back fallback_location: root_path, **message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
class JobsController < GoodJob::BaseController
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
def index
|
5
|
+
@filter = JobsFilter.new(params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
@executions = GoodJob::Execution.active_job_id(params[:id])
|
10
|
+
.order(Arel.sql("COALESCE(scheduled_at, created_at) DESC"))
|
11
|
+
redirect_to root_path, alert: "Executions for Active Job #{params[:id]} not found" if @executions.empty?
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class BaseFilter
|
4
|
+
attr_accessor :params
|
5
|
+
|
6
|
+
def initialize(params)
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def records
|
11
|
+
after_scheduled_at = params[:after_scheduled_at].present? ? Time.zone.parse(params[:after_scheduled_at]) : nil
|
12
|
+
|
13
|
+
filtered_query.display_all(
|
14
|
+
after_scheduled_at: after_scheduled_at,
|
15
|
+
after_id: params[:after_id]
|
16
|
+
).limit(params.fetch(:limit, 25))
|
17
|
+
end
|
18
|
+
|
19
|
+
def last
|
20
|
+
@_last ||= records.last
|
21
|
+
end
|
22
|
+
|
23
|
+
def job_classes
|
24
|
+
base_query.group("serialized_params->>'job_class'").count
|
25
|
+
.sort_by { |name, _count| name }
|
26
|
+
.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def queues
|
30
|
+
base_query.group(:queue_name).count
|
31
|
+
.sort_by { |name, _count| name }
|
32
|
+
.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
def states
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_params(override)
|
40
|
+
{
|
41
|
+
state: params[:state],
|
42
|
+
job_class: params[:job_class],
|
43
|
+
}.merge(override).delete_if { |_, v| v.nil? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def chart_data
|
47
|
+
count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
|
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
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def base_query
|
94
|
+
raise NotImplementedError
|
95
|
+
end
|
96
|
+
|
97
|
+
def filtered_query
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class ExecutionsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'finished' => base_query.finished.count,
|
7
|
+
'unfinished' => base_query.unfinished.count,
|
8
|
+
'running' => base_query.running.count,
|
9
|
+
'errors' => base_query.where.not(error: nil).count,
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def base_query
|
16
|
+
GoodJob::Execution.all
|
17
|
+
end
|
18
|
+
|
19
|
+
def filtered_query
|
20
|
+
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]
|
23
|
+
|
24
|
+
if params[:state]
|
25
|
+
case params[:state]
|
26
|
+
when 'finished'
|
27
|
+
query = query.finished
|
28
|
+
when 'unfinished'
|
29
|
+
query = query.unfinished
|
30
|
+
when 'running'
|
31
|
+
query = query.running
|
32
|
+
when 'errors'
|
33
|
+
query = query.where.not(error: nil)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
query
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
class JobsFilter < BaseFilter
|
4
|
+
def states
|
5
|
+
{
|
6
|
+
'scheduled' => base_query.scheduled.count,
|
7
|
+
'retried' => base_query.retried.count,
|
8
|
+
'queued' => base_query.queued.count,
|
9
|
+
'running' => base_query.running.count,
|
10
|
+
'finished' => base_query.finished.count,
|
11
|
+
'discarded' => base_query.discarded.count,
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def base_query
|
18
|
+
GoodJob::ActiveJobJob.all
|
19
|
+
end
|
20
|
+
|
21
|
+
def filtered_query
|
22
|
+
query = base_query
|
23
|
+
query = query.job_class(params[:job_class]) if params[:job_class]
|
24
|
+
query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
|
25
|
+
|
26
|
+
if params[:state]
|
27
|
+
case params[:state]
|
28
|
+
when 'discarded'
|
29
|
+
query = query.discarded
|
30
|
+
when 'finished'
|
31
|
+
query = query.finished
|
32
|
+
when 'retried'
|
33
|
+
query = query.retried
|
34
|
+
when 'scheduled'
|
35
|
+
query = query.scheduled
|
36
|
+
when 'running'
|
37
|
+
query = query.running.select('good_jobs.*', 'pg_locks.locktype')
|
38
|
+
when 'queued'
|
39
|
+
query = query.queued
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
query
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GoodJob
|
3
3
|
module ApplicationHelper
|
4
|
+
def relative_time(timestamp)
|
5
|
+
text = timestamp.future? ? "in #{time_ago_in_words(timestamp)}" : "#{time_ago_in_words(timestamp)} ago"
|
6
|
+
tag.time(text, datetime: timestamp, title: timestamp)
|
7
|
+
end
|
4
8
|
end
|
5
9
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GoodJob
|
3
|
+
# ActiveRecord model that represents an +ActiveJob+ job.
|
4
|
+
# Is the same record data as a {GoodJob::Execution} but only the most recent execution.
|
5
|
+
# Parent class can be configured with +GoodJob.active_record_parent_class+.
|
6
|
+
# @!parse
|
7
|
+
# class ActiveJob < ActiveRecord::Base; end
|
8
|
+
class ActiveJobJob < Object.const_get(GoodJob.active_record_parent_class)
|
9
|
+
include GoodJob::Lockable
|
10
|
+
|
11
|
+
self.table_name = 'good_jobs'
|
12
|
+
self.primary_key = 'active_job_id'
|
13
|
+
self.advisory_lockable_column = 'active_job_id'
|
14
|
+
|
15
|
+
has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id'
|
16
|
+
|
17
|
+
# Only the most-recent unretried execution represents a "Job"
|
18
|
+
default_scope { where(retried_good_job_id: nil) }
|
19
|
+
|
20
|
+
# Get Jobs with given class name
|
21
|
+
# @!method job_class
|
22
|
+
# @!scope class
|
23
|
+
# @param string [String]
|
24
|
+
# Execution class name
|
25
|
+
# @return [ActiveRecord::Relation]
|
26
|
+
scope :job_class, ->(job_class) { where("serialized_params->>'job_class' = ?", job_class) }
|
27
|
+
|
28
|
+
# First execution will run in the future
|
29
|
+
scope :scheduled, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer < 2") }
|
30
|
+
# Execution errored, will run in the future
|
31
|
+
scope :retried, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) > ?', DateTime.current).where("(serialized_params->>'executions')::integer > 1") }
|
32
|
+
# Immediate/Scheduled time to run has passed, waiting for an available thread run
|
33
|
+
scope :queued, -> { where(finished_at: nil).where('COALESCE(scheduled_at, created_at) <= ?', DateTime.current).joins_advisory_locks.where(pg_locks: { locktype: nil }) }
|
34
|
+
# Advisory locked and executing
|
35
|
+
scope :running, -> { where(finished_at: nil).joins_advisory_locks.where.not(pg_locks: { locktype: nil }) }
|
36
|
+
# Completed executing successfully
|
37
|
+
scope :finished, -> { where.not(finished_at: nil).where(error: nil) }
|
38
|
+
# Errored but will not be retried
|
39
|
+
scope :discarded, -> { where.not(finished_at: nil).where.not(error: nil) }
|
40
|
+
|
41
|
+
# Get Jobs in display order with optional keyset pagination.
|
42
|
+
# @!method display_all(after_scheduled_at: nil, after_id: nil)
|
43
|
+
# @!scope class
|
44
|
+
# @param after_scheduled_at [DateTime, String, nil]
|
45
|
+
# Display records scheduled after this time for keyset pagination
|
46
|
+
# @param after_id [Numeric, String, nil]
|
47
|
+
# Display records after this ID for keyset pagination
|
48
|
+
# @return [ActiveRecord::Relation]
|
49
|
+
scope :display_all, (lambda do |after_scheduled_at: nil, after_id: nil|
|
50
|
+
query = order(Arel.sql('COALESCE(scheduled_at, created_at) DESC, id DESC'))
|
51
|
+
if after_scheduled_at.present? && after_id.present?
|
52
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at), id) < (:after_scheduled_at, :after_id)'), after_scheduled_at: after_scheduled_at, after_id: after_id)
|
53
|
+
elsif after_scheduled_at.present?
|
54
|
+
query = query.where(Arel.sql('(COALESCE(scheduled_at, created_at)) < (:after_scheduled_at)'), after_scheduled_at: after_scheduled_at)
|
55
|
+
end
|
56
|
+
query
|
57
|
+
end)
|
58
|
+
|
59
|
+
def id
|
60
|
+
active_job_id
|
61
|
+
end
|
62
|
+
|
63
|
+
def _execution_id
|
64
|
+
attributes['id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def job_class
|
68
|
+
serialized_params['job_class']
|
69
|
+
end
|
70
|
+
|
71
|
+
def status
|
72
|
+
if finished_at.present?
|
73
|
+
if error.present?
|
74
|
+
:discarded
|
75
|
+
else
|
76
|
+
:finished
|
77
|
+
end
|
78
|
+
elsif (scheduled_at || created_at) > DateTime.current
|
79
|
+
if serialized_params.fetch('executions', 0) > 1
|
80
|
+
:retried
|
81
|
+
else
|
82
|
+
:scheduled
|
83
|
+
end
|
84
|
+
elsif running?
|
85
|
+
:running
|
86
|
+
else
|
87
|
+
:queued
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def head_execution
|
92
|
+
executions.last
|
93
|
+
end
|
94
|
+
|
95
|
+
def tail_execution
|
96
|
+
executions.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def executions_count
|
100
|
+
aj_count = serialized_params.fetch('executions', 0)
|
101
|
+
# The execution count within serialized_params is not updated
|
102
|
+
# once the underlying execution has been executed.
|
103
|
+
if status.in? [:discarded, :finished, :running]
|
104
|
+
aj_count + 1
|
105
|
+
else
|
106
|
+
aj_count
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def preserved_executions_count
|
111
|
+
executions.size
|
112
|
+
end
|
113
|
+
|
114
|
+
def recent_error
|
115
|
+
error.presence || executions[-2]&.error
|
116
|
+
end
|
117
|
+
|
118
|
+
def running?
|
119
|
+
# Avoid N+1 Query: `.joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')`
|
120
|
+
if has_attribute?(:locktype)
|
121
|
+
self['locktype'].present?
|
122
|
+
else
|
123
|
+
advisory_locked?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<% if @cron_schedules.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>Cron Job Name</th>
|
7
|
+
<th>Configuration</th>
|
8
|
+
<th>
|
9
|
+
Set
|
10
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
11
|
+
data: { bs_toggle: "collapse", bs_target: ".job-properties" },
|
12
|
+
aria: { expanded: false, controls: @cron_schedules.map { |job_key, _| "##{job_key.to_param}" }.join(" ") }
|
13
|
+
%>
|
14
|
+
</th>
|
15
|
+
<th>Class</th>
|
16
|
+
<th>Description</th>
|
17
|
+
<th>Next scheduled</th>
|
18
|
+
</thead>
|
19
|
+
<tbody>
|
20
|
+
<% @cron_schedules.each do |job_key, job| %>
|
21
|
+
<tr>
|
22
|
+
<td class="font-monospace"><%= job_key %></td>
|
23
|
+
<td class="font-monospace"><%= job[:cron] %></td>
|
24
|
+
<td>
|
25
|
+
<%=
|
26
|
+
case job[:set]
|
27
|
+
when NilClass
|
28
|
+
"None"
|
29
|
+
when Proc
|
30
|
+
"Lambda/Callable"
|
31
|
+
when Hash
|
32
|
+
tag.button("Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
33
|
+
data: { bs_toggle: "collapse", bs_target: "##{job_key.to_param}" },
|
34
|
+
aria: { expanded: false, controls: job_key.to_param }) +
|
35
|
+
tag.pre(JSON.pretty_generate(job[:set]), id: job_key.to_param, class: "collapse job-properties")
|
36
|
+
end
|
37
|
+
%>
|
38
|
+
</td>
|
39
|
+
<td class="font-monospace"><%= job[:class] %></td>
|
40
|
+
<td><%= job[:description] %></td>
|
41
|
+
<td><%= Fugit.parse_cron(job[:cron]).next_time.to_local_time %></td>
|
42
|
+
</tr>
|
43
|
+
<% end %>
|
44
|
+
</tbody>
|
45
|
+
</table>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
<% else %>
|
49
|
+
<em>No cron jobs present.</em>
|
50
|
+
<% end %>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="card my-3 p-6">
|
2
|
+
<%= render 'good_job/shared/chart', chart_data: @filter.chart_data %>
|
3
|
+
</div>
|
4
|
+
|
5
|
+
<%= render 'good_job/shared/filter', filter: @filter %>
|
6
|
+
|
7
|
+
<% if @filter.records.present? %>
|
8
|
+
<%= render 'good_job/shared/executions_table', executions: @filter.records %>
|
9
|
+
|
10
|
+
<nav aria-label="Job pagination" class="mt-3">
|
11
|
+
<ul class="pagination">
|
12
|
+
<li class="page-item">
|
13
|
+
<%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
|
14
|
+
Older executions <span aria-hidden="true">»</span>
|
15
|
+
<% end %>
|
16
|
+
</li>
|
17
|
+
</ul>
|
18
|
+
</nav>
|
19
|
+
<% else %>
|
20
|
+
<em>No executions present.</em>
|
21
|
+
<% end %>
|
@@ -0,0 +1,56 @@
|
|
1
|
+
<div class="card my-3">
|
2
|
+
<div class="table-responsive">
|
3
|
+
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<th>ActiveJob ID</th>
|
7
|
+
<th>Execution ID</th>
|
8
|
+
<th>Job Class</th>
|
9
|
+
<th>Queue</th>
|
10
|
+
<th>Scheduled At</th>
|
11
|
+
<th>Error</th>
|
12
|
+
<th>
|
13
|
+
ActiveJob Params
|
14
|
+
<%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
15
|
+
data: { bs_toggle: "collapse", bs_target: ".job-params" },
|
16
|
+
aria: { expanded: false, controls: executions.map { |execution| "##{dom_id(execution, "params")}" }.join(" ") }
|
17
|
+
%>
|
18
|
+
</th>
|
19
|
+
<th>Actions</th>
|
20
|
+
</tr>
|
21
|
+
</thead>
|
22
|
+
<tbody>
|
23
|
+
<% executions.each do |execution| %>
|
24
|
+
<tr id="<%= dom_id(execution) %>">
|
25
|
+
<td>
|
26
|
+
<%= link_to job_path(execution.serialized_params['job_id']) do %>
|
27
|
+
<code><%= execution.active_job_id %></code>
|
28
|
+
<% end %>
|
29
|
+
</td>
|
30
|
+
<td>
|
31
|
+
<%= link_to job_path(execution.active_job_id, anchor: dom_id(execution)) do %>
|
32
|
+
<code><%= execution.id %></code>
|
33
|
+
<% end %>
|
34
|
+
</td>
|
35
|
+
<td><%= execution.serialized_params['job_class'] %></td>
|
36
|
+
<td><%= execution.queue_name %></td>
|
37
|
+
<td><%= relative_time(execution.scheduled_at || execution.created_at) %></td>
|
38
|
+
<td class="text-break"><%= truncate(execution.error, length: 1_000) %></td>
|
39
|
+
<td>
|
40
|
+
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
41
|
+
data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
|
42
|
+
aria: { expanded: false, controls: dom_id(execution, "params") }
|
43
|
+
%>
|
44
|
+
<%= tag.pre JSON.pretty_generate(execution.serialized_params), id: dom_id(execution, "params"), class: "collapse job-params" %>
|
45
|
+
</td>
|
46
|
+
<td>
|
47
|
+
<%= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
|
48
|
+
<%= render "good_job/shared/icons/trash" %>
|
49
|
+
<% end %>
|
50
|
+
</td>
|
51
|
+
</tr>
|
52
|
+
<% end %>
|
53
|
+
</tbody>
|
54
|
+
</table>
|
55
|
+
</div>
|
56
|
+
</div>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<div class='card mb-2'>
|
2
|
+
<div class='card-body d-flex flex-wrap'>
|
3
|
+
|
4
|
+
<div class='me-4'>
|
5
|
+
<small>Filter by job class</small>
|
6
|
+
<br>
|
7
|
+
<% @filter.job_classes.each do |name, count| %>
|
8
|
+
<% if params[:job_class] == name %>
|
9
|
+
<%= link_to(@filter.to_params(job_class: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
10
|
+
<%= name %> (<%= count %>)
|
11
|
+
<% end %>
|
12
|
+
<% else %>
|
13
|
+
<%= link_to(@filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
14
|
+
<%= name %> (<%= count %>)
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class='me-4'>
|
21
|
+
<small>Filter by state</small>
|
22
|
+
<br>
|
23
|
+
<% @filter.states.each do |name, count| %>
|
24
|
+
<% if params[:state] == name %>
|
25
|
+
<%= link_to(@filter.to_params(state: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
26
|
+
<%= name %> (<%= count %>)
|
27
|
+
<% end %>
|
28
|
+
<% else %>
|
29
|
+
<%= link_to(@filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
30
|
+
<%= name %> (<%= count %>)
|
31
|
+
<% end %>
|
32
|
+
<% end %>
|
33
|
+
<% end %>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div>
|
37
|
+
<small>Filter by queue</small>
|
38
|
+
<br>
|
39
|
+
<% @filter.queues.each do |name, count| %>
|
40
|
+
<% if params[:queue_name] == name %>
|
41
|
+
<%= link_to(@filter.to_params(queue_name: nil), class: 'btn btn-sm btn-outline-secondary active', role: "button", "aria-pressed": true) do %>
|
42
|
+
<%= name %> (<%= count %>)
|
43
|
+
<% end %>
|
44
|
+
<% else %>
|
45
|
+
<%= link_to(@filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
|
46
|
+
<%= name %> (<%= count %>)
|
47
|
+
<% end %>
|
48
|
+
<% end %>
|
49
|
+
<% end %>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
@@ -3,11 +3,12 @@
|
|
3
3
|
<table class="table card-table table-bordered table-hover table-sm mb-0">
|
4
4
|
<thead>
|
5
5
|
<tr>
|
6
|
-
<th>GoodJob ID</th>
|
7
6
|
<th>ActiveJob ID</th>
|
7
|
+
<th>State</th>
|
8
8
|
<th>Job Class</th>
|
9
9
|
<th>Queue</th>
|
10
10
|
<th>Scheduled At</th>
|
11
|
+
<th>Executions</th>
|
11
12
|
<th>Error</th>
|
12
13
|
<th>
|
13
14
|
ActiveJob Params
|
@@ -22,12 +23,19 @@
|
|
22
23
|
<tbody>
|
23
24
|
<% jobs.each do |job| %>
|
24
25
|
<tr id="<%= dom_id(job) %>">
|
25
|
-
<td
|
26
|
-
|
27
|
-
|
26
|
+
<td>
|
27
|
+
<%= link_to job_path(job.id) do %>
|
28
|
+
<code><%= job.id %></code>
|
29
|
+
<% end %>
|
30
|
+
</td>
|
31
|
+
<td>
|
32
|
+
<span class="badge bg-secondary"><%= job.status %></span>
|
33
|
+
</td>
|
34
|
+
<td><%= job.job_class %></td>
|
28
35
|
<td><%= job.queue_name %></td>
|
29
|
-
<td><%= job.scheduled_at || job.created_at %></td>
|
30
|
-
<td
|
36
|
+
<td><%= relative_time(job.scheduled_at || job.created_at) %></td>
|
37
|
+
<td><%= job.executions_count %></td>
|
38
|
+
<td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
|
31
39
|
<td>
|
32
40
|
<%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
|
33
41
|
data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
|
@@ -35,11 +43,11 @@
|
|
35
43
|
%>
|
36
44
|
<%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
|
37
45
|
</td>
|
38
|
-
<td
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
</td
|
46
|
+
<!-- <td>-->
|
47
|
+
<%#= button_to execution_path(execution.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete execution" do %>
|
48
|
+
<%#= render "good_job/shared/icons/trash" %>
|
49
|
+
<%# end %>
|
50
|
+
<!-- </td>-->
|
43
51
|
</tr>
|
44
52
|
<% end %>
|
45
53
|
</tbody>
|
@@ -23,9 +23,18 @@
|
|
23
23
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
24
24
|
<ul class="navbar-nav me-auto">
|
25
25
|
<li class="nav-item">
|
26
|
-
<%= link_to root_path, class: ["nav-link", ("active" if current_page?(root_path))]
|
27
|
-
|
28
|
-
|
26
|
+
<%= link_to "All Executions", root_path, class: ["nav-link", ("active" if current_page?(root_path))] %>
|
27
|
+
</li>
|
28
|
+
<li class="nav-item">
|
29
|
+
<%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
|
30
|
+
</li>
|
31
|
+
<li class="nav-item">
|
32
|
+
<%= link_to "Cron Schedules", cron_schedules_path, class: ["nav-link", ("active" if current_page?(cron_schedules_path))] %>
|
33
|
+
</li>
|
34
|
+
<li class="nav-item">
|
35
|
+
<div class="nav-link">
|
36
|
+
<span class="badge bg-secondary">More views coming soon</span>
|
37
|
+
</div>
|
29
38
|
</li>
|
30
39
|
|
31
40
|
<!-- Coming Soon
|
@@ -59,7 +68,7 @@
|
|
59
68
|
</div>
|
60
69
|
<% elsif alert %>
|
61
70
|
<div class="alert alert-warning alert-dismissible fade show d-flex align-items-center offset-md-3 col-6" role="alert">
|
62
|
-
<%= render "good_job/shared/icons/
|
71
|
+
<%= render "good_job/shared/icons/exclamation", class: "flex-shrink-0 me-2" %>
|
63
72
|
<div><%= alert %></div>
|
64
73
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
65
74
|
</div>
|
data/engine/config/routes.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
GoodJob::Engine.routes.draw do
|
3
|
-
root to: '
|
4
|
-
resources :
|
5
|
-
resources :jobs, only: %i[
|
3
|
+
root to: 'executions#index'
|
4
|
+
resources :cron_schedules, only: %i[index]
|
5
|
+
resources :jobs, only: %i[index show]
|
6
|
+
resources :executions, only: %i[destroy]
|
6
7
|
|
7
8
|
scope controller: :assets do
|
8
9
|
constraints(format: :css) do
|