Merge prompt extra_vars before POSTing

* Merge the extra_vars field with survey question responses before sending
to API
* Clean up select and multi-select survey fields
This commit is contained in:
Keith Grant
2020-04-16 15:40:28 -07:00
parent 669d67b8fb
commit 08381577f5
7 changed files with 166 additions and 70 deletions

View File

@@ -8,19 +8,26 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px; margin-left: 10px;
`; `;
function FieldTooltip({ content }) { function FieldTooltip({ content, ...rest }) {
if (!content) {
return null;
}
return ( return (
<Tooltip <Tooltip
position="right" position="right"
content={content} content={content}
trigger="click mouseenter focus" trigger="click mouseenter focus"
{...rest}
> >
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
); );
} }
FieldTooltip.propTypes = { FieldTooltip.propTypes = {
content: node.isRequired, content: node,
};
FieldTooltip.defaultProps = {
content: null,
}; };
export default FieldTooltip; export default FieldTooltip;

View File

@@ -1,18 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useField } from 'formik'; import { useField } from 'formik';
import { import { FormGroup, TextInput, TextArea } from '@patternfly/react-core';
FormGroup, import FieldTooltip from './FieldTooltip';
TextInput,
TextArea,
Tooltip,
} from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;
`;
function FormField(props) { function FormField(props) {
const { const {
@@ -40,15 +30,7 @@ function FormField(props) {
isValid={isValid} isValid={isValid}
label={label} label={label}
> >
{tooltip && ( <FieldTooltip content={tooltip} maxWidth={tooltipMaxWidth} />
<Tooltip
content={tooltip}
maxWidth={tooltipMaxWidth}
position="right"
>
<QuestionCircleIcon />
</Tooltip>
)}
<TextArea <TextArea
id={id} id={id}
isRequired={isRequired} isRequired={isRequired}
@@ -69,15 +51,7 @@ function FormField(props) {
isValid={isValid} isValid={isValid}
label={label} label={label}
> >
{tooltip && ( <FieldTooltip content={tooltip} maxWidth={tooltipMaxWidth} />
<Tooltip
content={tooltip}
maxWidth={tooltipMaxWidth}
position="right"
>
<QuestionCircleIcon />
</Tooltip>
)}
<TextInput <TextInput
id={id} id={id}
isRequired={isRequired} isRequired={isRequired}

View File

@@ -8,6 +8,7 @@ import CredentialsStep from './CredentialsStep';
import OtherPromptsStep from './OtherPromptsStep'; import OtherPromptsStep from './OtherPromptsStep';
import SurveyStep from './SurveyStep'; import SurveyStep from './SurveyStep';
import PreviewStep from './PreviewStep'; import PreviewStep from './PreviewStep';
import mergeExtraVars from './mergeExtraVars';
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) { function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
const steps = []; const steps = [];
@@ -69,6 +70,7 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
}); });
} }
if (config.survey_enabled) { if (config.survey_enabled) {
initialValues.survey = {};
steps.push({ steps.push({
name: i18n._(t`Survey`), name: i18n._(t`Survey`),
component: <SurveyStep template={resource} />, component: <SurveyStep template={resource} />,
@@ -93,7 +95,7 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
setValue('limit', values.limit); setValue('limit', values.limit);
setValue('job_tags', values.job_tags); setValue('job_tags', values.job_tags);
setValue('skip_tags', values.skip_tags); setValue('skip_tags', values.skip_tags);
setValue('extra_vars', values.extra_vars); setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
onLaunch(postValues); onLaunch(postValues);
}; };

View File

@@ -1,9 +1,15 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { useField } from 'formik'; import { Formik, useField } from 'formik';
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api'; import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import { Form } from '@patternfly/react-core'; import {
import FormField from '@components/FormField'; Form,
FormGroup,
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core';
import FormField, { FieldTooltip } from '@components/FormField';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
@@ -38,6 +44,27 @@ function SurveyStep({ template, i18n }) {
return <ContentLoading />; return <ContentLoading />;
} }
const initialValues = {};
survey.spec.forEach(question => {
if (question.type === 'multiselect') {
initialValues[question.variable] = question.default.split('\n');
} else {
initialValues[question.variable] = question.default;
}
});
return (
<SurveySubForm survey={survey} initialValues={initialValues} i18n={i18n} />
);
}
function SurveySubForm({ survey, initialValues, i18n }) {
const [, , surveyFieldHelpers] = useField('survey');
useEffect(() => {
surveyFieldHelpers.setValue(initialValues);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);
const fieldTypes = { const fieldTypes = {
text: TextField, text: TextField,
textarea: TextField, textarea: TextField,
@@ -47,15 +74,24 @@ function SurveyStep({ template, i18n }) {
integer: NumberField, integer: NumberField,
float: NumberField, float: NumberField,
}; };
// This is a nested Formik form to perform validation on individual
// survey questions. When changes to the inner form occur (onBlur), the
// values for all questions are added to the outer form's `survey` field
// as a single object.
return ( return (
<Form> <Formik initialValues={initialValues}>
{survey.spec.map(question => { {({ values }) => (
const Field = fieldTypes[question.type]; <Form onBlur={() => surveyFieldHelpers.setValue(values)}>
return ( {' '}
<Field key={question.variable} question={question} i18n={i18n} /> {survey.spec.map(question => {
); const Field = fieldTypes[question.type];
})} return (
</Form> <Field key={question.variable} question={question} i18n={i18n} />
);
})}
</Form>
)}
</Formik>
); );
} }
@@ -101,36 +137,66 @@ function NumberField({ question, i18n }) {
); );
} }
function MultipleChoiceField({ question, i18n }) { function MultipleChoiceField({ question }) {
const [field, meta] = useField(question.question_name); const [field, meta] = useField(question.variable);
console.log(question, field); const id = `survey-question-${question.variable}`;
const isValid = !(meta.touched && meta.error);
return ( return (
<AnsibleSelect <FormGroup
id={`survey-question-${question.variable}`} fieldId={id}
isValid={!meta.errors} helperTextInvalid={meta.error}
{...field} isRequired={question.required}
data={question.choices.split('/n').map(opt => ({ isValid={isValid}
key: opt, label={question.question_name}
value: opt, >
label: opt, <FieldTooltip content={question.question_description} />
}))} <AnsibleSelect
/> id={id}
isValid={isValid}
{...field}
data={question.choices.split('\n').map(opt => ({
key: opt,
value: opt,
label: opt,
}))}
/>
</FormGroup>
); );
} }
function MultiSelectField({ question, i18n }) { function MultiSelectField({ question }) {
const [field, meta] = useField(question.question_name); const [isOpen, setIsOpen] = useState(false);
const [field, meta, helpers] = useField(question.variable);
const id = `survey-question-${question.variable}`;
const isValid = !(meta.touched && meta.error);
return ( return (
<AnsibleSelect <FormGroup
id={`survey-question-${question.variable}`} fieldId={id}
isValid={!meta.errors} helperTextInvalid={meta.error}
{...field} isRequired={question.required}
data={question.choices.split('/n').map(opt => ({ isValid={isValid}
key: opt, label={question.question_name}
value: opt, >
label: opt, <FieldTooltip content={question.question_description} />
}))} <Select
/> variant={SelectVariant.typeaheadMulti}
id={id}
onToggle={setIsOpen}
onSelect={(event, option) => {
if (field.value.includes(option)) {
helpers.setValue(field.value.filter(o => o !== option));
} else {
helpers.setValue(field.value.concat(option));
}
}}
isExpanded={isOpen}
selections={field.value}
>
{question.choices.split('\n').map(opt => (
<SelectOption key={opt} value={opt} />
))}
</Select>
</FormGroup>
); );
} }

View File

@@ -0,0 +1,11 @@
import yaml from 'js-yaml';
export default function mergeExtraVars(extraVars, survey = {}) {
const vars = yaml.safeLoad(extraVars) || {};
return {
...vars,
...survey,
};
}
// TODO: "safe" version that obscures passwords for preview step

View File

@@ -0,0 +1,34 @@
import mergeExtraVars from './mergeExtraVars';
describe('mergeExtraVars', () => {
test('should handle yaml string', () => {
const yaml = '---\none: 1\ntwo: 2';
expect(mergeExtraVars(yaml)).toEqual({
one: 1,
two: 2,
});
});
test('should handle json string', () => {
const jsonString = '{"one": 1, "two": 2}';
expect(mergeExtraVars(jsonString)).toEqual({
one: 1,
two: 2,
});
});
test('should handle empty string', () => {
expect(mergeExtraVars('')).toEqual({});
});
test('should merge survey results into extra vars object', () => {
const yaml = '---\none: 1\ntwo: 2';
const survey = { foo: 'bar', bar: 'baz' };
expect(mergeExtraVars(yaml, survey)).toEqual({
one: 1,
two: 2,
foo: 'bar',
bar: 'baz',
});
});
});

View File

@@ -322,6 +322,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
className="pf-c-backdrop" className="pf-c-backdrop"
> >
<FocusTrap <FocusTrap
_createFocusTrap={[Function]}
active={true} active={true}
className="pf-l-bullseye" className="pf-l-bullseye"
focusTrapOptions={ focusTrapOptions={
@@ -330,6 +331,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
} }
} }
paused={false} paused={false}
tag="div"
> >
<div <div
className="pf-l-bullseye" className="pf-l-bullseye"