diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx index 6a5169e942..62817abb24 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/LaunchFormInputs.tsx @@ -8,6 +8,7 @@ import { MapInput } from './MapInput'; import { NoInputsNeeded } from './NoInputsNeeded'; import { SimpleInput } from './SimpleInput'; import { StructInput } from './StructInput'; +import { UnionInput } from './UnionInput'; import { useStyles } from './styles'; import { BaseInterpretedLaunchState, InputProps, InputType, LaunchFormInputsRef } from './types'; import { UnsupportedInput } from './UnsupportedInput'; @@ -15,9 +16,12 @@ import { UnsupportedRequiredInputsError } from './UnsupportedRequiredInputsError import { useFormInputsState } from './useFormInputsState'; import { isEnterInputsState } from './utils'; -function getComponentForInput(input: InputProps, showErrors: boolean) { +export function getComponentForInput(input: InputProps, showErrors: boolean) { const props = { ...input, error: showErrors ? input.error : undefined }; + switch (input.typeDefinition.type) { + case InputType.Union: + return ; case InputType.Blob: return ; case InputType.Collection: diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/SimpleInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/SimpleInput.tsx index f5077e2338..ccafe29f06 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/SimpleInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/SimpleInput.tsx @@ -68,6 +68,7 @@ export const SimpleInput: React.FC = (props) => { ({ + inlineTitle: { + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + paddingBottom: theme.spacing(3), + }, +})); + +const generateInputTypeToValueMap = ( + listOfSubTypes: InputTypeDefinition[] | undefined, + initialInputValue: UnionValue | undefined, + initialType: InputTypeDefinition, +): Record | {} => { + if (!listOfSubTypes?.length) { + return {}; + } + + return listOfSubTypes.reduce(function (map, subType) { + if (initialInputValue && subType === initialType) { + map[subType.type] = initialInputValue; + } else { + map[subType.type] = { value: '', typeDefinition: subType }; + } + return map; + }, {}); +}; + +const generateSearchableSelectorOption = ( + inputTypeDefinition: InputTypeDefinition, +): SearchableSelectorOption => { + return { + id: inputTypeDefinition.type, + data: inputTypeDefinition.type, + name: formatType(inputTypeDefinition), + } as SearchableSelectorOption; +}; + +const generateListOfSearchableSelectorOptions = ( + listOfInputTypeDefinition: InputTypeDefinition[], +): SearchableSelectorOption[] => { + return listOfInputTypeDefinition.map((inputTypeDefinition) => + generateSearchableSelectorOption(inputTypeDefinition), + ); +}; + +export const UnionInput = (props: InputProps) => { + const { initialValue, required, label, onChange, typeDefinition, error, description } = props; + + const classes = useStyles(); + + const listOfSubTypes = typeDefinition?.listOfSubTypes; + + if (!listOfSubTypes?.length) { + return <>; + } + + const inputTypeToInputTypeDefinition = listOfSubTypes.reduce( + (previous, current) => ({ ...previous, [current.type]: current }), + {}, + ); + + const initialInputValue = + initialValue && + (getHelperForInput(typeDefinition.type).fromLiteral( + initialValue, + typeDefinition, + ) as UnionValue); + + const initialInputTypeDefinition = initialInputValue?.typeDefinition ?? listOfSubTypes[0]; + + if (!initialInputTypeDefinition) { + return <>; + } + + const [inputTypeToValueMap, setInputTypeToValueMap] = React.useState< + Record | {} + >(generateInputTypeToValueMap(listOfSubTypes, initialInputValue, initialInputTypeDefinition)); + + const [selectedInputType, setSelectedInputType] = React.useState( + initialInputTypeDefinition.type, + ); + + const selectedInputTypeDefintion = inputTypeToInputTypeDefinition[ + selectedInputType + ] as InputTypeDefinition; + + const handleTypeOnSelectionChanged = (value: SearchableSelectorOption) => { + setSelectedInputType(value.data); + }; + + const handleSubTypeOnChange = (input: InputValue) => { + onChange({ + value: input, + typeDefinition: selectedInputTypeDefintion, + } as UnionValue); + setInputTypeToValueMap({ + ...inputTypeToValueMap, + [selectedInputType]: { + value: input, + typeDefinition: selectedInputTypeDefintion, + } as UnionValue, + }); + }; + + return ( + + +
+ + {label} + + + +
+ +
+ {getComponentForInput( + { + description: description, + name: `${formatType(selectedInputTypeDefintion)}`, + label: '', + required: required, + typeDefinition: selectedInputTypeDefintion, + onChange: handleSubTypeOnChange, + value: inputTypeToValueMap[selectedInputType]?.value, + error: error, + } as InputProps, + true, + )} +
+
+
+ ); +}; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/constants.ts b/packages/zapp/console/src/components/Launch/LaunchForm/constants.ts index 81a326c215..5b5081b042 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/constants.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/constants.ts @@ -45,6 +45,7 @@ export const typeLabels: { [k in InputType]: string } = { [InputType.Schema]: 'schema - uri', [InputType.String]: 'string', [InputType.Struct]: 'struct', + [InputType.Union]: 'union', [InputType.Unknown]: 'unknown', }; 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 819faad788..106f31bbee 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/getHelperForInput.ts @@ -11,6 +11,7 @@ import { noneHelper } from './none'; import { schemaHelper } from './schema'; import { stringHelper } from './string'; import { structHelper } from './struct'; +import { unionHelper } from './union'; import { InputHelper } from './types'; const unsupportedHelper = noneHelper; @@ -32,6 +33,7 @@ const inputHelpers: Record = { [InputType.Schema]: schemaHelper, [InputType.String]: stringHelper, [InputType.Struct]: structHelper, + [InputType.Union]: unionHelper, [InputType.Unknown]: unsupportedHelper, }; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/union.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/union.ts new file mode 100644 index 0000000000..a8e485a78c --- /dev/null +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/union.ts @@ -0,0 +1,65 @@ +import { Core } from 'flyteidl'; +import { isObject } from 'lodash'; +import { InputTypeDefinition, InputValue, UnionValue } from '../types'; +import { getHelperForInput } from './getHelperForInput'; +import { ConverterInput, InputHelper, InputValidatorParams } from './types'; +import t from '../../../common/strings'; + +function fromLiteral(literal: Core.ILiteral, inputTypeDefinition: InputTypeDefinition): InputValue { + const { listOfSubTypes } = inputTypeDefinition; + if (!listOfSubTypes?.length) { + throw new Error(t('missingUnionListOfSubType')); + } + + // loop though the subtypes to find the correct match literal type + for (let i = 0; i < listOfSubTypes.length; i++) { + try { + const value = getHelperForInput(listOfSubTypes[i].type).fromLiteral( + literal, + listOfSubTypes[i], + ); + return { value, typeDefinition: listOfSubTypes[i] } as UnionValue; + } catch (error) { + // do nothing here. it's expected to have error from fromLiteral + // because we loop through all the type to decode the input value + // the error should be something like this + // new Error(`Failed to extract literal value with path ${path}`); + } + } + throw new Error(t('noMatchingResults')); +} + +function toLiteral({ value, typeDefinition: { listOfSubTypes } }: ConverterInput): Core.ILiteral { + if (!listOfSubTypes) { + throw new Error(t('missingUnionListOfSubType')); + } + + if (!isObject(value)) { + throw new Error(t('valueMustBeObject')); + } + + const { value: unionValue, typeDefinition } = value as UnionValue; + + return getHelperForInput(typeDefinition.type).toLiteral({ + value: unionValue, + typeDefinition: typeDefinition, + } as ConverterInput); +} + +function validate({ value, typeDefinition: { listOfSubTypes } }: InputValidatorParams) { + if (!value) { + throw new Error(t('valueRequired')); + } + if (!isObject(value)) { + throw new Error(t('valueMustBeObject')); + } + + const { typeDefinition } = value as UnionValue; + getHelperForInput(typeDefinition.type).validate(value as InputValidatorParams); +} + +export const unionHelper: 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 2f989dba3e..83b570cbc2 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts @@ -1,6 +1,6 @@ import { assertNever, stringifyValue } from 'common/utils'; import { Core } from 'flyteidl'; -import { get } from 'lodash'; +import { get, has } from 'lodash'; import { BlobDimensionality } from 'models/Common/types'; import { InputType, InputTypeDefinition } from '../types'; @@ -8,11 +8,10 @@ import { InputType, InputTypeDefinition } from '../types'; * if the given property doesn't exist. */ export function extractLiteralWithCheck(literal: Core.ILiteral, path: string): T { - const value = get(literal, path); - if (value === undefined) { + if (!has(literal, path)) { throw new Error(`Failed to extract literal value with path ${path}`); } - return value as T; + return get(literal, path) as T; } /** Converts a value within a collection to the appropriate string @@ -29,7 +28,7 @@ export function collectionChildToString(type: InputType, value: any) { * supported for use in the Launch form. */ export function typeIsSupported(typeDefinition: InputTypeDefinition): boolean { - const { type, subtype } = typeDefinition; + const { type, subtype, listOfSubTypes } = typeDefinition; switch (type) { case InputType.Binary: case InputType.Error: @@ -47,6 +46,17 @@ export function typeIsSupported(typeDefinition: InputTypeDefinition): boolean { case InputType.String: case InputType.Struct: return true; + case InputType.Union: + if (listOfSubTypes?.length) { + var isSupported = true; + listOfSubTypes.forEach((subtype) => { + if (!typeIsSupported(subtype)) { + isSupported = false; + } + }); + return isSupported; + } + return false; case InputType.Map: if (!subtype) { return false; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/types.ts b/packages/zapp/console/src/components/Launch/LaunchForm/types.ts index 7c3ff2f523..e605a83f30 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/types.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/types.ts @@ -159,6 +159,7 @@ export enum InputType { Schema = 'SCHEMA', String = 'STRING', Struct = 'STRUCT', + Union = 'Union', Unknown = 'UNKNOWN', } @@ -166,6 +167,7 @@ export interface InputTypeDefinition { literalType: LiteralType; type: InputType; subtype?: InputTypeDefinition; + listOfSubTypes?: InputTypeDefinition[]; } export interface BlobValue { @@ -174,7 +176,12 @@ export interface BlobValue { uri: string; } -export type InputValue = string | number | boolean | Date | BlobValue; +export interface UnionValue { + value: InputValue; + typeDefinition: InputTypeDefinition; +} + +export type InputValue = string | number | boolean | Date | BlobValue | UnionValue; export type InputChangeHandler = (newValue: InputValue) => void; export interface InputProps { diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts index 05b65e54a5..2b508f65aa 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -75,13 +75,20 @@ export function getTaskInputs(task: Task): Record { /** Returns a formatted string based on an InputTypeDefinition. * ex. `string`, `string[]`, `map` */ -export function formatType({ type, subtype }: InputTypeDefinition): string { +export function formatType({ type, subtype, listOfSubTypes }: InputTypeDefinition): string { if (type === InputType.Collection) { return subtype ? `${formatType(subtype)}[]` : 'collection'; } if (type === InputType.Map) { return subtype ? `map` : 'map'; } + if (type === InputType.Union) { + if (!listOfSubTypes) return typeLabels[type]; + + const concatListOfSubTypes = listOfSubTypes.map((subtype) => formatType(subtype)).join(' | '); + + return `${typeLabels[type]} [${concatListOfSubTypes}]`; + } return typeLabels[type]; } @@ -157,6 +164,11 @@ export function getInputDefintionForLiteralType(literalType: LiteralType): Input result.type = simpleTypeToInputType[literalType.simple]; } else if (literalType.enumType) { result.type = InputType.Enum; + } else if (literalType.unionType) { + result.type = InputType.Union; + result.listOfSubTypes = literalType.unionType.variants?.map((variant) => + getInputDefintionForLiteralType(variant as LiteralType), + ); } return result; } diff --git a/packages/zapp/console/src/components/common/strings.ts b/packages/zapp/console/src/components/common/strings.ts index 97b6cad043..31d84397fe 100644 --- a/packages/zapp/console/src/components/common/strings.ts +++ b/packages/zapp/console/src/components/common/strings.ts @@ -12,6 +12,7 @@ const str = { securityContextHeader: 'Security Context', serviceAccountHeader: 'Service Account', noMatchingResults: 'No matching results', + missingUnionListOfSubType: 'Unexpected missing type for union', missingMapSubType: 'Unexpected missing subtype for map', mapMissingMapProperty: 'Map literal missing `map` property', mapMissingMapLiteralsProperty: 'Map literal missing `map.literals` property', @@ -22,6 +23,8 @@ const str = { valueNotParse: 'Value did not parse to an object', valueKeyRequired: "Value's key is required", valueValueInvalid: "Value's value is invalid", + valueMustBeObject: 'Value must be an object', + type: 'Type', }; export { patternKey } from '@flyteconsole/locale';