Skip to content

Commit

Permalink
feat: add new job to queue
Browse files Browse the repository at this point in the history
  • Loading branch information
felixmosh committed May 22, 2024
1 parent 1a1ffd0 commit 95bc661
Show file tree
Hide file tree
Showing 21 changed files with 1,117 additions and 200 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = {
files: ['*.{ts,tsx}'],
extends: ['plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-use-before-define': 'off',
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 @@ -89,6 +89,7 @@ async function getAppQueues(
allowRetries: queue.allowRetries,
allowCompletedRetries: queue.allowCompletedRetries,
isPaused,
type: queue.type,
};
})
);
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/queueAdapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
QueueAdapterOptions,
QueueJob,
QueueJobOptions,
QueueType,
Status,
} from '../../typings/app';

Expand All @@ -15,16 +16,19 @@ export abstract class BaseAdapter {
public readonly allowCompletedRetries: boolean;
public readonly prefix: string;
public readonly description: string;
public readonly type: QueueType;
private formatters = new Map<FormatterField, (data: any) => any>();

protected constructor(
type: QueueType,
options: Partial<QueueAdapterOptions & { allowCompletedRetries: boolean }> = {}
) {
this.readOnlyMode = options.readOnlyMode === true;
this.allowRetries = this.readOnlyMode ? false : options.allowRetries !== false;
this.allowCompletedRetries = this.allowRetries && options.allowCompletedRetries !== false;
this.prefix = options.prefix || '';
this.description = options.description || '';
this.type = type;
}

public getDescription(): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/queueAdapters/bull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BaseAdapter } from './base';

export class BullAdapter extends BaseAdapter {
constructor(public queue: Queue, options: Partial<QueueAdapterOptions> = {}) {
super({ ...options, allowCompletedRetries: false });
super('bull', { ...options, allowCompletedRetries: false });
}

public getRedisInfo(): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/queueAdapters/bullMQ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BaseAdapter } from './base';

