good_job 4.13.0 → 4.13.2

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/app/controllers/good_job/batches_controller.rb +1 -1
  4. data/app/controllers/good_job/cron_entries_controller.rb +3 -3
  5. data/app/controllers/good_job/frontends_controller.rb +15 -10
  6. data/app/controllers/good_job/jobs_controller.rb +7 -7
  7. data/app/controllers/good_job/pauses_controller.rb +2 -2
  8. data/app/frontend/good_job/application.js +23 -9
  9. data/app/frontend/good_job/modules/form_controller.js +11 -0
  10. data/app/frontend/good_job/vendor/turbo.js +34 -0
  11. data/app/models/good_job/batch.rb +8 -2
  12. data/app/models/good_job/batch_record.rb +35 -11
  13. data/app/models/good_job/job.rb +61 -29
  14. data/app/models/good_job/process.rb +2 -0
  15. data/app/models/good_job/setting.rb +2 -0
  16. data/app/views/good_job/batches/_jobs.erb +4 -4
  17. data/app/views/good_job/batches/_table.erb +2 -2
  18. data/app/views/good_job/batches/show.html.erb +1 -1
  19. data/app/views/good_job/cron_entries/index.html.erb +2 -2
  20. data/app/views/good_job/jobs/_table.erb +12 -9
  21. data/app/views/good_job/jobs/show.html.erb +4 -4
  22. data/app/views/good_job/pauses/_group.html.erb +1 -1
  23. data/app/views/good_job/pauses/_pause.html.erb +1 -1
  24. data/app/views/good_job/shared/_filter.erb +4 -14
  25. data/app/views/good_job/shared/_navbar.erb +6 -6
  26. data/app/views/layouts/good_job/application.html.erb +10 -6
  27. data/config/brakeman.ignore +25 -25
  28. data/lib/generators/good_job/templates/install/migrations/create_good_jobs.rb.erb +1 -1
  29. data/lib/generators/good_job/templates/update/migrations/05_add_index_good_jobs_finished_at_for_cleanup.rb.erb +17 -0
  30. data/lib/generators/good_job/templates/update/migrations/06_remove_extraneous_finished_at_index.rb.erb +17 -0
  31. data/lib/good_job/version.rb +1 -1
  32. data/lib/good_job.rb +5 -1
  33. metadata +6 -4
  34. data/app/frontend/good_job/modules/document_ready.js +0 -7
  35. data/app/frontend/good_job/vendor/rails_ujs.js +0 -7
@@ -59,25 +59,47 @@ module GoodJob
59
59
  job_discarded = job && job.finished_at.present? && job.error.present?
60
60
  buffer = GoodJob::Adapter::InlineBuffer.capture do
61
61
  advisory_lock_maybe(lock) do
62
- Batch.within_thread(batch_id: nil, batch_callback_id: id) do
63
- reload
64
-
65
- if job_discarded && !discarded_at
66
- update(discarded_at: Time.current)
67
- on_discard.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard }) if on_discard.present?
62
+ reload
63
+
64
+ if job_discarded && !discarded_at
65
+ update(discarded_at: Time.current)
66
+
67
+ if on_discard.present?
68
+ discard_job_class = on_discard.constantize
69
+ Job.defer_after_commit_maybe(discard_job_class) do
70
+ Batch.within_thread(batch_id: nil, batch_callback_id: id) do
71
+ discard_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard })
72
+ end
73
+ end
68
74
  end
75
+ end
69
76
 
70
- if enqueued_at && !(self.class.jobs_finished_at_migrated? ? jobs_finished_at : finished_at) && jobs.where(finished_at: nil).none?
71
- self.class.jobs_finished_at_migrated? ? update(jobs_finished_at: Time.current) : update(finished_at: Time.current)
77
+ if enqueued_at && !(self.class.jobs_finished_at_migrated? ? jobs_finished_at : finished_at) && jobs.where(finished_at: nil).none?
78
+ self.class.jobs_finished_at_migrated? ? update(jobs_finished_at: Time.current) : update(finished_at: Time.current)
72
79
 
73
- on_success.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success }) if !discarded_at && on_success.present?
74
- on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish }) if on_finish.present?
80
+ if !discarded_at && on_success.present?
81
+ success_job_class = on_success.constantize
82
+ Job.defer_after_commit_maybe(success_job_class) do
83
+ Batch.within_thread(batch_id: nil, batch_callback_id: id) do
84
+ success_job_class.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success })
85
+ end
86
+ end
75
87
  end
76
88
 
