Skip to content

Commit

Permalink
Cleanup work for launching single task executions. (#102)
Browse files Browse the repository at this point in the history
* feat: add support for launch tasks in entity details view

* fix: correctly map initial parameters when relaunching

* fix: correct parent name and back link in execution details page

* fix: pass through referenceExecution when relaunching

* test: check rendering of referenceExecution

* test: adding tests for relaunch flow
  • Loading branch information
schottra authored Oct 8, 2020
1 parent b71b307 commit c32b653
Show file tree
Hide file tree
Showing 19 changed files with 612 additions and 136 deletions.
15 changes: 13 additions & 2 deletions src/components/Entities/EntityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { WaitForData } from 'components/common';
import { EntityDescription } from 'components/Entities/EntityDescription';
import { useProject } from 'components/hooks';
import { LaunchForm } from 'components/Launch/LaunchForm/LaunchForm';
import { ResourceIdentifier } from 'models';
import { ResourceIdentifier, ResourceType } from 'models';
import * as React from 'react';
import { entitySections } from './constants';
import { EntityDetailsHeader } from './EntityDetailsHeader';
Expand Down Expand Up @@ -39,6 +39,14 @@ export interface EntityDetailsProps {
id: ResourceIdentifier;
}

function getLaunchProps(id: ResourceIdentifier) {
if (id.resourceType === ResourceType.TASK) {
return { taskId: id };
}

return { workflowId: id };
}

/** A view which optionally renders description, schedules, executions, and a
* launch button/form for a given entity. Note: not all components are suitable
* for use with all entities (not all entities have schedules, for example).
Expand Down Expand Up @@ -83,7 +91,10 @@ export const EntityDetails: React.FC<EntityDetailsProps> = ({ id }) => {
fullWidth={true}
open={showLaunchForm}
>
<LaunchForm onClose={onCancelLaunch} workflowId={id} />
<LaunchForm
onClose={onCancelLaunch}
{...getLaunchProps(id)}
/>
</Dialog>
) : null}
</WaitForData>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { interactiveTextDisabledColor } from 'components/Theme';
import { Execution } from 'models';
import * as React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Routes } from 'routes';
import { ExecutionInputsOutputsModal } from '../ExecutionInputsOutputsModal';
import { ExecutionStatusBadge } from '../ExecutionStatusBadge';
import { TerminateExecutionButton } from '../TerminateExecution';
import { executionIsTerminal } from '../utils';
import { executionActionStrings } from './constants';
import { backLinkTitle, executionActionStrings } from './constants';
import { RelaunchExecutionForm } from './RelaunchExecutionForm';
import { getExecutionBackLink, getExecutionSourceId } from './utils';

const useStyles = makeStyles((theme: Theme) => {
return {
Expand Down Expand Up @@ -77,14 +77,9 @@ export const ExecutionDetailsAppBarContent: React.FC<{
const [showInputsOutputs, setShowInputsOutputs] = React.useState(false);
const [showRelaunchForm, setShowRelaunchForm] = React.useState(false);
const { domain, name, project } = execution.id;
const { phase, workflowId } = execution.closure;
const {
backLink = Routes.WorkflowDetails.makeUrl(
workflowId.project,
workflowId.domain,
workflowId.name
)
} = useLocationState();
const { phase } = execution.closure;
const sourceId = getExecutionSourceId(execution);
const { backLink = getExecutionBackLink(execution) } = useLocationState();
const isTerminal = executionIsTerminal(execution);
const onClickShowInputsOutputs = () => setShowInputsOutputs(true);
const onClickRelaunch = () => setShowRelaunchForm(true);
Expand Down Expand Up @@ -136,7 +131,11 @@ export const ExecutionDetailsAppBarContent: React.FC<{
<>
<NavBarContent>
<div className={styles.container}>
<RouterLink className={styles.backLink} to={backLink}>
<RouterLink
title={backLinkTitle}
className={styles.backLink}
to={backLink}
>
<ArrowBack />
</RouterLink>
<ExecutionStatusBadge phase={phase} type="workflow" />
Expand All @@ -149,7 +148,7 @@ export const ExecutionDetailsAppBarContent: React.FC<{
)}
>
<span>
{`${project}/${domain}/${workflowId.name}/`}
{`${project}/${domain}/${sourceId.name}/`}
<strong>{`${name}`}</strong>
</span>
</Typography>
Expand Down
18 changes: 17 additions & 1 deletion src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useCommonStyles } from 'components/common/styles';
import { secondaryBackgroundColor } from 'components/Theme';
import { Execution } from 'models';
import * as React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Routes } from 'routes/routes';
import { ExpandableExecutionError } from '../Tables/ExpandableExecutionError';
import { ExecutionMetadataLabels } from './constants';

Expand Down Expand Up @@ -65,7 +67,7 @@ export const ExecutionMetadata: React.FC<{

const { domain } = execution.id;
const { duration, error, startedAt, workflowId } = execution.closure;
const { systemMetadata } = execution.spec.metadata;
const { referenceExecution, systemMetadata } = execution.spec.metadata;
const cluster = systemMetadata?.executionCluster ?? dashedValueString;

const details: DetailItem[] = [
Expand Down Expand Up @@ -93,6 +95,20 @@ export const ExecutionMetadata: React.FC<{
}
];

if (referenceExecution != null) {
details.push({
label: ExecutionMetadataLabels.relatedTo,
value: (
<RouterLink
className={commonStyles.primaryLink}
to={Routes.ExecutionDetails.makeUrl(referenceExecution)}
>
{referenceExecution.name}
</RouterLink>
)
});
}

return (
<div className={styles.container}>
<div className={styles.detailsContainer}>
Expand Down
139 changes: 112 additions & 27 deletions src/components/Executions/ExecutionDetails/RelaunchExecutionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,129 @@
import { WaitForData } from 'components/common';
import { useWorkflow } from 'components/hooks';
import { useAPIContext } from 'components/data/apiContext';
import { fetchStates, useFetchableData } from 'components/hooks';
import { LaunchForm } from 'components/Launch/LaunchForm/LaunchForm';
import { useExecutionLaunchConfiguration } from 'components/Launch/LaunchForm/useExecutionLaunchConfiguration';
import { getWorkflowInputs } from 'components/Launch/LaunchForm/utils';
import { Execution, Workflow } from 'models';
import {
TaskInitialLaunchParameters,
WorkflowInitialLaunchParameters
} from 'components/Launch/LaunchForm/types';
import { fetchAndMapExecutionInputValues } from 'components/Launch/LaunchForm/useMappedExecutionInputValues';
import {
getTaskInputs,
getWorkflowInputs
} from 'components/Launch/LaunchForm/utils';
import { Execution } from 'models';
import * as React from 'react';
import { isSingleTaskExecution } from './utils';

export interface RelaunchExecutionFormProps {
execution: Execution;
onClose(): void;
}

const RenderForm: React.FC<RelaunchExecutionFormProps & {
workflow: Workflow;
}> = ({ execution, onClose, workflow }) => {
const { workflowId } = execution.closure;
const workflowInputs = getWorkflowInputs(workflow);
const launchConfiguration = useExecutionLaunchConfiguration({
execution,
workflowInputs
});
function useRelaunchWorkflowFormState({
execution
}: RelaunchExecutionFormProps) {
const apiContext = useAPIContext();
const initialParameters = useFetchableData<
WorkflowInitialLaunchParameters,
Execution
>(
{
defaultValue: {} as WorkflowInitialLaunchParameters,
doFetch: async execution => {
const {
closure: { workflowId },
spec: { launchPlan }
} = execution;
const workflow = await apiContext.getWorkflow(workflowId);
const inputDefinitions = getWorkflowInputs(workflow);
const values = await fetchAndMapExecutionInputValues(
{
execution,
inputDefinitions
},
apiContext
);
return { values, launchPlan, workflowId };
}
},
execution
);
return { initialParameters };
}

function useRelaunchTaskFormState({ execution }: RelaunchExecutionFormProps) {
const apiContext = useAPIContext();
const initialParameters = useFetchableData<
TaskInitialLaunchParameters,
Execution
>(
{
defaultValue: {} as TaskInitialLaunchParameters,
doFetch: async execution => {
const {
spec: { launchPlan: taskId }
} = execution;
const task = await apiContext.getTask(taskId);
const inputDefinitions = getTaskInputs(task);
const values = await fetchAndMapExecutionInputValues(
{
execution,
inputDefinitions
},
apiContext
);
return { values, taskId };
}
},
execution
);
return { initialParameters };
}

const RelaunchTaskForm: React.FC<RelaunchExecutionFormProps> = props => {
const { initialParameters } = useRelaunchTaskFormState(props);
const {
spec: { launchPlan: taskId }
} = props.execution;
return (
<WaitForData {...launchConfiguration}>
<LaunchForm
initialParameters={launchConfiguration.value}
onClose={onClose}
workflowId={workflowId}
/>
<WaitForData {...initialParameters}>
{initialParameters.state.matches(fetchStates.LOADED) ? (
<LaunchForm
initialParameters={initialParameters.value}
onClose={props.onClose}
referenceExecutionId={props.execution.id}
taskId={taskId}
/>
) : null}
</WaitForData>
);
};

/** For a given execution, fetches the associated workflow and renders a
* `LaunchWorkflowForm` based on the workflow, launch plan, and inputs of the
* execution. */
export const RelaunchExecutionForm: React.FC<RelaunchExecutionFormProps> = props => {
const workflow = useWorkflow(props.execution.closure.workflowId);
const RelaunchWorkflowForm: React.FC<RelaunchExecutionFormProps> = props => {
const { initialParameters } = useRelaunchWorkflowFormState(props);
const {
closure: { workflowId }
} = props.execution;
return (
<WaitForData {...workflow}>
{() => <RenderForm {...props} workflow={workflow.value} />}
<WaitForData {...initialParameters}>
{initialParameters.state.matches(fetchStates.LOADED) ? (
<LaunchForm
initialParameters={initialParameters.value}
onClose={props.onClose}
referenceExecutionId={props.execution.id}
workflowId={workflowId}
/>
) : null}
</WaitForData>
);
};

/** For a given execution, fetches the associated Workflow/Task and renders a
* `LaunchForm` based on the same source with input values taken from the execution. */
export const RelaunchExecutionForm: React.FC<RelaunchExecutionFormProps> = props => {
return isSingleTaskExecution(props.execution) ? (
<RelaunchTaskForm {...props} />
) : (
<RelaunchWorkflowForm {...props} />
);
};
3 changes: 3 additions & 0 deletions src/components/Executions/ExecutionDetails/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum ExecutionMetadataLabels {
domain = 'Domain',
duration = 'Duration',
time = 'Time',
relatedTo = 'Related to',
version = 'Version'
}

Expand All @@ -20,3 +21,5 @@ export const tabs = {
export const executionActionStrings = {
clone: 'Clone Execution'
};

export const backLinkTitle = 'Back to parent';
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
ExecutionContext,
ExecutionContextData
} from 'components/Executions/contexts';
import { Execution } from 'models';
import { Execution, Identifier, ResourceType } from 'models';
import { createMockExecution } from 'models/__mocks__/executionsData';
import { WorkflowExecutionPhase } from 'models/Execution/enums';
import * as React from 'react';
import { MemoryRouter } from 'react-router';
import { Routes } from 'routes';
import { delayedPromise, DelayedPromiseResult } from 'test/utils';
import { executionActionStrings } from '../constants';
import { backLinkTitle, executionActionStrings } from '../constants';
import { ExecutionDetailsAppBarContent } from '../ExecutionDetailsAppBarContent';

jest.mock('components/Navigation/NavBarContent', () => ({
Expand All @@ -27,9 +28,11 @@ describe('ExecutionDetailsAppBarContent', () => {
let executionContext: ExecutionContextData;
let mockTerminateExecution: jest.Mock<Promise<void>>;
let terminatePromise: DelayedPromiseResult<void>;
let sourceId: Identifier;

beforeEach(() => {
execution = createMockExecution();
sourceId = execution.closure.workflowId;
mockTerminateExecution = jest.fn().mockImplementation(() => {
terminatePromise = delayedPromise();
return terminatePromise;
Expand Down Expand Up @@ -95,4 +98,59 @@ describe('ExecutionDetailsAppBarContent', () => {
expect(queryByLabelText(commonLabels.moreOptionsButton)).toBeNull();
});
});

it('renders a back link to the parent workflow', async () => {
const { getByTitle } = renderContent();
await waitFor(() =>
expect(getByTitle(backLinkTitle)).toHaveAttribute(
'href',
Routes.WorkflowDetails.makeUrl(
sourceId.project,
sourceId.domain,
sourceId.name
)
)
);
});

it('renders the workflow name in the app bar content', async () => {
const { getByText } = renderContent();
const { project, domain } = execution.id;
await waitFor(() =>
expect(
getByText(`${project}/${domain}/${sourceId.name}/`)
).toBeInTheDocument()
);
});

describe('for single task executions', () => {
beforeEach(() => {
execution.spec.launchPlan.resourceType = ResourceType.TASK;
sourceId = execution.spec.launchPlan;
});

it('renders a back link to the parent task', async () => {
const { getByTitle } = renderContent();
await waitFor(() =>
expect(getByTitle(backLinkTitle)).toHaveAttribute(
'href',
Routes.TaskDetails.makeUrl(
sourceId.project,
sourceId.domain,
sourceId.name
)
)
);
});

it('renders the task name in the app bar content', async () => {
const { getByText } = renderContent();
const { project, domain } = execution.id;
await waitFor(() =>
expect(
getByText(`${project}/${domain}/${sourceId.name}/`)
).toBeInTheDocument()
);
});
});
});
Loading

0 comments on commit c32b653

Please sign in to comment.