From a5d937ade340574d02d69e5d7ad0d767c3904e30 Mon Sep 17 00:00:00 2001 From: Jeremy Gillick Date: Sat, 19 Feb 2022 03:41:59 -0800 Subject: [PATCH] Add canRetry options to queue (#384) * Add canRetry options to queue * Add unit tests * Rename canRetry to allowRetries * Update readme with allowRetries option * Copy change. --- README.md | 22 +++ packages/api/src/handlers/queues.ts | 1 + packages/api/src/queueAdapters/base.ts | 2 + packages/api/tests/api/index.spec.ts | 142 ++++++++++++++++++ packages/api/typings/app.ts | 2 + .../JobCard/JobActions/JobActions.tsx | 8 +- .../ui/src/components/JobCard/JobCard.tsx | 7 +- .../components/QueueActions/QueueActions.tsx | 17 ++- .../ui/src/components/QueuePage/QueuePage.tsx | 8 +- 9 files changed, 197 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0e6956f9..9d35b9f5 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,28 @@ const { setQueues, replaceQueues } = createBullBoard({ }) ``` +2. `allowRetries` (default: `true`) +When set to `false` the UI removes the job retry buttons for a queue. This option will be ignored if `readOnlyMode` is `true`. + +```js +const QueueMQ = require('bullmq') +const { createBullBoard } = require('@bull-board/api') +const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter') +const { BullAdapter } = require('@bull-board/api/bullAdapter') + +const someQueue = new Queue() +const someOtherQueue = new Queue() +const queueMQ = new QueueMQ() + +const { setQueues, replaceQueues } = createBullBoard({ + queues: [ + new BullAdapter(someQueue), + new BullAdapter(someOtherQueue, , { allowRetries: false }), // No retry buttons + new BullMQAdapter(queueMQ, { allowRetries: true, readOnlyMode: true }), // allowRetries will be ignored in this case in lieu of readOnlyMode + ] +}) +``` + ### Hosting router on a sub path If you host your express service on a different path than root (/) ie. https:////, then you can add the following code to provide the configuration to the bull-board router. In this example the sub path will be `my-base-path`. diff --git a/packages/api/src/handlers/queues.ts b/packages/api/src/handlers/queues.ts index bd8c1927..ddc8666a 100644 --- a/packages/api/src/handlers/queues.ts +++ b/packages/api/src/handlers/queues.ts @@ -115,6 +115,7 @@ async function getAppQueues( jobs: jobs.filter(Boolean).map((job) => formatJob(job, queue)), pagination, readOnlyMode: queue.readOnlyMode, + allowRetries: queue.allowRetries, isPaused, }; }) diff --git a/packages/api/src/queueAdapters/base.ts b/packages/api/src/queueAdapters/base.ts index dedbe1b3..772b9677 100644 --- a/packages/api/src/queueAdapters/base.ts +++ b/packages/api/src/queueAdapters/base.ts @@ -9,11 +9,13 @@ import { export abstract class BaseAdapter { public readonly readOnlyMode: boolean; + public readonly allowRetries: boolean; public readonly prefix: string; private formatters = new Map any>(); protected constructor(options: Partial = {}) { this.readOnlyMode = options.readOnlyMode === true; + this.allowRetries = this.readOnlyMode ? false : options.allowRetries !== false; this.prefix = options.prefix || ''; } diff --git a/packages/api/tests/api/index.spec.ts b/packages/api/tests/api/index.spec.ts index 890a490d..301ad12a 100644 --- a/packages/api/tests/api/index.spec.ts +++ b/packages/api/tests/api/index.spec.ts @@ -42,6 +42,7 @@ describe('happy', () => { Object { "queues": Array [ Object { + "allowRetries": true, "counts": Object { "active": 0, "completed": 0, @@ -126,6 +127,7 @@ describe('happy', () => { Object { "queues": Array [ Object { + "allowRetries": true, "counts": Object { "active": 0, "completed": 0, @@ -193,6 +195,7 @@ describe('happy', () => { Object { "queues": Array [ Object { + "allowRetries": true, "counts": Object { "active": 0, "completed": 0, @@ -326,6 +329,7 @@ describe('happy', () => { Object { "queues": Array [ Object { + "allowRetries": true, "counts": Object { "active": 0, "completed": 0, @@ -360,4 +364,142 @@ describe('happy', () => { ); }); }); + + it('should disable retries in queue', async () => { + const paintQueue = new Queue('Paint', { + connection: { + host: 'localhost', + port: 6379, + }, + }); + + createBullBoard({ + queues: [new BullMQAdapter(paintQueue, { allowRetries: false })], + serverAdapter, + }); + + await request(serverAdapter.getRouter()) + .get('/api/queues') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(JSON.parse(res.text)).toMatchInlineSnapshot( + { + stats: { + blocked_clients: expect.any(String), + connected_clients: expect.any(String), + mem_fragmentation_ratio: expect.any(String), + redis_version: expect.any(String), + total_system_memory: expect.any(String), + used_memory: expect.any(String), + }, + }, + ` + Object { + "queues": Array [ + Object { + "allowRetries": false, + "counts": Object { + "active": 0, + "completed": 0, + "delayed": 0, + "failed": 0, + "paused": 0, + "waiting": 0, + }, + "isPaused": false, + "jobs": Array [], + "name": "Paint", + "pagination": Object { + "pageCount": 1, + "range": Object { + "end": 9, + "start": 0, + }, + }, + "readOnlyMode": false, + }, + ], + "stats": Object { + "blocked_clients": Any, + "connected_clients": Any, + "mem_fragmentation_ratio": Any, + "redis_version": Any, + "total_system_memory": Any, + "used_memory": Any, + }, + } + ` + ); + }); + }); + + it('should disable retries in queue if readOnlyMode is true', async () => { + const paintQueue = new Queue('Paint', { + connection: { + host: 'localhost', + port: 6379, + }, + }); + + createBullBoard({ + queues: [new BullMQAdapter(paintQueue, { allowRetries: true, readOnlyMode: true })], + serverAdapter, + }); + + await request(serverAdapter.getRouter()) + .get('/api/queues') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + expect(JSON.parse(res.text)).toMatchInlineSnapshot( + { + stats: { + blocked_clients: expect.any(String), + connected_clients: expect.any(String), + mem_fragmentation_ratio: expect.any(String), + redis_version: expect.any(String), + total_system_memory: expect.any(String), + used_memory: expect.any(String), + }, + }, + ` + Object { + "queues": Array [ + Object { + "allowRetries": false, + "counts": Object { + "active": 0, + "completed": 0, + "delayed": 0, + "failed": 0, + "paused": 0, + "waiting": 0, + }, + "isPaused": false, + "jobs": Array [], + "name": "Paint", + "pagination": Object { + "pageCount": 1, + "range": Object { + "end": 9, + "start": 0, + }, + }, + "readOnlyMode": true, + }, + ], + "stats": Object { + "blocked_clients": Any, + "connected_clients": Any, + "mem_fragmentation_ratio": Any, + "redis_version": Any, + "total_system_memory": Any, + "used_memory": Any, + }, + } + ` + ); + }); + }); }); diff --git a/packages/api/typings/app.ts b/packages/api/typings/app.ts index 08da7b51..95355c6b 100644 --- a/packages/api/typings/app.ts +++ b/packages/api/typings/app.ts @@ -11,6 +11,7 @@ export type JobCounts = Record; export interface QueueAdapterOptions { readOnlyMode: boolean; + allowRetries: boolean; prefix: string; } @@ -80,6 +81,7 @@ export interface AppQueue { jobs: AppJob[]; pagination: Pagination; readOnlyMode: boolean; + allowRetries: boolean; isPaused: boolean; } diff --git a/packages/ui/src/components/JobCard/JobActions/JobActions.tsx b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx index 97ef935c..686c6bc0 100644 --- a/packages/ui/src/components/JobCard/JobActions/JobActions.tsx +++ b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx @@ -10,6 +10,7 @@ import { STATUSES } from '@bull-board/api/src/constants/statuses'; interface JobActionsProps { status: Status; + allowRetries: boolean; actions: { promoteJob: () => Promise; retryJob: () => Promise; @@ -36,11 +37,14 @@ const statusToButtonsMap: Record = { [STATUSES.waiting]: [buttonTypes.clean], }; -export const JobActions = ({ actions, status }: JobActionsProps) => { - const buttons = statusToButtonsMap[status]; +export const JobActions = ({ actions, status, allowRetries }: JobActionsProps) => { + let buttons = statusToButtonsMap[status]; if (!buttons) { return null; } + if (!allowRetries) { + buttons = buttons.filter((btn) => btn.actionKey !== 'retryJob'); + } return (
    {buttons.map((type) => ( diff --git a/packages/ui/src/components/JobCard/JobCard.tsx b/packages/ui/src/components/JobCard/JobCard.tsx index 53ba7345..831478e5 100644 --- a/packages/ui/src/components/JobCard/JobCard.tsx +++ b/packages/ui/src/components/JobCard/JobCard.tsx @@ -11,6 +11,7 @@ interface JobCardProps { job: AppJob; status: Status; readOnlyMode: boolean; + allowRetries: boolean; actions: { promoteJob: () => Promise; retryJob: () => Promise; @@ -21,7 +22,7 @@ interface JobCardProps { const greenStatuses = [STATUSES.active, STATUSES.completed]; -export const JobCard = ({ job, status, actions, readOnlyMode }: JobCardProps) => ( +export const JobCard = ({ job, status, actions, readOnlyMode, allowRetries }: JobCardProps) => (
    #{job.id} @@ -39,7 +40,9 @@ export const JobCard = ({ job, status, actions, readOnlyMode }: JobCardProps) => )} - {!readOnlyMode && } + {!readOnlyMode && ( + + )}
    diff --git a/packages/ui/src/components/QueueActions/QueueActions.tsx b/packages/ui/src/components/QueueActions/QueueActions.tsx index fa1a0619..065a1305 100644 --- a/packages/ui/src/components/QueueActions/QueueActions.tsx +++ b/packages/ui/src/components/QueueActions/QueueActions.tsx @@ -11,6 +11,7 @@ interface QueueActionProps { queue: AppQueue; actions: Store['actions']; status: Status; + allowRetries: boolean; } const ACTIONABLE_STATUSES = [STATUSES.failed, STATUSES.delayed, STATUSES.completed] as const; @@ -24,7 +25,7 @@ const CleanAllButton = ({ onClick }: any) => ( ); -export const QueueActions = ({ status, actions, queue }: QueueActionProps) => { +export const QueueActions = ({ status, actions, queue, allowRetries }: QueueActionProps) => { if (!isStatusActionable(status)) { return null; } @@ -33,12 +34,14 @@ export const QueueActions = ({ status, actions, queue }: QueueActionProps) => {
      {status === STATUSES.failed && ( <> -
    • - -
    • + {allowRetries && ( +
    • + +
    • + )}
    • diff --git a/packages/ui/src/components/QueuePage/QueuePage.tsx b/packages/ui/src/components/QueuePage/QueuePage.tsx index 508d63d8..1a1fb4e5 100644 --- a/packages/ui/src/components/QueuePage/QueuePage.tsx +++ b/packages/ui/src/components/QueuePage/QueuePage.tsx @@ -27,7 +27,12 @@ export const QueuePage = ({
      {queue.jobs.length > 0 && !queue.readOnlyMode && ( - + )}
      @@ -45,6 +50,7 @@ export const QueuePage = ({ getJobLogs: actions.getJobLogs(queue?.name)(job), }} readOnlyMode={queue?.readOnlyMode} + allowRetries={queue?.allowRetries} /> ))}