77
- update(finished_at: Time.current) if !finished_at && self.class.jobs_finished_at_migrated? && jobs_finished? && callback_jobs.where(finished_at: nil).none?
89
+ if on_finish.present?
90
+ finish_job_class = on_finish.constantize
91
+ Job.defer_after_commit_maybe(finish_job_class) do
92
+ Batch.within_thread(batch_id: nil, batch_callback_id: id) do
93
+ on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish })
94
+ end
95
+ end
96
+ end
78
97
  end
98
+
99
+ update(finished_at: Time.current) if !finished_at && self.class.jobs_finished_at_migrated? && jobs_finished? && callback_jobs.where(finished_at: nil).none?
79
100
  end
80
101
  end
102
+
81
103
  buffer.call
82
104
  end
83
105
 
@@ -123,3 +145,5 @@ module GoodJob
123
145
  end
124
146
  end
125
147
  end
148
+
149
+ ActiveSupport.run_load_hooks(:good_job_batch_record, GoodJob::BatchRecord)
@@ -250,8 +250,8 @@ module GoodJob
250
250
  )
251
251
  end
252
252
 
253
- def job_class_index_migrated?
254
- return true if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_job_class)
253
+ def historic_finished_at_index_migrated?
254
+ return true unless connection.index_name_exists?(:good_jobs, :index_good_jobs_jobs_on_finished_at)
255
255
 
256
256
  migration_pending_warning!
257
257
  false
@@ -420,6 +420,33 @@ module GoodJob
420
420
  [error.class.to_s, ERROR_MESSAGE_SEPARATOR, error.message].join
421
421
  end
422
422
 
423
+ # When code needs to optionally handle enqueue_after_transaction_commit
424
+ def self.defer_after_commit_maybe(good_job_or_active_job_classes)
425
+ if enqueue_after_commit?(good_job_or_active_job_classes)
426
+ ActiveRecord.after_all_transactions_commit { yield(true) }
427
+ else
428
+ yield(false)
429
+ end
430
+ end
431
+
432
+ def self.enqueue_after_commit?(good_job_or_active_job_classes)
433
+ good_job_or_active_job_classes = Array(good_job_or_active_job_classes)
434
+
435
+ feature_exists = ActiveRecord.respond_to?(:after_all_transactions_commit)
436
+ feature_exists && good_job_or_active_job_classes.any? do |klass|
437
+ active_job_class = case klass
438
+ when String
439
+ klass.constantize
440
+ when Job
441
+ klass.job_class.constantize
442
+ else
443
+ klass
444
+ end
445
+
446
+ active_job_class.respond_to?(:enqueue_after_transaction_commit)
447
+ end
448
+ end
449
+
423
450
  # TODO: it would be nice to enforce these values at the model
424
451
  # validates :active_job_id, presence: true
425
452
  # validates :scheduled_at, presence: true
@@ -499,41 +526,46 @@ module GoodJob
499
526
  # This action will create a new {Execution} record for the job.
500
527
  # @return [ActiveJob::Base]
501
528
  def retry_job
502
- with_advisory_lock do
503
- reload
504
- active_job = self.active_job(ignore_deserialization_errors: true)
505
-
506
- raise ActiveJobDeserializationError if active_job.nil?
507
- raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
508
- raise ActionForStateMismatchError if finished_at.blank? || error.blank?
509
-
510
- # Update the executions count because the previous execution will not have been preserved
511
- # Do not update `exception_executions` because that comes from rescue_from's arguments
512
- active_job.executions = (active_job.executions || 0) + 1
513
-
514
- begin
515
- error_class, error_message = error.split(ERROR_MESSAGE_SEPARATOR).map(&:strip)
516
- error = error_class.constantize.new(error_message)
517
- rescue StandardError
518
- error = StandardError.new(error)
519
- end
529
+ Rails.application.executor.wrap do
530
+ with_advisory_lock do
531
+ reload
532
+ active_job = self.active_job(ignore_deserialization_errors: true)
533
+
534
+ raise ActiveJobDeserializationError if active_job.nil?
535
+ raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
536
+ raise ActionForStateMismatchError if finished_at.blank? || error.blank?
537
+
538
+ # Update the executions count because the previous execution will not have been preserved
539
+ # Do not update `exception_executions` because that comes from rescue_from's arguments
540
+ active_job.executions = (active_job.executions || 0) + 1
541
+
542
+ begin
543
+ error_class, error_message = error.split(ERROR_MESSAGE_SEPARATOR).map(&:strip)
544
+ error = error_class.constantize.new(error_message)
545
+ rescue StandardError
546
+ error = StandardError.new(error)
547
+ end
520
548
 
