mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 13:55:31 +03:00
add survey form
This commit is contained in:
parent
e92acce4eb
commit
1412bf6232
41
awx/ui_next/package-lock.json
generated
41
awx/ui_next/package-lock.json
generated
@ -10625,7 +10625,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -10646,12 +10647,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -10666,17 +10669,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -10793,7 +10799,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -10805,6 +10812,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -10819,6 +10827,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -10826,12 +10835,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -10850,6 +10861,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -10930,7 +10942,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -10942,6 +10955,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -11027,7 +11041,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -11063,6 +11078,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -11082,6 +11098,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -11125,12 +11142,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8,6 +8,7 @@ import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import useRequest, { useDismissableError } from '@util/useRequest';
|
||||
import SurveyList from './shared/SurveyList';
|
||||
import SurveyQuestionAdd from './shared/SurveyQuestionAdd';
|
||||
|
||||
function TemplateSurvey({ template, i18n }) {
|
||||
const [surveyEnabled, setSurveyEnabled] = useState(template.survey_enabled);
|
||||
@ -37,6 +38,12 @@ function TemplateSurvey({ template, i18n }) {
|
||||
[template.id, setSurvey]
|
||||
)
|
||||
);
|
||||
const updateSurveySpec = spec => {
|
||||
updateSurvey({
|
||||
...survey,
|
||||
spec,
|
||||
});
|
||||
};
|
||||
|
||||
const { request: deleteSurvey, error: deleteError } = useRequest(
|
||||
useCallback(async () => {
|
||||
@ -64,13 +71,16 @@ function TemplateSurvey({ template, i18n }) {
|
||||
return (
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/templates/:templateType/:id/survey">
|
||||
<Route path="/templates/:templateType/:id/survey/add">
|
||||
<SurveyQuestionAdd survey={survey} updateSurvey={updateSurveySpec} />
|
||||
</Route>
|
||||
<Route path="/templates/:templateType/:id/survey" exact>
|
||||
<SurveyList
|
||||
isLoading={isLoading}
|
||||
survey={survey}
|
||||
surveyEnabled={surveyEnabled}
|
||||
toggleSurvey={toggleSurvey}
|
||||
updateSurvey={spec => updateSurvey({ ...survey, spec })}
|
||||
updateSurvey={updateSurveySpec}
|
||||
deleteSurvey={deleteSurvey}
|
||||
/>
|
||||
</Route>
|
||||
|
@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { CardBody } from '@components/Card';
|
||||
import SurveyQuestionForm from './SurveyQuestionForm';
|
||||
|
||||
export default function SurveyQuestionAdd({ template }) {
|
||||
const [formError, setFormError] = useState(null);
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
|
||||
const handleSubmit = async formData => {
|
||||
//
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(match.url.replace('/add', ''));
|
||||
};
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<SurveyQuestionForm
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
231
awx/ui_next/src/screens/Template/shared/SurveyQuestionForm.jsx
Normal file
231
awx/ui_next/src/screens/Template/shared/SurveyQuestionForm.jsx
Normal file
@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import { func, string, bool, number, shape } from 'prop-types';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import { FormColumnLayout } from '@components/FormLayout';
|
||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||
import FormField, {
|
||||
CheckboxField,
|
||||
PasswordField,
|
||||
FormSubmitError,
|
||||
FieldTooltip,
|
||||
} from '@components/FormField';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import { required, noWhiteSpace, combine } from '@util/validators';
|
||||
|
||||
function AnswerType({ i18n }) {
|
||||
const [field] = useField({
|
||||
name: 'type',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n._(t`Answer Type`)}
|
||||
isRequired
|
||||
fieldId="question-answer-type"
|
||||
>
|
||||
<FieldTooltip
|
||||
content={i18n._(
|
||||
t`Choose an answer type or format you want as the prompt for the user.
|
||||
Refer to the Ansible Tower Documentation for more additional
|
||||
information about each option.`
|
||||
)}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id="question-type"
|
||||
{...field}
|
||||
data={[
|
||||
{ key: 'text', value: 'text', label: i18n._(t`Text`) },
|
||||
{ key: 'textarea', value: 'textarea', label: i18n._(t`Textarea`) },
|
||||
{ key: 'password', value: 'password', label: i18n._(t`Password`) },
|
||||
{
|
||||
key: 'multiplechoice',
|
||||
value: 'multiplechoice',
|
||||
label: i18n._(t`Multiple Choice (single select)`),
|
||||
},
|
||||
{
|
||||
key: 'multiselect',
|
||||
value: 'multiselect',
|
||||
label: i18n._(t`Multiple Choice (multiple select)`),
|
||||
},
|
||||
{ key: 'integer', value: 'integer', label: i18n._(t`Integer`) },
|
||||
{ key: 'float', value: 'float', label: i18n._(t`Float`) },
|
||||
]}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function SurveyQuestionForm({
|
||||
question,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
submitError,
|
||||
i18n,
|
||||
}) {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
question_name: question?.question_name || '',
|
||||
question_description: question?.question_description || '',
|
||||
required: question ? question?.required : true,
|
||||
type: question?.type || 'text',
|
||||
variable: question?.variable || '',
|
||||
min: question?.min || 0,
|
||||
max: question?.max || 1024,
|
||||
default: question?.default,
|
||||
choices: question?.choices,
|
||||
new_question: !question,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<FormField
|
||||
id="question-name"
|
||||
name="question_name"
|
||||
type="text"
|
||||
label={i18n._(t`Question`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="question-description"
|
||||
name="question_description"
|
||||
type="text"
|
||||
label={i18n._(t`Description`)}
|
||||
/>
|
||||
<FormField
|
||||
id="question-variable"
|
||||
name="variable"
|
||||
type="text"
|
||||
label={i18n._(t`Answer Variable Name`)}
|
||||
validate={combine([noWhiteSpace(i18n), required(null, i18n)])}
|
||||
isRequired
|
||||
tooltip={i18n._(
|
||||
t`The suggested format for variable names is lowercase and
|
||||
underscore-separated (for example, foo_bar, user_id, host_name,
|
||||
etc.). Variable names with spaces are not allowed.`
|
||||
)}
|
||||
/>
|
||||
<AnswerType i18n={i18n} />
|
||||
<CheckboxField
|
||||
id="question-required"
|
||||
name="required"
|
||||
label="Required"
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
<FormColumnLayout>
|
||||
{['text', 'textarea', 'password'].includes(formik.values.type) && (
|
||||
<>
|
||||
<FormField
|
||||
id="question-min"
|
||||
name="min"
|
||||
type="number"
|
||||
label={i18n._(t`Minimum length`)}
|
||||
/>
|
||||
<FormField
|
||||
id="question-max"
|
||||
name="max"
|
||||
type="number"
|
||||
label={i18n._(t`Maximum length`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{['integer', 'float'].includes(formik.values.type) && (
|
||||
<>
|
||||
<FormField
|
||||
id="question-min"
|
||||
name="min"
|
||||
type="number"
|
||||
label={i18n._(t`Minimum`)}
|
||||
/>
|
||||
<FormField
|
||||
id="question-max"
|
||||
name="max"
|
||||
type="number"
|
||||
label={i18n._(t`Maximum`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{['text', 'integer', 'float'].includes(formik.values.type) && (
|
||||
<FormField
|
||||
id="question-default"
|
||||
name="default"
|
||||
type="text"
|
||||
label={i18n._(t`Default answer`)}
|
||||
/>
|
||||
)}
|
||||
{formik.values.type === 'textarea' && (
|
||||
<FormField
|
||||
id="question-default"
|
||||
name="default"
|
||||
type="textarea"
|
||||
label={i18n._(t`Default answer`)}
|
||||
/>
|
||||
)}
|
||||
{formik.values.type === 'password' && (
|
||||
<PasswordField
|
||||
id="question-default"
|
||||
name="default"
|
||||
label={i18n._(t`Default answer`)}
|
||||
/>
|
||||
)}
|
||||
{['multiplechoice', 'multiselect'].includes(formik.values.type) && (
|
||||
<>
|
||||
<FormField
|
||||
id="question-options"
|
||||
name="choices"
|
||||
type="textarea"
|
||||
label={i18n._(t`Multiple Choice Options`)}
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="question-default"
|
||||
name="default"
|
||||
type={
|
||||
formik.values.type === 'multiplechoice'
|
||||
? 'text'
|
||||
: 'textarea'
|
||||
}
|
||||
label={i18n._(t`Default answer`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormColumnLayout>
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
SurveyQuestionForm.propTypes = {
|
||||
question: shape({
|
||||
question_name: string.isRequired,
|
||||
question_description: string.isRequired,
|
||||
required: bool,
|
||||
type: string.isRequired,
|
||||
min: number,
|
||||
max: number,
|
||||
}),
|
||||
handleSubmit: func.isRequired,
|
||||
handleCancel: func.isRequired,
|
||||
submitError: shape({}),
|
||||
};
|
||||
|
||||
SurveyQuestionForm.defaultProps = {
|
||||
question: null,
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
export default withI18n()(SurveyQuestionForm);
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
@ -20,6 +21,7 @@ function SurveyToolbar({
|
||||
isDeleteDisabled,
|
||||
onToggleDeleteModal,
|
||||
}) {
|
||||
const match = useRouteMatch();
|
||||
return (
|
||||
<DataToolbar id="survey-toolbar">
|
||||
<DataToolbarContent>
|
||||
@ -45,7 +47,7 @@ function SurveyToolbar({
|
||||
</DataToolbarItem>
|
||||
<DataToolbarGroup>
|
||||
<DataToolbarItem>
|
||||
<ToolbarAddButton linkTo="/" />
|
||||
<ToolbarAddButton linkTo={`${match.url}/add`} />
|
||||
</DataToolbarItem>
|
||||
<DataToolbarItem>
|
||||
<Button
|
||||
|
@ -47,3 +47,24 @@ export function requiredEmail(i18n) {
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function noWhiteSpace(i18n) {
|
||||
return value => {
|
||||
if (/\s/.test(value)) {
|
||||
return i18n._(t`This field must not contain spaces`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function combine(validators) {
|
||||
return value => {
|
||||
for (let i = 0; i < validators.length; i++) {
|
||||
const error = validators[i](value);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { required, maxLength } from './validators';
|
||||
import { required, maxLength, noWhiteSpace, combine } from './validators';
|
||||
|
||||
const i18n = { _: val => val };
|
||||
|
||||
@ -51,4 +51,31 @@ describe('validators', () => {
|
||||
values: { max: 8 },
|
||||
});
|
||||
});
|
||||
|
||||
test('noWhiteSpace returns error', () => {
|
||||
expect(noWhiteSpace(i18n)('this has spaces')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
});
|
||||
|
||||
test('noWhiteSpace should accept valid string', () => {
|
||||
expect(noWhiteSpace(i18n)('this_has_no_whitespace')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('combine should run all validators', () => {
|
||||
const validators = [required(null, i18n), noWhiteSpace(i18n)];
|
||||
expect(combine(validators)('')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(combine(validators)('one two')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(combine(validators)('ok')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user