-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Make launch state and components generic (#100)
* refactor: splitting launch machine into two separate types * refactor: move shared state out to component * refactor: use composition to create workflow form * refactor: update usage of LaunchWorkflowForm -> LaunchForm * chore: cleanup and fix tests
- Loading branch information
Showing
54 changed files
with
760 additions
and
586 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import * as React from 'react'; | ||
import { | ||
createInputValueCache, | ||
InputValueCacheContext | ||
} from './inputValueCache'; | ||
import { LaunchWorkflowForm } from './LaunchWorkflowForm'; | ||
import { LaunchFormProps, LaunchWorkflowFormProps } from './types'; | ||
|
||
function isWorkflowPropsObject( | ||
props: LaunchFormProps | ||
): props is LaunchWorkflowFormProps { | ||
return (props as LaunchWorkflowFormProps).workflowId !== undefined; | ||
} | ||
|
||
/** Renders the form for initiating a Launch request based on a Workflow or Task */ | ||
export const LaunchForm: React.FC<LaunchFormProps> = props => { | ||
const [inputValueCache] = React.useState(createInputValueCache()); | ||
|
||
// TODO: Use LaunchTaskForm when it has been implemented. | ||
return ( | ||
<InputValueCacheContext.Provider value={inputValueCache}> | ||
{isWorkflowPropsObject(props) ? ( | ||
<LaunchWorkflowForm {...props} /> | ||
) : null} | ||
</InputValueCacheContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { Button, DialogActions, FormHelperText } from '@material-ui/core'; | ||
import { ButtonCircularProgress } from 'components'; | ||
import * as React from 'react'; | ||
import { history } from 'routes/history'; | ||
import { Routes } from 'routes/routes'; | ||
import { formStrings } from './constants'; | ||
import { LaunchState } from './launchMachine'; | ||
import { useStyles } from './styles'; | ||
import { BaseInterpretedLaunchState, BaseLaunchService } from './types'; | ||
|
||
export interface LaunchFormActionsProps { | ||
state: BaseInterpretedLaunchState; | ||
service: BaseLaunchService; | ||
onClose(): void; | ||
} | ||
/** Renders the Submit/Cancel buttons for a LaunchForm */ | ||
export const LaunchFormActions: React.FC<LaunchFormActionsProps> = ({ | ||
state, | ||
service, | ||
onClose | ||
}) => { | ||
const styles = useStyles(); | ||
const submissionInFlight = state.matches(LaunchState.SUBMITTING); | ||
const canSubmit = [ | ||
LaunchState.ENTER_INPUTS, | ||
LaunchState.VALIDATING_INPUTS, | ||
LaunchState.INVALID_INPUTS, | ||
LaunchState.SUBMIT_FAILED | ||
].some(state.matches); | ||
|
||
const submit: React.FormEventHandler = event => { | ||
event.preventDefault(); | ||
service.send({ type: 'SUBMIT' }); | ||
}; | ||
|
||
const onCancel = () => { | ||
service.send({ type: 'CANCEL' }); | ||
onClose(); | ||
}; | ||
|
||
React.useEffect(() => { | ||
const subscription = service.subscribe(newState => { | ||
// On transition to final success state, read the resulting execution | ||
// id and navigate to the Execution Details page. | ||
// if (state.matches({ submit: 'succeeded' })) { | ||
if (newState.matches(LaunchState.SUBMIT_SUCCEEDED)) { | ||
history.push( | ||
Routes.ExecutionDetails.makeUrl( | ||
newState.context.resultExecutionId | ||
) | ||
); | ||
} | ||
}); | ||
|
||
return subscription.unsubscribe; | ||
}, [service]); | ||
|
||
return ( | ||
<div className={styles.footer}> | ||
{state.matches(LaunchState.SUBMIT_FAILED) ? ( | ||
<FormHelperText error={true}> | ||
{state.context.error.message} | ||
</FormHelperText> | ||
) : null} | ||
<DialogActions> | ||
<Button | ||
color="primary" | ||
disabled={submissionInFlight} | ||
id="launch-workflow-cancel" | ||
onClick={onCancel} | ||
variant="outlined" | ||
> | ||
{formStrings.cancel} | ||
</Button> | ||
<Button | ||
color="primary" | ||
disabled={!canSubmit} | ||
id="launch-workflow-submit" | ||
onClick={submit} | ||
type="submit" | ||
variant="contained" | ||
> | ||
{formStrings.submit} | ||
{submissionInFlight && <ButtonCircularProgress />} | ||
</Button> | ||
</DialogActions> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { DialogTitle, Typography } from '@material-ui/core'; | ||
import * as React from 'react'; | ||
import { formStrings } from './constants'; | ||
import { useStyles } from './styles'; | ||
|
||
/** Shared header component for the Launch form */ | ||
export const LaunchFormHeader: React.FC<{ title?: string }> = ({ | ||
title = '' | ||
}) => { | ||
const styles = useStyles(); | ||
return ( | ||
<DialogTitle disableTypography={true} className={styles.header}> | ||
<div className={styles.inputLabel}>{formStrings.title}</div> | ||
<Typography variant="h6">{title}</Typography> | ||
</DialogTitle> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as React from 'react'; | ||
import { BlobInput } from './BlobInput'; | ||
import { CollectionInput } from './CollectionInput'; | ||
import { formStrings } from './constants'; | ||
import { LaunchState } from './launchMachine'; | ||
import { SimpleInput } from './SimpleInput'; | ||
import { useStyles } from './styles'; | ||
import { | ||
BaseInterpretedLaunchState, | ||
InputProps, | ||
InputType, | ||
LaunchFormInputsRef | ||
} from './types'; | ||
import { UnsupportedInput } from './UnsupportedInput'; | ||
import { UnsupportedRequiredInputsError } from './UnsupportedRequiredInputsError'; | ||
import { useFormInputsState } from './useFormInputsState'; | ||
|
||
function getComponentForInput(input: InputProps, showErrors: boolean) { | ||
const props = { ...input, error: showErrors ? input.error : undefined }; | ||
switch (input.typeDefinition.type) { | ||
case InputType.Blob: | ||
return <BlobInput {...props} />; | ||
case InputType.Collection: | ||
return <CollectionInput {...props} />; | ||
case InputType.Map: | ||
case InputType.Schema: | ||
case InputType.Unknown: | ||
case InputType.None: | ||
return <UnsupportedInput {...props} />; | ||
default: | ||
return <SimpleInput {...props} />; | ||
} | ||
} | ||
|
||
export interface LaunchFormInputsProps { | ||
state: BaseInterpretedLaunchState; | ||
} | ||
|
||
export const LaunchFormInputsImpl: React.RefForwardingComponent< | ||
LaunchFormInputsRef, | ||
LaunchFormInputsProps | ||
> = ({ state }, ref) => { | ||
const { | ||
parsedInputs, | ||
unsupportedRequiredInputs, | ||
showErrors | ||
} = state.context; | ||
const { getValues, inputs, validate } = useFormInputsState(parsedInputs); | ||
const styles = useStyles(); | ||
React.useImperativeHandle(ref, () => ({ | ||
getValues, | ||
validate | ||
})); | ||
|
||
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); | ||
|
||
return showInputs ? ( | ||
<section title={formStrings.inputs}> | ||
{state.matches(LaunchState.UNSUPPORTED_INPUTS) ? ( | ||
<UnsupportedRequiredInputsError | ||
inputs={unsupportedRequiredInputs} | ||
/> | ||
) : ( | ||
<> | ||
{inputs.map(input => ( | ||
<div key={input.label} className={styles.formControl}> | ||
{getComponentForInput(input, showErrors)} | ||
</div> | ||
))} | ||
</> | ||
)} | ||
</section> | ||
) : null; | ||
}; | ||
|
||
/** Renders an array of `ParsedInput` values using the appropriate | ||
* components. A `ref` to this component is used to access the current | ||
* form values and trigger manual validation if needed. | ||
*/ | ||
export const LaunchFormInputs = React.forwardRef(LaunchFormInputsImpl); |
105 changes: 105 additions & 0 deletions
105
src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { DialogContent } from '@material-ui/core'; | ||
import { getCacheKey } from 'components/Cache/utils'; | ||
import * as React from 'react'; | ||
import { formStrings } from './constants'; | ||
import { LaunchFormActions } from './LaunchFormActions'; | ||
import { LaunchFormHeader } from './LaunchFormHeader'; | ||
import { LaunchFormInputs } from './LaunchFormInputs'; | ||
import { LaunchState } from './launchMachine'; | ||
import { SearchableSelector } from './SearchableSelector'; | ||
import { useStyles } from './styles'; | ||
import { | ||
BaseInterpretedLaunchState, | ||
BaseLaunchService, | ||
LaunchWorkflowFormProps | ||
} from './types'; | ||
import { useLaunchWorkflowFormState } from './useLaunchWorkflowFormState'; | ||
|
||
/** Renders the form for initiating a Launch request based on a Workflow */ | ||
export const LaunchWorkflowForm: React.FC<LaunchWorkflowFormProps> = props => { | ||
const { | ||
formInputsRef, | ||
state, | ||
service, | ||
workflowSourceSelectorState | ||
} = useLaunchWorkflowFormState(props); | ||
const styles = useStyles(); | ||
const baseState = state as BaseInterpretedLaunchState; | ||
const baseService = service as BaseLaunchService; | ||
|
||
// Any time the inputs change (even if it's just re-ordering), we must | ||
// change the form key so that the inputs component will re-mount. | ||
const formKey = React.useMemo<string>(() => { | ||
return getCacheKey(state.context.parsedInputs); | ||
}, [state.context.parsedInputs]); | ||
|
||
const { | ||
fetchSearchResults, | ||
launchPlanSelectorOptions, | ||
onSelectLaunchPlan, | ||
onSelectWorkflowVersion, | ||
selectedLaunchPlan, | ||
selectedWorkflow, | ||
workflowSelectorOptions | ||
} = workflowSourceSelectorState; | ||
|
||
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); | ||
|
||
// TODO: We removed all loading indicators here. Decide if we want skeletons | ||
// instead. | ||
return ( | ||
<> | ||
<LaunchFormHeader title={state.context.sourceId?.name} /> | ||
<DialogContent dividers={true} className={styles.inputsSection}> | ||
{showWorkflowSelector ? ( | ||
<section | ||
title={formStrings.workflowVersion} | ||
className={styles.formControl} | ||
> | ||
<SearchableSelector | ||
id="launch-workflow-selector" | ||
label={formStrings.workflowVersion} | ||
onSelectionChanged={onSelectWorkflowVersion} | ||
options={workflowSelectorOptions} | ||
fetchSearchResults={fetchSearchResults} | ||
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> | ||
) : null} | ||
<LaunchFormInputs | ||
key={formKey} | ||
ref={formInputsRef} | ||
state={baseState} | ||
/> | ||
</DialogContent> | ||
<LaunchFormActions | ||
state={baseState} | ||
service={baseService} | ||
onClose={props.onClose} | ||
/> | ||
</> | ||
); | ||
}; |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.