521
- new_active_job = nil
522
- GoodJob::CurrentThread.within do |current_thread|
523
- current_thread.job = self
524
- current_thread.retry_now = true
549
+ new_active_job = nil
525
550
 
526
551
  transaction do
527
- # NOTE: Required until fixed in rails https://github.com/rails/rails/pull/52121
528
- I18n.with_locale(active_job.locale) do
529
- new_active_job = active_job.retry_job(wait: 0, error: error)
552
+ Job.defer_after_commit_maybe(active_job.class) do
553
+ GoodJob::CurrentThread.within do |current_thread|
554
+ current_thread.job = self
555
+ current_thread.retry_now = true
556
+
557
+ # NOTE: I18n.with_locale necessary until fixed in rails https://github.com/rails/rails/pull/52121
558
+ I18n.with_locale(active_job.locale) do
559
+ new_active_job = active_job.retry_job(wait: 0, error: error)
560
+ end
561
+ end
530
562
  end
531
563
  self.error_event = :retried if error
532
564
  save!
533
565
  end
534
- end
535
566
 
536
- new_active_job
567
+ new_active_job
568
+ end
537
569
  end
538
570
  end
539
571
 
@@ -167,3 +167,5 @@ module GoodJob # :nodoc:
167
167
  end
168
168
  end
169
169
  end
170
+
171
+ ActiveSupport.run_load_hooks(:good_job_process, GoodJob::Process)
@@ -127,3 +127,5 @@ module GoodJob
127
127
  end
128
128
  end
129
129
  end
130
+
131
+ ActiveSupport.run_load_hooks(:good_job_setting, GoodJob::Setting)
@@ -68,26 +68,26 @@
68
68
  <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
69
69
  <li>
70
70
  <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
71
- <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t(".actions.reschedule"), data: { confirm: t(".actions.confirm_reschedule"), disable: true } do %>
71
+ <%= button_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t(".actions.reschedule"), data: { turbo_confirm: t(".actions.confirm_reschedule") } do %>
72
72
  <%= render_icon "skip_forward" %>
73
73
  <%= t "good_job.actions.reschedule" %>
74
74
  <% end %>
75
75
  </li>
76
76
  <li>
77
77
  <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
78
- <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t(".actions.discard"), data: { confirm: t(".actions.confirm_discard"), disable: true } do %>
78
+ <%= button_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t(".actions.discard"), data: { turbo_confirm: t(".actions.confirm_discard") } do %>
79
79
  <%= render_icon "stop" %>
80
80
  <%= t "good_job.actions.discard" %>
81
81
  <% end %>
82
82
  </li>
83
83
  <li>
84
- <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t(".actions.retry"), data: { confirm: t(".actions.confirm_retry"), disable: true } do %>
84
+ <%= button_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t(".actions.retry"), data: { turbo_confirm: t(".actions.confirm_retry") } do %>
85
85
  <%= render_icon "arrow_clockwise" %>
86
86
  <%= t "good_job.actions.retry" %>
87
87
  <% end %>
88
88
  </li>
89
89
  <li>
90
- <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.finished?}", title: t(".actions.destroy"), data: { confirm: t(".actions.confirm_destroy"), disable: true } do %>
90
+ <%= button_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.finished?}", title: t(".actions.destroy"), data: { turbo_confirm: t(".actions.confirm_destroy") } do %>
91
91
  <%= render_icon "trash" %>
92
92
  <%= t "good_job.actions.destroy" %>
93
93
  <% end %>
@@ -58,9 +58,9 @@
58
58
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.batch.jobs" %></div>
59
59
  <%= batch.jobs.size %>
60
60
  </div>
61
- <div class="col text-end">
61
+ <div class="col text-end d-flex flex-row justify-content-end">
62
62
  <% if batch.discarded? %>
63
- <%= link_to retry_batch_path(batch), method: :put, class: "btn btn-sm btn-outline-primary", title: t("good_job.batches.actions.retry"), data: { confirm: t("good_job.batches.actions.confirm_retry") } do %>
63
+ <%= button_to retry_batch_path(batch), method: :put, class: "btn btn-sm btn-outline-primary", title: t("good_job.batches.actions.retry"), data: { turbo_confirm: t("good_job.batches.actions.confirm_retry") } do %>
64
64
  <%= render_icon "arrow_clockwise" %>
65
65
  <%= t "good_job.batches.actions.retry" %>
66
66
  <% end %>
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
  <div class="col text-end">
14
14
  <% if @batch.discarded? %>
