├── .husky ├── .gitignore └── pre-commit ├── public ├── vendor │ ├── .gitignore │ ├── img │ │ └── .gitignore │ ├── bootstrap-treeview.min.css │ ├── tablesort.css │ ├── tablesort.min.js │ └── bootstrap-treeview.min.js ├── favicon.ico ├── highlight.default.min.css ├── dashboard.css └── dashboard.js ├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── node.js.yml ├── .prettierignore ├── screenshots ├── screen1.png ├── screen2.png ├── screen3.png ├── screen1_sm.png ├── screen2_sm.png └── screen3_sm.png ├── .lintstagedrc ├── commitlint.config.js ├── src └── server │ ├── views │ ├── api │ │ ├── bulkJobsClean.js │ │ ├── bulkJobsRetry.js │ │ ├── bulkJobsPromote.js │ │ ├── bulkJobsRemove.js │ │ ├── queueResume.js │ │ ├── queuePause.js │ │ ├── queueRemoveRateLimitKey.js │ │ ├── jobAdd.js │ │ ├── jobRemove.js │ │ ├── jobPromote.js │ │ ├── addFlow.js │ │ ├── jobDataUpdate.js │ │ ├── getFlow.js │ │ ├── jobRetry.js │ │ ├── repeatableJobRemove.js │ │ ├── queueUpdateMeta.js │ │ ├── bulkAction.js │ │ └── index.js │ ├── routes.js │ ├── dashboard │ │ ├── flowList.js │ │ ├── queueList.js │ │ ├── templates │ │ │ ├── flowNotFound.hbs │ │ │ ├── queueNotFound.hbs │ │ │ ├── jobNotFound.hbs │ │ │ ├── jobStateNotFound.hbs │ │ │ ├── flowList.hbs │ │ │ ├── queueList.hbs │ │ │ ├── jobDetails.hbs │ │ │ ├── flowDetails.hbs │ │ │ ├── queueDetails.hbs │ │ │ └── queueJobsByState.hbs │ │ ├── flowDetails.js │ │ ├── index.js │ │ ├── queueDetails.js │ │ ├── jobDetails.js │ │ └── queueJobsByState.js │ ├── helpers │ │ ├── jobHelpers.js │ │ ├── flowHelpers.js │ │ ├── queueHelpers.js │ │ └── handlebars.js │ ├── partials │ │ └── dashboard │ │ │ ├── flowTree.hbs │ │ │ └── jobDetails.hbs │ └── layout.hbs │ ├── config │ └── index.json │ ├── app.js │ ├── flow │ └── index.js │ └── queue │ └── index.js ├── .gitignore ├── docker-compose.yml ├── example ├── docker-compose.yml ├── public │ └── example.js ├── package.json ├── README.md ├── bee.js ├── bull.js ├── express.js ├── bullmq.js ├── fastify.js └── bullmq_with_flows.js ├── .prettierrc ├── CONTRIBUTING.md ├── release.config.js ├── .eslintrc.js ├── LICENSE ├── index.js ├── package.json ├── README.md └── CHANGELOG.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /public/vendor/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vendor/img/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github @skeggse 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.hbs 2 | *.min.js 3 | *.min.css 4 | pull_request_template.md 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen1.png -------------------------------------------------------------------------------- /screenshots/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen2.png -------------------------------------------------------------------------------- /screenshots/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen3.png -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js}": "node_modules/.bin/eslint . --ignore-path ./.prettierignore --fix" 3 | } -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /screenshots/screen1_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen1_sm.png -------------------------------------------------------------------------------- /screenshots/screen2_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen2_sm.png -------------------------------------------------------------------------------- /screenshots/screen3_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bee-queue/arena/HEAD/screenshots/screen3_sm.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run pretty:quick 5 | npm run lint:staged -------------------------------------------------------------------------------- /src/server/views/api/bulkJobsClean.js: -------------------------------------------------------------------------------- 1 | const bulkAction = require('./bulkAction'); 2 | 3 | module.exports = bulkAction('clean'); 4 | -------------------------------------------------------------------------------- /src/server/views/api/bulkJobsRetry.js: -------------------------------------------------------------------------------- 1 | const bulkAction = require('./bulkAction'); 2 | 3 | module.exports = bulkAction('retry'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.DS_Store 4 | .vscode 5 | .idea 6 | *.tern-port 7 | *.sublime-workspace 8 | dump.rdb -------------------------------------------------------------------------------- /src/server/views/api/bulkJobsPromote.js: -------------------------------------------------------------------------------- 1 | const bulkAction = require('./bulkAction'); 2 | 3 | module.exports = bulkAction('promote'); 4 | -------------------------------------------------------------------------------- /src/server/views/api/bulkJobsRemove.js: -------------------------------------------------------------------------------- 1 | const bulkAction = require('./bulkAction'); 2 | 3 | module.exports = bulkAction('remove'); 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | redis: 4 | image: redis:6-alpine 5 | container_name: redis-6 6 | ports: 7 | - 6379:6379 8 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | redis: 4 | image: redis:7-alpine 5 | container_name: cache 6 | ports: 7 | - 6379:6379 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "bracketSpacing": false, 8 | "arrowParens": "always", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /public/vendor/bootstrap-treeview.min.css: -------------------------------------------------------------------------------- 1 | .treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed} -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Arena 2 | 3 | The easiest way to get started contributing is to run the [example project](example/README.md). 4 | 5 | ### Development 6 | 7 | Arena is written using Express, with simple jQuery and Handlebars on the front end. 8 | -------------------------------------------------------------------------------- /src/server/config/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "__example_queues": [ 3 | { 4 | "name": "my_queue", 5 | "port": 6381, 6 | "host": "127.0.0.1", 7 | "hostId": "AWS Server 2" 8 | } 9 | ], 10 | "queues": [], 11 | "flows": [] 12 | } 13 | -------------------------------------------------------------------------------- /src/server/views/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const dashboardRoutes = require('./dashboard'); 4 | const apiRoutes = require('./api'); 5 | 6 | router.use('/api', apiRoutes); 7 | router.use('/', dashboardRoutes); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: widen 9 | commit-message: 10 | prefix: chore 11 | include: scope 12 | -------------------------------------------------------------------------------- /src/server/views/dashboard/flowList.js: -------------------------------------------------------------------------------- 1 | function handler(req, res) { 2 | const {Flows} = req.app.locals; 3 | const flows = Flows.list(); 4 | const basePath = req.baseUrl; 5 | 6 | return res.render('dashboard/templates/flowList', {basePath, flows}); 7 | } 8 | 9 | module.exports = handler; 10 | -------------------------------------------------------------------------------- /src/server/views/helpers/jobHelpers.js: -------------------------------------------------------------------------------- 1 | const Helpers = { 2 | getKeyProperties: function (jobData) { 3 | if (!jobData) return ''; 4 | const [, queueName, id] = jobData.split(':'); 5 | 6 | return { 7 | id, 8 | queueName, 9 | }; 10 | }, 11 | }; 12 | 13 | module.exports = Helpers; 14 | -------------------------------------------------------------------------------- /src/server/views/partials/dashboard/flowTree.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Flow Tree

5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/server/views/dashboard/queueList.js: -------------------------------------------------------------------------------- 1 | function handler(req, res) { 2 | const {Queues, Flows} = req.app.locals; 3 | const queues = Queues.list(); 4 | const basePath = req.baseUrl; 5 | 6 | return res.render('dashboard/templates/queueList', { 7 | basePath, 8 | queues, 9 | hasFlows: Flows.hasFlows(), 10 | }); 11 | } 12 | 13 | module.exports = handler; 14 | -------------------------------------------------------------------------------- /example/public/example.js: -------------------------------------------------------------------------------- 1 | function myFunction() { 2 | const navBarList = document.getElementById('navbar-list'); 3 | const a = document.createElement('a'); 4 | a.textContent = 'google'; 5 | a.setAttribute('href', 'https://www.google.com'); 6 | 7 | const li = document.createElement('li'); 8 | li.appendChild(a); 9 | navBarList.appendChild(li); 10 | } 11 | 12 | myFunction(); 13 | -------------------------------------------------------------------------------- /src/server/views/api/queueResume.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | 6 | const queue = await Queues.get(queueName, queueHost); 7 | if (!queue) return res.status(404).json({error: 'queue not found'}); 8 | 9 | try { 10 | await queue.resume(); 11 | } catch (err) { 12 | return res.status(500).json({error: err.message}); 13 | } 14 | return res.sendStatus(200); 15 | } 16 | 17 | module.exports = handler; 18 | -------------------------------------------------------------------------------- /src/server/views/api/queuePause.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | 6 | const queue = await Queues.get(queueName, queueHost); 7 | 8 | if (!queue) return res.status(404).json({error: 'queue not found'}); 9 | 10 | try { 11 | await queue.pause(); 12 | } catch (err) { 13 | return res.status(500).json({error: err.message}); 14 | } 15 | return res.sendStatus(200); 16 | } 17 | 18 | module.exports = handler; 19 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/flowNotFound.hbs: -------------------------------------------------------------------------------- 1 |

Flow {{ queueHost }}/{{ queueName }}

2 | 3 |

404

4 |

Flow {{ queueHost }}/{{ queueName }} not found

5 |

6 | Go back to Overview 7 |

