├── .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 |
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 | Host
8 | Name
9 |
10 |
11 | {{#each flows}}
12 |
13 | {{ this.hostId }}
14 | {{ this.name }}
15 |
16 |
17 | {{/each}}
18 |
19 |
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 | Host
8 | Name
9 |
10 |
11 | {{#each queues}}
12 |
13 | {{ this.hostId }}
14 | {{ this.name }}
15 |
16 | {{/each}}
17 |
18 |
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 |
19 |
20 |
26 |
32 |
38 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Redis Statistics
56 |
57 |
58 |
59 | {{#each stats}}
60 |
61 | {{ @key }}
62 | {{ this }}
63 |
64 | {{/each}}
65 |
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 |
38 |
39 |
49 |
50 |
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 |
7 | Resume queue
8 |
9 | {{else}}
10 |
12 | Pause queue
13 |
14 | {{/if}}
15 | {{/unless}}
16 |
17 | {{#if queueIsBullMQ}}
18 | {{#if hasRateLimitTtl}}
19 |
21 | Remove Rate Limit Key
22 |
23 | {{/if}}
24 | {{/if}}
25 |
26 |
27 |
28 |
29 |
30 |
Add Job
31 |
Job Types
32 |
33 |
80 |
81 |
82 |
83 |
84 |
85 |
Redis Statistics
86 |
87 |
88 |
89 | {{#each stats}}
90 |
91 | {{ @key }}
92 | {{ this }}
93 |
94 | {{/each}}
95 |
96 |
97 |
98 | {{#if globalConfig}}
99 |
100 |
101 |
Update
102 |
Global Configuration
103 |
104 |
105 |
106 |
117 |
128 |
139 |
140 | Update
141 |
142 |
143 |
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 |
131 |
132 |
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 |
13 | Remove
14 |
15 |
16 | {{#if showRetryButton}}
17 |
19 | {{ retryButtonText }}
20 |
21 | {{/if}}
22 |
23 | {{#if showPromoteButton}}
24 |
26 | Promote
27 |
28 | {{/if}}
29 |
30 | {{#if showDeleteRepeatableButton}}
31 |
33 | Remove Repeatable
34 |
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 |
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 |
183 |
184 |
185 |
186 | {{/if}}
187 | {{/unless}}
188 |
189 | {{#if this.queue.IS_BULLMQ}}
190 | {{#if this.parent }}
191 |
199 | {{/if}}
200 |
201 | {{#if this.unprocessedChildren }}
202 |
203 |
204 |
Unprocessed Children {{ this.countDependencies.unprocessed}}
205 |
206 |
207 |
215 |
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 |
232 |
240 |
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 |
257 |
265 |
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 | [](https://www.npmjs.com/package/bull-arena) [](https://github.com/prettier/prettier) [](https://www.npmjs.com/package/bull-arena) [](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.png) [](screenshots/screen2.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 |
--------------------------------------------------------------------------------