good_job 2.5.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/README.md +58 -1
  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/base_controller.rb +19 -0
  11. data/engine/app/controllers/good_job/cron_entries_controller.rb +19 -0
  12. data/engine/app/filters/good_job/base_filter.rb +12 -54
  13. data/engine/app/filters/good_job/executions_filter.rb +9 -8
  14. data/engine/app/filters/good_job/jobs_filter.rb +9 -8
  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} +4 -4
  20. data/engine/app/views/good_job/jobs/index.html.erb +2 -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/_play.html.erb +4 -0
  25. data/engine/app/views/layouts/good_job/base.html.erb +6 -4
  26. data/engine/config/routes.rb +10 -3
  27. data/{engine/app/models → lib}/good_job/active_job_job.rb +2 -19
  28. data/lib/good_job/cli.rb +8 -0
  29. data/lib/good_job/configuration.rb +8 -1
  30. data/lib/good_job/cron_entry.rb +75 -4
  31. data/lib/good_job/cron_manager.rb +1 -5
  32. data/lib/good_job/current_thread.rb +26 -8
  33. data/lib/good_job/execution.rb +16 -31
  34. data/lib/good_job/filterable.rb +42 -0
  35. data/lib/good_job/notifier.rb +17 -7
  36. data/lib/good_job/probe_server.rb +51 -0
  37. data/lib/good_job/version.rb +1 -1
  38. metadata +41 -21
  39. data/engine/app/assets/vendor/chartist/chartist.css +0 -613
  40. data/engine/app/assets/vendor/chartist/chartist.js +0 -4516
  41. data/engine/app/controllers/good_job/cron_schedules_controller.rb +0 -9
  42. 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
@@ -5,6 +5,25 @@ module GoodJob
5
5
 
6
6
  around_action :switch_locale
7
7
 
8
+ content_security_policy do |policy|
9
+ policy.default_src(:none) if policy.default_src.blank?
10
+ policy.connect_src(:self) if policy.connect_src.blank?
11
+ policy.base_uri(:none) if policy.base_uri.blank?
12
+ policy.font_src(:self) if policy.font_src.blank?
13
+ policy.img_src(:self, :data) if policy.img_src.blank?
14
+ policy.object_src(:none) if policy.object_src.blank?
15
+ policy.script_src(:self) if policy.script_src.blank?
16
+ policy.style_src(:self) if policy.style_src.blank?
17
+ policy.form_action(:self) if policy.form_action.blank?
18
+ policy.frame_ancestors(:none) if policy.frame_ancestors.blank?
19
+ end
20
+
21
+ before_action do
22
+ next if request.content_security_policy_nonce_generator
23
+
24
+ request.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
25
+ end
26
+
8
27
  private
9
28
 
10
29
  def switch_locale(&action)
@@ -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
@@ -2,11 +2,13 @@
2
2
  module GoodJob
3
3
  class BaseFilter
4
4
  DEFAULT_LIMIT = 25
5
+ EMPTY = '[none]'
5
6
 
6
- attr_accessor :params
7
+ attr_accessor :params, :base_query
7
8
 
8
- def initialize(params)
9
+ def initialize(params, base_query = nil)
9
10
  @params = params
11
+ @base_query = base_query || default_base_query
10
12
  end
11
13
 
12
14
  def records
@@ -24,13 +26,13 @@ module GoodJob
24
26
 
25
27
  def job_classes
26
28
  base_query.group("serialized_params->>'job_class'").count
27
- .sort_by { |name, _count| name }
29
+ .sort_by { |name, _count| name.to_s }
28
30
  .to_h
29
31
  end
30
32
 
31
33
  def queues
32
34
  base_query.group(:queue_name).count
33
- .sort_by { |name, _count| name }
35
+ .sort_by { |name, _count| name.to_s || EMPTY }
34
36
  .to_h
35
37
  end
36
38
 
@@ -38,67 +40,23 @@ module GoodJob
38
40
  raise NotImplementedError
39
41
  end
40
42
 
41
- def to_params(override)
43
+ def to_params(override = {})
42
44
  {
43
45
  job_class: params[:job_class],
44
46
  limit: params[:limit],
45
47
  queue_name: params[:queue_name],
48
+ query: params[:query],
46
49
  state: params[:state],
47
- }.merge(override).delete_if { |_, v| v.nil? }
50
+ }.merge(override).delete_if { |_, v| v.blank? }
48
51
  end
49
52
 
