Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup work for launching single task executions. #102

Merged
merged 6 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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