15
- <%= button_to retry_batch_path(@batch), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.batches.actions.retry") }, title: t("good_job.batches.actions.retry"), data: { confirm: t("good_job.batches.actions.confirm_retry") } do %>
15
+ <%= button_to retry_batch_path(@batch), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.batches.actions.retry") }, title: t("good_job.batches.actions.retry"), data: { turbo_confirm: t("good_job.batches.actions.confirm_retry") } do %>
16
16
  <%= render_icon "arrow_clockwise" %>
17
17
  <%= t "good_job.actions.retry" %>
18
18
  <% end %>
@@ -58,11 +58,11 @@
58
58
  <% end %>
59
59
 
60
60
  <% if cron_entry.enabled? %>
61
- <%= button_to disable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.disable") }, title: t("good_job.cron_entries.actions.disable"), data: { confirm: t("good_job.cron_entries.actions.confirm_disable") } do %>
61
+ <%= button_to disable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.disable") }, title: t("good_job.cron_entries.actions.disable"), data: { turbo_confirm: t("good_job.cron_entries.actions.confirm_disable") } do %>
62
62
  <%= render_icon "pause" %>
63
63
  <% end %>
64
64
  <% else %>
65
- <%= button_to enable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.enable") }, title: t("good_job.cron_entries.actions.enable"), data: { confirm: t("good_job.cron_entries.actions.confirm_enable") } do %>
65
+ <%= button_to enable_cron_entry_path(cron_entry), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.cron_entries.actions.enable") }, title: t("good_job.cron_entries.actions.enable"), data: { turbo_confirm: t("good_job.cron_entries.actions.confirm_enable") } do %>
66
66
  <%= render_icon "play" %>
67
67
  <% end %>
68
68
  <% end %>
@@ -1,3 +1,6 @@
1
+ <%= form_with(id: "job_action_form", method: :put, model: false, local: true) { nil } %>
2
+ <%= form_with(id: "job_destroy_form", method: :delete, model: false, local: true) { nil } %>
3
+
1
4
  <%= form_with(model: false, url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
2
5
  <div class="my-3 card" data-gj-poll-replace id="jobs-table">
3
6
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
@@ -8,16 +11,16 @@
8
11
  <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
9
12
  <%= label_tag('toggle_job_ids', t(".toggle_all_jobs"), class: "visually-hidden") %>
10
13
  </div>
11
- <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: t(".actions.reschedule_all"), data: { confirm: t(".actions.confirm_reschedule_all"), disable: true } do %>
14
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: t(".actions.reschedule_all"), data: { turbo_confirm: t(".actions.confirm_reschedule_all") } do %>
12
15
  <span class="me-1"><%= render_icon "skip_forward" %></span> <%= t "good_job.actions.reschedule" %>
13
16
  <% end %>
14
17
 
15
- <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.retry_all"), data: { confirm: t(".actions.confirm_retry_all"), disable: true } do %>
18
+ <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.retry_all"), data: { turbo_confirm: t(".actions.confirm_retry_all") } do %>
16
19
  <span class="me-1"><%= render_icon "arrow_clockwise" %></span> <%= t "good_job.actions.retry" %>
17
20
  <% end %>
18
21
 
19
22
  <div class="btn-group" role="group">
20
- <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.discard_all"), data: { confirm: t(".actions.confirm_discard_all"), disable: true } do %>
23
+ <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-secondary', title: t(".actions.discard_all"), data: { turbo_confirm: t(".actions.confirm_discard_all") } do %>
21
24
  <span class="me-1"><%= render_icon "stop" %></span> <%= t "good_job.actions.discard" %>
22
25
  <% end %>
23
26
  <button id="destroy-dropdown-toggle" type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
@@ -25,7 +28,7 @@
25
28
  </button>
26
29
  <ul class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
27
30
  <li>
28
- <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn dropdown-item', title: t(".actions.destroy_all"), data: { confirm: t(".actions.confirm_destroy_all"), disable: true } do %>
31
+ <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn dropdown-item', title: t(".actions.destroy_all"), data: { turbo_confirm: t(".actions.confirm_destroy_all") } do %>
29
32
  <span class="me-1"><%= render_icon "trash" %></span> <%= t "good_job.actions.destroy" %>
30
33
  <% end %>
31
34
  </li>
@@ -125,33 +128,33 @@
125
128
  <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
126
129
  <li>
127
130
  <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
128
- <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t("good_job.jobs.actions.reschedule"), data: { confirm: t("good_job.jobs.actions.confirm_reschedule"), disable: true } do %>
131
+ <%= tag.button form: "job_action_form", formaction: reschedule_job_path(job.id), class: "dropdown-item #{'disabled' unless job_reschedulable}", title: t("good_job.jobs.actions.reschedule"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
129
132
  <%= render_icon "skip_forward" %>
130
133
  <%= t "good_job.actions.reschedule" %>
131
134
  <% end %>
132
135
  </li>
133
136
  <li>
134
137
  <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
135
- <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: t("good_job.jobs.actions.discard"), data: { confirm: t("good_job.jobs.actions.confirm_discard"), disable: true } do %>
138
+ <%= tag.button form: "job_action_form", formaction: discard_job_path(job.id), class: "dropdown-item #{'disabled' unless job_discardable}", title: t("good_job.jobs.actions.discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_discard") } do %>
136
139
  <%= render_icon "stop" %>
137
140
  <%= t "good_job.actions.discard" %>
138
141
  <% end %>
139
142
  </li>
140
143
  <li>
141
144
  <% job_force_discardable = job.status.in? [:running] %>
142
- <%= link_to force_discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { confirm: t("good_job.jobs.actions.confirm_force_discard"), disable: true } do %>
145
+ <%= tag.button form: "job_action_form", formaction: force_discard_job_path(job.id), class: "dropdown-item #{'disabled' unless job_force_discardable}", title: t("good_job.jobs.actions.force_discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_force_discard") } do %>
143
146
  <%= render_icon "eject" %>
