diff --git a/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx
index a389db0cff..5288e5f8cc 100644
--- a/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/CredentialsStep.jsx
@@ -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();
diff --git a/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx
index e34dc40664..d892e0c91b 100644
--- a/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/InventoryStep.jsx
@@ -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 {
diff --git a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
index 2db38bd918..8666c838ea 100644
--- a/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/LaunchPrompt.jsx
@@ -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 ;
}
+ if (config.survey_enabled && !survey) {
+ return ;
+ }
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: ,
});
@@ -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: ,
});
@@ -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: ,
});
}
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: ,
});
}
steps.push({
+ id: STEPS.PREVIEW,
name: i18n._(t`Preview`),
component: (
-
+
),
+ 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 (
-
- {({ handleSubmit }) => (
+
+ {({ errors, values, touched, validateForm, handleSubmit }) => (
{
+ // 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={}
/>
)}
diff --git a/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx
index 0989368652..7b552b8b23 100644
--- a/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/OtherPromptsStep.jsx
@@ -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 && }
diff --git a/awx/ui_next/src/components/LaunchPrompt/PreviewStep.jsx b/awx/ui_next/src/components/LaunchPrompt/PreviewStep.jsx
index 981c05e076..26d0572457 100644
--- a/awx/ui_next/src/components/LaunchPrompt/PreviewStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/PreviewStep.jsx
@@ -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 (
-
+ <>
+
+ {formErrors && (
+
+ {Object.keys(formErrors).map(
+ field => `${field}: ${formErrors[field]}`
+ )}
+
+ )}
+ >
);
}
diff --git a/awx/ui_next/src/components/LaunchPrompt/PromptFooter.jsx b/awx/ui_next/src/components/LaunchPrompt/PromptFooter.jsx
new file mode 100644
index 0000000000..1c56c0b66a
--- /dev/null
+++ b/awx/ui_next/src/components/LaunchPrompt/PromptFooter.jsx
@@ -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 (
+
+
+ {({
+ activeStep,
+ goToStepByName,
+ goToStepById,
+ onNext,
+ onBack,
+ onClose,
+ }) => {
+ if (activeStep.name !== STEPS.PREVIEW) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+ return (
+ <>
+
+
+ >
+ );
+ }}
+
+
+ );
+}
+
+export { PromptFooter as _PromptFooter };
+export default withI18n()(PromptFooter);
diff --git a/awx/ui_next/src/components/LaunchPrompt/SurveyStep.jsx b/awx/ui_next/src/components/LaunchPrompt/SurveyStep.jsx
index 451836ba8d..d9120d95d1 100644
--- a/awx/ui_next/src/components/LaunchPrompt/SurveyStep.jsx
+++ b/awx/ui_next/src/components/LaunchPrompt/SurveyStep.jsx
@@ -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 ;
- }
-
const initialValues = {};
survey.spec.forEach(question => {
if (question.type === 'multiselect') {
@@ -38,6 +34,9 @@ function SurveyStep({ survey, 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
diff --git a/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js b/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js
index b5e8fe1442..261c02a875 100644
--- a/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js
+++ b/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.js
@@ -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] = '········';
}
});
diff --git a/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.test.js b/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.test.js
index ef1420fb06..bd696ab9e5 100644
--- a/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.test.js
+++ b/awx/ui_next/src/components/LaunchPrompt/mergeExtraVars.test.js
@@ -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',
+ });
+ });
});
});
diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js
index e46021a5b6..52e07d53cb 100644
--- a/awx/ui_next/src/types.js
+++ b/awx/ui_next/src/types.js
@@ -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),
+});