diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx index 4a0716f320..83eee59d88 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx @@ -1,13 +1,26 @@ import React, { useState } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons'; +import { Tooltip } from '@patternfly/react-core'; import { withFormik, useFormikContext } from 'formik'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; import Wizard from '../Wizard'; import AdHocCredentialStep from './AdHocCredentialStep'; import AdHocDetailsStep from './AdHocDetailsStep'; +const AlertText = styled.div` + color: var(--pf-global--danger-color--200); + font-weight: var(--pf-global--FontWeight--bold); +`; + +const ExclamationCircleIcon = styled(PFExclamationCircleIcon)` + margin-left: 10px; + color: var(--pf-global--danger-color--100); +`; + function AdHocCommandsWizard({ onLaunch, i18n, @@ -19,7 +32,7 @@ function AdHocCommandsWizard({ const [currentStepId, setCurrentStepId] = useState(1); const [enableLaunch, setEnableLaunch] = useState(false); - const { values } = useFormikContext(); + const { values, errors, touched } = useFormikContext(); const enabledNextOnDetailsStep = () => { if (!values.module_name) { @@ -36,11 +49,26 @@ function AdHocCommandsWizard({ } return undefined; // makes the linter happy; }; + const hasDetailsStepError = errors.module_args && touched.module_args; + const steps = [ { id: 1, key: 1, - name: i18n._(t`Details`), + name: hasDetailsStepError ? ( + + {i18n._(t`Details`)} + + + + + ) : ( + i18n._(t`Details`) + ), component: ( setEnableLaunch(true)} /> ), - enableNext: enableLaunch, + enableNext: enableLaunch && Object.values(errors).length === 0, nextButtonText: i18n._(t`Launch`), canJumpTo: currentStepId >= 2, }, diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.test.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.test.jsx index ed3ab4ed99..d38aa28bbc 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.test.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.test.jsx @@ -148,6 +148,20 @@ describe('', () => { expect(onLaunch).toHaveBeenCalled(); }); + test('should show error in navigation bar', async () => { + await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0); + + await act(async () => { + wrapper.find('AnsibleSelect[name="module_name"]').prop('onChange')( + {}, + 'command' + ); + wrapper.find('input#module_args').simulate('change', { + target: { value: '', name: 'module_args' }, + }); + }); + waitForElement(wrapper, 'ExclamationCircleIcon', el => el.length > 0); + }); test('expect credential step to throw error', async () => { CredentialsAPI.read.mockRejectedValue( diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx index ade3528ea1..9a1f4fb094 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx @@ -65,6 +65,7 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) { argumentsHelpers.setTouched(true)} placeholder={i18n._(t`Enter arguments`)} isRequired={ moduleNameField.value === 'command' || @@ -133,6 +136,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) { /> {i18n._( @@ -185,6 +190,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) { type="number" min="0" label={i18n._(t`Forks`)} + aria-label={i18n._(t`Forks`)} tooltip={ {i18n._( @@ -203,6 +209,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) { } label={i18n._(t`Extra variables`)} + aria-label={i18n._(t`Extra variables`)} />