Skip to content

Commit

Permalink
Merge branch 'refs/heads/pr-744'
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/api/src/queueAdapters/base.ts
#	packages/ui/src/components/AddJobModal/AddJobModal.tsx
#	packages/ui/src/components/Form/JsonField/JsonField.tsx
#	packages/ui/src/components/Icons/Add.tsx
#	packages/ui/src/components/QueueDropdownActions/QueueDropdownActions.tsx
#	packages/ui/src/hooks/useQueues.ts
#	packages/ui/src/services/Api.ts
#	packages/ui/src/static/locales/en-US/messages.json
  • Loading branch information
felixmosh committed May 22, 2024
2 parents 5187262 + 95bc661 commit 403d10c
Show file tree
Hide file tree
Showing 26 changed files with 1,184 additions and 209 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
13 changes: 12 additions & 1 deletion 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 Down
119 changes: 66 additions & 53 deletions packages/ui/src/components/AddJobModal/AddJobModal.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import { AppQueue } from '@bull-board/api/dist/typings/app';
import * as Dialog from '@radix-ui/react-dialog';
import React, { useState } from 'react';
import { AppQueue } from '@bull-board/api/typings/app';
import React, { FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { QueueActions } from '../../../typings/app';
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/SelectField';
import { Modal } from '../Modal/Modal';

export interface AddJobModalProps {
open: boolean;

onClose(): void;

actions: QueueActions;
queue: AppQueue;
}

export const AddJobModal = ({ open, onClose, actions, queue }: AddJobModalProps) => {
const [isValid, setValid] = useState(true);
const [jobName, setJobName] = useState('');
const [jobData, setJobData] = useState<any>({});
const [jobDelay, setJobDelay] = useState('');
const [jobAttempts, setJobAttempts] = useState('');
const jobOptionsSchema = {
bull: bullJobOptionsSchema,
bullmq: bullMQJobOptionsSchema,
} as const;

export const AddJobModal = ({ open, onClose }: AddJobModalProps) => {
const { queues, actions } = useQueues();
const activeQueue = useActiveQueue();
const [selectedQueue, setSelectedQueue] = useState<AppQueue | null>(activeQueue);
const { t } = useTranslation();

const addJob = () => {
actions.addJob(queue.name, jobName || '__default__', jobData, {
delay: jobDelay ? +jobDelay : undefined,
attempts: jobAttempts ? +jobAttempts : undefined,
})();
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);

await actions.addJob(
formData.queueName,
formData.jobName || '__default__',
formData.jobData,
formData.jobOptions
)();
onClose();
};

return (
Expand All @@ -39,43 +58,37 @@ export const AddJobModal = ({ open, onClose, actions, queue }: AddJobModalProps)
onClose={onClose}
title={t('ADD_JOB.TITLE')}
actionButton={
<Dialog.Close asChild>
<Button theme="primary" onClick={addJob} disabled={!isValid}>
{t('ADD_JOB.ADD')}
</Button>
</Dialog.Close>
<Button type="submit" theme="primary" form="add-job-form">
{t('ADD_JOB.ADD')}
</Button>
}
>
<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)}
onValidationError={(errors) => setValid(!errors.length)}
/>
<InputField
label={t('ADD_JOB.JOB_DELAY')}
id="job-delay"
type="number"
value={jobDelay}
min={0}
onChange={(event) => setJobDelay(event.target.value)}
/>
<InputField
label={t('ADD_JOB.JOB_ATTEMPTS')}
id="job-attempts"
type="number"
value={jobAttempts}
min={1}
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>
);
};
15 changes: 6 additions & 9 deletions packages/ui/src/components/Form/JsonField/JsonField.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +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;
onValidationError?: (errors: Error[]) => 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>
);
36 changes: 10 additions & 26 deletions packages/ui/src/components/HeaderActions/HeaderActions.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import React, { useState, Suspense } from 'react';
import React, { Suspense } from 'react';
import { useModal } from '../../hooks/useModal';
import { useUIConfig } from '../../hooks/useUIConfig';
import { Button } from '../Button/Button';
import { CustomLinksDropdown } from '../CustomLinksDropdown/CustomLinksDropdown';
import { FullscreenIcon } from '../Icons/Fullscreen';
import { RedisIcon } from '../Icons/Redis';
import { Settings } from '../Icons/Settings';
import { Button } from '../Button/Button';
import s from './HeaderActions.module.css';

