From 29c797d62313cf561b0083a79268f2ba7cea8102 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 15 Jan 2025 16:58:38 +0100 Subject: [PATCH 1/3] Add workflow actions impl --- .../__fixtures__/workflow-actions-config.ts | 10 +- .../__fixtures__/workflow-details-params.ts | 6 + .../__tests__/workflow-actions.test.tsx | 8 +- .../config/workflow-actions.config.ts | 19 ++- .../workflow-actions-menu.types.ts | 2 +- .../workflow-actions-modal-content.test.tsx | 114 +++++++++++++++++ .../workflow-actions-modal-content.styles.ts | 27 ++++ .../workflow-actions-modal-content.tsx | 121 ++++++++++++++++++ .../workflow-actions-modal-content.types.ts | 10 ++ .../__tests__/workflow-actions-modal.test.tsx | 30 ++--- .../workflow-actions-modal.styles.ts | 22 +--- .../workflow-actions-modal.tsx | 49 ++----- .../workflow-actions-modal.types.ts | 10 ++ .../workflow-actions.styles.ts | 8 ++ .../workflow-actions/workflow-actions.tsx | 28 ++-- .../workflow-actions.types.ts | 20 ++- 16 files changed, 387 insertions(+), 97 deletions(-) create mode 100644 src/views/workflow-actions/__fixtures__/workflow-details-params.ts create mode 100644 src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx create mode 100644 src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.styles.ts create mode 100644 src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx create mode 100644 src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.types.ts create mode 100644 src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.types.ts diff --git a/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts b/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts index b9455760e..46087c5f4 100644 --- a/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts +++ b/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts @@ -9,6 +9,10 @@ export const mockWorkflowActionsConfig = [ subtitle: 'Mock cancel a workflow execution', icon: MdHighlightOff, getIsEnabled: () => true, + apiRoute: 'cancel', + onSuccess: (params) => { + params.sendNotification('Mock notification'); + }, }, { id: 'terminate', @@ -16,5 +20,9 @@ export const mockWorkflowActionsConfig = [ subtitle: 'Mock terminate a workflow execution', icon: MdPowerSettingsNew, getIsEnabled: () => false, + apiRoute: 'terminate', + onSuccess: (params) => { + params.sendNotification('Mock notification'); + }, }, -] as const satisfies Array; +] as const satisfies Array>>; diff --git a/src/views/workflow-actions/__fixtures__/workflow-details-params.ts b/src/views/workflow-actions/__fixtures__/workflow-details-params.ts new file mode 100644 index 000000000..12e1b8f93 --- /dev/null +++ b/src/views/workflow-actions/__fixtures__/workflow-details-params.ts @@ -0,0 +1,6 @@ +export const mockWorkflowDetailsParams = { + cluster: 'testCluster', + domain: 'testDomain', + workflowId: 'testWorkflowId', + runId: 'testRunId', +}; diff --git a/src/views/workflow-actions/__tests__/workflow-actions.test.tsx b/src/views/workflow-actions/__tests__/workflow-actions.test.tsx index fcddae2d4..6405b4055 100644 --- a/src/views/workflow-actions/__tests__/workflow-actions.test.tsx +++ b/src/views/workflow-actions/__tests__/workflow-actions.test.tsx @@ -7,16 +7,12 @@ import { act, render, screen, userEvent } from '@/test-utils/rtl'; import { describeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response'; import { mockWorkflowActionsConfig } from '../__fixtures__/workflow-actions-config'; +import { mockWorkflowDetailsParams } from '../__fixtures__/workflow-details-params'; import WorkflowActions from '../workflow-actions'; jest.mock('next/navigation', () => ({ ...jest.requireActual('next/navigation'), - useParams: () => ({ - cluster: 'testCluster', - domain: 'testDomain', - workflowId: 'testWorkflowId', - runId: 'testRunId', - }), + useParams: () => mockWorkflowDetailsParams, })); jest.mock('../workflow-actions-modal/workflow-actions-modal', () => diff --git a/src/views/workflow-actions/config/workflow-actions.config.ts b/src/views/workflow-actions/config/workflow-actions.config.ts index 41f954f7b..c62f836c8 100644 --- a/src/views/workflow-actions/config/workflow-actions.config.ts +++ b/src/views/workflow-actions/config/workflow-actions.config.ts @@ -1,10 +1,15 @@ import { MdHighlightOff, MdPowerSettingsNew } from 'react-icons/md'; -import getWorkflowIsCompleted from '@/views/workflow-page/helpers/get-workflow-is-completed'; +import { type CancelWorkflowResponse } from '@/route-handlers/cancel-workflow/cancel-workflow.types'; +import { type TerminateWorkflowResponse } from '@/route-handlers/terminate-workflow/terminate-workflow.types'; +import getWorkflowIsCompleted from '../../workflow-page/helpers/get-workflow-is-completed'; import { type WorkflowAction } from '../workflow-actions.types'; -const workflowActionsConfig = [ +const workflowActionsConfig: [ + WorkflowAction, + WorkflowAction, +] = [ { id: 'cancel', label: 'Cancel', @@ -14,6 +19,10 @@ const workflowActionsConfig = [ !getWorkflowIsCompleted( workflow.workflowExecutionInfo?.closeEvent?.attributes ?? '' ), + apiRoute: 'cancel', + onSuccess: ({ sendNotification }) => { + sendNotification('Workflow cancellation has been requested.'); + }, }, { id: 'terminate', @@ -24,7 +33,11 @@ const workflowActionsConfig = [ !getWorkflowIsCompleted( workflow.workflowExecutionInfo?.closeEvent?.attributes ?? '' ), + apiRoute: 'terminate', + onSuccess: ({ sendNotification }) => { + sendNotification('Workflow has been terminated.'); + }, }, -] as const satisfies Array; +] as const; export default workflowActionsConfig; diff --git a/src/views/workflow-actions/workflow-actions-menu/workflow-actions-menu.types.ts b/src/views/workflow-actions/workflow-actions-menu/workflow-actions-menu.types.ts index f5f571f25..739f7e4ca 100644 --- a/src/views/workflow-actions/workflow-actions-menu/workflow-actions-menu.types.ts +++ b/src/views/workflow-actions/workflow-actions-menu/workflow-actions-menu.types.ts @@ -4,5 +4,5 @@ import { type WorkflowAction } from '../workflow-actions.types'; export type Props = { workflow: DescribeWorkflowResponse; - onActionSelect: (action: WorkflowAction) => void; + onActionSelect: (action: WorkflowAction) => void; }; diff --git a/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx new file mode 100644 index 000000000..94999e68e --- /dev/null +++ b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx @@ -0,0 +1,114 @@ +import { HttpResponse } from 'msw'; + +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import { type CancelWorkflowResponse } from '@/route-handlers/cancel-workflow/cancel-workflow.types'; + +import { mockWorkflowActionsConfig } from '../../__fixtures__/workflow-actions-config'; +import { mockWorkflowDetailsParams } from '../../__fixtures__/workflow-details-params'; +import WorkflowActionsModalContent from '../workflow-actions-modal-content'; + +const mockEnqueue = jest.fn(); +const mockDequeue = jest.fn(); +jest.mock('baseui/snackbar', () => ({ + ...jest.requireActual('baseui/snackbar'), + useSnackbar: () => ({ + enqueue: mockEnqueue, + dequeue: mockDequeue, + }), +})); + +describe(WorkflowActionsModalContent.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the modal content as expected', async () => { + setup({}); + + expect(await screen.findAllByText('Mock cancel workflow')).toHaveLength(2); + expect( + screen.getByText('Mock cancel a workflow execution') + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Mock cancel workflow' }) + ).toBeInTheDocument(); + }); + + it('calls onCloseModal when the Go Back button is clicked', async () => { + const { user, mockOnClose } = setup({}); + + const goBackButton = await screen.findByText('Go back'); + await user.click(goBackButton); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('calls mockCancelWorkflow, sends toast, and closes modal when the action button is clicked', async () => { + const { user, mockOnClose, mockCancelWorkflow } = setup({}); + + const cancelButton = await screen.findByRole('button', { + name: 'Mock cancel workflow', + }); + await user.click(cancelButton); + + expect(mockCancelWorkflow).toHaveBeenCalled(); + expect(mockEnqueue).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Mock notification', + }), + undefined + ); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('Displays banner when the action button is clicked and action fails', async () => { + const { user, mockOnClose, mockCancelWorkflow } = setup({ error: true }); + + const cancelButton = await screen.findByRole('button', { + name: 'Mock cancel workflow', + }); + await user.click(cancelButton); + + expect(mockCancelWorkflow).toHaveBeenCalled(); + expect( + await screen.findByText('Failed to cancel workflow') + ).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); +}); + +function setup({ error }: { error?: boolean }) { + const user = userEvent.setup(); + const mockOnClose = jest.fn(); + const mockCancelWorkflow = jest.fn(); + + render( + , + { + endpointsMocks: [ + { + path: '/api/domains/:domain/:cluster/workflows/:workflowId/:runId/cancel', + httpMethod: 'POST', + mockOnce: false, + httpResolver: () => { + mockCancelWorkflow(); + if (error) { + return HttpResponse.json( + { message: 'Failed to cancel workflow' }, + { status: 500 } + ); + } + return HttpResponse.json({} satisfies CancelWorkflowResponse); + }, + }, + ], + } + ); + + return { user, mockOnClose, mockCancelWorkflow }; +} diff --git a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.styles.ts b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.styles.ts new file mode 100644 index 000000000..0d0d7f62e --- /dev/null +++ b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.styles.ts @@ -0,0 +1,27 @@ +import { type Theme, withStyle } from 'baseui'; +import { type BannerOverrides } from 'baseui/banner'; +import { ModalBody, ModalFooter, ModalHeader } from 'baseui/modal'; + +export const styled = { + ModalHeader: withStyle(ModalHeader, ({ $theme }: { $theme: Theme }) => ({ + marginTop: $theme.sizing.scale850, + })), + ModalBody: withStyle(ModalBody, ({ $theme }: { $theme: Theme }) => ({ + marginBottom: $theme.sizing.scale800, + })), + ModalFooter: withStyle(ModalFooter, { + display: 'flex', + justifyContent: 'space-between', + }), +}; + +export const overrides = { + banner: { + Root: { + style: { + marginLeft: 0, + marginRight: 0, + }, + }, + } satisfies BannerOverrides, +}; diff --git a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx new file mode 100644 index 000000000..efd7a672b --- /dev/null +++ b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx @@ -0,0 +1,121 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Banner, HIERARCHY, KIND as BANNER_KIND } from 'baseui/banner'; +import { KIND as BUTTON_KIND, SIZE } from 'baseui/button'; +import { ModalButton } from 'baseui/modal'; +import { useSnackbar } from 'baseui/snackbar'; +import { MdCheckCircle, MdErrorOutline } from 'react-icons/md'; + +import logger from '@/utils/logger'; +import request from '@/utils/request'; +import { type RequestError } from '@/utils/request/request-error'; + +import { type WorkflowActionInputParams } from '../workflow-actions.types'; + +import { overrides, styled } from './workflow-actions-modal-content.styles'; +import { type Props } from './workflow-actions-modal-content.types'; + +export default function WorkflowActionsModalContent({ + action, + params, + onCloseModal, +}: Props) { + const queryClient = useQueryClient(); + const { enqueue, dequeue } = useSnackbar(); + const { mutateAsync, isPending, error } = useMutation< + R, + RequestError, + WorkflowActionInputParams + >( + { + mutationFn: ({ + domain, + cluster, + workflowId, + runId, + }: WorkflowActionInputParams) => + request( + `/api/domains/${domain}/${cluster}/workflows/${workflowId}/${runId}/${action.apiRoute}`, + { + method: 'POST', + body: JSON.stringify({ + // TODO: pass the input here when implementing extended workflow actions + }), + } + ).then((res) => res.json() as R), + }, + queryClient + ); + + return ( + <> + {action.label} workflow + + {action.subtitle} + {error && ( + + {error.message} + + )} + + + + Go back + + { + mutateAsync(params).then( + (result) => { + const { + // TODO: input, + ...workflowDetailsParams + } = params; + + queryClient.invalidateQueries({ + queryKey: ['describe_workflow', workflowDetailsParams], + }); + + onCloseModal(); + + action.onSuccess({ + result, + inputParams: params, + sendNotification: (message, duration) => + enqueue( + { + message, + startEnhancer: MdCheckCircle, + actionMessage: 'OK', + actionOnClick: () => dequeue(), + }, + duration + ), + }); + }, + (e) => + logger.error( + { error: e, params }, + 'Failed to perform workflow action' + ) + ); + }} + isLoading={isPending} + > + {action.label} workflow + + + + ); +} diff --git a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.types.ts b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.types.ts new file mode 100644 index 000000000..445a3e56d --- /dev/null +++ b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.types.ts @@ -0,0 +1,10 @@ +import { + type WorkflowAction, + type WorkflowActionInputParams, +} from '../workflow-actions.types'; + +export type Props = { + action: WorkflowAction; + params: WorkflowActionInputParams; + onCloseModal: () => void; +}; diff --git a/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx b/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx index fb32521ed..9ef085246 100644 --- a/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx +++ b/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx @@ -1,10 +1,9 @@ import { type ModalProps } from 'baseui/modal'; -import { render, screen, userEvent } from '@/test-utils/rtl'; - -import { describeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response'; +import { render, screen } from '@/test-utils/rtl'; import { mockWorkflowActionsConfig } from '../../__fixtures__/workflow-actions-config'; +import { mockWorkflowDetailsParams } from '../../__fixtures__/workflow-details-params'; import WorkflowActionsModal from '../workflow-actions-modal'; jest.mock('baseui/modal', () => ({ @@ -17,6 +16,11 @@ jest.mock('baseui/modal', () => ({ ) : null, })); +jest.mock( + '../../workflow-actions-modal-content/workflow-actions-modal-content', + () => jest.fn(() =>
Actions Modal Content
) +); + describe(WorkflowActionsModal.name, () => { beforeEach(() => { jest.clearAllMocks(); @@ -25,12 +29,8 @@ describe(WorkflowActionsModal.name, () => { it('renders the action as expected', async () => { setup({}); - expect(await screen.findAllByText('Mock cancel workflow')).toHaveLength(2); expect( - screen.getByText('Mock cancel a workflow execution') - ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Mock cancel workflow' }) + await screen.findByText('Actions Modal Content') ).toBeInTheDocument(); }); @@ -39,28 +39,16 @@ describe(WorkflowActionsModal.name, () => { expect(screen.queryByRole('dialog')).toBeNull(); }); - - it('calls onClose when the Go Back button is pressed', async () => { - const { user, mockOnClose } = setup({}); - - const goBackButton = await screen.findByText('Go back'); - await user.click(goBackButton); - - expect(mockOnClose).toHaveBeenCalled(); - }); }); function setup({ omitAction }: { omitAction?: boolean }) { - const user = userEvent.setup(); const mockOnClose = jest.fn(); render( ); - - return { user, mockOnClose }; } diff --git a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts index 85f5ec7e8..19e74a9a3 100644 --- a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts +++ b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts @@ -1,25 +1,7 @@ -import { type Theme, withStyle } from 'baseui'; -import { - ModalBody, - ModalFooter, - ModalHeader, - type ModalOverrides, -} from 'baseui/modal'; +import { type Theme } from 'baseui'; +import { type ModalOverrides } from 'baseui/modal'; import { type StyleObject } from 'styletron-react'; -export const styled = { - ModalHeader: withStyle(ModalHeader, ({ $theme }: { $theme: Theme }) => ({ - marginTop: $theme.sizing.scale850, - })), - ModalBody: withStyle(ModalBody, ({ $theme }: { $theme: Theme }) => ({ - marginBottom: $theme.sizing.scale800, - })), - ModalFooter: withStyle(ModalFooter, { - display: 'flex', - justifyContent: 'space-between', - }), -}; - export const overrides = { modal: { Close: { diff --git a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.tsx b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.tsx index 36458833b..018b81389 100644 --- a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.tsx +++ b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.tsx @@ -1,21 +1,15 @@ -import { KIND, SIZE } from 'baseui/button'; -import { Modal, ModalButton } from 'baseui/modal'; +import { Modal } from 'baseui/modal'; -import { type DescribeWorkflowResponse } from '@/route-handlers/describe-workflow/describe-workflow.types'; +import WorkflowActionsModalContent from '../workflow-actions-modal-content/workflow-actions-modal-content'; -import { type WorkflowAction } from '../workflow-actions.types'; +import { overrides } from './workflow-actions-modal.styles'; +import { type Props } from './workflow-actions-modal.types'; -import { overrides, styled } from './workflow-actions-modal.styles'; - -export default function WorkflowActionsModal({ - // workflow, +export default function WorkflowActionsModal({ action, onClose, -}: { - workflow: DescribeWorkflowResponse; - action: WorkflowAction | undefined; - onClose: () => void; -}) { + ...workflowDetailsParams +}: Props) { return ( {action && ( - <> - {action.label} workflow - {action.subtitle} - - - Go back - - { - // perform the action here - }} - > - {action.label} workflow - - - + )} ); diff --git a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.types.ts b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.types.ts new file mode 100644 index 000000000..9fc95c10a --- /dev/null +++ b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.types.ts @@ -0,0 +1,10 @@ +import { type WorkflowAction } from '../workflow-actions.types'; + +export type Props = { + domain: string; + cluster: string; + workflowId: string; + runId: string; + action: WorkflowAction | undefined; + onClose: () => void; +}; diff --git a/src/views/workflow-actions/workflow-actions.styles.ts b/src/views/workflow-actions/workflow-actions.styles.ts index 6670c2327..f0dd108d8 100644 --- a/src/views/workflow-actions/workflow-actions.styles.ts +++ b/src/views/workflow-actions/workflow-actions.styles.ts @@ -1,5 +1,6 @@ import { type Theme } from 'baseui'; import { type PopoverOverrides } from 'baseui/popover'; +import { type SnackbarElementOverrides } from 'baseui/snackbar'; import { type StyleObject } from 'styletron-react'; export const overrides = { @@ -10,4 +11,11 @@ export const overrides = { }), }, } satisfies PopoverOverrides, + snackbar: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + backgroundColor: $theme.colors.contentPositive, + }), + }, + } satisfies SnackbarElementOverrides, }; diff --git a/src/views/workflow-actions/workflow-actions.tsx b/src/views/workflow-actions/workflow-actions.tsx index 1d5a060a3..679c784b4 100644 --- a/src/views/workflow-actions/workflow-actions.tsx +++ b/src/views/workflow-actions/workflow-actions.tsx @@ -2,13 +2,21 @@ import React, { useState } from 'react'; import { Button, KIND, SIZE } from 'baseui/button'; -import { PLACEMENT, StatefulPopover } from 'baseui/popover'; +import { + StatefulPopover, + PLACEMENT as POPOVER_PLACEMENT, +} from 'baseui/popover'; +import { + SnackbarProvider, + PLACEMENT as SNACKBAR_PLACEMENT, + DURATION, +} from 'baseui/snackbar'; import { pick } from 'lodash'; import { useParams } from 'next/navigation'; import { MdArrowDropDown } from 'react-icons/md'; -import useDescribeWorkflow from '../workflow-page/hooks/use-describe-workflow'; -import { type WorkflowPageParams } from '../workflow-page/workflow-page.types'; +import useDescribeWorkflow from '@/views/workflow-page/hooks/use-describe-workflow'; +import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; import WorkflowActionsMenu from './workflow-actions-menu/workflow-actions-menu'; import WorkflowActionsModal from './workflow-actions-modal/workflow-actions-modal'; @@ -30,13 +38,17 @@ export default function WorkflowActions() { }); const [selectedAction, setSelectedAction] = useState< - WorkflowAction | undefined + WorkflowAction | undefined >(undefined); return ( - <> + ( setSelectedAction(undefined)} /> - + ); } diff --git a/src/views/workflow-actions/workflow-actions.types.ts b/src/views/workflow-actions/workflow-actions.types.ts index 8199df641..b4ff35139 100644 --- a/src/views/workflow-actions/workflow-actions.types.ts +++ b/src/views/workflow-actions/workflow-actions.types.ts @@ -1,8 +1,23 @@ import { type IconProps } from 'baseui/icon'; +import { type Duration } from 'baseui/snackbar'; import { type DescribeWorkflowResponse } from '@/route-handlers/describe-workflow/describe-workflow.types'; -export type WorkflowAction = { +export type WorkflowActionInputParams = { + domain: string; + cluster: string; + workflowId: string; + runId: string; + // TODO: add input here for extended workflow actions +}; + +export type WorkflowActionOnSuccessParams = { + result: R; + inputParams: WorkflowActionInputParams; + sendNotification: (message: React.ReactNode, duration?: Duration) => void; +}; + +export type WorkflowAction = { id: string; label: string; subtitle: string; @@ -11,5 +26,6 @@ export type WorkflowAction = { color?: IconProps['color']; }>; getIsEnabled: (workflow: DescribeWorkflowResponse) => boolean; - // Add a field for the endpoint to call + apiRoute: string; + onSuccess: (params: WorkflowActionOnSuccessParams) => void | Promise; }; From 269a5d37f8eece69693a5f06bafea64ade01c6d4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 17 Jan 2025 12:56:21 +0100 Subject: [PATCH 2/3] Resolve comments --- .../__fixtures__/workflow-actions-config.ts | 18 +++--- .../__tests__/workflow-actions.test.tsx | 2 +- .../config/workflow-actions.config.ts | 8 +-- .../workflow-actions-modal-content.test.tsx | 7 +-- .../workflow-actions-modal-content.tsx | 58 +++++++------------ .../__tests__/workflow-actions-modal.test.tsx | 2 +- .../workflow-actions.types.ts | 12 ++-- .../__fixtures__/workflow-details-params.ts | 0 8 files changed, 41 insertions(+), 66 deletions(-) rename src/views/{workflow-actions => workflow-page}/__fixtures__/workflow-details-params.ts (100%) diff --git a/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts b/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts index 46087c5f4..64513e4d7 100644 --- a/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts +++ b/src/views/workflow-actions/__fixtures__/workflow-actions-config.ts @@ -1,8 +1,14 @@ import { MdHighlightOff, MdPowerSettingsNew } from 'react-icons/md'; +import { type CancelWorkflowResponse } from '@/route-handlers/cancel-workflow/cancel-workflow.types'; +import { type TerminateWorkflowResponse } from '@/route-handlers/terminate-workflow/terminate-workflow.types'; + import { type WorkflowAction } from '../workflow-actions.types'; -export const mockWorkflowActionsConfig = [ +export const mockWorkflowActionsConfig: [ + WorkflowAction, + WorkflowAction, +] = [ { id: 'cancel', label: 'Mock cancel', @@ -10,9 +16,7 @@ export const mockWorkflowActionsConfig = [ icon: MdHighlightOff, getIsEnabled: () => true, apiRoute: 'cancel', - onSuccess: (params) => { - params.sendNotification('Mock notification'); - }, + getSuccessMessage: () => 'Mock cancel notification', }, { id: 'terminate', @@ -21,8 +25,6 @@ export const mockWorkflowActionsConfig = [ icon: MdPowerSettingsNew, getIsEnabled: () => false, apiRoute: 'terminate', - onSuccess: (params) => { - params.sendNotification('Mock notification'); - }, + getSuccessMessage: () => 'Mock terminate notification', }, -] as const satisfies Array>>; +] as const; diff --git a/src/views/workflow-actions/__tests__/workflow-actions.test.tsx b/src/views/workflow-actions/__tests__/workflow-actions.test.tsx index 6405b4055..2d85408f0 100644 --- a/src/views/workflow-actions/__tests__/workflow-actions.test.tsx +++ b/src/views/workflow-actions/__tests__/workflow-actions.test.tsx @@ -6,8 +6,8 @@ import { act, render, screen, userEvent } from '@/test-utils/rtl'; import { describeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response'; +import { mockWorkflowDetailsParams } from '../../workflow-page/__fixtures__/workflow-details-params'; import { mockWorkflowActionsConfig } from '../__fixtures__/workflow-actions-config'; -import { mockWorkflowDetailsParams } from '../__fixtures__/workflow-details-params'; import WorkflowActions from '../workflow-actions'; jest.mock('next/navigation', () => ({ diff --git a/src/views/workflow-actions/config/workflow-actions.config.ts b/src/views/workflow-actions/config/workflow-actions.config.ts index c62f836c8..7d14ab734 100644 --- a/src/views/workflow-actions/config/workflow-actions.config.ts +++ b/src/views/workflow-actions/config/workflow-actions.config.ts @@ -20,9 +20,7 @@ const workflowActionsConfig: [ workflow.workflowExecutionInfo?.closeEvent?.attributes ?? '' ), apiRoute: 'cancel', - onSuccess: ({ sendNotification }) => { - sendNotification('Workflow cancellation has been requested.'); - }, + getSuccessMessage: () => 'Workflow cancellation has been requested.', }, { id: 'terminate', @@ -34,9 +32,7 @@ const workflowActionsConfig: [ workflow.workflowExecutionInfo?.closeEvent?.attributes ?? '' ), apiRoute: 'terminate', - onSuccess: ({ sendNotification }) => { - sendNotification('Workflow has been terminated.'); - }, + getSuccessMessage: () => 'Workflow has been terminated.', }, ] as const; diff --git a/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx index 94999e68e..c4eb7d153 100644 --- a/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx +++ b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx @@ -3,9 +3,9 @@ import { HttpResponse } from 'msw'; import { render, screen, userEvent } from '@/test-utils/rtl'; import { type CancelWorkflowResponse } from '@/route-handlers/cancel-workflow/cancel-workflow.types'; +import { mockWorkflowDetailsParams } from '@/views/workflow-page/__fixtures__/workflow-details-params'; import { mockWorkflowActionsConfig } from '../../__fixtures__/workflow-actions-config'; -import { mockWorkflowDetailsParams } from '../../__fixtures__/workflow-details-params'; import WorkflowActionsModalContent from '../workflow-actions-modal-content'; const mockEnqueue = jest.fn(); @@ -55,9 +55,8 @@ describe(WorkflowActionsModalContent.name, () => { expect(mockCancelWorkflow).toHaveBeenCalled(); expect(mockEnqueue).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Mock notification', - }), - undefined + message: 'Mock cancel notification', + }) ); expect(mockOnClose).toHaveBeenCalled(); }); diff --git a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx index efd7a672b..e7baa0ad7 100644 --- a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx +++ b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx @@ -5,7 +5,6 @@ import { ModalButton } from 'baseui/modal'; import { useSnackbar } from 'baseui/snackbar'; import { MdCheckCircle, MdErrorOutline } from 'react-icons/md'; -import logger from '@/utils/logger'; import request from '@/utils/request'; import { type RequestError } from '@/utils/request/request-error'; @@ -21,7 +20,7 @@ export default function WorkflowActionsModalContent({ }: Props) { const queryClient = useQueryClient(); const { enqueue, dequeue } = useSnackbar(); - const { mutateAsync, isPending, error } = useMutation< + const { mutate, isPending, error } = useMutation< R, RequestError, WorkflowActionInputParams @@ -42,6 +41,24 @@ export default function WorkflowActionsModalContent({ }), } ).then((res) => res.json() as R), + onSuccess: (result, params) => { + const { + // TODO: input, + ...workflowDetailsParams + } = params; + + queryClient.invalidateQueries({ + queryKey: ['describe_workflow', workflowDetailsParams], + }); + + onCloseModal(); + enqueue({ + message: action.getSuccessMessage(result, params), + startEnhancer: MdCheckCircle, + actionMessage: 'OK', + actionOnClick: () => dequeue(), + }); + }, }, queryClient ); @@ -75,42 +92,7 @@ export default function WorkflowActionsModalContent({ { - mutateAsync(params).then( - (result) => { - const { - // TODO: input, - ...workflowDetailsParams - } = params; - - queryClient.invalidateQueries({ - queryKey: ['describe_workflow', workflowDetailsParams], - }); - - onCloseModal(); - - action.onSuccess({ - result, - inputParams: params, - sendNotification: (message, duration) => - enqueue( - { - message, - startEnhancer: MdCheckCircle, - actionMessage: 'OK', - actionOnClick: () => dequeue(), - }, - duration - ), - }); - }, - (e) => - logger.error( - { error: e, params }, - 'Failed to perform workflow action' - ) - ); - }} + onClick={() => mutate(params)} isLoading={isPending} > {action.label} workflow diff --git a/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx b/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx index 9ef085246..93c78e743 100644 --- a/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx +++ b/src/views/workflow-actions/workflow-actions-modal/__tests__/workflow-actions-modal.test.tsx @@ -2,8 +2,8 @@ import { type ModalProps } from 'baseui/modal'; import { render, screen } from '@/test-utils/rtl'; +import { mockWorkflowDetailsParams } from '../../../workflow-page/__fixtures__/workflow-details-params'; import { mockWorkflowActionsConfig } from '../../__fixtures__/workflow-actions-config'; -import { mockWorkflowDetailsParams } from '../../__fixtures__/workflow-details-params'; import WorkflowActionsModal from '../workflow-actions-modal'; jest.mock('baseui/modal', () => ({ diff --git a/src/views/workflow-actions/workflow-actions.types.ts b/src/views/workflow-actions/workflow-actions.types.ts index b4ff35139..79485945c 100644 --- a/src/views/workflow-actions/workflow-actions.types.ts +++ b/src/views/workflow-actions/workflow-actions.types.ts @@ -1,5 +1,4 @@ import { type IconProps } from 'baseui/icon'; -import { type Duration } from 'baseui/snackbar'; import { type DescribeWorkflowResponse } from '@/route-handlers/describe-workflow/describe-workflow.types'; @@ -11,12 +10,6 @@ export type WorkflowActionInputParams = { // TODO: add input here for extended workflow actions }; -export type WorkflowActionOnSuccessParams = { - result: R; - inputParams: WorkflowActionInputParams; - sendNotification: (message: React.ReactNode, duration?: Duration) => void; -}; - export type WorkflowAction = { id: string; label: string; @@ -27,5 +20,8 @@ export type WorkflowAction = { }>; getIsEnabled: (workflow: DescribeWorkflowResponse) => boolean; apiRoute: string; - onSuccess: (params: WorkflowActionOnSuccessParams) => void | Promise; + getSuccessMessage: ( + result: R, + inputParams: WorkflowActionInputParams + ) => string; }; diff --git a/src/views/workflow-actions/__fixtures__/workflow-details-params.ts b/src/views/workflow-page/__fixtures__/workflow-details-params.ts similarity index 100% rename from src/views/workflow-actions/__fixtures__/workflow-details-params.ts rename to src/views/workflow-page/__fixtures__/workflow-details-params.ts From 72dc6a9a68d30dc45d5bbd87f426ea366121fd25 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 17 Jan 2025 13:01:47 +0100 Subject: [PATCH 3/3] Remove unnecessary mock fn --- .../__tests__/workflow-actions-modal-content.test.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx index c4eb7d153..adbab3ff1 100644 --- a/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx +++ b/src/views/workflow-actions/workflow-actions-modal-content/__tests__/workflow-actions-modal-content.test.tsx @@ -45,14 +45,13 @@ describe(WorkflowActionsModalContent.name, () => { }); it('calls mockCancelWorkflow, sends toast, and closes modal when the action button is clicked', async () => { - const { user, mockOnClose, mockCancelWorkflow } = setup({}); + const { user, mockOnClose } = setup({}); const cancelButton = await screen.findByRole('button', { name: 'Mock cancel workflow', }); await user.click(cancelButton); - expect(mockCancelWorkflow).toHaveBeenCalled(); expect(mockEnqueue).toHaveBeenCalledWith( expect.objectContaining({ message: 'Mock cancel notification', @@ -62,14 +61,13 @@ describe(WorkflowActionsModalContent.name, () => { }); it('Displays banner when the action button is clicked and action fails', async () => { - const { user, mockOnClose, mockCancelWorkflow } = setup({ error: true }); + const { user, mockOnClose } = setup({ error: true }); const cancelButton = await screen.findByRole('button', { name: 'Mock cancel workflow', }); await user.click(cancelButton); - expect(mockCancelWorkflow).toHaveBeenCalled(); expect( await screen.findByText('Failed to cancel workflow') ).toBeInTheDocument(); @@ -80,7 +78,6 @@ describe(WorkflowActionsModalContent.name, () => { function setup({ error }: { error?: boolean }) { const user = userEvent.setup(); const mockOnClose = jest.fn(); - const mockCancelWorkflow = jest.fn(); render( { - mockCancelWorkflow(); if (error) { return HttpResponse.json( { message: 'Failed to cancel workflow' }, @@ -109,5 +105,5 @@ function setup({ error }: { error?: boolean }) { } ); - return { user, mockOnClose, mockCancelWorkflow }; + return { user, mockOnClose }; }