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,