good_job 2.10.0 → 2.11.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: 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