144
147
  <%= t "good_job.actions.force_discard" %>
145
148
  <% end %>
146
149
  </li>
147
150
  <li>
148
- <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry"), disable: true } do %>
151
+ <%= tag.button form: "job_action_form", formaction: retry_job_path(job.id), class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: t("good_job.jobs.actions.retry"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_retry") } do %>
149
152
  <%= render_icon "arrow_clockwise" %>
150
153
  <%= t "good_job.actions.retry" %>
151
154
  <% end %>
152
155
  </li>
153
156
  <li>
154
- <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: t("good_job.jobs.actions.destroy"), data: { confirm: t("good_job.jobs.actions.confirm_destroy"), disable: true } do %>
157
+ <%= tag.button form: "job_destroy_form", formaction: job_path(job.id), class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :succeeded]}", title: t("good_job.jobs.actions.destroy"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
155
158
  <%= render_icon "trash" %>
156
159
  <%= t "good_job.actions.destroy" %>
157
160
  <% end %>
@@ -33,28 +33,28 @@
33
33
  form_class: "d-inline-block",
34
34
  aria: { label: t("good_job.jobs.actions.reschedule") },
35
35
  title: t("good_job.jobs.actions.reschedule"),
36
- data: { confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
36
+ data: { turbo_confirm: t("good_job.jobs.actions.confirm_reschedule") } do %>
37
37
  <%= render_icon "skip_forward" %>
38
38
  <%= t "good_job.actions.reschedule" %>
39
39
  <% end %>
40
40
  <% end %>
41
41
 
42
42
  <% if @job.status.in? [:scheduled, :retried, :queued] %>
43
- <%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.discard") }, title: t("good_job.jobs.actions.discard"), data: { confirm: t("good_job.jobs.actions.confirm_discard") } do %>
43
+ <%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.discard") }, title: t("good_job.jobs.actions.discard"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_discard") } do %>
44
44
  <%= render_icon "stop" %>
45
45
  <%= t "good_job.actions.discard" %>
46
46
  <% end %>
47
47
  <% end %>
48
48
 
49
49
  <% if @job.status == :discarded %>
50
- <%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.retry") }, title: t("good_job.jobs.actions.retry"), data: { confirm: t("good_job.jobs.actions.confirm_retry") } do %>
50
+ <%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.retry") }, title: t("good_job.jobs.actions.retry"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_retry") } do %>
51
51
  <%= render_icon "arrow_clockwise" %>
52
52
  <%= t "good_job.actions.retry" %>
53
53
  <% end %>
54
54
  <% end %>
55
55
 
56
56
  <% if @job.status.in? [:discarded, :succeeded] %>
57
- <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.destroy") }, title: t("good_job.jobs.actions.destroy"), data: { confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
57
+ <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: t("good_job.jobs.actions.destroy") }, title: t("good_job.jobs.actions.destroy"), data: { turbo_confirm: t("good_job.jobs.actions.confirm_destroy") } do %>
58
58
  <%= render_icon "trash" %>
59
59
  <%= t "good_job.actions.destroy" %>
60
60
  <% end %>
@@ -16,7 +16,7 @@
16
16
  { action: :destroy, type: type, value: value },
17
17
  method: :delete,
18
18
  class: 'btn btn-sm btn-outline-primary',
