diff --git a/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx b/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx index a4b168b95..f68e59eb5 100644 --- a/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx +++ b/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx @@ -3,6 +3,7 @@ import { makeStyles, Theme } from '@material-ui/core/styles'; import ArrowBack from '@material-ui/icons/ArrowBack'; import * as classnames from 'classnames'; import { navbarGridHeight } from 'common/layout'; +import { ButtonCircularProgress } from 'components/common/ButtonCircularProgress'; import { MoreOptionsMenu } from 'components/common/MoreOptionsMenu'; import { useCommonStyles } from 'components/common/styles'; import { useLocationState } from 'components/hooks/useLocationState'; @@ -20,6 +21,7 @@ import { executionIsRunning, executionIsTerminal } from '../utils'; import { backLinkTitle, executionActionStrings } from './constants'; import { RelaunchExecutionForm } from './RelaunchExecutionForm'; import { getExecutionBackLink, getExecutionSourceId } from './utils'; +import { useRecoverExecutionState } from './useRecoverExecutionState'; const useStyles = makeStyles((theme: Theme) => { return { @@ -95,6 +97,16 @@ export const ExecutionDetailsAppBarContent: React.FC<{ const backLink = fromExecutionNav ? Routes.ProjectDetails.sections.executions.makeUrl(project, domain) : originalBackLink; + const { + recoverExecution, + recoverState: { isLoading: recovering, error, data: recoveredId } + } = useRecoverExecutionState(); + + React.useEffect(() => { + if (!recovering && recoveredId) { + history.push(Routes.ExecutionDetails.makeUrl(recoveredId)); + } + }, [recovering, recoveredId]); let modalContent: JSX.Element | null = null; if (showInputsOutputs) { @@ -107,21 +119,41 @@ export const ExecutionDetailsAppBarContent: React.FC<{ ); } + const onClickRecover = React.useCallback(async () => { + await recoverExecution(); + }, [recoverExecution]); + const actionContent = isRunning ? ( ) : isTerminal ? ( - + <> + + + ) : null; // For non-terminal executions, add an overflow menu with the ability to clone diff --git a/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx b/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx index c0226dd09..d921ab354 100644 --- a/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx @@ -16,6 +16,8 @@ import { createMockExecution } from 'models/__mocks__/executionsData'; import * as React from 'react'; import { MemoryRouter } from 'react-router'; import { Routes } from 'routes/routes'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { createTestQueryClient } from 'test/utils'; import { backLinkTitle, executionActionStrings } from '../constants'; import { ExecutionDetailsAppBarContent } from '../ExecutionDetailsAppBarContent'; @@ -27,6 +29,7 @@ describe('ExecutionDetailsAppBarContent', () => { let execution: Execution; let executionContext: ExecutionContextData; let sourceId: Identifier; + let queryClient: QueryClient; beforeEach(() => { execution = createMockExecution(); @@ -35,15 +38,19 @@ describe('ExecutionDetailsAppBarContent', () => { executionContext = { execution }; + + queryClient = createTestQueryClient(); }); const renderContent = () => render( - - - - - + + + + + + + ); describe('for running executions', () => { diff --git a/src/components/Executions/ExecutionDetails/useRecoverExecutionState.ts b/src/components/Executions/ExecutionDetails/useRecoverExecutionState.ts new file mode 100644 index 000000000..c449aeabe --- /dev/null +++ b/src/components/Executions/ExecutionDetails/useRecoverExecutionState.ts @@ -0,0 +1,30 @@ +import { useAPIContext } from 'components/data/apiContext'; +import { useContext } from 'react'; +import { useMutation } from 'react-query'; +import { ExecutionContext } from '../contexts'; +import { WorkflowExecutionIdentifier } from 'models/Execution/types'; + +export function useRecoverExecutionState() { + const { recoverWorkflowExecution } = useAPIContext(); + const { + execution: { id } + } = useContext(ExecutionContext); + + const { mutate, ...recoverState } = useMutation< + WorkflowExecutionIdentifier, + Error + >(async () => { + const { id: recoveredId } = await recoverWorkflowExecution({ id }); + if (!recoveredId) { + throw new Error('API Response did not include new execution id'); + } + return recoveredId as WorkflowExecutionIdentifier; + }); + + const recoverExecution = () => mutate(); + + return { + recoverState, + recoverExecution + }; +} diff --git a/src/models/Common/constants.ts b/src/models/Common/constants.ts index 02b813799..fbd8b3c21 100644 --- a/src/models/Common/constants.ts +++ b/src/models/Common/constants.ts @@ -11,6 +11,7 @@ export const endpointPrefixes = { nodeExecution: '/node_executions', project: '/projects', relaunchExecution: '/executions/relaunch', + recoverExecution: '/executions/recover', task: '/tasks', taskExecution: '/task_executions', taskExecutionChildren: '/children/task_executions', diff --git a/src/models/Execution/api.ts b/src/models/Execution/api.ts index fd95d6610..b0b03b3f2 100644 --- a/src/models/Execution/api.ts +++ b/src/models/Execution/api.ts @@ -19,6 +19,7 @@ import { defaultExecutionPrincipal } from './constants'; import { Execution, ExecutionData, + ExecutionMetadata, NodeExecution, NodeExecutionIdentifier, TaskExecution, @@ -181,6 +182,33 @@ export const relaunchWorkflowExecution = ( config ); +interface RecoverParams { + id: WorkflowExecutionIdentifier; + name?: string; + metadata?: ExecutionMetadata; +} + +/** + * Submits a request to recover a WorkflowExecution + */ + +export const recoverWorkflowExecution = ( + { id, name, metadata }: RecoverParams, + config?: RequestConfig +) => + postAdminEntity< + Admin.IExecutionRecoverRequest, + Admin.ExecutionCreateResponse + >( + { + data: { id, name, metadata }, + path: endpointPrefixes.recoverExecution, + requestMessageType: Admin.ExecutionRecoverRequest, + responseMessageType: Admin.ExecutionCreateResponse + }, + config + ); + /** Retrieves a single `NodeExecution` record */ export const getNodeExecution = ( id: NodeExecutionIdentifier,