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

Refactor Launch form to use a state machine #99

Merged
merged 10 commits into from
Oct 2, 2020
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"@types/webpack-env": "^1.13.1",
"@types/webpack-hot-middleware": "^2.15.0",
"@typescript-eslint/parser": "^1.0.0",
"@xstate/react": "^0.8.1",
"@xstate/react": "^1.0.0",
BobNisco marked this conversation as resolved.
Show resolved Hide resolved
"add-asset-html-webpack-plugin": "^2.1.3",
"autoprefixer": "^8.3.0",
"axios": "^0.18.1",
Expand Down
203 changes: 114 additions & 89 deletions src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,146 +6,171 @@ import {
FormHelperText,
Typography
} from '@material-ui/core';
import { WaitForData } from 'components/common';
import { ButtonCircularProgress } from 'components/common/ButtonCircularProgress';
import { APIContextValue, useAPIContext } from 'components/data/apiContext';
import { isLoadingState } from 'components/hooks/fetchMachine';
import {
FilterOperationName,
NamedEntityIdentifier,
SortDirection,
workflowSortFields
} from 'models';
import * as React from 'react';
import { formStrings } from './constants';
import { InputValueCacheContext } from './inputValueCache';
import { LaunchState } from './launchMachine';
import { LaunchWorkflowFormInputs } from './LaunchWorkflowFormInputs';
import { SearchableSelector } from './SearchableSelector';
import { useStyles } from './styles';
import { LaunchWorkflowFormProps } from './types';
import { UnsupportedRequiredInputsError } from './UnsupportedRequiredInputsError';
import { useLaunchWorkflowFormState } from './useLaunchWorkflowFormState';
import { workflowsToSearchableSelectorOptions } from './utils';

function generateFetchSearchResults(
{ listWorkflows }: APIContextValue,
workflowId: NamedEntityIdentifier
) {
return async (query: string) => {
const { project, domain, name } = workflowId;
const { entities: workflows } = await listWorkflows(
{ project, domain, name },
{
filter: [
{
key: 'version',
operation: FilterOperationName.CONTAINS,
value: query
}
],
sort: {
key: workflowSortFields.createdAt,
direction: SortDirection.DESCENDING
}
}
);
return workflowsToSearchableSelectorOptions(workflows);
};
}

