good_job 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ecbb75f2764a72686acb96367389abb1900321b5cf1292d6522aa8094c68451
4
- data.tar.gz: 43a5305a9f2610c1544d6e52c61f0b338771b07d92d6493fed0058ad33bbc045
3
+ metadata.gz: 2225caa419c57cd76c9acba358fa94e2fd2d29c914d25459093a5a65b785087d
4
+ data.tar.gz: 2c6af031bb80859108ed847a7d891a2066b06004edb518fa8921b01b3c9b662a
5
5
  SHA512:
6
- metadata.gz: 0e637579c190a3f3d1aba6c02f6a71e2193d9e5c4bb449a3d4d9571093150a25a1c293ac3f5c541d2fae09aeffff771692fa7ef58bc8ed672f61a2c9d35a7e74
7
- data.tar.gz: bc669b4d1628f7ff4b885787c65a38a57ad020aa168c3ae85f0c6bb26c9f0e19b64947682b0c99cf3eb28c2d660e1d79ee6095000a72340f09229d1679fa5dfe
6
+ metadata.gz: 00bfcfcf22a4f3e5138bc2d85a172bbf4c74515c38759af561567c42f5464825fd47aa3f5c369f91196ede2b73211c9e1b3422f5d555bcff439e7fd32fc9db1a
7
+ data.tar.gz: cc28292dd555ddd951ab465534302f0775784074e456bb181eb2f5efc07f4281bcd49783fa38e4a205591fb05daa19f88ff013995fe06fe3761e675b1e84e0ba
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.3.0](https://github.com/bensheldon/good_job/tree/v3.3.0) (2022-07-24)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.2.0...v3.3.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Calculating database connections [\#669](https://github.com/bensheldon/good_job/issues/669)
10
+ - Unable to Replace GoodJob's Logger [\#667](https://github.com/bensheldon/good_job/issues/667)
11
+ - Readme should consistently encourage usage of `config.good_job....` instead of `GoodJob.` configuration [\#628](https://github.com/bensheldon/good_job/issues/628)
12
+ - Improve the "Gem development" section of README? [\#551](https://github.com/bensheldon/good_job/issues/551)
13
+ - Simplify Rails initialization to only be a mountable Engine [\#543](https://github.com/bensheldon/good_job/issues/543)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Dashboard: Update cron and processes to match jobs listing [\#676](https://github.com/bensheldon/good_job/pull/676) ([bkeepers](https://github.com/bkeepers))
18
+ - Dashboard: improvements to jobs index and show pages [\#672](https://github.com/bensheldon/good_job/pull/672) ([bkeepers](https://github.com/bkeepers))
19
+ - Replace "timestamp" column-type in migrations with "datetime" [\#671](https://github.com/bensheldon/good_job/pull/671) ([bensheldon](https://github.com/bensheldon))
20
+ - Improve Readme description of v3 job preservation defaults [\#670](https://github.com/bensheldon/good_job/pull/670) ([bensheldon](https://github.com/bensheldon))
21
+ - update Gemfile.lock to latest dependencies [\#647](https://github.com/bensheldon/good_job/pull/647) ([jrochkind](https://github.com/jrochkind))
22
+
3
23
  ## [v3.2.0](https://github.com/bensheldon/good_job/tree/v3.2.0) (2022-07-12)
4
24
 
5
25
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.1.0...v3.2.0)
@@ -1055,7 +1075,6 @@
1055
1075
  - PgBouncer and prepared statements [\#269](https://github.com/bensheldon/good_job/issues/269)
1056
1076
  - Question about locking internals [\#212](https://github.com/bensheldon/good_job/issues/212)
1057
1077
  - Encoding::UndefinedConversionError \("\xE2" from ASCII-8BIT to UTF-8\) [\#198](https://github.com/bensheldon/good_job/issues/198)
1058
- - tools for managing a 'fleet' of processes [\#150](https://github.com/bensheldon/good_job/issues/150)
1059
1078
 
1060
1079
  **Merged pull requests:**
1061
1080
 
data/README.md CHANGED
@@ -117,7 +117,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
117
117
  YourJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later
118
118
  ```
119
119
 
120
- 1. In development, GoodJob executes jobs immediately in a separate thread (async mode). In production, GoodJob provides different options:
120
+ 1. In development, GoodJob executes jobs immediately in a separate thread ("async" mode). In production, GoodJob provides different options:
121
121
 
122
122
  - By default, GoodJob separates job enqueuing from job execution so that jobs can be scaled independently of the web server. Use the GoodJob command-line tool to execute jobs:
123
123
 
@@ -199,7 +199,7 @@ Usage:
199
199
  good_job cleanup_preserved_jobs
200
200
 
201
201
  Options:
202
- [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)
202
+ [--before-seconds-ago=SECONDS] # Destroy records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 1209600 (14 days))
203
203
 
204
204
  Destroys preserved job records.
205
205
 
@@ -226,6 +226,8 @@ GoodJob configuration can be placed within Rails `config` directory for all envi
226
226
  Configuration examples:
227
227
 
228
228
  ```ruby
229
+ # config/initializers/good_job.rb OR config/application.rb OR config/environments/{RAILS_ENV}.rb
230
+
229
231
  Rails.application.configure do
230
232
  # Configure options individually...
231
233
  config.good_job.preserve_job_records = true
@@ -275,9 +277,9 @@ Available configuration options are:
275
277
  - `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`.
276
278
  - `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON`
277
279
  - `cleanup_discarded_jobs` (boolean) whether to destroy discarded jobs when cleaning up preserved jobs using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `true`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_DISCARDED_JOBS`. _This configuration is only used when {GoodJob.preserve_job_records} is `true`._
278
- - `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `86400` (1 day). Can also be set with the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`. _This configuration is only used when {GoodJob.preserve_job_records} is `true`._
279
- - `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS`.
280
- - `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `nil`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS`.
280
+ - `cleanup_preserved_jobs_before_seconds_ago` (integer) number of seconds to preserve jobs when using the `$ good_job cleanup_preserved_jobs` CLI command or calling `GoodJob.cleanup_preserved_jobs`. Defaults to `1209600` (14 days). Can also be set with the environment variable `GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO`. _This configuration is only used when {GoodJob.preserve_job_records} is `true`._
281
+ - `cleanup_interval_jobs` (integer) Number of jobs a Scheduler will execute before cleaning up preserved jobs. Defaults to `1000`. Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_JOBS`.
282
+ - `cleanup_interval_seconds` (integer) Number of seconds a Scheduler will wait before cleaning up preserved jobs. Defaults to `600` (10 minutes). Can also be set with the environment variable `GOOD_JOB_CLEANUP_INTERVAL_SECONDS`.
281
283
  - `inline_execution_respects_schedule` (boolean) Opt-in to future behavior of inline execution respecting scheduled jobs. Defaults to `false`.
282
284
  - `logger` ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger` (Default: `Rails.logger`).
283
285
  - `preserve_job_records` (boolean) keeps job records in your database even after jobs are completed. (Default: `true`)
@@ -310,21 +312,21 @@ config.good_job.execution_mode = :external
310
312
  Good Job’s general behavior can also be configured via attributes directly on the `GoodJob` module:
311
313
 
312
314
  - **`GoodJob.active_record_parent_class`** (string) The ActiveRecord parent class inherited by GoodJob's ActiveRecord model `GoodJob::Job` (defaults to `"ActiveRecord::Base"`). Configure this when using [multiple databases with ActiveRecord](https://guides.rubyonrails.org/active_record_multiple_databases.html) or when other custom configuration is necessary for the ActiveRecord model to connect to the Postgres database. _The value must be a String to avoid premature initialization of ActiveRecord._
313
- - **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
314
- - **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `true`)
315
- - **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `false`)
316
- - **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
317
315
 
318
316
  You’ll generally want to configure these in `config/initializers/good_job.rb`, like so:
319
317
 
320
318
  ```ruby
321
319
  # config/initializers/good_job.rb
322
320
  GoodJob.active_record_parent_class = "ApplicationRecord"
323
- GoodJob.preserve_job_records = true
324
- GoodJob.retry_on_unhandled_error = false
325
- GoodJob.on_thread_error = -> (exception) { Raven.capture_exception(exception) }
326
321
  ```
327
322
 
323
+ The following options are also configurable via accessors, but you are encouraged to use the configuration attributes instead because these may be deprecated and removed in the future:
324
+
325
+ - **`GoodJob.logger`** ([Rails Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)) lets you set a custom logger for GoodJob. It should be an instance of a Rails `Logger`.
326
+ - **`GoodJob.preserve_job_records`** (boolean) keeps job records in your database even after jobs are completed. (Default: `true`)
327
+ - **`GoodJob.retry_on_unhandled_error`** (boolean) causes jobs to be re-queued and retried if they raise an instance of `StandardError`. Be advised this may lead to jobs being repeated infinitely ([see below for more on retries](#retries)). Instances of `Exception`, like SIGINT, will *always* be retried, regardless of this attribute’s value. (Default: `false`)
328
+ - **`GoodJob.on_thread_error`** (proc, lambda, or callable) will be called when an Exception. It can be useful for logging errors to bug tracking services, like Sentry or Airbrake.
329
+
328
330
  ### Dashboard
329
331
 
330
332
  ![Dashboard UI](https://github.com/bensheldon/good_job/raw/main/SCREENSHOT.png)
@@ -361,14 +363,7 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
361
363
  end
362
364
  ```
363
365
 
364
- 1. If you want to see finished (successful) and discarded (failed) jobs on the dashboard, you will have to configure job records to be preserved:
365
-
366
- ```ruby
367
- # eg in config/initializers/good_job.rb
368
- GoodJob.preserve_job_records = true
369
- ```
370
-
371
- See more at [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
366
+ _To view finished (successful) and discarded (failed) jobs on the Dashboard, GoodJob must be configured to preserve job records. Preservation is enabled by default._
372
367
 
373
368
  **Troubleshooting the Dashboard:** Some applications are unable to autoload the Goodjob Engine. To work around this, explicitly require the Engine at the top of your `config/application.rb` file, immediately after Rails is required and before Bundler requires the Rails' groups.
374
369
 
@@ -878,25 +873,24 @@ If your application is already using an ActiveJob backend, you will need to inst
878
873
 
879
874
  GoodJob is fully instrumented with [`ActiveSupport::Notifications`](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#introduction-to-instrumentation).
880
875
 
881
- By default, GoodJob will preserve job records for 14 days after they are run, regardless of whether they succeed or not (raising a kind of `StandardError`), unless they are interrupted (raising a kind of `Exception`).
876
+ By default, GoodJob will preserve job records for 14 days after they are run, regardless of whether they succeed or raised an exception.
882
877
 
883
- To not preserve job records for later inspection, set an initializer:
878
+ To instead delete job records immediately after they are finished:
884
879
 
885
880
  ```ruby
886
881
  # config/initializers/good_job.rb
887
- GoodJob.preserve_job_records = false # defaults to true, or `false` or `:on_unhandled_error`
882
+ config.good_job.preserve_job_records = false # defaults to true; can also be `false` or `:on_unhandled_error`
888
883
  ```
889
884
 
890
- GoodJob will automatically delete these job records after 14 days. The retention period, as well as the frequency GoodJob checks for deletable records can be configured:
885
+ GoodJob will automatically delete preserved job records after 14 days. The retention period, as well as the frequency GoodJob checks for deletable records can be configured:
891
886
 
892
887
  ```ruby
893
-
894
- config.cleanup_preserved_jobs_before_seconds_ago = 14.days.to_i
895
- config.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
896
- config.cleanup_interval_seconds = 10.minutes.to_i # Number of seconds between deletion sweeps.
888
+ config.good_job.cleanup_preserved_jobs_before_seconds_ago = 14.days.to_i
889
+ config.good_job.cleanup_interval_jobs = 1_000 # Number of executed jobs between deletion sweeps.
890
+ config.good_job.cleanup_interval_seconds = 10.minutes.to_i # Number of seconds between deletion sweeps.
897
891
  ```
898
892
 
899
- It is also possible to manually trigger a cleanup:
893
+ It is also possible to manually trigger a cleanup of preserved job records:
900
894
 
901
895
  - For example, in a Rake task:
902
896
 
@@ -1041,7 +1035,7 @@ For gem development and debugging information, please review the [README's Gem D
1041
1035
  # Clone the repository locally
1042
1036
  git clone git@github.com:bensheldon/good_job.git
1043
1037
 
1044
- # Set up the local environment
1038
+ # Set up the gem development environment
1045
1039
  bin/setup
1046
1040
  ```
1047
1041
 
@@ -1080,6 +1074,10 @@ bundle install
1080
1074
  Tests can be run against the primary development environment:
1081
1075
 
1082
1076
  ```bash
1077
+ # Set up the gem development environment
1078
+ bin/setup
1079
+
1080
+ # Run the tests
1083
1081
  bin/rspec
1084
1082
  ```
1085
1083
 
@@ -4,11 +4,13 @@ import renderCharts from "charts";
4
4
  import checkboxToggle from "checkbox_toggle";
5
5
  import documentReady from "document_ready";
6
6
  import showToasts from "toasts";
7
+ import setupPopovers from "popovers";
7
8
  import LivePoll from "live_poll";
8
9
 
9
10
  documentReady(function() {
10
11
  renderCharts();
11
12
  showToasts();
13
+ setupPopovers();
12
14
  checkboxToggle();
13
15
 
14
16
  const livePoll = new LivePoll
@@ -0,0 +1,7 @@
1
+ export default function() {
2
+ document.querySelectorAll('[data-bs-toggle="popover"]').forEach((el) => {
3
+ new bootstrap.Popover(el, {
4
+ template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><pre class="popover-body text-wrap text-break"></pre></div>'
5
+ })
6
+ })
7
+ }
@@ -39,3 +39,7 @@
39
39
  background-color: red;
40
40
  color: white;
41
41
  }
42
+
43
+ .btn-outline-secondary {
44
+ border-color: #ced4da; /* $gray-400 */
45
+ }
@@ -34,8 +34,8 @@ module GoodJob
34
34
  STATUS_COLOR = {
35
35
  discarded: "danger",
36
36
  finished: "success",
37
- queued: "warning",
38
- retried: "secondary",
37
+ queued: "secondary",
38
+ retried: "warning",
39
39
  running: "primary",
40
40
  scheduled: "secondary",
41
41
  }.freeze
@@ -1,61 +1,64 @@
1
- <div class="my-3 flex">
2
- <h2>Cron Schedules</h2>
1
+ <div class="bg-light break-out border-bottom">
2
+ <h2 class="container-fluid pt-3 pb-2">Cron Schedules</h2>
3
3
  </div>
4
4
 
5
- <% if @cron_entries.present? %>
6
- <div class="card my-3">
7
- <div class="table-responsive">
8
- <table class="table card-table table-bordered table-hover table-sm mb-0">
9
- <thead>
10
- <th>Key</th>
11
- <th>Schedule</th>
12
- <th>
13
- Properties
14
- <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
15
- data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
16
- aria: { expanded: false, controls: @cron_entries.map { |cron_entry| "##{dom_id(cron_entry, 'properties')}" }.join(" ") }
17
- %>
18
- </th>
19
- <th>Description</th>
20
- <th>Next scheduled</th>
21
- <th>Last run</th>
22
- <th>Actions</th>
23
- </thead>
24
- <tbody>
25
- <% @cron_entries.each do |cron_entry| %>
26
- <tr id="<%= dom_id(cron_entry) %>">
27
- <td class="font-monospace"><%= cron_entry.key %></td>
28
- <td class="font-monospace"><%= cron_entry.schedule %></td>
29
- <td>
30
- <%= tag.button("Inspect", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
31
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
32
- aria: { expanded: false, controls: dom_id(cron_entry, 'properties') }) %>
33
- <%= tag.pre(JSON.pretty_generate(cron_entry.display_properties), id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties") %>
34
- </td>
35
- <td><%= cron_entry.description %></td>
36
- <td><%= cron_entry.next_at %></td>
37
- <td>
38
- <% if cron_entry.last_job.present? %>
39
- <%= link_to cron_entry.last_at, cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
40
- <% end %>
41
- </td>
42
- <td>
43
- <%= 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 %>
44
- <%= render "good_job/shared/icons/play" %>
45
- <% end %>
46
- </td>
47
- </tr>
5
+ <div class="card my-3">
6
+ <div class="list-group list-group-flush text-nowrap" role="table">
7
+ <header class="list-group-item bg-light">
8
+ <div class="row small text-muted text-uppercase align-items-center">
9
+ <div class="col"></div>
10
+ <div class="col">Class</div>
11
+ <div class="col">Schedule</div>
12
+ <div class="col">Next scheduled</div>
13
+ <div class="col">Last run</div>
14
+ <div class="col text-end">
15
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
16
+ data: { bs_toggle: "collapse", bs_target: ".cron-entry-properties" },
17
+ aria: { expanded: false, controls: @cron_entries.map { |cron_entry| "##{dom_id(cron_entry, 'properties')}" }.join(" ") } do %>
18
+ <%= render_icon "info" %>
19
+ <span class="visually-hidden">Inspect</span>
48
20
  <% end %>
49
- </tbody>
50
- </table>
51
- </div>
21
+ </div>
22
+ </div>
23
+ </header>
24
+ <% @cron_entries.each do |cron_entry| %>
25
+ <div id="<%= dom_id(cron_entry) %>" class="list-group-item py-3" role="row">
26
+ <div class="row align-items-center">
27
+ <div class="col">
28
+ <div class="small font-monospace"><%= cron_entry.key %></div>
29
+ <div class="small text-muted"><%= cron_entry.description %></div>
30
+ </div>
31
+ <div class="col"><%= tag.span tag.code(cron_entry.job_class), class: "fs-5 mb-0" %></div>
32
+ <div class="col font-monospace fw-bold"><%= cron_entry.schedule %></div>
33
+ <div class="col small"><%= relative_time cron_entry.next_at %></div>
34
+ <div class="col small">
35
+ <% if cron_entry.last_job.present? %>
36
+ <%= link_to relative_time(cron_entry.last_at), cron_entry_path(cron_entry), title: "Job #{cron_entry.last_job.id}" %>
37
+ <% end %>
38
+ </div>
39
+ <div class="col d-flex gap-3 justify-content-end">
40
+ <%= 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 %>
41
+ <%= render "good_job/shared/icons/play" %>
42
+ Run Now
43
+ <% end %>
44
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
45
+ title: "Inspect",
46
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(cron_entry, 'properties')}" },
47
+ aria: { expanded: false, controls: dom_id(cron_entry, "properties") } do %>
48
+ <%= render_icon "info" %>
49
+ <span class="visually-hidden">Inspect</span>
50
+ <% end %>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <%= tag.div id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties list-group-item collapse small bg-dark text-light" do %>
55
+ <%= tag.pre JSON.pretty_generate(cron_entry.display_properties) %>
56
+ <% end %>
57
+ <% end %>
58
+ <% if @cron_entries.empty? %>
59
+ <div class="list-group-item py-4 text-center text-muted">
60
+ No cron schedules found.
61
+ </div>
62
+ <% end %>
52
63
  </div>
53
- <% else %>
54
- <div class="card my-3">
55
- <div class="card-body">
56
- <p class="card-text">
57
- <em>No cron schedules found.</em>
58
- </p>
59
- </div>
60
- </div>
61
- <% end %>
64
+ </div>
@@ -3,43 +3,46 @@
3
3
  <div class="list-group list-group-flush">
4
4
  <% executions.each do |execution| %>
5
5
  <%= tag.div id: dom_id(execution), class: "list-group-item py-3" do %>
6
- <div class="d-md-flex">
7
- <div class="flex-fill">
8
- <div class="small text-muted">
9
- #<%= execution.number %>:
10
- <%= tag.code link_to(execution.id, "##{dom_id(execution)}", class: "text-muted text-decoration-none") %>
11
- </div>
12
- <div class="d-flex gap-2 align-items-center text-muted mt-1">
6
+ <div class="row align-items-center text-nowrap">
7
+ <div class="col-5 d-flex gap-2">
8
+ <%= tag.span execution.number, class: "badge bg-secondary bg-opacity-50 rounded-pill" %>
9
+ <%= tag.code link_to(execution.id, "##{dom_id(execution)}", class: "text-muted text-decoration-none small") %>
10
+ </div>
11
+ <div class="col-2 small">
12
+ <% if execution.queue_latency %>
13
+ <%= format_duration execution.queue_latency %> <span class="text-muted">in queue</span>
14
+ <% end %>
15
+ </div>
16
+ <div class="col-2 small">
17
+ <% if execution.runtime_latency %>
18
+ <%= format_duration execution.runtime_latency %> <span class="text-muted">runtime</span>
19
+ <% end %>
20
+ </div>
21
+ <div class="col">
22
+ <div class="d-flex gap-3 align-items-center justify-content-end">
23
+ <%= tag.span relative_time(execution.last_status_at, include_seconds: true), class: "small" %>
13
24
  <%= status_badge execution.status %>
14
- <%= relative_time execution.last_status_at, include_seconds: true %>
15
-
16
- <% if execution.runtime_latency %>
17
- • <div><%= format_duration execution.runtime_latency %> runtime</div>
18
- <% end %>
19
- <% if execution.queue_latency %>
20
- • <div><%= format_duration execution.queue_latency %> in queue</div>
21
- <% end %>
22
25
  </div>
23
26
  </div>
24
- <div>
25
- <div class="mt-4 d-flex gap-2">
26
- <%= tag.button type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
27
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
28
- aria: { expanded: false, controls: dom_id(execution, "params") } do %>
29
- Inspect
30
- <% end %>
31
- </div>
27
+ <div class="col-auto">
28
+ <%= tag.button type: "button", class: "btn btn-sm text-muted ms-auto", role: "button",
29
+ title: "Inspect",
30
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(execution, 'params')}" },
31
+ aria: { expanded: false, controls: dom_id(execution, "params") } do %>
32
+ <%= render_icon "info" %>
33
+ <span class="visually-hidden">Inspect</span>
34
+ <% end %>
32
35
  </div>
33
36
  </div>
34
37
  <% if execution.error %>
35
- <div class="mt-3">
38
+ <div class="mt-3 small">
36
39
  <strong class="small">Error:</strong>
37
- <pre class="text-wrap text-break m-0"><%= execution.error %></pre>
40
+ <code class="text-wrap text-break m-0 text-black"><%= execution.error %></code>
38
41
  </div>
39
42
  <% end %>
40
- <div>
41
- <%= tag.pre JSON.pretty_generate(execution.display_serialized_params), id: dom_id(execution, "params"), class: "collapse bg-light card card-body p-3 my-3" %>
42
- </div>
43
+ <% end %>
44
+ <%= tag.div id: dom_id(execution, "params"), class: "list-group-item collapse small bg-dark text-light" do %>
45
+ <%= tag.pre JSON.pretty_generate(execution.display_serialized_params) %>
43
46
  <% end %>
44
47
  <% end %>
45
48
  </div>
@@ -1,122 +1,151 @@
1
- <div class="my-3">
2
- <div class="table-responsive">
3
- <%= form_with(url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
4
- <table class="table table-hover table-sm mb-0 table-jobs">
5
- <thead>
6
- <tr>
7
- <th><%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %></th>
8
- <th>ActiveJob ID</th>
9
- <th>State</th>
10
- <th>Job Class</th>
11
- <th>Queue</th>
12
- <th>Scheduled At</th>
13
- <th>Executions</th>
14
- <th>Error</th>
15
- <th>
16
- Parameters&nbsp;
17
- <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
18
- data: { bs_toggle: "collapse", bs_target: ".job-params" },
19
- aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
20
- %>
21
- </th>
22
- <th>
23
- Actions<br>
1
+ <%= form_with(url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
2
+ <div class="my-3 card" data-gj-poll-replace id="jobs-table">
3
+ <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
4
+ <header class="list-group-item bg-light">
5
+ <div class="row small text-muted text-uppercase align-items-center">
6
+ <div class="col-auto">
7
+ <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
8
+ </div>
9
+ <div class="col-4">
10
+ <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'btn btn-sm btn-outline-secondary', title: "Reschedule all", data: { confirm: "Are you sure you want to reschedule the selected jobs?", disable: true } do %>
11
+ <span class="me-1"><%= render_icon "skip_forward" %></span> Reschedule
12
+ <% end %>
24
13
 
25
- <div class="d-inline text-nowrap">
26
- <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'btn btn-sm btn-outline-primary', title: "Reschedule all", data: { confirm: "Confirm reschedule all", disable: true } do %>
27
- <%= render_icon "skip_forward" %> All
28
- <% end %>
14
+ <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-secondary', title: "Retry all", data: { confirm: "Are you sure you want to retry the selected jobs?", disable: true } do %>
15
+ <span class="me-1"><%= render_icon "arrow_clockwise" %></span> Retry
16
+ <% end %>
29
17
 
30
- <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-primary', title: "Discard all", data: { confirm: "Confirm discard all", disable: true } do %>
31
- <%= render_icon "stop" %> All
18
+ <div class="btn-group" role="group">
19
+ <%= form.button type: 'submit', name: 'mass_action', value: 'discard', class: 'btn btn-sm btn-outline-secondary', title: "Discard all", data: { confirm: "Are you usure you want to discard the selected jobs?", disable: true } do %>
20
+ <span class="me-1"><%= render_icon "stop" %></span> Discard
21
+ <% end %>
22
+ <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">
23
+ <span class="visually-hidden">Toggle Actions</span>
24
+ </button>
25
+ <div class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
26
+ <li>
27
+ <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn', title: "Destroy all", data: { confirm: "Are you sure you want to destroy the selected jobs?", disable: true } do %>
28
+ <span class="me-1"><%= render_icon "trash" %></span> Destroy
32
29
  <% end %>
30
+ </li>
31
+ </div>
32
+ </div>
33
33
 
34
- <%= form.button type: 'submit', name: 'mass_action', value: 'retry', class: 'btn btn-sm btn-outline-primary', title: "Retry all", data: { confirm: "Confirm retry all", disable: true } do %>
35
- <%= render_icon "arrow_clockwise" %> All
36
- <% end %>
34
+ </div>
35
+ <div class="col-1">Queue</div>
36
+ <div class="col-1">Priority</div>
37
+ <div class="col-1 text-end">Attempts</div>
38
+ <div class="col text-end">
39
+ <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
40
+ data: { bs_toggle: "collapse", bs_target: ".job-params" },
41
+ aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") } do %>
42
+ <%= render_icon "info" %>
43
+ <span class="visually-hidden">Inspect</span>
44
+ <% end %>
45
+ </div>
46
+ </div>
47
+ </header>
48
+ <label role="row" class="list-group-item list-group-item-warning list-group-item-action py-3 d-none" data-checkbox-toggle-show="job_ids">
49
+ <div class="row">
50
+ <div class="col-auto">
51
+ <%= check_box_tag "all_job_ids", 1, false, disabled: true, data: { "checkbox-toggle-show": "job_ids"} %>
52
+ </div>
53
+ <div class="col-auto">
54
+ Apply to all <%= filter.filtered_count %> <%= "job".pluralize(filter.filtered_count) %>.
55
+ </div>
56
+ </div>
57
+ </label>
37
58
 
38
- <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn btn-sm btn-outline-primary', title: "Destroy all", data: { confirm: "Confirm destroy all", disable: true } do %>
39
- <%= render_icon "trash" %> All
40
- <% end %>
41
- </div>
42
- </th>
43
- </tr>
44
- <tr class="d-none" data-checkbox-toggle-show="job_ids">
45
- <td class="text-center table-warning" colspan="10">
46
- <label>
47
- <%= check_box_tag "all_job_ids", 1, false, disabled: true, data: { "checkbox-toggle-show": "job_ids"} %>
48
- Apply to all <%= filter.filtered_count %> <%= "job".pluralize(filter.filtered_count) %>.
49
- </label>
50
- </td>
51
- </tr>
52
- </thead>
53
- <tbody>
54
- <% if jobs.present? %>
55
- <% jobs.each do |job| %>
56
- <tr class="<%= dom_class(job) %>" id="<%= dom_id(job) %>">
57
- <td><%= check_box_tag 'job_ids[]', job.id, false, data: { "checkbox-toggle-each": "job_ids" } %></td>
58
- <td>
59
- <%= link_to job_path(job.id) do %>
60
- <code><%= job.id %></code>
61
- <% end %>
62
- </td>
63
- <td><%= status_badge(job.status) %></td>
64
- <td><%= job.job_class %></td>
65
- <td><%= job.queue_name %></td>
66
- <td><%= relative_time(job.scheduled_at || job.created_at) %></td>
67
- <td><%= job.executions_count %></td>
68
- <td class="text-break"><%= truncate(job.recent_error, length: 1_000) %></td>
69
- <td>
70
- <%= tag.button "Inspect", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
71
- data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
72
- aria: { expanded: false, controls: dom_id(job, "params") }
73
- %>
74
- <%= tag.pre JSON.pretty_generate(job.display_serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
75
- </td>
76
- <td>
77
- <div class="text-nowrap">
78
- <% if job.status.in? [:scheduled, :retried, :queued] %>
79
- <%= link_to reschedule_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
80
- <%= render_icon "skip_forward" %>
81
- <% end %>
82
- <% else %>
83
- <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "skip_forward" %></button>
59
+ <% if jobs.present? %>
60
+ <% jobs.each do |job| %>
61
+ <%= label_tag dom_id(job, :checkbox), id: dom_id(job), role: "row", class: "list-group-item list-group-item-action py-3" do %>
62
+ <div class="row align-items-center">
63
+ <div class="col-auto">
64
+ <%= check_box_tag 'job_ids[]', job.id, false, id: dom_id(job, :checkbox), data: { "checkbox-toggle-each": "job_ids" } %>
65
+ </div>
66
+ <div class="col-4">
67
+ <%= tag.code link_to(job.id, job_path(job), class: "small text-muted text-decoration-none") %>
68
+ <%= tag.h5 tag.code(link_to(job.job_class, job_path(job), class: "text-reset text-decoration-none")), class: "text-reset mb-0" %>
69
+ </div>
70
+ <div class="col-1">
71
+ <span class="badge bg-primary bg-opacity-25 text-dark font-monospace"><%= job.queue_name %></span>
72
+ </div>
73
+ <div class="col-1 small text-center">
74
+ <span class="font-monospace fw-bold"><%= job.priority %></span>
75
+ </div>
76
+ <div class="col-1 text-center">
77
+ <% if job.executions_count > 0 && job.status != :finished %>
78
+ <%= tag.span job.executions_count, class: "badge rounded-pill bg-danger", data: {
79
+ bs_toggle: "popover",
80
+ bs_trigger: "hover focus click",
81
+ bs_placement: "bottom",
82
+ bs_content: job.recent_error
83
+ } %>
84
+ <% else %>
85
+ <span class="badge bg-secondary bg-opacity-50 rounded-pill"><%= job.executions_count %></span>
86
+ <% end %>
87
+ </div>
88
+ <div class="col d-flex gap-3 align-items-center justify-content-end">
89
+ <%= tag.span relative_time(job.last_status_at), class: "small" %>
90
+ <%= status_badge job.status %>
91
+ </div>
92
+ <div class="col-auto">
93
+ <div class="dropdown float-end">
94
+ <button class="d-flex align-items-center btn btn-sm" type="button" id="<%= dom_id(job, :actions) %>" data-bs-toggle="dropdown" aria-expanded="false">
95
+ <%= render "good_job/shared/icons/dots" %>
96
+ <span class="visually-hidden">Actions</span>
97
+ </button>
98
+ <ul class="dropdown-menu shadow" aria-labelledby="<%= dom_id(job, :actions) %>">
99
+ <li>
100
+ <% job_reschedulable = job.status.in? [:scheduled, :retried, :queued] %>
101
+ <%= link_to reschedule_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_reschedulable}", title: "Reschedule job", data: { confirm: "Confirm reschedule", disable: true } do %>
102
+ <%= render "good_job/shared/icons/skip_forward" %>
103
+ Reschedule
84
104
  <% end %>
85
-
86
- <% if job.status.in? [:scheduled, :retried, :queued] %>
87
- <%= link_to discard_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
88
- <%= render_icon "stop" %>
89
- <% end %>
90
- <% else %>
91
- <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "stop" %></button>
105
+ </li>
106
+ <li>
107
+ <% job_discardable = job.status.in? [:scheduled, :retried, :queued] %>
108
+ <%= link_to discard_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job_discardable}", title: "Discard job", data: { confirm: "Confirm discard", disable: true } do %>
109
+ <%= render "good_job/shared/icons/stop" %>
110
+ Discard
92
111
  <% end %>
93
-
94
- <% if job.status == :discarded %>
95
- <%= link_to retry_job_path(job.id), method: :put, class: "btn btn-sm btn-outline-primary", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
96
- <%= render_icon "arrow_clockwise" %>
97
- <% end %>
98
- <% else %>
99
- <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "arrow_clockwise" %></button>
112
+ </li>
113
+ <li>
114
+ <%= link_to retry_job_path(job.id), method: :put, class: "dropdown-item #{'disabled' unless job.status == :discarded}", title: "Retry job", data: { confirm: "Confirm retry", disable: true } do %>
115
+ <%= render "good_job/shared/icons/arrow_clockwise" %>
116
+ Retry
117
+ <% end %>
118
+ </li>
119
+ <li>
120
+ <%= link_to job_path(job.id), method: :delete, class: "dropdown-item #{'disabled' unless job.status.in? [:discarded, :finished]}", title: "Destroy job", data: { confirm: "Confirm destroy", disable: true } do %>
121
+ <%= render_icon "trash" %>
122
+ Destroy
100
123
  <% end %>
124
+ </li>
101
125
 
102
- <% if job.status.in? [:discarded, :finished] %>
103
- <%= link_to job_path(job.id), method: :delete, class: "btn btn-sm btn-outline-primary", title: "Destroy job", data: { confirm: "Confirm destroy", disable: true } do %>
104
- <%= render_icon "trash" %>
105
- <% end %>
106
- <% else %>
107
- <button class="btn btn-sm btn-outline-secondary" disabled><%= render_icon "trash" %></button>
126
+ <li>
127
+ <%= link_to "##{dom_id(job, 'params')}",
128
+ class: "dropdown-item",
129
+ data: { bs_toggle: "collapse" },
130
+ aria: { expanded: false, controls: dom_id(job, "params") } do %>
131
+ <%= render_icon "info" %>
132
+ Inspect
108
133
  <% end %>
109
- </div>
110
- </td>
111
- </tr>
112
- <% end %>
113
- <% else %>
114
- <tr>
115
- <td colspan="10" class="py-2 text-center text-muted">No jobs found.</td>
116
- </tr>
117
- <% end %>
118
- </tbody>
119
- </table>
134
+ </li>
135
+ </ul>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <% end %>
140
+ <%= tag.div id: dom_id(job, "params"), class: "job-params list-group-item collapse small bg-dark text-light" do %>
141
+ <%= tag.pre JSON.pretty_generate(job.display_serialized_params) %>
142
+ <% end %>
143
+ <% end %>
144
+ <% else %>
145
+ <div class="list-group-item py-4 text-center text-muted">
146
+ No jobs found.
147
+ </div>
120
148
  <% end %>
121
149
  </div>
122
150
  </div>
151
+ <% end %>
@@ -1,48 +1,57 @@
1
1
  <div class="break-out bg-light border-bottom py-2 mb-3">
2
2
  <div class="container-fluid pt-2">
3
- <div class="d-flex align-items-center">
4
- <div class="flex-fill">
3
+ <div class="row align-items-center">
4
+ <div class="col-5">
5
5
  <nav aria-label="breadcrumb">
6
6
  <ol class="breadcrumb small mb-0">
7
7
  <li class="breadcrumb-item"><%= link_to "Jobs", jobs_path %></li>
8
- <li class="breadcrumb-item active" aria-current="page">ActiveJob ID: <%= tag.code @job.id %></li>
8
+ <li class="breadcrumb-item active" aria-current="page"><%= tag.code @job.id, class: "text-muted" %></li>
9
9
  </ol>
10
10
  </nav>
11
- <h2 class="mb-1"><%= tag.code @job.job_class %></h2>
12
- <div class="text-muted small d-flex gap-2">
13
- <div>Queue: <%= tag.strong @job.queue_name %></div>
14
-
15
- <div>Priority: <%= tag.strong @job.priority %></div>
11
+ <h2 class="mb-0"><%= tag.code @job.job_class %></h2>
12
+ </div>
13
+ <div class="col-2">
14
+ <div class="small text-muted text-uppercase">Queue</div>
15
+ <div class="badge bg-primary bg-opacity-25 text-dark font-monospace my-2">
16
+ <%= tag.strong @job.queue_name %>
16
17
  </div>
17
18
  </div>
18
- <div>
19
- <% job_reschedulable = @job.status.in? [:scheduled, :retried, :queued] %>
20
- <%= button_to reschedule_job_path(@job.id), method: :put,
21
- class: "btn btn-sm #{job_reschedulable ? 'btn-outline-primary' : 'btn-outline-secondary'}",
22
- form_class: "d-inline-block",
23
- disabled: !job_reschedulable,
24
- aria: { label: "Reschedule job" },
25
- title: "Reschedule job",
26
- data: { confirm: "Confirm reschedule" } do %>
27
- <%= render_icon "skip_forward" %>
28
- Reschedule
19
+ <div class="col-2">
20
+ <div class="small text-muted text-uppercase">Priority</div>
21
+ <div class="font-monospace fw-bold small my-2"><%= tag.strong @job.priority %></div>
22
+ </div>
23
+ <div class="col text-end">
24
+ <% if @job.status.in? [:scheduled, :retried, :queued] %>
25
+ <%= button_to reschedule_job_path(@job.id), method: :put,
26
+ class: "btn btn-sm btn-outline-primary",
27
+ form_class: "d-inline-block",
28
+ aria: { label: "Reschedule job" },
29
+ title: "Reschedule job",
30
+ data: { confirm: "Confirm reschedule" } do %>
31
+ <%= render_icon "skip_forward" %>
32
+ Reschedule
33
+ <% end %>
29
34
  <% end %>
30
35
 
31
- <% job_discardable = @job.status.in? [:scheduled, :retried, :queued] %>
32
- <%= 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 %>
33
- <%= render_icon "stop" %>
34
- Discard
36
+ <% if @job.status.in? [:scheduled, :retried, :queued] %>
37
+ <%= button_to discard_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Discard job" }, title: "Discard job", data: { confirm: "Confirm discard" } do %>
38
+ <%= render_icon "stop" %>
39
+ Discard
40
+ <% end %>
35
41
  <% end %>
36
42
 
37
- <%= 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 %>
38
- <%= render_icon "arrow_clockwise" %>
39
- Retry
43
+ <% if @job.status == :discarded %>
44
+ <%= button_to retry_job_path(@job.id), method: :put, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Retry job" }, title: "Retry job", data: { confirm: "Confirm retry" } do %>
45
+ <%= render_icon "arrow_clockwise" %>
46
+ Retry
47
+ <% end %>
40
48
  <% end %>
41
49
 
42
- <% job_destroyable = @job.status.in? [:discarded, :finished] %>
43
- <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm #{job_destroyable ? 'btn-outline-primary' : 'btn-outline-secondary'}", form_class: "d-inline-block", disabled: !job_destroyable, aria: { label: "Destroy job" }, title: "Destroy job", data: { confirm: "Confirm destroy" } do %>
44
- <%= render_icon "trash" %>
45
- Destroy
50
+ <% if @job.status.in? [:discarded, :finished] %>
51
+ <%= button_to job_path(@job.id), method: :delete, class: "btn btn-sm btn-outline-primary", form_class: "d-inline-block", aria: { label: "Destroy job" }, title: "Destroy job", data: { confirm: "Confirm destroy" } do %>
52
+ <%= render_icon "trash" %>
53
+ Destroy
54
+ <% end %>
46
55
  <% end %>
47
56
  </div>
48
57
  </div>
@@ -51,7 +60,7 @@
51
60
 
52
61
  <div class="my-4">
53
62
  <h5>Arguments</h5>
54
- <%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', ') %>
63
+ <%= tag.pre @job.serialized_params["arguments"].map(&:inspect).join(', '), class: 'text-wrap text-break' %>
55
64
  </div>
56
65
 
57
66
  <%= render 'executions', executions: @job.executions.includes_advisory_locks.reverse %>
@@ -1,38 +1,66 @@
1
- <div class="my-3 flex">
2
- <h2>Processes</h2>
1
+ <div class="bg-light break-out border-bottom">
2
+ <h2 class="container-fluid pt-3 pb-2">Processes</h2>
3
3
  </div>
4
4
 
5
- <div data-live-poll-region="processes">
6
- <% if @processes.present? %>
7
- <div class="card my-3">
8
- <div class="table-responsive">
9
- <table class="table card-table table-bordered table-hover table-sm mb-0">
10
- <thead>
11
- <tr>
12
- <th>Process UUID</th>
13
- <th>Created At</th></th>
14
- <th>State</th>
15
- </tr>
16
- </thead>
17
- <tbody>
18
- <% @processes.each do |process| %>
19
- <tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
20
- <td><%= process.id %></td>
21
- <td><%= relative_time(process.created_at) %></td>
22
- <td><%= tag.pre JSON.pretty_generate(process.state) %></td>
23
- </tr>
5
+ <div class="card my-3" data-live-poll-region="processes">
6
+ <div class="list-group list-group-flush text-nowrap" role="table">
7
+ <header class="list-group-item bg-light">
8
+ <div class="row small text-muted text-uppercase align-items-center">
9
+ <div class="col">Process</div>
10
+ <div class="col">Schedulers</div>
11
+ <div class="col-2 d-flex gap-2">
12
+ <span>Started</span>
13
+ </div>
14
+ <div class="col-auto">
15
+ <%= tag.button type: "button", class: "btn btn-sm text-muted ms-auto", role: "button",
16
+ data: { bs_toggle: "collapse", bs_target: ".process-state" },
17
+ aria: { expanded: false, controls: @processes.map { |process| "##{dom_id(process, 'state')}" }.join(" ") } do %>
18
+ <%= render_icon "info" %>
19
+ <span class="visually-hidden">Inspect</span>
24
20
  <% end %>
25
- </tbody>
26
- </table>
21
+ </div>
27
22
  </div>
28
- </div>
29
- <% else %>
30
- <div class="card my-3">
31
- <div class="card-body">
32
- <p class="card-text">
33
- <em>No GoodJob processes found.</em>
34
- </p>
23
+ </header>
24
+ <% @processes.each do |process| %>
25
+ <div id="<%= dom_id(process) %>" class="<%= dom_class(process) %> list-group-item py-3" role="row">
26
+ <div class="row align-items-center">
27
+ <div class="col">
28
+ <code class="font-monospace">
29
+ <span class="text-muted opacity-50">$</span>
30
+ <%= process.state["proctitle"] %>
31
+ </code>
32
+ <div>
33
+ <span class="text-muted small">PID</span>
34
+ <span class="badge rounded-pill bg-light text-dark"><%= process.state["pid"] %></span>
35
+ <span class="text-muted small">@</span>
36
+ <span class="badge rounded-pill bg-light text-dark"><%= process.state["hostname"] %></span>
37
+ </div>
38
+ </div>
39
+ <div class="col">
40
+ <% process.state["schedulers"].each do |scheduler| %>
41
+ <pre class="mb-0"><%= scheduler %></pre>
42
+ <% end %>
43
+ </div>
44
+ <div class="col-2 small"><%= relative_time(process.created_at) %></div>
45
+ <div class="col-auto">
46
+ <%= tag.button type: "button", class: "btn btn-sm text-muted ms-auto", role: "button",
47
+ title: "Inspect",
48
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(process, 'state')}" },
49
+ aria: { expanded: false, controls: dom_id(process, "state") } do %>
50
+ <%= render_icon "info" %>
51
+ <span class="visually-hidden">Inspect</span>
52
+ <% end %>
53
+ </div>
54
+ </div>
35
55
  </div>
36
- </div>
37
- <% end %>
56
+ <%= tag.div id: dom_id(process, "state"), class: "process-state list-group-item collapse small bg-dark text-light" do %>
57
+ <%= tag.pre JSON.pretty_generate(process.state) %>
58
+ <% end %>
59
+ <% end %>
60
+ <% if @processes.empty? %>
61
+ <div class="list-group-item py-4 text-center text-muted">
62
+ No GoodJob processes found.
63
+ </div>
64
+ <% end %>
65
+ </div>
38
66
  </div>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
2
+ <path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" />
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
2
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
3
+ <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
4
+ </svg>
@@ -7,9 +7,9 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
7
7
  t.text :queue_name
8
8
  t.integer :priority
9
9
  t.jsonb :serialized_params
10
- t.timestamp :scheduled_at
11
- t.timestamp :performed_at
12
- t.timestamp :finished_at
10
+ t.datetime :scheduled_at
11
+ t.datetime :performed_at
12
+ t.datetime :finished_at
13
13
  t.text :error
14
14
 
15
15
  t.timestamps
@@ -18,7 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
18
18
  t.text :concurrency_key
19
19
  t.text :cron_key
20
20
  t.uuid :retried_good_job_id
21
- t.timestamp :cron_at
21
+ t.datetime :cron_at
22
22
  end
23
23
 
24
24
  create_table :good_job_processes, id: :uuid do |t|
@@ -7,9 +7,9 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
7
7
  t.text :queue_name
8
8
  t.integer :priority
9
9
  t.jsonb :serialized_params
10
- t.timestamp :scheduled_at
11
- t.timestamp :performed_at
12
- t.timestamp :finished_at
10
+ t.datetime :scheduled_at
11
+ t.datetime :performed_at
12
+ t.datetime :finished_at
13
13
  t.text :error
14
14
 
15
15
  t.timestamps
@@ -18,7 +18,7 @@ class CreateGoodJobs < ActiveRecord::Migration<%= migration_version %>
18
18
  t.text :concurrency_key
19
19
  t.text :cron_key
20
20
  t.uuid :retried_good_job_id
21
- t.timestamp :cron_at
21
+ t.datetime :cron_at
22
22
  end
23
23
 
24
24
  create_table :good_job_processes, id: :uuid do |t|
@@ -26,7 +26,7 @@ module GoodJob
26
26
  # @return [void]
27
27
  def write_pid
28
28
  File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(::Process.pid.to_s) }
29
- at_exit { File.delete(pidfile) if File.exist?(pidfile) }
29
+ at_exit { File.delete(pidfile) if File.exist?(pidfile) } # rubocop:disable Lint/NonAtomicFileOperation
30
30
  rescue Errno::EEXIST
31
31
  check_pid
32
32
  retry
@@ -34,7 +34,7 @@ module GoodJob
34
34
 
35
35
  # @return [void]
36
36
  def delete_pid
37
- File.delete(pidfile) if File.exist?(pidfile)
37
+ File.delete(pidfile) if File.exist?(pidfile) # rubocop:disable Lint/NonAtomicFileOperation
38
38
  end
39
39
 
40
40
  # @return [void]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.2.0'
4
+ VERSION = '3.3.0'
5
5
  end
@@ -4,6 +4,7 @@ module GoodJob
4
4
  class Execution < BaseRecord
5
5
  include Lockable
6
6
  include Filterable
7
+ include Reportable
7
8
 
8
9
  # Raised if something attempts to execute a previously completed Execution again.
9
10
  PreviouslyPerformedError = Class.new(StandardError)
@@ -312,40 +313,6 @@ module GoodJob
312
313
  end
313
314
  end
314
315
 
315
- # There are 3 buckets of non-overlapping statuses:
316
- # 1. The job will be executed
317
- # - queued: The job will execute immediately when an execution thread becomes available.
318
- # - scheduled: The job is scheduled to execute in the future.
319
- # - retried: The job previously errored on execution and will be re-executed in the future.
320
- # 2. The job is being executed
321
- # - running: the job is actively being executed by an execution thread
322
- # 3. The job will not execute
323
- # - finished: The job executed successfully
324
- # - discarded: The job previously errored on execution and will not be re-executed in the future.
325
- #
326
- # @return [Symbol]
327
- def status
328
- if finished_at.present?
329
- if error.present? && retried_good_job_id.present?
330
- :retried
331
- elsif error.present? && retried_good_job_id.nil?
332
- :discarded
333
- else
334
- :finished
335
- end
336
- elsif (scheduled_at || created_at) > DateTime.current
337
- if serialized_params.fetch('executions', 0) > 1
338
- :retried
339
- else
340
- :scheduled
341
- end
342
- elsif running?
343
- :running
344
- else
345
- :queued
346
- end
347
- end
348
-
349
316
  # Return formatted serialized_params for display in the dashboard
350
317
  # @return [Hash]
351
318
  def display_serialized_params
@@ -366,11 +333,6 @@ module GoodJob
366
333
  serialized_params.fetch('executions', 0) + 1
367
334
  end
368
335
 
369
- # The last relevant timestamp for this execution
370
- def last_status_at
371
- finished_at || performed_at || scheduled_at || created_at
372
- end
373
-
374
336
  # Time between when this job was expected to run and when it started running
375
337
  def queue_latency
376
338
  now = Time.zone.now
@@ -8,6 +8,7 @@ module GoodJob
8
8
  class Job < BaseRecord
9
9
  include Filterable
10
10
  include Lockable
11
+ include Reportable
11
12
 
12
13
  # Raised when an inappropriate action is applied to a Job based on its state.
13
14
  ActionForStateMismatchError = Class.new(StandardError)
@@ -73,32 +74,6 @@ module GoodJob
73
74
  serialized_params['job_class']
74
75
  end
75
76
 
76
- def last_status_at
77
- finished_at || performed_at || scheduled_at || created_at
78
- end
79
-
80
- def status
81
- if finished_at.present?
82
- if error.present? && retried_good_job_id.present?
83
- :retried
84
- elsif error.present? && retried_good_job_id.nil?
85
- :discarded
86
- else
87
- :finished
88
- end
89
- elsif (scheduled_at || created_at) > DateTime.current
90
- if serialized_params.fetch('executions', 0) > 1
91
- :retried
92
- else
93
- :scheduled
94
- end
95
- elsif running?
96
- :running
97
- else
98
- :queued
99
- end
100
- end
101
-
102
77
  # Override #reload to add a custom scope to ensure the reloaded record is the head execution
103
78
  # @return [Job]
104
79
  def reload(options = nil)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module GoodJob
3
+ module Reportable
4
+ # There are 3 buckets of non-overlapping statuses:
5
+ # 1. The job will be executed
6
+ # - queued: The job will execute immediately when an execution thread becomes available.
7
+ # - scheduled: The job is scheduled to execute in the future.
8
+ # - retried: The job previously errored on execution and will be re-executed in the future.
9
+ # 2. The job is being executed
10
+ # - running: the job is actively being executed by an execution thread
11
+ # 3. The job will not execute
12
+ # - finished: The job executed successfully
13
+ # - discarded: The job previously errored on execution and will not be re-executed in the future.
14
+ #
15
+ # @return [Symbol]
16
+ def status
17
+ if finished_at.present?
18
+ if error.present? && retried_good_job_id.present?
19
+ :retried
20
+ elsif error.present? && retried_good_job_id.nil?
21
+ :discarded
22
+ else
23
+ :finished
24
+ end
25
+ elsif (scheduled_at || created_at) > DateTime.current
26
+ if serialized_params.fetch('executions', 0) > 1
27
+ :retried
28
+ else
29
+ :scheduled
30
+ end
31
+ elsif running?
32
+ :running
33
+ else
34
+ :queued
35
+ end
36
+ end
37
+
38
+ # The last relevant timestamp for this execution
39
+ def last_status_at
40
+ finished_at || performed_at || scheduled_at || created_at
41
+ end
42
+ end
43
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-12 00:00:00.000000000 Z
11
+ date: 2022-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -365,6 +365,7 @@ files:
365
365
  - app/assets/good_job/modules/checkbox_toggle.js
366
366
  - app/assets/good_job/modules/document_ready.js
367
367
  - app/assets/good_job/modules/live_poll.js
368
+ - app/assets/good_job/modules/popovers.js
368
369
  - app/assets/good_job/modules/toasts.js
369
370
  - app/assets/good_job/scripts.js
370
371
  - app/assets/good_job/style.css
@@ -398,7 +399,9 @@ files:
398
399
  - app/views/good_job/shared/icons/_check.html.erb
399
400
  - app/views/good_job/shared/icons/_clock.html.erb
400
401
  - app/views/good_job/shared/icons/_dash_circle.html.erb
402
+ - app/views/good_job/shared/icons/_dots.html.erb
401
403
  - app/views/good_job/shared/icons/_exclamation.html.erb
404
+ - app/views/good_job/shared/icons/_info.html.erb
402
405
  - app/views/good_job/shared/icons/_play.html.erb
403
406
  - app/views/good_job/shared/icons/_skip_forward.html.erb
404
407
  - app/views/good_job/shared/icons/_stop.html.erb
@@ -446,6 +449,7 @@ files:
446
449
  - lib/models/good_job/job.rb
447
450
  - lib/models/good_job/lockable.rb
448
451
  - lib/models/good_job/process.rb
452
+ - lib/models/good_job/reportable.rb
449
453
  homepage: https://github.com/bensheldon/good_job
450
454
  licenses:
451
455
  - MIT