Skip to content

Commit

Permalink
feat: Add Task support to Launch form (#101)
Browse files Browse the repository at this point in the history
* feat: add task support in launch components

* test: updating launch form tests to handle task cases

* fix: remaining broken tests
  • Loading branch information
schottra authored Oct 6, 2020
1 parent 2254ef6 commit b71b307
Show file tree
Hide file tree
Showing 26 changed files with 1,332 additions and 101 deletions.
5 changes: 4 additions & 1 deletion src/components/Launch/LaunchForm/LaunchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createInputValueCache,
InputValueCacheContext
} from './inputValueCache';
import { LaunchTaskForm } from './LaunchTaskForm';
import { LaunchWorkflowForm } from './LaunchWorkflowForm';
import { LaunchFormProps, LaunchWorkflowFormProps } from './types';

Expand All @@ -21,7 +22,9 @@ export const LaunchForm: React.FC<LaunchFormProps> = props => {
<InputValueCacheContext.Provider value={inputValueCache}>
{isWorkflowPropsObject(props) ? (
<LaunchWorkflowForm {...props} />
) : null}
) : (
<LaunchTaskForm {...props} />
)}
</InputValueCacheContext.Provider>
);
};
4 changes: 3 additions & 1 deletion src/components/Launch/LaunchForm/LaunchFormInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ function getComponentForInput(input: InputProps, showErrors: boolean) {

export interface LaunchFormInputsProps {
state: BaseInterpretedLaunchState;
variant: 'workflow' | 'task';
}

export const LaunchFormInputsImpl: React.RefForwardingComponent<
LaunchFormInputsRef,
LaunchFormInputsProps
> = ({ state }, ref) => {
> = ({ state, variant }, ref) => {
const {
parsedInputs,
unsupportedRequiredInputs,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const LaunchFormInputsImpl: React.RefForwardingComponent<
{state.matches(LaunchState.UNSUPPORTED_INPUTS) ? (
<UnsupportedRequiredInputsError
inputs={unsupportedRequiredInputs}
variant={variant}
/>
) : (
<>
Expand Down
83 changes: 83 additions & 0 deletions src/components/Launch/LaunchForm/LaunchTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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,
LaunchTaskFormProps
} from './types';
import { useLaunchTaskFormState } from './useLaunchTaskFormState';

/** Renders the form for initiating a Launch request based on a Task */
export const LaunchTaskForm: React.FC<LaunchTaskFormProps> = props => {
const {
formInputsRef,
state,
service,
taskSourceSelectorState
} = useLaunchTaskFormState(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,
onSelectTaskVersion,
selectedTask,
taskSelectorOptions
} = taskSourceSelectorState;

const showTaskSelector = ![
LaunchState.LOADING_TASK_VERSIONS,
LaunchState.FAILED_LOADING_TASK_VERSIONS
].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}>
{showTaskSelector ? (
<section
title={formStrings.taskVersion}
className={styles.formControl}
>
<SearchableSelector
id="launch-task-selector"
label={formStrings.taskVersion}
onSelectionChanged={onSelectTaskVersion}
options={taskSelectorOptions}
fetchSearchResults={fetchSearchResults}
selectedItem={selectedTask}
/>
</section>
) : null}
<LaunchFormInputs
key={formKey}
ref={formInputsRef}
state={baseState}
variant="task"
/>
</DialogContent>
<LaunchFormActions
state={baseState}
service={baseService}
onClose={props.onClose}
/>
</>
);
};
1 change: 1 addition & 0 deletions src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const LaunchWorkflowForm: React.FC<LaunchWorkflowFormProps> = props => {
key={formKey}
ref={formInputsRef}
state={baseState}
variant="workflow"
/>
</DialogContent>
<LaunchFormActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { NonIdealState } from 'components/common';
import { useCommonStyles } from 'components/common/styles';
import * as React from 'react';
import {
cannotLaunchTaskString,
cannotLaunchWorkflowString,
requiredInputSuffix,
unsupportedRequiredInputsString
taskUnsupportedRequiredInputsString,
workflowUnsupportedRequiredInputsString
} from './constants';
import { ParsedInput } from './types';

Expand All @@ -28,24 +30,33 @@ function formatLabel(label: string) {

export interface UnsupportedRequiredInputsErrorProps {
inputs: ParsedInput[];
variant: 'workflow' | 'task';
}
/** An informational error to be shown if a Workflow cannot be launch due to
* required inputs for which we will not be able to provide a value.
*/
export const UnsupportedRequiredInputsError: React.FC<UnsupportedRequiredInputsErrorProps> = ({
inputs
inputs,
variant
}) => {
const styles = useStyles();
const commonStyles = useCommonStyles();
const [titleString, errorString] =
variant === 'workflow'
? [
cannotLaunchWorkflowString,
workflowUnsupportedRequiredInputsString
]
: [cannotLaunchTaskString, taskUnsupportedRequiredInputsString];
return (
<NonIdealState
className={styles.errorContainer}
icon={ErrorOutline}
size="medium"
title={cannotLaunchWorkflowString}
title={titleString}
>
<div className={styles.contentContainer}>
<p>{unsupportedRequiredInputsString}</p>
<p>{errorString}</p>
<ul className={commonStyles.listUnstyled}>
{inputs.map(input => (
<li
Expand Down
2 changes: 1 addition & 1 deletion src/components/Launch/LaunchForm/__mocks__/mockInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const mockNestedCollectionVariables: Record<
type: { collectionType: v.type }
}));

export function createMockWorkflowInputsInterface(
export function createMockInputsInterface(
variables: Record<string, Variable>
): TypedInterface {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { mockWorkflowExecutionResponse } from 'models/Execution/__mocks__/mockWorkflowExecutionsData';
import * as React from 'react';
import {
createMockWorkflowInputsInterface,
createMockInputsInterface,
mockCollectionVariables,
mockNestedCollectionVariables,
mockSimpleVariables,
Expand All @@ -44,7 +44,7 @@ const submitAction = action('createWorkflowExecution');
const generateMocks = (variables: Record<string, Variable>) => {
const mockWorkflow = createMockWorkflow('MyWorkflow');
mockWorkflow.closure = createMockWorkflowClosure();
mockWorkflow.closure!.compiledWorkflow!.primary.template.interface = createMockWorkflowInputsInterface(
mockWorkflow.closure!.compiledWorkflow!.primary.template.interface = createMockInputsInterface(
variables
);

Expand Down Expand Up @@ -95,7 +95,7 @@ const generateMocks = (variables: Record<string, Variable>) => {
id
};
workflow.closure = createMockWorkflowClosure();
workflow.closure!.compiledWorkflow!.primary.template.interface = createMockWorkflowInputsInterface(
workflow.closure!.compiledWorkflow!.primary.template.interface = createMockInputsInterface(
variables
);

Expand Down
7 changes: 5 additions & 2 deletions src/components/Launch/LaunchForm/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export const formStrings = {
cancel: 'Cancel',
inputs: 'Inputs',
submit: 'Launch',
title: 'Launch Workflow',
taskVersion: 'Task Version',
title: 'Create New Execution',
workflowVersion: 'Workflow Version',
launchPlan: 'Launch Plan'
};
Expand Down Expand Up @@ -62,6 +63,8 @@ export const defaultBlobValue: BlobValue = {

export const requiredInputSuffix = '*';
export const cannotLaunchWorkflowString = 'Workflow cannot be launched';
export const unsupportedRequiredInputsString = `This Workflow version contains one or more required inputs which are not supported by Flyte Console and do not have default values specified in the Workflow definition or the selected Launch Plan.\n\nYou can launch this Workflow version with the Flyte CLI or by selecting a Launch Plan which provides values for the unsupported inputs.\n\nThe required inputs are :`;
export const cannotLaunchTaskString = 'Task cannot be launched';
export const workflowUnsupportedRequiredInputsString = `This Workflow version contains one or more required inputs which are not supported by Flyte Console and do not have default values specified in the Workflow definition or the selected Launch Plan.\n\nYou can launch this Workflow version with the Flyte CLI or by selecting a Launch Plan which provides values for the unsupported inputs.\n\nThe required inputs are :`;
export const taskUnsupportedRequiredInputsString = `This Task version contains one or more required inputs which are not supported by Flyte Console.\n\nYou can launch this Task version with the Flyte CLI instead.\n\nThe required inputs are :`;
export const blobUriHelperText = '(required) location of the data';
export const blobFormatHelperText = '(optional) csv, parquet, etc...';
37 changes: 35 additions & 2 deletions src/components/Launch/LaunchForm/getInputs.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { sortedObjectEntries } from 'common/utils';
import { LaunchPlan, Workflow } from 'models';
import { LaunchPlan, Task, Workflow } from 'models';
import { requiredInputSuffix } from './constants';
import { LiteralValueMap, ParsedInput } from './types';
import {
createInputCacheKey,
formatLabelWithType,
getInputDefintionForLiteralType,
getTaskInputs,
getWorkflowInputs
} from './utils';

// We use a non-empty string for the description to allow display components
// to depend on the existence of a value
const emptyDescription = ' ';

export function getInputs(
export function getInputsForWorkflow(
workflow: Workflow,
launchPlan: LaunchPlan,
initialValues: LiteralValueMap = new Map()
Expand Down Expand Up @@ -57,3 +58,35 @@ export function getInputs(
};
});
}

export function getInputsForTask(
task: Task,
initialValues: LiteralValueMap = new Map()
): ParsedInput[] {
if (!task) {
return [];
}

const taskInputs = getTaskInputs(task);
return sortedObjectEntries(taskInputs).map(value => {
const [name, { description = emptyDescription, type }] = value;
const typeDefinition = getInputDefintionForLiteralType(type);
const typeLabel = formatLabelWithType(name, typeDefinition);
const label = `${typeLabel}${requiredInputSuffix}`;
const inputKey = createInputCacheKey(name, typeDefinition);
const initialValue = initialValues.has(inputKey)
? initialValues.get(inputKey)
: undefined;

return {
description,
initialValue,
label,
name,
typeDefinition,
// Task inputs are always required, as there is no default provided
// by a parent workflow.
required: true
};
});
}
21 changes: 15 additions & 6 deletions src/components/Launch/LaunchForm/launchMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Identifier,
LaunchPlan,
NamedEntityIdentifier,
Task,
Workflow,
WorkflowExecutionIdentifier,
WorkflowId
Expand Down Expand Up @@ -30,7 +31,7 @@ export type SelectLaunchPlanEvent = {
};
export type WorkflowVersionOptionsLoadedEvent = DoneInvokeEvent<Workflow[]>;
export type LaunchPlanOptionsLoadedEvent = DoneInvokeEvent<LaunchPlan[]>;
export type TaskVersionOptionsLoadedEvent = DoneInvokeEvent<Identifier[]>;
export type TaskVersionOptionsLoadedEvent = DoneInvokeEvent<Task[]>;
export type ExecutionCreatedEvent = DoneInvokeEvent<
WorkflowExecutionIdentifier
>;
Expand Down Expand Up @@ -81,8 +82,9 @@ export interface WorkflowLaunchContext extends BaseLaunchContext {
}

export interface TaskLaunchContext extends BaseLaunchContext {
preferredTaskId?: Identifier;
taskVersion?: Identifier;
taskVersionOptions?: Identifier[];
taskVersionOptions?: Task[];
}

export enum LaunchState {
Expand Down Expand Up @@ -199,21 +201,28 @@ export type WorkflowLaunchTypestate =
| {
value: LaunchState.SELECT_WORKFLOW_VERSION;
context: WorkflowLaunchContext & {
sourceId: WorkflowId;
sourceId: NamedEntityIdentifier;
workflowVersionOptions: Workflow[];
};
}
| {
value: LaunchState.SELECT_LAUNCH_PLAN;
context: WorkflowLaunchContext & {
launchPlanOptions: LaunchPlan[];
sourceId: WorkflowId;
sourceId: NamedEntityIdentifier;
workflowVersionOptions: Workflow[];
};
};

// TODO:
export type TaskLaunchTypestate = BaseLaunchTypestate;
export type TaskLaunchTypestate =
| BaseLaunchTypestate
| {
value: LaunchState.SELECT_TASK_VERSION;
context: TaskLaunchContext & {
sourceId: NamedEntityIdentifier;
taskVersionOptions: Task[];
};
};

const defaultBaseContext: BaseLaunchContext = {
parsedInputs: [],
Expand Down
18 changes: 18 additions & 0 deletions src/components/Launch/LaunchForm/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RefObject } from 'react';
import { WorkflowLaunchContext } from './launchMachine';
import { LaunchFormInputsRef } from './types';

export async function validate(
formInputsRef: RefObject<LaunchFormInputsRef>,
{}: WorkflowLaunchContext
) {
if (formInputsRef.current === null) {
throw new Error('Unexpected empty form inputs ref');
}

if (!formInputsRef.current.validate()) {
throw new Error(
'Some inputs have errors. Please correct them before submitting.'
);
}
}
Loading

0 comments on commit b71b307

Please sign in to comment.