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

Add support for Collections in the launch UI #30

Merged
merged 6 commits into from
Dec 18, 2019
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@types/js-yaml": "^3.10.1",
"@types/lodash": "^4.14.68",
"@types/long": "^3.0.32",
"@types/lossless-json": "^1.0.0",
"@types/memoize-one": "^4.1.0",
"@types/memory-fs": "^0.3.0",
"@types/node": "^9.6.6",
Expand Down Expand Up @@ -144,6 +145,7 @@
"intersection-observer": "^0.7.0",
"jest": "^24.9.0",
"lint-staged": "^7.0.4",
"lossless-json": "^1.0.3",
"memoize-one": "^5.0.0",
"moment": "^2.18.1",
"object-hash": "^1.3.1",
Expand Down
55 changes: 55 additions & 0 deletions src/components/Launch/LaunchWorkflowForm/CollectionInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { TextField } from '@material-ui/core';
import * as React from 'react';
import { InputChangeHandler, InputProps, InputType } from './types';
import { UnsupportedInput } from './UnsupportedInput';

function stringChangeHandler(onChange: InputChangeHandler) {
return ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
onChange(value);
};
}

/** Handles rendering of the input component for a Collection of SimpleType values*/
export const CollectionInput: React.FC<InputProps> = props => {
const {
label,
helperText,
onChange,
typeDefinition: { subtype },
value = ''
} = props;
if (!subtype) {
console.error(
'Unexpected missing subtype for collection input',
props.typeDefinition
);
return <UnsupportedInput {...props} />;
}
switch (subtype.type) {
case InputType.Blob:
case InputType.Boolean:
case InputType.Collection:
case InputType.Datetime:
case InputType.Duration:
case InputType.Error:
case InputType.Float:
case InputType.Integer:
case InputType.Map:
case InputType.String:
case InputType.Struct:
return (
<TextField
helperText={helperText}
fullWidth={true}
label={label}
multiline={true}
onChange={stringChangeHandler(onChange)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the input value to stringChangeHandler here get defined? Is it recursively referencing the onChange handler of the TextField? So, x = stringChangeHandler(x)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stringChangeHandler generates an event handler that will forward the value received to its argument. So in this case, the prop value ends up looking like:

onChange = ({ target: { value } }) => props.onChange(value); // props here are the props to CollectionInput

rowsMax={8}
value={value}
variant="outlined"
/>
);
default:
return <UnsupportedInput {...props} />;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
workflowSortFields
} from 'models';
import * as React from 'react';
import { CollectionInput } from './CollectionInput';
import { SearchableSelector } from './SearchableSelector';
import { SimpleInput } from './SimpleInput';
import { InputProps, InputType, LaunchWorkflowFormProps } from './types';
Expand Down Expand Up @@ -54,6 +55,7 @@ const useStyles = makeStyles((theme: Theme) => ({
function getComponentForInput(input: InputProps) {
switch (input.typeDefinition.type) {
case InputType.Collection:
return <CollectionInput {...input} />;
case InputType.Map:
case InputType.Schema:
case InputType.Unknown:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,88 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import { resolveAfter } from 'common/promiseUtils';
import { mockAPIContextValue } from 'components/data/__mocks__/apiContext';
import { APIContext } from 'components/data/apiContext';
import { Workflow } from 'models';
import { Admin } from 'flyteidl';
import { mapValues } from 'lodash';
import { Variable, Workflow } from 'models';
import { createMockLaunchPlan } from 'models/__mocks__/launchPlanData';
import {
createMockWorkflow,
createMockWorkflowClosure,
createMockWorkflowVersions
} from 'models/__mocks__/workflowData';
import { mockExecution } from 'models/Execution/__mocks__/mockWorkflowExecutionsData';
import * as React from 'react';
import { LaunchWorkflowForm } from '../LaunchWorkflowForm';
import { mockParameterMap, mockWorkflowInputsInterface } from './mockInputs';
import {
createMockWorkflowInputsInterface,
mockCollectionVariables,
mockNestedCollectionVariables,
mockSimpleVariables
} from './mockInputs';

const mockWorkflow = createMockWorkflow('MyWorkflow');
const mockLaunchPlan = createMockLaunchPlan(
mockWorkflow.id.name,
mockWorkflow.id.version
);
const submitAction = action('createWorkflowExecution');

const mockWorkflowVersions = createMockWorkflowVersions(
mockWorkflow.id.name,
10
);
const renderForm = (variables: Record<string, Variable>) => {
const mockWorkflow = createMockWorkflow('MyWorkflow');
const mockLaunchPlan = createMockLaunchPlan(
mockWorkflow.id.name,
mockWorkflow.id.version
);

const mockWorkflowVersions = createMockWorkflowVersions(
mockWorkflow.id.name,
10
);

mockLaunchPlan.closure!.expectedInputs = mockParameterMap;
const parameterMap = {
parameters: mapValues(variables, v => ({ var: v }))
};

const mockApi = mockAPIContextValue({
getLaunchPlan: () => resolveAfter(500, mockLaunchPlan),
getWorkflow: id => {
const workflow: Workflow = {
id
};
workflow.closure = createMockWorkflowClosure();
workflow.closure!.compiledWorkflow!.primary.template.interface = mockWorkflowInputsInterface;
mockLaunchPlan.closure!.expectedInputs = parameterMap;

return resolveAfter(500, workflow);
},
listWorkflows: () => resolveAfter(500, { entities: mockWorkflowVersions }),
listLaunchPlans: () => resolveAfter(500, { entities: [mockLaunchPlan] })
});
const mockApi = mockAPIContextValue({
createWorkflowExecution: input => {
console.log(input);
submitAction('See console for data');
return Promise.reject('Not implemented');
},
getLaunchPlan: () => resolveAfter(500, mockLaunchPlan),
getWorkflow: id => {
const workflow: Workflow = {
id
};
workflow.closure = createMockWorkflowClosure();
workflow.closure!.compiledWorkflow!.primary.template.interface = createMockWorkflowInputsInterface(
variables
);

const onClose = () => console.log('Close');
return resolveAfter(500, workflow);
},
listWorkflows: () =>
resolveAfter(500, { entities: mockWorkflowVersions }),
listLaunchPlans: () => resolveAfter(500, { entities: [mockLaunchPlan] })
});

const onClose = () => console.log('Close');

return (
<APIContext.Provider value={mockApi}>
<div style={{ width: 600, height: '95vh' }}>
<LaunchWorkflowForm
onClose={onClose}
workflowId={mockWorkflow.id}
/>
</div>
</APIContext.Provider>
);
};

const stories = storiesOf('Launch/LaunchWorkflowForm', module);
stories.addDecorator(story => (
<APIContext.Provider value={mockApi}>
<div style={{ width: 600, height: '95vh' }}>{story()}</div>
</APIContext.Provider>
));

stories.add('Basic', () => (
<LaunchWorkflowForm onClose={onClose} workflowId={mockWorkflow.id} />
));

stories.add('Simple', () => renderForm(mockSimpleVariables));
stories.add('Collections', () => renderForm(mockCollectionVariables));
stories.add('Nested Collections', () =>
renderForm(mockNestedCollectionVariables)
);
34 changes: 25 additions & 9 deletions src/components/Launch/LaunchWorkflowForm/__stories__/mockInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function simpleType(primitiveType: SimpleType, description?: string): Variable {
};
}

export const mockVariables: Record<string, Variable> = {
export const mockSimpleVariables: Record<string, Variable> = {
simpleString: simpleType(SimpleType.STRING, 'a simple string value'),
stringNoLabel: simpleType(SimpleType.STRING),
simpleInteger: simpleType(SimpleType.INTEGER, 'a simple integer value'),
Expand All @@ -32,12 +32,28 @@ export const mockVariables: Record<string, Variable> = {
// blob: {}
};

export const mockWorkflowInputsInterface: TypedInterface = {
inputs: {
variables: { ...mockVariables }
}
};
export const mockCollectionVariables: Record<string, Variable> = mapValues(
mockSimpleVariables,
v => ({
description: `A collection of: ${v.description}`,
type: { collectionType: v.type }
})
);

export const mockParameterMap: ParameterMap = {
parameters: mapValues(mockVariables, v => ({ var: v }))
};
export const mockNestedCollectionVariables: Record<
string,
Variable
> = mapValues(mockCollectionVariables, v => ({
description: `${v.description} (nested)`,
type: { collectionType: v.type }
}));

export function createMockWorkflowInputsInterface(
variables: Record<string, Variable>
): TypedInterface {
return {
inputs: {
variables: { ...variables }
}
};
}
Loading