50
- def chart_data
51
- count_query = Arel.sql(GoodJob::Execution.pg_or_jdbc_query(<<~SQL.squish))
52
- SELECT *
53
- FROM generate_series(
54
- date_trunc('hour', $1::timestamp),
55
- date_trunc('hour', $2::timestamp),
56
- '1 hour'
57
- ) timestamp
58
- LEFT JOIN (
59
- SELECT
60
- date_trunc('hour', scheduled_at) AS scheduled_at,
61
- queue_name,
62
- count(*) AS count
63
- FROM (
64
- #{filtered_query.except(:select).select('queue_name', 'COALESCE(good_jobs.scheduled_at, good_jobs.created_at)::timestamp AS scheduled_at').to_sql}
65
- ) sources
66
- GROUP BY date_trunc('hour', scheduled_at), queue_name
67
- ) sources ON sources.scheduled_at = timestamp
68
- ORDER BY timestamp ASC
69
- SQL
70
-
71
- current_time = Time.current
72
- binds = [[nil, current_time - 1.day], [nil, current_time]]
73
- executions_data = GoodJob::Execution.connection.exec_query(count_query, "GoodJob Dashboard Chart", binds)
74
-
75
- queue_names = executions_data.map { |d| d['queue_name'] }.uniq
76
- labels = []
77
- queues_data = executions_data.to_a.group_by { |d| d['timestamp'] }.each_with_object({}) do |(timestamp, values), hash|
78
- labels << timestamp.in_time_zone.strftime('%H:%M %z')
79
- queue_names.each do |queue_name|
80
- (hash[queue_name] ||= []) << values.find { |d| d['queue_name'] == queue_name }&.[]('count')
81
- end
82
- end
83
-
84
- {
85
- labels: labels,
86
- series: queues_data.map do |queue, data|
87
- {
88
- name: queue,
89
- data: data,
90
- }
91
- end,
92
- }
53
+ def filtered_query
54
+ raise NotImplementedError
93
55
  end
94
56
 
95
57
  private
96
58
 
97
- def base_query
98
- raise NotImplementedError
99
- end
100
-
101
- def filtered_query
59
+ def default_base_query
102
60
  raise NotImplementedError
103
61
  end
104
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,18 +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
16
  query = base_query.includes(:executions)
23
17
  .joins_advisory_locks.select('good_jobs.*', 'pg_locks.locktype AS locktype')
24
18
 
25
- query = query.job_class(params[:job_class]) if params[:job_class]
26
- query = query.where(queue_name: params[:queue_name]) if params[:queue_name]
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?
27
22
 
28
23
  if params[:state]
29
24
  case params[:state]
@@ -44,5 +39,11 @@ module GoodJob
44
39
 
45
40
  query
46
41
  end
42
+
43
+ private
44
+
45
+ def default_base_query
46
+ GoodJob::ActiveJobJob.all
47
+ end
47
48
  end
48
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>
@@ -46,16 +46,16 @@
46
46
  <td>
47
47
  <div class="text-nowrap">
48
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" do %>
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
50
  <%= render "good_job/shared/icons/skip_forward" %>
51
51
  <% end %>
52
52
 
53
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" do %>
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
55
  <%= render "good_job/shared/icons/stop" %>
56
56
  <% end %>
57
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" do %>
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
59
  <%= render "good_job/shared/icons/arrow_clockwise" %>
60
60
  <% end %>
61
61
  </div>
@@ -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/jobs_table', jobs: @filter.records %>
8
+ <%= render 'good_job/jobs/table', jobs: @filter.records %>
9
9
  <nav aria-label="Job pagination" class="mt-3">
10
10
  <ul class="pagination">
11
11
  <li class="page-item">
@@ -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,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/play/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play" viewBox="0 0 16 16">
3
+ <path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z" />
4
+ </svg>
@@ -1,16 +1,18 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <title>Good Job Dashboard</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
8
8
  <%= stylesheet_link_tag bootstrap_path(format: :css, v: GoodJob::VERSION) %>
9
- <%= stylesheet_link_tag chartist_path(format: :css, v: GoodJob::VERSION) %>
10
9
  <%= stylesheet_link_tag style_path(format: :css, v: GoodJob::VERSION) %>
11
10
 
12
11
  <%= javascript_include_tag bootstrap_path(format: :js, v: GoodJob::VERSION) %>
13
- <%= javascript_include_tag chartist_path(format: :js, v: GoodJob::VERSION) %>
12
+ <%= javascript_include_tag chartjs_path(format: :js, v: GoodJob::VERSION) %>
13
+ <%= javascript_include_tag scripts_path(format: :js, v: GoodJob::VERSION) %>
14
+
15
+ <%= javascript_include_tag rails_ujs_path(format: :js, v: GoodJob::VERSION) %>
14
16
  </head>
15
17
  <body>
16
18
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
@@ -29,7 +31,7 @@
29
31
  <%= link_to "All Jobs", jobs_path, class: ["nav-link", ("active" if current_page?(jobs_path))] %>
30
32
  </li>
31
33
  <li class="nav-item">
32
- <%= link_to "Cron Schedules", cron_schedules_path, class: ["nav-link", ("active" if current_page?(cron_schedules_path))] %>
34
+ <%= link_to "Cron Schedules", cron_entries_path, class: ["nav-link", ("active" if current_page?(cron_entries_path))] %>
33
35
  </li>
34
36
  <li class="nav-item">
35
37
  <div class="nav-link">