export class BullMQAdapter extends BaseAdapter {
constructor(private queue: Queue, options: Partial<QueueAdapterOptions> = {}) {
super(options);
super('bullmq', options);
}

public async getRedisInfo(): Promise<string> {
Expand Down
3 changes: 3 additions & 0 deletions packages/api/typings/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export interface AppJob {
isFailed: boolean;
}

export type QueueType = 'bull' | 'bullmq';

export interface AppQueue {
name: string;
description?: string;
Expand All @@ -123,6 +125,7 @@ export interface AppQueue {
allowRetries: boolean;
allowCompletedRetries: boolean;
isPaused: boolean;
type: QueueType;
}

export type HTTPMethod = 'get' | 'post' | 'put';
Expand Down
15 changes: 12 additions & 3 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
"build": "NODE_ENV=production webpack --mode=production",
"build:analyze": "NODE_ENV=production ANALYZE=true webpack --mode=production",
"clean": "rm -rf dist",
"sync:locales": "npx i18next-locales-sync -c ./localesSync.config.js"
"sync:locales": "npx i18next-locales-sync -c ./localesSync.config.js",
"gen:jsonSchema": "npm run gen:jsonSchema:bullmq && npm run gen:jsonSchema:bull",
"gen:jsonSchema:bullmq": "npx ts-json-schema-generator --path '../../node_modules/bullmq/dist/esm/interfaces/**/*.ts' --type 'JobsOptions' --out './src/schemas/bullmq/jobOptions.json'",
"gen:jsonSchema:bull": "npx ts-json-schema-generator --path '../../node_modules/bull/*.ts' --type 'Bull.JobOptions' --out './src/schemas/bull/jobOptions.json'"
},
"dependencies": {
"@bull-board/api": "5.17.1"
Expand All @@ -40,6 +43,13 @@
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.13.0",
"@babel/runtime": "^7.17.9",
"@codemirror/commands": "^6.5.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.1",
"@codemirror/lint": "^6.7.1",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3",
"@lezer/common": "^1.2.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
Expand All @@ -54,6 +64,7 @@
"babel-loader": "^9.1.3",
"clean-webpack-plugin": "^4.0.0",
"clsx": "^1.1.1",
"codemirror-json-schema": "^0.7.2",
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
Expand All @@ -65,8 +76,6 @@
"i18next-hmr": "^3.0.3",
"i18next-http-backend": "^2.4.2",
"i18next-locales-sync": "^2.0.1",
"jsoneditor": "^10.0.3",
"jsoneditor-react": "^3.1.2",
"mini-css-extract-plugin": "^2.6.0",
"nanoid": "^4.0.1",
"postcss": "^8.4.12",
Expand Down
124 changes: 63 additions & 61 deletions packages/ui/src/components/AddJobModal/AddJobModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as Dialog from '@radix-ui/react-dialog';
import React, { useEffect, useState } from 'react';
import { AppQueue } from '@bull-board/api/typings/app';
import React, { FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useActiveQueue } from '../../hooks/useActiveQueue';
import { useQueues } from '../../hooks/useQueues';
import bullJobOptionsSchema from '../../schemas/bull/jobOptions.json';
import bullMQJobOptionsSchema from '../../schemas/bullmq/jobOptions.json';
import { Button } from '../Button/Button';
import { InputField } from '../Form/InputField/InputField';
import { JsonField } from '../Form/JsonField/JsonField';
import { SelectField } from '../Form/SelectField/InputField';
import { SelectField } from '../Form/SelectField/SelectField';
import { Modal } from '../Modal/Modal';

export interface AddJobModalProps {
Expand All @@ -14,26 +17,38 @@ export interface AddJobModalProps {
onClose(): void;
}

const jobOptionsSchema = {
bull: bullJobOptionsSchema,
bullmq: bullMQJobOptionsSchema,
} as const;

export const AddJobModal = ({ open, onClose }: AddJobModalProps) => {
const { actions, queues } = useQueues();
const [queueName, setQueueName] = useState('');
const [jobName, setJobName] = useState('');
const [jobData, setJobData] = useState<any>({});
const [jobDelay, setJobDelay] = useState('');
const [jobAttempts, setJobAttempts] = useState('');
const { queues, actions } = useQueues();
const activeQueue = useActiveQueue();
const [selectedQueue, setSelectedQueue] = useState<AppQueue | null>(activeQueue);
const { t } = useTranslation();

useEffect(() => {
if (queues && queues.length) {
setQueueName(queues[0].name);
}
}, [queues]);
if (!queues || !activeQueue || !selectedQueue) {
return null;
}

const addJob = async (evt: FormEvent) => {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const formData = Object.fromEntries(
Array.from(form.elements).map((input: any) => [input.name, input.value])
);

formData.jobData = JSON.parse(formData.jobData);
formData.jobOptions = JSON.parse(formData.jobOptions);

const addJob = () => {
actions.addJob(queueName, jobName || '__default__', jobData, {
delay: jobDelay ? +jobDelay : undefined,
attempts: jobAttempts ? +jobAttempts : undefined,
})();
await actions.addJob(
formData.queueName,
formData.jobName || '__default__',
formData.jobData,
formData.jobOptions
)();
onClose();
};

return (
Expand All @@ -43,50 +58,37 @@ export const AddJobModal = ({ open, onClose }: AddJobModalProps) => {
onClose={onClose}
title={t('ADD_JOB.TITLE')}
actionButton={
<Dialog.Close asChild>
<Button theme="primary" onClick={addJob}>
{t('ADD_JOB.ADD')}
</Button>
</Dialog.Close>
<Button type="submit" theme="primary" form="add-job-form">
{t('ADD_JOB.ADD')}
</Button>
}
>
<SelectField
label={t('ADD_JOB.QUEUE_NAME')}
id="queue-name"
options={(queues || []).map((queue) => ({
text: queue.name,
value: queue.name,
}))}
value={queueName}
onChange={(event) => setQueueName(event.target.value)}
/>
<InputField
label={t('ADD_JOB.JOB_NAME')}
id="job-name"
value={jobName}
placeholder="__default__"
onChange={(event) => setJobName(event.target.value)}
/>
<JsonField
label={t('ADD_JOB.JOB_DATA')}
id="job-data"
value={jobData}
onChange={(v) => setJobData(v)}
/>
<InputField
label={t('ADD_JOB.JOB_DELAY')}
id="job-delay"
type="number"
value={jobDelay}
onChange={(event) => setJobDelay(event.target.value)}
/>
<InputField
label={t('ADD_JOB.JOB_ATTEMPTS')}
id="job-attempts"
type="number"
value={jobAttempts}
onChange={(event) => setJobAttempts(event.target.value)}
/>
<form id="add-job-form" onSubmit={addJob}>
<SelectField
label={t('ADD_JOB.QUEUE_NAME')}
id="queue-name"
options={(queues || []).map((queue) => ({
text: queue.name,
value: queue.name,
}))}
name="queueName"
value={selectedQueue.name || ''}
onChange={(event) => setSelectedQueue(queues.find((q) => q.name === event.target.value)!)}
/>
<InputField
label={t('ADD_JOB.JOB_NAME')}
id="job-name"
name="jobName"
placeholder="__default__"
/>
<JsonField label={t('ADD_JOB.JOB_DATA')} id="job-data" name="jobData" />
<JsonField
label={t('ADD_JOB.JOB_OPTIONS')}
id="job-options"
name="jobOptions"
schema={jobOptionsSchema[selectedQueue.type]}
/>
</form>
</Modal>
);
};
14 changes: 6 additions & 8 deletions packages/ui/src/components/Form/JsonField/JsonField.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { JsonEditor as Editor } from 'jsoneditor-react';
import 'jsoneditor-react/es/editor.min.css';
import React, { HTMLProps } from 'react';
import { JsonEditor } from '../../JsonEditor/JsonEditor';
import { Field } from '../Field/Field';

interface JsonFieldProps extends HTMLProps<any> {
label?: string;
value?: any;
onChange?: (v: any) => void;
interface JsonFieldProps extends Omit<HTMLProps<HTMLInputElement>, 'value' | 'ref'> {
value?: Record<any, any>;
schema?: Record<string, any>;
}

export const JsonField = ({ label, id, ...props }: JsonFieldProps) => (
export const JsonField = ({ label, id, value, ...rest }: JsonFieldProps) => (
<Field label={label} id={id}>
<Editor mode="code" {...props} />
<JsonEditor doc={value || {}} id={id} {...rest} />
</Field>
);
Loading

0 comments on commit 95bc661

Please sign in to comment.