mirror of
https://github.com/ansible/awx.git
synced 2026-03-27 13:55:04 -02:30
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:
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js
Normal file
11
awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js
Normal 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
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user