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

feat: Add Task support to Launch form #101

Merged
merged 3 commits into from
Oct 6, 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
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