From 29528cfeea91dcf8771551c44fd4dc5e29f87718 Mon Sep 17 00:00:00 2001 From: Rogger Valverde Date: Tue, 18 Oct 2022 10:57:51 -0500 Subject: [PATCH] feat(bull): support removing repeatable jobs (#574) --- example/bull.js | 3 ++ public/dashboard.js | 36 +++++++++++++++++++ src/server/views/api/index.js | 5 +++ src/server/views/api/repeatableJobRemove.js | 23 ++++++++++++ src/server/views/dashboard/jobDetails.js | 2 ++ .../views/dashboard/queueJobsByState.js | 2 ++ .../views/partials/dashboard/jobDetails.hbs | 7 ++++ 7 files changed, 78 insertions(+) create mode 100644 src/server/views/api/repeatableJobRemove.js diff --git a/example/bull.js b/example/bull.js index 4b4086d0..d9e09e22 100644 --- a/example/bull.js +++ b/example/bull.js @@ -38,6 +38,9 @@ async function main() { const delayedJob = await queue.add({}, {delay: 60 * 1000}); delayedJob.log('Log message'); + // add repeatable jobs + await queue.add({}, {repeat: {cron: '15 * * * *'}}); + const app = Arena( { Bull, diff --git a/public/dashboard.js b/public/dashboard.js index bb6dd3fa..da589ab2 100644 --- a/public/dashboard.js +++ b/public/dashboard.js @@ -127,6 +127,42 @@ $(document).ready(() => { } }); + // Set up individual "remove repeatable job" handler + $('.js-remove-repeatable-job').on('click', function (e) { + e.preventDefault(); + $(this).prop('disabled', true); + + const jobId = $(this).data('job-id'); + const queueName = $(this).data('queue-name'); + const queueHost = $(this).data('queue-host'); + const jobState = $(this).data('job-state'); + + const confirmationResponse = window.confirm( + `Remove repeatable job #${jobId} in queue "${queueHost}/${queueName}"?` + ); + if (confirmationResponse) { + $.ajax({ + method: 'DELETE', + url: `${basePath}/api/queue/${encodeURIComponent( + queueHost + )}/${encodeURIComponent(queueName)}/repeatable/job/${encodeURIComponent( + jobId + )}`, + }) + .done(() => { + window.location.href = `${basePath}/${encodeURIComponent( + queueHost + )}/${encodeURIComponent(queueName)}/${jobState}`; + }) + .fail((jqXHR) => { + window.alert(`Request failed, check console for error.`); + console.error(jqXHR.responseText); + }); + } else { + $(this).prop('disabled', false); + } + }); + // Set up "select all jobs" button handler $('.js-select-all-jobs').change(function () { const $jobBulkCheckboxes = $('.js-bulk-job'); diff --git a/src/server/views/api/index.js b/src/server/views/api/index.js index cefd7159..50523e44 100644 --- a/src/server/views/api/index.js +++ b/src/server/views/api/index.js @@ -6,6 +6,7 @@ const jobAdd = require('./jobAdd'); const jobPromote = require('./jobPromote'); const jobRetry = require('./jobRetry'); const jobRemove = require('./jobRemove'); +const repeatableJobRemove = require('./repeatableJobRemove'); const bulkJobsPromote = require('./bulkJobsPromote'); const bulkJobsRemove = require('./bulkJobsRemove'); const bulkJobsRetry = require('./bulkJobsRetry'); @@ -19,6 +20,10 @@ router.post('/queue/:queueHost/:queueName/job/bulk', bulkJobsRemove); router.patch('/queue/:queueHost/:queueName/job/bulk', bulkJobsRetry); router.patch('/queue/:queueHost/:queueName/delayed/job/bulk', bulkJobsPromote); router.patch('/queue/:queueHost/:queueName/delayed/job/:id', jobPromote); +router.delete( + '/queue/:queueHost/:queueName/repeatable/job/:id', + repeatableJobRemove +); router.patch('/queue/:queueHost/:queueName/job/:id', jobRetry); router.put('/queue/:queueHost/:queueName/pause', queuePause); router.put('/queue/:queueHost/:queueName/resume', queueResume); diff --git a/src/server/views/api/repeatableJobRemove.js b/src/server/views/api/repeatableJobRemove.js new file mode 100644 index 00000000..09b41b60 --- /dev/null +++ b/src/server/views/api/repeatableJobRemove.js @@ -0,0 +1,23 @@ +async function handler(req, res) { + const {queueName, queueHost, id} = req.params; + + const {Queues} = req.app.locals; + const queue = await Queues.get(queueName, queueHost); + if (!queue) return res.status(404).send({error: 'queue not found'}); + + const job = await queue.getJob(id); + if (!job) return res.status(404).send({error: 'job not found'}); + + try { + await queue.removeRepeatableByKey(job.opts.repeat.key); + return res.sendStatus(200); + } catch (e) { + const body = { + error: 'queue error', + details: e.stack, + }; + return res.status(500).send(body); + } +} + +module.exports = handler; diff --git a/src/server/views/dashboard/jobDetails.js b/src/server/views/dashboard/jobDetails.js index 1e8c42a4..7d9030d4 100644 --- a/src/server/views/dashboard/jobDetails.js +++ b/src/server/views/dashboard/jobDetails.js @@ -35,6 +35,8 @@ async function handler(req, res) { job.showRetryButton = !queue.IS_BEE || jobState === 'failed'; job.retryButtonText = jobState === 'failed' ? 'Retry' : 'Trigger'; job.showPromoteButton = !queue.IS_BEE && jobState === 'delayed'; + job.showDeleteRepeatableButton = + queue.IS_BULL && job.opts.repeat && job.opts.repeat.key; const stacktraces = queue.IS_BEE ? job.options.stacktraces : job.stacktrace; if (!queue.IS_BEE) { diff --git a/src/server/views/dashboard/queueJobsByState.js b/src/server/views/dashboard/queueJobsByState.js index 62b054cb..c941ec5e 100644 --- a/src/server/views/dashboard/queueJobsByState.js +++ b/src/server/views/dashboard/queueJobsByState.js @@ -130,6 +130,8 @@ async function _html(req, res) { job.showRetryButton = !queue.IS_BEE || jobState === 'failed'; job.retryButtonText = jobState === 'failed' ? 'Retry' : 'Trigger'; job.showPromoteButton = !queue.IS_BEE && jobState === 'delayed'; + job.showDeleteRepeatableButton = + queue.IS_BULL && job.opts.repeat && job.opts.repeat.key; job.parent = JobHelpers.getKeyProperties(job.parentKey); } diff --git a/src/server/views/partials/dashboard/jobDetails.hbs b/src/server/views/partials/dashboard/jobDetails.hbs index ae05ac33..3607cda5 100644 --- a/src/server/views/partials/dashboard/jobDetails.hbs +++ b/src/server/views/partials/dashboard/jobDetails.hbs @@ -22,6 +22,13 @@ {{/if}} +{{#if showDeleteRepeatableButton}} + +{{/if}} +
State