19
- data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) }
19
+ data: { turbo_confirm: t('good_job.pauses.index.confirm_unpause', value: value) }
20
20
  ) do %>
21
21
  <%= render_icon "play" %>
22
22
  <%= t("good_job.pauses.index.unpause") %>
@@ -4,5 +4,5 @@
4
4
  { action: :destroy, type: type, value: value },
5
5
  method: :delete,
6
6
  class: 'btn btn-sm btn-outline-primary',
7
- data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) } %>
7
+ data: { turbo_confirm: t('good_job.pauses.index.confirm_unpause', value: value) } %>
8
8
  </li>
@@ -4,14 +4,14 @@
4
4
  <h2 class="pt-3 pb-2"><%= title %></h2>
5
5
  </div>
6
6
 
7
- <%= form_with(model: false, url: "", method: :get, local: true, id: "filter_form", class: "") do |form| %>
7
+ <%= form_with(model: false, url: "", method: :get, local: true, data: { controller: "form" }) do |form| %>
8
8
  <%= hidden_field_tag :poll, params[:poll] %>
9
9
  <%= hidden_field_tag :state, params[:state] %>
10
10
  <%= hidden_field_tag :locale, params[:locale] if params[:locale] %>
11
11
  <div class="d-md-flex flex-row w-100">
12
12
  <div class="me-md-2 mb-2 mb-md-0">
13
13
  <%= label_tag "job_queue_filter", t(".queue_name"), class: "visually-hidden" %>
14
- <select name="queue_name" id="job_queue_filter" class="form-select form-select-sm">
14
+ <select name="queue_name" id="job_queue_filter" class="form-select form-select-sm" data-action="change->form#submit">
15
15
  <option value="" <%= "selected='selected'" if params[:queue_name].blank? %>><%= t ".all_queues" %></option>
16
16
 
17
17
  <% filter.queues.each do |name, count| %>
@@ -22,7 +22,7 @@
22
22
 
23
23
  <div class="me-md-2 mb-2 mb-md-0">
24
24
  <%= label_tag "job_class_filter", t(".job_name"), class: "visually-hidden" %>
25
- <select name="job_class" id="job_class_filter" class="form-select form-select-sm">
25
+ <select name="job_class" id="job_class_filter" class="form-select form-select-sm" data-action="change->form#submit">
26
26
  <option value="" <%= "selected='selected'" if params[:job_class].blank? %>><%= t ".all_jobs" %></option>
27
27
 
28
28
  <% filter.job_classes.each do |name, count| %>
@@ -56,20 +56,10 @@
56
56
  <li class="nav-item">
57
57
  <%= link_to filter.to_params(state: name), class: "nav-link #{'active' if params[:state] == name}" do %>
58
58
  <%= t(name, scope: 'good_job.status') %>
59
- <span data-async-values-target="value" data-async-values-key="<%= name %>" data-async-values-zero-class="text-bg-secondary" class="badge text-bg-primary rounded-pill d-none"></span>
59
+ <span data-async-values-target="value" data-async-values-key="<%= name %>" data-async-values-zero-class="bg-secondary" class="badge text-bg-primary rounded-pill d-none" id="filter_state_<%= name %>" data-turbo-permanent></span>
60
60
  <% end %>
61
61
  </li>
62
62
  <% end %>
63
63
  </ul>
64
64
  </div>
65
-
66
- <script nonce="<%= content_security_policy_nonce %>">
67
- document.addEventListener("DOMContentLoaded", () => {
68
- document.querySelectorAll("#job_class_filter, #job_queue_filter").forEach((filter) => {
69
- filter.addEventListener("change", () => {
70
- document.querySelector("#filter_form").submit();
71
- });
72
- })
73
- })
74
- </script>
75
65
  </div>
@@ -18,25 +18,25 @@
18
18
  <li class="nav-item">
19
19
  <%= link_to jobs_path, class: ["nav-link", ("active" if controller_name == 'jobs')] do %>
20
20
  <%= t(".jobs") %>
21
- <span data-async-values-target="value" data-async-values-key="jobs_count" class="badge text-bg-secondary rounded-pill d-none"></span>
21
+ <span data-async-values-target="value" data-async-values-key="jobs_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_jobs_count" data-turbo-permanent></span>
22
22
  <% end %>
23
23
  </li>
24
24
  <li class="nav-item">
25
25
  <%= link_to batches_path, class: ["nav-link", ("active" if controller_name == 'batches')] do %>
26
26
  <%= t ".batches" %>
