mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 19:30:39 -03:30
working on prompts validation
This commit is contained in:
parent
9b3b20c96b
commit
5c2eebf692
@ -12,15 +12,19 @@ import CredentialChip from '@components/CredentialChip';
|
||||
import ContentError from '@components/ContentError';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import useRequest from '@util/useRequest';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
const QS_CONFIG = getQSConfig('credential', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function CredentialsStep({ i18n }) {
|
||||
const [field, , helpers] = useField('credentials');
|
||||
const [field, , helpers] = useField({
|
||||
name: 'credentials',
|
||||
validate: required(null, i18n),
|
||||
});
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const history = useHistory();
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import useRequest from '@util/useRequest';
|
||||
import OptionsList from '@components/OptionsList';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import ContentError from '@components/ContentError';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
const QS_CONFIG = getQSConfig('inventory', {
|
||||
page: 1,
|
||||
@ -17,7 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
|
||||
});
|
||||
|
||||
function InventoryStep({ i18n }) {
|
||||
const [field, , helpers] = useField('inventory');
|
||||
const [field, , helpers] = useField({
|
||||
name: 'inventory',
|
||||
validate: required(null, i18n),
|
||||
});
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -6,14 +6,61 @@ import { Formik } from 'formik';
|
||||
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
|
||||
import useRequest from '@util/useRequest';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { required } from '@util/validators';
|
||||
import InventoryStep from './InventoryStep';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import OtherPromptsStep from './OtherPromptsStep';
|
||||
import SurveyStep from './SurveyStep';
|
||||
import PreviewStep from './PreviewStep';
|
||||
import PromptFooter from './PromptFooter';
|
||||
import mergeExtraVars from './mergeExtraVars';
|
||||
|
||||
const STEPS = {
|
||||
INVENTORY: 'inventory',
|
||||
CREDENTIALS: 'credentials',
|
||||
PASSWORDS: 'passwords',
|
||||
OTHER_PROMPTS: 'other',
|
||||
SURVEY: 'survey',
|
||||
PREVIEW: 'preview',
|
||||
};
|
||||
|
||||
function showOtherPrompts(config) {
|
||||
return (
|
||||
config.ask_job_type_on_launch ||
|
||||
config.ask_limit_on_launch ||
|
||||
config.ask_verbosity_on_launch ||
|
||||
config.ask_tags_on_launch ||
|
||||
config.ask_skip_tags_on_launch ||
|
||||
config.ask_variables_on_launch ||
|
||||
config.ask_scm_branch_on_launch ||
|
||||
config.ask_diff_mode_on_launch
|
||||
);
|
||||
}
|
||||
|
||||
function getInitialVisitedSteps(config) {
|
||||
const visited = {};
|
||||
if (config.ask_inventory_on_launch) {
|
||||
visited[STEPS.INVENTORY] = false;
|
||||
}
|
||||
if (config.ask_credential_on_launch) {
|
||||
visited[STEPS.CREDENTIALS] = false;
|
||||
}
|
||||
if (showOtherPrompts(config)) {
|
||||
visited[STEPS.OTHER_PROMPTS] = false;
|
||||
}
|
||||
if (config.survey_enabled) {
|
||||
visited[STEPS.SURVEY] = false;
|
||||
}
|
||||
return visited;
|
||||
}
|
||||
|
||||
function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [visitedSteps, setVisitedSteps] = useState(
|
||||
getInitialVisitedSteps(config)
|
||||
);
|
||||
|
||||
const {
|
||||
result: survey,
|
||||
request: fetchSurvey,
|
||||
@ -37,12 +84,16 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
if (surveyError) {
|
||||
return <ContentError error={surveyError} />;
|
||||
}
|
||||
if (config.survey_enabled && !survey) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
const steps = [];
|
||||
const initialValues = {};
|
||||
if (config.ask_inventory_on_launch) {
|
||||
initialValues.inventory = resource?.summary_fields?.inventory || null;
|
||||
steps.push({
|
||||
id: STEPS.INVENTORY,
|
||||
name: i18n._(t`Inventory`),
|
||||
component: <InventoryStep />,
|
||||
});
|
||||
@ -50,6 +101,7 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
if (config.ask_credential_on_launch) {
|
||||
initialValues.credentials = resource?.summary_fields?.credentials || [];
|
||||
steps.push({
|
||||
id: STEPS.CREDENTIALS,
|
||||
name: i18n._(t`Credentials`),
|
||||
component: <CredentialsStep />,
|
||||
});
|
||||
@ -81,36 +133,44 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
if (config.ask_diff_mode_on_launch) {
|
||||
initialValues.diff_mode = resource.diff_mode || false;
|
||||
}
|
||||
if (
|
||||
config.ask_job_type_on_launch ||
|
||||
config.ask_limit_on_launch ||
|
||||
config.ask_verbosity_on_launch ||
|
||||
config.ask_tags_on_launch ||
|
||||
config.ask_skip_tags_on_launch ||
|
||||
config.ask_variables_on_launch ||
|
||||
config.ask_scm_branch_on_launch ||
|
||||
config.ask_diff_mode_on_launch
|
||||
) {
|
||||
if (showOtherPrompts(config)) {
|
||||
steps.push({
|
||||
id: STEPS.OTHER_PROMPTS,
|
||||
name: i18n._(t`Other Prompts`),
|
||||
component: <OtherPromptsStep config={config} />,
|
||||
});
|
||||
}
|
||||
if (config.survey_enabled) {
|
||||
initialValues.survey = {};
|
||||
// survey.spec.forEach(question => {
|
||||
// initialValues[`survey_${question.variable}`] = question.default;
|
||||
// })
|
||||
steps.push({
|
||||
id: STEPS.SURVEY,
|
||||
name: i18n._(t`Survey`),
|
||||
component: <SurveyStep survey={survey} />,
|
||||
});
|
||||
}
|
||||
steps.push({
|
||||
id: STEPS.PREVIEW,
|
||||
name: i18n._(t`Preview`),
|
||||
component: (
|
||||
<PreviewStep resource={resource} config={config} survey={survey} />
|
||||
<PreviewStep
|
||||
resource={resource}
|
||||
config={config}
|
||||
survey={survey}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
),
|
||||
enableNext: Object.keys(formErrors).length === 0,
|
||||
nextButtonText: i18n._(t`Launch`),
|
||||
});
|
||||
|
||||
const validate = values => {
|
||||
// return {};
|
||||
return { limit: ['required field'] };
|
||||
};
|
||||
|
||||
const submit = values => {
|
||||
const postValues = {};
|
||||
const setValue = (key, value) => {
|
||||
@ -127,16 +187,32 @@ function LaunchPrompt({ config, resource, onLaunch, onCancel, i18n }) {
|
||||
setValue('extra_vars', mergeExtraVars(values.extra_vars, values.survey));
|
||||
onLaunch(postValues);
|
||||
};
|
||||
console.log('formErrors:', formErrors);
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={submit}>
|
||||
{({ handleSubmit }) => (
|
||||
<Formik initialValues={initialValues} onSubmit={submit} validate={validate}>
|
||||
{({ errors, values, touched, validateForm, handleSubmit }) => (
|
||||
<Wizard
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
onSave={handleSubmit}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
// console.log(`${prevStep.prevName} -> ${nextStep.name}`);
|
||||
// console.log('errors', errors);
|
||||
// console.log('values', values);
|
||||
const newErrors = await validateForm();
|
||||
setFormErrors(newErrors);
|
||||
// console.log('new errors:', newErrors);
|
||||
}}
|
||||
onGoToStep={async (newStep, prevStep) => {
|
||||
// console.log('errors', errors);
|
||||
// console.log('values', values);
|
||||
const newErrors = await validateForm();
|
||||
setFormErrors(newErrors);
|
||||
}}
|
||||
title={i18n._(t`Prompts`)}
|
||||
steps={steps}
|
||||
// footer={<PromptFooter firstStep={steps[0].id} />}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@ -8,6 +8,7 @@ import { TagMultiSelect } from '@components/MultiSelect';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import { VariablesField } from '@components/CodeMirrorInput';
|
||||
import styled from 'styled-components';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
const FieldHeader = styled.div`
|
||||
display: flex;
|
||||
@ -32,6 +33,9 @@ function OtherPromptsStep({ config, i18n }) {
|
||||
of hosts that will be managed or affected by the playbook. Multiple
|
||||
patterns are allowed. Refer to Ansible documentation for more
|
||||
information and examples on patterns.`)}
|
||||
// TODO: remove this validator (for testing only)
|
||||
isRequired
|
||||
validate={required(null, i18n)}
|
||||
/>
|
||||
)}
|
||||
{config.ask_verbosity_on_launch && <VerbosityField i18n={i18n} />}
|
||||
|
||||
@ -4,21 +4,30 @@ import yaml from 'js-yaml';
|
||||
import PromptDetail from '@components/PromptDetail';
|
||||
import mergeExtraVars, { maskPasswords } from './mergeExtraVars';
|
||||
|
||||
function PreviewStep({ resource, config, survey }) {
|
||||
function PreviewStep({ resource, config, survey, formErrors }) {
|
||||
const { values } = useFormikContext();
|
||||
const passwordFields = survey.spec
|
||||
.filter(q => q.type === 'password')
|
||||
.map(q => q.variable);
|
||||
const masked = maskPasswords(values.survey, passwordFields);
|
||||
return (
|
||||
<PromptDetail
|
||||
resource={resource}
|
||||
launchConfig={config}
|
||||
overrides={{
|
||||
...values,
|
||||
extra_vars: yaml.safeDump(mergeExtraVars(values.extra_vars, masked)),
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<PromptDetail
|
||||
resource={resource}
|
||||
launchConfig={config}
|
||||
overrides={{
|
||||
...values,
|
||||
extra_vars: yaml.safeDump(mergeExtraVars(values.extra_vars, masked)),
|
||||
}}
|
||||
/>
|
||||
{formErrors && (
|
||||
<ul css="color: red">
|
||||
{Object.keys(formErrors).map(
|
||||
field => `${field}: ${formErrors[field]}`
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
67
awx/ui_next/src/components/LaunchPrompt/PromptFooter.jsx
Normal file
67
awx/ui_next/src/components/LaunchPrompt/PromptFooter.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
WizardFooter,
|
||||
WizardContextConsumer,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
const STEPS = {
|
||||
INVENTORY: 'inventory',
|
||||
CREDENTIALS: 'credentials',
|
||||
PASSWORDS: 'passwords',
|
||||
OTHER_PROMPTS: 'other',
|
||||
SURVEY: 'survey',
|
||||
PREVIEW: 'preview',
|
||||
};
|
||||
|
||||
export function PromptFooter({ firstStep, i18n }) {
|
||||
return (
|
||||
<WizardFooter>
|
||||
<WizardContextConsumer>
|
||||
{({
|
||||
activeStep,
|
||||
goToStepByName,
|
||||
goToStepById,
|
||||
onNext,
|
||||
onBack,
|
||||
onClose,
|
||||
}) => {
|
||||
if (activeStep.name !== STEPS.PREVIEW) {
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" type="submit" onClick={onNext}>
|
||||
{activeStep.nextButtonText || i18n._(t`Next`)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onBack}
|
||||
className={activeStep.id === firstStep ? 'pf-m-disabled' : ''}
|
||||
>
|
||||
{i18n._(t`Back`)}
|
||||
</Button>
|
||||
<Button variant="link" onClick={onClose}>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => this.validateLastStep(onNext)}>
|
||||
{activeStep.nextButtonText || i18n._(t`Launch`)}
|
||||
</Button>
|
||||
<Button onClick={() => goToStepByName('Step 1')}>
|
||||
Go to Beginning
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</WizardContextConsumer>
|
||||
</WizardFooter>
|
||||
);
|
||||
}
|
||||
|
||||
export { PromptFooter as _PromptFooter };
|
||||
export default withI18n()(PromptFooter);
|
||||
@ -10,7 +10,6 @@ import {
|
||||
} from '@patternfly/react-core';
|
||||
import FormField, { FieldTooltip } from '@components/FormField';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import {
|
||||
required,
|
||||
minMaxValue,
|
||||
@ -19,12 +18,9 @@ import {
|
||||
integer,
|
||||
combine,
|
||||
} from '@util/validators';
|
||||
import { Survey } from '@types';
|
||||
|
||||
function SurveyStep({ survey, i18n }) {
|
||||
if (!survey) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
const initialValues = {};
|
||||
survey.spec.forEach(question => {
|
||||
if (question.type === 'multiselect') {
|
||||
@ -38,6 +34,9 @@ function SurveyStep({ survey, i18n }) {
|
||||
<SurveySubForm survey={survey} initialValues={initialValues} i18n={i18n} />
|
||||
);
|
||||
}
|
||||
SurveyStep.propTypes = {
|
||||
survey: Survey.isRequired,
|
||||
};
|
||||
|
||||
// This is a nested Formik form to perform validation on individual
|
||||
// survey questions. When changes to the inner form occur (onBlur), the
|
||||
|
||||
@ -11,7 +11,7 @@ export default function mergeExtraVars(extraVars, survey = {}) {
|
||||
export function maskPasswords(vars, passwordKeys) {
|
||||
const updated = { ...vars };
|
||||
passwordKeys.forEach(key => {
|
||||
if (updated[key]) {
|
||||
if (typeof updated[key] !== 'undefined') {
|
||||
updated[key] = '········';
|
||||
}
|
||||
});
|
||||
|
||||
@ -46,5 +46,17 @@ describe('mergeExtraVars', () => {
|
||||
three: '········',
|
||||
});
|
||||
});
|
||||
|
||||
test('should mask empty strings', () => {
|
||||
const vars = {
|
||||
one: '',
|
||||
two: 'bravo',
|
||||
};
|
||||
|
||||
expect(maskPasswords(vars, ['one', 'three'])).toEqual({
|
||||
one: '········',
|
||||
two: 'bravo',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -306,3 +306,21 @@ export const Schedule = shape({
|
||||
timezone: string,
|
||||
until: string,
|
||||
});
|
||||
|
||||
export const SurveyQuestion = shape({
|
||||
question_name: string,
|
||||
question_description: string,
|
||||
required: bool,
|
||||
type: string,
|
||||
variable: string,
|
||||
min: number,
|
||||
max: number,
|
||||
default: string,
|
||||
choices: string,
|
||||
});
|
||||
|
||||
export const Survey = shape({
|
||||
name: string,
|
||||
description: string,
|
||||
spec: arrayOf(SurveyQuestion),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user