diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 2df602f4..e01b055c 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- node: [ '10.x', '12.x', '14.x' ]
+ node: [ '12.x', '14.x' ]
os: [ubuntu-latest]
steps:
@@ -25,12 +25,18 @@ jobs:
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1
+ with:
+ install-command: yarn --frozen-lockfile --silent
- name: Lint
run: yarn lint
- - name: Test
- run: yarn test --ci --coverage --maxWorkers=2
-
- name: Build
run: yarn build
+ env:
+ CI: true
+
+ - name: Test
+ run: yarn test
+ env:
+ CI: true
diff --git a/.gitignore b/.gitignore
index ff1e3e5b..d0c5a9eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
node_modules/
-static/
+
dist
yarn-error.log
*.rdb
diff --git a/README.md b/README.md
index 0abdbf8f..0fff898d 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@ With this library you get a beautiful UI for visualizing what's happening with e
-![UI](https://raw.githubusercontent.com/vcapretz/bull-board/master/shot.png)
-![Fails](https://raw.githubusercontent.com/vcapretz/bull-board/master/fails.png)
+![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/shot.png)
+![Fails](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/fails.png)
## Notes
@@ -34,13 +34,21 @@ If you want to learn more about queues and Redis: https://redis.io/.
To add it to your project start by adding the library to your dependencies list:
```sh
-yarn add bull-board
+yarn add @bull-board/express
+# or
+yarn add @bull-board/fastify
+# or
+yarn add @bull-board/hapi
```
Or
```sh
-npm i bull-board
+npm i @bull-board/express
+# or
+npm i @bull-board/fastify
+# or
+npm i @bull-board/hapi
```
## Hello World
@@ -51,23 +59,30 @@ The first step is to setup `bull-board` by calling `createBullBoard` method.
const express = require('express')
const Queue = require('bull')
const QueueMQ = require('bullmq')
-const { createBullBoard } = require('bull-board')
-const { BullAdapter } = require('bull-board/bullAdapter')
-const { BullMQAdapter } = require('bull-board/bullMQAdapter')
+const { createBullBoard } = require('@bull-board/api')
+const { BullAdapter } = require('@bull-board/api/bullAdapter')
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter')
+const { ExpressAdapter } = require('@bull-board/express')
const someQueue = new Queue('someQueueName')
const someOtherQueue = new Queue('someOtherQueueName')
const queueMQ = new QueueMQ('queueMQName')
-const { router, setQueues, replaceQueues, addQueue, removeQueue } = createBullBoard([
- new BullAdapter(someQueue),
- new BullAdapter(someOtherQueue),
- new BullMQAdapter(queueMQ),
-])
+const serverAdapter = new ExpressAdapter();
+
+const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard({
+ queues: [
+ new BullAdapter(someQueue),
+ new BullAdapter(someOtherQueue),
+ new BullMQAdapter(queueMQ),
+ ],
+ serverAdapter:serverAdapter
+})
const app = express()
-app.use('/admin/queues', router)
+serverAdapter.setBasePath('/admin/queues')
+app.use('/admin/queues', serverAdapter.getRouter());
// other configurations of your server
```
@@ -78,6 +93,8 @@ That's it! Now you can access the `/admin/queues` route, and you will be able to
For more advanced usages check the `examples` folder, currently it contains:
1. [Basic authentication example](https://github.com/felixmosh/bull-board/tree/master/examples/with-auth)
2. [Multiple instance of the board](https://github.com/felixmosh/bull-board/tree/master/examples/with-multiple-instances)
+2. [With Fastify server](https://github.com/felixmosh/bull-board/tree/master/examples/with-fastify)
+2. [With Hapi.js server](https://github.com/felixmosh/bull-board/tree/master/examples/with-hapi)
### Queue options
1. `readOnlyMode` (default: `false`)
Makes the UI as read only, hides all queue & job related actions
@@ -85,19 +102,21 @@ Makes the UI as read only, hides all queue & job related actions
```js
const Queue = require('bull')
const QueueMQ = require('bullmq')
-const { setQueues } = require('bull-board')
-const { BullMQAdapter } = require('bull-board/bullMQAdapter')
-const { BullAdapter } = require('bull-board/bullAdapter')
+const { createBullBoard } = require('@bull-board/api')
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter')
+const { BullAdapter } = require('@bull-board/api/bullAdapter')
const someQueue = new Queue()
const someOtherQueue = new Queue()
const queueMQ = new QueueMQ()
-const { router, setQueues, replaceQueues } = createBullBoard([
- new BullAdapter(someQueue, { readOnlyMode: true }), // only this queue will be in read only mode
- new BullAdapter(someOtherQueue),
- new BullMQAdapter(queueMQ, { readOnlyMode: true }),
-])
+const { setQueues, replaceQueues } = createBullBoard({
+ queues: [
+ new BullAdapter(someQueue, { readOnlyMode: true }), // only this queue will be in read only mode
+ new BullAdapter(someOtherQueue),
+ new BullMQAdapter(queueMQ, { readOnlyMode: true }),
+ ]
+})
```
### Hosting router on a sub path
@@ -106,26 +125,27 @@ If you host your express service on a different path than root (/) ie. https://<
```js
const Queue = require('bull')
-const { createBullBoard } = require('bull-board')
-const { BullAdapter } = require('bull-board/bullAdapter')
+const { createBullBoard } = require('@bull-board/api')
+const { BullAdapter } = require('@bull-board/api/bullAdapter')
+const { ExpressAdapter } = require('@bull-board/express')
const someQueue = new Queue('someQueueName')
-const { router } = createBullBoard([
- new BullAdapter(someQueue),
-])
+const serverAdapter = new ExpressAdapter();
+
+const { router } = createBullBoard({
+ queues: [
+ new BullAdapter(someQueue),
+ ],
+ serverAdapter
+})
// ... express server configuration
-let basePath = 'my-base-path';
+const basePath = 'my-base-path';
+serverAdapter.setBasePath(basePath)
-app.use(
- '/queues',
- (req, res, next) => {
- req.proxyUrl = basePath + '/queues';
- next();
- },
- router);
+app.use('/queues', serverAdapter.getRouter());
```
You will then find the bull-board UI at the following address `https:///my-base-path/queues`.
@@ -134,11 +154,11 @@ You will then find the bull-board UI at the following address `https:// {
});
});
- const { router: bullBoardRouter } = createBullBoard([
- new BullMQAdapter(exampleBullMq),
- new BullAdapter(exampleBull),
- ]);
+ const serverAdapter = new ExpressAdapter();
+ serverAdapter.setBasePath('/ui');
- app.use('/ui', bullBoardRouter);
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq), new BullAdapter(exampleBull)],
+ serverAdapter,
+ });
+
+ app.use('/ui', serverAdapter.getRouter());
app.listen(3000, () => {
console.log('Running on 3000...');
diff --git a/examples/with-auth/README.md b/examples/with-express-auth/README.md
similarity index 100%
rename from examples/with-auth/README.md
rename to examples/with-express-auth/README.md
diff --git a/examples/with-auth/index.js b/examples/with-express-auth/index.js
similarity index 54%
rename from examples/with-auth/index.js
rename to examples/with-express-auth/index.js
index 40c693dc..fc63c6cd 100644
--- a/examples/with-auth/index.js
+++ b/examples/with-express-auth/index.js
@@ -1,12 +1,13 @@
-const { createBullBoard } = require('bull-board')
-const { BullMQAdapter } = require('bull-board/bullMQAdapter')
-const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq')
-const session = require('express-session')
-const bodyParser = require('body-parser')
-const passport = require('passport')
-const LocalStrategy = require('passport-local').Strategy
-const { ensureLoggedIn } = require('connect-ensure-login')
-const express = require('express')
+const { createBullBoard } = require('@bull-board/api');
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
+const { ExpressAdapter } = require('@bull-board/express');
+const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq');
+const session = require('express-session');
+const bodyParser = require('body-parser');
+const passport = require('passport');
+const LocalStrategy = require('passport-local').Strategy;
+const { ensureLoggedIn } = require('connect-ensure-login');
+const express = require('express');
// Configure the local strategy for use by Passport.
//
@@ -17,11 +18,11 @@ const express = require('express')
passport.use(
new LocalStrategy(function (username, password, cb) {
if (username === 'bull' && password === 'board') {
- return cb(null, { user: 'bull-board' })
+ return cb(null, { user: 'bull-board' });
}
- return cb(null, false)
- }),
-)
+ return cb(null, false);
+ })
+);
// Configure Passport authenticated session persistence.
//
@@ -31,101 +32,110 @@ passport.use(
// serializing, and querying the user record by ID from the database when
// deserializing.
passport.serializeUser((user, cb) => {
- cb(null, user)
-})
+ cb(null, user);
+});
passport.deserializeUser((user, cb) => {
- cb(null, user)
-})
+ cb(null, user);
+});
-const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000))
+const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000));
const redisOptions = {
port: 6379,
host: 'localhost',
password: '',
tls: false,
-}
+};
-const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions })
+const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions });
async function setupBullMQProcessor(queueName) {
const queueScheduler = new QueueScheduler(queueName, {
connection: redisOptions,
- })
- await queueScheduler.waitUntilReady()
+ });
+ await queueScheduler.waitUntilReady();
new Worker(queueName, async (job) => {
for (let i = 0; i <= 100; i++) {
- await sleep(Math.random())
- await job.updateProgress(i)
- await job.log(`Processing job at interval ${i}`)
+ await sleep(Math.random());
+ await job.updateProgress(i);
+ await job.log(`Processing job at interval ${i}`);
- if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`)
+ if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`);
}
- return { jobId: `This is the return value of job (${job.id})` }
- })
+ return { jobId: `This is the return value of job (${job.id})` };
+ });
}
const run = async () => {
- const exampleBullMq = createQueueMQ('ExampleBullMQ')
- const { router: bullBoardRouter } = createBullBoard([
- new BullMQAdapter(exampleBullMq),
- ])
+ const exampleBullMq = createQueueMQ('ExampleBullMQ');
+
+ const serverAdapter = new ExpressAdapter();
+ serverAdapter.setBasePath('/ui');
+
+ const { router: bullBoardRouter } = createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq)],
+ serverAdapter,
+ });
- await setupBullMQProcessor(exampleBullMq.name)
+ await setupBullMQProcessor(exampleBullMq.name);
- const app = express()
+ const app = express();
// Configure view engine to render EJS templates.
- app.set('views', __dirname + '/views')
- app.set('view engine', 'ejs')
+ app.set('views', __dirname + '/views');
+ app.set('view engine', 'ejs');
- app.use(session({ secret: 'keyboard cat' }))
- app.use(bodyParser.urlencoded({ extended: false }))
+ app.use(session({ secret: 'keyboard cat' }));
+ app.use(bodyParser.urlencoded({ extended: false }));
// Initialize Passport and restore authentication state, if any, from the session.
- app.use(passport.initialize({}))
- app.use(passport.session({}))
+ app.use(passport.initialize({}));
+ app.use(passport.session({}));
app.get('/ui/login', (req, res) => {
- res.render('login')
- })
+ res.render('login');
+ });
app.post(
'/ui/login',
passport.authenticate('local', { failureRedirect: '/ui/login' }),
(req, res) => {
- res.redirect('/ui')
- },
- )
+ res.redirect('/ui');
+ }
+ );
app.use('/add', (req, res) => {
- const opts = req.query.opts || {}
+ const opts = req.query.opts || {};
if (opts.delay) {
- opts.delay = +opts.delay * 1000 // delay must be a number
+ opts.delay = +opts.delay * 1000; // delay must be a number
}
- exampleBullMq.add('Add', { title: req.query.title }, opts)
+ exampleBullMq.add('Add', { title: req.query.title }, opts);
res.json({
ok: true,
- })
- })
+ });
+ });
- app.use('/ui', ensureLoggedIn({ redirectTo: '/ui/login' }), bullBoardRouter)
+ app.use(
+ '/ui',
+ ensureLoggedIn({ redirectTo: '/ui/login' }),
+ serverAdapter.getRouter()
+ );
app.listen(3000, () => {
- console.log('Running on 3000...')
- console.log('For the UI, open http://localhost:3000/ui')
- console.log('Make sure Redis is running on port 6379 by default')
- console.log('To populate the queue, run:')
- console.log(' curl http://localhost:3000/add?title=Example')
- console.log('To populate the queue with custom options (opts), run:')
- console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9')
- })
-}
+ console.log('Running on 3000...');
+ console.log('For the UI, open http://localhost:3000/ui');
+ console.log('Make sure Redis is running on port 6379 by default');
+ console.log('To populate the queue, run:');
+ console.log(' curl http://localhost:3000/add?title=Example');
+ console.log('To populate the queue with custom options (opts), run:');
+ console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9');
+ });
+};
// eslint-disable-next-line no-console
-run().catch((e) => console.error(e))
+run().catch((e) => console.error(e));
diff --git a/examples/with-auth/package.json b/examples/with-express-auth/package.json
similarity index 93%
rename from examples/with-auth/package.json
rename to examples/with-express-auth/package.json
index c5bd1711..697e070b 100644
--- a/examples/with-auth/package.json
+++ b/examples/with-express-auth/package.json
@@ -10,8 +10,8 @@
"author": "felixmosh",
"license": "ISC",
"dependencies": {
+ "@bull-board/express": "^3.0.0",
"body-parser": "^1.19.0",
- "bull-board": "^2.0.0",
"bullmq": "^1.24.4",
"connect-ensure-login": "^0.1.1",
"express": "^4.17.1",
diff --git a/examples/with-auth/views/login.ejs b/examples/with-express-auth/views/login.ejs
similarity index 100%
rename from examples/with-auth/views/login.ejs
rename to examples/with-express-auth/views/login.ejs
diff --git a/examples/with-express-auth/yarn.lock b/examples/with-express-auth/yarn.lock
new file mode 100644
index 00000000..e7619aad
--- /dev/null
+++ b/examples/with-express-auth/yarn.lock
@@ -0,0 +1,604 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/ioredis@^4.22.2":
+ version "4.26.1"
+ resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.1.tgz#761f1812c48a3ecfbd9d9ecd35c49849f8693e2e"
+ integrity sha512-k9f+bda6y0ZNrwUMNYI9mqIPqjPZ4Jqc+jTRTUyhFz8aD8cHQBk+uenTKCZj9RhdfrU4sSqrot5sn5LqkAHODw==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*":
+ version "14.14.12"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.12.tgz#0b1d86f8c40141091285dea02e4940df73bba43f"
+ integrity sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g==
+
+accepts@~1.3.7:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
+ integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+ dependencies:
+ mime-types "~2.1.24"
+ negotiator "0.6.2"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+body-parser@1.19.0, body-parser@^1.19.0:
+ version "1.19.0"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
+ integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+ dependencies:
+ bytes "3.1.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ on-finished "~2.3.0"
+ qs "6.7.0"
+ raw-body "2.4.0"
+ type-is "~1.6.17"
+
+bullmq@^1.24.4:
+ version "1.24.4"
+ resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.24.4.tgz#cd1ac152362bd473d012abf67ccfb30a36206711"
+ integrity sha512-A2sARsokRi9+9YQLXLL2j1P1ULHrLsyKx/9UBHz48HsQAyJ36vpyQEU7measZ4+foLwfD/J1yItFvwgHhxJ+0w==
+ dependencies:
+ "@types/ioredis" "^4.22.2"
+ cron-parser "^2.7.3"
+ get-port "^5.0.0"
+ ioredis "^4.25.0"
+ lodash "^4.17.11"
+ semver "^6.3.0"
+ tslib "^1.10.0"
+ uuid "^8.2.0"
+
+bytes@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+ integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cluster-key-slot@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
+ integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
+
+connect-ensure-login@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz#174dcc51243b9eac23f8d98215aeb6694e2e8a12"
+ integrity sha1-F03MUSQ7nqwj+NmCFa62aU4uihI=
+
+content-disposition@0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
+ integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+ dependencies:
+ safe-buffer "5.1.2"
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
+ integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+
+cron-parser@^2.7.3:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf"
+ integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==
+ dependencies:
+ is-nan "^1.3.0"
+ moment-timezone "^0.5.31"
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+denque@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
+ integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+depd@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+express-session@^1.17.1:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
+ integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==
+ dependencies:
+ cookie "0.4.0"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~2.0.0"
+ on-headers "~1.0.2"
+ parseurl "~1.3.3"
+ safe-buffer "5.2.0"
+ uid-safe "~2.1.5"
+
+express@^4.17.1:
+ version "4.17.1"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
+ integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+ dependencies:
+ accepts "~1.3.7"
+ array-flatten "1.1.1"
+ body-parser "1.19.0"
+ content-disposition "0.5.3"
+ content-type "~1.0.4"
+ cookie "0.4.0"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.2"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "~1.1.2"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.5"
+ qs "6.7.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.1.2"
+ send "0.17.1"
+ serve-static "1.14.1"
+ setprototypeof "1.1.1"
+ statuses "~1.5.0"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+finalhandler@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+ integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+get-port@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+ integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
+http-errors@1.7.2:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
+ integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+http-errors@~1.7.2:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+ integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.4"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ioredis@^4.25.0:
+ version "4.27.2"
+ resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.2.tgz#6a79bca05164482da796f8fa010bccefd3bf4811"
+ integrity sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg==
+ dependencies:
+ cluster-key-slot "^1.1.0"
+ debug "^4.3.1"
+ denque "^1.1.0"
+ lodash.defaults "^4.2.0"
+ lodash.flatten "^4.4.0"
+ p-map "^2.1.0"
+ redis-commands "1.7.0"
+ redis-errors "^1.2.0"
+ redis-parser "^3.0.0"
+ standard-as-callback "^2.1.0"
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-nan@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
+ integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
+ dependencies:
+ define-properties "^1.1.3"
+
+lodash.defaults@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+ integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.flatten@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+ integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
+lodash@^4.17.11:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+mime-db@1.44.0:
+ version "1.44.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+ integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+
+mime-types@~2.1.24:
+ version "2.1.27"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+ integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+ dependencies:
+ mime-db "1.44.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+moment-timezone@^0.5.31:
+ version "0.5.32"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2"
+ integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0":
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+ integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+negotiator@0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
+ integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+
+object-keys@^1.0.12:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+p-map@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+ integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+passport-local@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
+ integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=
+ dependencies:
+ passport-strategy "1.x.x"
+
+passport-strategy@1.x.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
+ integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
+
+passport@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
+ integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
+ dependencies:
+ passport-strategy "1.x.x"
+ pause "0.0.1"
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+pause@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
+ integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
+
+proxy-addr@~2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
+ integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.9.1"
+
+qs@6.7.0:
+ version "6.7.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
+ integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+
+random-bytes@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
+ integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
+
+range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
+ integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+ dependencies:
+ bytes "3.1.0"
+ http-errors "1.7.2"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+redis-commands@1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
+ integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
+
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+ integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
+
+redis-parser@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+ integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
+ dependencies:
+ redis-errors "^1.0.0"
+
+safe-buffer@5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+ integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+send@0.17.1:
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
+ integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.7.2"
+ mime "1.6.0"
+ ms "2.1.1"
+ on-finished "~2.3.0"
+ range-parser "~1.2.1"
+ statuses "~1.5.0"
+
+serve-static@1.14.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
+ integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.17.1"
+
+setprototypeof@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+ integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+standard-as-callback@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+ integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+toidentifier@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+ integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+tslib@^1.10.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+type-is@~1.6.17, type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+uid-safe@~2.1.5:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
+ integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
+ dependencies:
+ random-bytes "~1.0.0"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@^8.2.0:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
diff --git a/examples/with-express/README.md b/examples/with-express/README.md
new file mode 100644
index 00000000..49dcfb06
--- /dev/null
+++ b/examples/with-express/README.md
@@ -0,0 +1,4 @@
+# Express example
+
+This example shows how to use [Express.js](https://expressjs.com/) as a server for bull-board.
+
diff --git a/examples/with-express/index.js b/examples/with-express/index.js
new file mode 100644
index 00000000..4fdd5121
--- /dev/null
+++ b/examples/with-express/index.js
@@ -0,0 +1,80 @@
+const { createBullBoard } = require('@bull-board/api');
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
+const { ExpressAdapter } = require('@bull-board/express');
+const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq');
+const express = require('express');
+
+const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000));
+
+const redisOptions = {
+ port: 6379,
+ host: 'localhost',
+ password: '',
+ tls: false,
+};
+
+const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions });
+
+async function setupBullMQProcessor(queueName) {
+ const queueScheduler = new QueueScheduler(queueName, {
+ connection: redisOptions,
+ });
+ await queueScheduler.waitUntilReady();
+
+ new Worker(queueName, async (job) => {
+ for (let i = 0; i <= 100; i++) {
+ await sleep(Math.random());
+ await job.updateProgress(i);
+ await job.log(`Processing job at interval ${i}`);
+
+ if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`);
+ }
+
+ return { jobId: `This is the return value of job (${job.id})` };
+ });
+}
+
+const run = async () => {
+ const exampleBullMq = createQueueMQ('BullMQ');
+
+ await setupBullMQProcessor(exampleBullMq.name);
+
+ const app = express();
+
+ const serverAdapter = new ExpressAdapter();
+ serverAdapter.setBasePath('/ui');
+
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq)],
+ serverAdapter,
+ });
+
+ app.use('/ui', serverAdapter.getRouter());
+
+ app.use('/add', (req, res) => {
+ const opts = req.query.opts || {};
+
+ if (opts.delay) {
+ opts.delay = +opts.delay * 1000; // delay must be a number
+ }
+
+ exampleBullMq.add('Add', { title: req.query.title }, opts);
+
+ res.json({
+ ok: true,
+ });
+ });
+
+ app.listen(3000, () => {
+ console.log('Running on 3000...');
+ console.log('For the UI of instance1, open http://localhost:3000/ui');
+ console.log('Make sure Redis is running on port 6379 by default');
+ console.log('To populate the queue, run:');
+ console.log(' curl http://localhost:3000/add?title=Example');
+ console.log('To populate the queue with custom options (opts), run:');
+ console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9');
+ });
+};
+
+// eslint-disable-next-line no-console
+run().catch((e) => console.error(e));
diff --git a/examples/with-express/package.json b/examples/with-express/package.json
new file mode 100644
index 00000000..ee8c8163
--- /dev/null
+++ b/examples/with-express/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "bull-board-express-example",
+ "version": "1.0.0",
+ "description": "Example of how to install bull-board on existing Express server",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "felixmosh",
+ "license": "ISC",
+ "dependencies": {
+ "@bull-board/express": "^3.0.0",
+ "bullmq": "^1.24.4",
+ "express": "^4.17.1"
+ }
+}
diff --git a/examples/with-auth/yarn.lock b/examples/with-express/yarn.lock
similarity index 90%
rename from examples/with-auth/yarn.lock
rename to examples/with-express/yarn.lock
index b2c578e1..30133925 100644
--- a/examples/with-auth/yarn.lock
+++ b/examples/with-express/yarn.lock
@@ -36,13 +36,20 @@
"@types/qs" "*"
"@types/serve-static" "*"
-"@types/ioredis@^4.22.2", "@types/ioredis@^4.22.3":
+"@types/ioredis@^4.22.2":
version "4.26.1"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.1.tgz#761f1812c48a3ecfbd9d9ecd35c49849f8693e2e"
integrity sha512-k9f+bda6y0ZNrwUMNYI9mqIPqjPZ4Jqc+jTRTUyhFz8aD8cHQBk+uenTKCZj9RhdfrU4sSqrot5sn5LqkAHODw==
dependencies:
"@types/node" "*"
+"@types/ioredis@^4.22.3":
+ version "4.26.0"
+ resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.0.tgz#7d171d013f52de9475d5bdbe9d8005dd0898be3e"
+ integrity sha512-lxF+O7a5lu08g8rmPTAlD105SorTbu1A3jZdH8CIra0eh3lmpqkRkJ3FAYFPTAXSlnE549xqbQIo1BLzx5smNg==
+ dependencies:
+ "@types/node" "*"
+
"@types/mime@*":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
@@ -187,11 +194,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-connect-ensure-login@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz#174dcc51243b9eac23f8d98215aeb6694e2e8a12"
- integrity sha1-F03MUSQ7nqwj+NmCFa62aU4uihI=
-
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@@ -253,11 +255,6 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
-depd@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
- integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -295,20 +292,6 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
-express-session@^1.17.1:
- version "1.17.1"
- resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
- integrity sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==
- dependencies:
- cookie "0.4.0"
- cookie-signature "1.0.6"
- debug "2.6.9"
- depd "~2.0.0"
- on-headers "~1.0.2"
- parseurl "~1.3.3"
- safe-buffer "5.2.0"
- uid-safe "~2.1.5"
-
express@4.17.1, express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -560,11 +543,6 @@ on-finished@~2.3.0:
dependencies:
ee-first "1.1.1"
-on-headers@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
- integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
-
p-map@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@@ -575,36 +553,11 @@ parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-passport-local@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
- integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=
- dependencies:
- passport-strategy "1.x.x"
-
-passport-strategy@1.x.x:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
- integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
-
-passport@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
- integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
- dependencies:
- passport-strategy "1.x.x"
- pause "0.0.1"
-
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
-pause@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
- integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
-
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@@ -618,11 +571,6 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
-random-bytes@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
- integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
-
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -667,11 +615,6 @@ safe-buffer@5.1.2:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
- integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
-
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -751,13 +694,6 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
-uid-safe@~2.1.5:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
- integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
- dependencies:
- random-bytes "~1.0.0"
-
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
diff --git a/examples/with-fastify/README.md b/examples/with-fastify/README.md
new file mode 100644
index 00000000..dc919a7a
--- /dev/null
+++ b/examples/with-fastify/README.md
@@ -0,0 +1,4 @@
+# Fastify example
+
+This example shows how to use [Fastify.js](https://www.fastify.io/) as a server for bull-board.
+
diff --git a/examples/with-fastify/index.js b/examples/with-fastify/index.js
new file mode 100644
index 00000000..427eb652
--- /dev/null
+++ b/examples/with-fastify/index.js
@@ -0,0 +1,83 @@
+const { createBullBoard } = require('@bull-board/api');
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
+const { FastifyAdapter } = require('@bull-board/fastify');
+const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq');
+const fastify = require('fastify');
+
+const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000));
+
+const redisOptions = {
+ port: 6379,
+ host: 'localhost',
+ password: '',
+ tls: false,
+};
+
+const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions });
+
+async function setupBullMQProcessor(queueName) {
+ const queueScheduler = new QueueScheduler(queueName, {
+ connection: redisOptions,
+ });
+ await queueScheduler.waitUntilReady();
+
+ new Worker(queueName, async (job) => {
+ for (let i = 0; i <= 100; i++) {
+ await sleep(Math.random());
+ await job.updateProgress(i);
+ await job.log(`Processing job at interval ${i}`);
+
+ if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`);
+ }
+
+ return { jobId: `This is the return value of job (${job.id})` };
+ });
+}
+
+const run = async () => {
+ const exampleBullMq = createQueueMQ('BullMQ');
+
+ await setupBullMQProcessor(exampleBullMq.name);
+
+ const app = fastify();
+
+ const serverAdapter = new FastifyAdapter();
+
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq)],
+ serverAdapter,
+ });
+
+ serverAdapter.setBasePath('/ui');
+ app.register(serverAdapter.registerPlugin(), { prefix: '/ui' });
+
+ app.get('/add', (req, reply) => {
+ const opts = req.query.opts || {};
+
+ if (opts.delay) {
+ opts.delay = +opts.delay * 1000; // delay must be a number
+ }
+
+ exampleBullMq.add('Add', { title: req.query.title }, opts);
+
+ reply.send({
+ ok: true,
+ });
+ });
+
+ await app.listen(3000);
+ // eslint-disable-next-line no-console
+ console.log('Running on 3000...');
+ console.log('For the UI of instance1, open http://localhost:3000/ui');
+ console.log('Make sure Redis is running on port 6379 by default');
+ console.log('To populate the queue, run:');
+ console.log(' curl http://localhost:3000/add?title=Example');
+ console.log('To populate the queue with custom options (opts), run:');
+ console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9');
+};
+
+run().catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ process.exit(1);
+});
diff --git a/examples/with-fastify/package.json b/examples/with-fastify/package.json
new file mode 100644
index 00000000..261a3c0c
--- /dev/null
+++ b/examples/with-fastify/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "bull-board-with-fastify",
+ "version": "1.0.0",
+ "description": "Example of how to use Fastify server with bull-board",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "felixmosh",
+ "license": "ISC",
+ "dependencies": {
+ "@bull-board/fastify": "^3.0.0",
+ "bullmq": "^1.24.4"
+ }
+}
diff --git a/examples/with-fastify/yarn.lock b/examples/with-fastify/yarn.lock
new file mode 100644
index 00000000..d02c7f79
--- /dev/null
+++ b/examples/with-fastify/yarn.lock
@@ -0,0 +1,522 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@fastify/ajv-compiler@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz#5ce80b1fc8bebffc8c5ba428d5e392d0f9ed10a1"
+ integrity sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==
+ dependencies:
+ ajv "^6.12.6"
+
+"@fastify/forwarded@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@fastify/forwarded/-/forwarded-1.0.0.tgz#cc4a3bc1f02856e56e67d6d655026e8d8c2e7429"
+ integrity sha512-VoO+6WD0aRz8bwgJZ8pkkxjq7o/782cQ1j945HWg0obZMgIadYW3Pew0+an+k1QL7IPZHM3db5WF6OP6x4ymMA==
+
+"@fastify/proxy-addr@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@fastify/proxy-addr/-/proxy-addr-3.0.0.tgz#7d3bb6474a01b206010329291f4edf6af8af582d"
+ integrity sha512-ty7wnUd/GeSqKTC2Jozsl5xGbnxUnEFC0On2/zPv/8ixywipQmVZwuWvNGnBoitJ2wixwVqofwXNua8j6Y62lQ==
+ dependencies:
+ "@fastify/forwarded" "^1.0.0"
+ ipaddr.js "^2.0.0"
+
+"@types/ioredis@^4.22.2":
+ version "4.26.4"
+ resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.4.tgz#a2b1ed51ddd2c707d7eaac5017cc34a0fe51558a"
+ integrity sha512-QFbjNq7EnOGw6d1gZZt2h26OFXjx7z+eqEnbCHSrDI1OOLEgOHMKdtIajJbuCr9uO+X9kQQRe7Lz6uxqxl5XKg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*":
+ version "15.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.1.tgz#32d43390d5c62c5b6ec486a9bc9c59544de39a08"
+ integrity sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==
+
+abstract-logging@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839"
+ integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==
+
+ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.6:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+archy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+ integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
+
+atomic-sleep@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
+ integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
+avvio@^7.1.2:
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/avvio/-/avvio-7.2.2.tgz#58e00e7968870026cd7b7d4f689d596db629e251"
+ integrity sha512-XW2CMCmZaCmCCsIaJaLKxAzPwF37fXi1KGxNOvedOpeisLdmxZnblGc3hpHWYnlP+KOUxZsazh43WXNHgXpbqw==
+ dependencies:
+ archy "^1.0.0"
+ debug "^4.0.0"
+ fastq "^1.6.1"
+ queue-microtask "^1.1.2"
+
+bullmq@^1.24.4:
+ version "1.28.0"
+ resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.28.0.tgz#f90f30cb6a326cdcb5e34d3a72830e2a56315050"
+ integrity sha512-1EjaZTaurJ4E9FYrDEdEyuotfG8lAATignQrpl6VtXSaElwN9TWNQR2G1EwNq2X0TqFKHINqkW52z68v7ZG33Q==
+ dependencies:
+ "@types/ioredis" "^4.22.2"
+ cron-parser "^2.7.3"
+ get-port "^5.0.0"
+ ioredis "^4.25.0"
+ lodash "^4.17.21"
+ semver "^6.3.0"
+ tslib "^1.10.0"
+ uuid "^8.2.0"
+
+call-bind@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
+cluster-key-slot@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
+ integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
+
+cookie@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
+cron-parser@^2.7.3:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf"
+ integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==
+ dependencies:
+ is-nan "^1.3.0"
+ moment-timezone "^0.5.31"
+
+debug@^4.0.0, debug@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
+deepmerge@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+ integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+denque@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
+ integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
+
+fast-decode-uri-component@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
+ integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-json-stringify@^2.5.2:
+ version "2.7.5"
+ resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.5.tgz#fdd348d204ba8fc81b3ab4bb947d10a50d953e48"
+ integrity sha512-VClYNkPo7tyZr0BMrRWraDMTJwjH6dIaHc/b/BiA4Z2MpxpKZBu45akYVb0dOVwQbF22zUMmhdg1WjrUjzAN2g==
+ dependencies:
+ ajv "^6.11.0"
+ deepmerge "^4.2.2"
+ rfdc "^1.2.0"
+ string-similarity "^4.0.1"
+
+fast-redact@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.1.tgz#d6015b971e933d03529b01333ba7f22c29961e92"
+ integrity sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw==
+
+fast-safe-stringify@^2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
+ integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
+
+fastify-error@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2"
+ integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==
+
+fastify-warning@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/fastify-warning/-/fastify-warning-0.2.0.tgz#e717776026a4493dc9a2befa44db6d17f618008f"
+ integrity sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==
+
+fastify@^3.16.2:
+ version "3.16.2"
+ resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.16.2.tgz#be26789fe9bac588bcbd5ae3ff6d1dc06cd53f00"
+ integrity sha512-tdu0fz6wk9AbtD91AbzZGjKgEQLcIy7rT2vEzTUL/zifAMS/L7ViKY9p9k3g3yCRnIQzYzxH2RAbvYZaTbKasw==
+ dependencies:
+ "@fastify/ajv-compiler" "^1.0.0"
+ "@fastify/proxy-addr" "^3.0.0"
+ abstract-logging "^2.0.0"
+ avvio "^7.1.2"
+ fast-json-stringify "^2.5.2"
+ fastify-error "^0.3.0"
+ fastify-warning "^0.2.0"
+ find-my-way "^4.0.0"
+ flatstr "^1.0.12"
+ light-my-request "^4.2.0"
+ pino "^6.2.1"
+ readable-stream "^3.4.0"
+ rfdc "^1.1.4"
+ secure-json-parse "^2.0.0"
+ semver "^7.3.2"
+ tiny-lru "^7.0.0"
+
+fastq@^1.6.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
+ integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
+ dependencies:
+ reusify "^1.0.4"
+
+find-my-way@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-4.1.0.tgz#e70aa10b3670cc8be96eb251357705644ea5087b"
+ integrity sha512-UBD94MdO6cBi6E97XA0fBA9nwqw+xG5x1TYIPHats33gEi/kNqy7BWHAWx8QHCQQRSU5Txc0JiD8nzba39gvMQ==
+ dependencies:
+ fast-decode-uri-component "^1.0.1"
+ fast-deep-equal "^3.1.3"
+ safe-regex2 "^2.0.0"
+ semver-store "^0.3.0"
+
+flatstr@^1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931"
+ integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-intrinsic@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+ integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+
+get-port@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+ integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
+has-symbols@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+ integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+inherits@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ioredis@^4.25.0:
+ version "4.27.3"
+ resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.3.tgz#4a020c9056bf4e016c476910fb59620a0d899654"
+ integrity sha512-eAirtUIljFkHJwuKQhbGajVrdCUMNKRuOrhzRFeYZRvXnLs4757Oss1S8aiheB4NSO1RsLeG+2RUjY/0/XiSig==
+ dependencies:
+ cluster-key-slot "^1.1.0"
+ debug "^4.3.1"
+ denque "^1.1.0"
+ lodash.defaults "^4.2.0"
+ lodash.flatten "^4.4.0"
+ p-map "^2.1.0"
+ redis-commands "1.7.0"
+ redis-errors "^1.2.0"
+ redis-parser "^3.0.0"
+ standard-as-callback "^2.1.0"
+
+ipaddr.js@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.0.tgz#77ccccc8063ae71ab65c55f21b090698e763fc6e"
+ integrity sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w==
+
+is-nan@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+ integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+light-my-request@^4.2.0:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-4.4.1.tgz#bfa2220316eef4f6465bf2f0667780da6b7f6a71"
+ integrity sha512-FDNRF2mYjthIRWE7O8d/X7AzDx4otQHl4/QXbu3Q/FRwBFcgb+ZoDaUd5HwN53uQXLAiw76osN+Va0NEaOW6rQ==
+ dependencies:
+ ajv "^6.12.2"
+ cookie "^0.4.0"
+ fastify-warning "^0.2.0"
+ readable-stream "^3.6.0"
+ set-cookie-parser "^2.4.1"
+
+lodash.defaults@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+ integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.flatten@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+ integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
+lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+moment-timezone@^0.5.31:
+ version "0.5.33"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c"
+ integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0":
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+ integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+object-keys@^1.0.12:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+p-map@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+ integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+pino-std-serializers@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671"
+ integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==
+
+pino@^6.2.1:
+ version "6.11.3"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-6.11.3.tgz#0c02eec6029d25e6794fdb6bbea367247d74bc29"
+ integrity sha512-drPtqkkSf0ufx2gaea3TryFiBHdNIdXKf5LN0hTM82SXI4xVIve2wLwNg92e1MT6m3jASLu6VO7eGY6+mmGeyw==
+ dependencies:
+ fast-redact "^3.0.0"
+ fast-safe-stringify "^2.0.7"
+ flatstr "^1.0.12"
+ pino-std-serializers "^3.1.0"
+ quick-format-unescaped "^4.0.3"
+ sonic-boom "^1.0.2"
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+queue-microtask@^1.1.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-format-unescaped@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652"
+ integrity sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg==
+
+readable-stream@^3.4.0, readable-stream@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+redis-commands@1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
+ integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
+
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+ integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
+
+redis-parser@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+ integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
+ dependencies:
+ redis-errors "^1.0.0"
+
+ret@~0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
+ integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rfdc@^1.1.4, rfdc@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
+ integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
+
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-regex2@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9"
+ integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==
+ dependencies:
+ ret "~0.2.0"
+
+secure-json-parse@^2.0.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85"
+ integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==
+
+semver-store@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9"
+ integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==
+
+semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^7.3.2:
+ version "7.3.5"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+ integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+ dependencies:
+ lru-cache "^6.0.0"
+
+set-cookie-parser@^2.4.1:
+ version "2.4.8"
+ resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2"
+ integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==
+
+sonic-boom@^1.0.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e"
+ integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ flatstr "^1.0.12"
+
+standard-as-callback@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+ integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
+string-similarity@^4.0.1:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
+ integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+tiny-lru@^7.0.0:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24"
+ integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==
+
+tslib@^1.10.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+uuid@^8.2.0:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
diff --git a/examples/with-hapi/README.md b/examples/with-hapi/README.md
new file mode 100644
index 00000000..b44dc79c
--- /dev/null
+++ b/examples/with-hapi/README.md
@@ -0,0 +1,4 @@
+# Hapi example
+
+This example shows how to use [Hapi.js](https://hapi.dev/) as a server for bull-board.
+
diff --git a/examples/with-hapi/index.js b/examples/with-hapi/index.js
new file mode 100644
index 00000000..3cdd016e
--- /dev/null
+++ b/examples/with-hapi/index.js
@@ -0,0 +1,92 @@
+const { createBullBoard } = require('@bull-board/api');
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
+const { HapiAdapter } = require('@bull-board/hapi');
+const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq');
+const Hapi = require('@hapi/hapi');
+
+const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000));
+
+const redisOptions = {
+ port: 6379,
+ host: 'localhost',
+ password: '',
+ tls: false,
+};
+
+const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions });
+
+async function setupBullMQProcessor(queueName) {
+ const queueScheduler = new QueueScheduler(queueName, {
+ connection: redisOptions,
+ });
+ await queueScheduler.waitUntilReady();
+
+ new Worker(queueName, async (job) => {
+ for (let i = 0; i <= 100; i++) {
+ await sleep(Math.random());
+ await job.updateProgress(i);
+ await job.log(`Processing job at interval ${i}`);
+
+ if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`);
+ }
+
+ return { jobId: `This is the return value of job (${job.id})` };
+ });
+}
+
+const run = async () => {
+ const exampleBullMq = createQueueMQ('BullMQ');
+
+ await setupBullMQProcessor(exampleBullMq.name);
+
+ const app = Hapi.server({
+ port: 3000,
+ host: 'localhost',
+ });
+
+ const serverAdapter = new HapiAdapter();
+
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq)],
+ serverAdapter,
+ });
+
+ serverAdapter.setBasePath('/ui');
+ await app.register(serverAdapter.registerPlugin(), {
+ routes: { prefix: '/ui' },
+ });
+
+ app.route({
+ method: 'GET',
+ path: '/add',
+ handler: (req) => {
+ const opts = req.query.opts || {};
+
+ if (opts.delay) {
+ opts.delay = +opts.delay * 1000; // delay must be a number
+ }
+
+ exampleBullMq.add('Add', { title: req.query.title }, opts);
+
+ return {
+ ok: true,
+ };
+ },
+ });
+
+ await app.start();
+ // eslint-disable-next-line no-console
+ console.log('Running on 3000...');
+ console.log('For the UI of instance1, open http://localhost:3000/ui');
+ console.log('Make sure Redis is running on port 6379 by default');
+ console.log('To populate the queue, run:');
+ console.log(' curl http://localhost:3000/add?title=Example');
+ console.log('To populate the queue with custom options (opts), run:');
+ console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9');
+};
+
+run().catch((e) => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ process.exit(1);
+});
diff --git a/examples/with-hapi/package.json b/examples/with-hapi/package.json
new file mode 100644
index 00000000..7fb72430
--- /dev/null
+++ b/examples/with-hapi/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "bull-board-with-hapi",
+ "version": "1.0.0",
+ "description": "Example of how to use Hapi.js server with bull-board",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "felixmosh",
+ "license": "ISC",
+ "dependencies": {
+ "@bull-board/hapi": "^3.0.0",
+ "@hapi/hapi": "^20.1.3",
+ "bullmq": "^1.24.4"
+ }
+}
diff --git a/examples/with-hapi/yarn.lock b/examples/with-hapi/yarn.lock
new file mode 100644
index 00000000..79ee84db
--- /dev/null
+++ b/examples/with-hapi/yarn.lock
@@ -0,0 +1,458 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@hapi/accept@^5.0.1":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
+ integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/ammo@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490"
+ integrity sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/b64@5.x.x":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d"
+ integrity sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/boom@9.x.x", "@hapi/boom@^9.1.0":
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.2.tgz#48bd41d67437164a2d636e3b5bc954f8c8dc5e38"
+ integrity sha512-uJEJtiNHzKw80JpngDGBCGAmWjBtzxDCz17A9NO2zCi8LLBlb5Frpq4pXwyN+2JQMod4pKz5BALwyneCgDg89Q==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d"
+ integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/bourne@2.x.x":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
+ integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==
+
+"@hapi/call@^8.0.0":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0"
+ integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/catbox-memory@^5.0.0":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz#cb63fca0ded01d445a2573b38eb2688df67f70ac"
+ integrity sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/catbox@^11.1.1":
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-11.1.1.tgz#d277e2d5023fd69cddb33d05b224ea03065fec0c"
+ integrity sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/podium" "4.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/content@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/content/-/content-5.0.2.tgz#ae57954761de570392763e64cdd75f074176a804"
+ integrity sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+
+"@hapi/cryptiles@5.x.x":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43"
+ integrity sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+
+"@hapi/file@2.x.x":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9"
+ integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==
+
+"@hapi/hapi@^20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.1.3.tgz#f4d000e429769d61df59c39cc66dfa252192eff6"
+ integrity sha512-ImOkrixD1kPUuvmSklwytPQ0sG8AtqydwU0JzvITLE6Z7wPMVf9i9LIMWywKPxHTNhCZ6W3oKP9yRjqM/IkHMQ==
+ dependencies:
+ "@hapi/accept" "^5.0.1"
+ "@hapi/ammo" "^5.0.1"
+ "@hapi/boom" "^9.1.0"
+ "@hapi/bounce" "^2.0.0"
+ "@hapi/call" "^8.0.0"
+ "@hapi/catbox" "^11.1.1"
+ "@hapi/catbox-memory" "^5.0.0"
+ "@hapi/heavy" "^7.0.1"
+ "@hapi/hoek" "^9.0.4"
+ "@hapi/mimos" "^6.0.0"
+ "@hapi/podium" "^4.1.1"
+ "@hapi/shot" "^5.0.5"
+ "@hapi/somever" "^3.0.0"
+ "@hapi/statehood" "^7.0.3"
+ "@hapi/subtext" "^7.0.3"
+ "@hapi/teamwork" "^5.1.0"
+ "@hapi/topo" "^5.0.0"
+ "@hapi/validate" "^1.1.1"
+
+"@hapi/heavy@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-7.0.1.tgz#73315ae33b6e7682a0906b7a11e8ca70e3045874"
+ integrity sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4":
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
+ integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
+
+"@hapi/iron@6.x.x":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-6.0.0.tgz#ca3f9136cda655bdd6028de0045da0de3d14436f"
+ integrity sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==
+ dependencies:
+ "@hapi/b64" "5.x.x"
+ "@hapi/boom" "9.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/cryptiles" "5.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/mimos@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484"
+ integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+ mime-db "1.x.x"
+
+"@hapi/nigel@4.x.x":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-4.0.2.tgz#8f84ef4bca4fb03b2376463578f253b0b8e863c4"
+ integrity sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==
+ dependencies:
+ "@hapi/hoek" "^9.0.4"
+ "@hapi/vise" "^4.0.0"
+
+"@hapi/pez@^5.0.1":
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.0.3.tgz#b75446e6fef8cbb16816573ab7da1b0522e7a2a1"
+ integrity sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==
+ dependencies:
+ "@hapi/b64" "5.x.x"
+ "@hapi/boom" "9.x.x"
+ "@hapi/content" "^5.0.2"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/nigel" "4.x.x"
+
+"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1":
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26"
+ integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+ "@hapi/teamwork" "5.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/shot@^5.0.5":
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd"
+ integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/somever@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.1.tgz#9961cd5bdbeb5bb1edc0b2acdd0bb424066aadcc"
+ integrity sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==
+ dependencies:
+ "@hapi/bounce" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/statehood@^7.0.3":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.3.tgz#655166f3768344ed3c3b50375a303cdeca8040d9"
+ integrity sha512-pYB+pyCHkf2Amh67QAXz7e/DN9jcMplIL7Z6N8h0K+ZTy0b404JKPEYkbWHSnDtxLjJB/OtgElxocr2fMH4G7w==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bounce" "2.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/cryptiles" "5.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/iron" "6.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/subtext@^7.0.3":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-7.0.3.tgz#f7440fc7c966858e1f39681e99eb6171c71e7abd"
+ integrity sha512-CekDizZkDGERJ01C0+TzHlKtqdXZxzSWTOaH6THBrbOHnsr3GY+yiMZC+AfNCypfE17RaIakGIAbpL2Tk1z2+A==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/content" "^5.0.2"
+ "@hapi/file" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/pez" "^5.0.1"
+ "@hapi/wreck" "17.x.x"
+
+"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.0.tgz#7801a61fc727f702fd2196ef7625eb4e389f4124"
+ integrity sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg==
+
+"@hapi/topo@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
+ integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
+ dependencies:
+ "@hapi/hoek" "^9.0.0"
+
+"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad"
+ integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==
+ dependencies:
+ "@hapi/hoek" "^9.0.0"
+ "@hapi/topo" "^5.0.0"
+
+"@hapi/vise@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-4.0.0.tgz#c6a94fe121b94a53bf99e7489f7fcc74c104db02"
+ integrity sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/wreck@17.x.x":
+ version "17.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.1.0.tgz#fbdc380c6f3fa1f8052dc612b2d3b6ce3e88dbec"
+ integrity sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@types/ioredis@^4.22.2":
+ version "4.26.4"
+ resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.4.tgz#a2b1ed51ddd2c707d7eaac5017cc34a0fe51558a"
+ integrity sha512-QFbjNq7EnOGw6d1gZZt2h26OFXjx7z+eqEnbCHSrDI1OOLEgOHMKdtIajJbuCr9uO+X9kQQRe7Lz6uxqxl5XKg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*":
+ version "15.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.1.tgz#32d43390d5c62c5b6ec486a9bc9c59544de39a08"
+ integrity sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==
+
+bullmq@^1.24.4:
+ version "1.28.0"
+ resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.28.0.tgz#f90f30cb6a326cdcb5e34d3a72830e2a56315050"
+ integrity sha512-1EjaZTaurJ4E9FYrDEdEyuotfG8lAATignQrpl6VtXSaElwN9TWNQR2G1EwNq2X0TqFKHINqkW52z68v7ZG33Q==
+ dependencies:
+ "@types/ioredis" "^4.22.2"
+ cron-parser "^2.7.3"
+ get-port "^5.0.0"
+ ioredis "^4.25.0"
+ lodash "^4.17.21"
+ semver "^6.3.0"
+ tslib "^1.10.0"
+ uuid "^8.2.0"
+
+call-bind@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+ integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ dependencies:
+ function-bind "^1.1.1"
+ get-intrinsic "^1.0.2"
+
+cluster-key-slot@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
+ integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
+
+cron-parser@^2.7.3:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf"
+ integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==
+ dependencies:
+ is-nan "^1.3.0"
+ moment-timezone "^0.5.31"
+
+debug@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+denque@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
+ integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-intrinsic@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+ integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+
+get-port@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+ integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
+has-symbols@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+ integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+ioredis@^4.25.0:
+ version "4.27.3"
+ resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.3.tgz#4a020c9056bf4e016c476910fb59620a0d899654"
+ integrity sha512-eAirtUIljFkHJwuKQhbGajVrdCUMNKRuOrhzRFeYZRvXnLs4757Oss1S8aiheB4NSO1RsLeG+2RUjY/0/XiSig==
+ dependencies:
+ cluster-key-slot "^1.1.0"
+ debug "^4.3.1"
+ denque "^1.1.0"
+ lodash.defaults "^4.2.0"
+ lodash.flatten "^4.4.0"
+ p-map "^2.1.0"
+ redis-commands "1.7.0"
+ redis-errors "^1.2.0"
+ redis-parser "^3.0.0"
+ standard-as-callback "^2.1.0"
+
+is-nan@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+ integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+
+lodash.defaults@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+ integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.flatten@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+ integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
+lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+mime-db@1.x.x:
+ version "1.47.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
+ integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
+
+moment-timezone@^0.5.31:
+ version "0.5.33"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c"
+ integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0":
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+ integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+object-keys@^1.0.12:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+p-map@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+ integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+redis-commands@1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
+ integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
+
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+ integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
+
+redis-parser@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+ integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
+ dependencies:
+ redis-errors "^1.0.0"
+
+semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+standard-as-callback@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+ integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
+tslib@^1.10.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+uuid@^8.2.0:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
diff --git a/examples/with-multiple-instances/index.js b/examples/with-multiple-instances/index.js
index 9644d354..e3b9089c 100644
--- a/examples/with-multiple-instances/index.js
+++ b/examples/with-multiple-instances/index.js
@@ -1,87 +1,100 @@
-const { createBullBoard } = require('bull-board')
-const { BullMQAdapter } = require('bull-board/bullMQAdapter')
-const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq')
-const express = require('express')
+const { createBullBoard } = require('@bull-board/api');
+const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter');
+const { ExpressAdapter } = require('@bull-board/express');
+const { Queue: QueueMQ, Worker, QueueScheduler } = require('bullmq');
+const express = require('express');
-const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000))
+const sleep = (t) => new Promise((resolve) => setTimeout(resolve, t * 1000));
const redisOptions = {
port: 6379,
host: 'localhost',
password: '',
tls: false,
-}
+};
-const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions })
+const createQueueMQ = (name) => new QueueMQ(name, { connection: redisOptions });
async function setupBullMQProcessor(queueName) {
const queueScheduler = new QueueScheduler(queueName, {
connection: redisOptions,
- })
- await queueScheduler.waitUntilReady()
+ });
+ await queueScheduler.waitUntilReady();
new Worker(queueName, async (job) => {
for (let i = 0; i <= 100; i++) {
- await sleep(Math.random())
- await job.updateProgress(i)
- await job.log(`Processing job at interval ${i}`)
+ await sleep(Math.random());
+ await job.updateProgress(i);
+ await job.log(`Processing job at interval ${i}`);
- if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`)
+ if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`);
}
- return { jobId: `This is the return value of job (${job.id})` }
- })
+ return { jobId: `This is the return value of job (${job.id})` };
+ });
}
const run = async () => {
- const exampleBullMq = createQueueMQ('BullMQ - instance1')
- const exampleBullMq2 = createQueueMQ('BullMQ - instance2')
+ const exampleBullMq = createQueueMQ('BullMQ - instance1');
+ const exampleBullMq2 = createQueueMQ('BullMQ - instance2');
- await setupBullMQProcessor(exampleBullMq.name)
- await setupBullMQProcessor(exampleBullMq2.name)
+ await setupBullMQProcessor(exampleBullMq.name);
+ await setupBullMQProcessor(exampleBullMq2.name);
- const app = express()
+ const app = express();
// Configure view engine to render EJS templates.
- app.set('views', __dirname + '/views')
- app.set('view engine', 'ejs')
+ app.set('views', __dirname + '/views');
+ app.set('view engine', 'ejs');
+
+ const serverAdapter1 = new ExpressAdapter();
+ const serverAdapter2 = new ExpressAdapter();
- const { router: instance1Router } = createBullBoard([
- new BullMQAdapter(exampleBullMq),
- ])
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq)],
+ serverAdapter: serverAdapter1,
+ });
- const { router: instance2Router } = createBullBoard([
- new BullMQAdapter(exampleBullMq2),
- ])
+ createBullBoard({
+ queues: [new BullMQAdapter(exampleBullMq2)],
+ serverAdapter: serverAdapter2,
+ });
- app.use('/instance1', instance1Router)
- app.use('/instance2', instance2Router)
+ serverAdapter1.setBasePath('/instance1');
+ serverAdapter2.setBasePath('/instance2');
+
+ app.use('/instance1', serverAdapter1.getRouter());
+ app.use('/instance2', serverAdapter2.getRouter());
app.use('/add', (req, res) => {
- const opts = req.query.opts || {}
+ const opts = req.query.opts || {};
if (opts.delay) {
- opts.delay = +opts.delay * 1000 // delay must be a number
+ opts.delay = +opts.delay * 1000; // delay must be a number
}
- exampleBullMq.add('Add instance 1', { title: req.query.title }, opts)
- exampleBullMq2.add('Add instance 2', { title: req.query.title }, opts)
+ exampleBullMq.add('Add instance 1', { title: req.query.title }, opts);
+ exampleBullMq2.add('Add instance 2', { title: req.query.title }, opts);
res.json({
ok: true,
- })
- })
+ });
+ });
app.listen(3000, () => {
- console.log('Running on 3000...')
- console.log('For the UI of instance1, open http://localhost:3000/instance1')
- console.log('For the UI of instance2, open http://localhost:3000/instance2')
- console.log('Make sure Redis is running on port 6379 by default')
- console.log('To populate the queue, run:')
- console.log(' curl http://localhost:3000/add?title=Example')
- console.log('To populate the queue with custom options (opts), run:')
- console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9')
- })
-}
+ console.log('Running on 3000...');
+ console.log(
+ 'For the UI of instance1, open http://localhost:3000/instance1'
+ );
+ console.log(
+ 'For the UI of instance2, open http://localhost:3000/instance2'
+ );
+ console.log('Make sure Redis is running on port 6379 by default');
+ console.log('To populate the queue, run:');
+ console.log(' curl http://localhost:3000/add?title=Example');
+ console.log('To populate the queue with custom options (opts), run:');
+ console.log(' curl http://localhost:3000/add?title=Test&opts[delay]=9');
+ });
+};
// eslint-disable-next-line no-console
-run().catch((e) => console.error(e))
+run().catch((e) => console.error(e));
diff --git a/examples/with-multiple-instances/package.json b/examples/with-multiple-instances/package.json
index fad823bc..7dc85991 100644
--- a/examples/with-multiple-instances/package.json
+++ b/examples/with-multiple-instances/package.json
@@ -11,7 +11,7 @@
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
- "bull-board": "^2.0.0",
+ "@bull-board/express": "^3.0.0",
"bullmq": "^1.24.4",
"express": "^4.17.1"
}
diff --git a/examples/with-multiple-instances/yarn.lock b/examples/with-multiple-instances/yarn.lock
index 30133925..01cca1a2 100644
--- a/examples/with-multiple-instances/yarn.lock
+++ b/examples/with-multiple-instances/yarn.lock
@@ -2,40 +2,6 @@
# yarn lockfile v1
-"@types/body-parser@*":
- version "1.19.0"
- resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
- integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
- dependencies:
- "@types/connect" "*"
- "@types/node" "*"
-
-"@types/connect@*":
- version "3.4.34"
- resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
- integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==
- dependencies:
- "@types/node" "*"
-
-"@types/express-serve-static-core@^4.17.14", "@types/express-serve-static-core@^4.17.18":
- version "4.17.18"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40"
- integrity sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==
- dependencies:
- "@types/node" "*"
- "@types/qs" "*"
- "@types/range-parser" "*"
-
-"@types/express@^4.17.9":
- version "4.17.11"
- resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545"
- integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==
- dependencies:
- "@types/body-parser" "*"
- "@types/express-serve-static-core" "^4.17.18"
- "@types/qs" "*"
- "@types/serve-static" "*"
-
"@types/ioredis@^4.22.2":
version "4.26.1"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.1.tgz#761f1812c48a3ecfbd9d9ecd35c49849f8693e2e"
@@ -43,41 +9,11 @@
dependencies:
"@types/node" "*"
-"@types/ioredis@^4.22.3":
- version "4.26.0"
- resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.26.0.tgz#7d171d013f52de9475d5bdbe9d8005dd0898be3e"
- integrity sha512-lxF+O7a5lu08g8rmPTAlD105SorTbu1A3jZdH8CIra0eh3lmpqkRkJ3FAYFPTAXSlnE549xqbQIo1BLzx5smNg==
- dependencies:
- "@types/node" "*"
-
-"@types/mime@*":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
- integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
-
"@types/node@*":
version "14.14.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.12.tgz#0b1d86f8c40141091285dea02e4940df73bba43f"
integrity sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g==
-"@types/qs@*":
- version "6.9.5"
- resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
- integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==
-
-"@types/range-parser@*":
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
- integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
-
-"@types/serve-static@*":
- version "1.13.8"
- resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46"
- integrity sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==
- dependencies:
- "@types/mime" "*"
- "@types/node" "*"
-
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -86,28 +22,11 @@ accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
-ansi-styles@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
- integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
- dependencies:
- color-convert "^1.9.0"
-
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
-async@0.9.x:
- version "0.9.2"
- resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
- integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
-
-balanced-match@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
- integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
-
body-parser@1.19.0, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -124,26 +43,6 @@ body-parser@1.19.0, body-parser@^1.19.0:
raw-body "2.4.0"
type-is "~1.6.17"
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-bull-board@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/bull-board/-/bull-board-2.0.2.tgz#4ed35b1a6fca4eb621575f7b90f3f2e0cd4459e4"
- integrity sha512-dUsovbIXMHMMQ1r5l/CTRWh3bt83hDnG/0FGN7kIfbT4c5JaOa9F1ho4LxC9OrYnIrs3Cq6DS3IlmHiB1KDoRQ==
- dependencies:
- "@types/express" "^4.17.9"
- "@types/express-serve-static-core" "^4.17.14"
- "@types/ioredis" "^4.22.3"
- ejs "3.1.6"
- express "4.17.1"
- redis-info "^3.0.8"
-
bullmq@^1.24.4:
version "1.24.4"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.24.4.tgz#cd1ac152362bd473d012abf67ccfb30a36206711"
@@ -163,37 +62,11 @@ bytes@3.1.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-chalk@^2.4.2:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
- integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
-color-convert@^1.9.0:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
- integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
- dependencies:
- color-name "1.1.3"
-
-color-name@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
- integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@@ -265,13 +138,6 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-ejs@3.1.6:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a"
- integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==
- dependencies:
- jake "^10.6.1"
-
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -282,17 +148,12 @@ escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
-escape-string-regexp@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
- integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
-express@4.17.1, express@^4.17.1:
+express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
@@ -328,13 +189,6 @@ express@4.17.1, express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
-filelist@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
- integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==
- dependencies:
- minimatch "^3.0.4"
-
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -363,11 +217,6 @@ get-port@^5.0.0:
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
-has-flag@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
- integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
-
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@@ -435,16 +284,6 @@ is-nan@^1.3.0:
dependencies:
define-properties "^1.1.3"
-jake@^10.6.1:
- version "10.8.2"
- resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
- integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==
- dependencies:
- async "0.9.x"
- chalk "^2.4.2"
- filelist "^1.0.1"
- minimatch "^3.0.4"
-
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@@ -492,13 +331,6 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
moment-timezone@^0.5.31:
version "0.5.32"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2"
@@ -596,13 +428,6 @@ redis-errors@^1.0.0, redis-errors@^1.2.0:
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
-redis-info@^3.0.8:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/redis-info/-/redis-info-3.0.8.tgz#27e696778c100d716324fa68b92cccf390487e02"
- integrity sha512-L7yPuGzRq+gu+ZYl/aO0TDgc4nNcMpDTaTN4P3bBi8ZENp1fk8gvtZQpidrYL5uAJYMIcMN81fgUz28qUpTeVA==
- dependencies:
- lodash "^4.17.11"
-
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
@@ -669,13 +494,6 @@ standard-as-callback@^2.1.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-supports-color@^5.3.0:
- version "5.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
- integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
- dependencies:
- has-flag "^3.0.0"
-
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
diff --git a/lerna.json b/lerna.json
new file mode 100644
index 00000000..7f26e559
--- /dev/null
+++ b/lerna.json
@@ -0,0 +1,8 @@
+{
+ "version": "3.0.0",
+ "npmClient": "yarn",
+ "packages": [
+ "packages/*"
+ ]
+
+}
diff --git a/package.json b/package.json
index f1c72542..ecaf9603 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,11 @@
{
- "name": "bull-board",
- "version": "2.1.2",
+ "name": "@bull-board/root",
+ "version": "2.0.3",
"description": "Bull queue UI for inspecting jobs",
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ],
"keywords": [
"bull",
"redis",
@@ -19,35 +23,17 @@
"author": "Vitor Capretz ",
"contributors": [
"Erik Engervall ",
- "Felix Mosheev"
- ],
- "main": "dist/index.js",
- "files": [
- "dist/**/*",
- "static/**/*",
- "*Adapter.{js,d.ts}"
+ "felixmosh"
],
"scripts": {
"prepublishOnly": "yarn build",
- "lint": "eslint \"./src/**/*.ts*\"",
- "build": "yarn build:clean && yarn build:ui && yarn build:routes",
- "build:ui": "NODE_ENV=production webpack --mode=production",
- "build:routes": "yarn tsc",
- "build:clean": "rm -rf ./static ./dist",
+ "lint": "eslint \"./packages/**/*.ts*\"",
+ "build": "lerna run build",
"start:dev:docker": "docker-compose up -d",
"start:dev:server": "ts-node-dev --rs example.ts",
- "start:dev:ui": "NODE_ENV=development webpack serve",
- "start:dev": "yarn start:dev:docker && yarn start:dev:server",
- "test": "yarn ts-node tests/dockest",
+ "test": "lerna run test",
"release": "release-it"
},
- "dependencies": {
- "@types/express": "^4.17.12",
- "@types/express-serve-static-core": "^4.17.20",
- "ejs": "3.1.6",
- "express": "4.17.1",
- "redis-info": "^3.0.8"
- },
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-class-properties": "^7.13.0",
@@ -57,61 +43,36 @@
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.14.0",
- "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@types/bull": "^3.15.1",
- "@types/classnames": "^2.2.11",
"@types/ioredis": "^4.26.4",
"@types/jest": "^26.0.23",
"@types/node": "^15.6.1",
- "@types/pretty-bytes": "^5.2.0",
- "@types/react-dom": "^17.0.5",
- "@types/react-highlight": "^0.12.2",
- "@types/react-router-dom": "^5.1.6",
"@types/redis-info": "^3.0.0",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
- "@typescript-eslint/typescript-estree": "^4.25.0",
"auto-changelog": "^2.3.0",
- "axios": "^0.21.1",
"babel-loader": "^8.2.2",
"bull": "^3.22.6",
"bullmq": "^1.28.0",
- "clsx": "^1.1.1",
- "css-loader": "^5.2.6",
- "css-minimizer-webpack-plugin": "^2.0.0",
- "date-fns": "2.21.3",
- "dockest": "2.1.0",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-no-only-tests": "^2.6.0",
"eslint-plugin-react": "^7.21.5",
- "fork-ts-checker-webpack-plugin": "^6.2.10",
- "highlight.js": "^10.7.1",
- "html-webpack-plugin": "^5.3.1",
"jest": "^27.0.1",
- "mini-css-extract-plugin": "^1.6.0",
- "postcss-loader": "^5.3.0",
- "postcss-preset-env": "^6.7.0",
+ "lerna": "^4.0.0",
"prettier": "^2.3.0",
- "pretty-bytes": "5.6.0",
- "react": "17.0.2",
- "react-dev-utils": "^11.0.1",
- "react-dom": "17.0.2",
- "react-refresh": "^0.10.0",
- "react-router-dom": "^5.2.0",
- "react-toastify": "^7.0.4",
"release-it": "^14.7.0",
- "style-loader": "^2.0.0",
+ "release-it-yarn-workspaces": "^2.0.1",
"supertest": "^6.0.1",
"ts-jest": "^27.0.1",
"ts-node-dev": "^1.1.6",
- "typescript": "^4.3.2",
- "webpack": "^5.37.1",
- "webpack-cli": "^4.7.0",
- "webpack-dev-server": "^3.11.0"
+ "typescript": "^4.3.2"
},
"release-it": {
+ "plugins": {
+ "release-it-yarn-workspaces": true
+ },
"git": {
"changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs"
},
@@ -120,6 +81,7 @@
},
"github": {
"release": true
- }
+ },
+ "npm": false
}
}
diff --git a/packages/api/__mocks__/ioredis/index.js b/packages/api/__mocks__/ioredis/index.js
new file mode 100644
index 00000000..fb3c7a84
--- /dev/null
+++ b/packages/api/__mocks__/ioredis/index.js
@@ -0,0 +1 @@
+module.exports = require('ioredis-mock/jest');
diff --git a/bullAdapter.d.ts b/packages/api/bullAdapter.d.ts
similarity index 100%
rename from bullAdapter.d.ts
rename to packages/api/bullAdapter.d.ts
diff --git a/bullAdapter.js b/packages/api/bullAdapter.js
similarity index 100%
rename from bullAdapter.js
rename to packages/api/bullAdapter.js
diff --git a/bullMQAdapter.d.ts b/packages/api/bullMQAdapter.d.ts
similarity index 100%
rename from bullMQAdapter.d.ts
rename to packages/api/bullMQAdapter.d.ts
diff --git a/bullMQAdapter.js b/packages/api/bullMQAdapter.js
similarity index 100%
rename from bullMQAdapter.js
rename to packages/api/bullMQAdapter.js
diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js
new file mode 100644
index 00000000..8a11a31d
--- /dev/null
+++ b/packages/api/jest.config.js
@@ -0,0 +1,15 @@
+const packageJson = require('./package.json');
+
+module.exports = {
+ name: packageJson.name,
+ displayName: packageJson.name,
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ globals: {
+ 'ts-jest': {
+ diagnostics: false, // https://huafu.github.io/ts-jest/user/config/diagnostics
+ },
+ },
+ testPathIgnorePatterns: ['/node_modules/'],
+ testMatch: ['/tests/**/*.spec.ts'],
+};
diff --git a/packages/api/package.json b/packages/api/package.json
new file mode 100644
index 00000000..dd54d477
--- /dev/null
+++ b/packages/api/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@bull-board/api",
+ "version": "3.0.0",
+ "main": "dist/index.js",
+ "author": "felixmosh",
+ "license": "MIT",
+ "scripts": {
+ "build": "tsc",
+ "test": "jest"
+ },
+ "files": [
+ "dist",
+ "typings",
+ "*Adapter.*"
+ ],
+ "dependencies": {
+ "redis-info": "^3.0.8"
+ },
+ "devDependencies": {
+ "@types/bull": "^3.15.1",
+ "@types/node": "^15.6.1",
+ "bullmq": "^1.28.0",
+ "ioredis-mock": "^5.5.7",
+ "redis-mock": "^0.56.3"
+ }
+}
diff --git a/packages/api/src/constants/statuses.ts b/packages/api/src/constants/statuses.ts
new file mode 100644
index 00000000..6d0970a2
--- /dev/null
+++ b/packages/api/src/constants/statuses.ts
@@ -0,0 +1,9 @@
+export const STATUSES = {
+ latest: 'latest',
+ active: 'active',
+ waiting: 'waiting',
+ completed: 'completed',
+ failed: 'failed',
+ delayed: 'delayed',
+ paused: 'paused',
+} as const;
diff --git a/packages/api/src/handlers/cleanAll.ts b/packages/api/src/handlers/cleanAll.ts
new file mode 100644
index 00000000..2cc56a6c
--- /dev/null
+++ b/packages/api/src/handlers/cleanAll.ts
@@ -0,0 +1,24 @@
+import { BaseAdapter } from '../queueAdapters/base';
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+} from '../../typings/app';
+import { queueProvider } from '../providers/queue';
+
+async function cleanAll(
+ req: BullBoardRequest,
+ queue: BaseAdapter
+): Promise {
+ const { queueStatus } = req.params;
+
+ const GRACE_TIME_MS = 5000;
+
+ await queue.clean(queueStatus as any, GRACE_TIME_MS);
+
+ return {
+ status: 200,
+ body: {},
+ };
+}
+
+export const cleanAllHandler = queueProvider(cleanAll);
diff --git a/packages/api/src/handlers/cleanJob.ts b/packages/api/src/handlers/cleanJob.ts
new file mode 100644
index 00000000..7561ecb9
--- /dev/null
+++ b/packages/api/src/handlers/cleanJob.ts
@@ -0,0 +1,21 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+ QueueJob,
+} from '../../typings/app';
+import { jobProvider } from '../providers/job';
+import { queueProvider } from '../providers/queue';
+
+async function cleanJob(
+ _req: BullBoardRequest,
+ job: QueueJob
+): Promise {
+ await job.remove();
+
+ return {
+ status: 204,
+ body: {},
+ };
+}
+
+export const cleanJobHandler = queueProvider(jobProvider(cleanJob));
diff --git a/packages/api/src/handlers/entryPoint.ts b/packages/api/src/handlers/entryPoint.ts
new file mode 100644
index 00000000..cb24dafb
--- /dev/null
+++ b/packages/api/src/handlers/entryPoint.ts
@@ -0,0 +1,5 @@
+import { ViewHandlerReturnType } from '../../typings/app';
+
+export function entryPoint(): ViewHandlerReturnType {
+ return { name: 'index.ejs' };
+}
diff --git a/packages/api/src/handlers/error.ts b/packages/api/src/handlers/error.ts
new file mode 100644
index 00000000..08931c60
--- /dev/null
+++ b/packages/api/src/handlers/error.ts
@@ -0,0 +1,12 @@
+import { ControllerHandlerReturnType } from '../../typings/app';
+
+export function errorHandler(error: Error): ControllerHandlerReturnType {
+ return {
+ status: 500,
+ body: {
+ error: 'Internal server error',
+ message: error.message,
+ details: error.stack,
+ },
+ };
+}
diff --git a/packages/api/src/handlers/jobLogs.ts b/packages/api/src/handlers/jobLogs.ts
new file mode 100644
index 00000000..f6d60059
--- /dev/null
+++ b/packages/api/src/handlers/jobLogs.ts
@@ -0,0 +1,23 @@
+import { BaseAdapter } from '../queueAdapters/base';
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+} from '../../typings/app';
+import { queueProvider } from '../providers/queue';
+
+async function jobLogs(
+ req: BullBoardRequest,
+ queue: BaseAdapter
+): Promise {
+ const { jobId } = req.params;
+ const logs = await queue.getJobLogs(jobId);
+
+ return {
+ status: 200,
+ body: logs,
+ };
+}
+
+export const jobLogsHandler = queueProvider(jobLogs, {
+ skipReadOnlyModeCheck: true,
+});
diff --git a/packages/api/src/handlers/promotJob.ts b/packages/api/src/handlers/promotJob.ts
new file mode 100644
index 00000000..ae988049
--- /dev/null
+++ b/packages/api/src/handlers/promotJob.ts
@@ -0,0 +1,21 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+ QueueJob,
+} from '../../typings/app';
+import { queueProvider } from '../providers/queue';
+import { jobProvider } from '../providers/job';
+
+async function promoteJob(
+ _req: BullBoardRequest,
+ job: QueueJob
+): Promise {
+ await job.promote();
+
+ return {
+ status: 204,
+ body: {},
+ };
+}
+
+export const promoteJobHandler = queueProvider(jobProvider(promoteJob));
diff --git a/src/routes/handlers/queues.ts b/packages/api/src/handlers/queues.ts
similarity index 51%
rename from src/routes/handlers/queues.ts
rename to packages/api/src/handlers/queues.ts
index 310d3c1c..4796514d 100644
--- a/src/routes/handlers/queues.ts
+++ b/packages/api/src/handlers/queues.ts
@@ -1,13 +1,17 @@
-import { Request, RequestHandler, Response } from 'express-serve-static-core';
import { parse as parseRedisInfo } from 'redis-info';
-
-import * as api from '../../@types/api';
-import * as app from '../../@types/app';
-import { BullBoardQueues, JobStatus, QueueJob } from '../../@types/app';
-import { BaseAdapter } from '../../queueAdapters/base';
-import { Status } from '../../ui/components/constants';
-
-type MetricName = keyof app.ValidMetrics;
+import { BaseAdapter } from '../queueAdapters/base';
+import {
+ AppJob,
+ AppQueue,
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+ JobStatus,
+ QueueJob,
+ Status,
+ ValidMetrics,
+} from '../../typings/app';
+
+type MetricName = keyof ValidMetrics;
const metrics: MetricName[] = [
'redis_version',
@@ -17,7 +21,7 @@ const metrics: MetricName[] = [
'blocked_clients',
];
-const getStats = async (queue: BaseAdapter): Promise => {
+const getStats = async (queue: BaseAdapter): Promise => {
const redisInfoRaw = await queue.getRedisInfo();
const redisInfo = parseRedisInfo(redisInfoRaw);
@@ -29,12 +33,13 @@ const getStats = async (queue: BaseAdapter): Promise => {
return acc;
}, {} as Record);
- validMetrics.total_system_memory = redisInfo.total_system_memory || redisInfo.maxmemory;
+ validMetrics.total_system_memory =
+ redisInfo.total_system_memory || redisInfo.maxmemory;
return validMetrics;
};
-const formatJob = (job: QueueJob, queue: BaseAdapter): app.AppJob => {
+const formatJob = (job: QueueJob, queue: BaseAdapter): AppJob => {
const jobProps = job.toJSON();
return {
@@ -54,23 +59,24 @@ const formatJob = (job: QueueJob, queue: BaseAdapter): app.AppJob => {
};
};
-const statuses: JobStatus[] = ['active', 'completed', 'delayed', 'failed', 'paused', 'waiting'];
-
-const getDataForQueues = async (bullBoardQueues: app.BullBoardQueues, req: Request): Promise => {
- const query = req.query || {};
- const pairs = [...bullBoardQueues.entries()];
-
- if (pairs.length == 0) {
- return {
- stats: {},
- queues: [],
- };
- }
+const statuses: JobStatus[] = [
+ 'active',
+ 'completed',
+ 'delayed',
+ 'failed',
+ 'paused',
+ 'waiting',
+];
- const queues: app.AppQueue[] = await Promise.all(
+async function getAppQueues(
+ pairs: [string, BaseAdapter][],
+ query: Record
+): Promise {
+ return await Promise.all(
pairs.map(async ([name, queue]) => {
const counts = await queue.getJobCounts(...statuses);
- const status = query[name] === 'latest' ? statuses : (query[name] as JobStatus[]);
+ const status =
+ query[name] === 'latest' ? statuses : (query[name] as JobStatus[]);
const jobs = await queue.getJobs(status, 0, 10);
return {
@@ -81,19 +87,21 @@ const getDataForQueues = async (bullBoardQueues: app.BullBoardQueues, req: Reque
};
})
);
+}
- const stats = await getStats(pairs[0][1]);
+export async function queuesHandler({
+ queues: bullBoardQueues,
+ query = {},
+}: BullBoardRequest): Promise {
+ const pairs = [...bullBoardQueues.entries()];
- return {
- stats,
- queues,
- };
-};
+ const queues = pairs.length > 0 ? await getAppQueues(pairs, query) : [];
+ const stats = pairs.length > 0 ? await getStats(pairs[0][1]) : {};
-export const queuesHandler: RequestHandler = async (req: Request, res: Response) => {
- const { bullBoardQueues } = req.app.locals as {
- bullBoardQueues: BullBoardQueues;
+ return {
+ body: {
+ stats,
+ queues,
+ },
};
-
- res.json(await getDataForQueues(bullBoardQueues, req));
-};
+}
diff --git a/packages/api/src/handlers/retryAll.ts b/packages/api/src/handlers/retryAll.ts
new file mode 100644
index 00000000..3e3f99dc
--- /dev/null
+++ b/packages/api/src/handlers/retryAll.ts
@@ -0,0 +1,18 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+} from '../../typings/app';
+import { BaseAdapter } from '../queueAdapters/base';
+import { queueProvider } from '../providers/queue';
+
+async function retryAll(
+ _req: BullBoardRequest,
+ queue: BaseAdapter
+): Promise {
+ const jobs = await queue.getJobs(['failed']);
+ await Promise.all(jobs.map((job) => job.retry()));
+
+ return { status: 200, body: {} };
+}
+
+export const retryAllHandler = queueProvider(retryAll);
diff --git a/packages/api/src/handlers/retryJob.ts b/packages/api/src/handlers/retryJob.ts
new file mode 100644
index 00000000..878786f9
--- /dev/null
+++ b/packages/api/src/handlers/retryJob.ts
@@ -0,0 +1,21 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+ QueueJob,
+} from '../../typings/app';
+import { jobProvider } from '../providers/job';
+import { queueProvider } from '../providers/queue';
+
+async function retryJob(
+ _req: BullBoardRequest,
+ job: QueueJob
+): Promise {
+ await job.retry();
+
+ return {
+ status: 204,
+ body: {},
+ };
+}
+
+export const retryJobHandler = queueProvider(jobProvider(retryJob));
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
new file mode 100644
index 00000000..b0d3f3ba
--- /dev/null
+++ b/packages/api/src/index.ts
@@ -0,0 +1,26 @@
+import { BaseAdapter } from './queueAdapters/base';
+import { IServerAdapter } from '../typings/app';
+import { getQueuesApi } from './queuesApi';
+import path from 'path';
+import { appRoutes } from './routes';
+import { errorHandler } from './handlers/error';
+
+export function createBullBoard({
+ queues,
+ serverAdapter,
+}: {
+ queues: ReadonlyArray;
+ serverAdapter: IServerAdapter;
+}) {
+ const { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue } = getQueuesApi(queues);
+
+ serverAdapter
+ .setQueues(bullBoardQueues)
+ .setViewsPath(path.resolve('node_modules/@bull-board/ui/dist'))
+ .setStaticPath('/static', path.resolve('node_modules/@bull-board/ui/dist/static'))
+ .setEntryRoute(appRoutes.entryPoint)
+ .setErrorHandler(errorHandler)
+ .setApiRoutes(appRoutes.api);
+
+ return { setQueues, replaceQueues, addQueue, removeQueue };
+}
diff --git a/packages/api/src/providers/job.ts b/packages/api/src/providers/job.ts
new file mode 100644
index 00000000..dc1e4ae7
--- /dev/null
+++ b/packages/api/src/providers/job.ts
@@ -0,0 +1,33 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+ QueueJob,
+} from '../../typings/app';
+import { BaseAdapter } from '../queueAdapters/base';
+
+export function jobProvider(
+ next: (
+ req: BullBoardRequest,
+ job: QueueJob
+ ) => Promise
+) {
+ return async (
+ req: BullBoardRequest,
+ queue: BaseAdapter
+ ): Promise => {
+ const { jobId } = req.params;
+
+ const job = await queue.getJob(jobId);
+
+ if (!job) {
+ return {
+ status: 404,
+ body: {
+ error: 'Job not found',
+ },
+ };
+ }
+
+ return next(req, job);
+ };
+}
diff --git a/packages/api/src/providers/queue.ts b/packages/api/src/providers/queue.ts
new file mode 100644
index 00000000..3175bd30
--- /dev/null
+++ b/packages/api/src/providers/queue.ts
@@ -0,0 +1,35 @@
+import {
+ BullBoardRequest,
+ ControllerHandlerReturnType,
+} from '../../typings/app';
+import { BaseAdapter } from '../queueAdapters/base';
+
+export function queueProvider(
+ next: (
+ req: BullBoardRequest,
+ queue: BaseAdapter
+ ) => Promise,
+ {
+ skipReadOnlyModeCheck = false,
+ }: {
+ skipReadOnlyModeCheck?: boolean;
+ } = {}
+) {
+ return async (
+ req: BullBoardRequest
+ ): Promise => {
+ const { queueName } = req.params;
+
+ const queue = req.queues.get(queueName);
+ if (!queue) {
+ return { status: 404, body: { error: 'Queue not found' } };
+ } else if (queue.readOnlyMode && !skipReadOnlyModeCheck) {
+ return {
+ status: 405,
+ body: { error: 'Method not allowed on read only queue' },
+ };
+ }
+
+ return next(req, queue);
+ };
+}
diff --git a/src/queueAdapters/base.ts b/packages/api/src/queueAdapters/base.ts
similarity index 58%
rename from src/queueAdapters/base.ts
rename to packages/api/src/queueAdapters/base.ts
index 3ac24d86..24c862fa 100644
--- a/src/queueAdapters/base.ts
+++ b/packages/api/src/queueAdapters/base.ts
@@ -1,4 +1,10 @@
-import { JobCleanStatus, JobCounts, JobStatus, QueueAdapterOptions, QueueJob } from '../@types/app';
+import {
+ JobCleanStatus,
+ JobCounts,
+ JobStatus,
+ QueueAdapterOptions,
+ QueueJob,
+} from '../../typings/app';
export abstract class BaseAdapter {
public readonly readOnlyMode: boolean;
@@ -8,22 +14,34 @@ export abstract class BaseAdapter {
this.readOnlyMode = options.readOnlyMode === true;
}
- public setFormatter(field: 'data' | 'returnValue', formatter: (data: any) => any): void {
+ public setFormatter(
+ field: 'data' | 'returnValue',
+ formatter: (data: any) => any
+ ): void {
this.formatters[field] = formatter;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public format(field: 'data' | 'returnValue', data: any): any {
- return typeof this.formatters[field] === 'function' ? this.formatters[field](data) : data;
+ return typeof this.formatters[field] === 'function'
+ ? this.formatters[field](data)
+ : data;
}
- public abstract clean(queueStatus: JobCleanStatus, graceTimeMs: number): Promise;
+ public abstract clean(
+ queueStatus: JobCleanStatus,
+ graceTimeMs: number
+ ): Promise;
public abstract getJob(id: string): Promise;
public abstract getJobCounts(...jobStatuses: JobStatus[]): Promise;
- public abstract getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise;
+ public abstract getJobs(
+ jobStatuses: JobStatus[],
+ start?: number,
+ end?: number
+ ): Promise;
public abstract getJobLogs(id: string): Promise;
diff --git a/src/queueAdapters/bull.ts b/packages/api/src/queueAdapters/bull.ts
similarity index 82%
rename from src/queueAdapters/bull.ts
rename to packages/api/src/queueAdapters/bull.ts
index 5d1e2323..62b94034 100644
--- a/src/queueAdapters/bull.ts
+++ b/packages/api/src/queueAdapters/bull.ts
@@ -1,5 +1,10 @@
import { Job, Queue } from 'bull';
-import { JobCleanStatus, JobCounts, JobStatus, QueueAdapterOptions } from '../@types/app';
+import {
+ JobCleanStatus,
+ JobCounts,
+ JobStatus,
+ QueueAdapterOptions,
+} from '../../typings/app';
import { BaseAdapter } from './base';
export class BullAdapter extends BaseAdapter {
@@ -23,7 +28,11 @@ export class BullAdapter extends BaseAdapter {
return this.queue.getJob(id);
}
- public getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise {
+ public getJobs(
+ jobStatuses: JobStatus[],
+ start?: number,
+ end?: number
+ ): Promise {
return this.queue.getJobs(jobStatuses as any, start, end);
}
diff --git a/src/queueAdapters/bullMQ.ts b/packages/api/src/queueAdapters/bullMQ.ts
similarity index 68%
rename from src/queueAdapters/bullMQ.ts
rename to packages/api/src/queueAdapters/bullMQ.ts
index fd28e2f3..1fad4a25 100644
--- a/src/queueAdapters/bullMQ.ts
+++ b/packages/api/src/queueAdapters/bullMQ.ts
@@ -1,11 +1,19 @@
import { Job, Queue } from 'bullmq';
-import { JobCleanStatus, JobCounts, JobStatus, QueueAdapterOptions } from '../@types/app';
+import {
+ JobCleanStatus,
+ JobCounts,
+ JobStatus,
+ QueueAdapterOptions,
+} from '../../typings/app';
import { BaseAdapter } from './base';
export class BullMQAdapter extends BaseAdapter {
private readonly LIMIT = 1000;
- constructor(private queue: Queue, options: Partial = {}) {
+ constructor(
+ private queue: Queue,
+ options: Partial = {}
+ ) {
super(options);
}
@@ -26,12 +34,18 @@ export class BullMQAdapter extends BaseAdapter {
return this.queue.getJob(id);
}
- public getJobs(jobStatuses: JobStatus[], start?: number, end?: number): Promise {
+ public getJobs(
+ jobStatuses: JobStatus[],
+ start?: number,
+ end?: number
+ ): Promise {
return this.queue.getJobs(jobStatuses, start, end);
}
public getJobCounts(...jobStatuses: JobStatus[]): Promise {
- return (this.queue.getJobCounts(...jobStatuses) as unknown) as Promise;
+ return (this.queue.getJobCounts(
+ ...jobStatuses
+ ) as unknown) as Promise;
}
public getJobLogs(id: string): Promise {
diff --git a/packages/api/src/queuesApi.ts b/packages/api/src/queuesApi.ts
new file mode 100644
index 00000000..e6eae1ee
--- /dev/null
+++ b/packages/api/src/queuesApi.ts
@@ -0,0 +1,41 @@
+import { BaseAdapter } from './queueAdapters/base';
+import { BullBoardQueues } from '../typings/app';
+
+export function getQueuesApi(queues: ReadonlyArray) {
+ const bullBoardQueues: BullBoardQueues = new Map();
+
+ function addQueue(queue: BaseAdapter): void {
+ const name = queue.getName();
+ bullBoardQueues.set(name, queue);
+ }
+
+ function removeQueue(queueOrName: string | BaseAdapter) {
+ const name = typeof queueOrName === 'string' ? queueOrName : queueOrName.getName();
+
+ bullBoardQueues.delete(name);
+ }
+
+ function setQueues(newBullQueues: ReadonlyArray): void {
+ newBullQueues.forEach((queue) => {
+ const name = queue.getName();
+
+ bullBoardQueues.set(name, queue);
+ });
+ }
+
+ function replaceQueues(newBullQueues: ReadonlyArray): void {
+ const queuesToPersist: string[] = newBullQueues.map((queue) => queue.getName());
+
+ bullBoardQueues.forEach((_queue, name) => {
+ if (queuesToPersist.indexOf(name) === -1) {
+ bullBoardQueues.delete(name);
+ }
+ });
+
+ return setQueues(newBullQueues);
+ }
+
+ setQueues(queues);
+
+ return { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue };
+}
diff --git a/packages/api/src/routes.ts b/packages/api/src/routes.ts
new file mode 100644
index 00000000..9bef03a8
--- /dev/null
+++ b/packages/api/src/routes.ts
@@ -0,0 +1,50 @@
+import { AppRouteDefs } from '../typings/app';
+import { entryPoint } from './handlers/entryPoint';
+import { queuesHandler } from './handlers/queues';
+import { retryAllHandler } from './handlers/retryAll';
+import { cleanAllHandler } from './handlers/cleanAll';
+import { retryJobHandler } from './handlers/retryJob';
+import { cleanJobHandler } from './handlers/cleanJob';
+import { promoteJobHandler } from './handlers/promotJob';
+import { jobLogsHandler } from './handlers/jobLogs';
+
+export const appRoutes: AppRouteDefs = {
+ entryPoint: {
+ method: 'get',
+ route: ['/', '/queue/:queueName'],
+ handler: entryPoint,
+ },
+ api: [
+ { method: 'get', route: '/api/queues', handler: queuesHandler },
+ {
+ method: 'put',
+ route: '/api/queues/:queueName/retry',
+ handler: retryAllHandler,
+ },
+ {
+ method: 'put',
+ route: '/api/queues/:queueName/clean/:queueStatus',
+ handler: cleanAllHandler,
+ },
+ {
+ method: 'put',
+ route: '/api/queues/:queueName/:jobId/retry',
+ handler: retryJobHandler,
+ },
+ {
+ method: 'put',
+ route: '/api/queues/:queueName/:jobId/clean',
+ handler: cleanJobHandler,
+ },
+ {
+ method: 'put',
+ route: '/api/queues/:queueName/:jobId/promote',
+ handler: promoteJobHandler,
+ },
+ {
+ method: 'get',
+ route: '/api/queues/:queueName/:jobId/logs',
+ handler: jobLogsHandler,
+ },
+ ],
+};
diff --git a/tests/api/index.spec.ts b/packages/api/tests/api/index.spec.ts
similarity index 88%
rename from tests/api/index.spec.ts
rename to packages/api/tests/api/index.spec.ts
index 963bae00..d769303b 100644
--- a/tests/api/index.spec.ts
+++ b/packages/api/tests/api/index.spec.ts
@@ -1,10 +1,17 @@
import { Queue } from 'bullmq';
import request from 'supertest';
-import { createBullBoard } from '../../src';
-import { BullMQAdapter } from '../../src/queueAdapters/bullMQ';
+import { createBullBoard } from '@bull-board/api';
+import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
+import { ExpressAdapter } from '@bull-board/express';
describe('happy', () => {
+ let serverAdapter: ExpressAdapter;
+
+ beforeEach(() => {
+ serverAdapter = new ExpressAdapter();
+ });
+
it('should be able to set queue', async () => {
const paintQueue = new Queue('Paint', {
connection: {
@@ -13,9 +20,9 @@ describe('happy', () => {
},
});
- const { router } = createBullBoard([new BullMQAdapter(paintQueue)]);
+ createBullBoard({ queues: [new BullMQAdapter(paintQueue)], serverAdapter });
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
@@ -84,14 +91,14 @@ describe('happy', () => {
},
});
- const { router, replaceQueues } = createBullBoard([
- new BullMQAdapter(paintQueue),
- new BullMQAdapter(drainQueue),
- ]);
+ const { replaceQueues } = createBullBoard({
+ queues: [new BullMQAdapter(paintQueue), new BullMQAdapter(drainQueue)],
+ serverAdapter,
+ });
replaceQueues([new BullMQAdapter(codeQueue)]);
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
@@ -146,11 +153,11 @@ describe('happy', () => {
},
});
- const { router, addQueue } = createBullBoard([]);
+ const { addQueue } = createBullBoard({ queues: [], serverAdapter });
addQueue(new BullMQAdapter(addedQueue));
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
@@ -205,12 +212,12 @@ describe('happy', () => {
},
});
- const { router, addQueue, removeQueue } = createBullBoard([]);
+ const { addQueue, removeQueue } = createBullBoard({ queues: [], serverAdapter });
addQueue(new BullMQAdapter(addedQueue));
removeQueue(new BullMQAdapter(addedQueue));
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
@@ -238,12 +245,12 @@ describe('happy', () => {
},
});
- const { router, addQueue, removeQueue } = createBullBoard([]);
+ const { addQueue, removeQueue } = createBullBoard({ queues: [], serverAdapter });
addQueue(new BullMQAdapter(addedQueue));
removeQueue('AddedQueue');
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
@@ -271,11 +278,11 @@ describe('happy', () => {
},
});
- const { router, replaceQueues } = createBullBoard([]);
+ const { replaceQueues } = createBullBoard({ queues: [], serverAdapter });
replaceQueues([new BullMQAdapter(codeQueue)]);
- await request(router)
+ await request(serverAdapter.getRouter())
.get('/api/queues')
.expect('Content-Type', /json/)
.expect(200)
diff --git a/tests/api/public-interface.spec.ts b/packages/api/tests/api/public-interface.spec.ts
similarity index 82%
rename from tests/api/public-interface.spec.ts
rename to packages/api/tests/api/public-interface.spec.ts
index d448769c..be76412e 100644
--- a/tests/api/public-interface.spec.ts
+++ b/packages/api/tests/api/public-interface.spec.ts
@@ -1,4 +1,4 @@
-import * as bullBoard from '../../src';
+import * as bullBoard from '@bull-board/api';
describe('lib public interface', () => {
it('should save the interface', () => {
diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json
new file mode 100644
index 00000000..e03a32cf
--- /dev/null
+++ b/packages/api/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "esModuleInterop": true,
+ "lib": ["es2019"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "noImplicitAny": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "es2019",
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "resolveJsonModule": true,
+ "declaration": true
+ },
+ "include": ["./src", "./typings/*.d.ts"]
+}
diff --git a/src/@types/app.ts b/packages/api/typings/app.d.ts
similarity index 52%
rename from src/@types/app.ts
rename to packages/api/typings/app.d.ts
index 60f31150..b30debce 100644
--- a/src/@types/app.ts
+++ b/packages/api/typings/app.d.ts
@@ -1,7 +1,14 @@
-import { BaseAdapter } from '../queueAdapters/base';
-import { Status } from '../ui/components/constants';
+import { BaseAdapter } from '../src/queueAdapters/base';
+import { STATUSES } from '../src/constants/statuses';
-export type JobCleanStatus = 'completed' | 'wait' | 'active' | 'delayed' | 'failed';
+export type JobCleanStatus =
+ | 'completed'
+ | 'wait'
+ | 'active'
+ | 'delayed'
+ | 'failed';
+
+export type Status = keyof typeof STATUSES;
export type JobStatus = Status;
@@ -77,15 +84,57 @@ export interface AppQueue {
readOnlyMode: boolean;
}
-export type SelectedStatuses = Record;
-
-export interface QueueActions {
- promoteJob: (queueName: string) => (job: AppJob) => () => Promise;
- retryJob: (queueName: string) => (job: AppJob) => () => Promise;
- cleanJob: (queueName: string) => (job: AppJob) => () => Promise;
- getJobLogs: (queueName: string) => (job: AppJob) => () => Promise;
- retryAll: (queueName: string) => () => Promise;
- cleanAllDelayed: (queueName: string) => () => Promise;
- cleanAllFailed: (queueName: string) => () => Promise;
- cleanAllCompleted: (queueName: string) => () => Promise;
+export type HTTPMethod = 'get' | 'post' | 'put';
+export type HTTPStatus = 200 | 204 | 404 | 405 | 500;
+
+export interface BullBoardRequest {
+ queues: BullBoardQueues;
+ query: Record;
+ params: Record;
+}
+
+export type ControllerHandlerReturnType = {
+ status?: HTTPStatus;
+ body: string | Record;
+};
+
+export type ViewHandlerReturnType = {
+ name: string;
+};
+
+export type Promisify = T | Promise;
+
+export interface AppControllerRoute {
+ method: HTTPMethod | HTTPMethod[];
+ route: string | string[];
+
+ handler(request?: BullBoardRequest): Promisify;
+}
+
+export interface AppViewRoute {
+ method: HTTPMethod;
+ route: string | string[];
+
+ handler(request?: BullBoardRequest): ViewHandlerReturnType;
+}
+
+export type AppRouteDefs = {
+ entryPoint: AppViewRoute;
+ api: AppControllerRoute[];
+};
+
+export interface IServerAdapter {
+ setQueues(bullBoardQueues: BullBoardQueues): IServerAdapter;
+
+ setViewsPath(viewPath: string): IServerAdapter;
+
+ setStaticPath(staticsRoute: string, staticsPath: string): IServerAdapter;
+
+ setEntryRoute(route: AppViewRoute): IServerAdapter;
+
+ setErrorHandler(
+ handler: (error: Error) => ControllerHandlerReturnType
+ ): IServerAdapter;
+
+ setApiRoutes(routes: AppControllerRoute[]): IServerAdapter;
}
diff --git a/src/@types/api.ts b/packages/api/typings/responses.ts
similarity index 73%
rename from src/@types/api.ts
rename to packages/api/typings/responses.ts
index b2c0f71e..9b06c603 100644
--- a/src/@types/api.ts
+++ b/packages/api/typings/responses.ts
@@ -1,6 +1,6 @@
import { AppQueue, ValidMetrics } from './app';
-export interface GetQueues {
+export interface GetQueuesResponse {
stats: Partial;
queues: AppQueue[];
}
diff --git a/src/@types/utils.ts b/packages/api/typings/utils.d.ts
similarity index 100%
rename from src/@types/utils.ts
rename to packages/api/typings/utils.d.ts
diff --git a/packages/express/package.json b/packages/express/package.json
new file mode 100644
index 00000000..9aa00797
--- /dev/null
+++ b/packages/express/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@bull-board/express",
+ "version": "3.0.0",
+ "main": "dist/index.js",
+ "author": "felixmosh",
+ "license": "MIT",
+ "scripts": {
+ "build": "tsc"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@bull-board/api": "*",
+ "@bull-board/ui": "*",
+ "ejs": "3.1.6",
+ "express": "4.17.1"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.12",
+ "@types/express-serve-static-core": "^4.17.20"
+ }
+}
diff --git a/packages/express/src/ExpressAdapter.ts b/packages/express/src/ExpressAdapter.ts
new file mode 100644
index 00000000..48e459fb
--- /dev/null
+++ b/packages/express/src/ExpressAdapter.ts
@@ -0,0 +1,119 @@
+import express, {
+ Express,
+ NextFunction,
+ Request,
+ Response,
+ Router,
+} from 'express';
+import {
+ AppControllerRoute,
+ AppViewRoute,
+ BullBoardQueues,
+ ControllerHandlerReturnType,
+ HTTPMethod,
+ IServerAdapter,
+} from '@bull-board/api/typings/app';
+import { wrapAsync } from './helpers/wrapAsync';
+
+export class ExpressAdapter implements IServerAdapter {
+ private readonly app: Express;
+ private basePath = '';
+ private bullBoardQueues: BullBoardQueues | undefined;
+ private errorHandler:
+ | ((error: Error) => ControllerHandlerReturnType)
+ | undefined;
+
+ constructor() {
+ this.app = express();
+ }
+
+ public setBasePath(path: string): ExpressAdapter {
+ this.basePath = path;
+ return this;
+ }
+
+ public setStaticPath(
+ staticsRoute: string,
+ staticsPath: string
+ ): ExpressAdapter {
+ this.app.use(staticsRoute, express.static(staticsPath));
+
+ return this;
+ }
+
+ public setViewsPath(viewPath: string): ExpressAdapter {
+ this.app.set('view engine', 'ejs').set('views', viewPath);
+ return this;
+ }
+
+ public setErrorHandler(
+ handler: (error: Error) => ControllerHandlerReturnType
+ ) {
+ this.errorHandler = handler;
+ return this;
+ }
+
+ public setApiRoutes(routes: AppControllerRoute[]): ExpressAdapter {
+ if (!this.errorHandler) {
+ throw new Error(
+ `Please call 'setErrorHandler' before using 'registerPlugin'`
+ );
+ } else if (!this.bullBoardQueues) {
+ throw new Error(`Please call 'setQueues' before using 'registerPlugin'`);
+ }
+ const router = Router();
+
+ routes.forEach((route) =>
+ (Array.isArray(route.method) ? route.method : [route.method]).forEach(
+ (method: HTTPMethod) => {
+ router[method](
+ route.route,
+ wrapAsync(async (req: Request, res: Response) => {
+ const response = await route.handler({
+ queues: this.bullBoardQueues as BullBoardQueues,
+ query: req.query,
+ params: req.params,
+ });
+
+ res.status(response.status || 200).json(response.body);
+ })
+ );
+ }
+ )
+ );
+
+ router.use(
+ (err: Error, _req: Request, res: Response, next: NextFunction) => {
+ if (!this.errorHandler) {
+ return next();
+ }
+
+ const response = this.errorHandler(err);
+ return res.status(response.status as 500).send(response.body);
+ }
+ );
+
+ this.app.use(router);
+ return this;
+ }
+
+ public setEntryRoute(routeDef: AppViewRoute): ExpressAdapter {
+ const { name } = routeDef.handler();
+
+ const viewHandler = (_req: Request, res: Response) => {
+ res.render(name, { basePath: this.basePath });
+ };
+
+ this.app[routeDef.method](routeDef.route, viewHandler);
+ return this;
+ }
+
+ public setQueues(bullBoardQueues: BullBoardQueues): ExpressAdapter {
+ this.bullBoardQueues = bullBoardQueues;
+ return this;
+ }
+
+ public getRouter(): any {
+ return this.app;
+ }
+}
diff --git a/src/routes/middlewares/wrapAsync.ts b/packages/express/src/helpers/wrapAsync.ts
similarity index 61%
rename from src/routes/middlewares/wrapAsync.ts
rename to packages/express/src/helpers/wrapAsync.ts
index 7c7e02a3..c27093dd 100644
--- a/src/routes/middlewares/wrapAsync.ts
+++ b/packages/express/src/helpers/wrapAsync.ts
@@ -2,4 +2,5 @@ import { ParamsDictionary, RequestHandler } from 'express-serve-static-core';
export const wrapAsync = (
fn: RequestHandler
-): RequestHandler => async (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
+): RequestHandler => async (req, res, next) =>
+ Promise.resolve(fn(req, res, next)).catch(next);
diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts
new file mode 100644
index 00000000..cbdabd3a
--- /dev/null
+++ b/packages/express/src/index.ts
@@ -0,0 +1 @@
+export { ExpressAdapter } from './ExpressAdapter';
diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json
new file mode 100644
index 00000000..e03a32cf
--- /dev/null
+++ b/packages/express/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "esModuleInterop": true,
+ "lib": ["es2019"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "noImplicitAny": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "es2019",
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "resolveJsonModule": true,
+ "declaration": true
+ },
+ "include": ["./src", "./typings/*.d.ts"]
+}
diff --git a/packages/fastify/package.json b/packages/fastify/package.json
new file mode 100644
index 00000000..3a6db6a0
--- /dev/null
+++ b/packages/fastify/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@bull-board/fastify",
+ "version": "3.0.0",
+ "main": "dist/index.js",
+ "author": "felixmosh",
+ "license": "MIT",
+ "scripts": {
+ "build": "tsc"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@bull-board/api": "*",
+ "@bull-board/ui": "*",
+ "ejs": "^3.1.6",
+ "fastify-static": "^4.2.2",
+ "point-of-view": "^4.14.0"
+ },
+ "devDependencies": {
+ "fastify": "^3.16.2"
+ }
+}
diff --git a/packages/fastify/src/FastifyAdapter.ts b/packages/fastify/src/FastifyAdapter.ts
new file mode 100644
index 00000000..c7f488ea
--- /dev/null
+++ b/packages/fastify/src/FastifyAdapter.ts
@@ -0,0 +1,185 @@
+import {
+ AppControllerRoute,
+ AppViewRoute,
+ BullBoardQueues,
+ ControllerHandlerReturnType,
+ IServerAdapter,
+} from '@bull-board/api/typings/app';
+import { FastifyInstance } from 'fastify';
+import pointOfView from 'point-of-view';
+
+import fastifyStatic from 'fastify-static';
+import { HTTPMethods } from 'fastify/types/utils';
+
+type FastifyRouteDef = {
+ method: HTTPMethods;
+ route: string;
+ handler: AppControllerRoute['handler'];
+};
+
+export class FastifyAdapter implements IServerAdapter {
+ private basePath = '';
+ private bullBoardQueues: BullBoardQueues | undefined;
+ private errorHandler:
+ | ((error: Error) => ControllerHandlerReturnType)
+ | undefined;
+ private statics: { path: string; route: string } | undefined;
+ private viewPath: string | undefined;
+ private entryRoute:
+ | { method: HTTPMethods; routes: string[]; filename: string }
+ | undefined;
+ private apiRoutes: Array | undefined;
+
+ public setBasePath(path: string): FastifyAdapter {
+ this.basePath = path;
+ return this;
+ }
+
+ public setStaticPath(
+ staticsRoute: string,
+ staticsPath: string
+ ): FastifyAdapter {
+ this.statics = { route: staticsRoute, path: staticsPath };
+
+ return this;
+ }
+
+ public setViewsPath(viewPath: string): FastifyAdapter {
+ this.viewPath = viewPath;
+ return this;
+ }
+
+ public setErrorHandler(
+ handler: (error: Error) => ControllerHandlerReturnType
+ ) {
+ this.errorHandler = handler;
+ return this;
+ }
+
+ public setApiRoutes(routes: AppControllerRoute[]): FastifyAdapter {
+ this.apiRoutes = routes.reduce((result, routeRaw) => {
+ const routes = Array.isArray(routeRaw.route)
+ ? routeRaw.route
+ : [routeRaw.route];
+ const methods = Array.isArray(routeRaw.method)
+ ? routeRaw.method
+ : [routeRaw.method];
+
+ routes.forEach((route) => {
+ result.push({
+ method: methods.map((method) =>
+ method.toUpperCase()
+ ) as unknown as HTTPMethods,
+ route,
+ handler: routeRaw.handler,
+ });
+ });
+
+ return result;
+ }, [] as FastifyRouteDef[]);
+ return this;
+ }
+
+ public setEntryRoute(routeDef: AppViewRoute): FastifyAdapter {
+ const { name } = routeDef.handler();
+
+ this.entryRoute = {
+ method: routeDef.method.toUpperCase() as HTTPMethods,
+ routes: ([] as string[]).concat(routeDef.route),
+ filename: name,
+ };
+
+ return this;
+ }
+
+ public setQueues(bullBoardQueues: BullBoardQueues): FastifyAdapter {
+ this.bullBoardQueues = bullBoardQueues;
+ return this;
+ }
+
+ public registerPlugin() {
+ return (
+ fastify: FastifyInstance,
+ _opts: { basePath: string },
+ next: (err?: Error) => void
+ ) => {
+ if (!this.statics) {
+ throw new Error(
+ `Please call 'setStaticPath' before using 'registerPlugin'`
+ );
+ } else if (!this.entryRoute) {
+ throw new Error(
+ `Please call 'setEntryRoute' before using 'registerPlugin'`
+ );
+ } else if (!this.viewPath) {
+ throw new Error(
+ `Please call 'setViewsPath' before using 'registerPlugin'`
+ );
+ } else if (!this.apiRoutes) {
+ throw new Error(
+ `Please call 'setApiRoutes' before using 'registerPlugin'`
+ );
+ } else if (!this.bullBoardQueues) {
+ throw new Error(
+ `Please call 'setQueues' before using 'registerPlugin'`
+ );
+ } else if (!this.errorHandler) {
+ throw new Error(
+ `Please call 'setErrorHandler' before using 'registerPlugin'`
+ );
+ }
+
+ fastify.register(pointOfView, {
+ engine: {
+ ejs: require('ejs'),
+ },
+ root: this.viewPath,
+ });
+
+ fastify.register(fastifyStatic, {
+ root: this.statics.path,
+ prefix: this.statics.route,
+ });
+
+ const { method, routes, filename } = this.entryRoute;
+ routes.forEach((url) =>
+ fastify.route({
+ method,
+ url,
+ handler: (_req, reply) => {
+ reply.view(filename, {
+ basePath: this.basePath,
+ });
+ },
+ })
+ );
+
+ this.apiRoutes.forEach((route) => {
+ fastify.route({
+ method: route.method,
+ url: route.route,
+ handler: async (request, reply) => {
+ const response = await route.handler({
+ queues: this.bullBoardQueues as any,
+ params: request.params as any,
+ query: request.query as any,
+ });
+
+ reply.status(response.status || 200).send(response.body);
+ },
+ });
+ });
+
+ const errorHandler = this.errorHandler;
+
+ fastify.setErrorHandler((error, _request, reply) => {
+ const response = errorHandler(error);
+ reply.status(response.status as 500).send(response.body);
+ });
+
+ next();
+ };
+ }
+}
+
+export class Fas {}
diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts
new file mode 100644
index 00000000..deb83f43
--- /dev/null
+++ b/packages/fastify/src/index.ts
@@ -0,0 +1 @@
+export { FastifyAdapter } from './FastifyAdapter';
diff --git a/packages/fastify/tsconfig.json b/packages/fastify/tsconfig.json
new file mode 100644
index 00000000..e03a32cf
--- /dev/null
+++ b/packages/fastify/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "esModuleInterop": true,
+ "lib": ["es2019"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "noImplicitAny": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "es2019",
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "resolveJsonModule": true,
+ "declaration": true
+ },
+ "include": ["./src", "./typings/*.d.ts"]
+}
diff --git a/packages/hapi/package.json b/packages/hapi/package.json
new file mode 100644
index 00000000..3c597f0f
--- /dev/null
+++ b/packages/hapi/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@bull-board/hapi",
+ "version": "3.0.0",
+ "main": "dist/index.js",
+ "author": "felixmosh",
+ "license": "MIT",
+ "scripts": {
+ "build": "tsc"
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "@bull-board/api": "*",
+ "@bull-board/ui": "*",
+ "@hapi/inert": "^6.0.3",
+ "@hapi/vision": "^6.1.0",
+ "ejs": "^3.1.6"
+ },
+ "devDependencies": {
+ "@hapi/hapi": "^20.1.3",
+ "@types/hapi__hapi": "^20.0.8",
+ "@types/hapi__inert": "^5.2.2",
+ "@types/hapi__vision": "^5.5.2"
+ }
+}
diff --git a/packages/hapi/src/HapiAdapter.ts b/packages/hapi/src/HapiAdapter.ts
new file mode 100644
index 00000000..726e1bda
--- /dev/null
+++ b/packages/hapi/src/HapiAdapter.ts
@@ -0,0 +1,157 @@
+import {
+ AppControllerRoute,
+ AppViewRoute,
+ BullBoardQueues,
+ ControllerHandlerReturnType,
+ IServerAdapter,
+} from '@bull-board/api/typings/app';
+import { PluginBase, PluginPackage } from '@hapi/hapi';
+import Vision from '@hapi/vision';
+import Inert from '@hapi/inert';
+import { toHapiPath } from './utils/toHapiPath';
+
+type HapiRouteDef = {
+ method: AppControllerRoute['method'];
+ path: string;
+ handler: AppControllerRoute['handler'];
+};
+
+export class HapiAdapter implements IServerAdapter {
+ private basePath = '';
+ private bullBoardQueues: BullBoardQueues | undefined;
+ private errorHandler: ((error: Error) => ControllerHandlerReturnType) | undefined;
+ private statics: { path: string; route: string } | undefined;
+ private viewPath: string | undefined;
+ private entryRoute: AppViewRoute | undefined;
+ private apiRoutes: HapiRouteDef[] | undefined;
+
+ public setBasePath(path: string): HapiAdapter {
+ this.basePath = path;
+ return this;
+ }
+
+ public setStaticPath(staticsRoute: string, staticsPath: string): HapiAdapter {
+ this.statics = { route: staticsRoute, path: staticsPath };
+
+ return this;
+ }
+
+ public setViewsPath(viewPath: string): HapiAdapter {
+ this.viewPath = viewPath;
+ return this;
+ }
+
+ public setErrorHandler(handler: (error: Error) => ControllerHandlerReturnType) {
+ this.errorHandler = handler;
+ return this;
+ }
+
+ public setApiRoutes(routes: AppControllerRoute[]): HapiAdapter {
+ this.apiRoutes = routes.reduce((result, routeRaw) => {
+ const routes = Array.isArray(routeRaw.route) ? routeRaw.route : [routeRaw.route];
+ const methods = Array.isArray(routeRaw.method) ? routeRaw.method : [routeRaw.method];
+
+ routes.forEach((path) => {
+ result.push({
+ method: methods.map((method) => method.toUpperCase()) as any,
+ path: toHapiPath(path),
+ handler: routeRaw.handler,
+ });
+ });
+
+ return result;
+ }, [] as HapiRouteDef[]);
+
+ return this;
+ }
+
+ public setEntryRoute(routeDef: AppViewRoute): HapiAdapter {
+ this.entryRoute = routeDef;
+
+ return this;
+ }
+
+ public setQueues(bullBoardQueues: BullBoardQueues): HapiAdapter {
+ this.bullBoardQueues = bullBoardQueues;
+ return this;
+ }
+
+ public registerPlugin(): PluginBase & PluginPackage {
+ return {
+ pkg: require('../package.json'),
+ register: async (server) => {
+ if (!this.statics) {
+ throw new Error(`Please call 'setStaticPath' before using 'registerPlugin'`);
+ } else if (!this.entryRoute) {
+ throw new Error(`Please call 'setEntryRoute' before using 'registerPlugin'`);
+ } else if (!this.viewPath) {
+ throw new Error(`Please call 'setViewsPath' before using 'registerPlugin'`);
+ } else if (!this.apiRoutes) {
+ throw new Error(`Please call 'setApiRoutes' before using 'registerPlugin'`);
+ } else if (!this.bullBoardQueues) {
+ throw new Error(`Please call 'setQueues' before using 'registerPlugin'`);
+ } else if (!this.errorHandler) {
+ throw new Error(`Please call 'setErrorHandler' before using 'registerPlugin'`);
+ }
+
+ await server.register(Vision);
+
+ server.views({
+ engines: {
+ ejs: require('ejs'),
+ },
+ path: this.viewPath,
+ });
+
+ await server.register(Inert);
+
+ server.route({
+ method: 'GET',
+ path: `${this.statics.route}/{param*}`,
+ handler: {
+ directory: {
+ path: this.statics.path,
+ },
+ },
+ });
+
+ const { method, route, handler } = this.entryRoute;
+ const routes = Array.isArray(route) ? route : [route];
+
+ routes.forEach((path) =>
+ server.route({
+ method: method.toUpperCase(),
+ path: toHapiPath(path),
+ handler: (_request, h) => {
+ const { name } = handler();
+ return h.view(name, { basePath: this.basePath });
+ },
+ })
+ );
+
+ const errorHandler = this.errorHandler;
+
+ this.apiRoutes.forEach((route) => {
+ server.route({
+ method: route.method,
+ path: route.path,
+ handler: async (request, h) => {
+ try {
+ const response = await route.handler({
+ queues: this.bullBoardQueues as any,
+ params: request.params as any,
+ query: request.query as any,
+ });
+
+ return h.response(response.body).code(response.status || 200);
+ } catch (e) {
+ const response = errorHandler(e);
+ return h.response(response.body).code(response.status as 500);
+ }
+ },
+ });
+ });
+ },
+ };
+ }
+}
diff --git a/packages/hapi/src/index.ts b/packages/hapi/src/index.ts
new file mode 100644
index 00000000..421c6fcf
--- /dev/null
+++ b/packages/hapi/src/index.ts
@@ -0,0 +1 @@
+export { HapiAdapter } from './HapiAdapter';
diff --git a/packages/hapi/src/utils/toHapiPath.ts b/packages/hapi/src/utils/toHapiPath.ts
new file mode 100644
index 00000000..636f1eb0
--- /dev/null
+++ b/packages/hapi/src/utils/toHapiPath.ts
@@ -0,0 +1,6 @@
+export function toHapiPath(path: string) {
+ return path
+ .split('/')
+ .map((path) => (path.startsWith(':') ? `{${path.substring(1)}}` : path))
+ .join('/');
+}
diff --git a/packages/hapi/tsconfig.json b/packages/hapi/tsconfig.json
new file mode 100644
index 00000000..e03a32cf
--- /dev/null
+++ b/packages/hapi/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "esModuleInterop": true,
+ "lib": ["es2019"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "noImplicitAny": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "es2019",
+ "noUnusedParameters": true,
+ "noUnusedLocals": true,
+ "resolveJsonModule": true,
+ "declaration": true
+ },
+ "include": ["./src", "./typings/*.d.ts"]
+}
diff --git a/babel.config.js b/packages/ui/babel.config.js
similarity index 100%
rename from babel.config.js
rename to packages/ui/babel.config.js
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 00000000..4aa01def
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@bull-board/ui",
+ "version": "3.0.0",
+ "license": "MIT",
+ "author": "felixmosh",
+ "files": [
+ "dist",
+ "typings"
+ ],
+ "scripts": {
+ "start": "NODE_ENV=development webpack serve",
+ "build": "NODE_ENV=production webpack --mode=production"
+ },
+ "dependencies": {
+ "@bull-board/api": "*"
+ },
+ "devDependencies": {
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
+ "@types/pretty-bytes": "^5.2.0",
+ "@types/react-dom": "^17.0.5",
+ "@types/react-highlight": "^0.12.2",
+ "@types/react-router-dom": "^5.1.6",
+ "axios": "^0.21.1",
+ "clean-webpack-plugin": "^4.0.0-alpha.0",
+ "clsx": "^1.1.1",
+ "css-loader": "^5.2.6",
+ "css-minimizer-webpack-plugin": "^2.0.0",
+ "date-fns": "2.21.3",
+ "fork-ts-checker-webpack-plugin": "^6.2.10",
+ "highlight.js": "^10.7.1",
+ "html-webpack-plugin": "^5.3.1",
+ "mini-css-extract-plugin": "^1.6.0",
+ "postcss-loader": "^5.3.0",
+ "postcss-preset-env": "^6.7.0",
+ "pretty-bytes": "5.6.0",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "react-refresh": "^0.10.0",
+ "react-router-dom": "^5.2.0",
+ "react-toastify": "^7.0.4",
+ "style-loader": "^2.0.0",
+ "webpack": "^5.38.0",
+ "webpack-cli": "^4.7.0",
+ "webpack-dev-server": "^3.11.0"
+ }
+}
diff --git a/src/ui/components/App.tsx b/packages/ui/src/components/App.tsx
similarity index 77%
rename from src/ui/components/App.tsx
rename to packages/ui/src/components/App.tsx
index eda56933..3d700763 100644
--- a/src/ui/components/App.tsx
+++ b/packages/ui/src/components/App.tsx
@@ -28,14 +28,18 @@ export const App = ({ api }: { api: Api }) => {
const currentQueueName = decodeURIComponent(params.name);
const queue = state.data?.queues.find((q) => q.name === currentQueueName);
- return ;
+ return (
+
+ );
}}
/>
- {!!state.data && Array.isArray(state.data?.queues) && state.data.queues.length > 0 && (
-
- )}
+ {!!state.data &&
+ Array.isArray(state.data?.queues) &&
+ state.data.queues.length > 0 && (
+
+ )}
)}
diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css
new file mode 100644
index 00000000..18287de6
--- /dev/null
+++ b/packages/ui/src/components/Header/Header.module.css
@@ -0,0 +1,36 @@
+.header {
+ z-index: 99;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ background: white;
+ transition: box-shadow 0.5s ease-in-out;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: var(--header-height);
+ box-sizing: border-box;
+ border-bottom: 1px solid #e6e7e8;
+}
+
+.header > .logo {
+ width: var(--menu-width);
+ flex-basis: var(--menu-width);
+ flex-shrink: 0;
+ white-space: nowrap;
+ text-align: center;
+ font-size: 1.728rem;
+ height: inherit;
+ line-height: var(--header-height);
+ background-color: hsl(217, 22%, 24%);
+ color: #f5f8fa;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
+ z-index: 2;
+ box-sizing: content-box;
+}
+
+.header + main {
+ padding-top: var(--header-height);
+}
diff --git a/src/ui/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx
similarity index 51%
rename from src/ui/components/Header/Header.tsx
rename to packages/ui/src/components/Header/Header.tsx
index 56adcac7..c314e012 100644
--- a/src/ui/components/Header/Header.tsx
+++ b/packages/ui/src/components/Header/Header.tsx
@@ -1,11 +1,9 @@
import React, { PropsWithChildren } from 'react';
import s from './Header.module.css';
-export const Header = ({ children }: PropsWithChildren) => {
- return (
-
- 🎯 Bull Dashboard
- {children}
-
- );
-};
+export const Header = ({ children }: PropsWithChildren) => (
+
+ 🎯 Bull Dashboard
+ {children}
+
+);
diff --git a/src/ui/components/Highlight/Highlight.tsx b/packages/ui/src/components/Highlight/Highlight.tsx
similarity index 91%
rename from src/ui/components/Highlight/Highlight.tsx
rename to packages/ui/src/components/Highlight/Highlight.tsx
index 6dc2b4a6..c02c49e7 100644
--- a/src/ui/components/Highlight/Highlight.tsx
+++ b/packages/ui/src/components/Highlight/Highlight.tsx
@@ -22,7 +22,9 @@ export class Highlight extends React.Component {
return (
nextProps.language !== this.props.language ||
(Array.isArray(this.props.children)
- ? this.props.children.some((item: any) => !([] as any).concat(nextProps.children).includes(item))
+ ? this.props.children.some(
+ (item: any) => !([] as any).concat(nextProps.children).includes(item)
+ )
: nextProps.children !== this.props.children)
);
}
diff --git a/src/ui/components/Highlight/languages/stacktrace.ts b/packages/ui/src/components/Highlight/languages/stacktrace.ts
similarity index 100%
rename from src/ui/components/Highlight/languages/stacktrace.ts
rename to packages/ui/src/components/Highlight/languages/stacktrace.ts
diff --git a/src/ui/components/Icons/Promote.tsx b/packages/ui/src/components/Icons/Promote.tsx
similarity index 100%
rename from src/ui/components/Icons/Promote.tsx
rename to packages/ui/src/components/Icons/Promote.tsx
diff --git a/src/ui/components/Icons/Retry.tsx b/packages/ui/src/components/Icons/Retry.tsx
similarity index 100%
rename from src/ui/components/Icons/Retry.tsx
rename to packages/ui/src/components/Icons/Retry.tsx
diff --git a/src/ui/components/Icons/Trash.tsx b/packages/ui/src/components/Icons/Trash.tsx
similarity index 100%
rename from src/ui/components/Icons/Trash.tsx
rename to packages/ui/src/components/Icons/Trash.tsx
diff --git a/packages/ui/src/components/JobCard/Button/Button.module.css b/packages/ui/src/components/JobCard/Button/Button.module.css
new file mode 100644
index 00000000..04608df0
--- /dev/null
+++ b/packages/ui/src/components/JobCard/Button/Button.module.css
@@ -0,0 +1,33 @@
+.button {
+ font-size: 1rem;
+ background: none;
+ border: none;
+ border-radius: 0.28571429rem;
+ cursor: pointer;
+ outline: none;
+ white-space: nowrap;
+ padding: 0.65em 0.92857143em;
+ color: inherit;
+}
+
+.button > svg {
+ width: 1.25em;
+ vertical-align: middle;
+ display: inline-block;
+ fill: #718096;
+}
+
+.button:hover,
+.button:focus {
+ background-color: #e2e8f0;
+}
+
+.button:active,
+.button.isActive {
+ background-color: #cbd5e0;
+}
+
+.button:hover > svg,
+.button:focus > svg {
+ fill: #718096;
+}
diff --git a/src/ui/components/JobCard/Button/Button.tsx b/packages/ui/src/components/JobCard/Button/Button.tsx
similarity index 100%
rename from src/ui/components/JobCard/Button/Button.tsx
rename to packages/ui/src/components/JobCard/Button/Button.tsx
diff --git a/packages/ui/src/components/JobCard/Details/Details.module.css b/packages/ui/src/components/JobCard/Details/Details.module.css
new file mode 100644
index 00000000..1cf16c2e
--- /dev/null
+++ b/packages/ui/src/components/JobCard/Details/Details.module.css
@@ -0,0 +1,87 @@
+.details {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.tabActions {
+ list-style: none;
+ padding: 0;
+ margin: 1rem 0 2rem;
+ display: flex;
+}
+
+.tabActions li + li {
+ margin-left: 0.75rem;
+}
+
+.tabContent {
+ flex: 1;
+ max-width: calc(100% - 80px);
+ position: relative;
+ overflow: hidden;
+}
+
+.tabContent:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 2rem;
+ background: linear-gradient(
+ 270deg,
+ hsl(0, 0%, 100%) 0%,
+ hsla(0, 0%, 100%, 0.738) 19%,
+ hsla(0, 0%, 100%, 0.541) 34%,
+ hsla(0, 0%, 100%, 0.382) 47%,
+ hsla(0, 0%, 100%, 0.278) 56.5%,
+ hsla(0, 0%, 100%, 0.194) 65%,
+ hsla(0, 0%, 100%, 0.126) 73%,
+ hsla(0, 0%, 100%, 0.075) 80.2%,
+ hsla(0, 0%, 100%, 0.042) 86.1%,
+ hsla(0, 0%, 100%, 0.021) 91%,
+ hsla(0, 0%, 100%, 0.008) 95.2%,
+ hsla(0, 0%, 100%, 0.002) 98.2%,
+ hsla(0, 0%, 100%, 0) 100%
+ );
+}
+
+.tabContent:after {
+ content: '';
+ position: absolute;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ height: 2rem;
+ background: linear-gradient(
+ 0deg,
+ hsl(0, 0%, 100%) 0%,
+ hsla(0, 0%, 100%, 0.738) 19%,
+ hsla(0, 0%, 100%, 0.541) 34%,
+ hsla(0, 0%, 100%, 0.382) 47%,
+ hsla(0, 0%, 100%, 0.278) 56.5%,
+ hsla(0, 0%, 100%, 0.194) 65%,
+ hsla(0, 0%, 100%, 0.126) 73%,
+ hsla(0, 0%, 100%, 0.075) 80.2%,
+ hsla(0, 0%, 100%, 0.042) 86.1%,
+ hsla(0, 0%, 100%, 0.021) 91%,
+ hsla(0, 0%, 100%, 0.008) 95.2%,
+ hsla(0, 0%, 100%, 0.002) 98.2%,
+ hsla(0, 0%, 100%, 0) 100%
+ );
+}
+
+.tabContent > div {
+ overflow: auto;
+ padding-bottom: 2rem;
+ height: 100%;
+}
+
+.tabContent :global(.error) {
+ color: #d73a49;
+}
+
+.tabContent pre {
+ margin: 0;
+}
diff --git a/src/ui/components/JobCard/Details/Details.tsx b/packages/ui/src/components/JobCard/Details/Details.tsx
similarity index 91%
rename from src/ui/components/JobCard/Details/Details.tsx
rename to packages/ui/src/components/JobCard/Details/Details.tsx
index 54015c7b..e569a43a 100644
--- a/src/ui/components/JobCard/Details/Details.tsx
+++ b/packages/ui/src/components/JobCard/Details/Details.tsx
@@ -1,10 +1,9 @@
import React from 'react';
-import { AppJob } from '../../../../@types/app';
import { useDetailsTabs } from '../../../hooks/useDetailsTabs';
-import { Status } from '../../constants';
import { Button } from '../Button/Button';
import s from './Details.module.css';
import { DetailsContent } from './DetailsContent/DetailsContent';
+import { AppJob, Status } from '@bull-board/api/typings/app';
interface DetailsProps {
job: AppJob;
diff --git a/src/ui/components/JobCard/Details/DetailsContent/DetailsContent.tsx b/packages/ui/src/components/JobCard/Details/DetailsContent/DetailsContent.tsx
similarity index 85%
rename from src/ui/components/JobCard/Details/DetailsContent/DetailsContent.tsx
rename to packages/ui/src/components/JobCard/Details/DetailsContent/DetailsContent.tsx
index e524efc9..d8a75e9e 100644
--- a/src/ui/components/JobCard/Details/DetailsContent/DetailsContent.tsx
+++ b/packages/ui/src/components/JobCard/Details/DetailsContent/DetailsContent.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import { AppJob } from '../../../../../@types/app';
import { TabsType } from '../../../../hooks/useDetailsTabs';
import { Highlight } from '../../../Highlight/Highlight';
import { JobLogs } from './JobLogs/JobLogs';
+import { AppJob } from '@bull-board/api/typings/app';
interface DetailsContentProps {
job: AppJob;
@@ -19,7 +19,9 @@ export const DetailsContent = ({
}: DetailsContentProps) => {
switch (selectedTab) {
case 'Data':
- return {JSON.stringify({ data, returnValue }, null, 2)};
+ return (
+ {JSON.stringify({ data, returnValue }, null, 2)}
+ );
case 'Options':
return {JSON.stringify(opts, null, 2)};
case 'Error':
diff --git a/src/ui/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.module.css b/packages/ui/src/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.module.css
similarity index 100%
rename from src/ui/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.module.css
rename to packages/ui/src/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.module.css
diff --git a/src/ui/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.tsx b/packages/ui/src/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.tsx
similarity index 100%
rename from src/ui/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.tsx
rename to packages/ui/src/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.tsx
diff --git a/packages/ui/src/components/JobCard/JobActions/JobActions.module.css b/packages/ui/src/components/JobCard/JobActions/JobActions.module.css
new file mode 100644
index 00000000..0825e0ae
--- /dev/null
+++ b/packages/ui/src/components/JobCard/JobActions/JobActions.module.css
@@ -0,0 +1,14 @@
+.jobActions {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+}
+
+.jobActions li + li {
+ margin-left: 0.5rem;
+}
+
+.jobActions .button {
+ padding: 0.5rem;
+}
diff --git a/src/ui/components/JobCard/JobActions/JobActions.tsx b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx
similarity index 93%
rename from src/ui/components/JobCard/JobActions/JobActions.tsx
rename to packages/ui/src/components/JobCard/JobActions/JobActions.tsx
index 9fa70820..97ef935c 100644
--- a/src/ui/components/JobCard/JobActions/JobActions.tsx
+++ b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import { Status, STATUSES } from '../../constants';
import { PromoteIcon } from '../../Icons/Promote';
import { RetryIcon } from '../../Icons/Retry';
import { TrashIcon } from '../../Icons/Trash';
import { Tooltip } from '../../Tooltip/Tooltip';
import { Button } from '../Button/Button';
import s from './JobActions.module.css';
+import { Status } from '@bull-board/api/typings/app';
+import { STATUSES } from '@bull-board/api/src/constants/statuses';
interface JobActionsProps {
status: Status;
diff --git a/packages/ui/src/components/JobCard/JobCard.module.css b/packages/ui/src/components/JobCard/JobCard.module.css
new file mode 100644
index 00000000..555b5d03
--- /dev/null
+++ b/packages/ui/src/components/JobCard/JobCard.module.css
@@ -0,0 +1,70 @@
+.card {
+ background-color: #fff;
+ box-shadow: 0 1px 1px 0 rgba(60, 75, 100, 0.14), 0 2px 1px -1px rgba(60, 75, 100, 0.12),
+ 0 1px 3px 0 rgba(60, 75, 100, 0.2);
+ border-radius: 0.25rem;
+ padding: 1em;
+ display: flex;
+ min-height: 320px;
+ max-height: 370px;
+}
+
+.card + .card {
+ margin-top: 2rem;
+}
+
+.contentWrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.title {
+ display: flex;
+ justify-content: space-between;
+}
+
+.title h4,
+.sideInfo span {
+ font-size: 1.44rem;
+ font-weight: 300;
+ color: #4a5568;
+ line-height: 1;
+}
+
+.title h4 span {
+ margin-left: 1.5rem;
+ color: #a0aec0;
+ font-size: 0.694em;
+}
+
+.sideInfo {
+ width: 200px;
+ padding-right: 2rem;
+ display: flex;
+ flex-direction: column;
+ text-align: right;
+ color: #cbd5e0;
+ flex-shrink: 0;
+}
+
+.sideInfo span {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ color: #cbd5e0;
+ padding-right: 1rem;
+}
+
+.content {
+ position: relative;
+ flex: 1;
+ overflow: hidden;
+}
+
+.content .progress {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+}
diff --git a/src/ui/components/JobCard/JobCard.tsx b/packages/ui/src/components/JobCard/JobCard.tsx
similarity index 93%
rename from src/ui/components/JobCard/JobCard.tsx
rename to packages/ui/src/components/JobCard/JobCard.tsx
index af1c90f3..3738479d 100644
--- a/src/ui/components/JobCard/JobCard.tsx
+++ b/packages/ui/src/components/JobCard/JobCard.tsx
@@ -1,11 +1,10 @@
import React from 'react';
-import { AppJob } from '../../../@types/app';
-import { Status } from '../constants';
import { Details } from './Details/Details';
import { JobActions } from './JobActions/JobActions';
import s from './JobCard.module.css';
import { Progress } from './Progress/Progress';
import { Timeline } from './Timeline/Timeline';
+import { AppJob, Status } from '@bull-board/api/typings/app';
interface JobCardProps {
job: AppJob;
diff --git a/packages/ui/src/components/JobCard/Progress/Progress.module.css b/packages/ui/src/components/JobCard/Progress/Progress.module.css
new file mode 100644
index 00000000..8252dc3a
--- /dev/null
+++ b/packages/ui/src/components/JobCard/Progress/Progress.module.css
@@ -0,0 +1,16 @@
+.progress {
+ width: 80px;
+ height: 80px;
+}
+
+.progress circle {
+ transform-origin: center;
+ transition: stroke-dashoffset 500ms ease-in-out;
+}
+
+.progress text {
+ font-size: 2.5rem;
+ font-family: inherit;
+ font-weight: 300;
+ fill: #a0aec0;
+}
diff --git a/src/ui/components/JobCard/Progress/Progress.tsx b/packages/ui/src/components/JobCard/Progress/Progress.tsx
similarity index 94%
rename from src/ui/components/JobCard/Progress/Progress.tsx
rename to packages/ui/src/components/JobCard/Progress/Progress.tsx
index aabc7eec..1d9d1064 100644
--- a/src/ui/components/JobCard/Progress/Progress.tsx
+++ b/packages/ui/src/components/JobCard/Progress/Progress.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-import { Status } from '../../constants';
import s from './Progress.module.css';
import cn from 'clsx';
+import { Status } from '@bull-board/api/typings/app';
export const Progress = ({
percentage,
diff --git a/packages/ui/src/components/JobCard/Timeline/Timeline.module.css b/packages/ui/src/components/JobCard/Timeline/Timeline.module.css
new file mode 100644
index 00000000..6e467554
--- /dev/null
+++ b/packages/ui/src/components/JobCard/Timeline/Timeline.module.css
@@ -0,0 +1,52 @@
+.timeline {
+ padding: 1.5rem 1rem 1.5rem 0;
+ margin: 0;
+ list-style: none;
+ border: 0;
+ border-right-width: 2px;
+ border-right-style: solid;
+ border-image: linear-gradient(to bottom, #fff, #e2e8f0 10%, #e2e8f0 90%, #fff) 1 100%;
+ color: #a0aec0;
+ font-weight: 300;
+ height: 100%;
+}
+
+.timeline li {
+ display: block;
+}
+
+.timeline li + li {
+ margin-top: 1.5rem;
+}
+
+.timeline li > time {
+ position: relative;
+ color: #718096;
+}
+
+.timeline li > time:before {
+ content: '';
+ width: 0.5rem;
+ height: 0.5rem;
+ position: absolute;
+ right: -1.5rem;
+ top: 50%;
+ margin-top: -0.5rem;
+ background-color: #cbd5e0;
+ border-radius: 100%;
+ border: 3px solid #fff;
+}
+
+.timeline li > small {
+ display: block;
+ line-height: 1;
+}
+
+.timeline li > small + small {
+ margin-top: 1.5rem;
+}
+
+.timelineWrapper {
+ position: relative;
+ flex: 1;
+}
diff --git a/src/ui/components/JobCard/Timeline/Timeline.tsx b/packages/ui/src/components/JobCard/Timeline/Timeline.tsx
similarity index 89%
rename from src/ui/components/JobCard/Timeline/Timeline.tsx
rename to packages/ui/src/components/JobCard/Timeline/Timeline.tsx
index b3830e70..0d28f0f7 100644
--- a/src/ui/components/JobCard/Timeline/Timeline.tsx
+++ b/packages/ui/src/components/JobCard/Timeline/Timeline.tsx
@@ -1,8 +1,7 @@
import { format, formatDistance, getYear, isToday } from 'date-fns';
import React from 'react';
-import { AppJob } from '../../../../@types/app';
-import { Status } from '../../constants';
import s from './Timeline.module.css';
+import { AppJob, Status } from '@bull-board/api/typings/app';
type TimeStamp = number | Date;
@@ -11,7 +10,9 @@ const formatDate = (ts: TimeStamp) => {
return format(ts, 'HH:mm:ss');
}
- return getYear(ts) === getYear(new Date()) ? format(ts, 'MM/dd HH:mm:ss') : format(ts, 'MM/dd/yyyy HH:mm:ss');
+ return getYear(ts) === getYear(new Date())
+ ? format(ts, 'MM/dd HH:mm:ss')
+ : format(ts, 'MM/dd/yyyy HH:mm:ss');
};
export const Timeline = function Timeline({ job, status }: { job: AppJob; status: Status }) {
diff --git a/packages/ui/src/components/Menu/Menu.module.css b/packages/ui/src/components/Menu/Menu.module.css
new file mode 100644
index 00000000..4a379d24
--- /dev/null
+++ b/packages/ui/src/components/Menu/Menu.module.css
@@ -0,0 +1,65 @@
+.aside {
+ position: fixed;
+ z-index: 99;
+ top: var(--header-height);
+ left: 0;
+ bottom: 0;
+ width: var(--menu-width);
+ background: linear-gradient(
+ to bottom,
+ hsl(217, 22%, 20%),
+ hsl(217, 22%, 16%) 80%,
+ hsl(217, 22%, 12%)
+ );
+ padding-top: 1rem;
+ color: #d5d9dc;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 4px 0 8px 3px rgba(38, 46, 60, 0.1);
+}
+
+.aside > div {
+ color: #828e97;
+ font-size: 0.833em;
+ padding: 0 1rem;
+}
+
+.aside nav {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.menu {
+ list-style: none;
+ padding: 0;
+}
+
+.menu li + li {
+ border-top: 1px solid hsl(206, 9%, 25%);
+}
+
+.menu a {
+ color: inherit;
+ text-decoration: none;
+ display: block;
+ padding: 1rem 1.25rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ transition: background-color 100ms ease-in;
+ border-left: 3px solid transparent;
+}
+
+.menu a:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+ border-left-color: hsl(184, 20%, 30%);
+}
+
+.menu a.active {
+ background-color: rgba(255, 255, 255, 0.1);
+ border-left-color: #4abec7;
+}
+
+.appVersion {
+ text-align: center;
+}
diff --git a/src/ui/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx
similarity index 94%
rename from src/ui/components/Menu/Menu.tsx
rename to packages/ui/src/components/Menu/Menu.tsx
index 731c0e71..bb897ad6 100644
--- a/src/ui/components/Menu/Menu.tsx
+++ b/packages/ui/src/components/Menu/Menu.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Store } from '../../hooks/useStore';
-import { STATUS_LIST } from '../constants';
import s from './Menu.module.css';
+import { STATUS_LIST } from '../../constants/status-list';
export const Menu = ({
queues,
diff --git a/packages/ui/src/components/QueueActions/QueueActions.module.css b/packages/ui/src/components/QueueActions/QueueActions.module.css
new file mode 100644
index 00000000..a186c730
--- /dev/null
+++ b/packages/ui/src/components/QueueActions/QueueActions.module.css
@@ -0,0 +1,20 @@
+.queueActions {
+ padding: 0 0 1rem;
+ margin: -1rem 0 0;
+ list-style: none;
+ display: flex;
+}
+
+.queueActions > li + li {
+ margin-left: 0.25rem;
+}
+
+.queueActions .button > svg {
+ fill: #a0aec0;
+ margin: -0.25em 0.5em 0 0;
+}
+
+.queueActions .button:hover > svg,
+.queueActions .button:focus > svg {
+ fill: #718096;
+}
diff --git a/src/ui/components/QueueActions/QueueActions.tsx b/packages/ui/src/components/QueueActions/QueueActions.tsx
similarity index 94%
rename from src/ui/components/QueueActions/QueueActions.tsx
rename to packages/ui/src/components/QueueActions/QueueActions.tsx
index 4fa90e4e..4da38561 100644
--- a/src/ui/components/QueueActions/QueueActions.tsx
+++ b/packages/ui/src/components/QueueActions/QueueActions.tsx
@@ -1,11 +1,10 @@
import React from 'react';
-import { AppQueue } from '../../../@types/app';
import { Store } from '../../hooks/useStore';
-import { Status } from '../constants';
import { RetryIcon } from '../Icons/Retry';
import { TrashIcon } from '../Icons/Trash';
import { Button } from '../JobCard/Button/Button';
import s from './QueueActions.module.css';
+import { AppQueue, Status } from '@bull-board/api/typings/app';
interface QueueActionProps {
queue: AppQueue;
diff --git a/packages/ui/src/components/QueuePage/QueuePage.module.css b/packages/ui/src/components/QueuePage/QueuePage.module.css
new file mode 100644
index 00000000..526eb19e
--- /dev/null
+++ b/packages/ui/src/components/QueuePage/QueuePage.module.css
@@ -0,0 +1,9 @@
+.stickyHeader {
+ position: sticky;
+ top: var(--header-height);
+ z-index: 2;
+ background-color: rgba(245, 248, 250, 0.85);
+ backdrop-filter: blur(4px);
+ margin: 0 -1rem;
+ padding: 0 1rem;
+}
diff --git a/src/ui/components/QueuePage/QueuePage.tsx b/packages/ui/src/components/QueuePage/QueuePage.tsx
similarity index 85%
rename from src/ui/components/QueuePage/QueuePage.tsx
rename to packages/ui/src/components/QueuePage/QueuePage.tsx
index dd677b4e..d3507e55 100644
--- a/src/ui/components/QueuePage/QueuePage.tsx
+++ b/packages/ui/src/components/QueuePage/QueuePage.tsx
@@ -1,10 +1,10 @@
import React from 'react';
-import { AppQueue } from '../../../@types/app';
import { Store } from '../../hooks/useStore';
import { JobCard } from '../JobCard/JobCard';
import { QueueActions } from '../QueueActions/QueueActions';
import { StatusMenu } from '../StatusMenu/StatusMenu';
import s from './QueuePage.module.css';
+import { AppQueue } from '@bull-board/api/typings/app';
export const QueuePage = ({
selectedStatus,
@@ -23,7 +23,9 @@ export const QueuePage = ({
- {!queue.readOnlyMode && }
+ {!queue.readOnlyMode && (
+
+ )}
{queue.jobs.map((job) => (
div {
+ text-align: center;
+ margin: 0 0.5rem;
+ position: relative;
+}
+
+.stats span {
+ display: block;
+ font-size: 1.44rem;
+ margin-top: 0.125em;
+}
+
+.stats small {
+ position: absolute;
+ white-space: nowrap;
+ left: 0;
+}
diff --git a/src/ui/components/RedisStats/RedisStats.tsx b/packages/ui/src/components/RedisStats/RedisStats.tsx
similarity index 98%
rename from src/ui/components/RedisStats/RedisStats.tsx
rename to packages/ui/src/components/RedisStats/RedisStats.tsx
index de9a7c03..71d48483 100644
--- a/src/ui/components/RedisStats/RedisStats.tsx
+++ b/packages/ui/src/components/RedisStats/RedisStats.tsx
@@ -1,7 +1,7 @@
import formatBytes from 'pretty-bytes';
import React from 'react';
-import { ValidMetrics } from '../../../@types/app';
import s from './RedisStats.module.css';
+import { ValidMetrics } from '@bull-board/api/typings/app';
const RedisLogo = () => (