Skip to content

Commit

Permalink
Refactor Launch form to use a state machine (#99)
Browse files Browse the repository at this point in the history
* refactor: filling out details of the state machine for launch

* refactor: checkpoint

* refactor: mostly finished wiring of machine to component state

* refactor: more work to get form component migrated to using machine

* refactor: cleaning up state for selectors

* fix: type error due to patch version upgrade

* refactor: trying a flat state structure

* fix: getting all tests passing again

* chore: cleanup and docs

* chore: pull request feedback
  • Loading branch information
schottra authored Oct 2, 2020
1 parent 38eab6d commit c5462f6
Show file tree
Hide file tree
Showing 11 changed files with 3,686 additions and 2,223 deletions.
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",
"add-asset-html-webpack-plugin": "^2.1.3",
"autoprefixer": "^8.3.0",
"axios": "^0.18.1",
Expand Down
183 changes: 94 additions & 89 deletions src/components/Launch/LaunchWorkflowForm/LaunchWorkflowForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,146 +6,151 @@ 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(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>
<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}
{showLaunchPlanSelector ? (
<section
title={formStrings.launchPlan}
className={styles.formControl}
>
<SearchableSelector
id="launch-lp-selector"
label={formStrings.launchPlan}
onSelectionChanged={onSelectLaunchPlan}
options={launchPlanSelectorOptions}
selectedItem={selectedLaunchPlan}
/>
</section>
) : 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

0 comments on commit c5462f6

Please sign in to comment.