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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +12 -4
  4. data/engine/app/assets/scripts.js +1 -0
  5. data/engine/app/assets/style.css +5 -0
  6. data/engine/app/assets/vendor/chartjs/chart.min.js +13 -0
  7. data/engine/app/assets/vendor/rails_ujs.js +747 -0
  8. data/engine/app/charts/good_job/scheduled_by_queue_chart.rb +69 -0
  9. data/engine/app/controllers/good_job/assets_controller.rb +8 -4
  10. data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
  11. data/engine/app/controllers/good_job/jobs_controller.rb +36 -0
  12. data/engine/app/filters/good_job/base_filter.rb +18 -56
  13. data/engine/app/filters/good_job/executions_filter.rb +9 -8
  14. data/engine/app/filters/good_job/jobs_filter.rb +12 -9
  15. data/engine/app/views/good_job/cron_entries/index.html.erb +51 -0
  16. data/engine/app/views/good_job/cron_entries/show.html.erb +4 -0
  17. data/engine/app/views/good_job/{shared/_executions_table.erb → executions/_table.erb} +1 -1
  18. data/engine/app/views/good_job/executions/index.html.erb +2 -2
  19. data/engine/app/views/good_job/{shared/_jobs_table.erb → jobs/_table.erb} +18 -6
  20. data/engine/app/views/good_job/jobs/index.html.erb +15 -2
  21. data/engine/app/views/good_job/jobs/show.html.erb +2 -2
  22. data/engine/app/views/good_job/shared/_chart.erb +19 -46
  23. data/engine/app/views/good_job/shared/_filter.erb +27 -13
  24. data/engine/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +5 -0
  25. data/engine/app/views/good_job/shared/icons/_play.html.erb +4 -0
  26. data/engine/app/views/good_job/shared/icons/_skip_forward.html.erb +4 -0
  27. data/engine/app/views/good_job/shared/icons/_stop.html.erb +4 -0
  28. data/engine/app/views/layouts/good_job/base.html.erb +6 -4
  29. data/engine/config/routes.rb +17 -4
  30. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +2 -0
  31. data/lib/generators/good_job/templates/update/migrations/02_add_cron_at_to_good_jobs.rb.erb +14 -0
  32. data/lib/generators/good_job/templates/update/migrations/03_add_cron_key_cron_at_index_to_good_jobs.rb.erb +20 -0
  33. data/lib/good_job/active_job_job.rb +228 -0
  34. data/lib/good_job/configuration.rb +1 -1
  35. data/lib/good_job/cron_entry.rb +78 -5
  36. data/lib/good_job/cron_manager.rb +4 -6
  37. data/lib/good_job/current_thread.rb +38 -5
  38. data/lib/good_job/execution.rb +53 -39
  39. data/lib/good_job/filterable.rb +42 -0
  40. data/lib/good_job/notifier.rb +17 -7
  41. data/lib/good_job/version.rb +1 -1
  42. metadata +31 -21
  43. data/engine/app/assets/vendor/chartist/chartist.css +0 -613
  44. data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
  45. data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
  46. data/engine/app/models/good_job/active_job_job.rb +0 -127
  47. 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 chartist_css
19
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.css")
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 chartist_js
23
- render file: GoodJob::Engine.root.join("app", "assets", "vendor", "chartist", "chartist.js")
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
- attr_accessor :params
4
+ DEFAULT_LIMIT = 25
5
+ EMPTY = '[none]'
5
6
 
6
- def initialize(params)
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, 25))
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
- }.merge(override).delete_if { |_, v| v.nil? }
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 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
- }
53
+ def filtered_query
54
+ raise NotImplementedError
89
55
  end
90
56
 
91
57
  private
92
58
 
93
- def base_query
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
- query = query.job_class(params[:job_class]) if params[:job_class]
24
- query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
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 %>
@@ -0,0 +1,4 @@
1
+ <h1 class="mb-3">Cron Entry Key: <code><%= @cron_entry.id %></code></h1>
2
+
3
+ <%= render 'good_job/shared/filter', filter: @jobs_filter %>
4
+ <%= render 'good_job/jobs/table', jobs: @jobs_filter.records %>
@@ -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.chart_data %>
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/shared/executions_table', executions: @filter.records %>
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
- <!-- <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>-->
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.chart_data %>
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
- <%= render 'good_job/shared/jobs_table', jobs: @filter.records %>
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">&raquo;</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/shared/executions_table', executions: @executions %>
3
+ <%= render 'good_job/executions/table', executions: @executions %>
@@ -1,52 +1,25 @@
1
- <div id="chart"></div>
1
+ <div class="chart-wrapper">
2
+ <canvas id="chart"></canvas>
3
+ </div>
2
4
 
3
5
  <%= javascript_tag nonce: true do %>
4
- new Chartist.Line('#chart', <%== chart_data.to_json %>, {
5
- height: '300px',
6
- fullWidth: true,
7
- chartPadding: {
8
- right: 40,
9
- top: 20,
10
- bottom: 20
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
- axisX: {
13
- labelInterpolationFnc: function(value, index) {
14
- return index % 3 === 0 ? value : null;
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
- <% @filter.job_classes.each do |name, count| %>
6
+ <% filter.job_classes.each do |name, count| %>
8
7
  <% 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 %>
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(@filter.to_params(job_class: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
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
- <% @filter.states.each do |name, count| %>
22
+ <% filter.states.each do |name, count| %>
24
23
  <% 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 %>
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(@filter.to_params(state: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
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
- <% @filter.queues.each do |name, count| %>
38
+ <% filter.queues.each do |name, count| %>
40
39
  <% 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 %>
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(@filter.to_params(queue_name: name), class: 'btn btn-sm btn-outline-secondary', role: "button") do %>
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>