From 6c8f92aa52e90bab0505e8c29d8e5e42e38bd5e4 Mon Sep 17 00:00:00 2001 From: Felix Mosheev <9304194+felixmosh@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:51:20 +0300 Subject: [PATCH] feat: add duplicate job, closes #426 --- .../components/AddJobModal/AddJobModal.tsx | 14 ++++++----- packages/ui/src/components/Icons/Copy.tsx | 4 ++-- .../ui/src/components/Icons/Duplicate.tsx | 7 ++++++ .../JobCard/JobActions/JobActions.tsx | 23 +++++++++++++++---- .../ui/src/components/JobCard/JobCard.tsx | 1 + packages/ui/src/hooks/useJob.ts | 6 +---- packages/ui/src/pages/JobPage/JobPage.tsx | 16 ++++++++++++- packages/ui/src/pages/QueuePage/QueuePage.tsx | 10 +++++++- .../ui/src/static/locales/en-US/messages.json | 8 ++++--- .../ui/src/static/locales/fr-FR/messages.json | 6 ++++- .../ui/src/static/locales/pt-BR/messages.json | 10 +++++--- .../ui/src/static/locales/zh-CN/messages.json | 6 ++++- 12 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 packages/ui/src/components/Icons/Duplicate.tsx diff --git a/packages/ui/src/components/AddJobModal/AddJobModal.tsx b/packages/ui/src/components/AddJobModal/AddJobModal.tsx index 7ab207a0..5dae8786 100644 --- a/packages/ui/src/components/AddJobModal/AddJobModal.tsx +++ b/packages/ui/src/components/AddJobModal/AddJobModal.tsx @@ -1,4 +1,4 @@ -import { AppQueue } from '@bull-board/api/typings/app'; +import { AppJob, AppQueue } from '@bull-board/api/typings/app'; import React, { FormEvent, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useActiveQueue } from '../../hooks/useActiveQueue'; @@ -13,7 +13,7 @@ import { Modal } from '../Modal/Modal'; export interface AddJobModalProps { open: boolean; - + job?: AppJob | null; onClose(): void; } @@ -22,7 +22,7 @@ const jobOptionsSchema = { bullmq: bullMQJobOptionsSchema, } as const; -export const AddJobModal = ({ open, onClose }: AddJobModalProps) => { +export const AddJobModal = ({ open, onClose, job }: AddJobModalProps) => { const { queues, actions } = useQueues(); const activeQueue = useActiveQueue(); const [selectedQueue, setSelectedQueue] = useState(activeQueue); @@ -58,10 +58,10 @@ export const AddJobModal = ({ open, onClose }: AddJobModalProps) => { width="small" open={open} onClose={onClose} - title={t('ADD_JOB.TITLE')} + title={t('ADD_JOB.TITLE', { context: job ? 'duplicate' : undefined })} actionButton={ } > @@ -81,14 +81,16 @@ export const AddJobModal = ({ open, onClose }: AddJobModalProps) => { label={t('ADD_JOB.JOB_NAME')} id="job-name" name="jobName" + defaultValue={job?.name} placeholder="__default__" /> - + diff --git a/packages/ui/src/components/Icons/Copy.tsx b/packages/ui/src/components/Icons/Copy.tsx index 954de0a3..bf70a7d5 100644 --- a/packages/ui/src/components/Icons/Copy.tsx +++ b/packages/ui/src/components/Icons/Copy.tsx @@ -1,7 +1,7 @@ import React from 'react'; export const CopyIcon = () => ( - - + + ); diff --git a/packages/ui/src/components/Icons/Duplicate.tsx b/packages/ui/src/components/Icons/Duplicate.tsx new file mode 100644 index 00000000..171fc981 --- /dev/null +++ b/packages/ui/src/components/Icons/Duplicate.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const DuplicateIcon = () => ( + + + +); diff --git a/packages/ui/src/components/JobCard/JobActions/JobActions.tsx b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx index a8df43a2..0e7b12a2 100644 --- a/packages/ui/src/components/JobCard/JobActions/JobActions.tsx +++ b/packages/ui/src/components/JobCard/JobActions/JobActions.tsx @@ -3,6 +3,7 @@ import { Status } from '@bull-board/api/typings/app'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../Button/Button'; +import { DuplicateIcon } from '../../Icons/Duplicate'; import { PromoteIcon } from '../../Icons/Promote'; import { RetryIcon } from '../../Icons/Retry'; import { TrashIcon } from '../../Icons/Trash'; @@ -18,13 +19,14 @@ interface JobActionsProps { retryJob: () => Promise; cleanJob: () => Promise; updateJobData: () => void; + duplicateJob: () => void; }; } interface ButtonType { titleKey: string; Icon: React.ElementType; - actionKey: 'promoteJob' | 'cleanJob' | 'retryJob' | 'updateJobData'; + actionKey: 'promoteJob' | 'cleanJob' | 'retryJob' | 'updateJobData' | 'duplicateJob'; } const buttonTypes: Record = { @@ -32,13 +34,24 @@ const buttonTypes: Record = { promote: { titleKey: 'PROMOTE', Icon: PromoteIcon, actionKey: 'promoteJob' }, clean: { titleKey: 'CLEAN', Icon: TrashIcon, actionKey: 'cleanJob' }, retry: { titleKey: 'RETRY', Icon: RetryIcon, actionKey: 'retryJob' }, + duplicate: { titleKey: 'DUPLICATE', Icon: DuplicateIcon, actionKey: 'duplicateJob' }, } as const; const statusToButtonsMap: Record = { - [STATUSES.failed]: [buttonTypes.retry, buttonTypes.updateData, buttonTypes.clean], - [STATUSES.delayed]: [buttonTypes.promote, buttonTypes.updateData, buttonTypes.clean], - [STATUSES.completed]: [buttonTypes.retry, buttonTypes.clean], - [STATUSES.waiting]: [buttonTypes.updateData, buttonTypes.clean], + [STATUSES.failed]: [ + buttonTypes.retry, + buttonTypes.duplicate, + buttonTypes.updateData, + buttonTypes.clean, + ], + [STATUSES.delayed]: [ + buttonTypes.promote, + buttonTypes.duplicate, + buttonTypes.updateData, + buttonTypes.clean, + ], + [STATUSES.completed]: [buttonTypes.duplicate, buttonTypes.retry, buttonTypes.clean], + [STATUSES.waiting]: [buttonTypes.duplicate, buttonTypes.updateData, buttonTypes.clean], } as const; export const JobActions = ({ actions, status, allowRetries }: JobActionsProps) => { diff --git a/packages/ui/src/components/JobCard/JobCard.tsx b/packages/ui/src/components/JobCard/JobCard.tsx index 038f9d47..d2c0c8e7 100644 --- a/packages/ui/src/components/JobCard/JobCard.tsx +++ b/packages/ui/src/components/JobCard/JobCard.tsx @@ -23,6 +23,7 @@ interface JobCardProps { allowRetries: boolean; actions: { updateJobData: () => void; + duplicateJob: () => void; promoteJob: () => Promise; retryJob: () => Promise; cleanJob: () => Promise; diff --git a/packages/ui/src/hooks/useJob.ts b/packages/ui/src/hooks/useJob.ts index be48e7f8..f2617691 100644 --- a/packages/ui/src/hooks/useJob.ts +++ b/packages/ui/src/hooks/useJob.ts @@ -75,11 +75,7 @@ export function useJob(): Omit & { actions: JobActions } ); const updateJobData = (queueName: string, job: AppJob, newData: Record) => - withConfirmAndUpdate( - () => api.updateJobData(queueName, job.id, newData), - t('JOB.ACTIONS.CONFIRM.UPDATE_JOB_DATA'), - false - ); + withConfirmAndUpdate(() => api.updateJobData(queueName, job.id, newData), '', false); const getJobLogs = (queueName: string) => (job: AppJob) => () => api.getJobLogs(queueName, job.id); diff --git a/packages/ui/src/pages/JobPage/JobPage.tsx b/packages/ui/src/pages/JobPage/JobPage.tsx index fdf3bfb6..fada608b 100644 --- a/packages/ui/src/pages/JobPage/JobPage.tsx +++ b/packages/ui/src/pages/JobPage/JobPage.tsx @@ -13,6 +13,12 @@ import { useSelectedStatuses } from '../../hooks/useSelectedStatuses'; import { links } from '../../utils/links'; import buttonS from '../../components/Button/Button.module.css'; +const AddJobModalLazy = React.lazy(() => + import('../../components/AddJobModal/AddJobModal').then(({ AddJobModal }) => ({ + default: AddJobModal, + })) +); + const UpdateJobDataModalLazy = React.lazy(() => import('../../components/UpdateJobDataModal/UpdateJobDataModal').then( ({ UpdateJobDataModal }) => ({ @@ -28,7 +34,7 @@ export const JobPage = () => { const queue = useActiveQueue(); const { job, status, actions } = useJob(); const selectedStatuses = useSelectedStatuses(); - const modal = useModal<'updateJobData'>(); + const modal = useModal<'updateJobData' | 'addJob'>(); actions.pollJob(); @@ -70,11 +76,19 @@ export const JobPage = () => { retryJob: actions.retryJob(queue.name, status as JobRetryStatus)(job), getJobLogs: actions.getJobLogs(queue.name)(job), updateJobData: () => modal.open('updateJobData'), + duplicateJob: () => modal.open('addJob'), }} readOnlyMode={queue.readOnlyMode} allowRetries={(job.isFailed || queue.allowCompletedRetries) && queue.allowRetries} /> + {modal.isMounted('addJob') && ( + + )} {modal.isMounted('updateJobData') && ( { setEditJob(job); modal.open('updateJobData'); }, + duplicateJob: () => { + setEditJob(job); + modal.open('addJob'); + }, }} readOnlyMode={queue?.readOnlyMode} allowRetries={(job.isFailed || queue.allowCompletedRetries) && queue.allowRetries} @@ -100,7 +104,11 @@ export const QueuePage = () => { ))} {modal.isMounted('addJob') && ( - + )} {modal.isMounted('updateJobData') && !!editJob && (