Skip to content

Commit

Permalink
Add canRetry options to queue (#384)
Browse files Browse the repository at this point in the history
* Add canRetry options to queue

* Add unit tests

* Rename canRetry to allowRetries

* Update readme with allowRetries option

* Copy change.
  • Loading branch information
jgillick authored Feb 19, 2022
1 parent 52d733a commit a5d937a
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 12 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<server_name>/<sub_path>/, 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`.
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/handlers/queues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ async function getAppQueues(
jobs: jobs.filter(Boolean).map((job) => formatJob(job, queue)),
pagination,
readOnlyMode: queue.readOnlyMode,
allowRetries: queue.allowRetries,
isPaused,
};
})
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/queueAdapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormatterField, (data: any) => any>();

protected constructor(options: Partial<QueueAdapterOptions> = {}) {
this.readOnlyMode = options.readOnlyMode === true;
this.allowRetries = this.readOnlyMode ? false : options.allowRetries !== false;
this.prefix = options.prefix || '';
}

Expand Down
142 changes: 142 additions & 0 deletions packages/api/tests/api/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('happy', () => {
Object {
"queues": Array [
Object {
"allowRetries": true,
"counts": Object {
"active": 0,
"completed": 0,
Expand Down Expand Up @@ -126,6 +127,7 @@ describe('happy', () => {
Object {
"queues": Array [
Object {
"allowRetries": true,
"counts": Object {
"active": 0,
"completed": 0,
Expand Down Expand Up @@ -193,6 +195,7 @@ describe('happy', () => {
Object {
"queues": Array [
Object {
"allowRetries": true,
"counts": Object {
"active": 0,
"completed": 0,
Expand Down Expand Up @@ -326,6 +329,7 @@ describe('happy', () => {
Object {
"queues": Array [
Object {
"allowRetries": true,
"counts": Object {
"active": 0,
"completed": 0,
Expand Down Expand Up @@ -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<String>,
"connected_clients": Any<String>,
"mem_fragmentation_ratio": Any<String>,
"redis_version": Any<String>,
"total_system_memory": Any<String>,
"used_memory": Any<String>,
},
}
`
);
});
});

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<String>,
"connected_clients": Any<String>,
"mem_fragmentation_ratio": Any<String>,
"redis_version": Any<String>,
"total_system_memory": Any<String>,
"used_memory": Any<String>,
},
}
`
);
});
});
});
2 changes: 2 additions & 0 deletions packages/api/typings/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type JobCounts = Record<Status, number>;

export interface QueueAdapterOptions {
readOnlyMode: boolean;
allowRetries: boolean;
prefix: string;
}

Expand Down Expand Up @@ -80,6 +81,7 @@ export interface AppQueue {
jobs: AppJob[];
pagination: Pagination;
readOnlyMode: boolean;
allowRetries: boolean;
isPaused: boolean;
}

Expand Down
8 changes: 6 additions & 2 deletions packages/ui/src/components/JobCard/JobActions/JobActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { STATUSES } from '@bull-board/api/src/constants/statuses';

interface JobActionsProps {
status: Status;
allowRetries: boolean;
actions: {
promoteJob: () => Promise<void>;
retryJob: () => Promise<void>;
Expand All @@ -36,11 +37,14 @@ const statusToButtonsMap: Record<string, ButtonType[]> = {
[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 (
<ul className={s.jobActions}>
{buttons.map((type) => (
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/src/components/JobCard/JobCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface JobCardProps {
job: AppJob;
status: Status;
readOnlyMode: boolean;
allowRetries: boolean;
actions: {
promoteJob: () => Promise<void>;
retryJob: () => Promise<void>;
Expand All @@ -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) => (
<div className={s.card}>
<div className={s.sideInfo}>
<span title={`#${job.id}`}>#{job.id}</span>
Expand All @@ -39,7 +40,9 @@ export const JobCard = ({ job, status, actions, readOnlyMode }: JobCardProps) =>
</span>
)}
</h4>
{!readOnlyMode && <JobActions status={status} actions={actions} />}
{!readOnlyMode && (
<JobActions status={status} actions={actions} allowRetries={allowRetries} />
)}
</div>
<div className={s.content}>
<Details status={status} job={job} actions={actions} />
Expand Down
17 changes: 10 additions & 7 deletions packages/ui/src/components/QueueActions/QueueActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +25,7 @@ const CleanAllButton = ({ onClick }: any) => (
</Button>
);

export const QueueActions = ({ status, actions, queue }: QueueActionProps) => {
export const QueueActions = ({ status, actions, queue, allowRetries }: QueueActionProps) => {
if (!isStatusActionable(status)) {
return null;
}
Expand All @@ -33,12 +34,14 @@ export const QueueActions = ({ status, actions, queue }: QueueActionProps) => {
<ul className={s.queueActions}>
{status === STATUSES.failed && (
<>
<li>
<Button onClick={actions.retryAll(queue.name)} className={s.button}>
<RetryIcon />
Retry all
</Button>
</li>
{allowRetries && (
<li>
<Button onClick={actions.retryAll(queue.name)} className={s.button}>
<RetryIcon />
Retry all
</Button>
</li>
)}
<li>
<CleanAllButton onClick={actions.cleanAllFailed(queue.name)} />
</li>
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/components/QueuePage/QueuePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export const QueuePage = ({
<div className={s.actionContainer}>
<div>
{queue.jobs.length > 0 && !queue.readOnlyMode && (
<QueueActions queue={queue} actions={actions} status={selectedStatus[queue.name]} />
<QueueActions
queue={queue}
actions={actions}
status={selectedStatus[queue.name]}
allowRetries={queue.allowRetries}
/>
)}
</div>
<Pagination pageCount={queue.pagination.pageCount} />
Expand All @@ -45,6 +50,7 @@ export const QueuePage = ({
getJobLogs: actions.getJobLogs(queue?.name)(job),
}}
readOnlyMode={queue?.readOnlyMode}
allowRetries={queue?.allowRetries}
/>
))}
</section>
Expand Down

0 comments on commit a5d937a

Please sign in to comment.