27
- <span data-async-values-target="value" data-async-values-key="batches_count" class="badge text-bg-secondary rounded-pill d-none"></span>
27
+ <span data-async-values-target="value" data-async-values-key="batches_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_batches_count" data-turbo-permanent></span>
28
28
  <% end %>
29
29
  </li>
30
30
  <li class="nav-item">
31
31
  <%= link_to cron_entries_path, class: ["nav-link", ("active" if controller_name == 'cron_entries')] do %>
32
32
  <%= t(".cron_schedules") %>
33
- <span data-async-values-target="value" data-async-values-key="cron_entries_count" class="badge text-bg-secondary rounded-pill d-none"></span>
33
+ <span data-async-values-target="value" data-async-values-key="cron_entries_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_cron_entries_count" data-turbo-permanent></span>
34
34
  <% end %>
35
35
  </li>
36
36
  <li class="nav-item">
37
37
  <%= link_to processes_path, class: ["nav-link", ("active" if controller_name == 'processes')] do %>
38
38
  <%= t(".processes") %>
39
- <span data-async-values-target="value" data-async-values-key="processes_count" data-async-values-zero-class="text-bg-danger" class="badge text-bg-secondary rounded-pill d-none"></span>
39
+ <span data-async-values-target="value" data-async-values-key="processes_count" data-async-values-zero-class="bg-danger" class="badge text-bg-secondary rounded-pill d-none" id="navbar_processes_count" data-turbo-permanent></span>
40
40
  <% end %>
41
41
  </li>
42
42
  <li class="nav-item">
@@ -47,13 +47,13 @@
47
47
  <li class="nav-item">
48
48
  <%= link_to pauses_path, class: ["nav-link", ("active" if controller_name == 'pauses')] do %>
49
49
  <%= t(".pauses") %>
50
- <span data-async-values-target="value" data-async-values-key="pauses_count" data-async-values-zero-class="d-none" class="badge text-bg-warning rounded-pill d-none"></span>
50
+ <span data-async-values-target="value" data-async-values-key="pauses_count" data-async-values-zero-class="d-none" class="badge text-bg-warning rounded-pill d-none" id="navbar_pauses_count" data-turbo-permanent></span>
51
51
  <% end %>
52
52
  </li>
53
53
  <li class="nav-item">
54
54
  <%= link_to cleaner_index_path, class: ["nav-link", ("active" if controller_name == 'cleaner')] do %>
55
55
  <%= t(".cleaner") %>
56
- <span data-async-values-target="value" data-async-values-key="discarded_count" class="badge text-bg-secondary rounded-pill d-none"></span>
56
+ <span data-async-values-target="value" data-async-values-key="discarded_count" class="badge text-bg-secondary rounded-pill d-none" id="navbar_discarded_count" data-turbo-permanent></span>
57
57
  <% end %>
58
58
  </li>
59
59
  </ul>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= I18n.locale %>" data-bs-theme="auto">
2
+ <html lang="<%= I18n.locale %>" data-bs-theme="auto" data-turbo-unloaded>
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
@@ -18,15 +18,19 @@
18
18
  </script>
19
19
 
20
20
  <%# Do not use asset tag helpers to avoid paths being overriden by config.asset_host %>
21
- <%= tag.link rel: "stylesheet", href: frontend_static_path(:bootstrap, format: :css, locale: nil), nonce: content_security_policy_nonce %>
22
- <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce %>
21
+ <%= tag.link rel: "stylesheet", href: frontend_static_path(:bootstrap, format: :css, locale: nil), nonce: content_security_policy_nonce, data: { turbo_track: "reload" } %>
22
+ <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce, data: { turbo_track: "reload" } %>
23
23
  <%= tag.script "", src: frontend_static_path(:bootstrap, format: :js, locale: nil), nonce: content_security_policy_nonce %>
24
24
  <%= tag.script "", src: frontend_static_path(:chartjs, format: :js, locale: nil), nonce: content_security_policy_nonce %>
25
- <%= tag.script "", src: frontend_static_path(:rails_ujs, format: :js, locale: nil), nonce: content_security_policy_nonce %>
26
25
  <%= tag.script "", src: frontend_static_path(:es_module_shims, format: :js, locale: nil), async: true, nonce: content_security_policy_nonce %>
26
+
27
27
  <% importmaps = GoodJob::FrontendsController.js_modules.keys.index_with { |module_name| frontend_module_path(module_name, format: :js, locale: nil) } %>
