good_job 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b22cbc6d37af9dd8db290a1dc8073d2cae14c63e5cf7180b06dce1a33fb9992
4
- data.tar.gz: 8cd87b2d69afea84db87125bad625558783adea98fee138be852e6a51c43dc50
3
+ metadata.gz: 4722bcae4ba953937f1e78dd730c61898ec0aa25e4c8d5fedc6163e218de4381
4
+ data.tar.gz: fe659df13800eadf8f8d45a2d31b4b4dd466d0f9517addefdfd5169c351c7c76
5
5
  SHA512:
6
- metadata.gz: 230b335c5b6a03397c3212cd2bc59d87b2c735073233d068cf2c3045933b2aace533b0699a6fa1ecd41d77890af5aa18e1685b2c85f4c97c868773a3211736b7
7
- data.tar.gz: b3fb4cb11f129fa8576e36c4e7401cdab927cf309bda963c3b82480dbe92663847b46a8e641b123e5ad7b28c53aa361d5fb6968131e23e85b0302768cc38e5d0
6
+ metadata.gz: 3fff2b35292aebd982e6a11fac22d9a16b2468a19bf4e50c354a3f6806831fbcb2ab07d5cbe74c4ccd6bbeeb178abeeee700da0dbfdb0281f79b173b22a2ce9a
7
+ data.tar.gz: ea88184a6b3b3be56604b90af426c4c491f735dc5951854710bfeda55a1c4b930b76190845cb90b512d4e9e2f9a1f3b746478f232d10529c8a6971e448ab1419
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.11.0](https://github.com/bensheldon/good_job/tree/v2.11.0) (2022-02-27)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.10.0...v2.11.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - How do I ensure that a the same job can't run twice? \(unique job / avoid duplicates\) [\#531](https://github.com/bensheldon/good_job/issues/531)
10
+ - Bulk reschedule and discard jobs via dashboard [\#527](https://github.com/bensheldon/good_job/issues/527)
11
+ - "Live Poll" dashboard [\#526](https://github.com/bensheldon/good_job/issues/526)
12
+
13
+ **Merged pull requests:**
14
+
15
+ - Add support for live polling the dashboard [\#528](https://github.com/bensheldon/good_job/pull/528) ([danielwestendorf](https://github.com/danielwestendorf))
16
+
3
17
  ## [v2.10.0](https://github.com/bensheldon/good_job/tree/v2.10.0) (2022-02-18)
4
18
 
5
19
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.9.6...v2.10.0)
data/README.md CHANGED
@@ -369,6 +369,10 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
369
369
  end
370
370
  ```
371
371
 
372
+ #### Live Polling
373
+
374
+ The Dashboard can be set to automatically refresh by checking "Live Poll" in the Dashboard header, or by setting `?poll=10` with the interval in seconds (default 30 seconds).
375
+
372
376
  ### ActiveJob concurrency
373
377
 
374
378
  GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
@@ -1 +1,133 @@
1
- GoodJob = {};
1
+ /*jshint esversion: 6, strict: false */
2
+ const GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS = 30;
3
+ const GOOD_JOB_MINIMUM_POLL_INTERVAL = 1000;
4
+
5
+ const GoodJob = {
6
+ // Register functions to execute when the DOM is ready
7
+ ready: (callback) => {
8
+ if (document.readyState !== "loading") {
9
+ callback();
10
+ } else {
11
+ document.addEventListener("DOMContentLoaded", callback);
12
+ }
13
+ },
14
+
15
+ init: () => {
16
+ GoodJob.updateSettings();
17
+ GoodJob.addListeners();
18
+ GoodJob.pollUpdates();
19
+ GoodJob.renderCharts(true);
20
+ },
21
+
22
+ addListeners: () => {
23
+ const gjActionEls = document.querySelectorAll('[data-gj-action]');
24
+
25
+ for (let i = 0; i < gjActionEls.length; i++) {
26
+ const el = gjActionEls[i];
27
+ const [eventName, func] = el.dataset.gjAction.split('#');
28
+
29
+ el.addEventListener(eventName, GoodJob[func]);
30
+ }
31
+ },
32
+
33
+ updateSettings: () => {
34
+ const queryString = window.location.search;
35
+ const urlParams = new URLSearchParams(queryString);
36
+
37
+ // live poll interval and enablement
38
+ if (urlParams.has('poll')) {
39
+ const parsedInterval = (parseInt(urlParams.get('poll')) || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
40
+
41
+ GoodJob.pollEnabled = true;
42
+ GoodJob.pollInterval = Math.max(parsedInterval, GOOD_JOB_MINIMUM_POLL_INTERVAL);
43
+ GoodJob.setStorage('pollInterval', GoodJob.pollInterval);
44
+ } else {
45
+ GoodJob.pollEnabled = GoodJob.getStorage('pollEnabled') || false;
46
+ GoodJob.pollInterval = GoodJob.getStorage('pollInterval') || GOOD_JOB_DEFAULT_POLL_INTERVAL_SECONDS;
47
+ }
48
+
49
+ document.getElementById('toggle-poll').checked = GoodJob.pollEnabled;
50
+ },
51
+
52
+ togglePoll: (ev) => {
53
+ GoodJob.pollEnabled = ev.currentTarget.checked;
54
+ GoodJob.setStorage('pollEnabled', GoodJob.pollEnabled);
55
+ },
56
+
57
+ pollUpdates: () => {
58
+ setTimeout(() => {
59
+ if (GoodJob.pollEnabled === true) {
60
+ fetch(window.location.href)
61
+ .then(resp => resp.text())
62
+ .then(GoodJob.updateContent)
63
+ .finally(GoodJob.pollUpdates);
64
+ } else {
65
+ GoodJob.pollUpdates();
66
+ }
67
+ }, GoodJob.pollInterval);
68
+ },
69
+
70
+ updateContent: (newContent) => {
71
+ const domParser = new DOMParser();
72
+ const parsedDOM = domParser.parseFromString(newContent, "text/html");
73
+
74
+ const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]');
75
+
76
+ for (let i = 0; i < newElements.length; i++) {
77
+ const newEl = newElements[i];
78
+ const oldEl = document.getElementById(newEl.id);
79
+
80
+ if (oldEl) {
81
+ oldEl.replaceWith(newEl);
82
+ }
83
+ }
84
+
85
+ GoodJob.renderCharts(false);
86
+ },
87
+
88
+ renderCharts: (animate) => {
89
+ const charts = document.querySelectorAll('.chart');
90
+
91
+ for (let i = 0; i < charts.length; i++) {
92
+ const chartEl = charts[i];
93
+ const chartData = JSON.parse(chartEl.dataset.json);
94
+
95
+ const ctx = chartEl.getContext('2d');
96
+ const chart = new Chart(ctx, {
97
+ type: 'line',
98
+ data: {
99
+ labels: chartData.labels,
100
+ datasets: chartData.datasets
101
+ },
102
+ options: {
103
+ animation: animate,
104
+ responsive: true,
105
+ maintainAspectRatio: false,
106
+ scales: {
107
+ y: {
108
+ beginAtZero: true
109
+ }
110
+ }
111
+ }
112
+ });
113
+ }
114
+ },
115
+
116
+ getStorage: (key) => {
117
+ const value = localStorage.getItem('good_job-' + key);
118
+
119
+ if (value === 'true') {
120
+ return true;
121
+ } else if (value === 'false') {
122
+ return false;
123
+ } else {
124
+ return value;
125
+ }
126
+ },
127
+
128
+ setStorage: (key, value) => {
129
+ localStorage.setItem('good_job-' + key, value);
130
+ }
131
+ };
132
+
133
+ GoodJob.ready(GoodJob.init);
@@ -1,4 +1,4 @@
1
- <div class="card my-3">
1
+ <div class="card my-3" data-gj-poll-replace id="executions-table">
2
2
  <div class="table-responsive">
3
3
  <table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
4
4
  <thead>
@@ -1,4 +1,4 @@
1
- <div class="card my-3 p-6">
1
+ <div class="card my-3 p-6" data-gj-poll-replace id="executions-chart">
2
2
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
3
3
  </div>
4
4
 
@@ -7,7 +7,7 @@
7
7
  <%= render 'good_job/executions/table', executions: @filter.records %>
8
8
 
9
9
  <% if @filter.records.present? %>
10
- <nav aria-label="Job pagination" class="mt-3">
10
+ <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="executions-pagination">
11
11
  <ul class="pagination">
12
12
  <li class="page-item">
13
13
  <%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
@@ -1,4 +1,4 @@
1
- <div class="card my-3">
1
+ <div class="card my-3" data-gj-poll-replace id="jobs-table">
2
2
  <div class="table-responsive">
3
3
  <table class="table card-table table-bordered table-hover table-sm mb-0">
4
4
  <thead>
@@ -2,7 +2,7 @@
2
2
  <h2>All Jobs</h2>
3
3
  </div>
4
4
 
5
- <div class="card my-3 p-6">
5
+ <div class="card my-3 p-6" data-gj-poll-replace id="jobs-chart">
6
6
  <%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
7
7
  </div>
8
8
 
@@ -11,7 +11,7 @@
11
11
  <%= render 'good_job/jobs/table', jobs: @filter.records %>
12
12
 
13
13
  <% if @filter.records.present? %>
14
- <nav aria-label="Job pagination" class="mt-3">
14
+ <nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
15
15
  <ul class="pagination">
16
16
  <li class="page-item">
17
17
  <%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
@@ -2,43 +2,45 @@
2
2
  <h2>Processes</h2>
3
3
  </div>
4
4
 
5
- <% if !GoodJob::Process.migrated? %>
6
- <div class="card my-3">
7
- <div class="card-body">
8
- <p class="card-text">
9
- <em>Feature unavailable because of pending database migration.</em>
10
- </p>
5
+ <div data-gj-poll-replace id="processes">
6
+ <% if !GoodJob::Process.migrated? %>
7
+ <div class="card my-3">
8
+ <div class="card-body">
9
+ <p class="card-text">
10
+ <em>Feature unavailable because of pending database migration.</em>
11
+ </p>
12
+ </div>
11
13
  </div>
12
- </div>
13
- <% elsif @processes.present? %>
14
- <div class="card my-3">
15
- <div class="table-responsive">
16
- <table class="table card-table table-bordered table-hover table-sm mb-0">
17
- <thead>
18
- <tr>
19
- <th>Process UUID</th>
20
- <th>Created At</th></th>
21
- <th>State</th>
22
- </tr>
23
- </thead>
24
- <tbody>
25
- <% @processes.each do |process| %>
26
- <tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
27
- <td><%= process.id %></td>
28
- <td><%= relative_time(process.created_at) %></td>
29
- <td><%= tag.pre JSON.pretty_generate(process.state) %></td>
14
+ <% elsif @processes.present? %>
15
+ <div class="card my-3">
16
+ <div class="table-responsive">
17
+ <table class="table card-table table-bordered table-hover table-sm mb-0">
18
+ <thead>
19
+ <tr>
20
+ <th>Process UUID</th>
21
+ <th>Created At</th></th>
22
+ <th>State</th>
30
23
  </tr>
31
- <% end %>
32
- </tbody>
33
- </table>
24
+ </thead>
25
+ <tbody>
26
+ <% @processes.each do |process| %>
27
+ <tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
28
+ <td><%= process.id %></td>
29
+ <td><%= relative_time(process.created_at) %></td>
30
+ <td><%= tag.pre JSON.pretty_generate(process.state) %></td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
34
36
  </div>
35
- </div>
36
- <% else %>
37
- <div class="card my-3">
38
- <div class="card-body">
39
- <p class="card-text">
40
- <em>No GoodJob processes found.</em>
41
- </p>
37
+ <% else %>
38
+ <div class="card my-3">
39
+ <div class="card-body">
40
+ <p class="card-text">
41
+ <em>No GoodJob processes found.</em>
42
+ </p>
43
+ </div>
42
44
  </div>
43
- </div>
44
- <% end %>
45
+ <% end %>
46
+ </div>
@@ -1,25 +1,3 @@
1
1
  <div class="chart-wrapper">
2
- <canvas id="chart"></canvas>
2
+ <canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
3
3
  </div>
4
-
5
- <%= javascript_tag nonce: true do %>
6
- const chartData = <%== chart_data.to_json %>;
7
-
8
- const ctx = document.getElementById('chart').getContext('2d');
9
- const chart = new Chart(ctx, {
10
- type: 'line',
11
- data: {
12
- labels: chartData.labels,
13
- datasets: chartData.datasets
14
- },
15
- options: {
16
- responsive: true,
17
- maintainAspectRatio: false,
18
- scales: {
19
- y: {
20
- beginAtZero: true
21
- }
22
- }
23
- }
24
- });
25
- <% end %>
@@ -1,4 +1,5 @@
1
1
  <%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
2
+ <%= hidden_field_tag :poll, value: params[:poll] %>
2
3
  <div class="d-flex flex-row w-100">
3
4
  <div class="me-2">
4
5
  <label for="job_class_filter">Job class</label>
@@ -55,4 +56,4 @@
55
56
  });
56
57
  })
57
58
  })
58
- <% end %>
59
+ <% end %>
@@ -45,7 +45,10 @@
45
45
  </div>
46
46
  </li>
47
47
  </ul>
48
- <div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
48
+ <div>
49
+ <input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
50
+ <label for="toggle-poll">Live Poll</label>
51
+ </div>
49
52
  </div>
50
53
  </div>
51
54
  </nav>
@@ -70,7 +73,24 @@
70
73
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
71
74
  </div>
72
75
  <% end %>
76
+
73
77
  <%= yield %>
74
78
  </div>
79
+
80
+ <footer class="footer mt-auto py-3 bg-light fixed-bottom" id="footer" data-gj-poll-replace>
81
+ <div class="container-fluid">
82
+ <div class="row">
83
+ <div class="col-6">
84
+ <span class="text-muted">
85
+ Last updated: <time id="page-updated-at" datetime="<%= Time.current.utc.iso8601 %>"><%= Time.current %></time>
86
+ </span>
87
+ </div>
88
+
89
+ <div class="col-6 text-end">
90
+ Remember, you're doing a Good Job too!
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </footer>
75
95
  </body>
76
96
  </html>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.10.0'
4
+ VERSION = '2.11.0'
5
5
  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: 2.10.0
4
+ version: 2.11.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-02-18 00:00:00.000000000 Z
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob