validate variables field in launch prompt

This commit is contained in:
Keith J. Grant 2021-05-07 14:48:32 -07:00
parent 13e1fc9839
commit 83b6a91623
8 changed files with 174 additions and 92 deletions

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { string, bool } from 'prop-types';
import { string, bool, func, oneOf } from 'prop-types';
import { t } from '@lingui/macro';
import { useField } from 'formik';
@ -24,11 +24,20 @@ const StyledCheckboxField = styled(CheckboxField)`
margin-left: auto;
`;
function VariablesField({ id, name, label, readOnly, promptId, tooltip }) {
function VariablesField({
id,
name,
label,
readOnly,
promptId,
tooltip,
initialMode,
onModeChange,
}) {
// track focus manually, because the Code Editor library doesn't wire
// into Formik completely
const [shouldValidate, setShouldValidate] = useState(false);
const [mode, setMode] = useState(YAML_MODE);
const [mode, setMode] = useState(initialMode || YAML_MODE);
const validate = useCallback(
value => {
if (!shouldValidate) {
@ -54,6 +63,7 @@ function VariablesField({ id, name, label, readOnly, promptId, tooltip }) {
// mode's useState above couldn't be initialized to JSON_MODE because
// the field value had to be defined below it
setMode(JSON_MODE);
onModeChange(JSON_MODE);
helpers.setValue(JSON.stringify(JSON.parse(field.value), null, 2));
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
@ -76,6 +86,7 @@ function VariablesField({ id, name, label, readOnly, promptId, tooltip }) {
if (newMode === YAML_MODE && !isJsonEdited && lastYamlValue !== null) {
helpers.setValue(lastYamlValue, false);
setMode(newMode);
onModeChange(newMode);
return;
}
@ -86,6 +97,7 @@ function VariablesField({ id, name, label, readOnly, promptId, tooltip }) {
: yamlToJson(field.value);
helpers.setValue(newVal, false);
setMode(newMode);
onModeChange(newMode);
} catch (err) {
helpers.setError(err.message);
}
@ -163,10 +175,14 @@ VariablesField.propTypes = {
label: string.isRequired,
readOnly: bool,
promptId: string,
initialMode: oneOf([YAML_MODE, JSON_MODE]),
onModeChange: func,
};
VariablesField.defaultProps = {
readOnly: false,
promptId: null,
initialMode: YAML_MODE,
onModeChange: () => {},
};
function VariablesFieldInternals({
@ -189,7 +205,11 @@ function VariablesFieldInternals({
if (mode === YAML_MODE) {
return;
}
helpers.setValue(JSON.stringify(JSON.parse(field.value), null, 2));
try {
helpers.setValue(JSON.stringify(JSON.parse(field.value), null, 2));
} catch (e) {
helpers.setError(e.message);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (

View File

@ -13,7 +13,6 @@ import AlertModal from '../AlertModal';
function PromptModalForm({
launchConfig,
onCancel,
onSubmit,
resource,
@ -33,7 +32,6 @@ function PromptModalForm({
launchConfig,
surveyConfig,
resource,
resourceDefaultCredentials
);
@ -124,7 +122,6 @@ function PromptModalForm({
function LaunchPrompt({
launchConfig,
onCancel,
onLaunch,
resource = {},

View File

@ -20,7 +20,7 @@ const FieldHeader = styled.div`
}
`;
function OtherPromptsStep({ launchConfig }) {
function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
return (
<Form
onSubmit={e => {
@ -78,6 +78,8 @@ function OtherPromptsStep({ launchConfig }) {
id="prompt-variables"
name="extra_vars"
label={t`Variables`}
initialMode={variablesMode}
onModeChange={onVarModeChange}
/>
)}
</Form>

View File

@ -107,4 +107,29 @@ describe('OtherPromptsStep', () => {
true
);
});
test('should pass mode and onModeChange to VariablesField', async () => {
let wrapper;
const onModeChange = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<Formik initialValues={{ extra_vars: '{}' }}>
<OtherPromptsStep
variablesMode="javascript"
onVarModeChange={onModeChange}
launchConfig={{
ask_variables_on_launch: true,
}}
/>
</Formik>
);
});
expect(wrapper.find('VariablesField').prop('initialMode')).toEqual(
'javascript'
);
expect(wrapper.find('VariablesField').prop('onModeChange')).toEqual(
onModeChange
);
});
});

View File

@ -34,20 +34,24 @@ function PreviewStep({ resource, launchConfig, surveyConfig, formErrors }) {
};
if (launchConfig.ask_variables_on_launch || launchConfig.survey_enabled) {
const initialExtraVars =
launchConfig.ask_variables_on_launch && (overrides.extra_vars || '---');
if (surveyConfig?.spec) {
const passwordFields = surveyConfig.spec
.filter(q => q.type === 'password')
.map(q => q.variable);
const masked = maskPasswords(surveyValues, passwordFields);
overrides.extra_vars = yaml.safeDump(
mergeExtraVars(initialExtraVars, masked)
);
} else {
overrides.extra_vars = yaml.safeDump(
mergeExtraVars(initialExtraVars, {})
);
try {
const initialExtraVars =
launchConfig.ask_variables_on_launch && (overrides.extra_vars || '---');
if (surveyConfig?.spec) {
const passwordFields = surveyConfig.spec
.filter(q => q.type === 'password')
.map(q => q.variable);
const masked = maskPasswords(surveyValues, passwordFields);
overrides.extra_vars = yaml.safeDump(
mergeExtraVars(initialExtraVars, masked)
);
} else {
overrides.extra_vars = yaml.safeDump(
mergeExtraVars(initialExtraVars, {})
);
}
} catch (e) {
//
}
}

View File

@ -12,12 +12,7 @@ const InventoryAlert = styled(Alert)`
const STEP_ID = 'inventory';
export default function useInventoryStep(
launchConfig,
resource,
visitedSteps
) {
export default function useInventoryStep(launchConfig, resource, visitedSteps) {
const [, meta, helpers] = useField('inventory');
const formError =
!resource || resource?.type === 'workflow_job_template'

View File

@ -1,44 +1,77 @@
import React from 'react';
import React, { useState } from 'react';
import { t } from '@lingui/macro';
import { jsonToYaml, parseVariableField } from '../../../util/yaml';
import { useField } from 'formik';
import { jsonToYaml, yamlToJson } from '../../../util/yaml';
import OtherPromptsStep from './OtherPromptsStep';
import StepName from './StepName';
const STEP_ID = 'other';
export const YAML_MODE = 'yaml';
export const JSON_MODE = 'javascript';
const getVariablesData = resource => {
if (resource?.extra_data) {
return jsonToYaml(JSON.stringify(resource.extra_data));
}
if (resource?.extra_vars && resource?.extra_vars !== '---') {
return jsonToYaml(JSON.stringify(parseVariableField(resource.extra_vars)));
return resource.extra_vars;
}
return '---';
};
const FIELD_NAMES = [
'job_type',
'limit',
'verbosity',
'diff_mode',
'job_tags',
'skip_tags',
'extra_vars',
];
export default function useOtherPromptsStep(launchConfig, resource) {
const [variablesField] = useField('extra_vars');
const [variablesMode, setVariablesMode] = useState(null);
const [isTouched, setIsTouched] = useState(false);
const handleModeChange = mode => {
setVariablesMode(mode);
};
const validateVariables = () => {
if (!isTouched) {
return false;
}
try {
if (variablesMode === JSON_MODE) {
JSON.parse(variablesField.value);
} else {
yamlToJson(variablesField.value);
}
} catch (error) {
return true;
}
return false;
};
const hasError = launchConfig.ask_variables_on_launch
? validateVariables()
: false;
return {
step: getStep(launchConfig),
step: getStep(launchConfig, hasError, variablesMode, handleModeChange),
initialValues: getInitialValues(launchConfig, resource),
isReady: true,
contentError: null,
hasError: false,
hasError,
setTouched: setFieldTouched => {
[
'job_type',
'limit',
'verbosity',
'diff_mode',
'job_tags',
'skip_tags',
'extra_vars',
].forEach(field => setFieldTouched(field, true, false));
setIsTouched(true);
FIELD_NAMES.forEach(fieldName => setFieldTouched(fieldName, true, false));
},
validate: () => {},
};
}
function getStep(launchConfig) {
function getStep(launchConfig, hasError, variablesMode, handleModeChange) {
if (!shouldShowPrompt(launchConfig)) {
return null;
}
@ -46,11 +79,17 @@ function getStep(launchConfig) {
id: STEP_ID,
key: 5,
name: (
<StepName hasErrors={false} id="other-prompts-step">
<StepName hasErrors={hasError} id="other-prompts-step">
{t`Other prompts`}
</StepName>
),
component: <OtherPromptsStep launchConfig={launchConfig} />,
component: (
<OtherPromptsStep
launchConfig={launchConfig}
variablesMode={variablesMode}
onVarModeChange={handleModeChange}
/>
),
enableNext: true,
};
}

View File

@ -54,12 +54,10 @@ export default function useLaunchSteps(
launchConfig,
resource,
resourceDefaultCredentials,
true
),
useCredentialPasswordsStep(
launchConfig,
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
visited
),
@ -77,52 +75,54 @@ export default function useLaunchSteps(
const stepsAreReady = !steps.some(s => !s.isReady);
useEffect(() => {
if (stepsAreReady) {
const initialValues = steps.reduce((acc, cur) => {
return {
...acc,
...cur.initialValues,
};
}, {});
const newFormValues = { ...initialValues };
Object.keys(formikValues).forEach(formikValueKey => {
if (
formikValueKey === 'credential_passwords' &&
Object.prototype.hasOwnProperty.call(
newFormValues,
'credential_passwords'
)
) {
const formikCredentialPasswords = formikValues.credential_passwords;
Object.keys(formikCredentialPasswords).forEach(
credentialPasswordValueKey => {
if (
Object.prototype.hasOwnProperty.call(
newFormValues.credential_passwords,
credentialPasswordValueKey
)
) {
newFormValues.credential_passwords[credentialPasswordValueKey] =
formikCredentialPasswords[credentialPasswordValueKey];
}
}
);
} else if (
Object.prototype.hasOwnProperty.call(newFormValues, formikValueKey)
) {
newFormValues[formikValueKey] = formikValues[formikValueKey];
}
});
resetForm({
values: newFormValues,
touched,
});
setIsReady(true);
if (!stepsAreReady) {
return;
}
const initialValues = steps.reduce((acc, cur) => {
return {
...acc,
...cur.initialValues,
};
}, {});
const newFormValues = { ...initialValues };
Object.keys(formikValues).forEach(formikValueKey => {
if (
formikValueKey === 'credential_passwords' &&
Object.prototype.hasOwnProperty.call(
newFormValues,
'credential_passwords'
)
) {
const formikCredentialPasswords = formikValues.credential_passwords;
Object.keys(formikCredentialPasswords).forEach(
credentialPasswordValueKey => {
if (
Object.prototype.hasOwnProperty.call(
newFormValues.credential_passwords,
credentialPasswordValueKey
)
) {
newFormValues.credential_passwords[credentialPasswordValueKey] =
formikCredentialPasswords[credentialPasswordValueKey];
}
}
);
} else if (
Object.prototype.hasOwnProperty.call(newFormValues, formikValueKey)
) {
newFormValues[formikValueKey] = formikValues[formikValueKey];
}
});
resetForm({
values: newFormValues,
touched,
});
setIsReady(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formikValues.credentials, stepsAreReady]);