Skip to content

Commit

Permalink
fix: support mapped tasks #none
Browse files Browse the repository at this point in the history
  • Loading branch information
james-union committed May 23, 2022
1 parent dd74c71 commit e0699db
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BlobInput } from './BlobInput';
import { CollectionInput } from './CollectionInput';
import { formStrings, inputsDescription } from './constants';
import { LaunchState } from './launchMachine';
import { MapInput } from './MapInput';
import { NoInputsNeeded } from './NoInputsNeeded';
import { SimpleInput } from './SimpleInput';
import { StructInput } from './StructInput';
Expand All @@ -24,6 +25,7 @@ function getComponentForInput(input: InputProps, showErrors: boolean) {
case InputType.Struct:
return <StructInput {...props} />;
case InputType.Map:
return <MapInput {...props} />;
case InputType.Unknown:
case InputType.None:
return <UnsupportedInput {...props} />;
Expand Down
110 changes: 110 additions & 0 deletions packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { FormControl, FormHelperText, TextField } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import * as React from 'react';
import { requiredInputSuffix } from './constants';
import { InputProps, InputType } from './types';
import { formatType, getLaunchInputId, parseMappedTypeValue } from './utils';

const useStyles = makeStyles((theme: Theme) => ({
formControl: {
width: '100%',
},
controls: {
width: '100%',
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
},
keyControl: {
marginRight: theme.spacing(1),
},
valueControl: {
flexGrow: 1,
},
}));

/** Handles rendering of the input component for any primitive-type input */
export const MapInput: React.FC<InputProps> = (props) => {
const {
error,
name,
onChange,
value = '',
typeDefinition: { subtype },
} = props;
const hasError = !!error;
const helperText = hasError ? error : props.helperText;
const classes = useStyles();

const { key: mapKey, value: mapValue } = parseMappedTypeValue(value);

const valueError = error?.startsWith("Value's value");

const handleKeyChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(JSON.stringify({ [e.target.value || '']: mapValue }));
},
[mapValue],
);

const handleValueChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
onChange(JSON.stringify({ [mapKey]: e.target.value || '' }));
},
[mapKey],
);

const keyControl = (
<TextField
error={hasError && !valueError}
id={`${getLaunchInputId(name)}-key`}
label={`string${requiredInputSuffix}`}
onChange={handleKeyChange}
value={mapKey}
variant="outlined"
className={classes.keyControl}
/>
);
let valueControl: JSX.Element;

switch (subtype?.type) {
case InputType.String:
case InputType.Integer:
valueControl = (
<TextField
error={valueError}
id={`${getLaunchInputId(name)}-value`}
label={`${formatType(subtype)}${requiredInputSuffix}`}
onChange={handleValueChange}
value={mapValue}
variant="outlined"
className={classes.valueControl}
type={subtype.type === InputType.Integer ? 'number' : 'text'}
/>
);
break;
default:
valueControl = (
<TextField
error={valueError}
id={`${getLaunchInputId(name)}-value`}
label={subtype ? formatType(subtype) + requiredInputSuffix : ''}
onChange={handleValueChange}
value={mapValue}
variant="outlined"
multiline
className={classes.valueControl}
/>
);
}

