From 524bb7ca60221c8de5f1633a5fc4808801db5f9e Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 May 2022 16:58:59 -0400 Subject: [PATCH 01/11] 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 fa59ec600..7d1453ed0 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -20,6 +20,7 @@ import { InputTypeDefinition, ParsedInput, SearchableVersion, + InputValue, } from './types'; /** Creates a unique cache key for an input based on its name and type. @@ -192,6 +193,19 @@ export function isEnterInputsState(state: BaseInterpretedLaunchState): boolean { ].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() }; + } +} + export function literalsToLiteralValueMap( literals: { [k: string]: Core.ILiteral; From 6f85de266e063f9f3934427ecde18b6e39074629 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 May 2022 04:35:41 -0400 Subject: [PATCH 02/11] fix: fix comments #none Signed-off-by: James --- .../src/components/Launch/LaunchForm/MapInput.tsx | 4 ++-- .../src/components/Launch/LaunchForm/inputHelpers/map.ts | 9 ++++----- packages/zapp/console/src/components/common/strings.ts | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index aeaca4e96..ef19029e3 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -42,14 +42,14 @@ export const MapInput: React.FC = (props) => { const handleKeyChange = React.useCallback( (e: React.ChangeEvent) => { - onChange(JSON.stringify({ [e.target.value || '']: mapValue })); + onChange(JSON.stringify({ [e.target.value ?? '']: mapValue })); }, [mapValue], ); const handleValueChange = React.useCallback( (e: React.ChangeEvent) => { - onChange(JSON.stringify({ [mapKey]: e.target.value || '' })); + onChange(JSON.stringify({ [mapKey]: e.target.value ?? '' })); }, [mapKey], ); diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts index 515c0a7ae..1ceadd919 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts @@ -3,12 +3,11 @@ 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'; +import t from '../../../common/strings'; function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): InputValue { if (!subtype) { - throw new Error(missingSubTypeError); + throw new Error(t('missingMapSubType')); } if (!literal.map) { throw new Error('Map literal missing `map` property'); @@ -32,7 +31,7 @@ function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core.ILiteral { if (!subtype) { - throw new Error(missingSubTypeError); + throw new Error(t('missingMapSubType')); } const obj = JSON.parse(value.toString()); const key = Object.keys(obj)?.[0]; @@ -46,7 +45,7 @@ function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) { if (!subtype) { - throw new Error(missingSubTypeError); + throw new Error(t('missingMapSubType')); } if (typeof value !== 'string') { throw new Error('Value is not a string'); diff --git a/packages/zapp/console/src/components/common/strings.ts b/packages/zapp/console/src/components/common/strings.ts index 13e17f455..8cf4c5945 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', + missingMapSubType: 'Unexpected missing subtype for map', }; export { patternKey } from '@flyteconsole/locale'; From 1c55e848f7d28b3c19ca69d2676a5637405897ba Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 May 2022 08:29:44 -0400 Subject: [PATCH 03/11] fix: fix unit test #none Signed-off-by: James --- .../Launch/LaunchForm/inputHelpers/test/testCases.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts index e2060a982..8f708d24e 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts @@ -107,12 +107,12 @@ export const supportedPrimitives: InputTypeDefinition[] = [ inputTypes.integer, inputTypes.schema, inputTypes.struct, + inputTypes.map, ]; export const unsupportedTypes: InputTypeDefinition[] = [ inputTypes.binary, inputTypes.error, - inputTypes.map, inputTypes.none, ]; @@ -194,6 +194,10 @@ export const validityTestCases = { Long.MIN_VALUE, ], }, + map: { + invalid: ['a', {}, true, new Date(), 1.1, 0 / 0, '1.1', '1a'], + valid: [{ a: '1' }, { a: [1, 2, 3] }, { a: { b: 'c' } }], + }, // schema is just a specialized string input, so it has the same validity cases as string schema: { invalid: [123, true, new Date(), {}], valid: ['', 'abcdefg'] }, string: { invalid: [123, true, new Date(), {}], valid: ['', 'abcdefg'] }, From c91eee7a2a2a1fbcd858f121cc0b6cd75f0fcc8f Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 May 2022 22:43:00 -0400 Subject: [PATCH 04/11] fix: add string constants #none Signed-off-by: James --- .../Launch/LaunchForm/inputHelpers/map.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts index 1ceadd919..75e9c3d42 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts @@ -10,16 +10,16 @@ function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): throw new Error(t('missingMapSubType')); } if (!literal.map) { - throw new Error('Map literal missing `map` property'); + throw new Error(t('mapMissingMapProperty')); } if (!literal.map.literals) { - throw new Error('Map literal missing `map.literals` property'); + throw new Error(t('mapMissingMapLiteralsProperty')); } if (typeof literal.map.literals !== 'object') { - throw new Error('Map literal is not an object'); + throw new Error(t('mapLiternalNotObject')); } if (!Object.keys(literal.map.literals).length) { - throw new Error('Map literal object is empty'); + throw new Error(t('mapLiternalObjectEmpty')); } const key = Object.keys(literal.map.literals)[0]; @@ -48,19 +48,19 @@ function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) throw new Error(t('missingMapSubType')); } if (typeof value !== 'string') { - throw new Error('Value is not a string'); + throw new Error(t('valueNotString')); } if (!value.toString().length) { - throw new Error('Value is required'); + throw new Error(t('valueRequired')); } try { JSON.parse(value.toString()); } catch (e) { - throw new Error(`Value did not parse to an object`); + throw new Error(t('valueNotParse')); } 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"); + throw new Error(t('valueKeyRequired')); } const key = Object.keys(obj)[0]; const helper = getHelperForInput(subtype.type); @@ -69,7 +69,7 @@ function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) try { helper.validate({ value: subValue, typeDefinition: subtype, name: '', required: false }); } catch (e) { - throw new Error("Value's value is invalid"); + throw new Error(t('valueValueInvalid')); } } From f1a2a4599e590f61e9924fe44e4d23075e75f012 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 26 May 2022 09:21:56 -0400 Subject: [PATCH 05/11] fix: added test for mapInputHelper #none Signed-off-by: James --- .../Launch/LaunchForm/__mocks__/utils.ts | 28 ++++++ .../Launch/LaunchForm/inputHelpers/map.ts | 30 +++++- .../inputHelpers/test/inputHelpers.test.ts | 91 +++++++++++++++++++ .../LaunchForm/inputHelpers/test/testCases.ts | 8 +- .../Launch/LaunchForm/inputHelpers/utils.ts | 6 +- .../console/src/components/common/strings.ts | 9 ++ 6 files changed, 162 insertions(+), 10 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/__mocks__/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/__mocks__/utils.ts index b6ccd6281..73f1356ea 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/__mocks__/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/__mocks__/utils.ts @@ -51,3 +51,31 @@ export function nestedCollectionInputTypeDefinition( }, }; } + +export function mapInputTypeDefinition(typeDefinition: InputTypeDefinition): InputTypeDefinition { + return { + literalType: { + mapValueType: typeDefinition.literalType, + }, + type: InputType.Map, + subtype: typeDefinition, + }; +} + +export function nestedMapInputTypeDefinition( + typeDefinition: InputTypeDefinition, +): InputTypeDefinition { + return { + literalType: { + mapValueType: { + mapValueType: typeDefinition.literalType, + }, + }, + type: InputType.Map, + subtype: { + literalType: { mapValueType: typeDefinition.literalType }, + type: InputType.Map, + subtype: typeDefinition, + }, + }; +} diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts index 75e9c3d42..fe2834639 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts @@ -4,6 +4,16 @@ import { InputTypeDefinition, InputValue } from '../types'; import { getHelperForInput } from './getHelperForInput'; import { ConverterInput, InputHelper, InputValidatorParams } from './types'; import t from '../../../common/strings'; +import { parseJSON } from './parseJson'; +import { literalNone } from './constants'; + +function parseMap(map: string) { + const parsed = parseJSON(map); + if (typeof parsed !== 'object') { + throw new Error(t('valueNotParse')); + } + return parsed; +} function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): InputValue { if (!subtype) { @@ -33,13 +43,23 @@ function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core if (!subtype) { throw new Error(t('missingMapSubType')); } - const obj = JSON.parse(value.toString()); - const key = Object.keys(obj)?.[0]; + let parsed: { [key: string]: any }; + // If we're processing a nested map, it may already have been parsed + if (typeof value === 'object') { + parsed = value; + } else { + const stringValue = typeof value === 'string' ? value : value.toString(); + if (!stringValue.length) { + return literalNone(); + } + parsed = parseMap(stringValue); + } + const key = Object.keys(parsed)?.[0]; const helper = getHelperForInput(subtype.type); return { - map: { literals: { [key]: helper.toLiteral({ value: obj[key], typeDefinition: subtype }) } }, + map: { literals: { [key]: helper.toLiteral({ value: parsed[key], typeDefinition: subtype }) } }, }; } @@ -54,11 +74,11 @@ function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) throw new Error(t('valueRequired')); } try { - JSON.parse(value.toString()); + parseMap(value); } catch (e) { throw new Error(t('valueNotParse')); } - const obj = JSON.parse(value.toString()); + const obj = parseJSON(value); if (!Object.keys(obj).length || !Object.keys(obj)[0].trim().length) { throw new Error(t('valueKeyRequired')); } diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/inputHelpers.test.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/inputHelpers.test.ts index a6a2c47bf..d84d83318 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/inputHelpers.test.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/inputHelpers.test.ts @@ -5,7 +5,9 @@ import { BlobDimensionality, SimpleType } from 'models/Common/types'; import { InputProps, InputType, InputTypeDefinition } from '../../types'; import { collectionInputTypeDefinition, + mapInputTypeDefinition, nestedCollectionInputTypeDefinition, + nestedMapInputTypeDefinition, primitiveLiteral, } from '../../__mocks__/utils'; import { literalNone } from '../constants'; @@ -34,6 +36,22 @@ function makeSimpleInput(typeDefinition: InputTypeDefinition, value: any): Input return { ...baseInputProps, value, typeDefinition }; } +function makeMapInput(typeDefinition: InputTypeDefinition, value: string): InputProps { + return { + ...baseInputProps, + value, + typeDefinition: mapInputTypeDefinition(typeDefinition), + }; +} + +function makeNestedMapInput(typeDefinition: InputTypeDefinition, value: string): InputProps { + return { + ...baseInputProps, + value, + typeDefinition: nestedMapInputTypeDefinition(typeDefinition), + }; +} + function makeCollectionInput(typeDefinition: InputTypeDefinition, value: string): InputProps { return { ...baseInputProps, @@ -66,6 +84,36 @@ describe('literalToInputValue', () => { expect(literalToInputValue(inputTypes.none, literalNone())).toEqual(undefined)); }); + describe('Map', () => { + literalToInputTestCases.map(([typeDefinition, input, output]) => { + it(`should correctly convert map of ${typeDefinition.type}: ${stringifyValue(input)}`, () => { + const map: Core.ILiteral = { + map: { + literals: { a: input }, + }, + }; + const expectedString = stringifyValue({ a: output }); + const result = literalToInputValue(mapInputTypeDefinition(typeDefinition), map); + expect(result).toEqual(expectedString); + }); + }); + + it('should return empty for noneType literals', () => { + const map: Core.ILiteral = { + map: { + literals: { a: literalNone() }, + }, + }; + + const typeDefinition: InputTypeDefinition = { + literalType: { simple: Core.SimpleType.NONE }, + type: InputType.None, + }; + + expect(literalToInputValue(mapInputTypeDefinition(typeDefinition), map)).toEqual('{}'); + }); + }); + describe('Collections', () => { literalToInputTestCases.map(([typeDefinition, input, output]) => { it(`should correctly convert collection of ${typeDefinition.type}: ${stringifyValue( @@ -128,6 +176,49 @@ describe('inputToLiteral', () => { }); }); + describe('Map', () => { + literalTestCases.map(([typeDefinition, input, output]) => { + let singleMapValue: any; + let nestedMapValue: any; + if (typeDefinition.type === InputType.Struct) { + const objValue = JSON.parse(input); + singleMapValue = stringifyValue({ a: objValue }); + nestedMapValue = stringifyValue({ a: { b: objValue } }); + } else if (['boolean', 'number'].includes(typeof input)) { + singleMapValue = `{"a":${input}}`; + nestedMapValue = `{"a":{"b":${input}}}`; + } else if (input == null) { + singleMapValue = '{"a":null}'; + nestedMapValue = '{"a":{"b":null}}'; + } else if (typeof input === 'string' || Long.isLong(input)) { + singleMapValue = `{"a":"${input}"}`; + nestedMapValue = `{"a":{"b":"${input}"}}`; + } else if (input instanceof Date) { + const dateString = input.toISOString(); + singleMapValue = `{"a":"${dateString}"}`; + nestedMapValue = `{"a":{"b":"${dateString}"}}`; + } else { + const stringValue = stringifyValue(input); + singleMapValue = `{"a":${stringValue}}`; + nestedMapValue = `{"a":{"b":${stringValue}}}`; + } + + it(`should correctly convert map of type ${ + typeDefinition.type + }: ${singleMapValue} (${typeof input})`, () => { + const result = inputToLiteral(makeMapInput(typeDefinition, singleMapValue)); + expect(result.map!.literals!.a).toEqual(output); + }); + + it(`should correctly convert nested map of type ${ + typeDefinition.type + }: ${nestedMapValue} (${typeof input})`, () => { + const result = inputToLiteral(makeNestedMapInput(typeDefinition, nestedMapValue)); + expect(result.map!.literals!.a.map!.literals!.b).toEqual(output); + }); + }); + }); + describe('Collections', () => { literalTestCases.map(([typeDefinition, input, output]) => { let singleCollectionValue: any; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts index 8f708d24e..0cb038323 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts @@ -67,6 +67,10 @@ export const inputTypes: Record = { literalType: { mapValueType: { simple: Core.SimpleType.STRING }, }, + subtype: { + literalType: { simple: Core.SimpleType.NONE }, + type: InputType.None, + }, type: InputType.Map, }, none: { @@ -194,10 +198,6 @@ export const validityTestCases = { Long.MIN_VALUE, ], }, - map: { - invalid: ['a', {}, true, new Date(), 1.1, 0 / 0, '1.1', '1a'], - valid: [{ a: '1' }, { a: [1, 2, 3] }, { a: { b: 'c' } }], - }, // schema is just a specialized string input, so it has the same validity cases as string schema: { invalid: [123, true, new Date(), {}], valid: ['', 'abcdefg'] }, string: { invalid: [123, true, new Date(), {}], valid: ['', 'abcdefg'] }, 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 3064bec47..2f989dba3 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/utils.ts @@ -46,8 +46,12 @@ export function typeIsSupported(typeDefinition: InputTypeDefinition): boolean { case InputType.Schema: case InputType.String: case InputType.Struct: - case InputType.Map: return true; + case InputType.Map: + if (!subtype) { + return false; + } + return typeIsSupported(subtype); case InputType.Collection: { if (!subtype) { console.error('Unexpected missing subtype for collection input', typeDefinition); diff --git a/packages/zapp/console/src/components/common/strings.ts b/packages/zapp/console/src/components/common/strings.ts index 8cf4c5945..97b6cad04 100644 --- a/packages/zapp/console/src/components/common/strings.ts +++ b/packages/zapp/console/src/components/common/strings.ts @@ -13,6 +13,15 @@ const str = { serviceAccountHeader: 'Service Account', noMatchingResults: 'No matching results', missingMapSubType: 'Unexpected missing subtype for map', + mapMissingMapProperty: 'Map literal missing `map` property', + mapMissingMapLiteralsProperty: 'Map literal missing `map.literals` property', + mapLiternalNotObject: 'Map literal is not an object', + mapLiternalObjectEmpty: 'Map literal object is empty', + valueNotString: 'Value is not a string', + valueRequired: 'Value is required', + valueNotParse: 'Value did not parse to an object', + valueKeyRequired: "Value's key is required", + valueValueInvalid: "Value's value is invalid", }; export { patternKey } from '@flyteconsole/locale'; From c7a681b92308ddc741617d3c87ff720cdff701ed Mon Sep 17 00:00:00 2001 From: James Date: Thu, 26 May 2022 12:42:31 -0400 Subject: [PATCH 06/11] fix: fix test for utils.test.ts #none Signed-off-by: James --- .../LaunchForm/inputHelpers/test/testCases.ts | 1 - .../inputHelpers/test/utils.test.ts | 22 +++++++++++++++++++ .../Launch/LaunchForm/test/constants.ts | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts index 0cb038323..569958b53 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/testCases.ts @@ -111,7 +111,6 @@ export const supportedPrimitives: InputTypeDefinition[] = [ inputTypes.integer, inputTypes.schema, inputTypes.struct, - inputTypes.map, ]; export const unsupportedTypes: InputTypeDefinition[] = [ diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/utils.test.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/utils.test.ts index d6f4129c9..bd3003d38 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/utils.test.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/test/utils.test.ts @@ -1,6 +1,8 @@ import { collectionInputTypeDefinition, + mapInputTypeDefinition, nestedCollectionInputTypeDefinition, + nestedMapInputTypeDefinition, } from '../../__mocks__/utils'; import { InputTypeDefinition } from '../../types'; import { typeIsSupported } from '../utils'; @@ -25,6 +27,16 @@ describe('Launch/inputHelpers/utils', () => { nestedCollectionInputTypeDefinition(typeDefinition), true, ]), + ...supportedPrimitives.map((typeDefinition) => [ + `supports 1-dimension map of type ${typeDefinition.type}`, + mapInputTypeDefinition(typeDefinition), + true, + ]), + ...supportedPrimitives.map((typeDefinition) => [ + `supports 2-dimension map of type: ${typeDefinition.type}`, + nestedMapInputTypeDefinition(typeDefinition), + true, + ]), ...unsupportedTypes.map((typeDefinition) => [ `does NOT support type ${typeDefinition.type}`, typeDefinition, @@ -40,6 +52,16 @@ describe('Launch/inputHelpers/utils', () => { nestedCollectionInputTypeDefinition(typeDefinition), false, ]), + ...unsupportedTypes.map((typeDefinition) => [ + `does NOT support 1-dimension map of type ${typeDefinition.type}`, + mapInputTypeDefinition(typeDefinition), + false, + ]), + ...unsupportedTypes.map((typeDefinition) => [ + `does NOT support 2-dimension map of type: ${typeDefinition.type}`, + nestedMapInputTypeDefinition(typeDefinition), + false, + ]), ]; cases.forEach(([description, value, expected]) => diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/test/constants.ts b/packages/zapp/console/src/components/Launch/LaunchForm/test/constants.ts index d86b78d91..16274440e 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/test/constants.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/test/constants.ts @@ -7,6 +7,7 @@ export const datetimeInputName = 'simpleDatetime'; export const integerInputName = 'simpleInteger'; export const binaryInputName = 'simpleBinary'; export const errorInputName = 'simpleError'; +export const mapInputName = 'simpleMap'; export const iamRoleString = 'arn:aws:iam::12345678:role/defaultrole'; export const k8sServiceAccountString = 'default-service-account'; From be21450b18870a8b265762737cca9a2f827a7413 Mon Sep 17 00:00:00 2001 From: Nastya <55718143+anrusina@users.noreply.github.com> Date: Fri, 27 May 2022 15:38:25 -0700 Subject: [PATCH 07/11] chore: trigger snyk re-run From 6f94527914ec3cd3ea0ce5f440d732f2e3f87234 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jun 2022 08:56:07 -0400 Subject: [PATCH 08/11] fix: multiple keys for mapped types; #none Signed-off-by: James --- .../components/Launch/LaunchForm/MapInput.tsx | 190 +++++++++------ .../Launch/LaunchForm/inputHelpers/map.ts | 42 ++-- .../src/components/Launch/LaunchForm/utils.ts | 18 +- yarn.lock | 226 +++++++++++++++++- 4 files changed, 375 insertions(+), 101 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index ef19029e3..f94392de6 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -1,13 +1,17 @@ -import { FormControl, FormHelperText, TextField } from '@material-ui/core'; +import { Button, FormControl, FormHelperText, IconButton, TextField } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import * as React from 'react'; +import RemoveIcon from '@material-ui/icons/Remove'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; import { requiredInputSuffix } from './constants'; -import { InputProps, InputType } from './types'; -import { formatType, getLaunchInputId, parseMappedTypeValue } from './utils'; +import { InputProps, InputType, InputValue } from './types'; +import { formatType, getLaunchInputId, parseMappedTypeValue, toMappedTypeValue } from './utils'; const useStyles = makeStyles((theme: Theme) => ({ formControl: { width: '100%', + marginTop: theme.spacing(1), }, controls: { width: '100%', @@ -21,6 +25,14 @@ const useStyles = makeStyles((theme: Theme) => ({ valueControl: { flexGrow: 1, }, + addButton: { + display: 'flex', + justifyContent: 'flex-end', + marginTop: theme.spacing(1), + }, + error: { + border: '1px solid #f44336', + }, })); /** Handles rendering of the input component for any primitive-type input */ @@ -33,78 +45,122 @@ export const MapInput: React.FC = (props) => { typeDefinition: { subtype }, } = props; const hasError = !!error; - const helperText = hasError ? error : props.helperText; const classes = useStyles(); + const [keyRefs, setKeyRefs] = React.useState([]); - const { key: mapKey, value: mapValue } = parseMappedTypeValue(value); + const [pairs, setPairs] = React.useState< + { + key: string; + value: string; + }[] + >([]); + const parsed = parseMappedTypeValue(value); + React.useEffect(() => { + setPairs(parsed); + }, [value]); const valueError = error?.startsWith("Value's value"); - const handleKeyChange = React.useCallback( - (e: React.ChangeEvent) => { - onChange(JSON.stringify({ [e.target.value ?? '']: mapValue })); - }, - [mapValue], - ); + const onAddItem = React.useCallback(() => { + setKeyRefs((refs) => [...refs, null]); + setPairs((pairs) => [...pairs, { key: '', value: '' }]); + }, []); - const handleValueChange = React.useCallback( - (e: React.ChangeEvent) => { - onChange(JSON.stringify({ [mapKey]: e.target.value ?? '' })); - }, - [mapKey], - ); + const onDeleteItem = React.useCallback((index) => { + setKeyRefs((refs) => [...refs.slice(0, index), ...refs.slice(index + 1)]); + setPairs((pairs) => [...pairs.slice(0, index), ...pairs.slice(index + 1)]); + }, []); - const keyControl = ( - - ); - let valueControl: JSX.Element; - - switch (subtype?.type) { - case InputType.String: - case InputType.Integer: - valueControl = ( - - ); - break; - default: - valueControl = ( - - ); - } + const onUpdate = (newPairs) => { + const newValue = toMappedTypeValue(newPairs); + setPairs(parseMappedTypeValue(newValue as InputValue)); + onChange(newValue); + }; return ( - -
- {keyControl} - {valueControl} -
- {helperText} -
+ + + {props.helperText} + {pairs.map(({ key: itemKey, value: itemValue }, index) => { + const keyControl = ( + (keyRefs[index] = ref)} + onBlur={() => { + onUpdate([ + ...pairs.slice(0, index), + { key: keyRefs[index].value, value: itemValue }, + ...pairs.slice(index + 1), + ]); + }} + defaultValue={itemKey} + variant="outlined" + className={classes.keyControl} + /> + ); + let valueControl: JSX.Element; + + switch (subtype?.type) { + case InputType.String: + case InputType.Integer: + valueControl = ( + ) => { + onUpdate([ + ...pairs.slice(0, index), + { key: itemKey, value: e.target.value ?? '' }, + ...pairs.slice(index + 1), + ]); + }} + value={itemValue} + variant="outlined" + className={classes.valueControl} + type={subtype.type === InputType.Integer ? 'number' : 'text'} + /> + ); + break; + default: + valueControl = ( + ) => { + onUpdate([ + ...pairs.slice(0, index), + { key: itemKey, value: e.target.value ?? '' }, + ...pairs.slice(index + 1), + ]); + }} + value={itemValue} + variant="outlined" + multiline + className={classes.valueControl} + /> + ); + } + + return ( + +
+ {keyControl} + {valueControl} + onDeleteItem(index)}> + + +
+
+ ); + })} +
+ +
+ {hasError && ( + {error} + )} +
+
); }; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts index fe2834639..840fe8996 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/inputHelpers/map.ts @@ -32,11 +32,14 @@ function fromLiteral(literal: Core.ILiteral, { subtype }: InputTypeDefinition): throw new Error(t('mapLiternalObjectEmpty')); } - const key = Object.keys(literal.map.literals)[0]; - const childLiteral = literal.map.literals[key]; - const helper = getHelperForInput(subtype.type); + const result = {}; - return stringifyValue({ [key]: helper.fromLiteral(childLiteral, subtype) }); + Object.entries(literal.map.literals).forEach(([key, childLiteral]) => { + const helper = getHelperForInput(subtype.type); + result[key] = helper.fromLiteral(childLiteral, subtype); + }); + + return stringifyValue(result); } function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core.ILiteral { @@ -54,13 +57,13 @@ function toLiteral({ value, typeDefinition: { subtype } }: ConverterInput): Core } parsed = parseMap(stringValue); } - const key = Object.keys(parsed)?.[0]; - - const helper = getHelperForInput(subtype.type); + const result = { map: { literals: {} } }; + Object.keys(parsed)?.forEach((key) => { + const helper = getHelperForInput(subtype.type); + result.map.literals[key] = helper.toLiteral({ value: parsed[key], typeDefinition: subtype }); + }); - return { - map: { literals: { [key]: helper.toLiteral({ value: parsed[key], typeDefinition: subtype }) } }, - }; + return result; } function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) { @@ -79,18 +82,19 @@ function validate({ value, typeDefinition: { subtype } }: InputValidatorParams) throw new Error(t('valueNotParse')); } const obj = parseJSON(value); - if (!Object.keys(obj).length || !Object.keys(obj)[0].trim().length) { + if (!Object.keys(obj).length || Object.keys(obj).some((key) => !key.trim().length)) { throw new Error(t('valueKeyRequired')); } - const key = Object.keys(obj)[0]; - const helper = getHelperForInput(subtype.type); - const subValue = obj[key]; + Object.keys(obj).forEach((key) => { + const helper = getHelperForInput(subtype.type); + const subValue = obj[key]; - try { - helper.validate({ value: subValue, typeDefinition: subtype, name: '', required: false }); - } catch (e) { - throw new Error(t('valueValueInvalid')); - } + try { + helper.validate({ value: subValue, typeDefinition: subtype, name: '', required: false }); + } catch (e) { + throw new Error(t('valueValueInvalid')); + } + }); } export const mapHelper: InputHelper = { diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts index 7d1453ed0..41b90232c 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -193,19 +193,25 @@ export function isEnterInputsState(state: BaseInterpretedLaunchState): boolean { ].some(state.matches); } -export function parseMappedTypeValue(value: InputValue): { key: string; value: string } { +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() }; + ? Object.keys(mapObj).map((key) => ({ key, value: mapObj[key] })) + : [{ key: '', value: value.toString() }]; } catch (e) { - return { key: '', value: value.toString() }; + return [{ key: '', value: value.toString() }]; } } +export function toMappedTypeValue(entries: { key: string; value: string }[]): string { + const result = {}; + entries.forEach( + ({ key, value }) => (result[key] = result[key] === undefined ? value : result[key]), + ); + return JSON.stringify(result); +} + export function literalsToLiteralValueMap( literals: { [k: string]: Core.ILiteral; diff --git a/yarn.lock b/yarn.lock index 66316da0d..e6813f00a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1647,6 +1647,21 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.2" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@flyteorg/flyteidl@1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@flyteorg/flyteidl/-/flyteidl-1.1.4.tgz#7a02d80e22c623817b6061f1f5e88ec243041cf1" @@ -5078,6 +5093,11 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-walk@^7.1.1, acorn-walk@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" @@ -5093,7 +5113,7 @@ acorn@^7.1.1, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0: +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: version "8.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== @@ -6041,6 +6061,15 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -6717,7 +6746,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: +chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -6725,7 +6754,7 @@ chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.0: +chalk@^5.0.0, chalk@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== @@ -6750,6 +6779,11 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chart.js@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.2.tgz#47342c551f688ffdda2cd53b534cb7e461ecec33" @@ -6952,6 +6986,18 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + cli-table3@^0.5.0, cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" @@ -6988,6 +7034,11 @@ cli-truncate@^0.2.1: slice-ansi "0.0.4" string-width "^1.0.1" +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -9331,6 +9382,47 @@ eslint@^8.11.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +eslint@^8.15.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.18.0.tgz#78d565d16c993d0b73968c523c0446b13da784fd" + integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA== + dependencies: + "@eslint/eslintrc" "^1.3.0" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.2" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + espree@^9.3.1: version "9.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" @@ -9340,6 +9432,15 @@ espree@^9.3.1: acorn-jsx "^5.3.1" eslint-visitor-keys "^3.3.0" +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== + dependencies: + acorn "^8.7.1" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -9629,6 +9730,15 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -10609,6 +10719,13 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== + dependencies: + type-fest "^0.20.2" + globals@^13.6.0, globals@^13.9.0: version "13.13.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" @@ -11305,7 +11422,7 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== -iconv-lite@0.4, iconv-lite@0.4.24: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -11532,6 +11649,27 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +inquirer@^8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -11898,6 +12036,11 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -12106,6 +12249,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-weakref@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -13428,7 +13576,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@^0.14.1: +listr@^0.14.1, listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -13702,6 +13850,14 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -14564,7 +14720,7 @@ multicast-dns@^7.2.4: dns-packet "^5.2.2" thunky "^1.0.2" -mute-stream@~0.0.4: +mute-stream@0.0.8, mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== @@ -14611,6 +14767,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ncp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -15573,6 +15734,21 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -15592,7 +15768,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -17810,6 +17986,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -17869,6 +18053,11 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.1.10" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" @@ -17893,6 +18082,13 @@ rxjs@^6.3.3: dependencies: tslib "^1.9.0" +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -19361,7 +19557,7 @@ through2@^4.0.0, through2@^4.0.2: dependencies: readable-stream "3" -through@2, "through@>=2.2.7 <3": +through@2, "through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -19408,6 +19604,13 @@ tinycolor2@^1.1.2, tinycolor2@^1.4.1, tinycolor2@^1.4.2: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -19618,6 +19821,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -20352,7 +20560,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -wcwidth@^1.0.0: +wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= From 72505a4c414f758f01691069f7ed68d5d4db0f74 Mon Sep 17 00:00:00 2001 From: Nastya <55718143+anrusina@users.noreply.github.com> Date: Thu, 30 Jun 2022 19:13:55 +0200 Subject: [PATCH 09/11] chore: storybook item (#530) Signed-off-by: Nastya Rusina --- .../components/Launch/LaunchForm/MapInput.tsx | 357 ++++++++++++------ .../__stories__/MapInput.stories.tsx | 26 ++ 2 files changed, 274 insertions(+), 109 deletions(-) create mode 100644 packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index f94392de6..f8f85cb28 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -5,7 +5,7 @@ import RemoveIcon from '@material-ui/icons/Remove'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import { requiredInputSuffix } from './constants'; -import { InputProps, InputType, InputValue } from './types'; +import { InputProps, InputType, InputTypeDefinition, InputValue } from './types'; import { formatType, getLaunchInputId, parseMappedTypeValue, toMappedTypeValue } from './utils'; const useStyles = makeStyles((theme: Theme) => ({ @@ -14,6 +14,7 @@ const useStyles = makeStyles((theme: Theme) => ({ marginTop: theme.spacing(1), }, controls: { + margin: theme.spacing(1), width: '100%', display: 'flex', alignItems: 'center', @@ -35,8 +36,64 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -/** Handles rendering of the input component for any primitive-type input */ -export const MapInput: React.FC = (props) => { +type MapInputItem = { + id: number | null; + key: string; + value: string; +}; + +interface MapInputItemProps { + data: MapInputItem; + subtype?: InputTypeDefinition; + setKey: (key: string) => void; + setValue: (value: string) => void; + isValid: (value: string) => boolean; + onDeleteItem: () => void; +} + +const MapSingleInputItem = (props: MapInputItemProps) => { + const classes = useStyles(); + const { data, subtype, setKey, setValue, isValid, onDeleteItem } = props; + // const [key, setKey] = React.useState(data.key); + // const [value, setValue] = React.useState(data.value); + const [error, setError] = React.useState(false); + + const isOneLineType = subtype?.type === InputType.String || subtype?.type === InputType.Integer; + + return ( +
+ ) => { + setKey(value); + setError(!!value && !isValid(value)); + }} + value={data.key} + error={error} + placeholder="key" + variant="outlined" + helperText={error ? 'This key already defined' : ''} + className={classes.keyControl} + /> + ) => { + setValue(value); + }} + value={data.value} + variant="outlined" + className={classes.valueControl} + multiline={!isOneLineType} + type={subtype?.type === InputType.Integer ? 'number' : 'text'} + /> + + + +
+ ); +}; + +export const MapInput = (props: InputProps) => { const { error, name, @@ -44,123 +101,205 @@ export const MapInput: React.FC = (props) => { value = '', typeDefinition: { subtype }, } = props; - const hasError = !!error; const classes = useStyles(); - const [keyRefs, setKeyRefs] = React.useState([]); - - const [pairs, setPairs] = React.useState< - { - key: string; - value: string; - }[] - >([]); - const parsed = parseMappedTypeValue(value); - React.useEffect(() => { - setPairs(parsed); - }, [value]); - - const valueError = error?.startsWith("Value's value"); - - const onAddItem = React.useCallback(() => { - setKeyRefs((refs) => [...refs, null]); - setPairs((pairs) => [...pairs, { key: '', value: '' }]); - }, []); - - const onDeleteItem = React.useCallback((index) => { - setKeyRefs((refs) => [...refs.slice(0, index), ...refs.slice(index + 1)]); - setPairs((pairs) => [...pairs.slice(0, index), ...pairs.slice(index + 1)]); - }, []); - - const onUpdate = (newPairs) => { + + const isValid = (id: number | null, value: string) => { + if (id === null) return true; + // findIndex returns -1 if value is not found, which means we can use that key + return ( + data + .filter((item) => item.id !== null && item.id !== id) + .findIndex((item) => item.key === value) === -1 + ); + }; + + const getNewItem = (id): MapInputItem => { + return { id, key: '', value: '' }; + }; + + const [data, setData] = React.useState([getNewItem(0)]); + + const onAddItem = () => { + setData((data) => [...data, getNewItem(data.length)]); + }; + + const updateUpperStream = () => { + const newPairs = data + .filter((item) => { + // we filter out delted values and items with errors or empty keys/values + return item.id !== null && !!item.key && !!item.value; + }) + .map((item) => { + return { + key: item.key, + value: item.value, + }; + }); const newValue = toMappedTypeValue(newPairs); - setPairs(parseMappedTypeValue(newValue as InputValue)); onChange(newValue); }; + const onSetKey = (id: number | null, key: string) => { + if (id === null) return; + setData((data) => { + data[id].key = key; + return [...data]; + }); + updateUpperStream(); + }; + + const onSetValue = (id: number | null, value: string) => { + if (id === null) return; + setData((data) => { + data[id].value = value; + return [...data]; + }); + updateUpperStream(); + }; + + const onDeleteItem = (id: number | null) => { + if (id === null) return; + setData((data) => { + const dataIndex = data.findIndex((item) => item.id === id); + if (dataIndex >= 0 && dataIndex < data.length) { + data[dataIndex].id = null; + } + return [...data]; + }); + updateUpperStream(); + }; + return ( - + - {props.helperText} - {pairs.map(({ key: itemKey, value: itemValue }, index) => { - const keyControl = ( - (keyRefs[index] = ref)} - onBlur={() => { - onUpdate([ - ...pairs.slice(0, index), - { key: keyRefs[index].value, value: itemValue }, - ...pairs.slice(index + 1), - ]); - }} - defaultValue={itemKey} - variant="outlined" - className={classes.keyControl} - /> - ); - let valueControl: JSX.Element; - - switch (subtype?.type) { - case InputType.String: - case InputType.Integer: - valueControl = ( - ) => { - onUpdate([ - ...pairs.slice(0, index), - { key: itemKey, value: e.target.value ?? '' }, - ...pairs.slice(index + 1), - ]); - }} - value={itemValue} - variant="outlined" - className={classes.valueControl} - type={subtype.type === InputType.Integer ? 'number' : 'text'} - /> - ); - break; - default: - valueControl = ( - ) => { - onUpdate([ - ...pairs.slice(0, index), - { key: itemKey, value: e.target.value ?? '' }, - ...pairs.slice(index + 1), - ]); - }} - value={itemValue} - variant="outlined" - multiline - className={classes.valueControl} - /> - ); - } - - return ( - -
- {keyControl} - {valueControl} - onDeleteItem(index)}> - - -
-
- ); - })} + {data + .filter((item) => item.id !== null) + .map((item) => { + return ( + onSetKey(item.id, key)} + setValue={(value) => onSetValue(item.id, value)} + isValid={(value) => isValid(item.id, value)} + onDeleteItem={() => onDeleteItem(item.id)} + /> + ); + })}
- {hasError && ( - {error} - )}
); }; + +// /** 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 classes = useStyles(); +// const [keyRefs, setKeyRefs] = React.useState([]); + +// const [pairs, setPairs] = React.useState< +// { +// key: string; +// value: string; +// }[] +// >([]); +// const parsed = parseMappedTypeValue(value); +// React.useEffect(() => { +// setPairs(parsed); +// }, [value]); + +// const valueError = error?.startsWith("Value's value"); + +// const onAddItem = React.useCallback(() => { +// setKeyRefs((refs) => [...refs, null]); +// setPairs((pairs) => [...pairs, { key: '', value: '' }]); +// }, []); + +// const onDeleteItem = React.useCallback((index) => { +// setKeyRefs((refs) => [...refs.slice(0, index), ...refs.slice(index + 1)]); +// setPairs((pairs) => [...pairs.slice(0, index), ...pairs.slice(index + 1)]); +// }, []); + +// const onUpdate = (newPairs) => { +// const newValue = toMappedTypeValue(newPairs); +// setPairs(parseMappedTypeValue(newValue as InputValue)); +// onChange(newValue); +// }; + +// return ( +// +// +// {props.helperText} +// {pairs.map(({ key: itemKey, value: itemValue }, index) => { +// const keyControl = ( +// (keyRefs[index] = ref)} +// onBlur={() => { +// onUpdate([ +// ...pairs.slice(0, index), +// { key: keyRefs[index].value, value: itemValue }, +// ...pairs.slice(index + 1), +// ]); +// }} +// defaultValue={itemKey} +// variant="outlined" +// className={classes.keyControl} +// /> +// ); + +// const isOneLineType = +// subtype?.type === InputType.String || subtype?.type === InputType.Integer; +// const valueControl = ( +// ) => { +// onUpdate([ +// ...pairs.slice(0, index), +// { key: itemKey, value: e.target.value ?? '' }, +// ...pairs.slice(index + 1), +// ]); +// }} +// value={itemValue} +// variant="outlined" +// className={classes.valueControl} +// multiline={!isOneLineType} +// type={subtype?.type === InputType.Integer ? 'number' : 'text'} +// /> +// ); + +// return ( +// +//
+// {keyControl} +// {valueControl} +// onDeleteItem(index)}> +// +// +//
+//
+// ); +// })} +//
+// +//
+// {hasError && ( +// {error} +// )} +//
+//
+// ); +// }; diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx new file mode 100644 index 000000000..73ee48456 --- /dev/null +++ b/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx @@ -0,0 +1,26 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { MapInput } from '../MapInput'; +import { InputValue } from '../types'; +import { inputTypes } from '../inputHelpers/test/testCases'; + +const stories = storiesOf('Launch/MapInput', module); + +stories.addDecorator((story) => { + return
{story()}
; +}); + +stories.add('Base', () => { + return ( + { + console.log('onChange', newValue); + }} + /> + ); +}); From 8398a1597f38ed766d0ad86cdd640fdb8b9dc01e Mon Sep 17 00:00:00 2001 From: James Date: Thu, 30 Jun 2022 13:15:19 -0400 Subject: [PATCH 10/11] fix: fix validation for duplicate and fix focus issue Signed-off-by: James --- .../components/Launch/LaunchForm/MapInput.tsx | 120 +----------------- 1 file changed, 4 insertions(+), 116 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index f8f85cb28..a61a42b69 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -1,12 +1,12 @@ -import { Button, FormControl, FormHelperText, IconButton, TextField } from '@material-ui/core'; +import { Button, IconButton, TextField } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import * as React from 'react'; import RemoveIcon from '@material-ui/icons/Remove'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import { requiredInputSuffix } from './constants'; -import { InputProps, InputType, InputTypeDefinition, InputValue } from './types'; -import { formatType, getLaunchInputId, parseMappedTypeValue, toMappedTypeValue } from './utils'; +import { InputProps, InputType, InputTypeDefinition } from './types'; +import { formatType, toMappedTypeValue } from './utils'; const useStyles = makeStyles((theme: Theme) => ({ formControl: { @@ -17,7 +17,7 @@ const useStyles = makeStyles((theme: Theme) => ({ margin: theme.spacing(1), width: '100%', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', flexDirection: 'row', }, keyControl: { @@ -95,10 +95,7 @@ const MapSingleInputItem = (props: MapInputItemProps) => { export const MapInput = (props: InputProps) => { const { - error, - name, onChange, - value = '', typeDefinition: { subtype }, } = props; const classes = useStyles(); @@ -194,112 +191,3 @@ export const MapInput = (props: InputProps) => {
); }; - -// /** 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 classes = useStyles(); -// const [keyRefs, setKeyRefs] = React.useState([]); - -// const [pairs, setPairs] = React.useState< -// { -// key: string; -// value: string; -// }[] -// >([]); -// const parsed = parseMappedTypeValue(value); -// React.useEffect(() => { -// setPairs(parsed); -// }, [value]); - -// const valueError = error?.startsWith("Value's value"); - -// const onAddItem = React.useCallback(() => { -// setKeyRefs((refs) => [...refs, null]); -// setPairs((pairs) => [...pairs, { key: '', value: '' }]); -// }, []); - -// const onDeleteItem = React.useCallback((index) => { -// setKeyRefs((refs) => [...refs.slice(0, index), ...refs.slice(index + 1)]); -// setPairs((pairs) => [...pairs.slice(0, index), ...pairs.slice(index + 1)]); -// }, []); - -// const onUpdate = (newPairs) => { -// const newValue = toMappedTypeValue(newPairs); -// setPairs(parseMappedTypeValue(newValue as InputValue)); -// onChange(newValue); -// }; - -// return ( -// -// -// {props.helperText} -// {pairs.map(({ key: itemKey, value: itemValue }, index) => { -// const keyControl = ( -// (keyRefs[index] = ref)} -// onBlur={() => { -// onUpdate([ -// ...pairs.slice(0, index), -// { key: keyRefs[index].value, value: itemValue }, -// ...pairs.slice(index + 1), -// ]); -// }} -// defaultValue={itemKey} -// variant="outlined" -// className={classes.keyControl} -// /> -// ); - -// const isOneLineType = -// subtype?.type === InputType.String || subtype?.type === InputType.Integer; -// const valueControl = ( -// ) => { -// onUpdate([ -// ...pairs.slice(0, index), -// { key: itemKey, value: e.target.value ?? '' }, -// ...pairs.slice(index + 1), -// ]); -// }} -// value={itemValue} -// variant="outlined" -// className={classes.valueControl} -// multiline={!isOneLineType} -// type={subtype?.type === InputType.Integer ? 'number' : 'text'} -// /> -// ); - -// return ( -// -//
-// {keyControl} -// {valueControl} -// onDeleteItem(index)}> -// -// -//
-//
-// ); -// })} -//
-// -//
-// {hasError && ( -// {error} -// )} -//
-//
-// ); -// }; From 9ef637a4697cb0478c6173d52eb79dda8140378a Mon Sep 17 00:00:00 2001 From: Nastya Rusina Date: Fri, 1 Jul 2022 08:30:23 +0200 Subject: [PATCH 11/11] chore: provide previous run values on relaunch Signed-off-by: Nastya Rusina --- .../components/Launch/LaunchForm/MapInput.tsx | 72 ++++++++++++------- .../__stories__/MapInput.stories.tsx | 4 +- .../src/components/Launch/LaunchForm/utils.ts | 11 --- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index a61a42b69..ae03a5b77 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -1,11 +1,11 @@ -import { Button, IconButton, TextField } from '@material-ui/core'; +import { Button, IconButton, TextField, Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import * as React from 'react'; import RemoveIcon from '@material-ui/icons/Remove'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import { requiredInputSuffix } from './constants'; -import { InputProps, InputType, InputTypeDefinition } from './types'; +import { InputProps, InputType, InputTypeDefinition, InputValue } from './types'; import { formatType, toMappedTypeValue } from './utils'; const useStyles = makeStyles((theme: Theme) => ({ @@ -36,12 +36,6 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -type MapInputItem = { - id: number | null; - key: string; - value: string; -}; - interface MapInputItemProps { data: MapInputItem; subtype?: InputTypeDefinition; @@ -54,8 +48,6 @@ interface MapInputItemProps { const MapSingleInputItem = (props: MapInputItemProps) => { const classes = useStyles(); const { data, subtype, setKey, setValue, isValid, onDeleteItem } = props; - // const [key, setKey] = React.useState(data.key); - // const [value, setValue] = React.useState(data.value); const [error, setError] = React.useState(false); const isOneLineType = subtype?.type === InputType.String || subtype?.type === InputType.Integer; @@ -93,31 +85,46 @@ const MapSingleInputItem = (props: MapInputItemProps) => { ); }; +type MapInputItem = { + id: number | null; + key: string; + value: string; +}; + +const getNewMapItem = (id, key = '', value = ''): MapInputItem => { + return { id, key, value }; +}; + +function parseMappedTypeValue(value?: InputValue): MapInputItem[] { + const fallback = [getNewMapItem(0)]; + if (!value) { + return fallback; + } + try { + const mapObj = JSON.parse(value.toString()); + if (typeof mapObj === 'object') { + return Object.keys(mapObj).map((key, index) => getNewMapItem(index, key, mapObj[key])); + } + } catch (e) { + // do nothing + } + + return fallback; +} + export const MapInput = (props: InputProps) => { const { + value, + label, onChange, typeDefinition: { subtype }, } = props; const classes = useStyles(); - const isValid = (id: number | null, value: string) => { - if (id === null) return true; - // findIndex returns -1 if value is not found, which means we can use that key - return ( - data - .filter((item) => item.id !== null && item.id !== id) - .findIndex((item) => item.key === value) === -1 - ); - }; - - const getNewItem = (id): MapInputItem => { - return { id, key: '', value: '' }; - }; - - const [data, setData] = React.useState([getNewItem(0)]); + const [data, setData] = React.useState(parseMappedTypeValue(value)); const onAddItem = () => { - setData((data) => [...data, getNewItem(data.length)]); + setData((data) => [...data, getNewMapItem(data.length)]); }; const updateUpperStream = () => { @@ -166,9 +173,22 @@ export const MapInput = (props: InputProps) => { updateUpperStream(); }; + const isValid = (id: number | null, value: string) => { + if (id === null) return true; + // findIndex returns -1 if value is not found, which means we can use that key + return ( + data + .filter((item) => item.id !== null && item.id !== id) + .findIndex((item) => item.key === value) === -1 + ); + }; + return ( + + {label} + {data .filter((item) => item.id !== null) .map((item) => { diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx index 73ee48456..4bea0afd5 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/__stories__/MapInput.stories.tsx @@ -14,8 +14,8 @@ stories.add('Base', () => { return ( { diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts index 41b90232c..05b65e54a 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts +++ b/packages/zapp/console/src/components/Launch/LaunchForm/utils.ts @@ -193,17 +193,6 @@ export function isEnterInputsState(state: BaseInterpretedLaunchState): boolean { ].some(state.matches); } -export function parseMappedTypeValue(value: InputValue): { key: string; value: string }[] { - try { - const mapObj = JSON.parse(value.toString()); - return typeof mapObj === 'object' - ? Object.keys(mapObj).map((key) => ({ key, value: mapObj[key] })) - : [{ key: '', value: value.toString() }]; - } catch (e) { - return [{ key: '', value: value.toString() }]; - } -} - export function toMappedTypeValue(entries: { key: string; value: string }[]): string { const result = {}; entries.forEach(