diff --git a/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx b/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
index 19ad76c796..b0d27ccc6e 100644
--- a/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
+++ b/awx/ui_next/src/components/FieldWithPrompt/FieldWithPrompt.jsx
@@ -7,16 +7,11 @@ import { CheckboxField, FieldTooltip } from '../FormField';
const FieldHeader = styled.div`
display: flex;
- justify-content: space-between;
- padding-bottom: var(--pf-c-form__label--PaddingBottom);
-
- label {
- --pf-c-form__label--PaddingBottom: 0px;
- }
`;
const StyledCheckboxField = styled(CheckboxField)`
--pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize);
+ margin-left: auto;
`;
function FieldWithPrompt({
diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
index c721b56789..da8cffdca0 100644
--- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx
@@ -25,17 +25,32 @@ function CredentialAdd({ me }) {
result: credentialId,
} = useRequest(
useCallback(
- async values => {
- const { inputs, organization, ...remainingValues } = values;
+ async (values, credentialTypesMap) => {
+ const {
+ inputs: { fields: possibleFields },
+ } = credentialTypesMap[values.credential_type];
+
+ const {
+ inputs,
+ organization,
+ passwordPrompts,
+ ...remainingValues
+ } = values;
+
const nonPluginInputs = {};
const pluginInputs = {};
- Object.entries(inputs).forEach(([key, value]) => {
- if (value.credential && value.inputs) {
- pluginInputs[key] = value;
+
+ possibleFields.forEach(field => {
+ const input = inputs[field.id];
+ if (input.credential && input.inputs) {
+ pluginInputs[field.id] = input;
+ } else if (passwordPrompts[field.id]) {
+ nonPluginInputs[field.id] = 'ASK';
} else {
- nonPluginInputs[key] = value;
+ nonPluginInputs[field.id] = input;
}
});
+
const {
data: { id: newCredentialId },
} = await CredentialsAPI.create({
@@ -44,18 +59,17 @@ function CredentialAdd({ me }) {
inputs: nonPluginInputs,
...remainingValues,
});
- const inputSourceRequests = [];
- Object.entries(pluginInputs).forEach(([key, value]) => {
- inputSourceRequests.push(
+
+ await Promise.all(
+ Object.entries(pluginInputs).map(([key, value]) =>
CredentialInputSourcesAPI.create({
input_field_name: key,
metadata: value.inputs,
source_credential: value.credential.id,
target_credential: newCredentialId,
})
- );
- });
- await Promise.all(inputSourceRequests);
+ )
+ );
return newCredentialId;
},
@@ -74,10 +88,13 @@ function CredentialAdd({ me }) {
try {
const {
data: { results: loadedCredentialTypes },
- } = await CredentialTypesAPI.read({
- or__namespace: ['gce', 'scm', 'ssh'],
- });
- setCredentialTypes(loadedCredentialTypes);
+ } = await CredentialTypesAPI.read();
+ setCredentialTypes(
+ loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
+ credentialTypesMap[credentialType.id] = credentialType;
+ return credentialTypesMap;
+ }, {})
+ );
} catch (err) {
setError(err);
} finally {
@@ -92,7 +109,7 @@ function CredentialAdd({ me }) {
};
const handleSubmit = async values => {
- await submitRequest(values);
+ await submitRequest(values, credentialTypes);
};
if (error) {
diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx
index c348c6e252..3f6e562434 100644
--- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.test.jsx
@@ -5,7 +5,6 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { sleep } from '../../../../testUtils/testUtils';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import CredentialAdd from './CredentialAdd';
@@ -175,23 +174,34 @@ describe('', () => {
});
test('handleSubmit should call the api and redirect to details page', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
-
- wrapper.find('CredentialForm').prop('onSubmit')({
- user: 1,
- organization: null,
- name: 'foo',
- description: 'bar',
- credential_type: '2',
- inputs: {},
+ await act(async () => {
+ wrapper.find('CredentialForm').prop('onSubmit')({
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {
+ username: '',
+ password: '',
+ ssh_key_data: '',
+ ssh_key_unlock: '',
+ },
+ passwordPrompts: {},
+ });
});
- await sleep(1);
expect(CredentialsAPI.create).toHaveBeenCalledWith({
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
- inputs: {},
+ inputs: {
+ username: '',
+ password: '',
+ ssh_key_data: '',
+ ssh_key_unlock: '',
+ },
});
expect(history.location.pathname).toBe('/credentials/13/details');
});
diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx
index aef18ea8e9..766f7146d9 100644
--- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx
@@ -1,7 +1,6 @@
import React, { useCallback, useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { object } from 'prop-types';
-
import { CardBody } from '../../../components/Card';
import {
CredentialsAPI,
@@ -22,8 +21,33 @@ function CredentialEdit({ credential, me }) {
const { error: submitError, request: submitRequest, result } = useRequest(
useCallback(
- async (values, inputSourceMap) => {
- const createAndUpdateInputSources = pluginInputs =>
+ async (values, credentialTypesMap, inputSourceMap) => {
+ const {
+ inputs: { fields: possibleFields },
+ } = credentialTypesMap[values.credential_type];
+
+ const {
+ inputs,
+ organization,
+ passwordPrompts,
+ ...remainingValues
+ } = values;
+
+ const nonPluginInputs = {};
+ const pluginInputs = {};
+
+ possibleFields.forEach(field => {
+ const input = inputs[field.id];
+ if (input.credential && input.inputs) {
+ pluginInputs[field.id] = input;
+ } else if (passwordPrompts[field.id]) {
+ nonPluginInputs[field.id] = 'ASK';
+ } else {
+ nonPluginInputs[field.id] = input;
+ }
+ });
+
+ const createAndUpdateInputSources = () =>
Object.entries(pluginInputs).map(([fieldName, fieldValue]) => {
if (!inputSourceMap[fieldName]) {
return CredentialInputSourcesAPI.create({
@@ -46,27 +70,15 @@ function CredentialEdit({ credential, me }) {
return null;
});
- const destroyInputSources = inputs => {
- const destroyRequests = [];
- Object.values(inputSourceMap).forEach(inputSource => {
+ const destroyInputSources = () =>
+ Object.values(inputSourceMap).map(inputSource => {
const { id, input_field_name } = inputSource;
if (!inputs[input_field_name]?.credential) {
- destroyRequests.push(CredentialInputSourcesAPI.destroy(id));
+ return CredentialInputSourcesAPI.destroy(id);
}
+ return null;
});
- return destroyRequests;
- };
- const { inputs, organization, ...remainingValues } = values;
- const nonPluginInputs = {};
- const pluginInputs = {};
- Object.entries(inputs).forEach(([key, value]) => {
- if (value.credential && value.inputs) {
- pluginInputs[key] = value;
- } else {
- nonPluginInputs[key] = value;
- }
- });
const [{ data }] = await Promise.all([
CredentialsAPI.update(credential.id, {
user: (me && me.id) || null,
@@ -74,12 +86,14 @@ function CredentialEdit({ credential, me }) {
inputs: nonPluginInputs,
...remainingValues,
}),
- ...destroyInputSources(inputs),
+ ...destroyInputSources(),
]);
- await Promise.all(createAndUpdateInputSources(pluginInputs));
+
+ await Promise.all(createAndUpdateInputSources());
+
return data;
},
- [credential.id, me]
+ [me, credential.id]
)
);
@@ -100,12 +114,15 @@ function CredentialEdit({ credential, me }) {
data: { results: loadedInputSources },
},
] = await Promise.all([
- CredentialTypesAPI.read({
- or__namespace: ['gce', 'scm', 'ssh'],
- }),
+ CredentialTypesAPI.read(),
CredentialsAPI.readInputSources(credential.id, { page_size: 200 }),
]);
- setCredentialTypes(loadedCredentialTypes);
+ setCredentialTypes(
+ loadedCredentialTypes.reduce((credentialTypesMap, credentialType) => {
+ credentialTypesMap[credentialType.id] = credentialType;
+ return credentialTypesMap;
+ }, {})
+ );
setInputSources(
loadedInputSources.reduce((inputSourcesMap, inputSource) => {
inputSourcesMap[inputSource.input_field_name] = inputSource;
@@ -127,7 +144,7 @@ function CredentialEdit({ credential, me }) {
};
const handleSubmit = async values => {
- await submitRequest(values, inputSources);
+ await submitRequest(values, credentialTypes, inputSources);
};
if (error) {
diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx
index 3d4ce756cb..b586735035 100644
--- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.test.jsx
@@ -5,7 +5,6 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { sleep } from '../../../../testUtils/testUtils';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import CredentialEdit from './CredentialEdit';
@@ -279,23 +278,34 @@ describe('', () => {
test('handleSubmit should post to the api', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0);
-
- wrapper.find('CredentialForm').prop('onSubmit')({
- user: 1,
- organization: null,
- name: 'foo',
- description: 'bar',
- credential_type: '2',
- inputs: {},
+ await act(async () => {
+ wrapper.find('CredentialForm').prop('onSubmit')({
+ user: 1,
+ organization: null,
+ name: 'foo',
+ description: 'bar',
+ credential_type: '2',
+ inputs: {
+ username: '',
+ password: '',
+ ssh_key_data: '',
+ ssh_key_unlock: '',
+ },
+ passwordPrompts: {},
+ });
});
- await sleep(1);
expect(CredentialsAPI.update).toHaveBeenCalledWith(3, {
user: 1,
organization: null,
name: 'foo',
description: 'bar',
credential_type: '2',
- inputs: {},
+ inputs: {
+ username: '',
+ password: '',
+ ssh_key_data: '',
+ ssh_key_unlock: '',
+ },
});
expect(history.location.pathname).toBe('/credentials/3/details');
});
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx
index ddd4ccaa5f..c215b7f8b4 100644
--- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx
@@ -3,30 +3,20 @@ import { Formik, useField } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { arrayOf, func, object, shape } from 'prop-types';
-import { Form, FormGroup, Title } from '@patternfly/react-core';
+import { Form, FormGroup } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
import AnsibleSelect from '../../../components/AnsibleSelect';
import { required } from '../../../util/validators';
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
-import {
- FormColumnLayout,
- SubFormLayout,
-} from '../../../components/FormLayout';
-import {
- GoogleComputeEngineSubForm,
- ManualSubForm,
- SourceControlSubForm,
-} from './CredentialSubForms';
+import { FormColumnLayout } from '../../../components/FormLayout';
+import CredentialSubForm from './CredentialSubForm';
function CredentialFormFields({
i18n,
credentialTypes,
formik,
- gceCredentialTypeId,
initialValues,
- scmCredentialTypeId,
- sshCredentialTypeId,
}) {
const [orgField, orgMeta, orgHelpers] = useField('organization');
const [credTypeField, credTypeMeta, credTypeHelpers] = useField({
@@ -34,23 +24,52 @@ function CredentialFormFields({
validate: required(i18n._(t`Select a value for this field`), i18n),
});
- const credentialTypeOptions = Object.keys(credentialTypes).map(key => {
- return {
- value: credentialTypes[key].id,
- key: credentialTypes[key].kind,
- label: credentialTypes[key].name,
- };
- });
+ const credentialTypeOptions = Object.keys(credentialTypes)
+ .map(key => {
+ return {
+ value: credentialTypes[key].id,
+ key: credentialTypes[key].id,
+ label: credentialTypes[key].name,
+ };
+ })
+ .sort((a, b) => (a.label > b.label ? 1 : -1));
- const resetSubFormFields = (value, form) => {
- Object.keys(form.initialValues.inputs).forEach(label => {
- if (parseInt(value, 10) === form.initialValues.credential_type) {
- form.setFieldValue(`inputs.${label}`, initialValues.inputs[label]);
- } else {
- form.setFieldValue(`inputs.${label}`, '');
+ const resetSubFormFields = (newCredentialType, form) => {
+ credentialTypes[newCredentialType].inputs.fields.forEach(
+ ({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
+ if (
+ parseInt(newCredentialType, 10) === form.initialValues.credential_type
+ ) {
+ form.setFieldValue(`inputs.${id}`, initialValues.inputs[id]);
+ if (ask_at_runtime) {
+ form.setFieldValue(
+ `passwordPrompts.${id}`,
+ initialValues.passwordPrompts[id]
+ );
+ }
+ } else {
+ switch (type) {
+ case 'string':
+ form.setFieldValue(`inputs.${id}`, defaultValue || '');
+ break;
+ case 'boolean':
+ form.setFieldValue(`inputs.${id}`, defaultValue || false);
+ break;
+ default:
+ break;
+ }
+
+ if (choices) {
+ form.setFieldValue(`inputs.${id}`, defaultValue);
+ }
+
+ if (ask_at_runtime) {
+ form.setFieldValue(`passwordPrompts.${id}`, false);
+ }
+ }
+ form.setFieldTouched(`inputs.${id}`, false);
}
- form.setFieldTouched(`inputs.${label}`, false);
- });
+ );
};
return (
@@ -106,16 +125,9 @@ function CredentialFormFields({
/>
{credTypeField.value !== undefined && credTypeField.value !== '' && (
-
- {i18n._(t`Type Details`)}
- {
- {
- [gceCredentialTypeId]: ,
- [sshCredentialTypeId]: ,
- [scmCredentialTypeId]: ,
- }[credTypeField.value]
- }
-
+
)}
>
);
@@ -135,19 +147,43 @@ function CredentialForm({
description: credential.description || '',
organization: credential?.summary_fields?.organization || null,
credential_type: credential.credential_type || '',
- inputs: {
- become_method: credential?.inputs?.become_method || '',
- become_password: credential?.inputs?.become_password || '',
- become_username: credential?.inputs?.become_username || '',
- password: credential?.inputs?.password || '',
- project: credential?.inputs?.project || '',
- ssh_key_data: credential?.inputs?.ssh_key_data || '',
- ssh_key_unlock: credential?.inputs?.ssh_key_unlock || '',
- ssh_public_key_data: credential?.inputs?.ssh_public_key_data || '',
- username: credential?.inputs?.username || '',
- },
+ inputs: {},
+ passwordPrompts: {},
};
+ Object.values(credentialTypes).forEach(credentialType => {
+ credentialType.inputs.fields.forEach(
+ ({ ask_at_runtime, type, id, choices, default: defaultValue }) => {
+ if (credential?.inputs && credential.inputs[id]) {
+ if (ask_at_runtime) {
+ initialValues.passwordPrompts[id] =
+ credential.inputs[id] === 'ASK' || false;
+ }
+ initialValues.inputs[id] = credential.inputs[id];
+ } else {
+ switch (type) {
+ case 'string':
+ initialValues.inputs[id] = defaultValue || '';
+ break;
+ case 'boolean':
+ initialValues.inputs[id] = defaultValue || false;
+ break;
+ default:
+ break;
+ }
+
+ if (choices) {
+ initialValues.inputs[id] = defaultValue;
+ }
+
+ if (ask_at_runtime) {
+ initialValues.passwordPrompts[id] = false;
+ }
+ }
+ }
+ );
+ });
+
Object.values(inputSources).forEach(inputSource => {
initialValues.inputs[inputSource.input_field_name] = {
credential: inputSource.summary_fields.source_credential,
@@ -155,60 +191,10 @@ function CredentialForm({
};
});
- const scmCredentialTypeId = Object.keys(credentialTypes)
- .filter(key => credentialTypes[key].namespace === 'scm')
- .map(key => credentialTypes[key].id)[0];
- const sshCredentialTypeId = Object.keys(credentialTypes)
- .filter(key => credentialTypes[key].namespace === 'ssh')
- .map(key => credentialTypes[key].id)[0];
- const gceCredentialTypeId = Object.keys(credentialTypes)
- .filter(key => credentialTypes[key].namespace === 'gce')
- .map(key => credentialTypes[key].id)[0];
-
return (
{
- const scmKeys = [
- 'username',
- 'password',
- 'ssh_key_data',
- 'ssh_key_unlock',
- ];
- const sshKeys = [
- 'username',
- 'password',
- 'ssh_key_data',
- 'ssh_public_key_data',
- 'ssh_key_unlock',
- 'become_method',
- 'become_username',
- 'become_password',
- ];
- const gceKeys = ['username', 'ssh_key_data', 'project'];
- if (parseInt(values.credential_type, 10) === scmCredentialTypeId) {
- Object.keys(values.inputs).forEach(key => {
- if (scmKeys.indexOf(key) < 0) {
- delete values.inputs[key];
- }
- });
- } else if (
- parseInt(values.credential_type, 10) === sshCredentialTypeId
- ) {
- Object.keys(values.inputs).forEach(key => {
- if (sshKeys.indexOf(key) < 0) {
- delete values.inputs[key];
- }
- });
- } else if (
- parseInt(values.credential_type, 10) === gceCredentialTypeId
- ) {
- Object.keys(values.inputs).forEach(key => {
- if (gceKeys.indexOf(key) < 0) {
- delete values.inputs[key];
- }
- });
- }
onSubmit(values);
}}
>
@@ -219,9 +205,6 @@ function CredentialForm({
formik={formik}
initialValues={initialValues}
credentialTypes={credentialTypes}
- gceCredentialTypeId={gceCredentialTypeId}
- scmCredentialTypeId={scmCredentialTypeId}
- sshCredentialTypeId={sshCredentialTypeId}
{...rest}
/>
@@ -239,13 +222,16 @@ function CredentialForm({
CredentialForm.proptype = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
+ credentialTypes: shape({}).isRequired,
credential: shape({}),
inputSources: arrayOf(object),
+ submitError: shape({}),
};
CredentialForm.defaultProps = {
credential: {},
inputSources: [],
+ submitError: null,
};
export default withI18n()(CredentialForm);
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx
index c77089b758..2062ed01ee 100644
--- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx
@@ -4,11 +4,19 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import machineCredential from './data.machineCredential.json';
import gceCredential from './data.gceCredential.json';
import scmCredential from './data.scmCredential.json';
-import credentialTypes from './data.credentialTypes.json';
+import credentialTypesArr from './data.credentialTypes.json';
import CredentialForm from './CredentialForm';
jest.mock('../../../api');
+const credentialTypes = credentialTypesArr.reduce(
+ (credentialTypesMap, credentialType) => {
+ credentialTypesMap[credentialType.id] = credentialType;
+ return credentialTypesMap;
+ },
+ {}
+);
+
describe('', () => {
let wrapper;
const onCancel = jest.fn();
@@ -28,23 +36,19 @@ describe('', () => {
expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
- expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
+ expect(wrapper.find('input#credential-password').length).toBe(1);
expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Signed SSH Certificate"]').length
).toBe(1);
+ expect(wrapper.find('input#credential-ssh_key_unlock').length).toBe(1);
expect(
- wrapper.find('FormGroup[label="Private Key Passphrase"]').length
- ).toBe(1);
- expect(
- wrapper.find('FormGroup[label="Privelege Escalation Method"]').length
+ wrapper.find('FormGroup[label="Privilege Escalation Method"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Privilege Escalation Username"]').length
).toBe(1);
- expect(
- wrapper.find('FormGroup[label="Privilege Escalation Password"]').length
- ).toBe(1);
+ expect(wrapper.find('input#credential-become_password').length).toBe(1);
};
const sourceFieldExpects = () => {
@@ -55,7 +59,7 @@ describe('', () => {
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
- expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="SCM Private Key"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Private Key Passphrase"]').length
).toBe(1);
@@ -71,10 +75,10 @@ describe('', () => {
wrapper.find('FormGroup[label="Service account JSON file"]').length
).toBe(1);
expect(
- wrapper.find('FormGroup[label="Service account email address"]').length
+ wrapper.find('FormGroup[label="Service Account Email Address"]').length
).toBe(1);
expect(wrapper.find('FormGroup[label="Project"]').length).toBe(1);
- expect(wrapper.find('FormGroup[label="RSA private key"]').length).toBe(1);
+ expect(wrapper.find('FormGroup[label="RSA Private Key"]').length).toBe(1);
};
describe('Add', () => {
@@ -152,9 +156,9 @@ describe('', () => {
gceFieldExpects();
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
- expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
- ''
- );
+ expect(
+ wrapper.find('textarea#credential-ssh_key_data').prop('value')
+ ).toBe('');
await act(async () => {
wrapper.find('FileUpload').invoke('onChange')({
name: 'foo.json',
@@ -169,7 +173,9 @@ describe('', () => {
expect(wrapper.find('input#credential-project').prop('value')).toBe(
'test123'
);
- expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
+ expect(
+ wrapper.find('textarea#credential-ssh_key_data').prop('value')
+ ).toBe(
'-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n'
);
});
@@ -180,9 +186,9 @@ describe('', () => {
wrapper.update();
expect(wrapper.find('input#credential-username').prop('value')).toBe('');
expect(wrapper.find('input#credential-project').prop('value')).toBe('');
- expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe(
- ''
- );
+ expect(
+ wrapper.find('textarea#credential-ssh_key_data').prop('value')
+ ).toBe('');
});
test('should show error when error thrown parsing JSON', async () => {
expect(wrapper.find('#credential-gce-file-helper').text()).toBe(
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/BecomeMethodField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/BecomeMethodField.jsx
new file mode 100644
index 0000000000..ee97e43d27
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/BecomeMethodField.jsx
@@ -0,0 +1,80 @@
+import React, { useState } from 'react';
+import { useField } from 'formik';
+import { bool, shape, string } from 'prop-types';
+import {
+ FormGroup,
+ Select,
+ SelectOption,
+ SelectVariant,
+} from '@patternfly/react-core';
+import { FieldTooltip } from '../../../../components/FormField';
+
+function BecomeMethodField({ fieldOptions, isRequired }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [options, setOptions] = useState(
+ [
+ 'sudo',
+ 'su',
+ 'pbrun',
+ 'pfexec',
+ 'dzdo',
+ 'pmrun',
+ 'runas',
+ 'enable',
+ 'doas',
+ 'ksu',
+ 'machinectl',
+ 'sesu',
+ ].map(val => ({ value: val }))
+ );
+ const [becomeMethodField, meta, helpers] = useField({
+ name: `inputs.${fieldOptions.id}`,
+ });
+ return (
+
+ {fieldOptions.help_text && (
+
+ )}
+
+
+ );
+}
+BecomeMethodField.propTypes = {
+ fieldOptions: shape({
+ id: string.isRequired,
+ label: string.isRequired,
+ }).isRequired,
+ isRequired: bool,
+};
+BecomeMethodField.defaultProps = {
+ isRequired: false,
+};
+
+export default BecomeMethodField;
diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx
new file mode 100644
index 0000000000..da7c94729c
--- /dev/null
+++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { useField, useFormikContext } from 'formik';
+import { shape, string } from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+import {
+ FormGroup,
+ InputGroup,
+ TextArea,
+ TextInput,
+} from '@patternfly/react-core';
+import { FieldTooltip, PasswordInput } from '../../../../components/FormField';
+import AnsibleSelect from '../../../../components/AnsibleSelect';
+import { CredentialType } from '../../../../types';
+import { required } from '../../../../util/validators';
+import { CredentialPluginField } from './CredentialPlugins';
+import BecomeMethodField from './BecomeMethodField';
+
+function CredentialInput({ fieldOptions, credentialKind, ...rest }) {
+ const [subFormField, meta] = useField(`inputs.${fieldOptions.id}`);
+ const isValid = !(meta.touched && meta.error);
+ if (fieldOptions.multiline) {
+ return (
+