mirror of
https://github.com/ansible/awx.git
synced 2026-05-15 13:27:40 -02:30
Adds credential password step to ad hoc commands wizard (#11598)
This commit is contained in:
@@ -79,11 +79,19 @@ function AdHocCommands({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values) => {
|
||||||
const { credential, execution_environment, ...remainingValues } = values;
|
const {
|
||||||
const newCredential = credential[0].id;
|
credentials,
|
||||||
|
credential_passwords: { become_password, ssh_password, ssh_key_unlock },
|
||||||
|
execution_environment,
|
||||||
|
...remainingValues
|
||||||
|
} = values;
|
||||||
|
const newCredential = credentials[0].id;
|
||||||
|
|
||||||
const manipulatedValues = {
|
const manipulatedValues = {
|
||||||
credential: newCredential,
|
credential: newCredential,
|
||||||
|
become_password,
|
||||||
|
ssh_password,
|
||||||
|
ssh_key_unlock,
|
||||||
execution_environment: execution_environment[0]?.id,
|
execution_environment: execution_environment[0]?.id,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -234,12 +234,15 @@ describe('<AdHocCommands />', () => {
|
|||||||
module_args: 'foo',
|
module_args: 'foo',
|
||||||
diff_mode: false,
|
diff_mode: false,
|
||||||
credential: 4,
|
credential: 4,
|
||||||
|
become_password: undefined,
|
||||||
job_type: 'run',
|
job_type: 'run',
|
||||||
become_enabled: '',
|
become_enabled: '',
|
||||||
extra_vars: '---',
|
extra_vars: '---',
|
||||||
forks: 0,
|
forks: 0,
|
||||||
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
||||||
module_name: 'command',
|
module_name: 'command',
|
||||||
|
ssh_key_unlock: undefined,
|
||||||
|
ssh_password: undefined,
|
||||||
verbosity: 1,
|
verbosity: 1,
|
||||||
execution_environment: 2,
|
execution_environment: 2,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const FormikApp = withFormik({
|
|||||||
const adHocItemStrings = adHocItems.map((item) => item.name).join(', ');
|
const adHocItemStrings = adHocItems.map((item) => item.name).join(', ');
|
||||||
return {
|
return {
|
||||||
limit: adHocItemStrings || 'all',
|
limit: adHocItemStrings || 'all',
|
||||||
credential: [],
|
credentials: [],
|
||||||
module_args: '',
|
module_args: '',
|
||||||
verbosity: verbosityOptions[0].value,
|
verbosity: verbosityOptions[0].value,
|
||||||
forks: 0,
|
forks: 0,
|
||||||
@@ -70,6 +70,7 @@ const FormikApp = withFormik({
|
|||||||
module_name: '',
|
module_name: '',
|
||||||
extra_vars: '---',
|
extra_vars: '---',
|
||||||
job_type: 'run',
|
job_type: 'run',
|
||||||
|
credential_passwords: {},
|
||||||
execution_environment: '',
|
execution_environment: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,7 +184,157 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
|
|
||||||
expect(onLaunch).toHaveBeenCalledWith({
|
expect(onLaunch).toHaveBeenCalledWith({
|
||||||
become_enabled: '',
|
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,
|
diff_mode: false,
|
||||||
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
|
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
|
||||||
extra_vars: '---',
|
extra_vars: '---',
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function AdHocCredentialStep({ credentialTypeId }) {
|
|||||||
}, [fetchCredentials]);
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
const [field, meta, helpers] = useField({
|
const [field, meta, helpers] = useField({
|
||||||
name: 'credential',
|
name: 'credentials',
|
||||||
validate: required(null),
|
validate: required(null),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ function AdHocPreviewStep({ hasErrors, values }) {
|
|||||||
([key, value]) =>
|
([key, value]) =>
|
||||||
key !== 'extra_vars' &&
|
key !== 'extra_vars' &&
|
||||||
key !== 'execution_environment' &&
|
key !== 'execution_environment' &&
|
||||||
key !== 'credential' && (
|
key !== 'credentials' &&
|
||||||
|
!key.startsWith('credential_passwords') && (
|
||||||
<Detail key={key} label={toTitleCase(key)} value={value} />
|
<Detail key={key} label={toTitleCase(key)} value={value} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
<StepName hasErrors={hasError} id="credential-passwords-step">
|
||||||
|
{t`Credential passwords`}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <CredentialPasswordsStep launchConfig={{}} />,
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ import { t } from '@lingui/macro';
|
|||||||
import StepName from '../LaunchPrompt/steps/StepName';
|
import StepName from '../LaunchPrompt/steps/StepName';
|
||||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||||
|
|
||||||
const STEP_ID = 'credential';
|
const STEP_ID = 'credentials';
|
||||||
export default function useAdHocExecutionEnvironmentStep(
|
export default function useAdHocExecutionEnvironmentStep(
|
||||||
visited,
|
visited,
|
||||||
credentialTypeId
|
credentialTypeId
|
||||||
) {
|
) {
|
||||||
const [field, meta, helpers] = useField('credential');
|
const [field, meta, helpers] = useField('credentials');
|
||||||
const hasError =
|
const hasError =
|
||||||
Object.keys(visited).includes('credential') &&
|
Object.keys(visited).includes('credentials') &&
|
||||||
!field.value.length &&
|
!field.value.length &&
|
||||||
meta.touched;
|
meta.touched;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export default function useAdHocExecutionEnvironmentStep(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setTouched: (setFieldTouched) => {
|
setTouched: (setFieldTouched) => {
|
||||||
setFieldTouched('credential', true, false);
|
setFieldTouched('credentials', true, false);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 useAdHocDetailsStep from './useAdHocDetailsStep';
|
||||||
import useAdHocExecutionEnvironmentStep from './useAdHocExecutionEnvironmentStep';
|
import useAdHocExecutionEnvironmentStep from './useAdHocExecutionEnvironmentStep';
|
||||||
import useAdHocCredentialStep from './useAdHocCredentialStep';
|
import useAdHocCredentialStep from './useAdHocCredentialStep';
|
||||||
import useAdHocPreviewStep from './useAdHocPreviewStep';
|
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(
|
export default function useAdHocLaunchSteps(
|
||||||
moduleOptions,
|
moduleOptions,
|
||||||
verbosityOptions,
|
verbosityOptions,
|
||||||
organizationId,
|
organizationId,
|
||||||
credentialTypeId
|
credentialTypeId
|
||||||
) {
|
) {
|
||||||
|
const { values, resetForm, touched } = useFormikContext();
|
||||||
|
|
||||||
const [visited, setVisited] = useState({});
|
const [visited, setVisited] = useState({});
|
||||||
const steps = [
|
const steps = [
|
||||||
useAdHocDetailsStep(visited, moduleOptions, verbosityOptions),
|
useAdHocDetailsStep(visited, moduleOptions, verbosityOptions),
|
||||||
useAdHocExecutionEnvironmentStep(organizationId),
|
useAdHocExecutionEnvironmentStep(organizationId),
|
||||||
useAdHocCredentialStep(visited, credentialTypeId),
|
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);
|
const hasErrors = steps.some((step) => step.hasError);
|
||||||
|
|
||||||
steps.push(useAdHocPreviewStep(hasErrors));
|
steps.push(useAdHocPreviewStep(hasErrors));
|
||||||
@@ -35,7 +86,8 @@ export default function useAdHocLaunchSteps(
|
|||||||
setVisited({
|
setVisited({
|
||||||
details: true,
|
details: true,
|
||||||
executionEnvironment: true,
|
executionEnvironment: true,
|
||||||
credential: true,
|
credentials: true,
|
||||||
|
credentialPasswords: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
});
|
});
|
||||||
steps.forEach((s) => s.setTouched(setFieldTouched));
|
steps.forEach((s) => s.setTouched(setFieldTouched));
|
||||||
|
|||||||
Reference in New Issue
Block a user