8 | 9 | {{#contentFor 'sidebar'}} 10 |
  • Queues Overview
  • 11 |
  • Flows Overview
  • 12 |
  • 404
  • 13 | {{/contentFor}} -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | ['@semantic-release/commit-analyzer', {preset: 'conventionalcommits'}], 4 | [ 5 | '@semantic-release/release-notes-generator', 6 | {preset: 'conventionalcommits'}, 7 | ], 8 | '@semantic-release/github', 9 | '@semantic-release/changelog', 10 | [ 11 | '@semantic-release/exec', 12 | {prepareCmd: 'npx prettier --write CHANGELOG.md'}, 13 | ], 14 | '@semantic-release/npm', 15 | '@semantic-release/git', 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/server/views/api/queueRemoveRateLimitKey.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | 6 | const queue = await Queues.get(queueName, queueHost); 7 | if (!queue) return res.status(404).json({error: 'queue not found'}); 8 | 9 | try { 10 | await queue.removeRateLimitKey(); 11 | } catch (err) { 12 | return res.status(500).json({error: err.message}); 13 | } 14 | return res.sendStatus(200); 15 | } 16 | 17 | module.exports = handler; 18 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/queueNotFound.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 |

    404

    4 |

    Queue {{ queueHost }}/{{ queueName }} not found

    5 |

    6 | Go back to Overview 7 |

    8 | 9 | {{#contentFor 'sidebar'}} 10 |
  • Queues Overview
  • 11 |
  • 404
  • 12 | {{#if hasFlows}} 13 |
  • Flows Overview
  • 14 | {{/if}} 15 | {{/contentFor}} -------------------------------------------------------------------------------- /src/server/views/api/jobAdd.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost} = req.params; 3 | const {name, data, opts} = req.body; 4 | 5 | const {Queues} = req.app.locals; 6 | 7 | const queue = await Queues.get(queueName, queueHost); 8 | if (!queue) return res.status(404).json({error: 'queue not found'}); 9 | 10 | try { 11 | await Queues.set(queue, data, name, opts); 12 | } catch (err) { 13 | return res.status(500).json({error: err.message}); 14 | } 15 | return res.sendStatus(200); 16 | } 17 | 18 | module.exports = handler; 19 | -------------------------------------------------------------------------------- /src/server/views/helpers/flowHelpers.js: -------------------------------------------------------------------------------- 1 | const Helpers = { 2 | processFlow: function (flow) { 3 | if (!flow) return {}; 4 | const {job, children = []} = flow; 5 | const filteredChildren = children.filter((child) => child); 6 | const queueName = job.queueName; 7 | 8 | if (filteredChildren.length > 0) { 9 | return { 10 | job: {...job, queueName}, 11 | children: filteredChildren.map((child) => this.processFlow(child)), 12 | }; 13 | } else { 14 | return { 15 | job: {...job, queueName}, 16 | }; 17 | } 18 | }, 19 | }; 20 | 21 | module.exports = Helpers; 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Changes Made 2 | 3 | #### Potential Risks 4 | 5 | 6 | #### Test Plan 7 | 8 | 9 | #### Checklist 10 | 11 | - [ ] I've increased test coverage 12 | - [ ] Since this is a public repository, I've checked I'm not publishing private data in the code, commit comments, or this PR. 13 | -------------------------------------------------------------------------------- /src/server/views/api/jobRemove.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost, id} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | const queue = await Queues.get(queueName, queueHost); 6 | if (!queue) return res.status(404).send({error: 'queue not found'}); 7 | 8 | const job = await queue.getJob(id); 9 | if (!job) return res.status(404).send({error: 'job not found'}); 10 | 11 | try { 12 | await job.remove(); 13 | return res.sendStatus(200); 14 | } catch (e) { 15 | const body = { 16 | error: 'queue error', 17 | details: e.stack, 18 | }; 19 | return res.status(500).send(body); 20 | } 21 | } 22 | 23 | module.exports = handler; 24 | -------------------------------------------------------------------------------- /src/server/views/api/jobPromote.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost, id} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | const queue = await Queues.get(queueName, queueHost); 6 | if (!queue) return res.status(404).send({error: 'queue not found'}); 7 | 8 | const job = await queue.getJob(id); 9 | if (!job) return res.status(404).send({error: 'job not found'}); 10 | 11 | try { 12 | await job.promote(); 13 | return res.sendStatus(200); 14 | } catch (e) { 15 | const body = { 16 | error: 'queue error', 17 | details: e.stack, 18 | }; 19 | return res.status(500).send(body); 20 | } 21 | } 22 | 23 | module.exports = handler; 24 | -------------------------------------------------------------------------------- /src/server/views/api/addFlow.js: -------------------------------------------------------------------------------- 1 | const flowHelpers = require('../helpers/flowHelpers'); 2 | 3 | async function handler(req, res) { 4 | const {connectionName, flowHost} = req.params; 5 | const {data} = req.body; 6 | 7 | const {Flows} = req.app.locals; 8 | 9 | const flow = await Flows.get(connectionName, flowHost); 10 | if (!flow) return res.status(404).json({error: 'flow not found'}); 11 | 12 | try { 13 | const flowTree = await Flows.set(flow, data); 14 | const processedFlow = flowHelpers.processFlow(flowTree); 15 | 16 | return res.status(200).json(processedFlow); 17 | } catch (err) { 18 | return res.status(500).json({error: err.message}); 19 | } 20 | } 21 | 22 | module.exports = handler; 23 | -------------------------------------------------------------------------------- /src/server/views/dashboard/flowDetails.js: -------------------------------------------------------------------------------- 1 | const QueueHelpers = require('../helpers/queueHelpers'); 2 | 3 | async function handler(req, res) { 4 | const {connectionName, flowHost} = req.params; 5 | const {Flows} = req.app.locals; 6 | const flow = await Flows.get(connectionName, flowHost); 7 | const basePath = req.baseUrl; 8 | 9 | if (!flow) 10 | return res.status(404).render('dashboard/templates/flowNotFound', { 11 | basePath, 12 | connectionName, 13 | flowHost, 14 | }); 15 | 16 | const stats = await QueueHelpers.getStats(flow); 17 | 18 | return res.render('dashboard/templates/flowDetails', { 19 | basePath, 20 | connectionName, 21 | flowHost, 22 | stats, 23 | }); 24 | } 25 | 26 | module.exports = handler; 27 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/jobNotFound.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 |

    404

    4 |

    Job {{ id }} not found

    5 |

    6 | Go back to 7 | {{ queueHost }}/{{ queueName }} 8 |

    9 | 10 | {{#contentFor 'sidebar'}} 11 |
  • Queues Overview
  • 12 |
  • Queue 13 | {{ queueHost }}/{{ queueName }}
  • 14 |
  • 404
  • 15 | {{#if hasFlows}} 16 |
  • Flows Overview
  • 17 | {{/if}} 18 | {{/contentFor}} -------------------------------------------------------------------------------- /src/server/views/api/jobDataUpdate.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost, id} = req.params; 3 | const data = req.body; 4 | 5 | const {Queues} = req.app.locals; 6 | 7 | const queue = await Queues.get(queueName, queueHost); 8 | if (!queue) return res.status(404).json({error: 'queue not found'}); 9 | 10 | const job = await queue.getJob(id); 11 | if (!job) return res.status(404).send({error: 'job not found'}); 12 | 13 | try { 14 | if (job.updateData) { 15 | await job.updateData(data); 16 | } else { 17 | await job.update(data); 18 | } 19 | } catch (err) { 20 | return res.status(500).json({error: err.message}); 21 | } 22 | return res.sendStatus(200); 23 | } 24 | 25 | module.exports = handler; 26 | -------------------------------------------------------------------------------- /public/highlight.default.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/jobStateNotFound.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 |

    400

    4 |

    Job state {{ state }} does not exist

    5 |

    6 | Go back to 7 | {{ queueHost }}/{{ queueName }} 8 |

    9 | 10 | {{#contentFor 'sidebar'}} 11 |
  • Queues Overview
  • 12 |
  • Queue 13 | {{ queueHost }}/{{ queueName }}
  • 14 |
  • 404
  • 15 | {{#if hasFlows}} 16 |
  • Flows Overview
  • 17 | {{/if}} 18 | {{/contentFor}} -------------------------------------------------------------------------------- /public/vendor/tablesort.css: -------------------------------------------------------------------------------- 1 | th[role='columnheader']:not(.no-sort) { 2 | cursor: pointer; 3 | } 4 | 5 | th[role='columnheader']:not(.no-sort):after { 6 | content: ''; 7 | float: right; 8 | margin-top: 7px; 9 | border-width: 0 4px 4px; 10 | border-style: solid; 11 | border-color: #404040 transparent; 12 | visibility: hidden; 13 | opacity: 0; 14 | -ms-user-select: none; 15 | -webkit-user-select: none; 16 | -moz-user-select: none; 17 | user-select: none; 18 | } 19 | 20 | th[aria-sort='ascending']:not(.no-sort):after { 21 | border-bottom: none; 22 | border-width: 4px 4px 0; 23 | } 24 | 25 | th[aria-sort]:not(.no-sort):after { 26 | visibility: visible; 27 | opacity: 0.4; 28 | } 29 | 30 | th[role='columnheader']:not(.no-sort):hover:after { 31 | visibility: visible; 32 | opacity: 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/server/views/dashboard/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const queueList = require('./queueList'); 4 | const queueDetails = require('./queueDetails'); 5 | const queueJobsByState = require('./queueJobsByState'); 6 | const flowList = require('./flowList'); 7 | const flowDetails = require('./flowDetails'); 8 | const jobDetails = require('./jobDetails'); 9 | 10 | router.get('/', queueList); 11 | router.get('/flows', flowList); 12 | router.get('/flows/:flowHost/:connectionName', flowDetails); 13 | router.get('/:queueHost/:queueName', queueDetails); 14 | router.get( 15 | '/:queueHost/:queueName/:state(waiting|prioritized|active|completed|succeeded|failed|delayed|paused|waiting-children).:ext?', 16 | queueJobsByState 17 | ); 18 | router.get('/:queueHost/:queueName/:id', jobDetails); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /src/server/views/api/getFlow.js: -------------------------------------------------------------------------------- 1 | const flowHelpers = require('../helpers/flowHelpers'); 2 | 3 | async function handler(req, res) { 4 | const {connectionName, flowHost} = req.params; 5 | const {depth, jobId, maxChildren, queueName} = req.query; 6 | const {Flows} = req.app.locals; 7 | const flow = await Flows.get(connectionName, flowHost); 8 | if (!flow) return res.status(404).json({error: 'flow not found'}); 9 | try { 10 | const flowTree = await flow.getFlow({ 11 | id: jobId, 12 | queueName, 13 | depth: Number(depth), 14 | maxChildren: Number(maxChildren), 15 | }); 16 | const processedFlow = flowHelpers.processFlow(flowTree); 17 | 18 | return res.status(200).json(processedFlow); 19 | } catch (err) { 20 | return res.status(500).json({error: err.message}); 21 | } 22 | } 23 | module.exports = handler; 24 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/flowList.hbs: -------------------------------------------------------------------------------- 1 |

    Flows

    2 |

    Overview

    3 | 4 |
    5 | 6 | 7 | 8 | 9 | 10 | 11 | {{#each flows}} 12 | 13 | 14 | 16 | 17 | {{/each}} 18 | 19 |
    HostName
    {{ this.hostId }}{{ this.name }} 15 |
    20 |
    21 | 22 | {{#contentFor 'sidebar'}} 23 |
  • Queues Overview
  • 24 |
  • Flows Overview
  • 25 | {{/contentFor}} 26 | 27 | {{#contentFor 'script'}} 28 | new Tablesort(document.getElementById('flowList')); 29 | {{/contentFor}} -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/queueList.hbs: -------------------------------------------------------------------------------- 1 |

    Queues

    2 |

    Overview

    3 | 4 |
    5 | 6 | 7 | 8 | 9 | 10 | 11 | {{#each queues}} 12 | 13 | 14 | 15 | 16 | {{/each}} 17 | 18 |
    HostName
    {{ this.hostId }}{{ this.name }}
    19 |
    20 | 21 | {{#contentFor 'sidebar'}} 22 |
  • Queues Overview
  • 23 | {{#if hasFlows}} 24 |
  • Flows Overview
  • 25 | {{/if}} 26 | {{/contentFor}} 27 | 28 | {{#contentFor 'script'}} 29 | new Tablesort(document.getElementById('queueList')); 30 | {{/contentFor}} -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arena-example-project", 3 | "version": "1.0.0", 4 | "description": "An example project that uses Arena", 5 | "main": "bee.js", 6 | "scripts": { 7 | "dc:up": "docker-compose -f docker-compose.yml up -d", 8 | "dc:down": "docker-compose -f docker-compose.yml down", 9 | "start:fastify": "node fastify.js", 10 | "start:express": "node express.js", 11 | "start:bee": "node bee.js", 12 | "start:bull": "node bull.js", 13 | "start:bullmq": "node bullmq.js", 14 | "start:bullmq_with_flows": "node bullmq_with_flows.js", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@fastify/express": "^2.3.0", 21 | "bee-queue": "^1.4.0", 22 | "bull": "^4.12.4", 23 | "bullmq": "^5.63.0", 24 | "express": "^4.17.1", 25 | "fastify": "^4.13.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/server/views/api/jobRetry.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost, id} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | 6 | const queue = await Queues.get(queueName, queueHost); 7 | if (!queue) return res.status(404).send({error: 'queue not found'}); 8 | 9 | const job = await queue.getJob(id); 10 | if (!job) return res.status(404).send({error: 'job not found'}); 11 | 12 | try { 13 | const jobState = queue.IS_BEE ? job.status : await job.getState(); 14 | 15 | if (jobState === 'failed' && typeof job.retry === 'function') { 16 | await job.retry(); 17 | } else { 18 | await Queues.set(queue, job.data, job.name); 19 | } 20 | 21 | return res.sendStatus(200); 22 | } catch (e) { 23 | const body = { 24 | error: 'queue error', 25 | details: e.stack, 26 | }; 27 | return res.status(500).send(body); 28 | } 29 | } 30 | 31 | module.exports = handler; 32 | -------------------------------------------------------------------------------- /src/server/views/api/repeatableJobRemove.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost, id} = req.params; 3 | 4 | const {Queues} = req.app.locals; 5 | const queue = await Queues.get(queueName, queueHost); 6 | if (!queue) return res.status(404).send({error: 'queue not found'}); 7 | 8 | const job = await queue.getJob(id); 9 | if (!job) return res.status(404).send({error: 'job not found'}); 10 | 11 | try { 12 | if (job.opts.repeat.key || job.repeatJobKey) { 13 | await queue.removeRepeatableByKey( 14 | job.opts.repeat.key || job.repeatJobKey 15 | ); 16 | } else { 17 | await queue.removeRepeatable(job.name, job.opts.repeat, job.opts.jobId); 18 | } 19 | return res.sendStatus(200); 20 | } catch (e) { 21 | const body = { 22 | error: 'queue error', 23 | details: e.stack, 24 | }; 25 | return res.status(500).send(body); 26 | } 27 | } 28 | 29 | module.exports = handler; 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true, 5 | browser: true, 6 | commonjs: true, 7 | jquery: true, 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | }, 12 | plugins: ['prettier'], 13 | extends: ['prettier'], 14 | overrides: [ 15 | { 16 | files: ['lib/**'], 17 | rules: { 18 | 'max-len': 'error', 19 | }, 20 | }, 21 | { 22 | files: ['benchmark/**', 'examples/**'], 23 | parserOptions: { 24 | ecmaVersion: 8, 25 | }, 26 | rules: { 27 | 'no-console': 'off', 28 | }, 29 | }, 30 | { 31 | files: ['test/**'], 32 | parserOptions: { 33 | ecmaVersion: 2017, 34 | sourceType: 'module', 35 | }, 36 | rules: { 37 | 'handle-callback-err': 'warn', 38 | 'no-shadow': 'off', 39 | 'no-warning-comments': 'off', 40 | }, 41 | }, 42 | ], 43 | rules: { 44 | strict: 'off', 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a simple demonstration of how to run Arena and connect it to [Bee Queue](https://github.com/mixmaxhq/bee-queue) or [Bull Queue](https://github.com/OptimalBits/bull) or [BullMQ](https://github.com/taskforcesh/bullmq). 4 | 5 | ## Requirements 6 | 7 | - Node >= 7.6 8 | - No other services running on ports 4735 or 6379 9 | 10 | ## Start Redis 11 | 12 | In case you don't have redis installed, there is a redis docker-compose for development purposes. 13 | 14 | - Before starting Redis, make sure you have [docker-compose](https://docs.docker.com/compose/install/) installed. 15 | - Then execute `npm run dc:up` 16 | 17 | ## Install 18 | 19 | `npm install` 20 | 21 | ## Running 22 | 23 | `npm run start:fastify` 24 | 25 | or 26 | 27 | `npm run start:express` 28 | 29 | or 30 | 31 | `npm run start:bee` 32 | 33 | or 34 | 35 | `npm run start:bull` 36 | 37 | or 38 | 39 | `npm run start:bullmq` 40 | 41 | or 42 | 43 | `npm run start:bullmq_with_flows` 44 | 45 | Then open http://localhost:4735 in your browser. 46 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/jobDetails.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 | {{> dashboard/jobDetails job basePath=basePath queueName=queueName queueHost=queueHost jobState=jobState stacktraces=stacktraces view=true}} 4 | 5 | {{#contentFor 'sidebar'}} 6 |
  • Queues Overview
  • 7 |
  • Queue 8 | {{ queueHost }}/{{ queueName }}
  • 9 |
  • {{capitalize jobState}} 10 | Jobs
  • 11 |
  • Job {{ job.id }}
  • 12 | {{#if hasFlows}} 13 |
  • Flows Overview
  • 14 | {{/if}} 15 | {{/contentFor}} 16 | 17 | {{#contentFor 'script'}} 18 | if(document.getElementById('jsoneditor')) { 19 | window.jsonEditor = new JSONEditor(document.getElementById('jsoneditor'), { modes: ['code','tree','text'] }); 20 | window.jsonEditor.set({{json job.data true}}) 21 | } 22 | {{/contentFor}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mixmax, inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/server/views/api/queueUpdateMeta.js: -------------------------------------------------------------------------------- 1 | async function handler(req, res) { 2 | const {queueName, queueHost} = req.params; 3 | const data = req.body; 4 | 5 | const {Queues} = req.app.locals; 6 | 7 | const queue = await Queues.get(queueName, queueHost); 8 | 9 | if (!queue) return res.status(404).json({error: 'queue not found'}); 10 | 11 | try { 12 | if (queue.setGlobalConcurrency && queue.setGlobalRateLimit) { 13 | if (data.concurrency !== null && data.concurrency !== undefined) { 14 | await queue.setGlobalConcurrency(data.concurrency); 15 | } else { 16 | await queue.removeGlobalConcurrency(); 17 | } 18 | 19 | if ( 20 | data.max !== null && 21 | data.max !== undefined && 22 | data.duration !== null && 23 | data.duration !== undefined 24 | ) { 25 | await queue.setGlobalRateLimit(data.max, data.duration); 26 | } else { 27 | await queue.removeGlobalRateLimit(); 28 | } 29 | } 30 | } catch (err) { 31 | console.log('err', err); 32 | return res.status(500).json({error: err.message}); 33 | } 34 | return res.sendStatus(200); 35 | } 36 | 37 | module.exports = handler; 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const Arena = require('./src/server/app'); 4 | const routes = require('./src/server/views/routes'); 5 | 6 | function run(config, listenOpts = {}) { 7 | const {app, Queues} = Arena(config); 8 | 9 | Queues.useCdn = 10 | typeof listenOpts.useCdn !== 'undefined' ? listenOpts.useCdn : true; 11 | 12 | if (listenOpts.disableListen) { 13 | app.locals.appBasePath = 14 | listenOpts.basePath == '/' ? app.locals.appBasePath : listenOpts.basePath; 15 | app.use( 16 | listenOpts.basePath ? listenOpts.basePath : '/', 17 | express.static(path.join(__dirname, 'public')) 18 | ); 19 | app.use(listenOpts.basePath ? listenOpts.basePath : '/', routes); 20 | } else { 21 | const appBasePath = listenOpts.basePath || app.locals.appBasePath; 22 | app.use(appBasePath, express.static(path.join(__dirname, 'public'))); 23 | app.use(appBasePath, routes); 24 | const port = listenOpts.port || 4567; 25 | const host = listenOpts.host || '0.0.0.0'; // Default: listen to all network interfaces. 26 | app.listen(port, host, () => { 27 | console.log(`Arena is running on port ${port} at host ${host}`); 28 | }); 29 | } 30 | 31 | return app; 32 | } 33 | 34 | module.exports = run; 35 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const handlebars = require('handlebars'); 4 | const exphbs = require('express-handlebars'); 5 | 6 | module.exports = function (config) { 7 | const hbs = exphbs.create({ 8 | defaultLayout: `${__dirname}/views/layout`, 9 | handlebars, 10 | partialsDir: `${__dirname}/views/partials/`, 11 | extname: 'hbs', 12 | }); 13 | 14 | const app = express(); 15 | 16 | const defaultConfig = require('./config/index.json'); 17 | 18 | const Queues = require('./queue'); 19 | const Flows = require('./flow'); 20 | 21 | const queues = new Queues({...defaultConfig, ...config}); 22 | const flows = new Flows({...defaultConfig, ...config}); 23 | require('./views/helpers/handlebars')(handlebars, {queues}); 24 | app.locals.Queues = queues; 25 | app.locals.Flows = flows; 26 | app.locals.appBasePath = ''; 27 | app.locals.vendorPath = '/vendor'; 28 | app.locals.customCssPath = config.customCssPath; 29 | app.locals.customJsPath = config.customJsPath; 30 | 31 | app.set('views', `${__dirname}/views`); 32 | app.set('view engine', 'hbs'); 33 | app.set('json spaces', 2); 34 | 35 | app.engine('hbs', hbs.engine); 36 | 37 | app.use(bodyParser.json()); 38 | 39 | return { 40 | app, 41 | Queues: app.locals.Queues, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /example/bee.js: -------------------------------------------------------------------------------- 1 | const Arena = require('../'); 2 | const Bee = require('bee-queue'); 3 | 4 | // Select ports that are unlikely to be used by other services a developer might be running locally. 5 | const HTTP_SERVER_PORT = 4735; 6 | const REDIS_SERVER_PORT = 6379; 7 | 8 | async function main() { 9 | const queue = new Bee('name_of_my_queue', { 10 | activateDelayedJobs: true, 11 | redis: { 12 | port: REDIS_SERVER_PORT, 13 | }, 14 | }); 15 | 16 | // Fake process function to move newly created jobs in the UI through a few of the job states. 17 | queue.process(async function () { 18 | // Wait 5sec 19 | await new Promise((res) => setTimeout(res, 5000)); 20 | 21 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 22 | if (Math.random() > 0.5) { 23 | throw new Error('fake error'); 24 | } 25 | }); 26 | 27 | // adding delayed jobs 28 | await queue 29 | .createJob({}) 30 | .delayUntil(Date.now() + 60 * 1000) 31 | .save(); 32 | 33 | await queue.createJob({}).save(); 34 | 35 | Arena( 36 | { 37 | Bee, 38 | 39 | queues: [ 40 | { 41 | // Required for each queue definition. 42 | name: 'name_of_my_queue', 43 | 44 | // User-readable display name for the host. Required. 45 | hostId: 'Queue Server 1', 46 | 47 | // Queue type (Bull or Bee - default Bull). 48 | type: 'bee', 49 | 50 | redis: { 51 | // host: 'localhost', 52 | port: REDIS_SERVER_PORT, 53 | }, 54 | }, 55 | ], 56 | }, 57 | { 58 | port: HTTP_SERVER_PORT, 59 | } 60 | ); 61 | } 62 | 63 | main().catch((err) => { 64 | console.error(err); 65 | process.exit(1); 66 | }); 67 | -------------------------------------------------------------------------------- /src/server/views/dashboard/queueDetails.js: -------------------------------------------------------------------------------- 1 | const {rest} = require('lodash'); 2 | const QueueHelpers = require('../helpers/queueHelpers'); 3 | 4 | async function handler(req, res) { 5 | const {queueName, queueHost} = req.params; 6 | const {Queues, Flows} = req.app.locals; 7 | const queue = await Queues.get(queueName, queueHost); 8 | const basePath = req.baseUrl; 9 | 10 | if (!queue) 11 | return res.status(404).render('dashboard/templates/queueNotFound', { 12 | basePath, 13 | queueName, 14 | queueHost, 15 | hasFlows: Flows.hasFlows(), 16 | }); 17 | 18 | let jobCounts, hasRateLimitTtl, isPaused, globalConfig; 19 | if (queue.IS_BEE) { 20 | jobCounts = await queue.checkHealth(); 21 | delete jobCounts.newestJob; 22 | } else if (queue.IS_BULLMQ) { 23 | jobCounts = await queue.getJobCounts(...QueueHelpers.BULLMQ_STATES); 24 | if (queue.getMeta) { 25 | const meta = await queue.getMeta(); 26 | if (meta) { 27 | const {paused, ...restMeta} = meta; 28 | globalConfig = restMeta; 29 | } 30 | } 31 | } else { 32 | jobCounts = await queue.getJobCounts(); 33 | } 34 | 35 | if (queue.IS_BULLMQ) { 36 | const rateLimitTtl = await queue.getRateLimitTtl(); 37 | hasRateLimitTtl = rateLimitTtl > 0; 38 | } 39 | 40 | const stats = await QueueHelpers.getStats(queue); 41 | 42 | if (!queue.IS_BEE) { 43 | isPaused = await QueueHelpers.isPaused(queue); 44 | } 45 | 46 | return res.render('dashboard/templates/queueDetails', { 47 | basePath, 48 | hasRateLimitTtl, 49 | isPaused, 50 | queueName, 51 | queueHost, 52 | queueIsBee: !!queue.IS_BEE, 53 | queueIsBullMQ: !!queue.IS_BULLMQ, 54 | hasFlows: Flows.hasFlows(), 55 | globalConfig, 56 | jobCounts, 57 | stats, 58 | }); 59 | } 60 | 61 | module.exports = handler; 62 | -------------------------------------------------------------------------------- /src/server/views/api/bulkAction.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const ACTIONS = ['clean', 'remove', 'retry', 'promote']; 4 | 5 | function bulkAction(action) { 6 | return async function handler(req, res) { 7 | if (!_.includes(ACTIONS, action)) { 8 | res.status(401).send({ 9 | error: 'unauthorized action', 10 | details: `action ${action} not permitted`, 11 | }); 12 | } 13 | 14 | const {queueName, queueHost} = req.params; 15 | const {Queues} = req.app.locals; 16 | const queue = await Queues.get(queueName, queueHost); 17 | if (!queue) return res.status(404).send({error: 'queue not found'}); 18 | 19 | const {jobs, queueState} = req.body; 20 | 21 | try { 22 | if (!_.isEmpty(jobs) && jobs.length > 0) { 23 | const jobsPromises = jobs.map((id) => 24 | queue.getJob(decodeURIComponent(id)) 25 | ); 26 | const fetchedJobs = await Promise.all(jobsPromises); 27 | const actionPromises = 28 | action === 'retry' 29 | ? fetchedJobs.map((job) => { 30 | if ( 31 | queueState === 'failed' && 32 | typeof job.retry === 'function' 33 | ) { 34 | return job.retry(); 35 | } else { 36 | return Queues.set(queue, job.data, job.name); 37 | } 38 | }) 39 | : fetchedJobs.map((job) => job[action]()); 40 | await Promise.all(actionPromises); 41 | return res.sendStatus(200); 42 | } else if (action === 'clean') { 43 | if (queue.IS_BULLMQ) { 44 | await queue.clean(0, 1000, queueState); 45 | } else { 46 | await queue.clean(1000, queueState); 47 | } 48 | 49 | return res.sendStatus(200); 50 | } 51 | } catch (e) { 52 | const body = { 53 | error: 'queue error', 54 | details: e.stack, 55 | }; 56 | return res.status(500).send(body); 57 | } 58 | 59 | return res.sendStatus(200); 60 | }; 61 | } 62 | 63 | module.exports = bulkAction; 64 | -------------------------------------------------------------------------------- /src/server/views/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const addFlow = require('./addFlow'); 4 | const getFlow = require('./getFlow'); 5 | const jobAdd = require('./jobAdd'); 6 | const jobPromote = require('./jobPromote'); 7 | const jobRetry = require('./jobRetry'); 8 | const jobRemove = require('./jobRemove'); 9 | const jobDataUpdate = require('./jobDataUpdate'); 10 | const repeatableJobRemove = require('./repeatableJobRemove'); 11 | const bulkJobsClean = require('./bulkJobsClean'); 12 | const bulkJobsPromote = require('./bulkJobsPromote'); 13 | const bulkJobsRemove = require('./bulkJobsRemove'); 14 | const bulkJobsRetry = require('./bulkJobsRetry'); 15 | const queuePause = require('./queuePause'); 16 | const queueResume = require('./queueResume'); 17 | const queueUpdateMeta = require('./queueUpdateMeta'); 18 | const queueRemoveRateLimitKey = require('./queueRemoveRateLimitKey'); 19 | 20 | router.post('/queue/:queueHost/:queueName/job', jobAdd); 21 | router.post('/flow/:flowHost/:connectionName/flow', addFlow); 22 | router.get('/flow/:flowHost/:connectionName/flow', getFlow); 23 | router.post('/queue/:queueHost/:queueName/job/bulk', bulkJobsRemove); 24 | router.patch('/queue/:queueHost/:queueName/job/bulk', bulkJobsRetry); 25 | router.patch('/queue/:queueHost/:queueName/delayed/job/bulk', bulkJobsPromote); 26 | router.patch('/queue/:queueHost/:queueName/delayed/job/:id', jobPromote); 27 | router.delete( 28 | '/queue/:queueHost/:queueName/repeatable/job/:id', 29 | repeatableJobRemove 30 | ); 31 | router.put('/queue/:queueHost/:queueName/job/:id/data', jobDataUpdate); 32 | router.patch('/queue/:queueHost/:queueName/job/:id', jobRetry); 33 | router.put('/queue/:queueHost/:queueName/pause', queuePause); 34 | router.put('/queue/:queueHost/:queueName/resume', queueResume); 35 | router.put('/queue/:queueHost/:queueName/update-meta', queueUpdateMeta); 36 | router.delete('/queue/:queueHost/:queueName/job/:id', jobRemove); 37 | router.delete('/queue/:queueHost/:queueName/jobs/bulk', bulkJobsClean); 38 | router.delete( 39 | '/queue/:queueHost/:queueName/rate-limit-key', 40 | queueRemoveRateLimitKey 41 | ); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-arena", 3 | "description": "An interactive UI dashboard for Bee Queue", 4 | "main": "index.js", 5 | "author": "Mixmax ", 6 | "license": "MIT", 7 | "dependencies": { 8 | "body-parser": "^1.20.0", 9 | "express": "^4.19.2", 10 | "express-handlebars": "^5.1.0", 11 | "handlebars": "^4.7.7", 12 | "lodash": "^4.17.15", 13 | "moment": "^2.29.1", 14 | "tablesort": "^5.0.1" 15 | }, 16 | "devDependencies": { 17 | "@commitlint/cli": "^11.0.0", 18 | "@commitlint/config-conventional": "^11.0.0", 19 | "@semantic-release/changelog": "^5.0.1", 20 | "@semantic-release/commit-analyzer": "^8.0.1", 21 | "@semantic-release/exec": "^5.0.0", 22 | "@semantic-release/git": "^9.0.0", 23 | "@semantic-release/github": "^7.0.7", 24 | "@semantic-release/npm": "^7.0.5", 25 | "@semantic-release/release-notes-generator": "^9.0.1", 26 | "bull": "^3.16.0", 27 | "commitizen": "^4.2.5", 28 | "conventional-changelog-conventionalcommits": "^4.3.0", 29 | "eslint": "^7.23.0", 30 | "eslint-config-prettier": "^8.1.0", 31 | "eslint-plugin-prettier": "^3.3.1", 32 | "husky": "^6.0.0", 33 | "lint-staged": "^10.5.4", 34 | "prettier": "^2.0.5", 35 | "pretty-quick": "^3.1.0", 36 | "semantic-release": "^17.4.2" 37 | }, 38 | "scripts": { 39 | "ci": "npm run lint && if [ -z \"$CI\" ]; then npm run ci:commitlint; fi", 40 | "ci:commitlint": "commitlint --from \"origin/${GITHUB_BASE_REF:-master}\"", 41 | "cm": "git cz", 42 | "dc:up": "docker-compose -f docker-compose.yml up -d", 43 | "dc:down": "docker-compose -f docker-compose.yml down", 44 | "dry:run": "npm publish --dry-run", 45 | "lint": "prettier -c .", 46 | "lint:staged": "lint-staged", 47 | "prepare": "husky install", 48 | "pretty:quick": "pretty-quick --ignore-path ./.prettierignore --staged" 49 | }, 50 | "config": { 51 | "commitizen": { 52 | "path": "node_modules/cz-conventional-changelog" 53 | } 54 | }, 55 | "engines": { 56 | "node": ">=7.6.0" 57 | }, 58 | "files": [ 59 | "index.js", 60 | "public", 61 | "src" 62 | ], 63 | "keywords": [ 64 | "bull-arena", 65 | "bull", 66 | "bee", 67 | "bullmq" 68 | ], 69 | "repository": "https://github.com/bee-queue/arena.git", 70 | "version": "4.9.2" 71 | } 72 | -------------------------------------------------------------------------------- /example/bull.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const IORedis = require('ioredis'); 3 | const path = require('path'); 4 | const Arena = require('../'); 5 | const Bull = require('bull'); 6 | 7 | // Select ports that are unlikely to be used by other services a developer might be running locally. 8 | const HTTP_SERVER_PORT = 4735; 9 | const REDIS_SERVER_PORT = 6379; 10 | 11 | async function main() { 12 | const queueName1 = 'name_of_my_queue_1'; 13 | const connection = new IORedis({port: REDIS_SERVER_PORT}); 14 | 15 | const createClient = (type) => { 16 | switch (type) { 17 | case 'client': 18 | return connection; 19 | default: 20 | return new IORedis({port: REDIS_SERVER_PORT}); 21 | } 22 | }; 23 | 24 | const queue = new Bull(queueName1, { 25 | redis: { 26 | port: REDIS_SERVER_PORT, 27 | }, 28 | }); 29 | 30 | // Fake process function to move newly created jobs in the UI through a few of the job states. 31 | queue.process(async function (job) { 32 | // Wait 5sec 33 | job.progress(20); 34 | await new Promise((res) => setTimeout(res, 5000)); 35 | 36 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 37 | if (Math.random() > 0.5) { 38 | throw new Error('fake error'); 39 | } 40 | }); 41 | 42 | await queue.add({data: 'data'}); 43 | 44 | // adding delayed jobs 45 | const delayedJob = await queue.add({}, {delay: 60 * 1000}); 46 | delayedJob.log('Log message'); 47 | 48 | // add repeatable jobs 49 | await queue.add({}, {repeat: {cron: '15 * * * *'}}); 50 | 51 | const app = Arena( 52 | { 53 | Bull, 54 | 55 | queues: [ 56 | { 57 | // Required for each queue definition. 58 | name: queueName1, 59 | 60 | // User-readable display name for the host. Required. 61 | hostId: 'Queue Server 1', 62 | 63 | // Queue type (Bull or Bee - default Bull). 64 | type: 'bull', 65 | 66 | createClient, 67 | }, 68 | { 69 | // Required for each queue definition. 70 | name: 'name_of_my_queue_2', 71 | 72 | // User-readable display name for the host. Required. 73 | hostId: 'Queue Server 2', 74 | 75 | // Queue type (Bull or Bee - default Bull). 76 | type: 'bull', 77 | 78 | redis: { 79 | createClient, 80 | }, 81 | }, 82 | ], 83 | customJsPath: 'http://localhost:4735/example.js', 84 | }, 85 | { 86 | port: HTTP_SERVER_PORT, 87 | } 88 | ); 89 | 90 | app.use(express.static(path.join(__dirname, 'public'))); 91 | } 92 | 93 | main().catch((err) => { 94 | console.error(err); 95 | process.exit(1); 96 | }); 97 | -------------------------------------------------------------------------------- /public/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | /* 11 | * Global add-ons 12 | */ 13 | 14 | .sub-header { 15 | padding-bottom: 10px; 16 | border-bottom: 1px solid #eee; 17 | } 18 | 19 | /* 20 | * Top navigation 21 | * Hide default border to remove 1px line. 22 | */ 23 | .navbar-fixed-top { 24 | border: 0; 25 | } 26 | 27 | /* 28 | * Sidebar 29 | */ 30 | 31 | /* Hide for mobile, show later */ 32 | .sidebar { 33 | display: none; 34 | } 35 | @media (min-width: 768px) { 36 | .sidebar { 37 | position: fixed; 38 | top: 51px; 39 | bottom: 0; 40 | left: 0; 41 | z-index: 1000; 42 | display: block; 43 | padding: 20px; 44 | overflow-x: hidden; 45 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 46 | background-color: #f5f5f5; 47 | border-right: 1px solid #eee; 48 | } 49 | } 50 | 51 | /* Sidebar navigation */ 52 | .nav-sidebar { 53 | margin-right: -21px; /* 20px padding + 1px border */ 54 | margin-bottom: 20px; 55 | margin-left: -20px; 56 | } 57 | .nav-sidebar > li > a { 58 | padding-right: 20px; 59 | padding-left: 20px; 60 | } 61 | .nav-sidebar > .active > a, 62 | .nav-sidebar > .active > a:hover, 63 | .nav-sidebar > .active > a:focus { 64 | color: #fff; 65 | background-color: #428bca; 66 | } 67 | 68 | /* 69 | * Main content 70 | */ 71 | 72 | .main { 73 | padding: 20px; 74 | margin-top: 50px; 75 | } 76 | @media (min-width: 768px) { 77 | .main { 78 | padding-right: 40px; 79 | padding-left: 40px; 80 | } 81 | } 82 | .main .page-header { 83 | margin-top: 0; 84 | } 85 | 86 | /* 87 | * Placeholder dashboard ideas 88 | */ 89 | 90 | .placeholders { 91 | margin-bottom: 30px; 92 | text-align: center; 93 | } 94 | .placeholders h4 { 95 | margin-bottom: 0; 96 | } 97 | .placeholder { 98 | margin-bottom: 20px; 99 | } 100 | .placeholder img { 101 | display: inline-block; 102 | border-radius: 50%; 103 | } 104 | 105 | /* 106 | * Pagination 107 | */ 108 | .pagination-container { 109 | margin: 20px 0; 110 | } 111 | 112 | .pagination { 113 | margin: 0; 114 | } 115 | 116 | /* 117 | * Collapsing 118 | */ 119 | .header-collapse { 120 | display: inline; 121 | margin: 0 0 0 0.5em; 122 | } 123 | 124 | /* 125 | * Bulk job selection 126 | */ 127 | .bulk-job-container { 128 | display: inline-block; 129 | } 130 | 131 | .select-all-text { 132 | font-weight: 400; 133 | } 134 | 135 | pre { 136 | max-height: 250px; 137 | overflow-y: auto; 138 | } 139 | 140 | .overflow-hidden { 141 | overflow: hidden; 142 | } 143 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, and run the linter 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | # Any additional branches here will currently be treated as release (or maintenance) branches. 8 | # if the need to run jobs on other branches emerges, then the release job will need a better 9 | # condition expression. 10 | push: 11 | branches: [master, 2.x] 12 | pull_request: 13 | branches: [master, 2.x] 14 | 15 | jobs: 16 | lint: 17 | # https://github.community/t/github-actions-does-not-respect-skip-ci/17325/9 18 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | node-version: [10.x, 12.x, 14.x] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | with: 28 | # For commitlint; ideally this would only check out the feature branch's history, but 29 | # that's not currently an option. 30 | fetch-depth: ${{ github.event_name == 'push' }} 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - run: npm ci 36 | - run: npm run ci --if-present 37 | - run: npm run ci:commitlint 38 | if: "github.event_name != 'push' && github.actor != 'dependabot[bot]'" 39 | env: 40 | GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref }} 41 | 42 | release: 43 | # https://github.community/t/github-actions-does-not-respect-skip-ci/17325/9 44 | if: "github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip ci]')" 45 | runs-on: ubuntu-latest 46 | 47 | needs: [lint] 48 | 49 | steps: 50 | - uses: actions/checkout@v2 51 | with: 52 | # Necessary to prevent the checkout action from writing credentials to .git/config, which 53 | # semantic-release attempts to use to push despite those credentials being denied the 54 | # push. 55 | # See https://github.com/semantic-release/git/issues/196#issuecomment-601310576. 56 | persist-credentials: false 57 | - name: Use Node.js 14.x 58 | uses: actions/setup-node@v1 59 | with: 60 | node-version: 14.x 61 | - run: npm ci 62 | - name: Release 63 | run: npx semantic-release -b master,2.x 64 | env: 65 | # Need to use a separate token so we can push to the protected default branch. 66 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 67 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 68 | -------------------------------------------------------------------------------- /public/vendor/tablesort.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v5.1.0 (2018-09-14) 3 | * http://tristen.ca/tablesort/demo/ 4 | * Copyright (c) 2018 ; Licensed MIT 5 | */ 6 | !function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&l.push(k),m++;if(!l)return}for(m=0;m flow.type === 'bullmq' 48 | ); 49 | 50 | return hasBullMQ && this._config.BullMQ; 51 | } 52 | 53 | async get(connectionName, queueHost) { 54 | const flowConfig = _.find(this._config.flows, { 55 | name: connectionName, 56 | hostId: queueHost, 57 | }); 58 | if (!flowConfig) return null; 59 | 60 | if (this._flows[queueHost] && this._flows[queueHost][connectionName]) { 61 | return this._flows[queueHost][connectionName]; 62 | } 63 | 64 | const { 65 | type, 66 | port, 67 | host, 68 | db, 69 | password, 70 | prefix, 71 | url, 72 | redis, 73 | tls, 74 | } = flowConfig; 75 | 76 | const redisHost = {host}; 77 | if (password) redisHost.password = password; 78 | if (port) redisHost.port = port; 79 | if (db) redisHost.db = db; 80 | if (tls) redisHost.tls = tls; 81 | 82 | const isBullMQ = type === 'bullmq'; 83 | 84 | const options = { 85 | redis: redis || url || redisHost, 86 | }; 87 | if (prefix) options.prefix = prefix; 88 | 89 | let flow; 90 | if (isBullMQ) { 91 | if (flowConfig.createClient) 92 | options.createClient = flowConfig.createClient; 93 | 94 | const {FlowBullMQ} = this._config; 95 | const {redis, ...rest} = options; 96 | flow = new FlowBullMQ({ 97 | connection: redis, 98 | ...rest, 99 | }); 100 | flow.IS_BULLMQ = true; 101 | } 102 | 103 | this._flows[queueHost] = this._flows[queueHost] || {}; 104 | this._flows[queueHost][connectionName] = flow; 105 | 106 | return flow; 107 | } 108 | 109 | /** 110 | * Creates and adds jobs with the given data using the provided flow. 111 | * 112 | * @param {Object} flow A Bullmq flow class 113 | * @param {Object} data The data to be used within the flow 114 | */ 115 | async set(flow, data) { 116 | const args = [data]; 117 | 118 | return flow.add.apply(flow, args); 119 | } 120 | } 121 | 122 | module.exports = Flows; 123 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/flowDetails.hbs: -------------------------------------------------------------------------------- 1 |

    Flow {{ flowHost }}/{{ connectionName }}

    2 | 3 |
    4 |
    5 |
    6 |
    7 |
    Search Flow
    8 |
    Add Flow
    9 |

    Flow Panel

    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    Create
    17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 | 24 |
    25 |
    26 |
    27 | 28 |
    29 | 30 |
    31 |
    32 |
    33 | 34 |
    35 | 36 |
    37 |
    38 |
    39 | 40 |
    41 | 42 |
    43 |
    44 |
    45 |
    46 |
    Search
    47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |

    Redis Statistics

    56 |
    57 |
    58 | 59 | {{#each stats}} 60 | 61 | 62 | 63 | 64 | {{/each}} 65 |
    {{ @key }}{{ this }}
    66 |
    67 |
    68 |
    69 |
    70 | 71 |
    72 | {{~> dashboard/flowTree }} 73 |
    74 | 75 | {{#contentFor 'sidebar'}} 76 |
  • Queues Overview
  • 77 |
  • Flows Overview
  • 78 |
  • Flow {{ flowHost }}/{{ connectionName }}
  • 79 | {{/contentFor}} 80 | 81 | {{#contentFor 'script'}} 82 | window.jsonEditor = new JSONEditor(document.getElementById('jsoneditor'), { modes: ['code','tree','text'] }); 83 | window.arenaInitialPayload = { 84 | flowHost: "{{ flowHost }}", 85 | connectionName: "{{ connectionName }}" 86 | }; 87 | {{/contentFor}} -------------------------------------------------------------------------------- /example/express.js: -------------------------------------------------------------------------------- 1 | const Arena = require('../'); 2 | const express = require('express'); 3 | const {Queue, Worker, FlowProducer} = require('bullmq'); 4 | 5 | // Select ports that are unlikely to be used by other services a developer might be running locally. 6 | const HTTP_SERVER_PORT = 4735; 7 | const REDIS_SERVER_PORT = 6379; 8 | 9 | async function main() { 10 | const app = express(); 11 | const queueName = 'name_of_my_queue'; 12 | const parentQueueName = 'name_of_my_parent_queue'; 13 | 14 | const queue = new Queue(queueName, { 15 | connection: {port: REDIS_SERVER_PORT}, 16 | }); 17 | new Queue(parentQueueName, { 18 | connection: {port: REDIS_SERVER_PORT}, 19 | }); 20 | 21 | const flow = new FlowProducer({ 22 | connection: {port: REDIS_SERVER_PORT}, 23 | }); 24 | 25 | new Worker( 26 | queueName, 27 | async function (job) { 28 | await job.updateProgress(20); 29 | 30 | // Wait 5sec 31 | await new Promise((res) => setTimeout(res, 5000)); 32 | 33 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 34 | if (Math.random() > 0.5) { 35 | throw new Error('fake error'); 36 | } 37 | }, 38 | { 39 | concurrency: 3, 40 | connection: {port: REDIS_SERVER_PORT}, 41 | } 42 | ); 43 | 44 | new Worker( 45 | parentQueueName, 46 | async function () { 47 | // Wait 10sec 48 | await new Promise((res) => setTimeout(res, 10000)); 49 | 50 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 51 | if (Math.random() > 0.5) { 52 | throw new Error('fake error'); 53 | } 54 | }, 55 | { 56 | connection: {port: REDIS_SERVER_PORT}, 57 | } 58 | ); 59 | 60 | const children = Array.from(Array(65).keys()).map((index) => ({ 61 | name: 'child', 62 | data: {idx: index, foo: 'bar'}, 63 | queueName, 64 | })); 65 | await flow.add({ 66 | name: 'parent-job', 67 | queueName: parentQueueName, 68 | data: {}, 69 | children, 70 | }); 71 | 72 | // adding delayed jobs 73 | const delayedJob = await queue.add('delayed', {}, {delay: 60 * 1000}); 74 | delayedJob.log('Log message'); 75 | 76 | const arena = Arena( 77 | { 78 | BullMQ: Queue, 79 | 80 | queues: [ 81 | { 82 | // Required for each queue definition. 83 | name: queueName, 84 | 85 | // User-readable display name for the host. Required. 86 | hostId: 'Queue Server 1', 87 | 88 | // Queue type (Bull or Bullmq or Bee - default Bull). 89 | type: 'bullmq', 90 | 91 | redis: { 92 | // host: 'localhost', 93 | port: REDIS_SERVER_PORT, 94 | }, 95 | }, 96 | { 97 | // Required for each queue definition. 98 | name: parentQueueName, 99 | 100 | // User-readable display name for the host. Required. 101 | hostId: 'Queue Server 2', 102 | 103 | // Queue type (Bull or Bullmq or Bee - default Bull). 104 | type: 'bullmq', 105 | 106 | redis: { 107 | // host: 'localhost', 108 | port: REDIS_SERVER_PORT, 109 | }, 110 | }, 111 | ], 112 | }, 113 | { 114 | basePath: '/', 115 | disableListen: true, 116 | } 117 | ); 118 | 119 | app.use('/arena', arena); 120 | 121 | app.listen(HTTP_SERVER_PORT, () => 122 | console.log(`Arena listening on port ${HTTP_SERVER_PORT}!`) 123 | ); 124 | } 125 | 126 | main().catch((err) => { 127 | console.error(err); 128 | process.exit(1); 129 | }); 130 | -------------------------------------------------------------------------------- /example/bullmq.js: -------------------------------------------------------------------------------- 1 | const Arena = require('../'); 2 | const IORedis = require('ioredis'); 3 | const {Queue, Worker, FlowProducer} = require('bullmq'); 4 | 5 | // Select ports that are unlikely to be used by other services a developer might be running locally. 6 | const HTTP_SERVER_PORT = 4735; 7 | const REDIS_SERVER_PORT = 6379; 8 | 9 | async function main() { 10 | const queueName = 'name_of_my_queue'; 11 | const parentQueueName = 'name_of_my_parent_queue'; 12 | 13 | const connection = new IORedis({port: REDIS_SERVER_PORT}); 14 | 15 | const queue = new Queue(queueName, { 16 | connection: {port: REDIS_SERVER_PORT}, 17 | }); 18 | new Queue(parentQueueName, { 19 | connection: {port: REDIS_SERVER_PORT}, 20 | }); 21 | 22 | const flow = new FlowProducer({ 23 | connection: {port: REDIS_SERVER_PORT}, 24 | }); 25 | 26 | new Worker( 27 | queueName, 28 | async function (job) { 29 | await job.updateProgress(20); 30 | 31 | // Wait 5sec 32 | await new Promise((res) => setTimeout(res, 5000)); 33 | 34 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 35 | if (Math.random() > 0.5) { 36 | throw new Error('fake error'); 37 | } 38 | }, 39 | { 40 | concurrency: 10, 41 | connection: {port: REDIS_SERVER_PORT}, 42 | } 43 | ); 44 | 45 | new Worker( 46 | parentQueueName, 47 | async function () { 48 | // Wait 10sec 49 | await new Promise((res) => setTimeout(res, 10000)); 50 | 51 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 52 | if (Math.random() > 0.5) { 53 | throw new Error('fake error'); 54 | } 55 | }, 56 | { 57 | connection: {port: REDIS_SERVER_PORT}, 58 | } 59 | ); 60 | 61 | const children = Array.from(Array(65).keys()).map((index) => ({ 62 | name: 'child', 63 | data: {idx: index, foo: 'bar'}, 64 | queueName, 65 | })); 66 | await flow.add({ 67 | name: 'parent-job', 68 | queueName: parentQueueName, 69 | data: {}, 70 | children, 71 | }); 72 | 73 | // adding delayed jobs 74 | const delayedJob = await queue.add('delayed', {}, {delay: 60 * 1000}); 75 | await queue.add( 76 | 'delayed', 77 | {}, 78 | { 79 | delay: 1000, 80 | attempts: 4, 81 | backoff: { 82 | type: 'exponential', 83 | delay: 60000, 84 | }, 85 | } 86 | ); 87 | await queue.add('cron', {}, {repeat: {pattern: '* 1 * 1 *'}}); 88 | delayedJob.log('Log message'); 89 | 90 | Arena( 91 | { 92 | BullMQ: Queue, 93 | 94 | queues: [ 95 | { 96 | // Required for each queue definition. 97 | name: queueName, 98 | 99 | // User-readable display name for the host. Required. 100 | hostId: 'Queue Server 1', 101 | 102 | // Queue type (Bull or Bullmq or Bee - default Bull). 103 | type: 'bullmq', 104 | 105 | redis: connection, 106 | }, 107 | { 108 | // Required for each queue definition. 109 | name: parentQueueName, 110 | 111 | // User-readable display name for the host. Required. 112 | hostId: 'Queue Server 2', 113 | 114 | // Queue type (Bull or Bullmq or Bee - default Bull). 115 | type: 'bullmq', 116 | 117 | redis: { 118 | // host: 'localhost', 119 | port: REDIS_SERVER_PORT, 120 | }, 121 | }, 122 | ], 123 | }, 124 | { 125 | port: HTTP_SERVER_PORT, 126 | } 127 | ); 128 | } 129 | 130 | main().catch((err) => { 131 | console.error(err); 132 | process.exit(1); 133 | }); 134 | -------------------------------------------------------------------------------- /example/fastify.js: -------------------------------------------------------------------------------- 1 | const Arena = require('../'); 2 | const fastify = require('fastify'); 3 | const {Queue, Worker, FlowProducer} = require('bullmq'); 4 | 5 | // Select ports that are unlikely to be used by other services a developer might be running locally. 6 | const HTTP_SERVER_PORT = 4735; 7 | const REDIS_SERVER_PORT = 4736; 8 | 9 | async function main() { 10 | const app = fastify(); 11 | const queueName = 'name_of_my_queue'; 12 | const parentQueueName = 'name_of_my_parent_queue'; 13 | 14 | const queue = new Queue(queueName, { 15 | connection: {port: REDIS_SERVER_PORT}, 16 | }); 17 | new Queue(parentQueueName, { 18 | connection: {port: REDIS_SERVER_PORT}, 19 | }); 20 | 21 | const flow = new FlowProducer({ 22 | connection: {port: REDIS_SERVER_PORT}, 23 | }); 24 | 25 | new Worker( 26 | queueName, 27 | async function (job) { 28 | await job.updateProgress(20); 29 | 30 | // Wait 5sec 31 | await new Promise((res) => setTimeout(res, 5000)); 32 | 33 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 34 | if (Math.random() > 0.5) { 35 | throw new Error('fake error'); 36 | } 37 | }, 38 | { 39 | concurrency: 3, 40 | connection: {port: REDIS_SERVER_PORT}, 41 | } 42 | ); 43 | 44 | new Worker( 45 | parentQueueName, 46 | async function () { 47 | // Wait 10sec 48 | await new Promise((res) => setTimeout(res, 10000)); 49 | 50 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 51 | if (Math.random() > 0.5) { 52 | throw new Error('fake error'); 53 | } 54 | }, 55 | { 56 | connection: {port: REDIS_SERVER_PORT}, 57 | } 58 | ); 59 | 60 | const children = Array.from(Array(65).keys()).map((index) => ({ 61 | name: 'child', 62 | data: {idx: index, foo: 'bar'}, 63 | queueName, 64 | })); 65 | await flow.add({ 66 | name: 'parent-job', 67 | queueName: parentQueueName, 68 | data: {}, 69 | children, 70 | }); 71 | 72 | // adding delayed jobs 73 | const delayedJob = await queue.add('delayed', {}, {delay: 60 * 1000}); 74 | await delayedJob.log('Log message'); 75 | 76 | const arena = Arena( 77 | { 78 | BullMQ: Queue, 79 | 80 | queues: [ 81 | { 82 | // Required for each queue definition. 83 | name: queueName, 84 | 85 | // User-readable display name for the host. Required. 86 | hostId: 'Queue Server 1', 87 | 88 | // Queue type (Bull or Bullmq or Bee - default Bull). 89 | type: 'bullmq', 90 | 91 | redis: { 92 | // host: 'localhost', 93 | port: REDIS_SERVER_PORT, 94 | }, 95 | }, 96 | { 97 | // Required for each queue definition. 98 | name: parentQueueName, 99 | 100 | // User-readable display name for the host. Required. 101 | hostId: 'Queue Server 2', 102 | 103 | // Queue type (Bull or Bullmq or Bee - default Bull). 104 | type: 'bullmq', 105 | 106 | redis: { 107 | // host: 'localhost', 108 | port: REDIS_SERVER_PORT, 109 | }, 110 | }, 111 | ], 112 | }, 113 | { 114 | basePath: '/', 115 | disableListen: true, 116 | } 117 | ); 118 | 119 | await app.register(require('@fastify/express')); 120 | app.use('/arena', arena); 121 | 122 | app.listen({port: HTTP_SERVER_PORT}, (err, address) => { 123 | if (err) throw err; 124 | console.log(`Arena listening on port ${HTTP_SERVER_PORT}!`); 125 | }); 126 | } 127 | 128 | main().catch((err) => { 129 | console.error(err); 130 | process.exit(1); 131 | }); 132 | -------------------------------------------------------------------------------- /src/server/views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Arena 9 | 10 | {{#if (useCdn)}} 11 | 12 | 13 | 14 | 15 | 16 | {{else}} 17 | 18 | 19 | 20 | 21 | 22 | {{/if}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{#if customCssPath}} 32 | 33 | {{/if}} 34 | 35 | 36 | 37 | 51 | 52 |
    53 |
    54 | 60 | 61 |
    62 | {{{ body }}} 63 |
    64 |
    65 |
    66 | 67 | 68 | {{#if (useCdn)}} 69 | 71 | {{else}} 72 | 73 | 74 | {{/if}} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 84 | {{#if customJsPath}} 85 | 86 | {{/if}} 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/server/views/dashboard/jobDetails.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const JobHelpers = require('../helpers/jobHelpers'); 3 | 4 | async function handler(req, res) { 5 | const {queueName, queueHost, id} = req.params; 6 | const {json} = req.query; 7 | const basePath = req.baseUrl; 8 | 9 | const {Queues, Flows} = req.app.locals; 10 | const queue = await Queues.get(queueName, queueHost); 11 | if (!queue) 12 | return res.status(404).render('dashboard/templates/queueNotFound', { 13 | basePath, 14 | queueName, 15 | queueHost, 16 | }); 17 | 18 | const job = await queue.getJob(id); 19 | if (!job) 20 | return res.status(404).render('dashboard/templates/jobNotFound', { 21 | basePath, 22 | id, 23 | queueName, 24 | queueHost, 25 | hasFlows: Flows.hasFlows(), 26 | }); 27 | 28 | if (json === 'true') { 29 | // Omit these private and non-stringifyable properties to avoid circular 30 | // references parsing errors. 31 | return res.json( 32 | _.omit(job, 'domain', 'queue', 'scripts', '_events', '_eventsCount') 33 | ); 34 | } 35 | 36 | const jobState = queue.IS_BEE ? job.status : await job.getState(); 37 | job.showRetryButton = !queue.IS_BEE || jobState === 'failed'; 38 | job.retryButtonText = jobState === 'failed' ? 'Retry' : 'Trigger'; 39 | job.showPromoteButton = !queue.IS_BEE && jobState === 'delayed'; 40 | job.showDeleteRepeatableButton = queue.IS_BULL && job.opts.repeat; 41 | const stacktraces = queue.IS_BEE ? job.options.stacktraces : job.stacktrace; 42 | 43 | if (!queue.IS_BEE) { 44 | const logs = await queue.getJobLogs(job.id); 45 | job.logs = logs.logs || 'No Logs'; 46 | } 47 | 48 | if (queue.IS_BULLMQ) { 49 | job.parent = JobHelpers.getKeyProperties(job.parentKey); 50 | const processedCursor = parseInt(req.query.processedCursor, 10) || 0; 51 | const processedCount = parseInt(req.query.processedCount, 10) || 25; 52 | const ignoredCursor = parseInt(req.query.ignoredCursor, 10) || 0; 53 | const ignoredCount = parseInt(req.query.ignoredCount, 10) || 25; 54 | const unprocessedCursor = parseInt(req.query.unprocessedCursor, 10) || 0; 55 | const unprocessedCount = parseInt(req.query.unprocessedCount, 10) || 25; 56 | job.processedCount = processedCount; 57 | job.unprocessedCount = unprocessedCount; 58 | job.ignoredCount = ignoredCount; 59 | const { 60 | processed = {}, 61 | ignored = {}, 62 | unprocessed = [], 63 | nextProcessedCursor, 64 | nextIgnoredCursor, 65 | nextUnprocessedCursor, 66 | } = await job.getDependencies({ 67 | processed: { 68 | cursor: processedCursor, 69 | count: processedCount, 70 | }, 71 | ignored: { 72 | cursor: ignoredCursor, 73 | count: ignoredCount, 74 | }, 75 | unprocessed: { 76 | cursor: unprocessedCursor, 77 | count: unprocessedCount, 78 | }, 79 | }); 80 | const count = await job.getDependenciesCount(); 81 | job.countDependencies = count; 82 | 83 | job.processedCursor = nextProcessedCursor; 84 | job.ignoredCursor = nextIgnoredCursor; 85 | job.unprocessedCursor = nextUnprocessedCursor; 86 | if (unprocessed && unprocessed.length) { 87 | job.unprocessedChildren = unprocessed.map((child) => { 88 | return JobHelpers.getKeyProperties(child); 89 | }); 90 | } 91 | 92 | const processedKeys = Object.keys(processed); 93 | if (processedKeys.length) { 94 | job.processedChildren = processedKeys.map((child) => { 95 | return JobHelpers.getKeyProperties(child); 96 | }); 97 | } 98 | 99 | const ignoredKeys = Object.keys(ignored); 100 | if (ignoredKeys.length) { 101 | job.ignoredChildren = ignoredKeys.map((child) => { 102 | return JobHelpers.getKeyProperties(child); 103 | }); 104 | } 105 | } 106 | 107 | return res.render('dashboard/templates/jobDetails', { 108 | basePath, 109 | queueName, 110 | queueHost, 111 | jobState, 112 | job, 113 | stacktraces, 114 | hasFlows: Flows.hasFlows(), 115 | }); 116 | } 117 | 118 | module.exports = handler; 119 | -------------------------------------------------------------------------------- /src/server/views/helpers/handlebars.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const _ = require('lodash'); 3 | const Handlebars = require('handlebars'); 4 | const moment = require('moment'); 5 | 6 | const replacer = (key, value) => { 7 | if (_.isObject(value)) { 8 | return _.transform(value, (result, v, k) => { 9 | result[Handlebars.Utils.escapeExpression(k)] = v; 10 | }); 11 | } else if (_.isString(value)) { 12 | return Handlebars.Utils.escapeExpression(value); 13 | } else { 14 | return value; 15 | } 16 | }; 17 | 18 | // For jobs that don't have a valid ID, produce a random ID we can use in its place. 19 | const idMapping = new WeakMap(); 20 | 21 | const getTimestamp = (job) => { 22 | // Bull 23 | if (job.timestamp) { 24 | return job.timestamp; 25 | } 26 | 27 | // Bee 28 | if (job.options && job.options.timestamp) { 29 | return job.options.timestamp; 30 | } 31 | }; 32 | 33 | const helpers = { 34 | json(obj, pretty = false) { 35 | const args = [obj, replacer]; 36 | if (pretty) { 37 | args.push(2); 38 | } 39 | return new Handlebars.SafeString(JSON.stringify(...args)); 40 | }, 41 | 42 | isNumber(operand) { 43 | return parseInt(operand, 10).toString() === String(operand); 44 | }, 45 | 46 | adjustedPage(currentPage, pageSize, newPageSize) { 47 | const firstId = (currentPage - 1) * pageSize; 48 | return _.ceil(firstId / newPageSize) + 1; 49 | }, 50 | 51 | block(name) { 52 | const blocks = this._blocks; 53 | const content = blocks && blocks[name]; 54 | return content ? content.join('\n') : null; 55 | }, 56 | 57 | contentFor(name, options) { 58 | const blocks = this._blocks || (this._blocks = {}); 59 | const block = blocks[name] || (blocks[name] = []); 60 | block.push(options.fn(this)); 61 | }, 62 | 63 | hashIdAttr(obj) { 64 | const {id} = obj; 65 | if (typeof id === 'string') { 66 | return crypto.createHash('sha256').update(id).digest('hex'); 67 | } 68 | let mapping = idMapping.get(obj); 69 | if (!mapping) { 70 | mapping = crypto.randomBytes(32).toString('hex'); 71 | idMapping.set(obj, mapping); 72 | } 73 | return mapping; 74 | }, 75 | 76 | getDelayedExecutionAt(job) { 77 | // Bull 78 | if (job.delay) { 79 | if (job.processedOn) { 80 | return job.processedOn + job.delay; 81 | } else { 82 | return job.timestamp + job.delay; 83 | } 84 | } 85 | 86 | // Bee 87 | if (job.options && job.options.delay) { 88 | return job.options.delay; 89 | } 90 | }, 91 | 92 | getTimestamp, 93 | 94 | encodeURI(url) { 95 | if (typeof url !== 'string') { 96 | return ''; 97 | } 98 | return encodeURIComponent(url); 99 | }, 100 | 101 | capitalize(value) { 102 | if (typeof value !== 'string') { 103 | return ''; 104 | } 105 | return value.charAt(0).toUpperCase() + value.slice(1); 106 | }, 107 | 108 | add(a, b) { 109 | if (Handlebars.helpers.isNumber(a) && Handlebars.helpers.isNumber(b)) { 110 | return parseInt(a, 10) + parseInt(b, 10); 111 | } 112 | 113 | if (typeof a === 'string' && typeof b === 'string') { 114 | return a + b; 115 | } 116 | 117 | return ''; 118 | }, 119 | 120 | subtract(a, b) { 121 | if (!Handlebars.helpers.isNumber(a)) { 122 | throw new TypeError('expected the first argument to be a number'); 123 | } 124 | if (!Handlebars.helpers.isNumber(b)) { 125 | throw new TypeError('expected the second argument to be a number'); 126 | } 127 | return parseInt(a, 10) - parseInt(b, 10); 128 | }, 129 | 130 | length(value) { 131 | if (typeof value === 'string' || Array.isArray(value)) { 132 | return value.length; 133 | } 134 | return 0; 135 | }, 136 | 137 | moment(date, format) { 138 | return moment(date).format(format); 139 | }, 140 | 141 | eq(a, b, options) { 142 | return a === b ? options.fn(this) : options.inverse(this); 143 | }, 144 | }; 145 | 146 | module.exports = function registerHelpers(hbs, {queues}) { 147 | _.each(helpers, (fn, helper) => { 148 | hbs.registerHelper(helper, fn); 149 | }); 150 | 151 | hbs.registerHelper('useCdn', () => { 152 | return queues.useCdn; 153 | }); 154 | }; 155 | -------------------------------------------------------------------------------- /src/server/queue/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | class Queues { 4 | constructor(config) { 5 | this._queues = {}; 6 | 7 | this.useCdn = { 8 | value: true, 9 | get useCdn() { 10 | return this.value; 11 | }, 12 | set useCdn(newValue) { 13 | this.value = newValue; 14 | }, 15 | }; 16 | 17 | this.setConfig(config); 18 | } 19 | 20 | list() { 21 | return this._config.queues; 22 | } 23 | 24 | setConfig(config) { 25 | this._config = {...config, queues: config.queues.slice()}; 26 | 27 | if (!this._config.queues.length) { 28 | throw new Error('unsupported configuration: no queues configured'); 29 | } 30 | 31 | if (!this._checkConstructors()) { 32 | throw new TypeError( 33 | 'as of 3.0.0, bull-arena requires that the queue constructors be provided to Arena' 34 | ); 35 | } 36 | } 37 | 38 | _checkConstructors() { 39 | let hasBull = false, 40 | hasBee = false, 41 | hasBullMQ = false; 42 | for (const queue of this._config.queues) { 43 | if (queue.type === 'bee') hasBee = true; 44 | else if (queue.type === 'bullmq') hasBullMQ = true; 45 | else hasBull = true; 46 | 47 | if (hasBull && hasBee && hasBullMQ) break; 48 | } 49 | 50 | return ( 51 | (hasBull || hasBee || hasBullMQ) && 52 | (!hasBull || !!this._config.Bull) && 53 | (!hasBee || !!this._config.Bee) && 54 | (!hasBullMQ || !!this._config.BullMQ) 55 | ); 56 | } 57 | 58 | async get(queueName, queueHost) { 59 | const queueConfig = _.find(this._config.queues, { 60 | name: queueName, 61 | hostId: queueHost, 62 | }); 63 | if (!queueConfig) return null; 64 | 65 | if (this._queues[queueHost] && this._queues[queueHost][queueName]) { 66 | return this._queues[queueHost][queueName]; 67 | } 68 | 69 | const { 70 | type, 71 | name, 72 | port, 73 | host, 74 | db, 75 | password, 76 | prefix, 77 | url, 78 | redis, 79 | tls, 80 | } = queueConfig; 81 | 82 | const redisHost = {host}; 83 | if (password) redisHost.password = password; 84 | if (port) redisHost.port = port; 85 | if (db) redisHost.db = db; 86 | if (tls) redisHost.tls = tls; 87 | 88 | const isBee = type === 'bee'; 89 | const isBullMQ = type === 'bullmq'; 90 | 91 | const options = { 92 | redis: redis || url || redisHost, 93 | }; 94 | if (prefix) options.prefix = prefix; 95 | 96 | let queue; 97 | if (isBee) { 98 | _.extend(options, { 99 | isWorker: false, 100 | getEvents: false, 101 | sendEvents: false, 102 | storeJobs: false, 103 | }); 104 | 105 | const {Bee} = this._config; 106 | queue = new Bee(name, options); 107 | queue.IS_BEE = true; 108 | } else if (isBullMQ) { 109 | const {BullMQ} = this._config; 110 | const {redis, ...rest} = options; 111 | queue = new BullMQ(name, { 112 | connection: redis, 113 | ...rest, 114 | }); 115 | queue.IS_BULLMQ = true; 116 | } else { 117 | if (queueConfig.createClient) 118 | options.createClient = queueConfig.createClient; 119 | 120 | if (typeof options.redis === 'string') delete options.redis; 121 | 122 | const {Bull} = this._config; 123 | if (url) { 124 | queue = new Bull(name, url, options); 125 | } else { 126 | queue = new Bull(name, options); 127 | } 128 | queue.IS_BULL = true; 129 | } 130 | 131 | this._queues[queueHost] = this._queues[queueHost] || {}; 132 | this._queues[queueHost][queueName] = queue; 133 | 134 | return queue; 135 | } 136 | 137 | /** 138 | * Creates and adds a job with the given `data` to the given `queue`. 139 | * 140 | * @param {Object} queue A bee or bull queue class 141 | * @param {Object} data The data to be used within the job 142 | * @param {String} name The name of the Bull job (optional) 143 | * @param {Object} opts The opts to be used within the job 144 | */ 145 | async set(queue, data, name, opts) { 146 | if (queue.IS_BEE) { 147 | return queue.createJob(data).save(); 148 | } else { 149 | const args = [ 150 | data, 151 | { 152 | ...opts, 153 | removeOnComplete: false, 154 | removeOnFail: false, 155 | }, 156 | ]; 157 | 158 | if (name) args.unshift(name); 159 | return queue.add.apply(queue, args); 160 | } 161 | } 162 | } 163 | 164 | module.exports = Queues; 165 | -------------------------------------------------------------------------------- /example/bullmq_with_flows.js: -------------------------------------------------------------------------------- 1 | const Arena = require('../'); 2 | const {Queue, Worker, FlowProducer} = require('bullmq'); 3 | 4 | // Select ports that are unlikely to be used by other services a developer might be running locally. 5 | const HTTP_SERVER_PORT = 4735; 6 | const REDIS_SERVER_PORT = 6379; 7 | 8 | async function main() { 9 | const queueName = 'name_of_my_queue'; 10 | const parentQueueName = 'name_of_my_parent_queue'; 11 | 12 | const queue = new Queue(queueName, { 13 | connection: {port: REDIS_SERVER_PORT}, 14 | }); 15 | new Queue(parentQueueName, { 16 | connection: {port: REDIS_SERVER_PORT}, 17 | }); 18 | 19 | const flow = new FlowProducer({ 20 | connection: {port: REDIS_SERVER_PORT}, 21 | }); 22 | 23 | new Worker( 24 | queueName, 25 | async function () { 26 | // Wait 5sec 27 | await new Promise((res) => setTimeout(res, 5000)); 28 | 29 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 30 | if (Math.random() > 0.5) { 31 | throw new Error('fake error'); 32 | } 33 | }, 34 | { 35 | connection: {port: REDIS_SERVER_PORT}, 36 | } 37 | ); 38 | 39 | new Worker( 40 | parentQueueName, 41 | async function () { 42 | // Wait 10sec 43 | await new Promise((res) => setTimeout(res, 10000)); 44 | 45 | // Randomly succeeds or fails the job to put some jobs in completed and some in failed. 46 | if (Math.random() > 0.5) { 47 | throw new Error('fake error'); 48 | } 49 | }, 50 | { 51 | connection: {port: REDIS_SERVER_PORT}, 52 | } 53 | ); 54 | 55 | await flow.add({ 56 | name: 'parent-job', 57 | queueName: parentQueueName, 58 | data: {}, 59 | children: [ 60 | { 61 | name: 'child', 62 | data: {idx: 0, foo: 'bar'}, 63 | queueName, 64 | opts: { 65 | ignoreDependencyOnFailure: true, 66 | removeOnComplete: true, 67 | removeOnFail: true, 68 | }, 69 | }, 70 | { 71 | name: 'child', 72 | data: {idx: 1, foo: 'baz'}, 73 | queueName, 74 | opts: { 75 | failParentOnFailure: true, 76 | removeOnComplete: true, 77 | removeOnFail: true, 78 | }, 79 | }, 80 | {name: 'child', data: {idx: 2, foo: 'qux'}, queueName}, 81 | ], 82 | }); 83 | 84 | Arena( 85 | { 86 | BullMQ: Queue, 87 | 88 | FlowBullMQ: FlowProducer, 89 | 90 | queues: [ 91 | { 92 | // Required for each queue definition. 93 | name: queueName, 94 | 95 | // User-readable display name for the host. Required. 96 | hostId: 'Server 1', 97 | 98 | // Queue type (Bull or Bullmq or Bee - default Bull). 99 | type: 'bullmq', 100 | 101 | redis: { 102 | // host: 'localhost', 103 | port: REDIS_SERVER_PORT, 104 | }, 105 | }, 106 | { 107 | // Required for each queue definition. 108 | name: parentQueueName, 109 | 110 | // User-readable display name for the host. Required. 111 | hostId: 'Server 1', 112 | 113 | // Queue type (Bull or Bullmq or Bee - default Bull). 114 | type: 'bullmq', 115 | 116 | redis: { 117 | // host: 'localhost', 118 | port: REDIS_SERVER_PORT, 119 | }, 120 | }, 121 | ], 122 | 123 | flows: [ 124 | { 125 | // User-readable display name for the host. Required. 126 | hostId: 'Server 1', 127 | 128 | // Required for each flow definition. 129 | name: 'Connection name 1', 130 | 131 | // Queue type (Bull or Bullmq or Bee - default Bull). 132 | type: 'bullmq', 133 | 134 | redis: { 135 | // host: 'localhost', 136 | port: REDIS_SERVER_PORT, 137 | }, 138 | }, 139 | { 140 | // User-readable display name for the host. Required. 141 | hostId: 'Server 1', 142 | 143 | // Required for each flow definition. 144 | name: 'Connection name 2', 145 | 146 | // Queue type (Bull or Bullmq or Bee - default Bull). 147 | type: 'bullmq', 148 | 149 | redis: { 150 | // host: 'localhost', 151 | port: REDIS_SERVER_PORT, 152 | }, 153 | }, 154 | ], 155 | }, 156 | { 157 | port: HTTP_SERVER_PORT, 158 | } 159 | ); 160 | } 161 | 162 | main().catch((err) => { 163 | console.error(err); 164 | process.exit(1); 165 | }); 166 | -------------------------------------------------------------------------------- /src/server/views/dashboard/queueJobsByState.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { 3 | BEE_STATES, 4 | BULL_STATES, 5 | BULLMQ_STATES, 6 | } = require('../helpers/queueHelpers'); 7 | const JobHelpers = require('../helpers/jobHelpers'); 8 | 9 | function getStates(queue) { 10 | if (queue.IS_BEE) { 11 | return BEE_STATES; 12 | } 13 | if (queue.IS_BULLMQ) { 14 | return BULLMQ_STATES; 15 | } 16 | return BULL_STATES; 17 | } 18 | /** 19 | * Determines if the requested job state lookup is valid. 20 | * 21 | * @param {String} state 22 | * @param {Object} queue Queue that contains which queue package is used (bee, bull or bullmq) 23 | * 24 | * @return {Boolean} 25 | */ 26 | function isValidState(state, queue) { 27 | const validStates = getStates(queue); 28 | return _.includes(validStates, state); 29 | } 30 | 31 | async function handler(req, res) { 32 | if (req.params.ext === 'json') return _json(req, res); 33 | 34 | return _html(req, res); 35 | } 36 | 37 | /** 38 | * Returns the queue jobs in the requested state as a json document. 39 | * 40 | * @prop {Object} req express request object 41 | * @prop {Object} res express response object 42 | */ 43 | async function _json(req, res) { 44 | const {queueName, queueHost, state} = req.params; 45 | const {Queues} = req.app.locals; 46 | const queue = await Queues.get(queueName, queueHost); 47 | if (!queue) return res.status(404).json({message: 'Queue not found'}); 48 | 49 | if (!isValidState(state, queue)) 50 | return res.status(400).json({message: `Invalid state requested: ${state}`}); 51 | 52 | let jobs; 53 | if (queue.IS_BEE) { 54 | jobs = await queue.getJobs(state, {size: 1000}); 55 | jobs = jobs.map((j) => 56 | _.pick(j, 'id', 'progress', 'data', 'options', 'status') 57 | ); 58 | } else { 59 | const words = state.split('-'); 60 | const finalStateName = words.map((word) => _.capitalize(word)).join(''); 61 | jobs = await queue[`get${finalStateName}`](0, 1000); 62 | jobs = jobs.map((j) => j && j.toJSON()); 63 | } 64 | 65 | const filename = `${queueName}-${state}-dump.json`; 66 | 67 | res.setHeader('Content-disposition', `attachment; filename=${filename}`); 68 | res.setHeader('Content-type', 'application/json'); 69 | res.write(JSON.stringify(jobs, null, 2), () => res.end()); 70 | } 71 | 72 | /** 73 | * Renders an html view of the queue jobs in the requested state. 74 | * 75 | * @prop {Object} req express request object 76 | * @prop {Object} res express response object 77 | */ 78 | async function _html(req, res) { 79 | const {queueName, queueHost, state} = req.params; 80 | const {Queues, Flows} = req.app.locals; 81 | const queue = await Queues.get(queueName, queueHost); 82 | const basePath = req.baseUrl; 83 | 84 | if (!queue) 85 | return res.status(404).render('dashboard/templates/queueNotFound', { 86 | basePath, 87 | queueName, 88 | queueHost, 89 | }); 90 | 91 | if (!isValidState(state, queue)) 92 | return res.status(400).json({message: `Invalid state requested: ${state}`}); 93 | 94 | let jobCounts; 95 | if (queue.IS_BEE) { 96 | jobCounts = await queue.checkHealth(); 97 | delete jobCounts.newestJob; 98 | } else { 99 | jobCounts = await queue.getJobCounts(); 100 | } 101 | 102 | const page = parseInt(req.query.page, 10) || 1; 103 | const pageSize = parseInt(req.query.pageSize, 10) || 100; 104 | const order = req.query.order || 'desc'; 105 | 106 | const startId = (page - 1) * pageSize; 107 | const endId = startId + pageSize - 1; 108 | 109 | let jobs; 110 | if (queue.IS_BEE) { 111 | const pageOptions = {}; 112 | 113 | if (['failed', 'succeeded'].includes(state)) { 114 | pageOptions.size = pageSize; 115 | } else { 116 | pageOptions.start = startId; 117 | pageOptions.end = endId; 118 | } 119 | 120 | jobs = await queue.getJobs(state, pageOptions); 121 | 122 | // Filter out Bee jobs that have already been removed by the time the promise resolves 123 | jobs = jobs.filter((job) => job); 124 | } else { 125 | const stateTypes = state === 'waiting' ? ['wait', 'paused'] : state; 126 | jobs = await queue.getJobs(stateTypes, startId, endId, order === 'asc'); 127 | } 128 | 129 | for (let i = 0; i < jobs.length; i++) { 130 | if (!jobs[i]) { 131 | jobs[i] = { 132 | showRetryButton: false, 133 | showPromoteButton: false, 134 | showDeleteRepeatableButton: false, 135 | }; 136 | } else { 137 | const jobState = queue.IS_BEE ? jobs[i].status : await jobs[i].getState(); 138 | jobs[i].showRetryButton = !queue.IS_BEE || jobState === 'failed'; 139 | jobs[i].retryButtonText = jobState === 'failed' ? 'Retry' : 'Trigger'; 140 | jobs[i].showPromoteButton = !queue.IS_BEE && jobState === 'delayed'; 141 | jobs[i].showDeleteRepeatableButton = !queue.IS_BEE && jobs[i].opts.repeat; 142 | jobs[i].parent = JobHelpers.getKeyProperties(jobs[i].parentKey); 143 | } 144 | } 145 | 146 | let pages = _.range(page - 6, page + 7).filter((page) => page >= 1); 147 | while (pages.length < 12) { 148 | pages.push(_.last(pages) + 1); 149 | } 150 | pages = pages.filter((page) => page <= _.ceil(jobCounts[state] / pageSize)); 151 | const disablePromote = !(state === 'delayed' && !queue.IS_BEE); 152 | const disableRetry = !( 153 | state === 'failed' || 154 | (state === 'delayed' && queue.IS_BEE) 155 | ); 156 | const disableClean = !( 157 | state === 'failed' || 158 | state === 'completed' || 159 | !queue.IS_BULL 160 | ); 161 | 162 | return res.render('dashboard/templates/queueJobsByState', { 163 | basePath, 164 | queueName, 165 | queueHost, 166 | state, 167 | jobs, 168 | jobsInStateCount: jobCounts[state], 169 | disablePagination: 170 | queue.IS_BEE && (state === 'succeeded' || state === 'failed'), 171 | disableOrdering: queue.IS_BEE, 172 | disableClean, 173 | disablePromote, 174 | disableRetry, 175 | currentPage: page, 176 | hasFlows: Flows.hasFlows(), 177 | pages, 178 | pageSize, 179 | lastPage: _.last(pages), 180 | order, 181 | }); 182 | } 183 | 184 | module.exports = handler; 185 | -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/queueDetails.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 | {{#unless queueIsBee}} 4 | {{#if isPaused}} 5 | 9 | {{else}} 10 | 14 | {{/if}} 15 | {{/unless}} 16 | 17 | {{#if queueIsBullMQ}} 18 | {{#if hasRateLimitTtl}} 19 | 23 | {{/if}} 24 | {{/if}} 25 | 26 |
    27 |
    28 |
    29 |
    30 |
    Add Job
    31 |

    Job Types

    32 |
    33 |
    34 |
    35 | {{#unless queueIsBee}} 36 |
    37 | 41 |
    42 |
    43 |
    44 | {{/unless}} 45 |
    46 | {{#unless queueIsBee}} 47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 | {{/unless}} 55 |
    56 |
    57 | {{#unless queueIsBee}} 58 | 65 | {{/unless}} 66 |
    Create
    67 |
    68 |
    69 | 79 |
    80 |
    81 |
    82 |
    83 |
    84 |
    85 |

    Redis Statistics

    86 |
    87 |
    88 | 89 | {{#each stats}} 90 | 91 | 92 | 93 | 94 | {{/each}} 95 |
    {{ @key }}{{ this }}
    96 |
    97 |
    98 | {{#if globalConfig}} 99 |
    100 |
    101 |
    Update
    102 |

    Global Configuration

    103 |
    104 |
    105 |
    106 |
    107 | 108 |
    109 | 115 |
    116 |
    117 |
    118 | 119 |
    120 | 126 |
    127 |
    128 |
    129 | 130 |
    131 | 137 |
    138 |
    139 |
    140 | Update 141 |
    142 |
    143 | 144 | {{#each globalConfig}} 145 | 146 | 147 | 148 | 149 | {{/each}} 150 |
    {{ @key }}{{ this }}
    151 |
    152 |
    153 | {{/if}} 154 |
    155 |
    156 | 157 | {{#contentFor 'sidebar'}} 158 |
  • Queues Overview
  • 159 |
  • Queue {{ queueHost }}/{{ queueName }}
  • 160 | {{#if hasFlows}} 161 |
  • Flows Overview
  • 162 | {{/if}} 163 | {{/contentFor}} 164 | 165 | {{#contentFor 'script'}} 166 | window.jsonEditor = new JSONEditor(document.getElementById('jsoneditor'), { modes: ['code','tree','text'] }); 167 | if(document.getElementById('jsoneditoropts')) window.jsonEditorOpts = new JSONEditor(document.getElementById('jsoneditoropts'), { modes: ['code','tree','text'] }); 168 | window.arenaInitialPayload = { 169 | queueHost: "{{ queueHost }}", 170 | queueName: "{{ queueName }}" 171 | }; 172 | {{/contentFor}} -------------------------------------------------------------------------------- /src/server/views/dashboard/templates/queueJobsByState.hbs: -------------------------------------------------------------------------------- 1 |

    Queue {{ queueHost }}/{{ queueName }}

    2 | 3 |

    {{capitalize state}} Jobs

    4 | 5 |
    6 |
    7 | {{#unless disablePagination}} 8 | 39 | {{else}} 40 | Bee-queue does not support pagination for {{ state }} queues — currently displaying up to {{ pageSize }} 41 | jobs. To change count, use "Size" dropdown. 42 | {{/unless}} 43 |
    44 |
    45 | 47 | Dump jobs to JSON (limited to 1000) 48 | 49 | 53 | {{#unless disableClean}} 54 | 58 | {{/unless}} 59 | {{#unless disableRetry}} 60 | 64 | {{/unless}} 65 | {{#unless disablePromote}} 66 | 70 | {{/unless}} 71 | 72 |
    73 |
    74 | 105 |
    106 | {{#unless disableOrdering}} 107 |
    108 | 126 |
    127 | {{/unless}} 128 |
    129 |
    130 |
    131 | 132 |
    133 |
    134 |
    135 | 173 |
    174 |
    175 |
    176 | 177 |

    178 | Hint: + Click to select a range of 179 | jobs. 180 |

    181 | 182 | {{#contentFor 'sidebar'}} 183 |
  • Queues Overview
  • 184 |
  • Queue 185 | {{ queueHost }}/{{ queueName }}
  • 186 |
  • {{capitalize state}} Jobs
  • 187 | {{#if hasFlows}} 188 |
  • Flows Overview
  • 189 | {{/if}} 190 | {{/contentFor}} -------------------------------------------------------------------------------- /src/server/views/partials/dashboard/jobDetails.hbs: -------------------------------------------------------------------------------- 1 | {{#unless displayJobInline}} 2 |

    {{ this.id }}

    3 | 4 | {{#if this.name}} 5 |
    Name
    6 | {{this.name}} 7 | {{/if}} 8 | {{/unless}} 9 | 10 |
    Actions
    11 | 15 | 16 | {{#if showRetryButton}} 17 | 21 | {{/if}} 22 | 23 | {{#if showPromoteButton}} 24 | 28 | {{/if}} 29 | 30 | {{#if showDeleteRepeatableButton}} 31 | 35 | {{/if}} 36 | 37 |
    38 |
    39 |
    State
    40 | {{capitalize jobState}} 41 |
    42 | 43 |
    44 |
    Timestamp
    45 | {{#if queue.IS_BEE}} 46 | {{#if this.options}} 47 | {{#if (isNumber this.options.timestamp)}} 48 | {{moment this.options.timestamp "llll"}} 49 | {{/if}} 50 | {{/if}} 51 | {{else}} 52 | {{#if (isNumber this.timestamp)}} 53 | {{moment this.timestamp "llll"}} 54 | {{/if}} 55 | {{/if}} 56 |
    57 | 58 | {{#if this.processedOn}} 59 |
    60 |
    Processed
    61 | {{moment this.processedOn "llll"}} 62 |
    63 | {{/if}} 64 | 65 | {{#if this.finishedOn}} 66 |
    67 |
    Finished
    68 | {{moment this.finishedOn "llll"}} 69 |
    70 | {{/if}} 71 | 72 | {{#eq jobState 'delayed'}} 73 |
    74 |
    Executes At
    75 | {{moment (getDelayedExecutionAt this) "llll"}} 76 |
    77 | {{/eq}} 78 | 79 |
    80 |
    Attempts Made
    81 | {{this.attemptsMade}} 82 | 83 | {{#if this.options}} 84 | {{length this.options.stacktraces}} 85 | {{/if}} 86 |
    87 | 88 | {{#if (isNumber this.attemptsStarted)}} 89 |
    90 |
    Attempts Started
    91 | {{this.attemptsStarted}} 92 |
    93 | {{/if}} 94 |
    95 |
    96 |
    97 |
    Permalinks
    98 | {{#unless view}} 99 | Job 100 | {{ this.id }} 101 | {{/unless}} 102 | JSON 104 |
    105 |
    106 | 107 | {{#if this.queue.IS_BULL}} 108 |
    Progress
    109 | {{#if (isNumber this._progress)}} 110 |
    111 |
    116 | {{ this._progress }}% 117 |
    118 |
    119 | {{else}} 120 |
    {{json this._progress true}}
    121 | {{/if}} 122 | {{else if this.queue.IS_BULLMQ}} 123 |
    Progress
    124 | {{#if (isNumber this.progress)}} 125 |
    126 |
    131 | {{ this.progress }}% 132 |
    133 |
    134 | {{else}} 135 |
    {{json this.progress true}}
    136 | {{/if}} 137 | {{/if}} 138 | 139 | {{#if this.returnvalue}} 140 |
    Return Value
    141 |
    {{json this.returnvalue true}}
    142 | {{/if}} 143 | 144 | {{#if this.failedReason}} 145 |
    Reason for failure
    146 |
    {{this.failedReason}}
    147 | {{/if}} 148 | 149 | {{#if stacktraces}} 150 |
    Stacktraces
    151 | {{#each stacktraces}} 152 |
    {{ this }}
    153 | {{/each}} 154 | {{/if}} 155 | 156 | 157 |
    Data
    158 | 159 | {{#unless queue.IS_BEE}} 160 | {{#if view }} 161 |
    162 | 166 |
    167 |
    168 |
    169 | {{/if}} 170 | {{/unless}} 171 |
    {{json this.data true}}
    172 | {{#unless queue.IS_BEE}} 173 | {{#if view }} 174 |
    175 |
    176 |
    177 |
    178 |
    179 |
    Update
    181 |
    182 |
    183 |
    184 |
    185 |
    186 | {{/if}} 187 | {{/unless}} 188 | 189 | {{#if this.queue.IS_BULLMQ}} 190 | {{#if this.parent }} 191 |
    192 |
    193 |
    Parent
    194 | 195 | {{ this.parent.id }} 196 | 197 |
    198 |
    199 | {{/if}} 200 | 201 | {{#if this.unprocessedChildren }} 202 |
    203 |
    204 |
    Unprocessed Children {{ this.countDependencies.unprocessed}}
    205 | 206 | 216 | 217 | {{#each this.unprocessedChildren}} 218 | 219 | {{ this.id }} 220 | 221 | {{/each}} 222 |
    223 |
    224 | {{/if}} 225 | 226 | {{#if this.processedChildren }} 227 |
    228 |
    229 |
    Processed Children {{ this.countDependencies.processed}}
    230 | 231 | 241 | 242 | {{#each this.processedChildren}} 243 | 244 | {{ this.id }} 245 | 246 | {{/each}} 247 |
    248 |
    249 | {{/if}} 250 | 251 | {{#if this.ignoredChildren }} 252 |
    253 |
    254 |
    Ignored Children {{ this.countDependencies.ignored}}
    255 | 256 | 266 | 267 | {{#each this.ignoredChildren}} 268 | 269 | {{ this.id }} 270 | 271 | {{/each}} 272 |
    273 |
    274 | {{/if}} 275 |
    276 | {{/if}} 277 | 278 | {{#if this.logs}} 279 |
    Logs
    280 |
    {{json this.logs true}}
    281 | {{/if}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arena 2 | 3 | [![NPM](https://img.shields.io/npm/v/bull-arena.svg)](https://www.npmjs.com/package/bull-arena) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![NPM downloads](https://img.shields.io/npm/dm/bull-arena)](https://www.npmjs.com/package/bull-arena) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 4 | 5 | An intuitive Web GUI for [Bee Queue](https://github.com/bee-queue/bee-queue), [Bull](https://github.com/optimalbits/bull) and [BullMQ](https://github.com/taskforcesh/bullmq). Built on Express so you can run Arena standalone, or mounted in another app as middleware. 6 | 7 | For a quick introduction to the motivations for creating Arena, read _[Interactively monitoring Bull, a Redis-backed job queue for Node](https://www.mixmax.com/blog/introducing-bull-arena)_. 8 | 9 | ### Screenshots 10 | 11 | [![](screenshots/screen1_sm.png)](screenshots/screen1.png) [![](screenshots/screen2_sm.png)](screenshots/screen2.png) [![](screenshots/screen3_sm.png)](screenshots/screen3.png) 12 | 13 | ### Features 14 | 15 | - Check the health of a queue and its jobs at a glance 16 | - Paginate and filter jobs by their state 17 | - View details and stacktraces of jobs with permalinks 18 | - Restart and retry jobs with one click 19 | 20 | ### Usage 21 | 22 | Arena accepts the following options: 23 | 24 | ```js 25 | const Arena = require('bull-arena'); 26 | 27 | // Mandatory import of queue library. 28 | const Bee = require('bee-queue'); 29 | 30 | Arena({ 31 | // All queue libraries used must be explicitly imported and included. 32 | Bee, 33 | 34 | // Provide a `Bull` option when using bull, similar to the `Bee` option above. 35 | 36 | queues: [ 37 | { 38 | // Required for each queue definition. 39 | name: 'name_of_my_queue', 40 | 41 | // User-readable display name for the host. Required. 42 | hostId: 'Queue Server 1', 43 | 44 | // Queue type (Bull or Bee - default Bull). 45 | type: 'bee', 46 | 47 | // Queue key prefix. Defaults to "bq" for Bee and "bull" for Bull. 48 | prefix: 'foo', 49 | }, 50 | ], 51 | 52 | // Optionally include your own stylesheet 53 | customCssPath: 'https://example.com/custom-arena-styles.css', 54 | 55 | // Optionally include your own script 56 | customJsPath: 'https://example.com/custom-arena-js.js', 57 | }); 58 | ``` 59 | 60 | The required `name` and `hostId` in each queue object have to be present in each queue object. Additional keys can be present in them, to configure the redis client itself. 61 | 62 | The three ways in which you can configure the client are: 63 | 64 | #### 1. port/host 65 | 66 | ```jsonc 67 | // In a queue object. 68 | { 69 | // Hostname or IP. Required. 70 | "host": "127.0.0.1", 71 | 72 | // Bound port. Optional, default: 6379. 73 | "port": 6379, 74 | 75 | // Optional, to issue a redis AUTH command. 76 | "password": "hello", 77 | 78 | // Optional; default 0. Most of the time, you'll leave this absent. 79 | "db": 1 80 | } 81 | ``` 82 | 83 | #### 2. URL 84 | 85 | You can also provide a `url` field instead of `host`, `port`, `db` and `password`. 86 | 87 | ```js 88 | { 89 | "url": "[redis:]//[[user][:password@]][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]" 90 | } 91 | ``` 92 | 93 | #### 3. Redis client options 94 | 95 | Arena is compatible with both Bee and Bull. 96 | If you need to pass some specific configuration options directly to the redis client library your queue uses, you can also do so. 97 | 98 | Bee uses node [redis](https://www.npmjs.com/package/redis) client, Bull uses [ioredis](https://www.npmjs.com/package/ioredis) client. 99 | These clients expect different configurations options. 100 | 101 | ```js 102 | { 103 | "redis": {} 104 | } 105 | ``` 106 | 107 | For Bee, the `redis` key will be directly passed to [`redis.createClient`](https://github.com/NodeRedis/node_redis#rediscreateclient), as explained [here](https://github.com/bee-queue/bee-queue#settings). 108 | 109 | For Bull, the `redis` key will be directly passed to [`ioredis`](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new), as explained [here](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queue). To use this to connect to a Sentinel cluster, see [here](https://github.com/luin/ioredis/blob/master/README.md#sentinel). 110 | 111 | #### Custom configuration file 112 | 113 | To specify a custom configuration file location, see [Running Arena as a node module](#running-arena-as-a-node-module). 114 | 115 | _Note that if you happen to use Amazon Web Services' ElastiCache as your Redis host, check out http://mixmax.com/blog/bull-queue-aws-autodiscovery_ 116 | 117 | #### Running Arena as a node module 118 | 119 | See the [Docker image](#docker-image) section or the [docker-arena] repository for information about running this standalone. 120 | 121 | Note that because Arena is implemented using `async`/`await`, Arena only currently supports Node `>=7.6`. 122 | 123 | Using Arena as a node module has potential benefits: 124 | 125 | - Arena can be configured to use any method of server/queue configuration desired 126 | - for example, fetching available redis queues from an AWS instance on server start 127 | - or even just plain old reading from environment variables 128 | - Arena can be mounted in other express apps as middleware 129 | 130 | Usage: 131 | 132 | In project folder: 133 | 134 | ```shell 135 | $ npm install bull-arena 136 | ``` 137 | 138 | In router.js: 139 | 140 | ```js 141 | const Arena = require('bull-arena'); 142 | 143 | const express = require('express'); 144 | const router = express.Router(); 145 | 146 | const arena = Arena({ 147 | // Include a reference to the bee-queue or bull libraries, depending on the library being used. 148 | 149 | queues: [ 150 | { 151 | // First queue configuration 152 | }, 153 | { 154 | // Second queue configuration 155 | }, 156 | { 157 | // And so on... 158 | }, 159 | ], 160 | }); 161 | 162 | router.use('/', arena); 163 | ``` 164 | 165 | `Arena` takes two arguments. The first, `config`, is a plain object containing the [queue configuration, flow configuration (just for bullmq for now) and other optional parameters](#usage). The second, `listenOpts`, is an object that can contain the following optional parameters: 166 | 167 | - `port` - specify custom port to listen on (default: 4567) 168 | - `host` - specify custom ip to listen on (default: '0.0.0.0') 169 | - `basePath` - specify custom path to mount server on (default: '/') 170 | - `disableListen` - don't let the server listen (useful when mounting Arena as a sub-app of another Express app) (default: false) 171 | - `useCdn` - set false to use the bundled js and css files (default: true) 172 | - `customCssPath` - an URL to an external stylesheet (default: null) 173 | 174 | ##### Example config (for bull) 175 | 176 | ```js 177 | import Arena from 'bull-arena'; 178 | import Bull from 'bull'; 179 | 180 | const arenaConfig = Arena({ 181 | Bull, 182 | queues: [ 183 | { 184 | type: 'bull', 185 | 186 | // Name of the bull queue, this name must match up exactly with what you've defined in bull. 187 | name: "Notification_Emailer", 188 | 189 | // Hostname or queue prefix, you can put whatever you want. 190 | hostId: "MyAwesomeQueues", 191 | 192 | // Redis auth. 193 | redis: { 194 | port: /* Your redis port */, 195 | host: /* Your redis host domain*/, 196 | password: /* Your redis password */, 197 | }, 198 | }, 199 | ], 200 | 201 | // Optionally include your own stylesheet 202 | customCssPath: 'https://example.com/custom-arena-styles.css', 203 | 204 | // Optionally include your own script 205 | customJsPath: 'https://example.com/custom-arena-js.js', 206 | }, 207 | { 208 | // Make the arena dashboard become available at {my-site.com}/arena. 209 | basePath: '/arena', 210 | 211 | // Let express handle the listening. 212 | disableListen: true, 213 | }); 214 | 215 | // Make arena's resources (js/css deps) available at the base app route 216 | app.use('/', arenaConfig); 217 | ``` 218 | 219 | (Credit to [tim-soft](https://github.com/tim-soft) for the example config.) 220 | 221 | ##### Example config (for bullmq) 222 | 223 | ```js 224 | import Arena from 'bull-arena'; 225 | import { Queue, FlowProducer } from "bullmq"; 226 | 227 | const arenaConfig = Arena({ 228 | BullMQ: Queue, 229 | FlowBullMQ: FlowProducer, 230 | queues: [ 231 | { 232 | type: 'bullmq', 233 | 234 | // Name of the bullmq queue, this name must match up exactly with what you've defined in bullmq. 235 | name: "testQueue", 236 | 237 | // Hostname or queue prefix, you can put whatever you want. 238 | hostId: "worker", 239 | 240 | // Redis auth. 241 | redis: { 242 | port: /* Your redis port */, 243 | host: /* Your redis host domain*/, 244 | password: /* Your redis password */, 245 | }, 246 | }, 247 | ], 248 | 249 | flows: [ 250 | { 251 | type: 'bullmq', 252 | 253 | // Name of the bullmq flow connection, this name helps to identify different connections. 254 | name: "testConnection", 255 | 256 | // Hostname, you can put whatever you want. 257 | hostId: "Flow", 258 | 259 | // Redis auth. 260 | redis: { 261 | port: /* Your redis port */, 262 | host: /* Your redis host domain*/, 263 | password: /* Your redis password */, 264 | }, 265 | }, 266 | ], 267 | 268 | // Optionally include your own stylesheet 269 | customCssPath: 'https://example.com/custom-arena-styles.css', 270 | 271 | // Optionally include your own script 272 | customJsPath: 'https://example.com/custom-arena-js.js', 273 | }, 274 | { 275 | // Make the arena dashboard become available at {my-site.com}/arena. 276 | basePath: '/arena', 277 | 278 | // Let express handle the listening. 279 | disableListen: true, 280 | }); 281 | 282 | // Make arena's resources (js/css deps) available at the base app route 283 | app.use('/', arenaConfig); 284 | ``` 285 | 286 | ### Bee Queue support 287 | 288 | Arena is dual-compatible with Bull 3.x and Bee-Queue 1.x. To add a Bee queue to the Arena dashboard, include the `type: 'bee'` property with an individual queue's configuration object. 289 | 290 | ### BullMQ Queue support 291 | 292 | Arena has added preliminary support for BullMQ post 3.4.x version. To add a BullMQ queue to the Arena dashboard, include the `type: 'bullmq'` property with an individual queue's configuration object. 293 | 294 | ### Docker image 295 | 296 | You can `docker pull` Arena from [Docker Hub](https://hub.docker.com/r/mixmaxhq/arena/). 297 | 298 | Please see the [docker-arena] repository for details. 299 | 300 | ### Official UIs 301 | 302 | - [Taskforce](https://taskforce.sh) for Bull and Bullmq 303 | 304 | ### Contributing 305 | 306 | See [contributing guidelines](CONTRIBUTING.md) and [an example](example/README.md). 307 | 308 | ### License 309 | 310 | The [MIT License](LICENSE). 311 | 312 | [docker-arena]: https://github.com/bee-queue/docker-arena 313 | -------------------------------------------------------------------------------- /public/vendor/bootstrap-treeview.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b,c,d){"use strict";var e="treeview",f={};f.settings={injectStyle:!0,levels:2,expandIcon:"glyphicon glyphicon-plus",collapseIcon:"glyphicon glyphicon-minus",emptyIcon:"glyphicon",nodeIcon:"",selectedIcon:"",checkedIcon:"glyphicon glyphicon-check",uncheckedIcon:"glyphicon glyphicon-unchecked",color:d,backColor:d,borderColor:d,onhoverColor:"#F5F5F5",selectedColor:"#FFFFFF",selectedBackColor:"#428bca",searchResultColor:"#D9534F",searchResultBackColor:d,enableLinks:!1,highlightSelected:!0,highlightSearchResults:!0,showBorder:!0,showIcon:!0,showCheckbox:!1,showTags:!1,multiSelect:!1,onNodeChecked:d,onNodeCollapsed:d,onNodeDisabled:d,onNodeEnabled:d,onNodeExpanded:d,onNodeSelected:d,onNodeUnchecked:d,onNodeUnselected:d,onSearchComplete:d,onSearchCleared:d},f.options={silent:!1,ignoreChildren:!1},f.searchOptions={ignoreCase:!0,exactMatch:!1,revealResults:!0};var g=function(b,c){return this.$element=a(b),this.elementId=b.id,this.styleId=this.elementId+"-style",this.init(c),{options:this.options,init:a.proxy(this.init,this),remove:a.proxy(this.remove,this),getNode:a.proxy(this.getNode,this),getParent:a.proxy(this.getParent,this),getSiblings:a.proxy(this.getSiblings,this),getSelected:a.proxy(this.getSelected,this),getUnselected:a.proxy(this.getUnselected,this),getExpanded:a.proxy(this.getExpanded,this),getCollapsed:a.proxy(this.getCollapsed,this),getChecked:a.proxy(this.getChecked,this),getUnchecked:a.proxy(this.getUnchecked,this),getDisabled:a.proxy(this.getDisabled,this),getEnabled:a.proxy(this.getEnabled,this),selectNode:a.proxy(this.selectNode,this),unselectNode:a.proxy(this.unselectNode,this),toggleNodeSelected:a.proxy(this.toggleNodeSelected,this),collapseAll:a.proxy(this.collapseAll,this),collapseNode:a.proxy(this.collapseNode,this),expandAll:a.proxy(this.expandAll,this),expandNode:a.proxy(this.expandNode,this),toggleNodeExpanded:a.proxy(this.toggleNodeExpanded,this),revealNode:a.proxy(this.revealNode,this),checkAll:a.proxy(this.checkAll,this),checkNode:a.proxy(this.checkNode,this),uncheckAll:a.proxy(this.uncheckAll,this),uncheckNode:a.proxy(this.uncheckNode,this),toggleNodeChecked:a.proxy(this.toggleNodeChecked,this),disableAll:a.proxy(this.disableAll,this),disableNode:a.proxy(this.disableNode,this),enableAll:a.proxy(this.enableAll,this),enableNode:a.proxy(this.enableNode,this),toggleNodeDisabled:a.proxy(this.toggleNodeDisabled,this),search:a.proxy(this.search,this),clearSearch:a.proxy(this.clearSearch,this)}};g.prototype.init=function(b){this.tree=[],this.nodes=[],b.data&&("string"==typeof b.data&&(b.data=a.parseJSON(b.data)),this.tree=a.extend(!0,[],b.data),delete b.data),this.options=a.extend({},f.settings,b),this.destroy(),this.subscribeEvents(),this.setInitialStates({nodes:this.tree},0),this.render()},g.prototype.remove=function(){this.destroy(),a.removeData(this,e),a("#"+this.styleId).remove()},g.prototype.destroy=function(){this.initialized&&(this.$wrapper.remove(),this.$wrapper=null,this.unsubscribeEvents(),this.initialized=!1)},g.prototype.unsubscribeEvents=function(){this.$element.off("click"),this.$element.off("nodeChecked"),this.$element.off("nodeCollapsed"),this.$element.off("nodeDisabled"),this.$element.off("nodeEnabled"),this.$element.off("nodeExpanded"),this.$element.off("nodeSelected"),this.$element.off("nodeUnchecked"),this.$element.off("nodeUnselected"),this.$element.off("searchComplete"),this.$element.off("searchCleared")},g.prototype.subscribeEvents=function(){this.unsubscribeEvents(),this.$element.on("click",a.proxy(this.clickHandler,this)),"function"==typeof this.options.onNodeChecked&&this.$element.on("nodeChecked",this.options.onNodeChecked),"function"==typeof this.options.onNodeCollapsed&&this.$element.on("nodeCollapsed",this.options.onNodeCollapsed),"function"==typeof this.options.onNodeDisabled&&this.$element.on("nodeDisabled",this.options.onNodeDisabled),"function"==typeof this.options.onNodeEnabled&&this.$element.on("nodeEnabled",this.options.onNodeEnabled),"function"==typeof this.options.onNodeExpanded&&this.$element.on("nodeExpanded",this.options.onNodeExpanded),"function"==typeof this.options.onNodeSelected&&this.$element.on("nodeSelected",this.options.onNodeSelected),"function"==typeof this.options.onNodeUnchecked&&this.$element.on("nodeUnchecked",this.options.onNodeUnchecked),"function"==typeof this.options.onNodeUnselected&&this.$element.on("nodeUnselected",this.options.onNodeUnselected),"function"==typeof this.options.onSearchComplete&&this.$element.on("searchComplete",this.options.onSearchComplete),"function"==typeof this.options.onSearchCleared&&this.$element.on("searchCleared",this.options.onSearchCleared)},g.prototype.setInitialStates=function(b,c){if(b.nodes){c+=1;var d=b,e=this;a.each(b.nodes,function(a,b){b.nodeId=e.nodes.length,b.parentId=d.nodeId,b.hasOwnProperty("selectable")||(b.selectable=!0),b.state=b.state||{},b.state.hasOwnProperty("checked")||(b.state.checked=!1),b.state.hasOwnProperty("disabled")||(b.state.disabled=!1),b.state.hasOwnProperty("expanded")||(!b.state.disabled&&c0?b.state.expanded=!0:b.state.expanded=!1),b.state.hasOwnProperty("selected")||(b.state.selected=!1),e.nodes.push(b),b.nodes&&e.setInitialStates(b,c)})}},g.prototype.clickHandler=function(b){this.options.enableLinks||b.preventDefault();var c=a(b.target),d=this.findNode(c);if(d&&!d.state.disabled){var e=c.attr("class")?c.attr("class").split(" "):[];-1!==e.indexOf("expand-icon")?(this.toggleExpandedState(d,f.options),this.render()):-1!==e.indexOf("check-icon")?(this.toggleCheckedState(d,f.options),this.render()):(d.selectable?this.toggleSelectedState(d,f.options):this.toggleExpandedState(d,f.options),this.render())}},g.prototype.findNode=function(a){var b=a.closest("li.list-group-item").attr("data-nodeid"),c=this.nodes[b];return c||console.log("Error: node does not exist"),c},g.prototype.toggleExpandedState=function(a,b){a&&this.setExpandedState(a,!a.state.expanded,b)},g.prototype.setExpandedState=function(b,c,d){c!==b.state.expanded&&(c&&b.nodes?(b.state.expanded=!0,d.silent||this.$element.trigger("nodeExpanded",a.extend(!0,{},b))):c||(b.state.expanded=!1,d.silent||this.$element.trigger("nodeCollapsed",a.extend(!0,{},b)),b.nodes&&!d.ignoreChildren&&a.each(b.nodes,a.proxy(function(a,b){this.setExpandedState(b,!1,d)},this))))},g.prototype.toggleSelectedState=function(a,b){a&&this.setSelectedState(a,!a.state.selected,b)},g.prototype.setSelectedState=function(b,c,d){c!==b.state.selected&&(c?(this.options.multiSelect||a.each(this.findNodes("true","g","state.selected"),a.proxy(function(a,b){this.setSelectedState(b,!1,d)},this)),b.state.selected=!0,d.silent||this.$element.trigger("nodeSelected",a.extend(!0,{},b))):(b.state.selected=!1,d.silent||this.$element.trigger("nodeUnselected",a.extend(!0,{},b))))},g.prototype.toggleCheckedState=function(a,b){a&&this.setCheckedState(a,!a.state.checked,b)},g.prototype.setCheckedState=function(b,c,d){c!==b.state.checked&&(c?(b.state.checked=!0,d.silent||this.$element.trigger("nodeChecked",a.extend(!0,{},b))):(b.state.checked=!1,d.silent||this.$element.trigger("nodeUnchecked",a.extend(!0,{},b))))},g.prototype.setDisabledState=function(b,c,d){c!==b.state.disabled&&(c?(b.state.disabled=!0,this.setExpandedState(b,!1,d),this.setSelectedState(b,!1,d),this.setCheckedState(b,!1,d),d.silent||this.$element.trigger("nodeDisabled",a.extend(!0,{},b))):(b.state.disabled=!1,d.silent||this.$element.trigger("nodeEnabled",a.extend(!0,{},b))))},g.prototype.render=function(){this.initialized||(this.$element.addClass(e),this.$wrapper=a(this.template.list),this.injectStyle(),this.initialized=!0),this.$element.empty().append(this.$wrapper.empty()),this.buildTree(this.tree,0)},g.prototype.buildTree=function(b,c){if(b){c+=1;var d=this;a.each(b,function(b,e){for(var f=a(d.template.item).addClass("node-"+d.elementId).addClass(e.state.checked?"node-checked":"").addClass(e.state.disabled?"node-disabled":"").addClass(e.state.selected?"node-selected":"").addClass(e.searchResult?"search-result":"").attr("data-nodeid",e.nodeId).attr("style",d.buildStyleOverride(e)),g=0;c-1>g;g++)f.append(d.template.indent);var h=[];if(e.nodes?(h.push("expand-icon"),h.push(e.state.expanded?d.options.collapseIcon:d.options.expandIcon)):h.push(d.options.emptyIcon),f.append(a(d.template.icon).addClass(h.join(" "))),d.options.showIcon){var h=["node-icon"];h.push(e.icon||d.options.nodeIcon),e.state.selected&&(h.pop(),h.push(e.selectedIcon||d.options.selectedIcon||e.icon||d.options.nodeIcon)),f.append(a(d.template.icon).addClass(h.join(" ")))}if(d.options.showCheckbox){var h=["check-icon"];h.push(e.state.checked?d.options.checkedIcon:d.options.uncheckedIcon),f.append(a(d.template.icon).addClass(h.join(" ")))}return f.append(d.options.enableLinks?a(d.template.link).attr("href",e.href).append(e.text):e.text),d.options.showTags&&e.tags&&a.each(e.tags,function(b,c){f.append(a(d.template.badge).append(c))}),d.$wrapper.append(f),e.nodes&&e.state.expanded&&!e.state.disabled?d.buildTree(e.nodes,c):void 0})}},g.prototype.buildStyleOverride=function(a){if(a.state.disabled)return"";var b=a.color,c=a.backColor;return this.options.highlightSelected&&a.state.selected&&(this.options.selectedColor&&(b=this.options.selectedColor),this.options.selectedBackColor&&(c=this.options.selectedBackColor)),this.options.highlightSearchResults&&a.searchResult&&!a.state.disabled&&(this.options.searchResultColor&&(b=this.options.searchResultColor),this.options.searchResultBackColor&&(c=this.options.searchResultBackColor)),"color:"+b+";background-color:"+c+";"},g.prototype.injectStyle=function(){this.options.injectStyle&&!c.getElementById(this.styleId)&&a('").appendTo("head")},g.prototype.buildStyle=function(){var a=".node-"+this.elementId+"{";return this.options.color&&(a+="color:"+this.options.color+";"),this.options.backColor&&(a+="background-color:"+this.options.backColor+";"),this.options.showBorder?this.options.borderColor&&(a+="border:1px solid "+this.options.borderColor+";"):a+="border:none;",a+="}",this.options.onhoverColor&&(a+=".node-"+this.elementId+":not(.node-disabled):hover{background-color:"+this.options.onhoverColor+";}"),this.css+a},g.prototype.template={list:'
      ',item:'
    • ',indent:'',icon:'',link:'',badge:''},g.prototype.css=".treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}",g.prototype.getNode=function(a){return this.nodes[a]},g.prototype.getParent=function(a){var b=this.identifyNode(a);return this.nodes[b.parentId]},g.prototype.getSiblings=function(a){var b=this.identifyNode(a),c=this.getParent(b),d=c?c.nodes:this.tree;return d.filter(function(a){return a.nodeId!==b.nodeId})},g.prototype.getSelected=function(){return this.findNodes("true","g","state.selected")},g.prototype.getUnselected=function(){return this.findNodes("false","g","state.selected")},g.prototype.getExpanded=function(){return this.findNodes("true","g","state.expanded")},g.prototype.getCollapsed=function(){return this.findNodes("false","g","state.expanded")},g.prototype.getChecked=function(){return this.findNodes("true","g","state.checked")},g.prototype.getUnchecked=function(){return this.findNodes("false","g","state.checked")},g.prototype.getDisabled=function(){return this.findNodes("true","g","state.disabled")},g.prototype.getEnabled=function(){return this.findNodes("false","g","state.disabled")},g.prototype.selectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!0,b)},this)),this.render()},g.prototype.unselectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeSelected=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleSelectedState(a,b)},this)),this.render()},g.prototype.collapseAll=function(b){var c=this.findNodes("true","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.collapseNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.expandAll=function(b){if(b=a.extend({},f.options,b),b&&b.levels)this.expandLevels(this.tree,b.levels,b);else{var c=this.findNodes("false","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!0,b)},this))}this.render()},g.prototype.expandNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!0,b),a.nodes&&b&&b.levels&&this.expandLevels(a.nodes,b.levels-1,b)},this)),this.render()},g.prototype.expandLevels=function(b,c,d){d=a.extend({},f.options,d),a.each(b,a.proxy(function(a,b){this.setExpandedState(b,c>0?!0:!1,d),b.nodes&&this.expandLevels(b.nodes,c-1,d)},this))},g.prototype.revealNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){for(var c=this.getParent(a);c;)this.setExpandedState(c,!0,b),c=this.getParent(c)},this)),this.render()},g.prototype.toggleNodeExpanded=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleExpandedState(a,b)},this)),this.render()},g.prototype.checkAll=function(b){var c=this.findNodes("false","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.checkNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.uncheckAll=function(b){var c=this.findNodes("true","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.uncheckNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeChecked=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleCheckedState(a,b)},this)),this.render()},g.prototype.disableAll=function(b){var c=this.findNodes("false","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.disableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.enableAll=function(b){var c=this.findNodes("true","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.enableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeDisabled=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!a.state.disabled,b)},this)),this.render()},g.prototype.forEachIdentifier=function(b,c,d){c=a.extend({},f.options,c),b instanceof Array||(b=[b]),a.each(b,a.proxy(function(a,b){d(this.identifyNode(b),c)},this))},g.prototype.identifyNode=function(a){return"number"==typeof a?this.nodes[a]:a},g.prototype.search=function(b,c){c=a.extend({},f.searchOptions,c),this.clearSearch({render:!1});var d=[];if(b&&b.length>0){c.exactMatch&&(b="^"+b+"$");var e="g";c.ignoreCase&&(e+="i"),d=this.findNodes(b,e),a.each(d,function(a,b){b.searchResult=!0})}return c.revealResults?this.revealNode(d):this.render(),this.$element.trigger("searchComplete",a.extend(!0,{},d)),d},g.prototype.clearSearch=function(b){b=a.extend({},{render:!0},b);var c=a.each(this.findNodes("true","g","searchResult"),function(a,b){b.searchResult=!1});b.render&&this.render(),this.$element.trigger("searchCleared",a.extend(!0,{},c))},g.prototype.findNodes=function(b,c,d){c=c||"g",d=d||"text";var e=this;return a.grep(this.nodes,function(a){var f=e.getNodeValue(a,d);return"string"==typeof f?f.match(new RegExp(b,c)):void 0})},g.prototype.getNodeValue=function(a,b){var c=b.indexOf(".");if(c>0){var e=a[b.substring(0,c)],f=b.substring(c+1,b.length);return this.getNodeValue(e,f)}return a.hasOwnProperty(b)?a[b].toString():d};var h=function(a){b.console&&b.console.error(a)};a.fn[e]=function(b,c){var d;return this.each(function(){var f=a.data(this,e);"string"==typeof b?f?a.isFunction(f[b])&&"_"!==b.charAt(0)?(c instanceof Array||(c=[c]),d=f[b].apply(f,c)):h("No such method : "+b):h("Not initialized, can not call method : "+b):"boolean"==typeof b?d=f:a.data(this,e,new g(this,a.extend(!0,{},b)))}),d||this}}(jQuery,window,document); -------------------------------------------------------------------------------- /public/dashboard.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | const basePath = $('#basePath').val(); 3 | 4 | function capitalize(string) { 5 | return string.charAt(0).toUpperCase() + string.slice(1); 6 | } 7 | 8 | function formatToTreeView(flow, flowHost) { 9 | const {job, children} = flow; 10 | if (!job) { 11 | return {}; 12 | } 13 | const text = `${job.name} ${job.id}`; 18 | 19 | if (children && children.length > 0) { 20 | return { 21 | text, 22 | nodes: children.map((child) => formatToTreeView(child, flowHost)), 23 | }; 24 | } else { 25 | return { 26 | text, 27 | }; 28 | } 29 | } 30 | 31 | // Set up individual "retry job" handler 32 | $('.js-retry-job').on('click', function (e) { 33 | e.preventDefault(); 34 | $(this).prop('disabled', true); 35 | 36 | const jobId = $(this).data('job-id'); 37 | const queueName = $(this).data('queue-name'); 38 | const queueHost = $(this).data('queue-host'); 39 | 40 | const r = window.confirm( 41 | `Retry job #${jobId} in queue "${queueHost}/${queueName}"?` 42 | ); 43 | if (r) { 44 | $.ajax({ 45 | method: 'PATCH', 46 | url: `${basePath}/api/queue/${encodeURIComponent( 47 | queueHost 48 | )}/${encodeURIComponent(queueName)}/job/${encodeURIComponent(jobId)}`, 49 | }) 50 | .done(() => { 51 | window.location.reload(); 52 | }) 53 | .fail((jqXHR) => { 54 | window.alert(`Request failed, check console for error.`); 55 | console.error(jqXHR.responseText); 56 | }); 57 | } else { 58 | $(this).prop('disabled', false); 59 | } 60 | }); 61 | 62 | // Set up individual "promote job" handler 63 | $('.js-promote-job').on('click', function (e) { 64 | e.preventDefault(); 65 | $(this).prop('disabled', true); 66 | 67 | const jobId = $(this).data('job-id'); 68 | const queueName = $(this).data('queue-name'); 69 | const queueHost = $(this).data('queue-host'); 70 | 71 | const r = window.confirm( 72 | `Promote job #${jobId} in queue "${queueHost}/${queueName}"?` 73 | ); 74 | if (r) { 75 | $.ajax({ 76 | method: 'PATCH', 77 | url: `${basePath}/api/queue/${encodeURIComponent( 78 | queueHost 79 | )}/${encodeURIComponent(queueName)}/delayed/job/${encodeURIComponent( 80 | jobId 81 | )}`, 82 | }) 83 | .done(() => { 84 | window.location.reload(); 85 | }) 86 | .fail((jqXHR) => { 87 | window.alert(`Request failed, check console for error.`); 88 | console.error(jqXHR.responseText); 89 | }); 90 | } else { 91 | $(this).prop('disabled', false); 92 | } 93 | }); 94 | 95 | // Set up individual "remove job" handler 96 | $('.js-remove-job').on('click', function (e) { 97 | e.preventDefault(); 98 | $(this).prop('disabled', true); 99 | 100 | const jobId = $(this).data('job-id'); 101 | const queueName = $(this).data('queue-name'); 102 | const queueHost = $(this).data('queue-host'); 103 | const jobState = $(this).data('job-state'); 104 | 105 | const r = window.confirm( 106 | `Remove job #${jobId} in queue "${queueHost}/${queueName}"?` 107 | ); 108 | if (r) { 109 | $.ajax({ 110 | method: 'DELETE', 111 | url: `${basePath}/api/queue/${encodeURIComponent( 112 | queueHost 113 | )}/${encodeURIComponent(queueName)}/job/${encodeURIComponent(jobId)}`, 114 | }) 115 | .done(() => { 116 | window.location.href = `${basePath}/${encodeURIComponent( 117 | queueHost 118 | )}/${encodeURIComponent(queueName)}/${jobState}`; 119 | }) 120 | .fail((jqXHR) => { 121 | window.alert(`Request failed, check console for error.`); 122 | console.error(jqXHR.responseText); 123 | }); 124 | } else { 125 | $(this).prop('disabled', false); 126 | } 127 | }); 128 | 129 | // Set up individual "remove repeatable job" handler 130 | $('.js-remove-repeatable-job').on('click', function (e) { 131 | e.preventDefault(); 132 | $(this).prop('disabled', true); 133 | 134 | const jobId = $(this).data('job-id'); 135 | const queueName = $(this).data('queue-name'); 136 | const queueHost = $(this).data('queue-host'); 137 | const jobState = $(this).data('job-state'); 138 | 139 | const confirmationResponse = window.confirm( 140 | `Remove repeatable job #${jobId} in queue "${queueHost}/${queueName}"?` 141 | ); 142 | if (confirmationResponse) { 143 | $.ajax({ 144 | method: 'DELETE', 145 | url: `${basePath}/api/queue/${encodeURIComponent( 146 | queueHost 147 | )}/${encodeURIComponent(queueName)}/repeatable/job/${encodeURIComponent( 148 | jobId 149 | )}`, 150 | }) 151 | .done(() => { 152 | window.location.href = `${basePath}/${encodeURIComponent( 153 | queueHost 154 | )}/${encodeURIComponent(queueName)}/${jobState}`; 155 | }) 156 | .fail((jqXHR) => { 157 | window.alert(`Request failed, check console for error.`); 158 | console.error(jqXHR.responseText); 159 | }); 160 | } else { 161 | $(this).prop('disabled', false); 162 | } 163 | }); 164 | 165 | // Set up "select all jobs" button handler 166 | $('.js-select-all-jobs').change(function () { 167 | const $jobBulkCheckboxes = $('.js-bulk-job'); 168 | $jobBulkCheckboxes.prop('checked', this.checked); 169 | }); 170 | 171 | // Set up "shift-click" multiple checkbox selection handler 172 | (function () { 173 | // https://stackoverflow.com/questions/659508/how-can-i-shift-select-multiple-checkboxes-like-gmail 174 | let lastChecked = null; 175 | let $jobBulkCheckboxes = $('.js-bulk-job'); 176 | $jobBulkCheckboxes.click(function (e) { 177 | if (!lastChecked) { 178 | lastChecked = this; 179 | return; 180 | } 181 | 182 | if (e.shiftKey) { 183 | let start = $jobBulkCheckboxes.index(this); 184 | let end = $jobBulkCheckboxes.index(lastChecked); 185 | 186 | $jobBulkCheckboxes 187 | .slice(Math.min(start, end), Math.max(start, end) + 1) 188 | .prop('checked', lastChecked.checked); 189 | } 190 | 191 | lastChecked = this; 192 | }); 193 | })(); 194 | 195 | // Set up bulk actions handler 196 | $('.js-bulk-action').on('click', function (e) { 197 | $(this).prop('disabled', true); 198 | 199 | const $bulkActionContainer = $('.js-bulk-action-container'); 200 | const action = $(this).data('action'); 201 | const queueName = $(this).data('queue-name'); 202 | const queueHost = $(this).data('queue-host'); 203 | const queueState = $(this).data('queue-state'); 204 | 205 | let data = { 206 | queueName, 207 | action, 208 | jobs: [], 209 | queueState, 210 | }; 211 | 212 | if (action !== 'clean') { 213 | $bulkActionContainer.each((index, value) => { 214 | const isChecked = $(value).find('[name=jobChecked]').is(':checked'); 215 | const id = encodeURIComponent($(value).find('[name=jobId]').val()); 216 | 217 | if (isChecked) { 218 | data.jobs.push(id); 219 | } 220 | }); 221 | } 222 | 223 | const count = action === 'clean' ? 1000 : data.jobs.length; 224 | 225 | const r = window.confirm( 226 | `${capitalize(action)} ${count} ${ 227 | count > 1 ? 'jobs' : 'job' 228 | } in queue "${queueHost}/${queueName}"?` 229 | ); 230 | if (r) { 231 | if (action === 'clean') { 232 | $.ajax({ 233 | method: 'DELETE', 234 | url: `${basePath}/api/queue/${encodeURIComponent( 235 | queueHost 236 | )}/${encodeURIComponent(queueName)}/jobs/bulk`, 237 | data: JSON.stringify(data), 238 | contentType: 'application/json', 239 | }) 240 | .done(() => { 241 | window.location.reload(); 242 | }) 243 | .fail((jqXHR) => { 244 | window.alert(`Request failed, check console for error.`); 245 | console.error(jqXHR.responseText); 246 | }); 247 | } else { 248 | $.ajax({ 249 | method: action === 'remove' ? 'POST' : 'PATCH', 250 | url: `${basePath}/api/queue/${encodeURIComponent( 251 | queueHost 252 | )}/${encodeURIComponent(queueName)}/${ 253 | action === 'promote' ? 'delayed/' : '' 254 | }job/bulk`, 255 | data: JSON.stringify(data), 256 | contentType: 'application/json', 257 | }) 258 | .done(() => { 259 | window.location.reload(); 260 | }) 261 | .fail((jqXHR) => { 262 | window.alert(`Request failed, check console for error.`); 263 | console.error(jqXHR.responseText); 264 | }); 265 | } 266 | } else { 267 | $(this).prop('disabled', false); 268 | } 269 | }); 270 | 271 | $('.js-toggle-add-job-editor').on('click', function () { 272 | const addJobText = $('.js-toggle-add-job-editor').text(); 273 | const shouldNotHide = addJobText === 'Add Job'; 274 | const newAddJobText = shouldNotHide ? 'Cancel' : 'Add Job'; 275 | $('.jsoneditorx').toggleClass('hide', !shouldNotHide); 276 | $('.js-toggle-add-job-editor').text(newAddJobText); 277 | 278 | const job = localStorage.getItem('arena:savedJob'); 279 | if (job) { 280 | const {name, data, opts} = JSON.parse(job); 281 | window.jsonEditor.set(data); 282 | if (window.jsonEditorOpts) window.jsonEditorOpts.set(opts); 283 | $('input.js-add-job-name').val(name); 284 | } else { 285 | window.jsonEditor.set({id: ''}); 286 | } 287 | }); 288 | 289 | $('.js-toggle-update-queue-meta').on('click', function () { 290 | const updateMetaText = $('.js-toggle-update-queue-meta').text(); 291 | const shouldNotHide = updateMetaText === 'Update'; 292 | const newUpdateMetaText = shouldNotHide ? 'Cancel' : 'Update'; 293 | $('.meta-config-editor').toggleClass('hide', !shouldNotHide); 294 | $('.js-toggle-update-queue-meta').text(newUpdateMetaText); 295 | }); 296 | 297 | $('.js-update-queue-meta').on('click', function () { 298 | const {queueHost, queueName} = window.arenaInitialPayload; 299 | const concurrency = $('input.js-update-meta-concurrency').val() || null; 300 | const max = $('input.js-update-meta-rl-max').val() || null; 301 | const duration = $('input.js-update-meta-rl-duration').val() || null; 302 | 303 | const stringifiedData = JSON.stringify({ 304 | concurrency: concurrency ? parseInt(concurrency, 10) : null, 305 | max: max ? parseInt(max, 10) : null, 306 | duration: duration ? parseInt(duration, 10) : null, 307 | }); 308 | 309 | const response = window.confirm( 310 | `Are you sure you want to update the queue "${queueHost}/${queueName}" configuration?` 311 | ); 312 | if (response) { 313 | $.ajax({ 314 | url: `${basePath}/api/queue/${encodeURIComponent( 315 | queueHost 316 | )}/${encodeURIComponent(queueName)}/update-meta`, 317 | type: 'PUT', 318 | data: stringifiedData, 319 | contentType: 'application/json', 320 | }) 321 | .done(() => { 322 | window.location.reload(); 323 | }) 324 | .fail((jqXHR) => { 325 | window.alert(`Request failed, check console for error.`); 326 | console.error(jqXHR.responseText); 327 | }); 328 | } 329 | }); 330 | 331 | $('.js-toggle-add-flow-editor').on('click', function () { 332 | const addFlowText = $('.js-toggle-add-flow-editor').text(); 333 | const shouldNotHide = addFlowText === 'Add Flow'; 334 | const newAddFlowText = shouldNotHide ? 'Cancel Add' : 'Add Flow'; 335 | $('.jsoneditorx').toggleClass('hide', !shouldNotHide); 336 | $('.js-toggle-add-flow-editor').text(newAddFlowText); 337 | 338 | const flow = localStorage.getItem('arena:savedFlow'); 339 | if (flow) { 340 | const {data} = JSON.parse(flow); 341 | window.jsonEditor.set(data); 342 | } else { 343 | window.jsonEditor.set({}); 344 | } 345 | }); 346 | 347 | $('.js-toggle-search-flow').on('click', function () { 348 | const searchFlowText = $('.js-toggle-search-flow').text(); 349 | const shouldNotHide = searchFlowText === 'Search Flow'; 350 | const newSearchFlowText = shouldNotHide ? 'Cancel Search' : 'Search Flow'; 351 | $('.searchflowx').toggleClass('hide', !shouldNotHide); 352 | $('.js-toggle-search-flow').text(newSearchFlowText); 353 | }); 354 | 355 | $('.js-add-job').on('click', function () { 356 | const name = $('input.js-add-job-name').val() || null; 357 | const data = window.jsonEditor.get(); 358 | const opts = window.jsonEditorOpts ? window.jsonEditorOpts.get() : {}; 359 | const job = JSON.stringify({name, data, opts}); 360 | localStorage.setItem('arena:savedJob', job); 361 | const {queueHost, queueName} = window.arenaInitialPayload; 362 | $.ajax({ 363 | url: `${basePath}/api/queue/${encodeURIComponent( 364 | queueHost 365 | )}/${encodeURIComponent(queueName)}/job`, 366 | type: 'POST', 367 | data: job, 368 | contentType: 'application/json', 369 | }) 370 | .done(() => { 371 | alert('Job successfully added!'); 372 | localStorage.removeItem('arena:savedJob'); 373 | }) 374 | .fail((jqXHR) => { 375 | window.alert('Failed to save job, check console for error.'); 376 | console.error(jqXHR.responseText); 377 | }); 378 | }); 379 | 380 | $('.js-update-job-data').on('click', function (e) { 381 | e.preventDefault(); 382 | const jobId = $(this).data('job-id'); 383 | const queueName = $(this).data('queue-name'); 384 | const queueHost = $(this).data('queue-host'); 385 | const stringifiedData = JSON.stringify(window.jsonEditor.get()); 386 | const r = window.confirm( 387 | `Update job #${jobId} data in queue "${queueHost}/${queueName}"?` 388 | ); 389 | 390 | if (r) { 391 | $.ajax({ 392 | url: `${basePath}/api/queue/${encodeURIComponent( 393 | queueHost 394 | )}/${encodeURIComponent(queueName)}/job/${encodeURIComponent( 395 | jobId 396 | )}/data`, 397 | type: 'PUT', 398 | data: stringifiedData, 399 | contentType: 'application/json', 400 | }) 401 | .done(() => { 402 | alert('Job data successfully updated!'); 403 | window.location.reload(); 404 | }) 405 | .fail((jqXHR) => { 406 | window.alert('Failed to update job data, check console for error.'); 407 | console.error(jqXHR.responseText); 408 | }); 409 | } 410 | }); 411 | 412 | $('.js-add-flow').on('click', function () { 413 | const data = window.jsonEditor.get(); 414 | const flow = JSON.stringify({data}); 415 | localStorage.setItem('arena:savedFlow', flow); 416 | const {flowHost, connectionName} = window.arenaInitialPayload; 417 | $.ajax({ 418 | url: `${basePath}/api/flow/${encodeURIComponent( 419 | flowHost 420 | )}/${encodeURIComponent(connectionName)}/flow`, 421 | type: 'POST', 422 | data: flow, 423 | contentType: 'application/json', 424 | }) 425 | .done((res) => { 426 | const flowTree = formatToTreeView(res, flowHost); 427 | alert('Flow successfully added!'); 428 | localStorage.removeItem('arena:savedFlow'); 429 | $('#tree').treeview({data: [flowTree], enableLinks: true}); 430 | $('.js-tree').toggleClass('hide', false); 431 | }) 432 | .fail((jqXHR) => { 433 | window.alert('Failed to save flow, check console for error.'); 434 | console.error(jqXHR.responseText); 435 | }); 436 | }); 437 | 438 | $('.js-search-flow').on('click', function (e) { 439 | e.preventDefault(); 440 | const queueName = $('#queue-name-input-search').val(); 441 | const jobId = $('#job-id-input-search').val(); 442 | const depth = $('#depth-input-search').val(); 443 | const maxChildren = $('#max-children-input-search').val(); 444 | 445 | const {flowHost, connectionName} = window.arenaInitialPayload; 446 | 447 | $.ajax({ 448 | url: `${basePath}/api/flow/${encodeURIComponent( 449 | flowHost 450 | )}/${encodeURIComponent( 451 | connectionName 452 | )}/flow?jobId=${jobId}&queueName=${queueName}&depth=${depth}&maxChildren=${maxChildren}`, 453 | type: 'GET', 454 | contentType: 'application/json', 455 | }) 456 | .done((res) => { 457 | const flowTree = formatToTreeView(res, flowHost); 458 | alert('Flow info successfully fetched!'); 459 | $('#tree').treeview({data: [flowTree], enableLinks: true}); 460 | $('.js-tree').toggleClass('hide', false); 461 | }) 462 | .fail((jqXHR) => { 463 | window.alert('Failed to get flow info, check console for error.'); 464 | console.error(jqXHR.responseText); 465 | }); 466 | }); 467 | 468 | $('.js-pause-queue').on('click', function (e) { 469 | e.preventDefault(); 470 | $(this).prop('disabled', true); 471 | const queueName = $(this).data('queue-name'); 472 | const queueHost = $(this).data('queue-host'); 473 | 474 | const response = window.confirm( 475 | `Are you sure you want to pause the queue "${queueHost}/${queueName}"?` 476 | ); 477 | if (response) { 478 | $.ajax({ 479 | method: 'PUT', 480 | url: `${basePath}/api/queue/${encodeURIComponent( 481 | queueHost 482 | )}/${encodeURIComponent(queueName)}/pause`, 483 | }) 484 | .done(() => { 485 | window.location.reload(); 486 | }) 487 | .fail((jqXHR) => { 488 | window.alert(`Request failed, check console for error.`); 489 | console.error(jqXHR.responseText); 490 | }); 491 | } else { 492 | $(this).prop('disabled', false); 493 | } 494 | }); 495 | 496 | $('.js-resume-queue').on('click', function (e) { 497 | e.preventDefault(); 498 | const queueName = $(this).data('queue-name'); 499 | const queueHost = $(this).data('queue-host'); 500 | 501 | const response = window.confirm( 502 | `Are you sure you want to resume the queue "${queueHost}/${queueName}"?` 503 | ); 504 | if (response) { 505 | $.ajax({ 506 | method: 'PUT', 507 | url: `${basePath}/api/queue/${encodeURIComponent( 508 | queueHost 509 | )}/${encodeURIComponent(queueName)}/resume`, 510 | }) 511 | .done(() => { 512 | window.location.reload(); 513 | }) 514 | .fail((jqXHR) => { 515 | window.alert(`Request failed, check console for error.`); 516 | console.error(jqXHR.responseText); 517 | }); 518 | } else { 519 | $(this).prop('disabled', false); 520 | } 521 | }); 522 | 523 | $('.js-remove-rate-limit-key').on('click', function (e) { 524 | e.preventDefault(); 525 | const queueName = $(this).data('queue-name'); 526 | const queueHost = $(this).data('queue-host'); 527 | 528 | const response = window.confirm( 529 | `Are you sure you want to remove the Rate Limit key for the queue "${queueHost}/${queueName}"?` 530 | ); 531 | if (response) { 532 | $.ajax({ 533 | method: 'DELETE', 534 | url: `${basePath}/api/queue/${encodeURIComponent( 535 | queueHost 536 | )}/${encodeURIComponent(queueName)}/rate-limit-key`, 537 | }) 538 | .done(() => { 539 | window.location.reload(); 540 | }) 541 | .fail((jqXHR) => { 542 | window.alert(`Request failed, check console for error.`); 543 | console.error(jqXHR.responseText); 544 | }); 545 | } else { 546 | $(this).prop('disabled', false); 547 | } 548 | }); 549 | }); 550 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [4.9.2](https://github.com/bee-queue/arena/compare/v4.9.1...v4.9.2) (2025-11-18) 2 | 3 | ### Bug Fixes 4 | 5 | - **queue-details:** show current global values when updating ([#690](https://github.com/bee-queue/arena/issues/690)) ([f40056d](https://github.com/bee-queue/arena/commit/f40056d8940c35570464afc7327a1faf6481dd0f)) 6 | 7 | ### [4.9.1](https://github.com/bee-queue/arena/compare/v4.9.0...v4.9.1) (2025-11-18) 8 | 9 | ### Bug Fixes 10 | 11 | - **job-details:** generate more accurate executes at ([#689](https://github.com/bee-queue/arena/issues/689)) ([b4b4931](https://github.com/bee-queue/arena/commit/b4b4931661494e7d61ce18b1d1077d4fa4ab92fa)) 12 | 13 | ## [4.9.0](https://github.com/bee-queue/arena/compare/v4.8.0...v4.9.0) (2025-11-13) 14 | 15 | ### Features 16 | 17 | - **queue-details:** add button to remove rate limit key ([#688](https://github.com/bee-queue/arena/issues/688)) ([ad87612](https://github.com/bee-queue/arena/commit/ad876123bfe41782e52db42f8d0ced25b503c0a4)) 18 | 19 | ## [4.8.0](https://github.com/bee-queue/arena/compare/v4.7.0...v4.8.0) (2025-11-02) 20 | 21 | ### Features 22 | 23 | - **queue:** support updating queue configuration ([#687](https://github.com/bee-queue/arena/issues/687)) ([ab2f6af](https://github.com/bee-queue/arena/commit/ab2f6af8cf21223bf8c88eaf0e5a73c9ee358700)) 24 | 25 | ## [4.7.0](https://github.com/bee-queue/arena/compare/v4.6.0...v4.7.0) (2025-10-31) 26 | 27 | ### Features 28 | 29 | - **queue:** show queue global config if present ([#686](https://github.com/bee-queue/arena/issues/686)) ([a49254f](https://github.com/bee-queue/arena/commit/a49254f04c5f8194ab5df7f20977e2b339f9ec50)) 30 | 31 | ## [4.6.0](https://github.com/bee-queue/arena/compare/v4.5.1...v4.6.0) (2025-08-28) 32 | 33 | ### Features 34 | 35 | - **job-details:** show job name if present ([#685](https://github.com/bee-queue/arena/issues/685)) ([112361c](https://github.com/bee-queue/arena/commit/112361c7ae7f7064386ccc521d79953a6cf37823)) 36 | 37 | ### [4.5.1](https://github.com/bee-queue/arena/compare/v4.5.0...v4.5.1) (2025-04-28) 38 | 39 | ### Bug Fixes 40 | 41 | - **get-flow:** consider when there are not children ([#683](https://github.com/bee-queue/arena/issues/683)) ([baa676a](https://github.com/bee-queue/arena/commit/baa676aa5544f09e645ebbd02b02adfb7695a938)) 42 | 43 | ## [4.5.0](https://github.com/bee-queue/arena/compare/v4.4.2...v4.5.0) (2025-04-25) 44 | 45 | ### Features 46 | 47 | - **bullmq:** support ignored children ([#682](https://github.com/bee-queue/arena/issues/682)) ([e41dfb8](https://github.com/bee-queue/arena/commit/e41dfb84201d997dc968f1fd4a9854d44d07d5f4)) 48 | 49 | ### [4.4.2](https://github.com/bee-queue/arena/compare/v4.4.1...v4.4.2) (2024-06-13) 50 | 51 | ### Bug Fixes 52 | 53 | - **bull:** retry only failed jobs ([#678](https://github.com/bee-queue/arena/issues/678)) ([8488040](https://github.com/bee-queue/arena/commit/848804051a5949487b06453fbf54b1f2ca2dc737)) 54 | 55 | ### [4.4.1](https://github.com/bee-queue/arena/compare/v4.4.0...v4.4.1) (2024-05-07) 56 | 57 | ### Bug Fixes 58 | 59 | - **jobs:** validate jobs length in bulk action ([#676](https://github.com/bee-queue/arena/issues/676)) ([2bce363](https://github.com/bee-queue/arena/commit/2bce36391052d747c01aa20adda28d38d4a8fdee)) 60 | 61 | ## [4.4.0](https://github.com/bee-queue/arena/compare/v4.3.0...v4.4.0) (2024-05-03) 62 | 63 | ### Features 64 | 65 | - **bull:** add clean jobs button for completed and failed ([#675](https://github.com/bee-queue/arena/issues/675)) ([e62aef0](https://github.com/bee-queue/arena/commit/e62aef022cf771b715cf65d100391f6f88c5cfd5)) 66 | 67 | ## [4.3.0](https://github.com/bee-queue/arena/compare/v4.2.0...v4.3.0) (2024-04-27) 68 | 69 | ### Features 70 | 71 | - **details:** update job data ([#674](https://github.com/bee-queue/arena/issues/674)) ([24ab67f](https://github.com/bee-queue/arena/commit/24ab67f637da2ae8add30231340e413342de8461)) 72 | 73 | ## [4.2.0](https://github.com/bee-queue/arena/compare/v4.1.1...v4.2.0) (2024-02-12) 74 | 75 | ### Features 76 | 77 | - **add-job:** provide editor for adding options ([#670](https://github.com/bee-queue/arena/issues/670)) ([3f4c640](https://github.com/bee-queue/arena/commit/3f4c6400b090b64fbcec87df03442bb8c2672790)) 78 | 79 | ### [4.1.1](https://github.com/bee-queue/arena/compare/v4.1.0...v4.1.1) (2024-02-08) 80 | 81 | ### Bug Fixes 82 | 83 | - **layouts:** include favicon at layout template ([#668](https://github.com/bee-queue/arena/issues/668)) ([a107ad9](https://github.com/bee-queue/arena/commit/a107ad9b16fb8b5c20abece0ed36ca3855895e41)) 84 | 85 | ## [4.1.0](https://github.com/bee-queue/arena/compare/v4.0.1...v4.1.0) (2023-10-28) 86 | 87 | ### Features 88 | 89 | - **bullmq:** support removing repeatable jobs ([#667](https://github.com/bee-queue/arena/issues/667)) ([df1ab37](https://github.com/bee-queue/arena/commit/df1ab37ac51af342ffbadff8aeba2b18313a2dbb)) 90 | 91 | ### [4.0.1](https://github.com/bee-queue/arena/compare/v4.0.0...v4.0.1) (2023-09-11) 92 | 93 | ### Bug Fixes 94 | 95 | - **job-details:** omit scripts attribute to stringify jobs ([#665](https://github.com/bee-queue/arena/issues/665)) fixes [#598](https://github.com/bee-queue/arena/issues/598) ([a76ed5f](https://github.com/bee-queue/arena/commit/a76ed5f9c7d48e647e62a16ef282a77c3eafc999)) 96 | 97 | ## [4.0.0](https://github.com/bee-queue/arena/compare/v3.30.4...v4.0.0) (2023-09-02) 98 | 99 | ### ⚠ BREAKING CHANGES 100 | 101 | - **bullmq:** add new prioritized state, previous versions of bullmq wont't see this state 102 | 103 | ### Features 104 | 105 | - **bullmq:** support prioritized state ([#664](https://github.com/bee-queue/arena/issues/664)) ([9a7541c](https://github.com/bee-queue/arena/commit/9a7541cd08fa33e8c4f75f8fc38dae3a63eacaa2)) 106 | 107 | ### [3.30.4](https://github.com/bee-queue/arena/compare/v3.30.3...v3.30.4) (2023-02-18) 108 | 109 | ### Bug Fixes 110 | 111 | - **mounting:** differentiate base and appBase paths when disableListen ([#623](https://github.com/bee-queue/arena/issues/623)) ([387e3ac](https://github.com/bee-queue/arena/commit/387e3ac7fdcf94f72765d80c26d3b074c91cdd03)) 112 | 113 | ### [3.30.3](https://github.com/bee-queue/arena/compare/v3.30.2...v3.30.3) (2023-01-06) 114 | 115 | ### Bug Fixes 116 | 117 | - **bullmq:** consider delayed markers ([#605](https://github.com/bee-queue/arena/issues/605)) fixes [#600](https://github.com/bee-queue/arena/issues/600) ([8b6edae](https://github.com/bee-queue/arena/commit/8b6edae1b0d27833af42f60acf522a5e34b1a97a)) 118 | 119 | ### [3.30.2](https://github.com/bee-queue/arena/compare/v3.30.1...v3.30.2) (2022-12-04) 120 | 121 | ### Bug Fixes 122 | 123 | - **qs:** security patches on body-parser and express dependencies ([#593](https://github.com/bee-queue/arena/issues/593)) ([6c5871f](https://github.com/bee-queue/arena/commit/6c5871ffa149e83f0e51a2123567a9c0856f67d5)) 124 | 125 | ### [3.30.1](https://github.com/bee-queue/arena/compare/v3.30.0...v3.30.1) (2022-11-07) 126 | 127 | ### Bug Fixes 128 | 129 | - **remove-repeatable:** consider old versions of bull ([#580](https://github.com/bee-queue/arena/issues/580)) ([f406750](https://github.com/bee-queue/arena/commit/f406750ba4654918e194574caa08d29048f6ed03)) 130 | 131 | ## [3.30.0](https://github.com/bee-queue/arena/compare/v3.29.5...v3.30.0) (2022-10-18) 132 | 133 | ### Features 134 | 135 | - **bull:** support removing repeatable jobs ([#574](https://github.com/bee-queue/arena/issues/574)) ([29528cf](https://github.com/bee-queue/arena/commit/29528cfeea91dcf8771551c44fd4dc5e29f87718)) 136 | 137 | ### [3.29.5](https://github.com/bee-queue/arena/compare/v3.29.4...v3.29.5) (2022-08-11) 138 | 139 | ### Bug Fixes 140 | 141 | - **deps:** bump minimist from 1.2.5 to 1.2.6 ([#507](https://github.com/bee-queue/arena/issues/507)) ([229cfe3](https://github.com/bee-queue/arena/commit/229cfe3a90e41f11a296fd169e09a5628e948f77)) 142 | 143 | ### [3.29.4](https://github.com/bee-queue/arena/compare/v3.29.3...v3.29.4) (2022-08-11) 144 | 145 | ### Bug Fixes 146 | 147 | - **deps:** bump moment from 2.29.1 to 2.29.4 ([#540](https://github.com/bee-queue/arena/issues/540)) ([81f13a8](https://github.com/bee-queue/arena/commit/81f13a83cf38574746b6f23ae981819383e7a6f6)) 148 | 149 | ### [3.29.3](https://github.com/bee-queue/arena/compare/v3.29.2...v3.29.3) (2021-09-08) 150 | 151 | ### Bug Fixes 152 | 153 | - 🐛 Update how Redis URL is passed to Bull ([81bf488](https://github.com/bee-queue/arena/commit/81bf488d3d668dba986bc03e171e6f3bc0faf761)), closes [OptimalBits/bull#2118](https://github.com/OptimalBits/bull/issues/2118) 154 | 155 | ### [3.29.2](https://github.com/bee-queue/arena/compare/v3.29.1...v3.29.2) (2021-08-12) 156 | 157 | ### Bug Fixes 158 | 159 | - Revert bootstrap upgrade in 3.24.0 ([PR](https://github.com/bee-queue/arena/pull/432)) 160 | 161 | ### [3.29.1](https://github.com/bee-queue/arena/compare/v3.29.0...v3.29.1) (2021-07-06) 162 | 163 | ### Bug Fixes 164 | 165 | - **job-details:** encodeURI of job ID for URL ([0b60010](https://github.com/bee-queue/arena/commit/0b600102769fe066c2aec7046c970009dbd5ef5f)), closes [#416](https://github.com/bee-queue/arena/issues/416) 166 | 167 | ## [3.29.0](https://github.com/bee-queue/arena/compare/v3.28.0...v3.29.0) (2021-06-14) 168 | 169 | ### Features 170 | 171 | - **tree-view:** use perma links on nodes ([07b6f3d](https://github.com/bee-queue/arena/commit/07b6f3d89970cc8da3b2988f68213b47eac86c51)) 172 | 173 | ## [3.28.0](https://github.com/bee-queue/arena/compare/v3.27.0...v3.28.0) (2021-06-11) 174 | 175 | ### Features 176 | 177 | - **flow:** add search button to get a flow tree ([59b0423](https://github.com/bee-queue/arena/commit/59b0423d1525f1166ded19dfe9fe937b3a98023e)) 178 | - **layout:** add treeview ([d3fa754](https://github.com/bee-queue/arena/commit/d3fa754bf292c1e4f3d2805af4dfd155f1437f2f)) 179 | - **tree-view:** add tree view when creating a flow ([eb93a60](https://github.com/bee-queue/arena/commit/eb93a60c96ec4322ea1ca1d9b85389b9839e4712)) 180 | 181 | ## [3.27.0](https://github.com/bee-queue/arena/compare/v3.26.0...v3.27.0) (2021-06-11) 182 | 183 | ### Features 184 | 185 | - **job-details:** add pagination options in getDependencies ([40e177f](https://github.com/bee-queue/arena/commit/40e177f7acdb2991d6ba6e58b2fa82bce641d348)) 186 | 187 | ## [3.26.0](https://github.com/bee-queue/arena/compare/v3.25.0...v3.26.0) (2021-06-10) 188 | 189 | ### Features 190 | 191 | - **job-details:** add children counters ([71bbb9d](https://github.com/bee-queue/arena/commit/71bbb9dc3d5ec3236b5281b87aad757acde40462)) 192 | 193 | ## [3.25.0](https://github.com/bee-queue/arena/compare/v3.24.1...v3.25.0) (2021-06-10) 194 | 195 | ### Features 196 | 197 | - **parent-children:** implement perma-link for bullmq ([bbd2317](https://github.com/bee-queue/arena/commit/bbd2317606ed5fab5626de06f381ee02d6d7ab45)) 198 | - initial changes for displaying parentJob and childrenJobs in JobDetails template - WIP ([61d93e2](https://github.com/bee-queue/arena/commit/61d93e2fc31080bb3bc6846c212c75b96b5267d9)) 199 | 200 | ### [3.24.1](https://github.com/bee-queue/arena/compare/v3.24.0...v3.24.1) (2021-06-08) 201 | 202 | ### Bug Fixes 203 | 204 | - **job-details:** show progress for bullmq ([8341174](https://github.com/bee-queue/arena/commit/8341174cba43bf24ca4863df6abe88f2fb37fc98)) 205 | 206 | ## [3.24.0](https://github.com/bee-queue/arena/compare/v3.23.0...v3.24.0) (2021-06-07) 207 | 208 | ### Features 209 | 210 | - **bootstrap:** upgrade to v4.6.0 ([c8d24c5](https://github.com/bee-queue/arena/commit/c8d24c58bce363a0931fe1c67b885c165dbfc21b)) 211 | 212 | ## [3.23.0](https://github.com/bee-queue/arena/compare/v3.22.0...v3.23.0) (2021-06-02) 213 | 214 | ### Features 215 | 216 | - **bullmq:** provide support for flow creation ([da783af](https://github.com/bee-queue/arena/commit/da783afd52853a9c63510c3d1483afe1b15cf6c1)) 217 | - **flow-details:** add redis statistics ([e2b20f3](https://github.com/bee-queue/arena/commit/e2b20f374ccda9d51d3f2b23db5e1c29a0994bba)) 218 | 219 | ## [3.22.0](https://github.com/bee-queue/arena/compare/v3.21.0...v3.22.0) (2021-05-25) 220 | 221 | ### Features 222 | 223 | - **bull:** adding pause queue button ([019f7f5](https://github.com/bee-queue/arena/commit/019f7f53740c0c1804bbc8506ee0f8155348bba0)) 224 | 225 | ### Bug Fixes 226 | 227 | - **bull:** consider paused state ([3651d52](https://github.com/bee-queue/arena/commit/3651d5252d2d5a6b1cf704f34f75a97fe8c7582a)) 228 | - **deps:** upgrading handlebars to 4.7.7 ([5a62529](https://github.com/bee-queue/arena/commit/5a62529507b6c1895facc596a24daea4b9c5f842)) 229 | 230 | ## [3.21.0](https://github.com/bee-queue/arena/compare/v3.20.1...v3.21.0) (2021-05-20) 231 | 232 | ### Features 233 | 234 | - **bullmq:** support waiting-children state ([8832821](https://github.com/bee-queue/arena/commit/8832821225f69b51f753f24aa76d72889515031f)) 235 | 236 | ### [3.20.1](https://github.com/bee-queue/arena/compare/v3.20.0...v3.20.1) (2021-04-15) 237 | 238 | ### Bug Fixes 239 | 240 | - **jsoneditor:** adding map file ([f374f98](https://github.com/bee-queue/arena/commit/f374f98bdc2594dfea147a7309b306522557ac3d)) 241 | 242 | ## [3.20.0](https://github.com/bee-queue/arena/compare/v3.19.0...v3.20.0) (2021-04-13) 243 | 244 | ### Features 245 | 246 | - **promote:** adding Promote Jobs button ([c0e0d59](https://github.com/bee-queue/arena/commit/c0e0d590c02f986d0671551fed28dbdcfb379e86)) 247 | 248 | ### Bug Fixes 249 | 250 | - **lintstage:** applying eslint only to js to avoid conflicts with changelog ([4914b10](https://github.com/bee-queue/arena/commit/4914b1042738a6365b09b2fe81364ad81b4c2af3)) 251 | 252 | ## [3.19.0](https://github.com/bee-queue/arena/compare/v3.18.0...v3.19.0) (2021-04-05) 253 | 254 | ### Features 255 | 256 | - **bull:** add button to promote delayed job ([73031dd](https://github.com/bee-queue/arena/commit/73031dd8e9b59821e07c2da32ddaa638bcf722cf)) 257 | 258 | ### Bug Fixes 259 | 260 | - merge conflicts ([4484f3e](https://github.com/bee-queue/arena/commit/4484f3e81aac311f36f1b96fe0a6c256ee89380c)) 261 | - solve merge conflicts ([1a5661c](https://github.com/bee-queue/arena/commit/1a5661c8b6d2da272b6335681abd451eb970102c)) 262 | 263 | ## [3.18.0](https://github.com/bee-queue/arena/compare/v3.17.1...v3.18.0) (2021-04-05) 264 | 265 | ### Features 266 | 267 | - **customjspath:** customize layout by custom script ([b5e3651](https://github.com/bee-queue/arena/commit/b5e3651be5974aba783cb6d834c4c159baa1953a)) 268 | 269 | ### Bug Fixes 270 | 271 | - **bull:** link reference ([04e87f2](https://github.com/bee-queue/arena/commit/04e87f28c0081a18ef62aebe6607c4c212efe389)) 272 | - merge conflicts ([1ce7788](https://github.com/bee-queue/arena/commit/1ce778833ba8638afbfb57af4a33b43e6ae25d6c)) 273 | - merge conflicts ([fabdae3](https://github.com/bee-queue/arena/commit/fabdae3fff6f8123f0b0c97f96a2e35923cd06c9)) 274 | 275 | ### [3.17.1](https://github.com/bee-queue/arena/compare/v3.17.0...v3.17.1) (2021-04-05) 276 | 277 | ### Bug Fixes 278 | 279 | - fixes misplaced parameters ([4b98628](https://github.com/bee-queue/arena/commit/4b986281786be13d6c7dda89d24776298edbf6b2)) 280 | 281 | ## [3.17.0](https://github.com/bee-queue/arena/compare/v3.16.0...v3.17.0) (2021-03-31) 282 | 283 | ### Features 284 | 285 | - simpler labels ([653bc7c](https://github.com/bee-queue/arena/commit/653bc7c48c57160d042b351388731285049721df)) 286 | 287 | ### Bug Fixes 288 | 289 | - wrong "execute at" date ([3d0a4d1](https://github.com/bee-queue/arena/commit/3d0a4d14511fc0a3f9a3101a2b94d812eb8f9bb9)) 290 | 291 | ## [3.16.0](https://github.com/bee-queue/arena/compare/v3.15.0...v3.16.0) (2021-03-31) 292 | 293 | ### Features 294 | 295 | - add optional custom css ([3f68dc1](https://github.com/bee-queue/arena/commit/3f68dc11da5a57f6b298825d3118a8b244c60a90)) 296 | 297 | ## [3.15.0](https://github.com/bee-queue/arena/compare/v3.14.0...v3.15.0) (2021-03-12) 298 | 299 | ### Features 300 | 301 | - **bull:** adding log message in bull example ([eb12399](https://github.com/bee-queue/arena/commit/eb123997662adb832c3bceeff41d3de7332f70aa)) 302 | 303 | ### Bug Fixes 304 | 305 | - **queuejobsbystate:** bring logs only in job page ([8ebd5c0](https://github.com/bee-queue/arena/commit/8ebd5c04bfc10bba4d2b4d814cef1663d05e070a)) 306 | 307 | ## [3.14.0](https://github.com/bee-queue/arena/compare/v3.13.0...v3.14.0) (2021-03-10) 308 | 309 | ### Features 310 | 311 | - **jobdetails:** adding executes at detail ([2e88919](https://github.com/bee-queue/arena/commit/2e88919d81c07f074b5ff8f035bdca5fadcc2225)) 312 | - **jobdetails:** support executes at for bee queue ([03b4932](https://github.com/bee-queue/arena/commit/03b493293c316ae044c6cbb471f0048f0c1308e7)) 313 | 314 | ## [3.13.0](https://github.com/bee-queue/arena/compare/v3.12.0...v3.13.0) (2021-03-08) 315 | 316 | ### Features 317 | 318 | - **jobdetails:** showing processed on and finished on ([48ca96a](https://github.com/bee-queue/arena/commit/48ca96a655f503c294668f6680714208afd9351b)) 319 | 320 | ### Bug Fixes 321 | 322 | - **capitalize:** using passed value to be capitalized ([2d98fee](https://github.com/bee-queue/arena/commit/2d98fee0ebd1eeb7db4a5ab271ae8db8bc2394e8)) 323 | 324 | ## [3.12.0](https://github.com/bee-queue/arena/compare/v3.11.0...v3.12.0) (2021-03-08) 325 | 326 | ### Features 327 | 328 | - better example showing jobs move through states ([7c0bc7c](https://github.com/bee-queue/arena/commit/7c0bc7c8697d20513ebf8314295dd866f61112e7)) 329 | 330 | ## [3.11.0](https://github.com/bee-queue/arena/compare/v3.10.0...v3.11.0) (2021-03-07) 331 | 332 | ### Features 333 | 334 | - **bull:** adding example for failed and completed jobs ([8e1fdbc](https://github.com/bee-queue/arena/commit/8e1fdbc4d493d61b2a6a2e0d585cfb7c82ffc098)) 335 | 336 | ### Bug Fixes 337 | 338 | - **bulkaction:** handling retry logic in bulk ([d396dac](https://github.com/bee-queue/arena/commit/d396dac9bd4588b74599ae8b5e87e7997c08f0b9)) 339 | - **bulkaction:** use queuestate to differentiate logic ([62f72cf](https://github.com/bee-queue/arena/commit/62f72cf14d5a5ef68e59bbdbf1f2ba2e70763f23)) 340 | - **deps:** delete jsoneditor dependency ([17bc341](https://github.com/bee-queue/arena/commit/17bc341deffd10c18ba4a8531d37b74953af90a9)) 341 | 342 | ## [3.10.0](https://github.com/bee-queue/arena/compare/v3.9.0...v3.10.0) (2021-03-02) 343 | 344 | ### Features 345 | 346 | - **bull:** adding bull in example ([da1ad97](https://github.com/bee-queue/arena/commit/da1ad97f6fd5ce765718c10bfec278f830c1f85b)) 347 | - **queuejobsbystate:** retry bulk delayed jobs ([d3eb2bf](https://github.com/bee-queue/arena/commit/d3eb2bf3d2dedbe44f683f172cef121e59a45bca)) 348 | 349 | ### Bug Fixes 350 | 351 | - **bee-queue:** disable retry jobs button for bee-queue ([57dc1d6](https://github.com/bee-queue/arena/commit/57dc1d61100e1f2d6fa7d9a726287c21cd63c201)) 352 | 353 | ## [3.9.0](https://github.com/bee-queue/arena/compare/v3.8.0...v3.9.0) (2021-02-25) 354 | 355 | ### Features 356 | 357 | - add contributing guidelines and working example ([8616383](https://github.com/bee-queue/arena/commit/86163830e3ed7d94c7b48ef21b9c058671ebd8f3)) 358 | 359 | ## [3.8.0](https://github.com/bee-queue/arena/compare/v3.7.1...v3.8.0) (2021-02-22) 360 | 361 | ### Features 362 | 363 | - **queuejobsbystate:** adding order dropdown ([c5d21a0](https://github.com/bee-queue/arena/commit/c5d21a0d4e15cb2444f904d199440707cf8fac6d)) 364 | 365 | ### Bug Fixes 366 | 367 | - **queuejobsbystate:** apply descending ordering for jobs when using bull queue ([1e1f891](https://github.com/bee-queue/arena/commit/1e1f8910bc3499419f4370dd45998df7b9317b8a)) 368 | 369 | ### [3.7.1](https://github.com/bee-queue/arena/compare/v3.7.0...v3.7.1) (2021-02-18) 370 | 371 | ### Bug Fixes 372 | 373 | - **dashboard:** change shouldHide condition ([5722b55](https://github.com/bee-queue/arena/commit/5722b551be4b7142806d39cc4c4297eb19cd3f13)) 374 | - **dashboard:** refresh page when adding a new job ([0fa5d02](https://github.com/bee-queue/arena/commit/0fa5d02ec03a53909de5519b0a2aa8f5f38065de)) 375 | 376 | ## [3.7.0](https://github.com/bee-queue/arena/compare/v3.6.1...v3.7.0) (2020-12-16) 377 | 378 | ### Features 379 | 380 | - **deps:** remove dependency on `handlebars-helpers` ([#302](https://github.com/bee-queue/arena/issues/302)) ([bbacae8](https://github.com/bee-queue/arena/commit/bbacae8d4af5e8f157e992d926b43ccb947e4015)) 381 | 382 | ### [3.6.1](https://github.com/bee-queue/arena/compare/v3.6.0...v3.6.1) (2020-11-26) 383 | 384 | ### Bug Fixes 385 | 386 | - support redis configuration with bullmq ([#294](https://github.com/bee-queue/arena/issues/294)) ([ab4b806](https://github.com/bee-queue/arena/commit/ab4b806308abec5d1824ee9b44f71bafcf8c6e3a)) 387 | 388 | ## [3.6.0](https://github.com/bee-queue/arena/compare/v3.5.0...v3.6.0) (2020-11-25) 389 | 390 | ### Features 391 | 392 | - support bullmq in docker image ([c10a294](https://github.com/bee-queue/arena/commit/c10a29448de701fece6efeac3d82d577d1683701)), closes [bee-queue/docker-arena#50](https://github.com/bee-queue/docker-arena/issues/50) 393 | 394 | ## [3.5.0](https://github.com/bee-queue/arena/compare/v3.4.0...v3.5.0) (2020-11-21) 395 | 396 | ### Features 397 | 398 | - **job-details:** support arenaName display field ([332fb3a](https://github.com/bee-queue/arena/commit/332fb3af0d6a3af802b5e2f52ceaaa2f6fa7613f)) 399 | 400 | ## [3.4.0](https://github.com/bee-queue/arena/compare/v3.3.3...v3.4.0) (2020-11-01) 401 | 402 | ### Features 403 | 404 | - **bullmq:** initial support for bullmq ([#251](https://github.com/bee-queue/arena/issues/251)) ([1159dde](https://github.com/bee-queue/arena/commit/1159dde1223259c21d260ba4491026f6020e367f)) 405 | 406 | ### [3.3.3](https://github.com/bee-queue/arena/compare/v3.3.2...v3.3.3) (2020-10-29) 407 | 408 | ### Bug Fixes 409 | 410 | - **job-details:** actually correctly wait for promises ([#271](https://github.com/bee-queue/arena/issues/271)) ([6e205a6](https://github.com/bee-queue/arena/commit/6e205a6a3efd4e56347fb6351b61f69755e598d9)) 411 | 412 | ### [3.3.2](https://github.com/bee-queue/arena/compare/v3.3.1...v3.3.2) (2020-10-29) 413 | 414 | ### Bug Fixes 415 | 416 | - **job-details:** correctly wait for promises ([#254](https://github.com/bee-queue/arena/issues/254)) ([934e92a](https://github.com/bee-queue/arena/commit/934e92ab840fdd63e1d88b1584447f493bd10e94)) 417 | 418 | ### [3.3.1](https://github.com/bee-queue/arena/compare/v3.3.0...v3.3.1) (2020-10-28) 419 | 420 | ### Bug Fixes 421 | 422 | - **deps:** use correct bootstrap css ([#266](https://github.com/bee-queue/arena/issues/266)) ([a5a5e23](https://github.com/bee-queue/arena/commit/a5a5e23b7e8d775b245f3b226790600d07d73650)) 423 | 424 | ## [3.3.0](https://github.com/bee-queue/arena/compare/v3.2.4...v3.3.0) (2020-10-28) 425 | 426 | ### Features 427 | 428 | - **job-details:** show stacktraces when job is delayed or done ([#238](https://github.com/bee-queue/arena/issues/238)) ([6b3dd6f](https://github.com/bee-queue/arena/commit/6b3dd6f3117cdd8296c4eec9c2b39da90acea77e)) 429 | 430 | ### [3.2.4](https://github.com/bee-queue/arena/compare/v3.2.3...v3.2.4) (2020-10-28) 431 | 432 | ### Bug Fixes 433 | 434 | - **security:** upgrade jquery and bootstrap ([#253](https://github.com/bee-queue/arena/issues/253)) ([14b317b](https://github.com/bee-queue/arena/commit/14b317b956f099fc6c1d2fdc3719abbdcfc87925)) 435 | - revert jQuery upgrade ([#252](https://github.com/bee-queue/arena/issues/252)) ([2a268c3](https://github.com/bee-queue/arena/commit/2a268c3d8fc6a799126669a8fdd77815b0dc72e8)) 436 | 437 | ### [3.2.3](https://github.com/bee-queue/arena/compare/v3.2.2...v3.2.3) (2020-10-17) 438 | 439 | ### Bug Fixes 440 | 441 | - **security:** upgrade jQuery to v3.5.1 ([#249](https://github.com/bee-queue/arena/issues/249)) ([c124a47](https://github.com/bee-queue/arena/commit/c124a472cfcceeb2e53502219b51d5bb0a69c2e4)) 442 | 443 | ### [3.2.2](https://github.com/bee-queue/arena/compare/v3.2.1...v3.2.2) (2020-08-06) 444 | 445 | ### Bug Fixes 446 | 447 | - **retry-job:** include job name ([#235](https://github.com/bee-queue/arena/issues/235)) ([c145fe5](https://github.com/bee-queue/arena/commit/c145fe5c91eaec90a3694fc2fa1d105c470b11e4)) 448 | 449 | ### [3.2.2](https://github.com/bee-queue/arena/compare/v3.2.1...v3.2.2) (2020-08-06) 450 | 451 | ### Bug Fixes 452 | 453 | - **retry-job:** include job name ([#235](https://github.com/bee-queue/arena/issues/235)) ([c145fe5](https://github.com/bee-queue/arena/commit/c145fe5c91eaec90a3694fc2fa1d105c470b11e4)) 454 | 455 | ### [3.2.1](https://github.com/bee-queue/arena/compare/v3.2.0...v3.2.1) (2020-08-05) 456 | 457 | ### Bug Fixes 458 | 459 | - **queue-view:** improve handling of falsy job.id values ([9415643](https://github.com/bee-queue/arena/commit/94156434685bc02e0119d1233220246d153180d9)), closes [#181](https://github.com/bee-queue/arena/issues/181) 460 | 461 | ## [3.2.0](https://github.com/bee-queue/arena/compare/v3.1.0...v3.2.0) (2020-08-05) 462 | 463 | ### Features 464 | 465 | - **add-job:** improve named job support ([#209](https://github.com/bee-queue/arena/issues/209)) ([1c131a9](https://github.com/bee-queue/arena/commit/1c131a9d143bdf8c2ef3c7bc2c1353b6f62346b1)) 466 | 467 | ## [3.1.0](https://github.com/bee-queue/arena/compare/v3.0.2...v3.1.0) (2020-08-05) 468 | 469 | ### Features 470 | 471 | - **add-job:** add jsoneditor code and text modes ([#217](https://github.com/bee-queue/arena/issues/217)) ([63ca0c8](https://github.com/bee-queue/arena/commit/63ca0c8623486a2b665d33de8bda5ff437f8f2ab)) 472 | - **job-details:** show raw progress when not numeric ([bd0d697](https://github.com/bee-queue/arena/commit/bd0d6970b6cf046c898569c8f0acbee88dfd642a)) 473 | 474 | ### [3.0.2](https://github.com/bee-queue/arena/compare/v3.0.1...v3.0.2) (2020-08-05) 475 | 476 | ### Bug Fixes 477 | 478 | - use normal require path for defaultConfig ([#196](https://github.com/bee-queue/arena/issues/196)) ([533f702](https://github.com/bee-queue/arena/commit/533f702079dced5986cc69d245e8a337acb6e657)) 479 | 480 | ### [3.0.1](https://github.com/bee-queue/arena/compare/v3.0.0...v3.0.1) (2020-08-05) 481 | 482 | ### Bug Fixes 483 | 484 | - improve error message for no queues ([b8f2afc](https://github.com/bee-queue/arena/commit/b8f2afc3a9a9e5f8802a8dc4147139e52eb6128a)) 485 | 486 | ## [3.0.0](https://github.com/bee-queue/arena/compare/v2.8.2...v3.0.0) (2020-08-05) 487 | 488 | ### ⚠ BREAKING CHANGES 489 | 490 | - all users must now pass in the queue constructor(s) 491 | for the configuration. 492 | 493 | ### Features 494 | 495 | - remove direct application execution support ([95ecf42](https://github.com/bee-queue/arena/commit/95ecf420f360c270d8db623ecad9d572e3891f4f)) 496 | - remove explicit queue dependencies ([ba190a4](https://github.com/bee-queue/arena/commit/ba190a480f1f2139380cee09f91a77aea0f7e926)) 497 | 498 | ## Release History 499 | 500 | - 2.8.2 501 | 502 | - [Fix] Move nodemon to dev dependencies and update (#184) - thanks @Jimmysh! 503 | - [Fix] Encode url for the action 'add job' (#194) - thanks @pluschnikow! 504 | - [Fix] Fix job retry (#223) - thanks @roychri! 505 | 506 | - 2.8.1 Fix bull queue job publishing 507 | 508 | - 2.8.0 Add ability to run jobs on demand (#211) - thanks @bvallelunga! 509 | 510 | - 2.7.1 Fix add job functionality (#197) - thanks @bogdan! 511 | 512 | - 2.7.0 Job logs show up for bull queue jobs (#201) - thanks @ganeshcse2991! 513 | 514 | - 2.6.4 Fix circular dependency issue when viewing failed jobs (#183) - thanks @ghmeier! 515 | 516 | - 2.6.3 Pull in handlebars security advisory patch (#168) - thanks @pklingem! 517 | 518 | - 2.6.2 Fix "add job" vendor/API path when basePath is set (#157) - thanks, @jacobpgn 519 | 520 | - 2.6.1 Hot patch: commit /vendor assets to fix new UI. 521 | 522 | - 2.6.0 Add the ability to add jobs via Arena (#55/#153) - thanks, @krazyjakee! 523 | 524 | - 2.5.4 Upgrade handlerbars-helpers to fix flagged vulnerability (#151) - thanks, @eeVoskos! 525 | 526 | - 2.5.3 Fix `navbar` reference (#146) - thanks @anurag-rai! 527 | 528 | - 2.5.2 Support custom job IDs in arena (#126) - thanks @gcox! 529 | 530 | - 2.5.1 Upgrade nodemon to avoid the vulnerable event-stream (#136) 531 | 532 | - 2.5.0 Support redis over TLS. (#122) - thanks @timcosta! 533 | 534 | - 2.4.5 Allow the package to be once again installed using Yarn (#99) 535 | 536 | - 2.4.4 deyarn 537 | 538 | - 2.4.3 Fix progress indicator for Bill 3.x https://github.com/bee-queue/arena/pull/96 539 | 540 | - 2.4.2 Fix XSS issue https://github.com/bee-queue/arena/pull/84 (thanks @ohwillie) 541 | 542 | - 2.4.1 Fix regression where 'url' parameter wasn't respected ([#85](https://github.com/bee-queue/arena/pull/85) - @ohwillie) 543 | 544 | - 2.4.0 Custom Redis configurations and documentation improvements ([#81](https://github.com/bee-queue/arena/pull/81) - @vhf) 545 | 546 | - 2.3.1 UI improvement: [add syntax highlighting to JSON](https://github.com/bee-queue/arena/pull/80) - thanks @emhagman! 547 | 548 | - 2.3.0 Upgraded Bull to v3.3.7 549 | 550 | - 2.2.2 Include name in description per [#74](https://github.com/bee-queue/arena/pull/74). 551 | 552 | - 2.2.1 Fixed links in interface 553 | 554 | - 2.2.0 Added `uri` coneection parameter. 555 | 556 | - 2.1.3 Fixed issue where [progress bar didn't work in Bull](https://github.com/bee-queue/arena/pull/46) in Bull 557 | 558 | - 2.1.2 Fixed issue where [paging wasn't working](https://github.com/bee-queue/arena/issues/39) in Bull 559 | 560 | - 2.0.0 Added support for [Bee Queue](https://github.com/bee-queue/bee-queue) 561 | 562 | - 1.0.0 Initial release 563 | --------------------------------------------------------------------------------