From b24250191e89b45534c09003efc8d05aed85e388 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 May 2022 16:58:59 -0400 Subject: [PATCH] fix: support mapped tasks #none Signed-off-by: James --- .../Launch/LaunchForm/LaunchFormInputs.tsx | 2 + .../components/Launch/LaunchForm/MapInput.tsx | 110 ++++++++++++++++++ .../inputHelpers/getHelperForInput.ts | 3 +- .../Launch/LaunchForm/inputHelpers/map.ts | 81 +++++++++++++ .../Launch/LaunchForm/inputHelpers/utils.ts | 2 +- .../src/components/Launch/LaunchForm/utils.ts | 14 +++ 6 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx create mode 100644 packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx index 63dcecab4..6a5169e94 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx @@ -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'; @@ -24,6 +25,7 @@ function getComponentForInput(input: InputProps, showErrors: boolean) { case InputType.Struct: return ; case InputType.Map: + return ; case InputType.Unknown: case InputType.None: return ; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx new file mode 100644 index 000000000..aeaca4e96 --- /dev/null +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -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 = (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) => { + onChange(JSON.stringify({ [e.target.value || '']: mapValue })); + }, + [mapValue], + ); + + const handleValueChange = React.useCallback( + (e: React.ChangeEvent) => { + onChange(JSON.stringify({ [mapKey]: e.target.value || '' })); + }, + [mapKey], + ); + + const keyControl = ( + + ); + let valueControl: JSX.Element; + + switch (subtype?.type) { + case InputType.String: + case InputType.Integer: + valueControl = ( + + ); + break; + default: + valueControl = ( + + ); + } + + return ( + +
+ {keyControl} + {valueControl} +
+ {helperText} +
+ ); +}; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts index 9b809ba90..819faad78 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts @@ -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'; @@ -26,7 +27,7 @@ const inputHelpers: Record = { [InputType.Error]: unsupportedHelper, [InputType.Float]: floatHelper, [InputType.Integer]: integerHelper, - [InputType.Map]: unsupportedHelper, + [InputType.Map]: mapHelper, [InputType.None]: noneHelper, [InputType.Schema]: schemaHelper, [InputType.String]: stringHelper, diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts new file mode 100644 index 000000000..515c0a7ae --- /dev/null +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts @@ -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, +}; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts index 2d3ee6971..3064bec47 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts @@ -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; @@ -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) { diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts index 2b8befe5d..9dcd3668d 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -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. @@ -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() }; + } +}