├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .prettierignore ├── .yarn └── releases │ └── yarn-4.9.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.redis.yml ├── example.ts ├── examples ├── with-elysia │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock ├── with-express-auth │ ├── README.md │ ├── index.js │ ├── package.json │ ├── views │ │ └── login.ejs │ └── yarn.lock ├── with-express-csrf │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-express │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-fastify-auth │ ├── README.md │ ├── basicAuth.js │ ├── cookieAuth.js │ ├── index.js │ ├── package.json │ ├── views │ │ └── login.ejs │ └── yarn.lock ├── with-fastify │ ├── .dockerignore │ ├── .env.example │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-h3 │ ├── README.md │ ├── index.ts │ ├── package.json │ └── yarn.lock ├── with-hapi-auth │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-hapi │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-hono │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-koa │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-multiple-instances │ ├── README.md │ ├── index.js │ ├── package.json │ └── yarn.lock ├── with-nestjs-fastify-auth │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── eslint.config.mjs │ ├── nest-cli.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── bullboard-auth.local.strategy.ts │ │ ├── exception-filter.ts │ │ ├── main.ts │ │ ├── queues │ │ │ ├── queue.module.ts │ │ │ └── test.processor.ts │ │ └── view │ │ │ └── login.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── with-nestjs-module │ ├── .gitignore │ ├── README.md │ ├── docker-compose.yml │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── app.module.ts │ │ ├── feature │ │ │ ├── feature.controller.ts │ │ │ └── feature.module.ts │ │ └── main.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── yarn.lock └── with-nestjs │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ └── queues │ │ ├── basic-auth.middleware.ts │ │ ├── queues.module.ts │ │ └── test.processor.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── package.json ├── packages ├── api │ ├── README.md │ ├── bullAdapter.d.ts │ ├── bullAdapter.js │ ├── bullMQAdapter.d.ts │ ├── bullMQAdapter.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── statuses.ts │ │ ├── handlers │ │ │ ├── addJob.ts │ │ │ ├── cleanAll.ts │ │ │ ├── cleanJob.ts │ │ │ ├── emptyQueue.ts │ │ │ ├── entryPoint.ts │ │ │ ├── error.ts │ │ │ ├── job.ts │ │ │ ├── jobLogs.ts │ │ │ ├── pauseAll.ts │ │ │ ├── pauseQueue.ts │ │ │ ├── promotJob.ts │ │ │ ├── promoteAll.ts │ │ │ ├── queues.ts │ │ │ ├── redisStats.ts │ │ │ ├── resumeAll.ts │ │ │ ├── resumeQueue.ts │ │ │ ├── retryAll.ts │ │ │ ├── retryJob.ts │ │ │ └── updateJobData.ts │ │ ├── index.ts │ │ ├── providers │ │ │ ├── job.ts │ │ │ └── queue.ts │ │ ├── queueAdapters │ │ │ ├── base.ts │ │ │ ├── bull.ts │ │ │ └── bullMQ.ts │ │ ├── queuesApi.ts │ │ └── routes.ts │ ├── tests │ │ └── api │ │ │ ├── index.spec.ts │ │ │ └── public-interface.spec.ts │ ├── tsconfig.json │ └── typings │ │ ├── app.ts │ │ ├── responses.ts │ │ └── utils.ts ├── elysia │ ├── README.md │ ├── package.json │ ├── src │ │ ├── ElysiaAdapter.ts │ │ └── index.ts │ └── tsconfig.json ├── express │ ├── README.md │ ├── package.json │ ├── src │ │ ├── ExpressAdapter.ts │ │ ├── helpers │ │ │ └── wrapAsync.ts │ │ └── index.ts │ └── tsconfig.json ├── fastify │ ├── README.md │ ├── package.json │ ├── src │ │ ├── FastifyAdapter.ts │ │ └── index.ts │ └── tsconfig.json ├── h3 │ ├── README.md │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── H3Adapter.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── getContentType.ts │ └── tsconfig.json ├── hapi │ ├── README.md │ ├── package.json │ ├── src │ │ ├── HapiAdapter.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── toHapiPath.ts │ └── tsconfig.json ├── hono │ ├── README.md │ ├── package.json │ ├── src │ │ ├── HonoAdapter.ts │ │ └── index.ts │ └── tsconfig.json ├── koa │ ├── README.md │ ├── package.json │ ├── src │ │ ├── KoaAdapter.ts │ │ └── index.ts │ └── tsconfig.json ├── nestjs │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bull-board.constants.ts │ │ ├── bull-board.decorator.ts │ │ ├── bull-board.feature-module.ts │ │ ├── bull-board.module.ts │ │ ├── bull-board.root-module.ts │ │ ├── bull-board.types.ts │ │ ├── bull-board.util.ts │ │ └── index.ts │ └── tsconfig.json └── ui │ ├── README.md │ ├── localesSync.config.js │ ├── package.json │ ├── rsbuild.config.ts │ ├── src │ ├── App.tsx │ ├── components │ │ ├── AddJobModal │ │ │ └── AddJobModal.tsx │ │ ├── Button │ │ │ ├── Button.module.css │ │ │ └── Button.tsx │ │ ├── Card │ │ │ ├── Card.module.css │ │ │ └── Card.tsx │ │ ├── ConfirmModal │ │ │ ├── ConfirmModal.module.css │ │ │ └── ConfirmModal.tsx │ │ ├── CustomLinksDropdown │ │ │ └── CustomLinksDropdown.tsx │ │ ├── DropdownContent │ │ │ ├── DropdownContent.module.css │ │ │ └── DropdownContent.tsx │ │ ├── Form │ │ │ ├── Field │ │ │ │ ├── Field.module.css │ │ │ │ └── Field.tsx │ │ │ ├── InputField │ │ │ │ └── InputField.tsx │ │ │ ├── JsonField │ │ │ │ └── JsonField.tsx │ │ │ ├── SelectField │ │ │ │ └── SelectField.tsx │ │ │ └── SwitchField │ │ │ │ ├── SwitchField.module.css │ │ │ │ └── SwitchField.tsx │ │ ├── Header │ │ │ ├── Header.module.css │ │ │ └── Header.tsx │ │ ├── HeaderActions │ │ │ ├── HeaderActions.module.css │ │ │ └── HeaderActions.tsx │ │ ├── Highlight │ │ │ ├── Highlight.module.css │ │ │ └── Highlight.tsx │ │ ├── Icons │ │ │ ├── Add.tsx │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.tsx │ │ │ ├── ChevronDown.tsx │ │ │ ├── ChevronUp.tsx │ │ │ ├── Copy.tsx │ │ │ ├── Duplicate.tsx │ │ │ ├── EllipsisVertical.tsx │ │ │ ├── Fullscreen.tsx │ │ │ ├── Pause.tsx │ │ │ ├── Play.tsx │ │ │ ├── Promote.tsx │ │ │ ├── Redis.tsx │ │ │ ├── Retry.tsx │ │ │ ├── Search.tsx │ │ │ ├── Settings.tsx │ │ │ ├── Sort.tsx │ │ │ ├── SortDirectionDown.tsx │ │ │ ├── SortDirectionUp.tsx │ │ │ ├── Trash.tsx │ │ │ ├── UpRightFromSquare.tsx │ │ │ ├── UpdateIcon.tsx │ │ │ └── User.tsx │ │ ├── JobCard │ │ │ ├── Details │ │ │ │ ├── Details.module.css │ │ │ │ ├── Details.tsx │ │ │ │ └── DetailsContent │ │ │ │ │ ├── DetailsContent.tsx │ │ │ │ │ └── JobLogs │ │ │ │ │ ├── JobLogs.module.css │ │ │ │ │ └── JobLogs.tsx │ │ │ ├── JobActions │ │ │ │ ├── JobActions.module.css │ │ │ │ └── JobActions.tsx │ │ │ ├── JobCard.module.css │ │ │ ├── JobCard.tsx │ │ │ ├── Progress │ │ │ │ ├── Progress.module.css │ │ │ │ └── Progress.tsx │ │ │ └── Timeline │ │ │ │ ├── Timeline.module.css │ │ │ │ └── Timeline.tsx │ │ ├── JsonEditor │ │ │ └── JsonEditor.tsx │ │ ├── Loader │ │ │ └── Loader.tsx │ │ ├── Menu │ │ │ ├── Menu.module.css │ │ │ ├── Menu.tsx │ │ │ └── MenuTree │ │ │ │ ├── MenuTree.module.css │ │ │ │ └── MenuTree.tsx │ │ ├── Modal │ │ │ ├── Modal.module.css │ │ │ └── Modal.tsx │ │ ├── OverviewDropDownActions │ │ │ ├── OverviewDropDownActions.module.css │ │ │ └── OverviewDropDownActions.tsx │ │ ├── Pagination │ │ │ ├── Pagination.module.css │ │ │ └── Pagination.tsx │ │ ├── QueueActions │ │ │ ├── QueueActions.module.css │ │ │ └── QueueActions.tsx │ │ ├── QueueCard │ │ │ ├── QueueCard.module.css │ │ │ ├── QueueCard.tsx │ │ │ └── QueueStats │ │ │ │ ├── QueueStats.module.css │ │ │ │ └── QueueStats.tsx │ │ ├── QueueDropdownActions │ │ │ ├── QueueDropdownActions.module.css │ │ │ └── QueueDropdownActions.tsx │ │ ├── RedisStatsModal │ │ │ ├── RedisStatsModal.module.css │ │ │ └── RedisStatsModal.tsx │ │ ├── SettingsModal │ │ │ └── SettingsModal.tsx │ │ ├── StatusLegend │ │ │ ├── StatusLegend.module.css │ │ │ └── StatusLegend.tsx │ │ ├── StatusMenu │ │ │ ├── StatusMenu.module.css │ │ │ └── StatusMenu.tsx │ │ ├── StickyHeader │ │ │ ├── StickyHeader.module.css │ │ │ └── StickyHeader.tsx │ │ ├── Title │ │ │ ├── Title.module.css │ │ │ └── Title.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.module.css │ │ │ └── Tooltip.tsx │ │ └── UpdateJobDataModal │ │ │ └── UpdateJobDataModal.tsx │ ├── constants │ │ ├── languages.ts │ │ └── queue-stats-status.ts │ ├── hooks │ │ ├── useActiveJobId.ts │ │ ├── useActiveQueue.ts │ │ ├── useActiveQueueName.ts │ │ ├── useApi.ts │ │ ├── useConfirm.ts │ │ ├── useDarkMode.ts │ │ ├── useDetailsTabs.tsx │ │ ├── useInterval.ts │ │ ├── useJob.ts │ │ ├── useLanguageWatch.ts │ │ ├── useModal.ts │ │ ├── useQuery.ts │ │ ├── useQueues.ts │ │ ├── useScrollTopOnNav.ts │ │ ├── useSelectedStatuses.ts │ │ ├── useSettings.ts │ │ ├── useSortQueues.ts │ │ └── useUIConfig.ts │ ├── index.css │ ├── index.ejs │ ├── index.tsx │ ├── pages │ │ ├── JobPage │ │ │ └── JobPage.tsx │ │ ├── OverviewPage │ │ │ ├── OverviewPage.module.css │ │ │ └── OverviewPage.tsx │ │ └── QueuePage │ │ │ └── QueuePage.tsx │ ├── schemas │ │ ├── bull │ │ │ └── jobOptions.json │ │ └── bullmq │ │ │ └── jobOptions.json │ ├── services │ │ ├── Api.ts │ │ └── i18n.ts │ ├── static │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── images │ │ │ └── logo.svg │ │ └── locales │ │ │ ├── en-US │ │ │ └── messages.json │ │ │ ├── es-ES │ │ │ └── messages.json │ │ │ ├── fr-FR │ │ │ └── messages.json │ │ │ ├── pt-BR │ │ │ └── messages.json │ │ │ └── zh-CN │ │ │ └── messages.json │ ├── theme.css │ ├── toastify.css │ └── utils │ │ ├── getConfirmFor.ts │ │ ├── getStaticPath.ts │ │ ├── highlight │ │ ├── config.ts │ │ ├── highlight.ts │ │ ├── languages │ │ │ └── stacktrace.ts │ │ └── worker.ts │ │ ├── links.ts │ │ ├── toCamelCase.ts │ │ └── toTree.ts │ ├── tsconfig.json │ └── typings │ ├── app.d.ts │ └── global.d.ts ├── prettier.config.js ├── screenshots ├── dashboard.png └── overview.png ├── tsconfig.json └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board", 3 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "christian-kohler.npm-intellisense", 9 | "christian-kohler.path-intellisense", 10 | "dbaeumer.vscode-eslint", 11 | "esbenp.prettier-vscode", 12 | "Orta.vscode-jest", 13 | "SonarSource.sonarlint-vscode", 14 | "streetsidesoftware.code-spell-checker" 15 | ], 16 | "settings": { 17 | "extensions.ignoreRecommendations": true 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | root: true, 4 | extends: ['plugin:react/recommended', 'prettier'], 5 | plugins: ['@typescript-eslint', 'no-only-tests', 'react'], 6 | parserOptions: { 7 | ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features 8 | sourceType: 'module', // Allows for the use of imports 9 | ecmaFeatures: { 10 | modules: true, 11 | }, 12 | }, 13 | env: { 14 | node: true, 15 | es6: true, 16 | jest: true, 17 | browser: true, 18 | }, 19 | settings: { 20 | react: { 21 | version: 'detect', 22 | }, 23 | }, 24 | rules: { 25 | // Routes 26 | 'no-console': ['error', { allow: ['warn', 'error'] }], 27 | '@typescript-eslint/no-unused-vars': [ 28 | 'error', 29 | { 30 | args: 'all', 31 | argsIgnorePattern: '^_', 32 | caughtErrors: 'all', 33 | caughtErrorsIgnorePattern: '^_', 34 | destructuredArrayIgnorePattern: '^_', 35 | varsIgnorePattern: '^_', 36 | ignoreRestSiblings: true, 37 | }, 38 | ], 39 | 40 | // UI 41 | 'react/prop-types': 'off', 42 | 'react/display-name': 'off', 43 | }, 44 | overrides: [ 45 | { 46 | // enable the rule specifically for TypeScript files 47 | files: ['*.{ts,tsx}'], 48 | extends: ['plugin:@typescript-eslint/recommended'], 49 | rules: { 50 | '@typescript-eslint/no-non-null-assertion': 'off', 51 | '@typescript-eslint/explicit-function-return-type': 'off', 52 | '@typescript-eslint/explicit-module-boundary-types': 'off', 53 | '@typescript-eslint/no-use-before-define': 'off', 54 | '@typescript-eslint/no-explicit-any': 'off', 55 | '@typescript-eslint/no-empty-interface': 'off', 56 | }, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [felixmosh] 4 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | services: 16 | # Label used to access the service container 17 | redis: 18 | # Docker Hub image 19 | image: redis 20 | # Set health checks to wait until redis has started 21 | options: >- 22 | --health-cmd "redis-cli ping" 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | ports: 27 | # Maps port 6379 on service container to the host 28 | - 6379:6379 29 | 30 | strategy: 31 | matrix: 32 | node: [ '20.x', '22.x' ] 33 | os: [ubuntu-latest] 34 | 35 | steps: 36 | - name: Checkout repo 37 | uses: actions/checkout@v3 38 | 39 | - name: Use Node ${{ matrix.node }} 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: ${{ matrix.node }} 43 | cache: 'yarn' 44 | 45 | - name: Install deps 46 | run: yarn install --immutable 47 | env: 48 | CI: true 49 | 50 | - name: Lint 51 | run: yarn lint 52 | 53 | - name: Build 54 | run: yarn build 55 | env: 56 | CI: true 57 | 58 | - name: Test 59 | run: yarn test 60 | env: 61 | CI: true 62 | REDIS_HOST: localhost 63 | # The default Redis port 64 | REDIS_PORT: 6379 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | yarn-error.log 5 | *.rdb 6 | website/build 7 | docker-compose.dockest-generated.yml 8 | dockest-error.json 9 | .idea/ 10 | 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/sdks 16 | !.yarn/versions 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /app/website 4 | 5 | EXPOSE 3000 35729 6 | COPY ./docs /app/docs 7 | COPY ./website /app/website 8 | RUN yarn install 9 | 10 | CMD ["yarn", "start"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vitor Capretz (capretzvitor@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docker-compose.redis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:latest 4 | hostname: redis 5 | restart: unless-stopped 6 | ports: 7 | - 6379:6379 8 | volumes: 9 | - redis_data:/data 10 | volumes: 11 | redis_data: 12 | -------------------------------------------------------------------------------- /examples/with-elysia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-elysia", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "Example of how to use Elysia server with bull-board", 6 | "module": "index.ts", 7 | "scripts": { 8 | "dev": "bun --watch index.ts", 9 | "build": "bun build ./index.ts --target bun --outdir ./dist --sourcemap=linked", 10 | "start": "bun ./dist/index.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "kravetsone", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@bull-board/elysia": "^6.9.3", 17 | "bullmq": "^5.13.2", 18 | "elysia": "^1.1.24" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-elysia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "allowSyntheticDefaultImports": true, 11 | "noUncheckedIndexedAccess": true, 12 | "verbatimModuleSyntax": true, 13 | "rootDir": "./src", 14 | "noEmit": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-express-auth/README.md: -------------------------------------------------------------------------------- 1 | # Local passport auth example 2 | 3 | This example shows how to secure your bull-board using local passport strategy. 4 | 5 | ### Notes 6 | 1. It will work with any **cookie** based auth, since the browser will attach 7 | the `session` cookie automatically to **each** request. 8 | 9 | 10 | Based on: https://github.com/passport/express-4.x-local-example/blob/master/server.js 11 | -------------------------------------------------------------------------------- /examples/with-express-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-auth-example", 3 | "version": "1.0.0", 4 | "description": "Example of how to use passport.js & custom login page with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/express": "^6.0.0", 14 | "body-parser": "^1.20.3", 15 | "bullmq": "^5.13.2", 16 | "connect-ensure-login": "^0.1.1", 17 | "express": "^4.21.2", 18 | "express-session": "^1.18.0", 19 | "passport": "^0.7.0", 20 | "passport-local": "^1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-express-csrf/README.md: -------------------------------------------------------------------------------- 1 | # Express with csrf protection example 2 | 3 | This example shows how to use [Express.js](https://expressjs.com/) as a server for bull-board and enable csrf protection using [csrf-csrf](https://github.com/Psifi-Solutions/csrf-csrf) lib. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-express-csrf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-express-with-csrf-example", 3 | "version": "1.0.0", 4 | "description": "Example of how to use bull-board on existing Express server with csrf protection", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/express": "^6.0.0", 14 | "bullmq": "^5.13.2", 15 | "cookie-parser": "^1.4.6", 16 | "csrf-csrf": "^3.0.8", 17 | "express": "^4.21.2" 18 | }, 19 | "devDependencies": { 20 | "nodemon": "^3.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-express/README.md: -------------------------------------------------------------------------------- 1 | # Express example 2 | 3 | This example shows how to use [Express.js](https://expressjs.com/) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-express-example", 3 | "version": "1.0.0", 4 | "description": "Example of how to install bull-board on existing Express server", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/express": "^6.0.0", 14 | "bullmq": "^5.13.2", 15 | "express": "^4.21.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/with-fastify-auth/README.md: -------------------------------------------------------------------------------- 1 | # Fastify example 2 | 3 | This example shows how to use [Fastify.js](https://www.fastify.io/) as a server for bull-board. 4 | 5 | ### Notes 6 | 1. It will work with any **cookie** / **basic auth** based auth, since the browser will attach 7 | the `session` cookie / basic auth header automatically to **each** request. 8 | 9 | 10 | ### Usage with Basic Auth 11 | 1. Navigate to `/basic/login` 12 | 2. Fill in username: `bull` & password: `board` 13 | 14 | *Based on: https://github.com/fastify/fastify-basic-auth* 15 | 16 | ### Usage with Cookie Auth 17 | 1. Navigate to `/cookie/login` 18 | 2. Fill in username: `bull` & password: `board` 19 | 20 | -------------------------------------------------------------------------------- /examples/with-fastify-auth/basicAuth.js: -------------------------------------------------------------------------------- 1 | const { FastifyAdapter } = require('@bull-board/fastify'); 2 | const { createBullBoard } = require('@bull-board/api'); 3 | const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter'); 4 | 5 | module.exports.basicAuth = function basicAuth(fastify, { queue }, next) { 6 | const authenticate = { realm: 'Bull-Board' }; 7 | function validate(username, password, req, reply, done) { 8 | if (username === 'bull' && password === 'board') { 9 | done(); 10 | } else { 11 | done(new Error('Unauthorized')); 12 | } 13 | } 14 | 15 | fastify.register(require('@fastify/basic-auth'), { validate, authenticate }); 16 | 17 | fastify.after(() => { 18 | const serverAdapter = new FastifyAdapter(); 19 | 20 | createBullBoard({ 21 | queues: [new BullMQAdapter(queue)], 22 | serverAdapter, 23 | }); 24 | 25 | serverAdapter.setBasePath('/basic/ui'); 26 | fastify.register(serverAdapter.registerPlugin(), { prefix: '/basic/ui' }); 27 | fastify.route({ 28 | method: 'GET', 29 | url: '/basic/login', 30 | handler: async (req, reply) => { 31 | reply.redirect('/basic/ui'); 32 | }, 33 | }); 34 | fastify.addHook('onRequest', (req, reply, next) => { 35 | fastify.basicAuth(req, reply, function (error) { 36 | if (!error) { 37 | return next(); 38 | } 39 | 40 | reply.code(error.statusCode || 500 >= 400).send({ error: error.name }); 41 | }); 42 | }); 43 | }); 44 | 45 | next(); 46 | }; 47 | -------------------------------------------------------------------------------- /examples/with-fastify-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-fastify", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Fastify server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/api": "^6.0.0", 14 | "@bull-board/fastify": "^6.0.0", 15 | "@fastify/basic-auth": "^6.0.1", 16 | "@fastify/cookie": "^10.0.1", 17 | "@fastify/jwt": "^9.0.1", 18 | "@fastify/view": "^10.0.1", 19 | "bullmq": "^5.13.2", 20 | "fastify": "^5.3.2" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^2.0.16" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/with-fastify/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | docker-compose.yml 3 | .env -------------------------------------------------------------------------------- /examples/with-fastify/.env.example: -------------------------------------------------------------------------------- 1 | REDIS_HOST=localhost 2 | REDIS_PORT=6379 3 | REDIS_PASS= 4 | BULL_QUEUE_NAMES_CSV=MonitoredQueue1,MonitoredQueue2,MonitoredQueue3 -------------------------------------------------------------------------------- /examples/with-fastify/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /examples/with-fastify/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | 3 | WORKDIR /app/website 4 | 5 | EXPOSE 3000 6 | COPY . /app/website 7 | RUN yarn install 8 | 9 | CMD ["yarn", "start"] 10 | -------------------------------------------------------------------------------- /examples/with-fastify/README.md: -------------------------------------------------------------------------------- 1 | # Fastify example 2 | 3 | This example shows how to use [Fastify.js](https://www.fastify.io/) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-fastify/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bullboard: 3 | build: . 4 | env_file: 5 | - .env 6 | 7 | ports: 8 | - 3333:3000 9 | -------------------------------------------------------------------------------- /examples/with-fastify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-fastify", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Fastify server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/fastify": "^6.0.0", 14 | "@bull-board/api": "^6.0.0", 15 | "bullmq": "^5.13.2", 16 | "fastify": "^5.3.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-h3/README.md: -------------------------------------------------------------------------------- 1 | # H3 example 2 | 3 | This example shows how to use [H3](https://github.com/unjs/h3) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-h3/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp, createRouter, eventHandler } from 'h3'; 2 | import { createBullBoard } from '@bull-board/api'; 3 | import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; 4 | import { H3Adapter } from '@bull-board/h3'; 5 | import { Queue as QueueMQ, RedisOptions, Worker } from 'bullmq'; 6 | 7 | const sleep = (t: number) => new Promise((resolve) => setTimeout(resolve, t * 1000)); 8 | 9 | const serverAdapter = new H3Adapter(); 10 | serverAdapter.setBasePath('/ui'); 11 | 12 | export const app = createApp(); 13 | 14 | const router = createRouter(); 15 | 16 | const redisOptions: RedisOptions = { 17 | port: 6379, 18 | host: 'localhost', 19 | password: '', 20 | }; 21 | 22 | const createQueueMQ = (name: string) => new QueueMQ(name, { connection: redisOptions }); 23 | 24 | async function setupBullMQProcessor(queueName: string) { 25 | new Worker( 26 | queueName, 27 | async (job) => { 28 | for (let i = 0; i <= 100; i++) { 29 | await sleep(Math.random()); 30 | await job.updateProgress(i); 31 | await job.log(`Processing job at interval ${i}`); 32 | 33 | if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`); 34 | } 35 | 36 | return { jobId: `This is the return value of job (${job.id})` }; 37 | }, 38 | { connection: redisOptions } 39 | ); 40 | } 41 | 42 | const exampleBullMq = createQueueMQ('BullMQ'); 43 | setupBullMQProcessor(exampleBullMq.name); 44 | 45 | createBullBoard({ 46 | queues: [new BullMQAdapter(exampleBullMq)], 47 | serverAdapter, 48 | }); 49 | 50 | app.use(router); 51 | app.use(serverAdapter.registerHandlers()); 52 | 53 | router.use( 54 | '/add', 55 | eventHandler(async () => { 56 | await exampleBullMq.add('myJob', { foo: 'bar' }); 57 | 58 | return true; 59 | }) 60 | ); 61 | -------------------------------------------------------------------------------- /examples/with-h3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-h3", 3 | "version": "1.0.0", 4 | "description": "Example of how to use H3 server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "listhen index.ts", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "genu", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/h3": "*", 14 | "bullmq": "^5.13.2", 15 | "listhen": "^1.8.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/with-hapi-auth/README.md: -------------------------------------------------------------------------------- 1 | # Hapi with auth example 2 | 3 | This example shows how to use [Hapi.js](https://hapi.dev/) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-hapi-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-hapi", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Hapi.js with auth server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/hapi": "^6.0.0", 14 | "@hapi/basic": "^7.0.2", 15 | "@hapi/hapi": "^21.3.10", 16 | "bullmq": "^5.13.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-hapi/README.md: -------------------------------------------------------------------------------- 1 | # Hapi example 2 | 3 | This example shows how to use [Hapi.js](https://hapi.dev/) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-hapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-hapi", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Hapi.js server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/hapi": "^6.0.0", 14 | "@hapi/hapi": "^21.3.10", 15 | "bullmq": "^5.13.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/with-hono/README.md: -------------------------------------------------------------------------------- 1 | # Hono example 2 | 3 | This example shows how to use [Hono](https://hono.dev) as a server for bull-board. 4 | -------------------------------------------------------------------------------- /examples/with-hono/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-hono", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Hono server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/hono": "^6.0.0", 14 | "@hono/node-server": "^1.13.1", 15 | "bullmq": "^5.13.2", 16 | "hono": "^4.6.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-koa/README.md: -------------------------------------------------------------------------------- 1 | # Koa example 2 | 3 | This example shows how to use [Koa.js](https://koajs.com/) as a server for bull-board. 4 | 5 | -------------------------------------------------------------------------------- /examples/with-koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-with-koa", 3 | "version": "1.0.0", 4 | "description": "Example of how to use Hapi.js server with bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/koa": "^6.2.4", 14 | "bullmq": "^5.21.2", 15 | "koa": "^2.16.1", 16 | "koa-router": "^13.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-multiple-instances/README.md: -------------------------------------------------------------------------------- 1 | # Multiple instances of `bull-board` example 2 | 3 | This example shows how to install multiple instances of your `bull-board`. 4 | -------------------------------------------------------------------------------- /examples/with-multiple-instances/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bull-board-multi-instance-example", 3 | "version": "1.0.0", 4 | "description": "Example of how to install multiple instances of bull-board", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "felixmosh", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@bull-board/express": "^6.0.0", 14 | "body-parser": "^1.20.3", 15 | "bullmq": "^5.13.2", 16 | "express": "^4.21.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['eslint.config.mjs'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-floating-promises': 'warn', 32 | '@typescript-eslint/no-unsafe-argument': 'warn' 33 | }, 34 | }, 35 | ); -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import { 3 | Controller, 4 | Get, 5 | Post, 6 | Request, 7 | Response, 8 | UseFilters, 9 | UseGuards, 10 | } from '@nestjs/common'; 11 | import { AppService } from './app.service'; 12 | import { FastifyReply, FastifyRequest } from 'fastify'; 13 | import { AuthGuard } from '@nestjs/passport'; 14 | import loginPageTemplate from './view/login'; 15 | import { AuthExceptionFilter } from './exception-filter'; 16 | 17 | @Controller() 18 | export class AppController { 19 | constructor(private readonly appService: AppService) {} 20 | 21 | @Get() 22 | getHello(): string { 23 | return this.appService.getHello(); 24 | } 25 | 26 | @Get('login') 27 | loginPage(@Request() req: FastifyRequest, @Response() res: FastifyReply) { 28 | res.type('text/html').send(loginPageTemplate(!!(req.query as any).invalid)); 29 | } 30 | 31 | @Post('login') 32 | @UseFilters(AuthExceptionFilter) 33 | @UseGuards(AuthGuard('local')) 34 | login(@Request() req: FastifyRequest, @Response() reply: FastifyReply) { 35 | req.session.set('sev-data' as never, (req as any)?.user as never); 36 | return reply.status(302).redirect('/queues'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { QueueModule } from './queues/queue.module'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { LocalStrategy } from './bullboard-auth.local.strategy'; 7 | 8 | @Module({ 9 | imports: [PassportModule.register({ session: true }), QueueModule.register()], 10 | controllers: [AppController], 11 | providers: [AppService, LocalStrategy], 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/bullboard-auth.local.strategy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | import { Strategy } from 'passport-local'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 6 | 7 | const { BULLBOARD_USER, BULLBOARD_PASSWORD } = process.env; 8 | 9 | @Injectable() 10 | export class LocalStrategy extends PassportStrategy(Strategy) { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | validate(username: string, password: string, done: any): void { 16 | if (username === BULLBOARD_USER && password === BULLBOARD_PASSWORD) { 17 | return done(null, { user: username }); 18 | } 19 | return done(new UnauthorizedException('Invalid credentials'), false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/exception-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { FastifyReply } from 'fastify'; 8 | 9 | @Catch(UnauthorizedException) 10 | export class AuthExceptionFilter implements ExceptionFilter { 11 | catch(exception: UnauthorizedException, host: ArgumentsHost): void { 12 | const ctx = host.switchToHttp(); 13 | const response = ctx.getResponse(); 14 | response.redirect(`/login?invalid=true`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(process.env.PORT ?? 3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/src/queues/test.processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Processor, 3 | WorkerHost, 4 | OnWorkerEvent, 5 | InjectQueue, 6 | } from '@nestjs/bullmq'; 7 | import { Logger } from '@nestjs/common'; 8 | import { Job } from 'bullmq'; 9 | 10 | export const TEST_QUEUE_NAME = 'test'; 11 | export const InjectTestQueue = (): ParameterDecorator => 12 | InjectQueue(TEST_QUEUE_NAME); 13 | 14 | @Processor(TEST_QUEUE_NAME, { 15 | concurrency: 3, 16 | }) 17 | export class TestProcessor extends WorkerHost { 18 | private readonly logger = new Logger(TestProcessor.name); 19 | 20 | async process(job: Job<{ fail: boolean }, any, string>): Promise { 21 | const fail = job.data.fail; 22 | 23 | this.logger.log(`Processing ${job.id}`); 24 | 25 | await new Promise((resolve, reject) => { 26 | setTimeout(() => { 27 | if (fail) { 28 | return reject(new Error('Failed')); 29 | } 30 | 31 | return resolve('Success'); 32 | }, 5_000); 33 | }); 34 | } 35 | 36 | @OnWorkerEvent('active') 37 | onActive(job: Job) { 38 | this.logger.log(`Active ${job.id}`); 39 | } 40 | 41 | @OnWorkerEvent('completed') 42 | onCompleted(job: Job) { 43 | this.logger.log(`Completed ${job.id}`); 44 | } 45 | 46 | @OnWorkerEvent('failed') 47 | onFailed(job: Job) { 48 | this.logger.log(`Failed ${job.id}`); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-nestjs-fastify-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /examples/with-nestjs-module/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:6.2-alpine 4 | ports: 5 | - '6379:6379' 6 | command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 -------------------------------------------------------------------------------- /examples/with-nestjs-module/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bullboard-with-nestjs-module", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main" 16 | }, 17 | "dependencies": { 18 | "@bull-board/api": "^6.0.0", 19 | "@bull-board/express": "^6.0.0", 20 | "@bull-board/nestjs": "^6.0.0", 21 | "@nestjs/bullmq": "^11.0.2", 22 | "@nestjs/common": "^11.0.16", 23 | "@nestjs/core": "^11.0.13", 24 | "@nestjs/platform-express": "^11.0.13", 25 | "bullmq": "^5.13.2", 26 | "reflect-metadata": "^0.2.2", 27 | "rimraf": "^6.0.1", 28 | "rxjs": "^7.8.1" 29 | }, 30 | "devDependencies": { 31 | "@nestjs/cli": "^8.0.0", 32 | "@nestjs/schematics": "^8.0.0", 33 | "@nestjs/testing": "^11.0.13", 34 | "@types/express": "^4.17.13", 35 | "@types/node": "^16.0.0", 36 | "jest": "^27.2.5", 37 | "source-map-support": "^0.5.20", 38 | "ts-loader": "^9.2.3", 39 | "ts-node": "^10.0.0", 40 | "tsconfig-paths": "^3.10.1", 41 | "typescript": "^4.3.5" 42 | }, 43 | "jest": { 44 | "moduleFileExtensions": [ 45 | "js", 46 | "json", 47 | "ts" 48 | ], 49 | "rootDir": "src", 50 | "testRegex": ".*\\.spec\\.ts$", 51 | "transform": { 52 | "^.+\\.(t|j)s$": "ts-jest" 53 | }, 54 | "collectCoverageFrom": [ 55 | "**/*.(t|j)s" 56 | ], 57 | "coverageDirectory": "../coverage", 58 | "testEnvironment": "node" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { BullModule } from "@nestjs/bullmq"; 3 | import { BullBoardModule } from "@bull-board/nestjs"; 4 | import { FeatureModule } from "./feature/feature.module"; 5 | import { ExpressAdapter } from "@bull-board/express"; 6 | 7 | @Module({ 8 | imports: [ 9 | // infrastructure from here 10 | BullModule.forRoot({ 11 | connection: { 12 | host: "localhost", 13 | port: 6379, 14 | username: "default", 15 | password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81" //defined in the docker compose yml 16 | } 17 | }), 18 | 19 | //register the bull-board module forRoot in your app.module 20 | BullBoardModule.forRoot({ 21 | route: "/queues", 22 | adapter: ExpressAdapter 23 | }), 24 | 25 | //feature modules from here. 26 | FeatureModule 27 | ], 28 | controllers: [], 29 | providers: [] 30 | }) 31 | export class AppModule { 32 | } 33 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/src/feature/feature.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from "@nestjs/common"; 2 | import { BullBoardInstance, InjectBullBoard } from "@bull-board/nestjs"; 3 | 4 | @Controller('my-feature') 5 | export class FeatureController { 6 | 7 | constructor( 8 | //inject the bull-board instance using the provided decorator 9 | @InjectBullBoard() private readonly boardInstance: BullBoardInstance 10 | ) { 11 | } 12 | 13 | @Get() 14 | getFeature() { 15 | // You can do anything from here with the boardInstance for example: 16 | 17 | //this.boardInstance.replaceQueues(); 18 | //this.boardInstance.addQueue(); 19 | //this.boardInstance.setQueues(); 20 | 21 | return 'ok'; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /examples/with-nestjs-module/src/feature/feature.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { BullBoardModule } from "@bull-board/nestjs"; 3 | import { BullModule } from "@nestjs/bullmq"; 4 | import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; 5 | 6 | // example feature module, feature can be anything. eg. user module 7 | @Module({ 8 | imports: [ 9 | BullModule.registerQueue({ 10 | name: 'feature_queue' 11 | }), 12 | 13 | //Register each queue using the `forFeature` method. 14 | BullBoardModule.forFeature({ 15 | name: 'feature_queue', 16 | adapter: BullMQAdapter 17 | }) 18 | ] 19 | }) 20 | export class FeatureModule {} -------------------------------------------------------------------------------- /examples/with-nestjs-module/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-nestjs-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-nestjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /examples/with-nestjs/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /examples/with-nestjs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /examples/with-nestjs/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | addToQueue(@Query('fail') fail: string) { 10 | return this.appService.addToQueue(fail ? true : false); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { QueuesModule } from './queues/queues.module'; 6 | 7 | @Module({ 8 | imports: [QueuesModule.register()], 9 | controllers: [AppController], 10 | providers: [AppService], 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Queue } from 'bullmq'; 3 | 4 | import { InjectTestQueue } from './queues/test.processor'; 5 | 6 | @Injectable() 7 | export class AppService { 8 | constructor(@InjectTestQueue() readonly testQueue: Queue) {} 9 | 10 | addToQueue(fail: boolean) { 11 | this.testQueue.add('123', { fail }); 12 | return 'OK'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/queues/basic-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import * as bcrypt from 'bcrypt'; 4 | import { NextFunction, Request, Response } from 'express'; 5 | 6 | @Injectable() 7 | export class BasicAuthMiddleware implements NestMiddleware { 8 | private readonly username: string; 9 | private readonly passwordHash: string; 10 | 11 | constructor(private readonly configService: ConfigService) { 12 | this.username = this.configService.get('BULL_BOARD_USERNAME') || ''; 13 | this.passwordHash = 14 | this.configService.get('BULL_BOARD_PASSWORD_HASH') || ''; 15 | } 16 | 17 | async use(req: Request, res: Response, next: NextFunction): Promise { 18 | const authHeader = req.get('authorization'); 19 | 20 | if (!authHeader?.startsWith('Basic ')) { 21 | this.sendUnauthorizedResponse(res); 22 | return; 23 | } 24 | 25 | const encodedCreds = authHeader.split(' ')[1]; 26 | const decodedCreds = Buffer.from(encodedCreds, 'base64').toString('utf-8'); 27 | const [username, password] = decodedCreds.split(':'); 28 | 29 | if (!this.username || !this.passwordHash || username !== this.username) { 30 | this.sendUnauthorizedResponse(res); 31 | return; 32 | } 33 | 34 | const isPasswordValid = await bcrypt.compare(password, this.passwordHash); 35 | 36 | if (!isPasswordValid) { 37 | this.sendUnauthorizedResponse(res); 38 | return; 39 | } 40 | 41 | next(); 42 | } 43 | 44 | private sendUnauthorizedResponse(res: Response): void { 45 | res.setHeader( 46 | 'WWW-Authenticate', 47 | 'Basic realm="Restricted Area", charset="UTF-8"', 48 | ); 49 | res.sendStatus(401); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/queues/queues.module.ts: -------------------------------------------------------------------------------- 1 | import { createBullBoard } from '@bull-board/api'; 2 | import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; 3 | import { ExpressAdapter } from '@bull-board/express'; 4 | import { BullModule } from '@nestjs/bullmq'; 5 | import { 6 | DynamicModule, 7 | MiddlewareConsumer, 8 | Module, 9 | NestModule, 10 | } from '@nestjs/common'; 11 | import { Queue } from 'bullmq'; 12 | 13 | import { ConfigModule } from '@nestjs/config'; 14 | import { BasicAuthMiddleware } from './basic-auth.middleware'; 15 | import { 16 | InjectTestQueue, 17 | TEST_QUEUE_NAME, 18 | TestProcessor, 19 | } from './test.processor'; 20 | 21 | @Module({ 22 | imports: [ConfigModule], 23 | }) 24 | export class QueuesModule implements NestModule { 25 | static register(): DynamicModule { 26 | const testQueue = BullModule.registerQueue({ 27 | name: TEST_QUEUE_NAME, 28 | }); 29 | 30 | if (!testQueue.providers || !testQueue.exports) { 31 | throw new Error('Unable to build queue'); 32 | } 33 | 34 | return { 35 | module: QueuesModule, 36 | imports: [ 37 | BullModule.forRoot({ 38 | connection: { 39 | host: 'localhost', 40 | port: 15610, 41 | }, 42 | defaultJobOptions: { 43 | attempts: 3, 44 | backoff: { 45 | type: 'exponential', 46 | delay: 1000, 47 | }, 48 | }, 49 | }), 50 | testQueue, 51 | ], 52 | providers: [TestProcessor, ...testQueue.providers], 53 | exports: [...testQueue.exports], 54 | }; 55 | } 56 | 57 | constructor(@InjectTestQueue() private readonly testQueue: Queue) {} 58 | 59 | configure(consumer: MiddlewareConsumer) { 60 | const serverAdapter = new ExpressAdapter(); 61 | serverAdapter.setBasePath('/queues'); 62 | 63 | createBullBoard({ 64 | queues: [new BullMQAdapter(this.testQueue)], 65 | serverAdapter, 66 | }); 67 | 68 | consumer 69 | .apply(BasicAuthMiddleware, serverAdapter.getRouter()) 70 | .forRoutes('/queues'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/with-nestjs/src/queues/test.processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Processor, 3 | WorkerHost, 4 | OnWorkerEvent, 5 | InjectQueue, 6 | } from '@nestjs/bullmq'; 7 | import { Logger } from '@nestjs/common'; 8 | import { Job } from 'bullmq'; 9 | 10 | export const TEST_QUEUE_NAME = 'test'; 11 | export const InjectTestQueue = (): ParameterDecorator => 12 | InjectQueue(TEST_QUEUE_NAME); 13 | 14 | @Processor(TEST_QUEUE_NAME, { 15 | concurrency: 3, 16 | }) 17 | export class TestProcessor extends WorkerHost { 18 | private readonly logger = new Logger(TestProcessor.name); 19 | 20 | async process(job: Job): Promise { 21 | const fail = job.data.fail; 22 | 23 | this.logger.log(`Processing ${job.id}`); 24 | 25 | await new Promise((resolve, reject) => { 26 | setTimeout(() => { 27 | if (fail) { 28 | return reject(new Error('Failed')); 29 | } 30 | 31 | return resolve('Success'); 32 | }, 5_000); 33 | }); 34 | } 35 | 36 | @OnWorkerEvent('active') 37 | onActive(job: Job) { 38 | this.logger.log(`Active ${job.id}`); 39 | } 40 | 41 | @OnWorkerEvent('completed') 42 | onCompleted(job: Job) { 43 | this.logger.log(`Completed ${job.id}`); 44 | } 45 | 46 | @OnWorkerEvent('failed') 47 | onFailed(job: Job) { 48 | this.logger.log(`Failed ${job.id}`); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/with-nestjs/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "noImplicitAny": true, 17 | "strictBindCallApply": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noFallthroughCasesInSwitch": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/api 2 | 3 | Core server APIs of `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 21 | -------------------------------------------------------------------------------- /packages/api/bullAdapter.d.ts: -------------------------------------------------------------------------------- 1 | export { BullAdapter } from './dist/src/queueAdapters/bull'; 2 | -------------------------------------------------------------------------------- /packages/api/bullAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/src/queueAdapters/bull'); 2 | -------------------------------------------------------------------------------- /packages/api/bullMQAdapter.d.ts: -------------------------------------------------------------------------------- 1 | export { BullMQAdapter } from './dist/src/queueAdapters/bullMQ'; 2 | -------------------------------------------------------------------------------- /packages/api/bullMQAdapter.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/src/queueAdapters/bullMQ'); 2 | -------------------------------------------------------------------------------- /packages/api/jest.config.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('./package.json'); 2 | const { defaults: tsJestTransform } = require('ts-jest/presets'); 3 | 4 | module.exports = { 5 | displayName: packageJson.name, 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | transform: { 9 | ...tsJestTransform.transform, 10 | }, 11 | testPathIgnorePatterns: ['/node_modules/'], 12 | testMatch: ['/tests/**/*.spec.ts'], 13 | }; 14 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/api", 3 | "version": "6.9.6", 4 | "description": "A Dashboard server API built on top of bull or bullmq.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "queue", 10 | "monitoring", 11 | "dashboard" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/felixmosh/bull-board.git", 16 | "directory": "packages/api" 17 | }, 18 | "license": "MIT", 19 | "author": "felixmosh", 20 | "main": "dist/src/index.js", 21 | "files": [ 22 | "dist", 23 | "*Adapter.*" 24 | ], 25 | "scripts": { 26 | "build": "tsc", 27 | "clean": "rm -rf dist", 28 | "test": "jest" 29 | }, 30 | "dependencies": { 31 | "redis-info": "^3.1.0" 32 | }, 33 | "devDependencies": { 34 | "@types/redis-info": "^3.0.3", 35 | "@types/supertest": "^2.0.16", 36 | "bull": "^4.16.5", 37 | "bullmq": "^5.52.1", 38 | "ioredis": "^5.6.1", 39 | "supertest": "^7.1.0" 40 | }, 41 | "peerDependencies": { 42 | "@bull-board/ui": "6.9.6" 43 | }, 44 | "publishConfig": { 45 | "access": "public" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/api/src/constants/statuses.ts: -------------------------------------------------------------------------------- 1 | export const STATUSES = { 2 | latest: 'latest', 3 | active: 'active', 4 | waiting: 'waiting', 5 | waitingChildren: 'waiting-children', 6 | prioritized: 'prioritized', 7 | completed: 'completed', 8 | failed: 'failed', 9 | delayed: 'delayed', 10 | paused: 'paused', 11 | } as const; 12 | -------------------------------------------------------------------------------- /packages/api/src/handlers/addJob.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../queueAdapters/base'; 2 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 3 | import { queueProvider } from '../providers/queue'; 4 | import { formatJob } from './queues'; 5 | 6 | async function addJob( 7 | req: BullBoardRequest, 8 | queue: BaseAdapter 9 | ): Promise { 10 | const { name, data, options } = req.body; 11 | 12 | const job = await queue.addJob(name, data, options); 13 | 14 | return { 15 | status: 200, 16 | body: { 17 | job: formatJob(job, queue), 18 | status: job.getState(), 19 | }, 20 | }; 21 | } 22 | 23 | export const addJobHandler = queueProvider(addJob); 24 | -------------------------------------------------------------------------------- /packages/api/src/handlers/cleanAll.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../queueAdapters/base'; 2 | import { 3 | BullBoardRequest, 4 | ControllerHandlerReturnType, 5 | } from '../../typings/app'; 6 | import { queueProvider } from '../providers/queue'; 7 | 8 | async function cleanAll( 9 | req: BullBoardRequest, 10 | queue: BaseAdapter 11 | ): Promise { 12 | const { queueStatus } = req.params; 13 | 14 | const GRACE_TIME_MS = 5000; 15 | 16 | await queue.clean(queueStatus as any, GRACE_TIME_MS); 17 | 18 | return { 19 | status: 200, 20 | body: {}, 21 | }; 22 | } 23 | 24 | export const cleanAllHandler = queueProvider(cleanAll); 25 | -------------------------------------------------------------------------------- /packages/api/src/handlers/cleanJob.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BullBoardRequest, 3 | ControllerHandlerReturnType, 4 | QueueJob, 5 | } from '../../typings/app'; 6 | import { jobProvider } from '../providers/job'; 7 | import { queueProvider } from '../providers/queue'; 8 | 9 | async function cleanJob( 10 | _req: BullBoardRequest, 11 | job: QueueJob 12 | ): Promise { 13 | await job.remove(); 14 | 15 | return { 16 | status: 204, 17 | body: {}, 18 | }; 19 | } 20 | 21 | export const cleanJobHandler = queueProvider(jobProvider(cleanJob)); 22 | -------------------------------------------------------------------------------- /packages/api/src/handlers/emptyQueue.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | import { queueProvider } from '../providers/queue'; 3 | import { BaseAdapter } from '../queueAdapters/base'; 4 | 5 | async function emptyQueue( 6 | _req: BullBoardRequest, 7 | queue: BaseAdapter 8 | ): Promise { 9 | await queue.empty(); 10 | 11 | return { status: 200, body: {} }; 12 | } 13 | 14 | export const emptyQueueHandler = queueProvider(emptyQueue); 15 | -------------------------------------------------------------------------------- /packages/api/src/handlers/entryPoint.ts: -------------------------------------------------------------------------------- 1 | import { UIConfig, ViewHandlerReturnType } from '../../typings/app'; 2 | 3 | export function entryPoint(params: { 4 | basePath: string; 5 | uiConfig: UIConfig; 6 | }): ViewHandlerReturnType { 7 | const basePath = params.basePath.endsWith('/') ? params.basePath : `${params.basePath}/`; 8 | const uiConfig = JSON.stringify(params.uiConfig) 9 | .replace(//g, '\\u003e'); 11 | 12 | return { 13 | name: 'index.ejs', 14 | params: { 15 | basePath, 16 | uiConfig, 17 | title: params.uiConfig.boardTitle as string, 18 | favIconDefault: params.uiConfig.favIcon?.default as string, 19 | favIconAlternative: params.uiConfig.favIcon?.alternative as string, 20 | }, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/api/src/handlers/error.ts: -------------------------------------------------------------------------------- 1 | import { ControllerHandlerReturnType, HTTPStatus } from '../../typings/app'; 2 | 3 | export function errorHandler( 4 | error: Error & { statusCode?: HTTPStatus } 5 | ): ControllerHandlerReturnType { 6 | return { 7 | status: error.statusCode || 500, 8 | body: { 9 | error: 'Internal server error', 10 | message: error.message, 11 | details: process.env.NODE_ENV === 'development' ? error.stack : undefined, 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/handlers/job.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType, QueueJob } from '../../typings/app'; 2 | import { queueProvider } from '../providers/queue'; 3 | import { jobProvider } from '../providers/job'; 4 | import { BaseAdapter } from '../queueAdapters/base'; 5 | import { formatJob } from './queues'; 6 | 7 | async function getJobState( 8 | _req: BullBoardRequest, 9 | job: QueueJob, 10 | queue: BaseAdapter 11 | ): Promise { 12 | const status = await job.getState(); 13 | 14 | return { 15 | status: 200, 16 | body: { 17 | job: formatJob(job, queue), 18 | status, 19 | }, 20 | }; 21 | } 22 | 23 | export const jobHandler = queueProvider(jobProvider(getJobState), { 24 | skipReadOnlyModeCheck: true, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/api/src/handlers/jobLogs.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../queueAdapters/base'; 2 | import { 3 | BullBoardRequest, 4 | ControllerHandlerReturnType, 5 | } from '../../typings/app'; 6 | import { queueProvider } from '../providers/queue'; 7 | 8 | async function jobLogs( 9 | req: BullBoardRequest, 10 | queue: BaseAdapter 11 | ): Promise { 12 | const { jobId } = req.params; 13 | const logs = await queue.getJobLogs(jobId); 14 | 15 | return { 16 | status: 200, 17 | body: logs, 18 | }; 19 | } 20 | 21 | export const jobLogsHandler = queueProvider(jobLogs, { 22 | skipReadOnlyModeCheck: true, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/api/src/handlers/pauseAll.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | 3 | async function pauseAll(req: BullBoardRequest): Promise { 4 | const relevantQueues = Array.from(req.queues.values()).filter((queue) => !queue.readOnlyMode); 5 | for (const queue of relevantQueues) { 6 | const isPaused = await queue.isPaused(); 7 | if (!isPaused) { 8 | await queue.pause(); 9 | } 10 | } 11 | 12 | return { status: 200, body: { message: 'All queues paused' } }; 13 | } 14 | 15 | export const pauseAllHandler = pauseAll; 16 | -------------------------------------------------------------------------------- /packages/api/src/handlers/pauseQueue.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | import { queueProvider } from '../providers/queue'; 3 | import { BaseAdapter } from '../queueAdapters/base'; 4 | 5 | async function pauseQueue( 6 | _req: BullBoardRequest, 7 | queue: BaseAdapter 8 | ): Promise { 9 | await queue.pause(); 10 | 11 | return { status: 200, body: {} }; 12 | } 13 | 14 | export const pauseQueueHandler = queueProvider(pauseQueue); 15 | -------------------------------------------------------------------------------- /packages/api/src/handlers/promotJob.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BullBoardRequest, 3 | ControllerHandlerReturnType, 4 | QueueJob, 5 | } from '../../typings/app'; 6 | import { queueProvider } from '../providers/queue'; 7 | import { jobProvider } from '../providers/job'; 8 | 9 | async function promoteJob( 10 | _req: BullBoardRequest, 11 | job: QueueJob 12 | ): Promise { 13 | await job.promote(); 14 | 15 | return { 16 | status: 204, 17 | body: {}, 18 | }; 19 | } 20 | 21 | export const promoteJobHandler = queueProvider(jobProvider(promoteJob)); 22 | -------------------------------------------------------------------------------- /packages/api/src/handlers/promoteAll.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | import { queueProvider } from '../providers/queue'; 3 | import { BaseAdapter } from '../queueAdapters/base'; 4 | 5 | async function promoteAll( 6 | _req: BullBoardRequest, 7 | queue: BaseAdapter 8 | ): Promise { 9 | await queue.promoteAll(); 10 | 11 | return { status: 200, body: {} }; 12 | } 13 | 14 | export const promoteAllHandler = queueProvider(promoteAll); 15 | -------------------------------------------------------------------------------- /packages/api/src/handlers/redisStats.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseRedisInfo } from 'redis-info'; 2 | import { BullBoardRequest, ControllerHandlerReturnType, RedisStats } from '../../typings/app'; 3 | import { BaseAdapter } from '../queueAdapters/base'; 4 | 5 | async function getStats(queue: BaseAdapter): Promise { 6 | const redisInfoRaw = await queue.getRedisInfo(); 7 | const redisInfo = parseRedisInfo(redisInfoRaw); 8 | 9 | return { 10 | version: redisInfo.redis_version, 11 | mode: redisInfo.redis_mode, 12 | port: +redisInfo.tcp_port, 13 | os: redisInfo.os, 14 | uptime: +redisInfo.uptime_in_seconds, 15 | memory: { 16 | total: +redisInfo.total_system_memory || +redisInfo.maxmemory, 17 | used: +redisInfo.used_memory, 18 | fragmentationRatio: +redisInfo.mem_fragmentation_ratio, 19 | peak: +redisInfo.used_memory_peak, 20 | }, 21 | clients: { 22 | connected: +redisInfo.connected_clients, 23 | blocked: +redisInfo.blocked_clients, 24 | }, 25 | }; 26 | } 27 | 28 | export async function redisStatsHandler({ 29 | queues: bullBoardQueues, 30 | }: BullBoardRequest): Promise { 31 | const pairs = [...bullBoardQueues.values()]; 32 | 33 | const body = pairs.length > 0 ? await getStats(pairs[0]) : {}; 34 | 35 | return { 36 | body, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/api/src/handlers/resumeAll.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | 3 | async function resumeAll(req: BullBoardRequest): Promise { 4 | const relevantQueues = Array.from(req.queues.values()).filter((queue) => !queue.readOnlyMode); 5 | 6 | for (const queue of relevantQueues) { 7 | const isPaused = await queue.isPaused(); 8 | if (isPaused) { 9 | await queue.resume(); 10 | } 11 | } 12 | 13 | return { status: 200, body: { message: 'All queues resumed' } }; 14 | } 15 | 16 | export const resumeAllHandler = resumeAll; 17 | -------------------------------------------------------------------------------- /packages/api/src/handlers/resumeQueue.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | import { queueProvider } from '../providers/queue'; 3 | import { BaseAdapter } from '../queueAdapters/base'; 4 | 5 | async function resumeQueue( 6 | _req: BullBoardRequest, 7 | queue: BaseAdapter 8 | ): Promise { 9 | await queue.resume(); 10 | 11 | return { status: 200, body: {} }; 12 | } 13 | 14 | export const resumeQueueHandler = queueProvider(resumeQueue); 15 | -------------------------------------------------------------------------------- /packages/api/src/handlers/retryAll.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType } from '../../typings/app'; 2 | import { BaseAdapter } from '../queueAdapters/base'; 3 | import { queueProvider } from '../providers/queue'; 4 | 5 | async function retryAll( 6 | req: BullBoardRequest, 7 | queue: BaseAdapter, 8 | ): Promise { 9 | const { queueStatus } = req.params; 10 | 11 | const jobs = await queue.getJobs([queueStatus]); 12 | await Promise.all(jobs.map((job) => job.retry(queueStatus))); 13 | 14 | return { status: 200, body: {} }; 15 | } 16 | 17 | export const retryAllHandler = queueProvider(retryAll); 18 | -------------------------------------------------------------------------------- /packages/api/src/handlers/retryJob.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BullBoardRequest, 3 | ControllerHandlerReturnType, 4 | QueueJob, 5 | } from '../../typings/app'; 6 | import { jobProvider } from '../providers/job'; 7 | import { queueProvider } from '../providers/queue'; 8 | 9 | async function retryJob( 10 | req: BullBoardRequest, 11 | job: QueueJob 12 | ): Promise { 13 | const { queueStatus } = req.params; 14 | 15 | await job.retry(queueStatus); 16 | 17 | return { 18 | status: 204, 19 | body: {}, 20 | }; 21 | } 22 | 23 | export const retryJobHandler = queueProvider(jobProvider(retryJob)); 24 | -------------------------------------------------------------------------------- /packages/api/src/handlers/updateJobData.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType, QueueJob } from '../../typings/app'; 2 | import { jobProvider } from '../providers/job'; 3 | import { queueProvider } from '../providers/queue'; 4 | 5 | async function updateJobData( 6 | req: BullBoardRequest, 7 | job: QueueJob 8 | ): Promise { 9 | const { jobData } = req.body; 10 | 11 | if ('updateData' in job) { 12 | await job.updateData!(jobData); 13 | } else if ('update' in job) { 14 | await job.update!(jobData); 15 | } 16 | 17 | return { 18 | status: 200, 19 | body: {}, 20 | }; 21 | } 22 | 23 | export const updateJobDataHandler = queueProvider(jobProvider(updateJobData)); 24 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { BoardOptions, IServerAdapter } from '../typings/app'; 3 | import { errorHandler } from './handlers/error'; 4 | import { BaseAdapter } from './queueAdapters/base'; 5 | import { getQueuesApi } from './queuesApi'; 6 | import { appRoutes } from './routes'; 7 | 8 | export function createBullBoard({ 9 | queues, 10 | serverAdapter, 11 | options = { uiConfig: {} }, 12 | }: { 13 | queues: ReadonlyArray; 14 | serverAdapter: IServerAdapter; 15 | options?: BoardOptions; 16 | }) { 17 | const { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue } = getQueuesApi(queues); 18 | const uiBasePath = 19 | options.uiBasePath || path.dirname(eval(`require.resolve('@bull-board/ui/package.json')`)); 20 | 21 | serverAdapter 22 | .setQueues(bullBoardQueues) 23 | .setViewsPath(path.join(uiBasePath, 'dist')) 24 | .setStaticPath('/static', path.join(uiBasePath, 'dist/static')) 25 | .setUIConfig({ 26 | boardTitle: 'Bull Dashboard', 27 | favIcon: { 28 | default: 'static/images/logo.svg', 29 | alternative: 'static/favicon-32x32.png', 30 | }, 31 | ...options.uiConfig, 32 | }) 33 | .setEntryRoute(appRoutes.entryPoint) 34 | .setErrorHandler(errorHandler) 35 | .setApiRoutes(appRoutes.api); 36 | 37 | return { setQueues, replaceQueues, addQueue, removeQueue }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/api/src/providers/job.ts: -------------------------------------------------------------------------------- 1 | import { BullBoardRequest, ControllerHandlerReturnType, QueueJob } from '../../typings/app'; 2 | import { BaseAdapter } from '../queueAdapters/base'; 3 | 4 | export function jobProvider( 5 | next: ( 6 | req: BullBoardRequest, 7 | job: QueueJob, 8 | queue: BaseAdapter 9 | ) => Promise 10 | ) { 11 | return async ( 12 | req: BullBoardRequest, 13 | queue: BaseAdapter 14 | ): Promise => { 15 | const { jobId } = req.params; 16 | 17 | const job = await queue.getJob(jobId); 18 | 19 | if (!job) { 20 | return { 21 | status: 404, 22 | body: { 23 | error: 'Job not found', 24 | }, 25 | }; 26 | } 27 | 28 | return next(req, job, queue); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/providers/queue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BullBoardRequest, 3 | ControllerHandlerReturnType, 4 | } from '../../typings/app'; 5 | import { BaseAdapter } from '../queueAdapters/base'; 6 | 7 | export function queueProvider( 8 | next: ( 9 | req: BullBoardRequest, 10 | queue: BaseAdapter 11 | ) => Promise, 12 | { 13 | skipReadOnlyModeCheck = false, 14 | }: { 15 | skipReadOnlyModeCheck?: boolean; 16 | } = {} 17 | ) { 18 | return async ( 19 | req: BullBoardRequest 20 | ): Promise => { 21 | const { queueName } = req.params; 22 | 23 | const queue = req.queues.get(queueName); 24 | if (!queue) { 25 | return { status: 404, body: { error: 'Queue not found' } }; 26 | } else if (queue.readOnlyMode && !skipReadOnlyModeCheck) { 27 | return { 28 | status: 405, 29 | body: { error: 'Method not allowed on read only queue' }, 30 | }; 31 | } 32 | 33 | return next(req, queue); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/api/src/queuesApi.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from './queueAdapters/base'; 2 | import { BullBoardQueues } from '../typings/app'; 3 | 4 | export function getQueuesApi(queues: ReadonlyArray) { 5 | const bullBoardQueues: BullBoardQueues = new Map(); 6 | 7 | function addQueue(queue: BaseAdapter): void { 8 | const name = queue.getName(); 9 | bullBoardQueues.set(name, queue); 10 | } 11 | 12 | function removeQueue(queueOrName: string | BaseAdapter) { 13 | const name = typeof queueOrName === 'string' ? queueOrName : queueOrName.getName(); 14 | 15 | bullBoardQueues.delete(name); 16 | } 17 | 18 | function setQueues(newBullQueues: ReadonlyArray): void { 19 | newBullQueues.forEach((queue) => { 20 | const name = queue.getName(); 21 | 22 | bullBoardQueues.set(name, queue); 23 | }); 24 | } 25 | 26 | function replaceQueues(newBullQueues: ReadonlyArray): void { 27 | const queuesToPersist: string[] = newBullQueues.map((queue) => queue.getName()); 28 | 29 | bullBoardQueues.forEach((_queue, name) => { 30 | if (queuesToPersist.indexOf(name) === -1) { 31 | bullBoardQueues.delete(name); 32 | } 33 | }); 34 | 35 | return setQueues(newBullQueues); 36 | } 37 | 38 | setQueues(queues); 39 | 40 | return { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/api/tests/api/public-interface.spec.ts: -------------------------------------------------------------------------------- 1 | import * as bullBoard from '@bull-board/api'; 2 | 3 | describe('lib public interface', () => { 4 | it('should save the interface', () => { 5 | expect(bullBoard).toMatchInlineSnapshot(` 6 | { 7 | "createBullBoard": [Function], 8 | } 9 | `); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/typings/responses.ts: -------------------------------------------------------------------------------- 1 | import { AppJob, AppQueue, Status } from './app'; 2 | 3 | export interface GetQueuesResponse { 4 | queues: AppQueue[]; 5 | } 6 | 7 | export interface GetJobResponse { 8 | job: AppJob; 9 | status: Status; 10 | } 11 | -------------------------------------------------------------------------------- /packages/api/typings/utils.ts: -------------------------------------------------------------------------------- 1 | export type KeyOf = Array; 2 | -------------------------------------------------------------------------------- /packages/elysia/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/elysia 2 | 3 | [Elysia](https://elysiajs.com/) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 22 | 1. [Simple Elysia setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-elysia) 23 | 24 | # Compatibility 25 | 26 | If using with `@elysiajs/node`, make sure you're using node v22 or higher. 27 | 28 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 29 | -------------------------------------------------------------------------------- /packages/elysia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/elysia", 3 | "type": "commonjs", 4 | "version": "6.9.6", 5 | "description": "A Elysia server adapter for Bull-Board dashboard.", 6 | "keywords": [ 7 | "bull", 8 | "bullmq", 9 | "redis", 10 | "elysia", 11 | "elysia-plugin", 12 | "adapter", 13 | "queue", 14 | "monitoring", 15 | "dashboard" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/felixmosh/bull-board.git", 20 | "directory": "packages/elysia" 21 | }, 22 | "license": "MIT", 23 | "author": { 24 | "name": "kravetsone", 25 | "url": "https://github.com/kravetsone" 26 | }, 27 | "main": "./dist/index.js", 28 | "types": "./dist/index.d.ts", 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "build": "tsc", 34 | "clean": "rm -rf dist" 35 | }, 36 | "dependencies": { 37 | "@bull-board/api": "6.9.6", 38 | "@bull-board/ui": "6.9.6", 39 | "ejs": "^3.1.10", 40 | "mimeV4": "npm:mime@^4.0.7" 41 | }, 42 | "peerDependencies": { 43 | "elysia": "^1.1.0" 44 | }, 45 | "publishConfig": { 46 | "access": "public" 47 | }, 48 | "devDependencies": { 49 | "@types/ejs": "^3.1.5", 50 | "@types/node": "^22.15.3", 51 | "elysia": "^1.2.25" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/elysia/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ElysiaAdapter } from './ElysiaAdapter'; 2 | -------------------------------------------------------------------------------- /packages/elysia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": [ 6 | "es2022", 7 | "DOM" 8 | ], 9 | "module": "CommonJS", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es2019", 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "resolveJsonModule": true, 18 | "declaration": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": [ 22 | "./src", 23 | "./typings/*.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/express/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/express 2 | 3 | [Express.js](https://expressjs.com/) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 1. [Simple express setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-express) 22 | 2. [Basic authentication example](https://github.com/felixmosh/bull-board/tree/master/examples/with-express-auth) 23 | 2. [Multiple instance of the board](https://github.com/felixmosh/bull-board/tree/master/examples/with-multiple-instances) 24 | 25 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 26 | -------------------------------------------------------------------------------- /packages/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/express", 3 | "version": "6.9.6", 4 | "description": "A Express.js server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "express", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/express" 19 | }, 20 | "license": "MIT", 21 | "author": "felixmosh", 22 | "main": "dist/index.js", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "clean": "rm -rf dist" 29 | }, 30 | "dependencies": { 31 | "@bull-board/api": "6.9.6", 32 | "@bull-board/ui": "6.9.6", 33 | "ejs": "^3.1.10", 34 | "express": "^4.21.1 || ^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/ejs": "^3.1.5", 38 | "@types/express": "^4.17.21 || ^5.0.0" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/express/src/helpers/wrapAsync.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction } from 'express'; 2 | import { Request, Response, RequestHandler } from 'express-serve-static-core'; 3 | 4 | export const wrapAsync = 5 | (fn: RequestHandler): RequestHandler => 6 | async (req: Request, res: Response, next: NextFunction) => 7 | Promise.resolve(fn(req, res, next)).catch(next); 8 | -------------------------------------------------------------------------------- /packages/express/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ExpressAdapter } from './ExpressAdapter'; 2 | -------------------------------------------------------------------------------- /packages/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/fastify/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/fastify 2 | 3 | [Fastify.js](https://www.fastify.io/) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 1. [Simple fastify setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-fastify) 22 | 2. [Auth with fastify setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-fastify-auth) 23 | 24 | 25 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 26 | -------------------------------------------------------------------------------- /packages/fastify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/fastify", 3 | "version": "6.9.6", 4 | "description": "A Fastify.js server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "fastify", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/fastify" 19 | }, 20 | "license": "MIT", 21 | "author": "felixmosh", 22 | "main": "dist/index.js", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "clean": "rm -rf dist" 29 | }, 30 | "dependencies": { 31 | "@bull-board/api": "6.9.6", 32 | "@bull-board/ui": "6.9.6", 33 | "@fastify/static": "^8.1.1", 34 | "@fastify/view": "^10.0.2", 35 | "ejs": "^3.1.10" 36 | }, 37 | "devDependencies": { 38 | "fastify": "^5.3.2" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/fastify/src/index.ts: -------------------------------------------------------------------------------- 1 | export { FastifyAdapter } from './FastifyAdapter'; 2 | -------------------------------------------------------------------------------- /packages/fastify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/h3/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/h3 2 | 3 | [h3](https://github.com/unjs/h3) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 22 | 1. [Simple h3 setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-h3) 23 | 24 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 25 | -------------------------------------------------------------------------------- /packages/h3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/h3", 3 | "version": "6.9.6", 4 | "description": "A H3 server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "h3", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/h3" 19 | }, 20 | "license": "MIT", 21 | "author": "genu", 22 | "main": "./dist/index.js", 23 | "types": "./dist/index.d.ts", 24 | "files": [ 25 | "dist" 26 | ], 27 | "scripts": { 28 | "build": "tsc", 29 | "clean": "rm -rf dist" 30 | }, 31 | "dependencies": { 32 | "@bull-board/api": "6.9.6", 33 | "@bull-board/ui": "6.9.6", 34 | "ejs": "^3.1.10", 35 | "h3": "^1.15.3" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "devDependencies": { 41 | "@types/ejs": "^3.1.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/h3/src/index.ts: -------------------------------------------------------------------------------- 1 | export { H3Adapter } from './H3Adapter'; 2 | -------------------------------------------------------------------------------- /packages/h3/src/utils/getContentType.ts: -------------------------------------------------------------------------------- 1 | export const getContentType = (filename?: string) => { 2 | let contentType = "text/html"; 3 | 4 | if (!filename) return contentType; 5 | 6 | switch (filename.split(".").pop()) { 7 | case "js": 8 | contentType = "text/javascript"; 9 | break; 10 | case "css": 11 | contentType = "text/css"; 12 | break; 13 | case "png": 14 | contentType = "image/png"; 15 | break; 16 | case "svg": 17 | contentType = "image/svg+xml"; 18 | break; 19 | case "json": 20 | contentType = "application/json"; 21 | break; 22 | case "ico": 23 | contentType = "image/x-icon"; 24 | break; 25 | } 26 | 27 | return contentType; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/h3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": [ 6 | "es2019", 7 | "DOM" 8 | ], 9 | "module": "CommonJS", 10 | "moduleResolution": "node", 11 | "noImplicitAny": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es2019", 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "resolveJsonModule": true, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | }, 21 | "include": [ 22 | "./src", 23 | "./typings/*.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/hapi/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/hapi 2 | 3 | [Hapi.js](https://hapi.dev/) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 1. [Simple hapi setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-hapi) 22 | 2. [Auth with hapi setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-hapi-auth) 23 | 24 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 25 | -------------------------------------------------------------------------------- /packages/hapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/hapi", 3 | "version": "6.9.6", 4 | "description": "A Hapi.js server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "hapi", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/hapi" 19 | }, 20 | "license": "MIT", 21 | "author": "felixmosh", 22 | "main": "dist/index.js", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "build:watch": "tsc -w", 29 | "clean": "rm -rf dist" 30 | }, 31 | "dependencies": { 32 | "@bull-board/api": "6.9.6", 33 | "@bull-board/ui": "6.9.6", 34 | "@hapi/inert": "^7.1.0", 35 | "@hapi/vision": "^7.0.3", 36 | "ejs": "^3.1.10" 37 | }, 38 | "devDependencies": { 39 | "@hapi/hapi": "^21.4.0", 40 | "@types/hapi__hapi": "^20.0.13", 41 | "@types/hapi__inert": "^5.2.10", 42 | "@types/hapi__vision": "^5.5.8" 43 | }, 44 | "publishConfig": { 45 | "access": "public" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/hapi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { HapiAdapter } from './HapiAdapter'; 2 | -------------------------------------------------------------------------------- /packages/hapi/src/utils/toHapiPath.ts: -------------------------------------------------------------------------------- 1 | export function toHapiPath(path: string) { 2 | return path 3 | .split('/') 4 | .map((path) => (path.startsWith(':') ? `{${path.substring(1)}}` : path)) 5 | .join('/'); 6 | } 7 | -------------------------------------------------------------------------------- /packages/hapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/hono/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/hono 2 | 3 | [Hono](https://hono.dev) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 22 | 1. [Simple Hono setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-hono) 23 | 24 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 25 | -------------------------------------------------------------------------------- /packages/hono/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/hono", 3 | "version": "6.9.6", 4 | "description": "A Hono server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "hono", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/hono" 19 | }, 20 | "license": "MIT", 21 | "author": "felixmosh", 22 | "main": "dist/index.js", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "clean": "rm -rf dist" 29 | }, 30 | "dependencies": { 31 | "@bull-board/api": "6.9.6", 32 | "@bull-board/ui": "6.9.6", 33 | "ejs": "^3.1.10" 34 | }, 35 | "devDependencies": { 36 | "@cloudflare/workers-types": "^4.20250503.0", 37 | "@hono/node-server": "^1.14.1", 38 | "hono": "^4.7.8" 39 | }, 40 | "peerDependencies": { 41 | "hono": "^4" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/hono/src/index.ts: -------------------------------------------------------------------------------- 1 | export { HonoAdapter } from './HonoAdapter'; 2 | -------------------------------------------------------------------------------- /packages/hono/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019", "DOM"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/koa/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/koa 2 | 3 | [Koa.js](https://koajs.com/) server adapter for `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | # Usage examples 21 | 1. [Simple koa setup](https://github.com/felixmosh/bull-board/tree/master/examples/with-koa) 22 | 23 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 24 | -------------------------------------------------------------------------------- /packages/koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/koa", 3 | "version": "6.9.6", 4 | "description": "A Koa.js server adapter for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "koa", 10 | "adapter", 11 | "queue", 12 | "monitoring", 13 | "dashboard" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/felixmosh/bull-board.git", 18 | "directory": "packages/koa" 19 | }, 20 | "license": "MIT", 21 | "author": "felixmosh", 22 | "main": "dist/index.js", 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "clean": "rm -rf dist" 29 | }, 30 | "dependencies": { 31 | "@bull-board/api": "6.9.6", 32 | "@bull-board/ui": "6.9.6", 33 | "@koa/bodyparser": "^5.1.1", 34 | "@ladjs/koa-views": "^9.0.0", 35 | "ejs": "^3.1.10", 36 | "koa": "^3.0.0", 37 | "koa-mount": "^4.2.0", 38 | "koa-router": "^13.0.1", 39 | "koa-static": "^5.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/co-body": "^6.1.3", 43 | "@types/koa": "^2.15.0", 44 | "@types/koa-mount": "^4.0.5", 45 | "@types/koa-router": "^7.4.8", 46 | "@types/koa-static": "^4.0.4", 47 | "@types/koa-views": "^7.0.0" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/koa/src/index.ts: -------------------------------------------------------------------------------- 1 | export { KoaAdapter } from './KoaAdapter'; 2 | -------------------------------------------------------------------------------- /packages/koa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2019"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2019", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "resolveJsonModule": true, 15 | "declaration": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src", "./typings/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bull-board/nestjs", 3 | "version": "6.9.6", 4 | "description": "A NestJS module for Bull-Board dashboard.", 5 | "keywords": [ 6 | "bull", 7 | "bullmq", 8 | "redis", 9 | "queue", 10 | "monitoring", 11 | "nestjs", 12 | "nestjs-module" 13 | ], 14 | "main": "dist/index.js", 15 | "author": "Dennis Snijder ", 16 | "license": "MIT", 17 | "scripts": { 18 | "build": "tsc", 19 | "clean": "rm -rf dist" 20 | }, 21 | "devDependencies": { 22 | "@bull-board/api": "^6.9.6", 23 | "@bull-board/express": "^6.9.6", 24 | "@bull-board/fastify": "^6.9.6", 25 | "@nestjs/bull-shared": "^11.0.2", 26 | "@nestjs/bullmq": "^11.0.2", 27 | "@nestjs/common": "^11.1.0", 28 | "@nestjs/core": "^11.1.0", 29 | "@types/node": "18.19.87", 30 | "bull": "^4.16.5", 31 | "bullmq": "^5.52.1", 32 | "reflect-metadata": "^0.2.2", 33 | "rxjs": "^7.8.2", 34 | "typescript": "^5.8.3" 35 | }, 36 | "peerDependencies": { 37 | "@bull-board/api": "^6.9.6", 38 | "@bull-board/express": "^6.9.6", 39 | "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", 40 | "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", 41 | "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", 42 | "reflect-metadata": "^0.1.13 || ^0.2.0", 43 | "rxjs": "^7.8.1" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/felixmosh/bull-board.git", 48 | "directory": "packages/nestjs" 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const BULL_BOARD_OPTIONS = 'bull_board_options'; 3 | export const BULL_BOARD_QUEUES = 'bull_board_queues'; 4 | export const BULL_BOARD_ADAPTER = 'bull_board_adapter'; 5 | export const BULL_BOARD_INSTANCE = 'bull_board_instance'; -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { BULL_BOARD_INSTANCE } from "./bull-board.constants"; 3 | 4 | export const InjectBullBoard = (): ParameterDecorator => Inject(BULL_BOARD_INSTANCE); -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.feature-module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Module, OnModuleInit } from "@nestjs/common"; 2 | import { ModuleRef } from "@nestjs/core"; 3 | import { getQueueToken } from "@nestjs/bull-shared"; 4 | import { BullBoardInstance, BullBoardQueueOptions } from "./bull-board.types"; 5 | import { Queue } from "bullmq"; 6 | import { BULL_BOARD_INSTANCE, BULL_BOARD_QUEUES } from "./bull-board.constants"; 7 | 8 | @Module({}) 9 | export class BullBoardFeatureModule implements OnModuleInit { 10 | 11 | constructor( 12 | private readonly moduleRef: ModuleRef, 13 | @Inject(BULL_BOARD_QUEUES) private readonly queues: BullBoardQueueOptions[], 14 | @Inject(BULL_BOARD_INSTANCE) private readonly board: BullBoardInstance 15 | ) { 16 | } 17 | 18 | onModuleInit(): any { 19 | for (const queueOption of this.queues) { 20 | const queue = this.moduleRef.get(getQueueToken(queueOption.name), {strict: false}); 21 | const queueAdapter = new queueOption.adapter(queue, queueOption.options); 22 | this.board.addQueue(queueAdapter); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module} from "@nestjs/common"; 2 | import { BullBoardFeatureModule } from "./bull-board.feature-module"; 3 | import { BullBoardRootModule } from "./bull-board.root-module"; 4 | import { BULL_BOARD_QUEUES } from "./bull-board.constants"; 5 | import { BullBoardModuleAsyncOptions, BullBoardModuleOptions, BullBoardQueueOptions } from "./bull-board.types"; 6 | 7 | @Module({}) 8 | export class BullBoardModule { 9 | 10 | static forFeature(...queues: BullBoardQueueOptions[]): DynamicModule { 11 | return { 12 | module: BullBoardFeatureModule, 13 | providers: [ 14 | { 15 | provide: BULL_BOARD_QUEUES, 16 | useValue: queues 17 | } 18 | ] 19 | }; 20 | } 21 | 22 | static forRoot(options: BullBoardModuleOptions): DynamicModule { 23 | return { 24 | module: BullBoardModule, 25 | imports: [ BullBoardRootModule.forRoot(options) ], 26 | exports: [ BullBoardRootModule ], 27 | }; 28 | } 29 | 30 | static forRootAsync(options: BullBoardModuleAsyncOptions): DynamicModule { 31 | return { 32 | module: BullBoardModule, 33 | imports: [ BullBoardRootModule.forRootAsync(options) ], 34 | exports: [ BullBoardRootModule ] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.types.ts: -------------------------------------------------------------------------------- 1 | import { createBullBoard } from "@bull-board/api"; 2 | import { BoardOptions, IServerAdapter, QueueAdapterOptions } from "@bull-board/api/dist/typings/app"; 3 | import { BaseAdapter } from "@bull-board/api/dist/src/queueAdapters/base"; 4 | import { InjectionToken, ModuleMetadata, OptionalFactoryDependency } from "@nestjs/common"; 5 | 6 | export type BullBoardInstance = ReturnType; 7 | 8 | export type BullBoardModuleOptions = { 9 | route: string; 10 | adapter: { new(): BullBoardServerAdapter }; 11 | boardOptions?: BoardOptions; 12 | middleware?: any 13 | } 14 | 15 | export type BullBoardModuleAsyncOptions = { 16 | useFactory: (...args: any[]) => BullBoardModuleOptions| Promise; 17 | imports?: ModuleMetadata['imports']; 18 | inject?: Array; 19 | } 20 | 21 | export type BullBoardQueueOptions = { 22 | name: string; 23 | adapter: { new(queue: any, options?: Partial): BaseAdapter }, 24 | options?: Partial, 25 | }; 26 | 27 | //create our own types with the needed functions, so we don't need to include express/fastify libraries here. 28 | export type BullBoardServerAdapter = IServerAdapter & { setBasePath(path: string): any }; 29 | export type BullBoardFastifyAdapter = BullBoardServerAdapter & { registerPlugin(): any }; 30 | export type BullBoardExpressAdapter = BullBoardServerAdapter & { getRouter(): any }; -------------------------------------------------------------------------------- /packages/nestjs/src/bull-board.util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BullBoardExpressAdapter, 3 | BullBoardFastifyAdapter, BullBoardServerAdapter, 4 | } from "./bull-board.types"; 5 | 6 | export const isFastifyAdapter = (adapter: BullBoardServerAdapter): adapter is BullBoardFastifyAdapter => { 7 | return 'registerPlugin' in adapter; 8 | } 9 | 10 | export const isExpressAdapter = (adapter: BullBoardServerAdapter): adapter is BullBoardExpressAdapter => { 11 | return 'getRouter' in adapter; 12 | } -------------------------------------------------------------------------------- /packages/nestjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bull-board.module'; 2 | export * from './bull-board.types'; 3 | export * from './bull-board.constants'; 4 | export * from './bull-board.decorator'; -------------------------------------------------------------------------------- /packages/nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "strict": true, 6 | "removeComments": false, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es6", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "rootDir": "./src", 14 | "skipLibCheck": true, 15 | "useUnknownInCatchVariables": false, 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts", 23 | "tests", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | # @bull-board @bull-board/ui 2 | 3 | UI packages of `bull-board`. 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | npm downloads 11 | 12 | 13 | licence 14 | 15 |

16 | 17 | ![Overview](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/overview.png) 18 | ![UI](https://raw.githubusercontent.com/felixmosh/bull-board/master/screenshots/dashboard.png) 19 | 20 | For more info visit the main [README](https://github.com/felixmosh/bull-board#readme) 21 | -------------------------------------------------------------------------------- /packages/ui/localesSync.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | primaryLanguage: 'en-US', 3 | secondaryLanguages: ['es-ES', 'fr-FR', 'pt-BR', 'zh-CN'], 4 | localesFolder: './src/static/locales', 5 | spaces: 2, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Button.module.css'; 3 | import cn from 'clsx'; 4 | 5 | interface ButtonProps 6 | extends React.DetailedHTMLProps< 7 | React.ButtonHTMLAttributes, 8 | HTMLButtonElement 9 | > { 10 | isActive?: boolean; 11 | theme?: 'basic' | 'primary' | 'default'; 12 | } 13 | 14 | export const Button = React.forwardRef( 15 | ( 16 | { children, className, isActive = false, theme = 'default', ...rest }: ButtonProps, 17 | forwardedRef 18 | ) => ( 19 | 29 | ) 30 | ); 31 | -------------------------------------------------------------------------------- /packages/ui/src/components/Card/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: var(--card-bg); 3 | box-shadow: var(--card-shadow); 4 | border-radius: 0.25rem; 5 | padding: 1em; 6 | display: flex; 7 | } -------------------------------------------------------------------------------- /packages/ui/src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import React, { PropsWithChildren } from 'react'; 3 | import s from './Card.module.css'; 4 | 5 | interface ICardProps { 6 | className?: string; 7 | } 8 | export const Card = ({ children, className }: PropsWithChildren) => ( 9 |

{children}
10 | ); 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/ConfirmModal/ConfirmModal.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | max-width: 450px; 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/components/ConfirmModal/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Cancel, 4 | Content, 5 | Description, 6 | Overlay, 7 | Root, 8 | Title, 9 | Portal, 10 | } from '@radix-ui/react-alert-dialog'; 11 | import cn from 'clsx'; 12 | import React from 'react'; 13 | import { useTranslation } from 'react-i18next'; 14 | import s from './ConfirmModal.module.css'; 15 | import modalStyles from '../Modal/Modal.module.css'; 16 | import { Button } from '../Button/Button'; 17 | 18 | export interface ConfirmProps { 19 | open: boolean; 20 | title: string; 21 | description: string; 22 | onCancel: () => void; 23 | onConfirm: () => void; 24 | } 25 | 26 | export const ConfirmModal = ({ open, onConfirm, title, onCancel, description }: ConfirmProps) => { 27 | const { t } = useTranslation(); 28 | const closeOnOpenChange = (open: boolean) => { 29 | if (!open) { 30 | onCancel(); 31 | } 32 | }; 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 |
40 | {!!title && ( 41 | 42 | <h3>{title}</h3> 43 | 44 | )} 45 | {!!description && {description}} 46 |
47 | 48 | 51 | 52 | 53 | 56 | 57 |
58 |
59 |
60 |
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/ui/src/components/CustomLinksDropdown/CustomLinksDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { UIConfig } from '@bull-board/api/typings/app'; 2 | import { Item, Portal, Root, Trigger } from '@radix-ui/react-dropdown-menu'; 3 | import React from 'react'; 4 | import { DropdownContent } from '../DropdownContent/DropdownContent'; 5 | import { UserIcon } from '../Icons/User'; 6 | import { Button } from '../Button/Button'; 7 | 8 | type CustomLinksDropdownProps = { 9 | options: UIConfig['miscLinks']; 10 | className: string; 11 | }; 12 | 13 | export const CustomLinksDropdown = ({ options = [], className }: CustomLinksDropdownProps) => { 14 | return ( 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | {options.map((option) => ( 25 | 26 | {option.text} 27 | 28 | ))} 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ui/src/components/DropdownContent/DropdownContent.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | background: var(--dropdown-bg); 3 | box-shadow: var(--dropdown-shadow); 4 | border: 1px solid var(--dropdown-border-color); 5 | border-radius: 0.28571429rem; 6 | padding-top: 0.25rem; 7 | padding-bottom: 0.25rem; 8 | font-size: 0.875rem; 9 | line-height: 1.25rem; 10 | font-weight: 400; 11 | min-width: 150px; 12 | z-index: 100; 13 | } 14 | 15 | .content > [role='menuitem'] { 16 | display: block; 17 | color: inherit; 18 | padding: 0.5rem 1rem; 19 | font-weight: 400; 20 | white-space: nowrap; 21 | cursor: pointer; 22 | text-decoration: none; 23 | } 24 | 25 | .content > [role='menuitem'] > svg { 26 | float: none; 27 | margin-right: 0.5rem; 28 | width: 1.18em; 29 | height: 1em; 30 | vertical-align: middle; 31 | fill: var(--button-icon-fill); 32 | } 33 | 34 | .content > [role='menuitem']:hover { 35 | color: inherit; 36 | background: var(--button-default-hover-bg); 37 | } 38 | -------------------------------------------------------------------------------- /packages/ui/src/components/DropdownContent/DropdownContent.tsx: -------------------------------------------------------------------------------- 1 | import { Content, DropdownMenuContentProps } from '@radix-ui/react-dropdown-menu'; 2 | import React from 'react'; 3 | import s from './DropdownContent.module.css'; 4 | 5 | export const DropdownContent = React.forwardRef((props: DropdownMenuContentProps, ref: any) => ( 6 | 7 | )); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/Field/Field.module.css: -------------------------------------------------------------------------------- 1 | .field > label { 2 | font-size: 0.875rem; 3 | line-height: 1.25rem; 4 | display: block; 5 | } 6 | 7 | .field + .field { 8 | margin-top: 1rem; 9 | } 10 | 11 | .field > input, 12 | .field > select { 13 | font-size: 0.875rem !important; 14 | line-height: 1.25rem !important; 15 | display: block; 16 | width: 100%; 17 | margin-top: 0.25rem !important; 18 | } 19 | 20 | .field.inline { 21 | display: flex; 22 | align-items: center; 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/Field/Field.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import React, { PropsWithChildren } from 'react'; 3 | import s from './Field.module.css'; 4 | 5 | interface FieldProps { 6 | label?: string; 7 | id?: string; 8 | inline?: boolean; 9 | } 10 | 11 | export const Field = ({ label, id, inline, children }: PropsWithChildren) => ( 12 |
13 | {!!label && !inline && } 14 | {children} 15 | {!!label && inline && } 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/InputField/InputField.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes } from 'react'; 2 | import { Field } from '../Field/Field'; 3 | 4 | interface InputFieldProps extends InputHTMLAttributes { 5 | label?: string; 6 | } 7 | 8 | export const InputField = ({ label, id, ...inputProps }: InputFieldProps) => ( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/JsonField/JsonField.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import { JsonEditor } from '../../JsonEditor/JsonEditor'; 3 | import { Field } from '../Field/Field'; 4 | 5 | interface JsonFieldProps extends Omit, 'value' | 'ref'> { 6 | value?: Record; 7 | schema?: Record; 8 | } 9 | 10 | export const JsonField = ({ label, id, value, ...rest }: JsonFieldProps) => ( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/SelectField/SelectField.tsx: -------------------------------------------------------------------------------- 1 | import React, { SelectHTMLAttributes } from 'react'; 2 | import { Field } from '../Field/Field'; 3 | 4 | export interface SelectItem { 5 | text: string; 6 | value: string; 7 | } 8 | 9 | interface SelectFieldProps extends SelectHTMLAttributes { 10 | label?: string; 11 | id?: string; 12 | options: SelectItem[]; 13 | } 14 | 15 | export const SelectField = ({ label, id, options, ...selectProps }: SelectFieldProps) => ( 16 | 17 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/SwitchField/SwitchField.module.css: -------------------------------------------------------------------------------- 1 | .switch { 2 | all: unset; 3 | width: 42px; 4 | height: 25px; 5 | background-color: transparent; 6 | border-radius: 9999px; 7 | border: 1px solid var(--input-border); 8 | position: relative; 9 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 10 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 11 | cursor: pointer; 12 | transition: border-color 0.2s ease-out, box-shadow 0.2s ease-out; 13 | margin-inline-end: 0.5rem; 14 | } 15 | 16 | .switch[data-state='checked'] { 17 | background-color: var(--button-default-hover-bg); 18 | } 19 | 20 | .switch:hover .thumb { 21 | background-color: #909bad; 22 | } 23 | 24 | .switch:focus { 25 | border-color: var(--input-focus-border); 26 | box-shadow: var(--input-focus-shadow); 27 | } 28 | 29 | .thumb { 30 | display: block; 31 | width: 21px; 32 | height: 21px; 33 | background-color: var(--accent-color-d1); 34 | border-radius: 9999px; 35 | /*box-shadow: 0 2px 2px hsl(0deg 0% 0% / 14%);*/ 36 | transition: transform 100ms; 37 | transform: translateX(2px); 38 | will-change: transform; 39 | } 40 | 41 | .thumb[data-state='checked'] { 42 | transform: translateX(19px); 43 | } 44 | -------------------------------------------------------------------------------- /packages/ui/src/components/Form/SwitchField/SwitchField.tsx: -------------------------------------------------------------------------------- 1 | import { SwitchProps } from '@radix-ui/react-switch'; 2 | import * as Switch from '@radix-ui/react-switch'; 3 | import React from 'react'; 4 | import { Field } from '../Field/Field'; 5 | import s from './SwitchField.module.css'; 6 | 7 | interface SwitchFieldProps extends SwitchProps { 8 | label?: string; 9 | id?: string; 10 | } 11 | 12 | export const SwitchField = ({ label, id, ...switchProps }: SwitchFieldProps) => ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | z-index: 99; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | background: var(--header-bg); 8 | transition: box-shadow 0.5s ease-in-out; 9 | display: flex; 10 | height: var(--header-height); 11 | box-sizing: border-box; 12 | border-bottom: 1px solid transparent; 13 | } 14 | 15 | .header > .logo { 16 | width: var(--menu-width); 17 | flex-basis: var(--menu-width); 18 | flex-shrink: 0; 19 | white-space: nowrap; 20 | text-align: start; 21 | font-size: 1.44rem; 22 | height: inherit; 23 | line-height: var(--header-height); 24 | background-color: hsl(217, 22%, 24%); 25 | color: #f5f8fa; 26 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 27 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1); 28 | z-index: 2; 29 | display: flex; 30 | align-items: center; 31 | padding: 0 1rem; 32 | text-decoration: none; 33 | overflow: hidden; 34 | } 35 | 36 | .header > .logo > span { 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | } 40 | 41 | .header > .logo > .img { 42 | max-height: 100%; 43 | width: auto; 44 | margin-right: 0.4em; 45 | } 46 | 47 | .header > .logo > .img.default { 48 | width: 1.2em; 49 | margin-bottom: 0.2em; 50 | } 51 | 52 | .header .content { 53 | flex: 1; 54 | display: flex; 55 | align-items: center; 56 | justify-content: space-between; 57 | padding: 0 2rem; 58 | overflow: hidden; 59 | } 60 | 61 | .header + main { 62 | padding-top: var(--header-height); 63 | } 64 | -------------------------------------------------------------------------------- /packages/ui/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import React, { PropsWithChildren } from 'react'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { useUIConfig } from '../../hooks/useUIConfig'; 5 | import { getStaticPath } from '../../utils/getStaticPath'; 6 | import s from './Header.module.css'; 7 | 8 | export const Header = ({ children }: PropsWithChildren) => { 9 | const uiConfig = useUIConfig(); 10 | const logoPath = uiConfig.boardLogo?.path ?? getStaticPath('/images/logo.svg'); 11 | const boardTitle = uiConfig.boardTitle ?? 'Bull Dashboard'; 12 | 13 | return ( 14 |
15 | 16 | {!!logoPath && ( 17 | {boardTitle} 24 | )} 25 | {boardTitle} 26 | 27 |
{children}
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ui/src/components/HeaderActions/HeaderActions.module.css: -------------------------------------------------------------------------------- 1 | .actions { 2 | padding: 0; 3 | margin: 0; 4 | list-style: none; 5 | display: flex; 6 | } 7 | 8 | .actions li + li { 9 | margin-inline-start: 0.5rem; 10 | } 11 | 12 | .button { 13 | font-size: 1rem; 14 | padding: 0.65rem; 15 | } 16 | 17 | .button > svg { 18 | width: 1.5rem; 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/components/Highlight/Highlight.module.css: -------------------------------------------------------------------------------- 1 | .codeContainerWrapper { 2 | display: flex; 3 | align-items: flex-start; 4 | gap: 1ch; 5 | } 6 | 7 | .codeContainerWrapper pre { 8 | flex: 1; 9 | } 10 | 11 | .copyBtn { 12 | opacity: 0; 13 | transition: opacity 150ms ease-in; 14 | } 15 | 16 | .codeContainerWrapper:hover .copyBtn { 17 | opacity: 1; 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/Highlight/Highlight.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { asyncHighlight } from '../../utils/highlight/highlight'; 4 | import s from './Highlight.module.css'; 5 | import { Button } from '../Button/Button'; 6 | import { CopyIcon } from '../Icons/Copy'; 7 | 8 | interface HighlightProps { 9 | language: 'json' | 'stacktrace'; 10 | text: string; 11 | } 12 | 13 | export const Highlight: React.FC = ({ language, text }) => { 14 | const [code, setCode] = useState(''); 15 | 16 | useEffect(() => { 17 | let unmount = false; 18 | asyncHighlight(text as string, language).then((newCode) => { 19 | if (!unmount) { 20 | setCode(newCode); 21 | } 22 | }); 23 | 24 | return () => { 25 | unmount = true; 26 | }; 27 | }, [language, text]); 28 | 29 | const handleCopyClick = () => { 30 | navigator.clipboard.writeText(text ?? ''); 31 | }; 32 | 33 | return ( 34 |
35 |
36 |         
37 |       
38 | 39 | 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Add.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const AddIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/ArrowLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ArrowLeftIcon = () => ( 4 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/ArrowRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ArrowRightIcon = () => ( 4 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/ChevronDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ChevronDown = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/ChevronUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ChevronUp = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CopyIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Duplicate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const DuplicateIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/EllipsisVertical.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const EllipsisVerticalIcon = () => ( 4 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Fullscreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const FullscreenIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Pause.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PauseIcon = () => ( 4 | 10 | ); 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Play.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PlayIcon = () => ( 4 | 10 | ); 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Promote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PromoteIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Redis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const RedisIcon = () => ( 4 | 5 | 16 | 20 | 24 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Retry.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const RetryIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SearchIcon = () => ( 4 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Settings = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Sort.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SortIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/SortDirectionDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SortDirectionDown = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/SortDirectionUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SortDirectionUp = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/Trash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const TrashIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/UpRightFromSquare.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const UpRightFromSquareSolid = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/UpdateIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const UpdateIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Icons/User.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const UserIcon = () => ( 4 | 5 | 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/Details/Details.module.css: -------------------------------------------------------------------------------- 1 | .details { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .tabActions { 8 | list-style: none; 9 | padding: 0; 10 | margin: 1rem 0 2rem; 11 | display: flex; 12 | } 13 | 14 | .tabActions li + li { 15 | margin-left: 0.75rem; 16 | } 17 | 18 | .tabContent::-webkit-scrollbar { 19 | width: 0.5rem; 20 | } 21 | 22 | .tabContent::-webkit-scrollbar-track { 23 | padding: 2px; 24 | background: var(--details-scrollbar-track-bg); 25 | } 26 | 27 | .tabContent::-webkit-scrollbar-thumb { 28 | background-color: var(--details-scrollbar-thumb-bg); 29 | border-radius: 4px; 30 | } 31 | 32 | .tabContent { 33 | flex: 1; 34 | max-width: calc(100% - 80px); 35 | overflow: auto; 36 | position: relative; 37 | } 38 | 39 | .tabContent :global(.error) { 40 | color: var(--details-error-color); 41 | } 42 | 43 | .tabContent :global(.warn) { 44 | color: var(--details-warn-color); 45 | } 46 | 47 | .tabContent pre { 48 | margin: 0; 49 | } 50 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/Details/Details.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useDetailsTabs } from '../../../hooks/useDetailsTabs'; 4 | import { Button } from '../../Button/Button'; 5 | import s from './Details.module.css'; 6 | import { DetailsContent } from './DetailsContent/DetailsContent'; 7 | import { AppJob, Status } from '@bull-board/api/typings/app'; 8 | 9 | interface DetailsProps { 10 | job: AppJob; 11 | status: Status; 12 | actions: { getJobLogs: () => Promise }; 13 | } 14 | 15 | export const Details = ({ status, job, actions }: DetailsProps) => { 16 | const { tabs, selectedTab } = useDetailsTabs(status); 17 | const { t } = useTranslation(); 18 | 19 | if (tabs.length === 0) { 20 | return null; 21 | } 22 | 23 | return ( 24 |
25 |
    26 | {tabs.map((tab) => ( 27 |
  • 28 | 31 |
  • 32 | ))} 33 |
34 |
35 | 36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/Details/DetailsContent/JobLogs/JobLogs.module.css: -------------------------------------------------------------------------------- 1 | .jobLogs { 2 | margin: 0; 3 | overflow: hidden; 4 | position: relative; 5 | background: var(--job-logs-bg); 6 | height: 100%; 7 | } 8 | 9 | .preWrapper { 10 | padding: 40px 0.5rem 0.5rem; 11 | max-height: 100%; 12 | overflow: auto; 13 | } 14 | 15 | .preWrapper ol { 16 | display: block; 17 | white-space: pre-wrap; 18 | word-break: break-all; 19 | } 20 | 21 | .preWrapper ol li { 22 | padding-inline-start: 1rem; 23 | } 24 | 25 | .preWrapper ol li::marker { 26 | color: var(--job-logs-marker-color); 27 | content: attr(data-line-number); 28 | } 29 | 30 | .preWrapper::-webkit-scrollbar { 31 | width: 0.5rem; 32 | } 33 | 34 | .preWrapper::-webkit-scrollbar-track { 35 | padding: 2px; 36 | background: var(--job-logs-scrollbar-track-bg); 37 | } 38 | 39 | .preWrapper::-webkit-scrollbar-thumb { 40 | background-color: var(--job-logs-scrollbar-thumb-bg); 41 | border-radius: 4px; 42 | } 43 | 44 | .toolbar { 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | right: 1rem; 49 | background: var(--job-logs-toolbar-bg); 50 | backdrop-filter: blur(4px); 51 | padding: 0 0 0.5rem 0.5rem; 52 | margin: 0; 53 | list-style: none; 54 | text-align: right; 55 | z-index: 1; 56 | display: flex; 57 | } 58 | 59 | .toolbar > li:first-child { 60 | flex: 1; 61 | } 62 | 63 | .toolbar > li button { 64 | height: 100%; 65 | } 66 | 67 | .toolbar > li + li { 68 | margin-left: 0.5rem; 69 | } 70 | 71 | .jobLogs:fullscreen .toolbar { 72 | position: fixed; 73 | padding: 1rem; 74 | } 75 | 76 | .jobLogs:fullscreen .preWrapper { 77 | height: 100%; 78 | padding: 60px 1rem 1rem; 79 | } 80 | 81 | .toolbar .searchBar { 82 | width: 100%; 83 | } 84 | 85 | .logLineCopyButton { 86 | opacity: 0; 87 | transition: opacity 150ms ease-in; 88 | margin-inline-start: 1ch; 89 | } 90 | 91 | li:hover .logLineCopyButton { 92 | opacity: 1; 93 | } 94 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/JobActions/JobActions.module.css: -------------------------------------------------------------------------------- 1 | .jobActions { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | } 7 | 8 | .jobActions li + li { 9 | margin-left: 0.5rem; 10 | } 11 | 12 | .jobActions .button { 13 | padding: 0.5rem; 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/Progress/Progress.module.css: -------------------------------------------------------------------------------- 1 | .progress { 2 | width: 80px; 3 | height: 80px; 4 | } 5 | 6 | .progress circle { 7 | transition: stroke-dashoffset 500ms ease-in-out; 8 | fill: none; 9 | 10 | &:first-child { 11 | stroke: var(--separator-color); 12 | } 13 | 14 | &.failed { 15 | stroke: var(--failed); 16 | } 17 | 18 | &.success { 19 | stroke: var(--completed); 20 | } 21 | } 22 | 23 | .progress text { 24 | font-size: 1.3rem; 25 | font-family: inherit; 26 | font-weight: 300; 27 | fill: var(--card-text-secondary-color); 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/components/JobCard/Timeline/Timeline.module.css: -------------------------------------------------------------------------------- 1 | .timeline { 2 | padding: 1.5rem 1rem 1.5rem 0; 3 | margin: 0; 4 | list-style: none; 5 | border: 0; 6 | border-right-width: 2px; 7 | border-right-style: solid; 8 | border-image: linear-gradient( 9 | to bottom, 10 | var(--card-bg), 11 | var(--separator-color) 10%, 12 | var(--separator-color) 90%, 13 | var(--card-bg) 14 | ) 15 | 1 100%; 16 | color: var(--card-text-secondary-color); 17 | font-weight: 300; 18 | height: 100%; 19 | } 20 | 21 | .timeline li { 22 | display: block; 23 | } 24 | 25 | .timeline li + li { 26 | margin-top: 1.5rem; 27 | } 28 | 29 | .timeline li > time { 30 | position: relative; 31 | color: var(--accent-color-d1); 32 | } 33 | 34 | .timeline li > time:before { 35 | content: ''; 36 | width: 0.5rem; 37 | height: 0.5rem; 38 | position: absolute; 39 | right: -1.5rem; 40 | top: 50%; 41 | margin-top: -0.5rem; 42 | background-color: var(--card-text-secondary-color); 43 | border-radius: 100%; 44 | border: 3px solid var(--card-bg); 45 | } 46 | 47 | .timeline li > small { 48 | display: block; 49 | line-height: 1; 50 | } 51 | 52 | .timeline li > small + small { 53 | margin-top: 1.5rem; 54 | } 55 | 56 | .timelineWrapper { 57 | position: relative; 58 | flex: 1; 59 | } 60 | -------------------------------------------------------------------------------- /packages/ui/src/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | export const Loader = () => { 5 | const { t } = useTranslation(); 6 | return
{t('LOADING')}
; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/Menu/Menu.module.css: -------------------------------------------------------------------------------- 1 | .aside { 2 | position: fixed; 3 | z-index: 99; 4 | top: var(--header-height); 5 | left: 0; 6 | bottom: 0; 7 | width: var(--menu-width); 8 | background: linear-gradient( 9 | to bottom, 10 | hsl(217, 22%, 20%), 11 | hsl(217, 22%, 16%) 80%, 12 | hsl(217, 22%, 12%) 13 | ); 14 | padding-top: 1rem; 15 | color: #d5d9dc; 16 | display: flex; 17 | flex-direction: column; 18 | box-shadow: 4px 0 8px 3px rgba(38, 46, 60, 0.1); 19 | } 20 | 21 | .aside .secondary { 22 | color: #828e97; 23 | font-size: 0.833em; 24 | padding: 0 1rem; 25 | } 26 | 27 | .aside nav { 28 | flex: 1; 29 | overflow-y: auto; 30 | } 31 | 32 | .appVersion { 33 | text-align: center; 34 | text-decoration: none; 35 | } 36 | 37 | .searchWrapper { 38 | color: #f5f8fa; 39 | position: relative; 40 | padding: 1rem; 41 | margin-top: 0.5rem; 42 | } 43 | 44 | .searchWrapper svg { 45 | width: 1rem; 46 | height: 1rem; 47 | position: absolute; 48 | pointer-events: none; 49 | top: 1.75rem; 50 | left: 1.75rem; 51 | stroke-width: 2px; 52 | color: #828e97; 53 | } 54 | 55 | .search.search { 56 | border: 2px solid rgba(0, 0, 0, 0.4); 57 | border-radius: 9999px; 58 | width: 100%; 59 | background-color: rgba(255, 255, 255, 0.1); 60 | height: 2.5rem; 61 | padding: 0 1rem 0 2rem; 62 | font-family: inherit; 63 | color: #f5f8fa; 64 | line-height: 2.5rem; 65 | transition: border-color 120ms ease-in-out; 66 | box-shadow: none !important; 67 | } 68 | 69 | .search.search:focus-visible, 70 | .search.search:focus, 71 | .search.search:active { 72 | border: 2px solid #4abec7; 73 | outline: 0; 74 | } 75 | -------------------------------------------------------------------------------- /packages/ui/src/components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import React, { useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useQueues } from '../../hooks/useQueues'; 5 | import { toTree } from '../../utils/toTree'; 6 | import { SearchIcon } from '../Icons/Search'; 7 | import s from './Menu.module.css'; 8 | import { MenuTree } from './MenuTree/MenuTree'; 9 | 10 | export const Menu = () => { 11 | const { t } = useTranslation(); 12 | const { queues } = useQueues(); 13 | const [searchTerm, setSearchTerm] = useState(''); 14 | 15 | const tree = toTree( 16 | queues?.filter((queue) => 17 | queue.name?.toLowerCase().includes(searchTerm?.toLowerCase() as string) 18 | ) || [] 19 | ); 20 | 21 | return ( 22 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/components/Menu/MenuTree/MenuTree.module.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | list-style: none; 3 | padding: 0; 4 | 5 | > li + li { 6 | border-top: 1px solid hsl(206, 9%, 25%); 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | display: block; 13 | padding: 0.75rem 1rem; 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | transition: background-color 100ms ease-in; 18 | border-left: 3px solid transparent; 19 | 20 | &:hover { 21 | background-color: rgba(255, 255, 255, 0.05); 22 | border-left-color: hsl(184, 20%, 30%); 23 | } 24 | 25 | &.active { 26 | background-color: rgba(255, 255, 255, 0.1); 27 | border-left-color: #4abec7; 28 | } 29 | } 30 | 31 | &.level-1 { 32 | > li > a, 33 | > li > details > summary { 34 | padding-left: calc(0.75 * 2rem); 35 | } 36 | } 37 | 38 | &.level-2 { 39 | > li > a, 40 | > li > details > summary { 41 | padding-left: calc(0.75 * 3rem); 42 | } 43 | } 44 | 45 | &.level-3 { 46 | > li > a, 47 | > li > details > summary { 48 | padding-left: calc(0.75 * 4rem); 49 | } 50 | } 51 | 52 | details { 53 | cursor: pointer; 54 | } 55 | 56 | summary { 57 | padding: 0.75rem 1rem; 58 | 59 | + .menu { 60 | padding: 0; 61 | } 62 | } 63 | 64 | .isPaused { 65 | color: #828e97; 66 | font-size: 0.833em; 67 | display: block; 68 | margin-bottom: -0.75em; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/ui/src/components/Menu/MenuTree/MenuTree.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useSelectedStatuses } from '../../../hooks/useSelectedStatuses'; 4 | import { links } from '../../../utils/links'; 5 | import { AppQueueTreeNode } from '../../../utils/toTree'; 6 | import { NavLink } from 'react-router-dom'; 7 | import React from 'react'; 8 | import s from './MenuTree.module.css'; 9 | 10 | export const MenuTree = ({ tree, level = 0 }: { tree: AppQueueTreeNode; level?: number }) => { 11 | const { t } = useTranslation(); 12 | const selectedStatuses = useSelectedStatuses(); 13 | 14 | return ( 15 |
    0 && s[`level-${level}`])}> 16 | {tree.children.map((node) => { 17 | const isLeafNode = !node.children.length; 18 | const displayName = isLeafNode ? node.queue?.displayName : node.name; 19 | 20 | return ( 21 |
  • 22 | {isLeafNode ? ( 23 | 28 | {displayName} 29 | {node.queue?.isPaused && [ {t('MENU.PAUSED')} ]} 30 | 31 | ) : ( 32 |
    33 | {displayName} 34 | 35 |
    36 | )} 37 |
  • 38 | ); 39 | })} 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ui/src/components/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as Dialog from '@radix-ui/react-dialog'; 2 | import cn from 'clsx'; 3 | import React, { PropsWithChildren } from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Button } from '../Button/Button'; 6 | import s from './Modal.module.css'; 7 | 8 | interface ModalProps { 9 | open: boolean; 10 | title?: string; 11 | width?: 'small' | 'medium' | 'wide'; 12 | actionButton?: React.ReactNode; 13 | onClose(): void; 14 | } 15 | 16 | export const Modal = ({ 17 | open, 18 | title, 19 | onClose, 20 | children, 21 | width, 22 | actionButton, 23 | }: PropsWithChildren) => { 24 | const { t } = useTranslation(); 25 | const closeOnOpenChange = (open: boolean) => { 26 | if (!open) { 27 | onClose(); 28 | } 29 | }; 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 |
37 | {!!title && {title}} 38 | 39 |
{children}
40 |
41 |
42 | {actionButton} 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/ui/src/components/OverviewDropDownActions/OverviewDropDownActions.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | background: var(--dropdown-bg); 3 | box-shadow: var(--dropdown-shadow); 4 | border: 1px solid var(--dropdown-border-color); 5 | border-radius: 0.28571429rem; 6 | padding-top: 0.25rem; 7 | padding-bottom: 0.25rem; 8 | font-size: 0.875rem; 9 | line-height: 1.25rem; 10 | font-weight: 400; 11 | min-width: 150px; 12 | z-index: 100; 13 | } 14 | 15 | .content [role='menuitem'] { 16 | display: block; 17 | color: inherit; 18 | padding: 0.5rem 1rem; 19 | font-weight: 400; 20 | white-space: nowrap; 21 | cursor: pointer; 22 | text-decoration: none; 23 | } 24 | 25 | .content [role='menuitem'] > svg { 26 | float: none; 27 | margin: 0 0.5rem 0 -0.25rem; 28 | width: 1.2em; 29 | height: 1em; 30 | vertical-align: middle; 31 | fill: var(--button-icon-fill); 32 | } 33 | 34 | .content [role='menuitem']:hover { 35 | color: inherit; 36 | background: var(--button-default-hover-bg); 37 | } 38 | 39 | .subTrigger { 40 | display: flex; 41 | align-items: center; 42 | width: 100%; 43 | } 44 | 45 | .subContent { 46 | composes: content; 47 | 48 | [role='menuitem'] { 49 | position: relative; 50 | padding-right: 2rem; 51 | } 52 | 53 | [role='menuitem'] > svg { 54 | position: absolute; 55 | right: 0; 56 | top: 0.7rem; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/ui/src/components/Pagination/Pagination.module.css: -------------------------------------------------------------------------------- 1 | .pagination { 2 | list-style: none; 3 | padding: 0; 4 | margin: 0; 5 | display: flex; 6 | flex-direction: row; 7 | } 8 | 9 | .pagination li + li { 10 | margin-left: 0.25em; 11 | } 12 | 13 | .pagination li { 14 | line-height: normal; 15 | } 16 | 17 | .pagination a { 18 | border-radius: 0.28571429rem; 19 | padding: 0.6em 0.92857143em; 20 | color: inherit; 21 | text-decoration: none; 22 | display: flex; 23 | line-height: 1; 24 | transition: background-color 150ms ease-in-out; 25 | } 26 | 27 | .pagination a:hover, 28 | .pagination a:focus { 29 | background-color: var(--button-default-hover-bg); 30 | } 31 | 32 | .pagination a:active, 33 | .pagination li.isActive a { 34 | background-color: var(--button-default-active-bg); 35 | } 36 | 37 | .pagination a svg { 38 | height: 1em; 39 | vertical-align: middle; 40 | display: inline-block; 41 | fill: currentColor; 42 | } 43 | 44 | .pagination li.disabled { 45 | opacity: 0.45; 46 | pointer-events: none; 47 | } 48 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueActions/QueueActions.module.css: -------------------------------------------------------------------------------- 1 | .queueActions { 2 | margin: 0; 3 | padding: 0; 4 | list-style: none; 5 | display: flex; 6 | } 7 | 8 | .queueActions > li + li { 9 | margin-left: 0.25rem; 10 | } 11 | 12 | .queueActions .button > svg { 13 | fill: var(--card-text-secondary-color); 14 | margin: -0.25em 0.5em 0 0; 15 | } 16 | 17 | .queueActions .button:hover > svg, 18 | .queueActions .button:focus > svg { 19 | fill: var(--accent-color-d1); 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueCard/QueueCard.module.css: -------------------------------------------------------------------------------- 1 | .queueCard { 2 | flex-direction: column; 3 | gap: 0.5rem; 4 | position: relative; 5 | } 6 | 7 | .link { 8 | color: inherit; 9 | text-decoration: none; 10 | } 11 | 12 | .link:after { 13 | content: ''; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | z-index: 1; 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueCard/QueueCard.tsx: -------------------------------------------------------------------------------- 1 | import { AppQueue } from '@bull-board/api/typings/app'; 2 | import React from 'react'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { links } from '../../utils/links'; 5 | import { Card } from '../Card/Card'; 6 | import { QueueStats } from './QueueStats/QueueStats'; 7 | import s from './QueueCard.module.css'; 8 | 9 | interface IQueueCardProps { 10 | queue: AppQueue; 11 | } 12 | 13 | export const QueueCard = ({ queue }: IQueueCardProps) => ( 14 | 15 |
16 | 17 | {queue.displayName} 18 | 19 |
20 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueCard/QueueStats/QueueStats.module.css: -------------------------------------------------------------------------------- 1 | .stats { 2 | display: flex; 3 | white-space: nowrap; 4 | gap: 1rem; 5 | align-items: center; 6 | } 7 | 8 | .progressBar { 9 | width: 100%; 10 | display: flex; 11 | overflow: hidden; 12 | height: 1rem; 13 | border-radius: 9999px; 14 | background-color: var(--separator-color); 15 | line-height: 1; 16 | font-size: 0.75rem; 17 | color: rgba(255, 255, 255, 1); 18 | text-align: center; 19 | z-index: 10; 20 | } 21 | 22 | .progressBar > .bar { 23 | padding: 0.125rem; 24 | transition: width 0.3s ease-in, opacity 0.2s ease-in-out; 25 | 26 | min-width: min-content; 27 | text-decoration: none; 28 | } 29 | 30 | .progressBar > .bar, 31 | .progressBar > .bar:visited, 32 | .progressBar > .bar:active { 33 | color: inherit; 34 | } 35 | 36 | .progressBar > .bar:hover { 37 | opacity: 0.75; 38 | } 39 | 40 | .progressBar > .bar + .bar { 41 | border-left: 2px solid var(--card-bg); 42 | } 43 | 44 | .waiting { 45 | background-color: var(--waiting); 46 | } 47 | 48 | .waitingChildren { 49 | background-color: var(--waiting-children); 50 | } 51 | 52 | .prioritized { 53 | background-color: var(--prioritized); 54 | } 55 | 56 | .completed { 57 | background-color: var(--completed); 58 | } 59 | 60 | .failed { 61 | background-color: var(--failed); 62 | } 63 | 64 | .active { 65 | background-color: var(--active); 66 | } 67 | 68 | .delayed { 69 | background-color: var(--delayed); 70 | } 71 | 72 | .paused { 73 | background-color: var(--paused); 74 | } 75 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueCard/QueueStats/QueueStats.tsx: -------------------------------------------------------------------------------- 1 | import { AppQueue } from '@bull-board/api/typings/app'; 2 | import cn from 'clsx'; 3 | import React from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Link } from 'react-router-dom'; 6 | import { links } from '../../../utils/links'; 7 | import { toCamelCase } from '../../../utils/toCamelCase'; 8 | import s from './QueueStats.module.css'; 9 | 10 | interface IQueueStatsProps { 11 | queue: AppQueue; 12 | } 13 | 14 | export const QueueStats = ({ queue }: IQueueStatsProps) => { 15 | const { t } = useTranslation(); 16 | const total = queue.statuses.reduce((result, status) => result + (queue.counts[status] || 0), 0); 17 | 18 | return ( 19 |
20 |
21 | {queue.statuses 22 | .filter((status) => queue.counts[status] > 0) 23 | .map((status) => { 24 | const value = queue.counts[status]; 25 | 26 | return ( 27 | 38 | {value} 39 | 40 | ); 41 | })} 42 |
43 |
{t('DASHBOARD.JOBS_COUNT', { count: total })}
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ui/src/components/QueueDropdownActions/QueueDropdownActions.module.css: -------------------------------------------------------------------------------- 1 | .trigger { 2 | padding: 0.5em 0.65em; 3 | margin-bottom: 0.25em; 4 | justify-self: flex-end; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/components/RedisStatsModal/RedisStatsModal.module.css: -------------------------------------------------------------------------------- 1 | .redisStats { 2 | padding: 0; 3 | margin: 0; 4 | list-style: none; 5 | font-weight: 200; 6 | } 7 | 8 | .redisStats > li { 9 | display: flex; 10 | justify-content: space-between; 11 | margin: 1rem 0; 12 | } 13 | 14 | .redisStats > li > span:first-child { 15 | color: var(--accent-color-d1); 16 | } 17 | 18 | .redisStats > li small { 19 | margin-inline-end: 1rem; 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/StatusLegend/StatusLegend.module.css: -------------------------------------------------------------------------------- 1 | .legend { 2 | display: flex; 3 | list-style: none; 4 | padding: 0; 5 | margin: 0; 6 | text-transform: uppercase; 7 | gap: 2em; 8 | 9 | > li { 10 | display: flex; 11 | align-items: center; 12 | 13 | &:before { 14 | content: ''; 15 | background-color: var(--item-bg); 16 | border-radius: 50%; 17 | width: 0.5rem; 18 | height: 0.5rem; 19 | display: inline-block; 20 | margin-right: 0.5rem; 21 | } 22 | 23 | > a { 24 | text-decoration: none; 25 | margin: 0 0 -2px; 26 | padding: 0.5em 0; 27 | color: var(--status-menu-text); 28 | transition: border-bottom-color 200ms ease-in-out, color 0.1s ease; 29 | display: flex; 30 | align-items: center; 31 | border-bottom: 2px solid transparent; 32 | 33 | &:hover, 34 | &:active { 35 | border-color: var(--status-menu-hover-text); 36 | } 37 | 38 | &.active { 39 | border-color: var(--status-menu-active-border); 40 | color: var(--status-menu-active-text); 41 | font-weight: 500; 42 | } 43 | 44 | > span { 45 | flex: 1; 46 | white-space: nowrap; 47 | 48 | &:before { 49 | display: block; 50 | content: attr(title); 51 | font-weight: 600; 52 | height: 0; 53 | overflow: hidden; 54 | visibility: hidden; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | .waiting { 62 | --item-bg: var(--waiting); 63 | } 64 | 65 | .waitingChildren { 66 | --item-bg: var(--waiting-children); 67 | } 68 | 69 | .prioritized { 70 | --item-bg: var(--prioritized); 71 | } 72 | 73 | .active { 74 | --item-bg: var(--active); 75 | } 76 | 77 | .failed { 78 | --item-bg: var(--failed); 79 | } 80 | 81 | .completed { 82 | --item-bg: var(--completed); 83 | } 84 | 85 | .delayed { 86 | --item-bg: var(--delayed); 87 | } 88 | 89 | .paused { 90 | --item-bg: var(--paused); 91 | } 92 | -------------------------------------------------------------------------------- /packages/ui/src/components/StatusLegend/StatusLegend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { queueStatsStatusList } from '../../constants/queue-stats-status'; 3 | import { links } from '../../utils/links'; 4 | import s from './StatusLegend.module.css'; 5 | import { NavLink } from 'react-router-dom'; 6 | import { useTranslation } from 'react-i18next'; 7 | import { toCamelCase } from '../../utils/toCamelCase'; 8 | import { useQuery } from '../../hooks/useQuery'; 9 | 10 | export const StatusLegend = () => { 11 | const { t } = useTranslation(); 12 | const query = useQuery(); 13 | 14 | return ( 15 |
    16 | {queueStatsStatusList.map((status) => { 17 | const displayStatus = t(`QUEUE.STATUS.${status.toUpperCase()}`).toLocaleUpperCase(); 18 | const isActive = query.get('status') === status; 19 | 20 | return ( 21 |
  • 22 | isActive} 26 | > 27 | {displayStatus} 28 | 29 |
  • 30 | ); 31 | })} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/StatusMenu/StatusMenu.module.css: -------------------------------------------------------------------------------- 1 | .statusMenu { 2 | display: flex; 3 | margin-bottom: 2rem; 4 | border-bottom: 2px solid var(--status-menu-border-color); 5 | } 6 | 7 | .statusMenu > a { 8 | text-decoration: none; 9 | border-bottom: 2px solid transparent; 10 | margin: 0 0 -2px; 11 | padding: 0.5em 1em; 12 | color: var(--status-menu-text); 13 | transition: border-bottom-color 200ms ease-in-out, color 0.1s ease; 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .statusMenu > a span { 19 | flex: 1; 20 | white-space: nowrap; 21 | } 22 | 23 | .statusMenu > a span:before { 24 | display: block; 25 | content: attr(title); 26 | font-weight: 600; 27 | height: 0; 28 | overflow: hidden; 29 | visibility: hidden; 30 | } 31 | 32 | .statusMenu > a:hover, 33 | .statusMenu > a:active { 34 | border-color: var(--status-menu-hover-text); 35 | } 36 | 37 | .statusMenu > a.active { 38 | border-color: var(--status-menu-active-border); 39 | color: var(--status-menu-active-text); 40 | font-weight: 500; 41 | } 42 | 43 | .statusMenu > div { 44 | flex: 1; 45 | text-align: right; 46 | } 47 | 48 | .badge { 49 | display: inline-block; 50 | background-color: var(--badge-bg); 51 | border-color: var(--badge-bg); 52 | color: var(--badge-text-color); 53 | font-size: 0.833rem; 54 | padding: 0.25em 0.75em; 55 | line-height: 1em; 56 | text-align: center; 57 | border-radius: 500rem; 58 | margin: -0.5em 0 -0.5em 0.5rem; 59 | font-weight: 400; 60 | } 61 | -------------------------------------------------------------------------------- /packages/ui/src/components/StatusMenu/StatusMenu.tsx: -------------------------------------------------------------------------------- 1 | import { AppQueue } from '@bull-board/api/typings/app'; 2 | import React, { PropsWithChildren } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { NavLink } from 'react-router-dom'; 5 | import { links } from '../../utils/links'; 6 | import s from './StatusMenu.module.css'; 7 | 8 | export const StatusMenu = ({ queue, children }: PropsWithChildren<{ queue: AppQueue }>) => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 |
13 | {queue.statuses.map((status) => { 14 | const isLatest = status === 'latest'; 15 | const displayStatus = t(`QUEUE.STATUS.${status.toUpperCase()}`).toLocaleUpperCase(); 16 | return ( 17 | { 21 | const query = new URLSearchParams(search); 22 | return query.get('status') === status || (isLatest && query.get('status') === null); 23 | }} 24 | key={`${queue.name}-${status}`} 25 | > 26 | {displayStatus} 27 | {queue.counts[status] > 0 && {queue.counts[status]}} 28 | 29 | ); 30 | })} 31 | {!!children &&
{children}
} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/StickyHeader/StickyHeader.module.css: -------------------------------------------------------------------------------- 1 | .stickyHeader { 2 | position: sticky; 3 | top: var(--header-height); 4 | z-index: 2; 5 | background-color: var(--sticky-header-bg); 6 | backdrop-filter: blur(4px); 7 | margin: 0 -1rem; 8 | padding: 0 1rem; 9 | } 10 | 11 | .actionContainer { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin-top: -1rem; 16 | padding-bottom: 1rem; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/StickyHeader/StickyHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import s from './StickyHeader.module.css'; 3 | 4 | export const StickyHeader = ({ 5 | actions, 6 | children, 7 | }: PropsWithChildren<{ actions: React.ReactElement }>) => ( 8 |
9 | {children} 10 | {!!actions &&
{actions}
} 11 |
12 | ); 13 | -------------------------------------------------------------------------------- /packages/ui/src/components/Title/Title.module.css: -------------------------------------------------------------------------------- 1 | .queueTitle { 2 | color: var(--accent-color-d1); 3 | overflow: hidden; 4 | } 5 | 6 | .name { 7 | font-size: 1.2rem; 8 | font-weight: 400; 9 | } 10 | 11 | .description { 12 | margin: 0; 13 | opacity: 0.85; 14 | font-size: 0.85rem; 15 | font-weight: 200; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Title.module.css'; 3 | import { useActiveQueue } from '../../hooks/useActiveQueue'; 4 | 5 | export const Title = () => { 6 | const queue = useActiveQueue(); 7 | 8 | if (!queue) return
; 9 | 10 | return ( 11 |
12 | {queue.displayName && ( 13 | <> 14 |

{queue.displayName}

15 | {queue.description &&

{queue.description}

} 16 | 17 | )} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/Tooltip/Tooltip.module.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: relative; 3 | } 4 | 5 | .tooltip:before { 6 | content: attr(data-title); 7 | padding: 0.2em 0.75em; 8 | background-color: var(--tooltip-bg); 9 | color: var(--tooltip-text-color); 10 | border-radius: 0.28571429rem; 11 | font-size: 0.833rem; 12 | font-weight: 300; 13 | position: absolute; 14 | bottom: 100%; 15 | margin-bottom: 0.75rem; 16 | left: 50%; 17 | transform: translateX(-50%) translateY(-25%); 18 | transition: transform 250ms ease-in-out, opacity 250ms ease-in-out; 19 | pointer-events: none; 20 | opacity: 0; 21 | z-index: 2; 22 | white-space: nowrap; 23 | } 24 | 25 | .tooltip:hover:before { 26 | transform: translateX(-50%) translateY(0); 27 | opacity: 1; 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Tooltip.module.css'; 3 | 4 | export const Tooltip = ({ title, children }: React.PropsWithChildren<{ title: string }>) => ( 5 | 6 | {children} 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/ui/src/constants/languages.ts: -------------------------------------------------------------------------------- 1 | export const languages = ['en-US', 'es-ES', 'fr-FR', 'pt-BR', 'zh-CN'] as const; 2 | -------------------------------------------------------------------------------- /packages/ui/src/constants/queue-stats-status.ts: -------------------------------------------------------------------------------- 1 | import { STATUSES } from '@bull-board/api/src/constants/statuses'; 2 | 3 | export const queueStatsStatusList = [ 4 | STATUSES.active, 5 | STATUSES.waiting, 6 | STATUSES.waitingChildren, 7 | STATUSES.prioritized, 8 | STATUSES.completed, 9 | STATUSES.failed, 10 | STATUSES.delayed, 11 | STATUSES.paused, 12 | ]; 13 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useActiveJobId.ts: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export function useActiveJobId(): string { 5 | const { pathname } = useLocation(); 6 | 7 | const match = matchPath<{ name: string; jobId: string }>(pathname, { 8 | path: ['/queue/:name/:jobId'], 9 | exact: false, 10 | strict: false, 11 | }); 12 | 13 | return decodeURIComponent(match?.params.jobId || ''); 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useActiveQueue.ts: -------------------------------------------------------------------------------- 1 | import { AppQueue } from '@bull-board/api/typings/app'; 2 | import { useActiveQueueName } from './useActiveQueueName'; 3 | import { useQueues } from './useQueues'; 4 | 5 | 6 | export function useActiveQueue(): AppQueue | null { 7 | const { queues } = useQueues(); 8 | 9 | if (!queues) { 10 | return null; 11 | } 12 | 13 | const activeQueueName = useActiveQueueName(); 14 | const activeQueue = queues.find((q) => q.name === activeQueueName); 15 | 16 | return activeQueue || null; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useActiveQueueName.ts: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export function useActiveQueueName(): string { 5 | const { pathname } = useLocation(); 6 | 7 | const match = matchPath<{ name: string; jobId: string }>(pathname, { 8 | path: ['/queue/:name', '/queue/:name/:jobId'], 9 | exact: false, 10 | strict: false, 11 | }); 12 | 13 | return decodeURIComponent(match?.params.name || ''); 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useApi.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Api } from '../services/Api'; 3 | 4 | export const ApiContext = React.createContext(null as any); 5 | 6 | export function useApi() { 7 | return useContext(ApiContext); 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useConfirm.ts: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import { create } from 'zustand'; 3 | import { ConfirmProps } from '../components/ConfirmModal/ConfirmModal'; 4 | 5 | interface ConfirmState { 6 | promise: { resolve: (value: unknown) => void; reject: () => void } | null; 7 | opts: { title?: string; description?: string } | null; 8 | setState(state: Omit): void; 9 | } 10 | 11 | export interface ConfirmApi { 12 | confirmProps: ConfirmProps; 13 | openConfirm: (opts?: ConfirmState['opts']) => Promise; 14 | } 15 | 16 | const useConfirmStore = create((set) => ({ 17 | opts: null, 18 | promise: null, 19 | setState: (state) => set(() => ({ ...state })), 20 | })); 21 | 22 | export function useConfirm(): ConfirmApi { 23 | const { t } = useTranslation(); 24 | const { promise, opts, setState } = useConfirmStore((state) => state); 25 | 26 | return { 27 | confirmProps: { 28 | open: !!promise, 29 | title: opts?.title || t('CONFIRM.DEFAULT_TITLE'), 30 | description: opts?.description || '', 31 | onCancel: function onCancel() { 32 | setState({ 33 | opts: { title: opts?.title, description: opts?.description }, 34 | promise: null, 35 | }); 36 | promise?.reject(); 37 | }, 38 | onConfirm: function onConfirm() { 39 | setState({ 40 | opts: { title: opts?.title, description: opts?.description }, 41 | promise: null, 42 | }); 43 | promise?.resolve(undefined); 44 | }, 45 | }, 46 | openConfirm: function openConfirm(opts: ConfirmState['opts'] = {}) { 47 | return new Promise((resolve, reject) => { 48 | setState({ promise: { resolve, reject }, opts }); 49 | }); 50 | }, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useDarkMode.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSettingsStore } from './useSettings'; 3 | 4 | export function useDarkMode() { 5 | const { darkMode } = useSettingsStore(); 6 | 7 | useEffect(() => { 8 | if (darkMode) { 9 | document.body.classList.add('dark-mode'); 10 | } else { 11 | document.body.classList.remove('dark-mode'); 12 | } 13 | }, [darkMode]); 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useDetailsTabs.tsx: -------------------------------------------------------------------------------- 1 | import { STATUSES } from '@bull-board/api/src/constants/statuses'; 2 | import { Status } from '@bull-board/api/typings/app'; 3 | import { useEffect, useState } from 'react'; 4 | import { useSettingsStore } from './useSettings'; 5 | 6 | export const availableJobTabs = ['Data', 'Options', 'Logs', 'Error'] as const; 7 | 8 | export type TabsType = (typeof availableJobTabs)[number]; 9 | 10 | export function useDetailsTabs(currentStatus: Status) { 11 | const [tabs, updateTabs] = useState([]); 12 | const { defaultJobTab } = useSettingsStore(); 13 | 14 | const [selectedTab, setSelectedTab] = useState( 15 | tabs.find((tab) => tab === defaultJobTab) || tabs[0] 16 | ); 17 | 18 | useEffect(() => { 19 | let nextTabs: TabsType[] = availableJobTabs.filter((tab) => tab !== 'Error'); 20 | if (currentStatus === STATUSES.failed) { 21 | nextTabs = ['Error', ...nextTabs]; 22 | } else { 23 | nextTabs = [...nextTabs, 'Error']; 24 | } 25 | 26 | updateTabs(nextTabs); 27 | }, [currentStatus]); 28 | 29 | useEffect(() => { 30 | setSelectedTab(tabs.includes(defaultJobTab) ? defaultJobTab : tabs[0]); 31 | }, [defaultJobTab, tabs]); 32 | 33 | return { 34 | tabs: tabs?.map((title) => ({ 35 | title, 36 | isActive: title === selectedTab, 37 | selectTab: () => setSelectedTab(title), 38 | })), 39 | selectedTab, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useRef } from 'react'; 2 | 3 | /** 4 | * Based on https://usehooks-ts.com/react-hook/use-interval 5 | * */ 6 | 7 | export function useInterval(callback: () => void, delay: number | null, deps: any[] = []): void { 8 | const savedCallback = useRef(callback); 9 | 10 | // Remember the latest callback if it changes. 11 | useLayoutEffect(() => { 12 | savedCallback.current = callback; 13 | }, [callback]); 14 | 15 | // Set up the interval. 16 | useEffect(() => { 17 | savedCallback.current(); 18 | 19 | // Don't schedule if no delay is specified. 20 | if (delay === null) { 21 | return; 22 | } 23 | let isLastFinished = true; 24 | 25 | const id = setInterval(async () => { 26 | if (!isLastFinished) { 27 | return; 28 | } 29 | 30 | isLastFinished = false; 31 | try { 32 | await savedCallback.current(); 33 | } finally { 34 | isLastFinished = true; 35 | } 36 | }, delay); 37 | 38 | return () => { 39 | clearInterval(id); 40 | }; 41 | }, [delay, ...deps]); 42 | } 43 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useLanguageWatch.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useSettingsStore } from './useSettings'; 4 | 5 | export function useLanguageWatch() { 6 | const { i18n } = useTranslation(); 7 | const { language } = useSettingsStore(); 8 | 9 | useEffect(() => { 10 | if (language && i18n.language !== language) { 11 | i18n.changeLanguage(language); 12 | } 13 | }, [language]); 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useModal() { 4 | type AllModalTypes = ModalTypes | `${ModalTypes}Closing` | null; 5 | const [openedModal, setModalOpen] = useState(null); 6 | 7 | return { 8 | isOpen(modal: ModalTypes): boolean { 9 | return openedModal === modal; 10 | }, 11 | isMounted(modal: ModalTypes): boolean { 12 | return [modal, `${modal}Closing`].includes(openedModal as any); 13 | }, 14 | open(modal: ModalTypes): void { 15 | setModalOpen(modal); 16 | }, 17 | close(modal: ModalTypes): () => void { 18 | return () => { 19 | setModalOpen(`${modal}Closing`); 20 | setTimeout(() => setModalOpen(null), 300); // fadeout animation duration 21 | }; 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | export function useQuery() { 4 | const { search } = useLocation(); 5 | return new URLSearchParams(search); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useScrollTopOnNav.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export function useScrollTopOnNav(): void { 5 | const { key } = useLocation(); 6 | useEffect(() => { 7 | window.scrollTo(0, 0); 8 | }, [key]); 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSelectedStatuses.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { SelectedStatuses, Status } from '../../typings/app'; 4 | import { useActiveQueueName } from './useActiveQueueName'; 5 | 6 | function getActiveStatus(search: string) { 7 | const query = new URLSearchParams(search); 8 | return (query.get('status') as Status) || 'latest'; 9 | } 10 | 11 | export function useSelectedStatuses(): SelectedStatuses { 12 | const { search } = useLocation(); 13 | const activeQueueName = useActiveQueueName(); 14 | 15 | const [selectedStatuses, setSelectedStatuses] = useState({ 16 | [activeQueueName]: getActiveStatus(search), 17 | }); 18 | 19 | useEffect(() => { 20 | const status = getActiveStatus(search); 21 | 22 | if (activeQueueName) { 23 | setSelectedStatuses({ ...selectedStatuses, [activeQueueName]: status }); 24 | } 25 | }, [search, activeQueueName]); 26 | 27 | return selectedStatuses; 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | import { TabsType } from './useDetailsTabs'; 4 | 5 | interface SettingsState { 6 | language: string; 7 | pollingInterval: number; 8 | jobsPerPage: number; 9 | confirmQueueActions: boolean; 10 | confirmJobActions: boolean; 11 | collapseJob: boolean; 12 | collapseJobData: boolean; 13 | collapseJobOptions: boolean; 14 | collapseJobError: boolean; 15 | darkMode: boolean; 16 | defaultJobTab: TabsType; 17 | setSettings: (settings: Partial>) => void; 18 | } 19 | 20 | export const useSettingsStore = create()( 21 | persist( 22 | (set) => ({ 23 | language: '', 24 | pollingInterval: 5, 25 | jobsPerPage: 10, 26 | confirmJobActions: true, 27 | confirmQueueActions: true, 28 | collapseJob: false, 29 | collapseJobData: false, 30 | collapseJobOptions: false, 31 | collapseJobError: false, 32 | darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, 33 | defaultJobTab: 'Data', 34 | setSettings: (settings) => set(() => settings), 35 | }), 36 | { 37 | name: 'board-settings', 38 | } 39 | ) 40 | ); 41 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSortQueues.ts: -------------------------------------------------------------------------------- 1 | import type { AppQueue } from '@bull-board/api/typings/app'; 2 | import { useCallback, useState } from 'react'; 3 | 4 | export type QueueSortKey = 'alphabetical' | keyof AppQueue['counts']; 5 | export type SortDirection = 'asc' | 'desc'; 6 | 7 | export function useSortQueues(queues: AppQueue[]) { 8 | const [sortKey, setSortKey] = useState('alphabetical'); 9 | const [sortDirection, setSortDirection] = useState('asc'); 10 | 11 | const sortedQueues = queues.slice(0).sort((a, z) => { 12 | if (sortKey === 'alphabetical') { 13 | return sortDirection === 'asc' 14 | ? a.displayName!.localeCompare(z.displayName!) 15 | : z.displayName!.localeCompare(a.displayName!); 16 | } 17 | return sortDirection === 'asc' 18 | ? a.counts[sortKey] - z.counts[sortKey] 19 | : z.counts[sortKey] - a.counts[sortKey]; 20 | }); 21 | 22 | const onSort = useCallback( 23 | (newSortKey: QueueSortKey) => { 24 | if (newSortKey === sortKey) { 25 | setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); 26 | } else { 27 | setSortKey(newSortKey); 28 | setSortDirection('asc'); 29 | } 30 | }, 31 | [sortKey, sortDirection] 32 | ); 33 | 34 | return { sortedQueues, sortDirection, sortKey, onSort }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useUIConfig.ts: -------------------------------------------------------------------------------- 1 | import { UIConfig } from '@bull-board/api/typings/app'; 2 | import React, { useContext } from 'react'; 3 | 4 | export const UIConfigContext = React.createContext(null as any); 5 | 6 | export function useUIConfig() { 7 | return useContext(UIConfigContext); 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Loading...
16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { UIConfig } from '@bull-board/api/typings/app'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { App } from './App'; 6 | import { ApiContext } from './hooks/useApi'; 7 | import './index.css'; 8 | import { useSettingsStore } from './hooks/useSettings'; 9 | import { UIConfigContext } from './hooks/useUIConfig'; 10 | import { Api } from './services/Api'; 11 | import './theme.css'; 12 | import './toastify.css'; 13 | import { initI18n } from './services/i18n'; 14 | 15 | const basePath = ((window as any).__basePath__ = 16 | document.head.querySelector('base')?.getAttribute('href') || ''); 17 | const api = new Api({ basePath }); 18 | const uiConfig = JSON.parse( 19 | document.getElementById('__UI_CONFIG__')?.textContent || '{}' 20 | ) as UIConfig; 21 | 22 | if (!!uiConfig.pollingInterval?.forceInterval) { 23 | useSettingsStore.setState({ pollingInterval: uiConfig.pollingInterval.forceInterval }); 24 | } 25 | 26 | const settingsLang = useSettingsStore.getState().language; 27 | const lng = settingsLang || uiConfig.locale?.lng || navigator.language || 'en-US'; 28 | 29 | initI18n({ lng, basePath }).then(() => { 30 | render( 31 | 32 | 33 | 34 | 35 | 36 | 37 | , 38 | document.getElementById('root') 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/ui/src/pages/OverviewPage/OverviewPage.module.css: -------------------------------------------------------------------------------- 1 | .overview { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 4 | grid-gap: 2rem; 5 | list-style: none; 6 | margin: 2rem 0 0; 7 | padding: 0; 8 | } 9 | 10 | .message { 11 | margin-top: 2rem; 12 | white-space: pre-line; 13 | line-height: 1.5rem; 14 | 15 | a { 16 | color: var(--accent-color); 17 | text-decoration: none; 18 | 19 | &:hover { 20 | color: var(--accent-color-d1); 21 | } 22 | } 23 | } 24 | 25 | .dropdown { 26 | margin: 2rem 100% 0; 27 | transform: translateX(-100%); 28 | position: relative; 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | gap: 0.5rem; 33 | } 34 | 35 | .header { 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | } 40 | -------------------------------------------------------------------------------- /packages/ui/src/pages/OverviewPage/OverviewPage.tsx: -------------------------------------------------------------------------------- 1 | import { Status } from '@bull-board/api/dist/typings/app'; 2 | import React from 'react'; 3 | import OverviewDropDownActions from '../../components/OverviewDropDownActions/OverviewDropDownActions'; 4 | import { QueueCard } from '../../components/QueueCard/QueueCard'; 5 | import { StatusLegend } from '../../components/StatusLegend/StatusLegend'; 6 | import { useQuery } from '../../hooks/useQuery'; 7 | import { useQueues } from '../../hooks/useQueues'; 8 | import { useSortQueues } from '../../hooks/useSortQueues'; 9 | import s from './OverviewPage.module.css'; 10 | 11 | export const OverviewPage = () => { 12 | const { actions, queues } = useQueues(); 13 | const query = useQuery(); 14 | 15 | actions.pollQueues(); 16 | 17 | const selectedStatus = query.get('status') as Status; 18 | const filteredQueues = 19 | queues?.filter((queue) => !selectedStatus || queue.counts[selectedStatus] > 0) || []; 20 | 21 | const { 22 | sortedQueues: queuesToView, 23 | onSort, 24 | sortKey, 25 | sortDirection, 26 | } = useSortQueues(filteredQueues); 27 | 28 | return ( 29 |
30 |
31 | 32 | 39 |
40 |
    41 | {queuesToView.map((queue) => ( 42 |
  • 43 | 44 |
  • 45 | ))} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/ui/src/services/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import HttpBackend from 'i18next-http-backend'; 3 | import { initReactI18next } from 'react-i18next'; 4 | import enLocale from 'date-fns/locale/en-US'; 5 | import { languages } from '../constants/languages'; 6 | 7 | export let dateFnsLocale = enLocale; 8 | const dateFnsLocaleMap = { 9 | 'es-ES': 'es', 10 | 'fr-FR': 'fr', 11 | } as const; 12 | 13 | async function setDateFnsLocale(lng: string) { 14 | const languageToLoad = dateFnsLocaleMap[lng as keyof typeof dateFnsLocaleMap] || lng; 15 | dateFnsLocale = await import(`date-fns/locale/${languageToLoad}/index.js`).catch((e) => { 16 | if (process.env.NODE_ENV === 'development') { 17 | // eslint-disable-next-line no-console 18 | console.info(e); 19 | } 20 | 21 | return enLocale; 22 | }); 23 | } 24 | 25 | export async function initI18n({ lng, basePath }: { lng: string; basePath: string }) { 26 | const fallbackLng = 'en-US'; 27 | const supportedLanguage = languages.find((language) => language === lng) || fallbackLng; 28 | 29 | const i18nextInstance = i18n 30 | .use(initReactI18next) // passes i18n down to react-i18next 31 | .use(HttpBackend); 32 | 33 | if (process.env.NODE_ENV === 'development') { 34 | const { HMRPlugin } = await import('i18next-hmr/plugin'); 35 | i18nextInstance.use(new HMRPlugin({ webpack: { client: true } })); 36 | (window as any).testI18n = (lng = 'cimode') => i18nextInstance.changeLanguage(lng); 37 | } 38 | 39 | i18nextInstance.on('languageChanged', (newLanguage) => setDateFnsLocale(newLanguage)); 40 | await setDateFnsLocale(supportedLanguage); 41 | 42 | return i18nextInstance.init({ 43 | lng: supportedLanguage, 44 | fallbackLng, 45 | defaultNS: 'messages', 46 | ns: 'messages', 47 | load: 'currentOnly', 48 | backend: { 49 | loadPath: `${basePath}static/locales/{{lng}}/{{ns}}.json`, 50 | queryParams: { v: process.env.APP_VERSION }, 51 | }, 52 | interpolation: { 53 | escapeValue: false, // react already safes from xss 54 | }, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/ui/src/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmosh/bull-board/6f48998b479dd52821fceed494459e6ae6824cff/packages/ui/src/static/favicon-32x32.png -------------------------------------------------------------------------------- /packages/ui/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmosh/bull-board/6f48998b479dd52821fceed494459e6ae6824cff/packages/ui/src/static/favicon.ico -------------------------------------------------------------------------------- /packages/ui/src/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /packages/ui/src/theme.css: -------------------------------------------------------------------------------- 1 | .hljs { 2 | display: block; 3 | padding: 0.5em; 4 | white-space: pre-wrap; 5 | word-break: break-all; 6 | } 7 | 8 | .hljs-comment, 9 | .hljs-quote { 10 | color: #998; 11 | font-style: italic; 12 | } 13 | 14 | .hljs-keyword, 15 | .hljs-selector-tag, 16 | .hljs-subst { 17 | font-weight: 500; 18 | } 19 | 20 | .hljs-number, 21 | .hljs-tag .hljs-attr, 22 | .hljs-template-variable, 23 | .hljs-variable { 24 | color: var(--hl-number); 25 | } 26 | 27 | .hljs-doctag, 28 | .hljs-string { 29 | color: var(--hl-string); 30 | } 31 | 32 | .hljs-section, 33 | .hljs-selector-id, 34 | .hljs-title { 35 | color: #d73a49; 36 | font-weight: 500; 37 | } 38 | 39 | .hljs-class .hljs-title, 40 | .hljs-type, 41 | .hljs-attr { 42 | color: var(--hl-type); 43 | font-weight: 500; 44 | } 45 | 46 | .hljs-attribute, 47 | .hljs-name, 48 | .hljs-tag { 49 | color: var(--hl-attribute); 50 | font-weight: 400; 51 | } 52 | 53 | .hljs-link, 54 | .hljs-regexp { 55 | color: #009926; 56 | } 57 | 58 | .hljs-keyword, 59 | .hljs-bullet, 60 | .hljs-symbol { 61 | color: var(--hl-keyword); 62 | } 63 | 64 | .hljs-built_in, 65 | .hljs-builtin-name { 66 | color: var(--hl-built_in); 67 | } 68 | 69 | .hljs-meta { 70 | color: var(--hl-meta); 71 | font-weight: 500; 72 | } 73 | 74 | .hljs-deletion { 75 | background: #fdd; 76 | } 77 | 78 | .hljs-addition { 79 | background: #dfd; 80 | } 81 | 82 | .hljs-emphasis { 83 | font-style: italic; 84 | } 85 | 86 | .hljs-strong { 87 | font-weight: 500; 88 | } 89 | 90 | .hljs-trace-line { 91 | color: #aaa; 92 | } 93 | 94 | .hljs-trace-line .hljs-code-path { 95 | color: var(--text-color); 96 | } 97 | 98 | .hljs-punctuation { 99 | color: var(--accent-color-d1); 100 | } 101 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getConfirmFor.ts: -------------------------------------------------------------------------------- 1 | export function getConfirmFor( 2 | afterAction: () => any, 3 | openConfirm: (params: { description: string }) => Promise 4 | ) { 5 | return function withConfirmAndFn( 6 | action: () => Promise, 7 | description: string, 8 | shouldConfirm: boolean 9 | ) { 10 | return async () => { 11 | try { 12 | if (shouldConfirm) { 13 | await openConfirm({ description }); 14 | } 15 | await action(); 16 | await afterAction(); 17 | } catch (e) { 18 | if (e) { 19 | // eslint-disable-next-line no-console 20 | console.error(e); 21 | } 22 | } 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getStaticPath.ts: -------------------------------------------------------------------------------- 1 | export function getStaticPath(path: string): string { 2 | return `static${path}`; 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/utils/highlight/config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import hljs from 'highlight.js/lib/core'; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore 6 | import json from 'highlight.js/lib/languages/json'; 7 | import { stacktraceJS } from './languages/stacktrace'; 8 | 9 | hljs.registerLanguage('json', json); 10 | hljs.registerLanguage('stacktrace', stacktraceJS); 11 | 12 | export const highlighter = hljs; 13 | -------------------------------------------------------------------------------- /packages/ui/src/utils/highlight/highlight.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | 3 | const isWebworkerSupported = typeof window.Worker !== 'undefined'; 4 | let highlightWorker: Worker | null = null; 5 | const messageQueue = new Map(); 6 | 7 | export async function asyncHighlight(code: string, language: string): Promise { 8 | if (isWebworkerSupported) { 9 | if (!highlightWorker) { 10 | highlightWorker = new Worker(new URL('./worker.ts', import.meta.url), { 11 | name: 'highlight-worker', 12 | }); 13 | highlightWorker.onmessage = ({ data }) => { 14 | const { id, code } = data; 15 | if (messageQueue.has(id)) { 16 | const { resolve } = messageQueue.get(id) as any; 17 | resolve(code); 18 | } 19 | }; 20 | } 21 | 22 | return new Promise((resolve, reject) => { 23 | const messageId = nanoid(5); 24 | highlightWorker?.postMessage({ id: messageId, code, language }); 25 | messageQueue.set(messageId, { 26 | resolve: (formattedCode: string) => { 27 | messageQueue.delete(messageId); 28 | resolve(formattedCode); 29 | }, 30 | reject: () => { 31 | messageQueue.delete(messageId); 32 | reject(); 33 | }, 34 | }); 35 | setTimeout(() => reject(), 60 * 1000); 36 | }); 37 | } else { 38 | const { highlighter } = await import( 39 | /* webpackChunkName: "highlight" */ 40 | /* webpackMode: "lazy-once" */ 41 | /* webpackPreload: true */ 42 | './config' 43 | ); 44 | 45 | return highlighter.highlightAuto(code, [language]).value || ''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/ui/src/utils/highlight/languages/stacktrace.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Language: StacktraceJS 3 | Author: FelixMosh 4 | Description: Node stacktrace highlighter 5 | */ 6 | 7 | export function stacktraceJS(): any { 8 | const ERROR = { 9 | className: 'type', 10 | begin: /^\w*Error:\s*/, 11 | relevance: 40, 12 | contains: [ 13 | { 14 | className: 'title', 15 | begin: /.*/, 16 | end: /$/, 17 | excludeStart: true, 18 | endsWithParent: true, 19 | }, 20 | ], 21 | }; 22 | 23 | const LINE_NUMBER = { 24 | className: 'number', 25 | begin: ':\\d+:\\d+', 26 | relevance: 5, 27 | }; 28 | 29 | const TRACE_LINE = { 30 | className: 'trace-line', 31 | begin: /^\s*at/, 32 | end: /$/, 33 | keywords: 'at as async prototype anonymous function', 34 | contains: [ 35 | { 36 | className: 'code-path', 37 | begin: /\(/, 38 | end: /\)$/, 39 | excludeEnd: true, 40 | excludeBegin: true, 41 | contains: [LINE_NUMBER], 42 | }, 43 | ], 44 | }; 45 | 46 | return { 47 | case_insensitive: true, 48 | contains: [ERROR, TRACE_LINE, LINE_NUMBER], 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/src/utils/highlight/worker.ts: -------------------------------------------------------------------------------- 1 | import { highlighter } from './config'; 2 | 3 | self.onmessage = ({ data = {} }) => { 4 | const { id = '', code = '', language = '' } = data; 5 | if (!id || !code || !language) { 6 | return; 7 | } 8 | 9 | const resp = highlighter.highlightAuto(code, [language]); 10 | 11 | self.postMessage({ code: resp.value, id }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ui/src/utils/links.ts: -------------------------------------------------------------------------------- 1 | import { SelectedStatuses, Status } from '../../typings/app'; 2 | 3 | export const links = { 4 | dashboardPage(status?: Status) { 5 | const search = status ? new URLSearchParams({ status }).toString() : ''; 6 | return { 7 | pathname: '/', 8 | search, 9 | }; 10 | }, 11 | queuePage( 12 | queueName: string, 13 | selectedStatuses: SelectedStatuses = {} 14 | ): { pathname: string; search: string } { 15 | const { pathname, searchParams } = new URL( 16 | `/queue/${encodeURIComponent(queueName)}`, 17 | 'http://fake.com' 18 | ); 19 | 20 | const withStatus = selectedStatuses[queueName] && selectedStatuses[queueName] !== 'latest'; 21 | if (withStatus) { 22 | searchParams.set('status', selectedStatuses[queueName]); 23 | } 24 | 25 | return { 26 | pathname, 27 | search: searchParams.toString(), 28 | }; 29 | }, 30 | jobPage( 31 | queueName: string, 32 | jobId: string, 33 | selectedStatuses: SelectedStatuses = {} 34 | ): { pathname: string; search: string } { 35 | const { pathname: queuePath, search } = links.queuePage(queueName, selectedStatuses); 36 | const { pathname } = new URL(`${queuePath}/${encodeURIComponent(jobId)}`, 'http://fake.com'); 37 | return { 38 | pathname, 39 | search, 40 | }; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ui/src/utils/toCamelCase.ts: -------------------------------------------------------------------------------- 1 | export function toCamelCase(val: string): string { 2 | return val 3 | .split('-') 4 | .map((part, idx) => (idx > 0 ? `${part[0].toUpperCase()}${part.slice(1)}` : part)) 5 | .join(''); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/utils/toTree.ts: -------------------------------------------------------------------------------- 1 | import { AppQueue } from '@bull-board/api/typings/app'; 2 | 3 | export interface AppQueueTreeNode { 4 | name: string; 5 | queue?: AppQueue; 6 | children: AppQueueTreeNode[]; 7 | } 8 | 9 | export function toTree(queues: AppQueue[]): AppQueueTreeNode { 10 | const root: AppQueueTreeNode = { 11 | name: 'root', 12 | children: [], 13 | }; 14 | 15 | queues.forEach((queue) => { 16 | if (!queue.delimiter) { 17 | // If no delimiter, add as direct child to root 18 | root.children.push({ 19 | name: queue.name, 20 | queue, 21 | children: [], 22 | }); 23 | return; 24 | } 25 | 26 | const nameToSplit = 27 | queue.name.startsWith('{') && queue.name.endsWith('}') ? queue.name.slice(1, -1) : queue.name; 28 | const parts = nameToSplit.split(queue.delimiter); 29 | let currentLevel = root.children; 30 | 31 | parts.forEach((part, index) => { 32 | let node = currentLevel.find((n) => n.name === part); 33 | 34 | if (!node) { 35 | const isLeafNode = index === parts.length - 1; 36 | node = { 37 | name: part, 38 | children: [], 39 | // Only set queue data if we're at the leaf node 40 | ...(isLeafNode ? { queue } : {}), 41 | }; 42 | currentLevel.push(node); 43 | } 44 | 45 | currentLevel = node.children; 46 | }); 47 | }); 48 | 49 | return root; 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "esModuleInterop": true, 5 | "lib": ["es2021", "dom"], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "jsx": "react", 12 | "target": "es2021", 13 | "noUnusedParameters": true, 14 | "noUnusedLocals": true, 15 | "resolveJsonModule": true, 16 | "declaration": true, 17 | "noEmit": true, 18 | "skipLibCheck": true 19 | }, 20 | "include": ["./src", "./typings/*.d.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/typings/app.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppJob, 3 | AppQueue, 4 | JobCleanStatus, 5 | JobRetryStatus, 6 | Status, 7 | } from '@bull-board/api/typings/app'; 8 | 9 | export { Status } from '@bull-board/api/typings/app'; 10 | 11 | export type SelectedStatuses = Record; 12 | 13 | export interface QueueActions { 14 | pauseAll: () => Promise; 15 | resumeAll: () => Promise; 16 | retryAll: (queueName: string, status: JobRetryStatus) => () => Promise; 17 | promoteAll: (queueName: string) => () => Promise; 18 | cleanAll: (queueName: string, status: JobCleanStatus) => () => Promise; 19 | pauseQueue: (queueName: string) => () => Promise; 20 | resumeQueue: (queueName: string) => () => Promise; 21 | emptyQueue: (queueName: string) => () => Promise; 22 | updateQueues: () => Promise; 23 | pollQueues: () => void; 24 | addJob: ( 25 | queueName: string, 26 | jobName: string, 27 | jobData: any, 28 | jobOptions: any 29 | ) => () => Promise; 30 | } 31 | 32 | export interface JobActions { 33 | promoteJob: (queueName: string) => (job: AppJob) => () => Promise; 34 | retryJob: (queueName: string, status: JobRetryStatus) => (job: AppJob) => () => Promise; 35 | cleanJob: (queueName: string) => (job: AppJob) => () => Promise; 36 | updateJobData: ( 37 | queueName: string, 38 | job: AppJob, 39 | newData: Record 40 | ) => () => Promise; 41 | getJobLogs: (queueName: string) => (job: AppJob) => () => Promise; 42 | getJob: () => Promise; 43 | pollJob: () => void; 44 | } 45 | -------------------------------------------------------------------------------- /packages/ui/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const resource: Record; 3 | export = resource; 4 | } 5 | 6 | declare module 'jsoneditor-react' { 7 | export const JsonEditor: (options: any) => JSX.Element; 8 | } 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | singleQuote: true, 4 | printWidth: 100, 5 | }; 6 | -------------------------------------------------------------------------------- /screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmosh/bull-board/6f48998b479dd52821fceed494459e6ae6824cff/screenshots/dashboard.png -------------------------------------------------------------------------------- /screenshots/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmosh/bull-board/6f48998b479dd52821fceed494459e6ae6824cff/screenshots/overview.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "lib": ["es2019", "dom"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noImplicitAny": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "target": "es2019", 11 | "noUnusedParameters": true, 12 | "noUnusedLocals": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "skipLibCheck": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------