type ModalTypes = 'redis' | 'settings';
type AllModalTypes = ModalTypes | `${ModalTypes}Closing` | null;

function waitForClosingAnimation(
state: ModalTypes,
setModalOpen: (newState: AllModalTypes) => void
) {
return () => {
setModalOpen(`${state}Closing`);
setTimeout(() => setModalOpen(null), 300); // fadeout animation duration
};
}

const RedisStatsModalLazy = React.lazy(() =>
import('../RedisStatsModal/RedisStatsModal').then(({ RedisStatsModal }) => ({
Expand All @@ -39,14 +29,14 @@ const onClickFullScreen = async () => {
};

export const HeaderActions = () => {
const [openedModal, setModalOpen] = useState<AllModalTypes>(null);
const { miscLinks = [] } = useUIConfig();
const modal = useModal<ModalTypes>();

return (
<>
<ul className={s.actions}>
<li>
<Button onClick={() => setModalOpen('redis')} className={s.button}>
<Button onClick={() => modal.open('redis')} className={s.button}>
<RedisIcon />
</Button>
</li>
Expand All @@ -56,7 +46,7 @@ export const HeaderActions = () => {
</Button>
</li>
<li>
<Button onClick={() => setModalOpen('settings')} className={s.button}>
<Button onClick={() => modal.open('settings')} className={s.button}>
<Settings />
</Button>
</li>
Expand All @@ -67,17 +57,11 @@ export const HeaderActions = () => {
)}
</ul>
<Suspense fallback={null}>
{(openedModal === 'redis' || openedModal === 'redisClosing') && (
<RedisStatsModalLazy
open={openedModal === 'redis'}
onClose={waitForClosingAnimation('redis', setModalOpen)}
/>
{modal.isMounted('redis') && (
<RedisStatsModalLazy open={modal.isOpen('redis')} onClose={modal.close('redis')} />
)}
{(openedModal === 'settings' || openedModal === 'settingsClosing') && (
<SettingsModalLazy
open={openedModal === 'settings'}
onClose={waitForClosingAnimation('settings', setModalOpen)}
/>
{modal.isMounted('settings') && (
<SettingsModalLazy open={modal.isOpen('settings')} onClose={modal.close('settings')} />
)}
</Suspense>
</>
Expand Down
25 changes: 2 additions & 23 deletions packages/ui/src/components/Icons/Add.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
import React from 'react';

export const AddIcon = () => (
<svg
role="img"
width="256px"
height="256px"
viewBox="0 0 24.00 24.00"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="#748094"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M12 6C12.5523 6 13 6.44772 13 7V11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H13V17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17V13H7C6.44772 13 6 12.5523 6 12C6 11.4477 6.44772 11 7 11H11V7C11 6.44772 11.4477 6 12 6Z"
fill="#748094"
></path>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 4.5C2 3.11929 3.11929 2 4.5 2H19.5C20.8807 2 22 3.11929 22 4.5V19.5C22 20.8807 20.8807 22 19.5 22H4.5C3.11929 22 2 20.8807 2 19.5V4.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5V19.5C4 19.7761 4.22386 20 4.5 20H19.5C19.7761 20 20 19.7761 20 19.5V4.5C20 4.22386 19.7761 4 19.5 4H4.5Z"
fill="#748094"
></path>
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M64 80c-8.8 0-16 7.2-16 16v320c0 8.8 7.2 16 16 16h320c8.8 0 16-7.2 16-16V96c0-8.8-7.2-16-16-16H64zM0 96c0-35.3 28.7-64 64-64h320c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm200 248v-64h-64c-13.3 0-24-10.7-24-24s10.7-24 24-24h64v-64c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24h-64v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z" />
</svg>
);
Loading

0 comments on commit 403d10c

Please sign in to comment.