/** Renders the form for initiating a Launch request based on a Workflow */
export const LaunchWorkflowForm: React.FC<LaunchWorkflowFormProps> = props => {
const state = useLaunchWorkflowFormState(props);
const { submissionState, unsupportedRequiredInputs, workflows } = state;
const {
formKey,
formInputsRef,
showErrors,
inputValueCache,
onCancel,
onSubmit,
state,
workflowSourceSelectorState
} = useLaunchWorkflowFormState(props);
const styles = useStyles();
const fetchSearchResults = generateFetchSearchResults(
useAPIContext(),
props.workflowId
);

const {
fetchSearchResults,
launchPlanSelectorOptions,
onSelectLaunchPlan,
onSelectWorkflowVersion,
selectedLaunchPlan,
selectedWorkflow,
workflowSelectorOptions
} = workflowSourceSelectorState;

const submit: React.FormEventHandler = event => {
event.preventDefault();
state.onSubmit();
onSubmit();
};

const submissionInFlight = isLoadingState(submissionState.state);
const preventSubmit =
submissionInFlight ||
!state.inputsReady ||
unsupportedRequiredInputs.length > 0;
// const submissionInFlight = state.matches({ submit: 'submitting' });
schottra marked this conversation as resolved.
Show resolved Hide resolved
// const canSubmit = !state.matches({ sourceSelected: 'enterInputs' });
// const showWorkflowSelector = [
// { working: 'sourceSelected' },
// { workflow: 'select' }
// ].some(state.matches);
// console.log(JSON.stringify(state.value), showWorkflowSelector);
// console.log(
// 'dot match',
// state.matches('select')
// );
// const showLaunchPlanSelector = [
// { working: 'sourceSelected' },
// { launchPlan: 'select' }
// ].some(state.matches);
// const showInputs = [
// { sourceSelected: 'enterInputs' },
// { sourceSelected: 'submit' }
// ].some(state.matches);

const submissionInFlight = state.matches(LaunchState.SUBMITTING);
const canSubmit = [
LaunchState.ENTER_INPUTS,
LaunchState.VALIDATING_INPUTS,
LaunchState.INVALID_INPUTS,
LaunchState.SUBMIT_FAILED
].some(state.matches);
const showWorkflowSelector = ![
LaunchState.LOADING_WORKFLOW_VERSIONS,
LaunchState.FAILED_LOADING_WORKFLOW_VERSIONS
].some(state.matches);
const showLaunchPlanSelector =
state.context.workflowVersion &&
![
LaunchState.LOADING_LAUNCH_PLANS,
LaunchState.FAILED_LOADING_LAUNCH_PLANS
].some(state.matches);
const showInputs = [
LaunchState.UNSUPPORTED_INPUTS,
LaunchState.ENTER_INPUTS,
LaunchState.VALIDATING_INPUTS,
LaunchState.INVALID_INPUTS,
LaunchState.SUBMIT_VALIDATING,
LaunchState.SUBMITTING,
LaunchState.SUBMIT_FAILED,
LaunchState.SUBMIT_SUCCEEDED
].some(state.matches);

// TODO: We removed all loading indicators here. Decide if we want skeletons
// instead.
return (
<InputValueCacheContext.Provider value={state.inputValueCache}>
<InputValueCacheContext.Provider value={inputValueCache}>
<DialogTitle disableTypography={true} className={styles.header}>
<div className={styles.inputLabel}>{formStrings.title}</div>
<Typography variant="h6">{state.workflowName}</Typography>
<Typography variant="h6">
{state.context.sourceWorkflowId?.name}
</Typography>
</DialogTitle>
<DialogContent dividers={true} className={styles.inputsSection}>
<WaitForData spinnerVariant="medium" {...workflows}>
{showWorkflowSelector ? (
<section
title={formStrings.workflowVersion}
className={styles.formControl}
>
<SearchableSelector
id="launch-workflow-selector"
label={formStrings.workflowVersion}
onSelectionChanged={state.onSelectWorkflow}
options={state.workflowSelectorOptions}
onSelectionChanged={onSelectWorkflowVersion}
options={workflowSelectorOptions}
fetchSearchResults={fetchSearchResults}
selectedItem={state.selectedWorkflow}
selectedItem={selectedWorkflow}
/>
</section>
) : null}
{showLaunchPlanSelector ? (
<section
title={formStrings.launchPlan}
className={styles.formControl}
>
<SearchableSelector
id="launch-lp-selector"
label={formStrings.launchPlan}
onSelectionChanged={onSelectLaunchPlan}
options={launchPlanSelectorOptions}
selectedItem={selectedLaunchPlan}
/>
</section>
<WaitForData {...state.launchPlans} spinnerVariant="medium">
<section
title={formStrings.launchPlan}
className={styles.formControl}
>
<SearchableSelector
id="launch-lp-selector"
label={formStrings.launchPlan}
onSelectionChanged={state.onSelectLaunchPlan}
options={state.launchPlanSelectorOptions}
selectedItem={state.selectedLaunchPlan}
) : null}
{showInputs ? (
<section title={formStrings.inputs}>
{state.matches(LaunchState.UNSUPPORTED_INPUTS) ? (
<UnsupportedRequiredInputsError
inputs={state.context.unsupportedRequiredInputs}
/>
) : (
<LaunchWorkflowFormInputs
key={formKey}
inputs={state.context.parsedInputs}
ref={formInputsRef}
showErrors={showErrors}
/>
</section>
</WaitForData>
{state.inputsReady ? (
<section title={formStrings.inputs}>
{state.unsupportedRequiredInputs.length > 0 ? (
<UnsupportedRequiredInputsError
inputs={state.unsupportedRequiredInputs}
/>
) : (
<LaunchWorkflowFormInputs
key={state.formKey}
inputs={state.inputs}
ref={state.formInputsRef}
showErrors={state.showErrors}
/>
)}
</section>
) : null}
</WaitForData>
)}
</section>
) : null}
</DialogContent>
<div className={styles.footer}>
{!!submissionState.lastError && (
{state.matches(LaunchState.SUBMIT_FAILED) ? (
<FormHelperText error={true}>
{submissionState.lastError.message}
{state.context.error.message}
</FormHelperText>
)}
) : null}
<DialogActions>
<Button
color="primary"
disabled={submissionInFlight}
id="launch-workflow-cancel"
onClick={state.onCancel}
onClick={onCancel}
variant="outlined"
>
{formStrings.cancel}
</Button>
<Button
color="primary"
disabled={preventSubmit}
disabled={!canSubmit}
id="launch-workflow-submit"
onClick={submit}
type="submit"
Expand Down
Loading