mirror of
https://github.com/ansible/awx.git
synced 2026-01-20 22:18:01 -03:30
Adds credential password step to ad hoc commands wizard (#11598)
This commit is contained in:
parent
f8e680867b
commit
6f9d4d89cd
@ -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,
|
||||
};
|
||||
|
||||
@ -234,12 +234,15 @@ describe('<AdHocCommands />', () => {
|
||||
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,
|
||||
});
|
||||
|
||||
@ -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: '',
|
||||
};
|
||||
},
|
||||
|
||||
@ -184,7 +184,157 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
|
||||
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: '---',
|
||||
|
||||
@ -76,7 +76,7 @@ function AdHocCredentialStep({ credentialTypeId }) {
|
||||
}, [fetchCredentials]);
|
||||
|
||||
const [field, meta, helpers] = useField({
|
||||
name: 'credential',
|
||||
name: 'credentials',
|
||||
validate: required(null),
|
||||
});
|
||||
|
||||
|
||||
@ -43,7 +43,8 @@ function AdHocPreviewStep({ hasErrors, values }) {
|
||||
([key, value]) =>
|
||||
key !== 'extra_vars' &&
|
||||
key !== 'execution_environment' &&
|
||||
key !== 'credential' && (
|
||||
key !== 'credentials' &&
|
||||
!key.startsWith('credential_passwords') && (
|
||||
<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 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user