28
- <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
29
- <%= tag.script "", type: "module", nonce: content_security_policy_nonce do %> import "application"; <% end %>
28
+ <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce, data: { turbo_track: "reload" }) %>
29
+ <% importmaps.each do |_module_name, path| %>
30
+ <%= tag.link rel: "modulepreload", href: path, nonce: content_security_policy_nonce %>
31
+ <% end %>
32
+
33
+ <%= tag.script "import \"application\";".html_safe, type: "module", nonce: content_security_policy_nonce %>
30
34
 
31
35
  <title>Good Job Dashboard</title>
32
36
  <%= tag.link rel: "icon", href: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 0 100 100"><text y=".90em" font-size="90">👍</text></svg>' %>
@@ -7,7 +7,7 @@
7
7
  "check_name": "Render",
8
8
  "message": "Render path contains parameter value",
9
9
  "file": "app/controllers/good_job/frontends_controller.rb",
10
- "line": 47,
10
+ "line": 52,
11
11
  "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
12
12
  "code": "render(file => (self.class.js_modules[params[:id].to_sym] or raise(ActionController::RoutingError, \"Not Found\")), {})",
13
13
  "render_path": null,
@@ -23,29 +23,6 @@
23
23
  ],
24
24
  "note": "Files are explicitly enumerated in the array"
25
25
  },
26
- {
27
- "warning_type": "Dynamic Render Path",
28
- "warning_code": 15,
29
- "fingerprint": "b0c2888c9b217671d90d0141b49b036af3b2a70c63b02968cc97ae2052c86272",
30
- "check_name": "Render",
31
- "message": "Render path contains parameter value",
32
- "file": "app/controllers/good_job/frontends_controller.rb",
33
- "line": 41,
34
- "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
35
- "code": "render(file => ({ :css => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.min.css\"), :style => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"style.css\") }), :js => ({ :bootstrap => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"bootstrap\", \"bootstrap.bundle.min.js\"), :chartjs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"chartjs\", \"chart.min.js\"), :es_module_shims => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"es_module_shims.js\"), :rails_ujs => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"vendor\", \"rails_ujs.js\") }), :svg => ({ :icons => GoodJob::Engine.root.join(\"app\", \"frontend\", \"good_job\", \"icons.svg\") }) }.dig(params[:format].to_sym, params[:id].to_sym) or raise(ActionController::RoutingError, \"Not Found\")), {})",
36
- "render_path": null,
37
- "location": {
38
- "type": "method",
39
- "class": "GoodJob::FrontendsController",
40
- "method": "static"
41
- },
42
- "user_input": "params[:id].to_sym",
43
- "confidence": "Weak",
44
- "cwe_id": [
45
- 22
46
- ],
47
- "note": "Files are explicitly enumerated in the array"
48
- },
49
26
  {
50
27
  "warning_type": "Dangerous Eval",
51
28
  "warning_code": 13,
@@ -115,7 +92,30 @@
115
92
  77
116
93
  ],
117
94
  "note": ""
95
+ },
96
+ {
97
+ "warning_type": "Dynamic Render Path",
98
+ "warning_code": 15,
99
+ "fingerprint": "ebc35a48169b7ee1f2e1b024f3bf6abe7138924a3a318ab2ad405c2b6e9bbb55",
100
+ "check_name": "Render",
101
+ "message": "Render path contains parameter value",
102
+ "file": "app/controllers/good_job/frontends_controller.rb",
103
+ "line": 46,
104
+ "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
105
+ "code": "render(file => ({ :css => ({ :bootstrap => asset_path(\"vendor\", \"bootstrap\", \"bootstrap.min.css\"), :style => asset_path(\"style.css\") }), :js => ({ :bootstrap => asset_path(\"vendor\", \"bootstrap\", \"bootstrap.bundle.min.js\"), :chartjs => asset_path(\"vendor\", \"chartjs\", \"chart.min.js\"), :es_module_shims => asset_path(\"vendor\", \"es_module_shims.js\"), :turbo => asset_path(\"vendor\", \"turbo.js\") }), :svg => ({ :icons => asset_path(\"icons.svg\") }) }.dig(params[:format].to_sym, params[:id].to_sym) or raise(ActionController::RoutingError, \"Not Found\")), {})",
106
+ "render_path": null,
107
+ "location": {
108
+ "type": "method",
109
+ "class": "GoodJob::FrontendsController",
110
+ "method": "static"
111
+ },
112
+ "user_input": "params[:id].to_sym",
113
+ "confidence": "Weak",
114
+ "cwe_id": [
115
+ 22
116
+ ],
117
+ "note": ""
118
118
  }
119
119
  ],
120
- "brakeman_version": "7.0.2"
120
+ "brakeman_version": "7.1.1"
121
121
  }