diff --git a/awx/ui/src/components/AdHocCommands/AdHocCommands.js b/awx/ui/src/components/AdHocCommands/AdHocCommands.js
index d37106285f..7ddee926bf 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCommands.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCommands.js
@@ -79,11 +79,19 @@ function AdHocCommands({
);
const handleSubmit = async (values) => {
- const { credential, execution_environment, ...remainingValues } = values;
- const newCredential = credential[0].id;
+ const {
+ credentials,
+ credential_passwords: { become_password, ssh_password, ssh_key_unlock },
+ execution_environment,
+ ...remainingValues
+ } = values;
+ const newCredential = credentials[0].id;
const manipulatedValues = {
credential: newCredential,
+ become_password,
+ ssh_password,
+ ssh_key_unlock,
execution_environment: execution_environment[0]?.id,
...remainingValues,
};
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCommands.test.js b/awx/ui/src/components/AdHocCommands/AdHocCommands.test.js
index 2dd22369a5..25eb74ffbc 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCommands.test.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCommands.test.js
@@ -234,12 +234,15 @@ describe('', () => {
module_args: 'foo',
diff_mode: false,
credential: 4,
+ become_password: undefined,
job_type: 'run',
become_enabled: '',
extra_vars: '---',
forks: 0,
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
module_name: 'command',
+ ssh_key_unlock: undefined,
+ ssh_password: undefined,
verbosity: 1,
execution_environment: 2,
});
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.js b/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.js
index a7647c368d..c86890502c 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.js
@@ -61,7 +61,7 @@ const FormikApp = withFormik({
const adHocItemStrings = adHocItems.map((item) => item.name).join(', ');
return {
limit: adHocItemStrings || 'all',
- credential: [],
+ credentials: [],
module_args: '',
verbosity: verbosityOptions[0].value,
forks: 0,
@@ -70,6 +70,7 @@ const FormikApp = withFormik({
module_name: '',
extra_vars: '---',
job_type: 'run',
+ credential_passwords: {},
execution_environment: '',
};
},
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.test.js b/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.test.js
index 7d19f3cadc..b9bb928a73 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.test.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCommandsWizard.test.js
@@ -184,7 +184,157 @@ describe('', () => {
expect(onLaunch).toHaveBeenCalledWith({
become_enabled: '',
- credential: [{ id: 1, name: 'Cred 1', url: '' }],
+ credentials: [{ id: 1, name: 'Cred 1', url: '' }],
+ credential_passwords: {},
+ diff_mode: false,
+ execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
+ extra_vars: '---',
+ forks: 0,
+ job_type: 'run',
+ limit: 'Inventory 1, Inventory 2, inventory 3',
+ module_args: 'foo',
+ module_name: 'command',
+ verbosity: 1,
+ });
+ });
+
+ test('should render credential passwords step', async () => {
+ ExecutionEnvironmentsAPI.read.mockResolvedValue({
+ data: {
+ results: [
+ { id: 1, name: 'EE 1', url: '' },
+ { id: 2, name: 'EE 2', url: '' },
+ ],
+ count: 2,
+ },
+ });
+ ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {} } },
+ });
+ CredentialsAPI.read.mockResolvedValue({
+ data: {
+ results: [
+ {
+ id: 1,
+ name: 'Cred 1',
+ url: '',
+ inputs: { password: 'ASK' },
+ },
+ { id: 2, name: 'Cred2', url: '' },
+ ],
+ count: 2,
+ },
+ });
+ CredentialsAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {} } },
+ });
+ 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: 'foo', name: 'module_args' },
+ });
+ wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
+ });
+ wrapper.update();
+ expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
+ false
+ );
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ wrapper.update();
+
+ // step 2
+
+ await waitForElement(wrapper, 'OptionsList', (el) => el.length > 0);
+ expect(wrapper.find('CheckboxListItem').length).toBe(2);
+ expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
+ false
+ );
+
+ await act(async () => {
+ wrapper.find('td#check-action-item-1').find('input').simulate('click');
+ });
+
+ wrapper.update();
+
+ expect(
+ wrapper.find('CheckboxListItem[label="EE 1"]').prop('isSelected')
+ ).toBe(true);
+ expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
+ false
+ );
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+
+ wrapper.update();
+ // step 3
+
+ await waitForElement(wrapper, 'OptionsList', (el) => el.length > 0);
+ expect(wrapper.find('CheckboxListItem').length).toBe(2);
+
+ await act(async () => {
+ wrapper.find('td#check-action-item-1').find('input').simulate('click');
+ });
+
+ wrapper.update();
+
+ expect(
+ wrapper.find('CheckboxListItem[label="Cred 1"]').prop('isSelected')
+ ).toBe(true);
+
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ wrapper.update();
+
+ // step 4
+
+ expect(wrapper.find('PasswordInput')).toHaveLength(1);
+ await act(async () =>
+ wrapper
+ .find('TextInputBase[name="credential_passwords.ssh_password"]')
+ .prop('onChange')('', {
+ target: {
+ value: 'password',
+ name: 'credential_passwords.ssh_password',
+ },
+ })
+ );
+ wrapper.update();
+ expect(
+ wrapper
+ .find('TextInput[name="credential_passwords.ssh_password"]')
+ .prop('value')
+ ).toBe('password');
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ await act(async () =>
+ wrapper.find('Button[type="submit"]').prop('onClick')()
+ );
+ wrapper.update();
+
+ // step 5
+
+ expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
+ false
+ );
+
+ expect(onLaunch).toHaveBeenCalledWith({
+ become_enabled: '',
+ credentials: [
+ { id: 1, name: 'Cred 1', url: '', inputs: { password: 'ASK' } },
+ ],
+ credential_passwords: { ssh_password: 'password' },
diff_mode: false,
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
extra_vars: '---',
diff --git a/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js b/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
index 1042f082c8..8001315cbf 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocCredentialStep.js
@@ -76,7 +76,7 @@ function AdHocCredentialStep({ credentialTypeId }) {
}, [fetchCredentials]);
const [field, meta, helpers] = useField({
- name: 'credential',
+ name: 'credentials',
validate: required(null),
});
diff --git a/awx/ui/src/components/AdHocCommands/AdHocPreviewStep.js b/awx/ui/src/components/AdHocCommands/AdHocPreviewStep.js
index b61c0261bc..595ab7676f 100644
--- a/awx/ui/src/components/AdHocCommands/AdHocPreviewStep.js
+++ b/awx/ui/src/components/AdHocCommands/AdHocPreviewStep.js
@@ -43,7 +43,8 @@ function AdHocPreviewStep({ hasErrors, values }) {
([key, value]) =>
key !== 'extra_vars' &&
key !== 'execution_environment' &&
- key !== 'credential' && (
+ key !== 'credentials' &&
+ !key.startsWith('credential_passwords') && (
)
)}
diff --git a/awx/ui/src/components/AdHocCommands/useAdHocCredentialPasswordStep.js b/awx/ui/src/components/AdHocCommands/useAdHocCredentialPasswordStep.js
new file mode 100644
index 0000000000..3f63a3db71
--- /dev/null
+++ b/awx/ui/src/components/AdHocCommands/useAdHocCredentialPasswordStep.js
@@ -0,0 +1,82 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { t } from '@lingui/macro';
+import StepName from '../LaunchPrompt/steps/StepName';
+import CredentialPasswordsStep from '../LaunchPrompt/steps/CredentialPasswordsStep';
+
+const STEP_ID = 'credentialPasswords';
+
+const isValueMissing = (val) => !val || val === '';
+
+export default function useCredentialPasswordsStep(showStep, visitedSteps) {
+ const { values, setFieldError } = useFormikContext();
+ const hasError =
+ showStep &&
+ Object.keys(visitedSteps).includes(STEP_ID) &&
+ checkForError(values);
+ return {
+ step: showStep
+ ? {
+ id: STEP_ID,
+ name: (
+
+ {t`Credential passwords`}
+
+ ),
+ component: ,
+ enableNext: true,
+ }
+ : null,
+ isReady: true,
+ contentError: null,
+ hasError,
+ setTouched: (setFieldTouched) => {
+ Object.keys(values.credential_passwords).forEach((credentialValueKey) =>
+ setFieldTouched(
+ `credential_passwords['${credentialValueKey}']`,
+ true,
+ false
+ )
+ );
+ },
+ validate: () => {
+ const setPasswordFieldError = (fieldName) => {
+ setFieldError(fieldName, t`This field may not be blank`);
+ };
+
+ Object.entries(values.credentials[0].inputs).forEach(([key, value]) => {
+ if (
+ value === 'ASK' &&
+ isValueMissing(
+ key === 'password'
+ ? values.credential_passwords.ssh_password
+ : values.credential_passwords[key]
+ )
+ ) {
+ setPasswordFieldError(
+ key === 'password'
+ ? `credential_passwords.ssh_password`
+ : `credential_passwords.${key}`
+ );
+ }
+ });
+ },
+ };
+}
+
+function checkForError(values) {
+ let hasError = false;
+ Object.entries(values.credentials[0]?.inputs).forEach(([key, value]) => {
+ if (
+ value === 'ASK' &&
+ isValueMissing(
+ key === 'password'
+ ? values.credential_passwords.ssh_password
+ : values.credential_passwords[key]
+ )
+ ) {
+ hasError = true;
+ }
+ });
+ return hasError;
+}
diff --git a/awx/ui/src/components/AdHocCommands/useAdHocCredentialStep.js b/awx/ui/src/components/AdHocCommands/useAdHocCredentialStep.js
index d4da6b1aa7..1b98ff94ed 100644
--- a/awx/ui/src/components/AdHocCommands/useAdHocCredentialStep.js
+++ b/awx/ui/src/components/AdHocCommands/useAdHocCredentialStep.js
@@ -4,14 +4,14 @@ import { t } from '@lingui/macro';
import StepName from '../LaunchPrompt/steps/StepName';
import AdHocCredentialStep from './AdHocCredentialStep';
-const STEP_ID = 'credential';
+const STEP_ID = 'credentials';
export default function useAdHocExecutionEnvironmentStep(
visited,
credentialTypeId
) {
- const [field, meta, helpers] = useField('credential');
+ const [field, meta, helpers] = useField('credentials');
const hasError =
- Object.keys(visited).includes('credential') &&
+ Object.keys(visited).includes('credentials') &&
!field.value.length &&
meta.touched;
@@ -35,7 +35,7 @@ export default function useAdHocExecutionEnvironmentStep(
}
},
setTouched: (setFieldTouched) => {
- setFieldTouched('credential', true, false);
+ setFieldTouched('credentials', true, false);
},
};
}
diff --git a/awx/ui/src/components/AdHocCommands/useAdHocLaunchSteps.js b/awx/ui/src/components/AdHocCommands/useAdHocLaunchSteps.js
index 6fa2ad3404..caf7a91462 100644
--- a/awx/ui/src/components/AdHocCommands/useAdHocLaunchSteps.js
+++ b/awx/ui/src/components/AdHocCommands/useAdHocLaunchSteps.js
@@ -1,22 +1,73 @@
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { useFormikContext } from 'formik';
+import useCredentialPasswordsStep from './useAdHocCredentialPasswordStep';
import useAdHocDetailsStep from './useAdHocDetailsStep';
import useAdHocExecutionEnvironmentStep from './useAdHocExecutionEnvironmentStep';
import useAdHocCredentialStep from './useAdHocCredentialStep';
import useAdHocPreviewStep from './useAdHocPreviewStep';
+function showCredentialPasswordsStep(credential) {
+ if (!credential?.inputs) {
+ return false;
+ }
+ const { inputs } = credential;
+ if (
+ inputs?.password === 'ASK' ||
+ inputs?.become_password === 'ASK' ||
+ inputs?.ssh_key_unlock === 'ASK'
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
export default function useAdHocLaunchSteps(
moduleOptions,
verbosityOptions,
organizationId,
credentialTypeId
) {
+ const { values, resetForm, touched } = useFormikContext();
+
const [visited, setVisited] = useState({});
const steps = [
useAdHocDetailsStep(visited, moduleOptions, verbosityOptions),
useAdHocExecutionEnvironmentStep(organizationId),
useAdHocCredentialStep(visited, credentialTypeId),
+ useCredentialPasswordsStep(
+ showCredentialPasswordsStep(values.credentials[0]),
+ visited
+ ),
];
+ useEffect(() => {
+ const newFormValues = { ...values };
+
+ if (!values.credentials[0]?.inputs) {
+ return;
+ }
+ if (
+ (values.credentials[0].inputs?.password ||
+ values.credentials[0].inputs?.become_password ||
+ values.credentials[0].inputs?.ssh_key_unlock) === 'ASK'
+ )
+ newFormValues.credential_passwords = {};
+ Object.keys(values.credentials[0].inputs).forEach((inputKey) => {
+ if (inputKey === 'become_password' || inputKey === 'ssh_key_unlock') {
+ newFormValues.credential_passwords[inputKey] = '';
+ }
+ if (inputKey === 'password') {
+ newFormValues.credential_passwords.ssh_password = '';
+ }
+ });
+ resetForm({
+ values: newFormValues,
+ touched,
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [values.credentials.length]);
+
const hasErrors = steps.some((step) => step.hasError);
steps.push(useAdHocPreviewStep(hasErrors));
@@ -35,7 +86,8 @@ export default function useAdHocLaunchSteps(
setVisited({
details: true,
executionEnvironment: true,
- credential: true,
+ credentials: true,
+ credentialPasswords: true,
preview: true,
});
steps.forEach((s) => s.setTouched(setFieldTouched));