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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +32 -0
  4. data/engine/app/assets/vendor/bootstrap/bootstrap.bundle.min.js +2 -2
  5. data/engine/app/assets/vendor/bootstrap/bootstrap.min.css +2 -2
  6. data/engine/app/controllers/good_job/cron_schedules_controller.rb +9 -0
  7. data/engine/app/controllers/good_job/executions_controller.rb +14 -0
  8. data/engine/app/controllers/good_job/jobs_controller.rb +8 -4
  9. data/engine/app/filters/good_job/base_filter.rb +101 -0
  10. data/engine/app/filters/good_job/executions_filter.rb +40 -0
  11. data/engine/app/filters/good_job/jobs_filter.rb +46 -0
  12. data/engine/app/helpers/good_job/application_helper.rb +4 -0
  13. data/engine/app/models/good_job/active_job_job.rb +127 -0
  14. data/engine/app/views/good_job/cron_schedules/index.html.erb +50 -0
  15. data/engine/app/views/good_job/executions/index.html.erb +21 -0
  16. data/engine/app/views/good_job/jobs/index.html.erb +7 -0
  17. data/engine/app/views/good_job/jobs/show.html.erb +3 -0
  18. data/engine/app/views/good_job/shared/_executions_table.erb +56 -0
  19. data/engine/app/views/good_job/shared/_filter.erb +52 -0
  20. data/engine/app/views/good_job/shared/_jobs_table.erb +19 -11
  21. data/engine/app/views/layouts/good_job/base.html.erb +13 -4
  22. data/engine/config/routes.rb +4 -3
  23. data/lib/good_job/active_job_extensions/concurrency.rb +6 -6
  24. data/lib/good_job/adapter.rb +10 -10
  25. data/lib/good_job/cron_manager.rb +3 -3
  26. data/lib/good_job/{current_execution.rb → current_thread.rb} +8 -8
  27. data/lib/good_job/execution.rb +308 -0
  28. data/lib/good_job/job.rb +6 -294
  29. data/lib/good_job/job_performer.rb +2 -2
  30. data/lib/good_job/log_subscriber.rb +4 -4
  31. data/lib/good_job/notifier.rb +3 -3
  32. data/lib/good_job/railtie.rb +2 -2
  33. data/lib/good_job/scheduler.rb +3 -3
  34. data/lib/good_job/version.rb +1 -1
  35. data/lib/good_job.rb +2 -2
  36. metadata +16 -7
  37. data/engine/app/controllers/good_job/active_jobs_controller.rb +0 -9
  38. data/engine/app/controllers/good_job/dashboards_controller.rb +0 -106
  39. data/engine/app/views/good_job/active_jobs/show.html.erb +0 -1
  40. data/engine/app/views/good_job/dashboards/index.html.erb +0 -54
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ class CronSchedulesController < GoodJob::BaseController
4
+ def index
5
+ configuration = GoodJob::Configuration.new({})
6
+ @cron_schedules = configuration.cron
7
+ end
8
+ end
9
+ end
@@ -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 destroy
5
- deleted_count = GoodJob::Job.where(id: params[:id]).delete_all
6
- message = deleted_count.positive? ? { notice: "Job deleted" } : { alert: "Job not deleted" }
7
- redirect_to root_path, **message
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&nbsp;
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">&raquo;</span>
15
+ <% end %>
16
+ </li>
17
+ </ul>
18
+ </nav>
19
+ <% else %>
20
+ <em>No executions present.</em>
21
+ <% end %>
@@ -0,0 +1,7 @@
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
+ <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
@@ -0,0 +1,3 @@
1
+ <h1>ActiveJob ID: <code><%= @executions.first.id %></code></h1>
2
+
3
+ <%= render 'good_job/shared/executions_table', executions: @executions %>
@@ -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&nbsp;
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&nbsp;
@@ -22,12 +23,19 @@
22
23
  <tbody>
23
24
  <% jobs.each do |job| %>
24
25
  <tr id="<%= dom_id(job) %>">
25
- <td><%= link_to job.id, active_job_path(job.serialized_params['job_id'], anchor: dom_id(job)) %></td>
26
- <td><%= link_to job.serialized_params['job_id'], active_job_path(job.serialized_params['job_id']) %></td>
27
- <td><%= job.serialized_params['job_class'] %></td>
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 class="text-break"><%= truncate(job.error, length: 1_000) %></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
- <%= button_to job_path(job.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete job" do %>
40
- <%= render "good_job/shared/icons/trash" %>
41
- <% end %>
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))] do %>
27
- All jobs <span class="badge bg-secondary">More views coming soon</span>
28
- <% end %>
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/check", class: "flex-shrink-0 me-2" %>
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>
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  GoodJob::Engine.routes.draw do
3
- root to: 'dashboards#index'
4
- resources :active_jobs, only: %i[show]
5
- resources :jobs, only: %i[destroy]
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