return (
<FormControl className={classes.formControl}>
<div className={classes.controls}>
{keyControl}
{valueControl}
</div>
<FormHelperText id={`${getLaunchInputId(name)}-helper`}>{helperText}</FormHelperText>
</FormControl>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { datetimeHelper } from './datetime';
import { durationHelper } from './duration';
import { floatHelper } from './float';
import { integerHelper } from './integer';
import { mapHelper } from './map';
import { noneHelper } from './none';
import { schemaHelper } from './schema';
import { stringHelper } from './string';
Expand All @@ -26,7 +27,7 @@ const inputHelpers: Record<InputType, InputHelper> = {
[InputType.Error]: unsupportedHelper,
[InputType.Float]: floatHelper,
[InputType.Integer]: integerHelper,
[InputType.Map]: unsupportedHelper,
[InputType.Map]: mapHelper,
[InputType.None]: noneHelper,
[InputType.Schema]: schemaHelper,
[InputType.String]: stringHelper,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { stringifyValue } from 'common/utils';
import { Core } from 'flyteidl';
import { InputTypeDefinition, InputValue } from '../types';
import { getHelperForInput } from './getHelperForInput';
import { ConverterInput, InputHelper, InputValidatorParams } from './types';

const missingSubTypeError = 'Unexpected missing subtype for map';

function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): InputValue {
if (!subtype) {
throw new Error(missingSubTypeError);
}
if (!literal.map) {
throw new Error('Map literal missing `map` property');
}
if (!literal.map.literals) {
throw new Error('Map literal missing `map.literals` property');
}
if (typeof literal.map.literals !== 'object') {
throw new Error('Map literal is not an object');
}
if (!Object.keys(literal.map.literals).length) {
throw new Error('Map literal object is empty');
}

const key = Object.keys(literal.map.literals)[0];
const childLiteral = literal.map.literals[key];
const helper = getHelperForInput(subtype.type);

return stringifyValue({ [key]: helper.fromLiteral(childLiteral, subtype) });
}

function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core.ILiteral {
if (!subtype) {
throw new Error(missingSubTypeError);
}
const obj = JSON.parse(value.toString());
const key = Object.keys(obj)?.[0];

const helper = getHelperForInput(subtype.type);

return {
map: { literals: { [key]: helper.toLiteral({ value: obj[key], typeDefinition: subtype }) } },
};
}

function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) {
if (!subtype) {
throw new Error(missingSubTypeError);
}
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
if (!value.toString().length) {
throw new Error('Value is required');
}
try {
JSON.parse(value.toString());
} catch (e) {
throw new Error(`Value did not parse to an object`);
}
const obj = JSON.parse(value.toString());
if (!Object.keys(obj).length || !Object.keys(obj)[0].trim().length) {
throw new Error("Value's key is required");
}
const key = Object.keys(obj)[0];
const helper = getHelperForInput(subtype.type);
const subValue = obj[key];

try {
helper.validate({ value: subValue, typeDefinition: subtype, name: '', required: false });
} catch (e) {
throw new Error("Value's value is invalid");
}
}

export const mapHelper: InputHelper = {
fromLiteral,
toLiteral,
validate,
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function typeIsSupported(typeDefinition: InputTypeDefinition): boolean {
switch (type) {
case InputType.Binary:
case InputType.Error:
case InputType.Map:
case InputType.None:
case InputType.Unknown:
return false;
Expand All @@ -47,6 +46,7 @@ export function typeIsSupported(typeDefinition: InputTypeDefinition): boolean {
case InputType.Schema:
case InputType.String:
case InputType.Struct:
case InputType.Map:
return true;
case InputType.Collection: {
if (!subtype) {
Expand Down
14 changes: 14 additions & 0 deletions packages/zapp/console/src/components/Launch/LaunchForm/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InputTypeDefinition,
ParsedInput,
SearchableVersion,
InputValue,
} from './types';

/** Creates a unique cache key for an input based on its name and type.
Expand Down Expand Up @@ -190,3 +191,16 @@ export function isEnterInputsState(state: BaseInterpretedLaunchState): boolean {
LaunchState.SUBMIT_SUCCEEDED,
].some(state.matches);
}

export function parseMappedTypeValue(value: InputValue): { key: string; value: string } {
try {
const mapObj = JSON.parse(value.toString());
const mapKey = Object.keys(mapObj)?.[0] || '';
const mapValue = mapObj[mapKey] || '';
return typeof mapObj === 'object'
? { key: mapKey, value: mapValue }
: { key: '', value: value.toString() };
} catch (e) {
return { key: '', value: value.toString() };
}
}

0 comments on commit e0699db

Please sign in to comment.