From 3dfc9328a9b9599ef73f7990ab90aad730ad2aaf Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 4 Jun 2020 14:29:28 -0400 Subject: [PATCH] Dynamically render credential subform fields based on options responses for each credential type --- .../FieldWithPrompt/FieldWithPrompt.jsx | 7 +- .../CredentialAdd/CredentialAdd.jsx | 51 +- .../CredentialAdd/CredentialAdd.test.jsx | 32 +- .../CredentialEdit/CredentialEdit.jsx | 71 +- .../CredentialEdit/CredentialEdit.test.jsx | 32 +- .../Credential/shared/CredentialForm.jsx | 190 ++- .../Credential/shared/CredentialForm.test.jsx | 44 +- .../BecomeMethodField.jsx | 80 ++ .../CredentialFormFields/CredentialField.jsx | 167 +++ .../CredentialPluginField.jsx | 155 +++ .../CredentialPluginField.test.jsx | 20 +- .../CredentialPluginPrompt.jsx | 0 .../CredentialPluginPrompt.test.jsx | 14 +- .../CredentialsStep.jsx | 14 +- .../CredentialPluginPrompt/MetadataStep.jsx | 16 +- .../CredentialPluginPrompt/index.js | 0 .../CredentialPluginSelected.jsx | 4 +- .../CredentialPluginSelected.test.jsx | 4 +- .../CredentialPlugins/index.js | 0 .../GceFileUploadField.jsx | 89 ++ .../shared/CredentialFormFields/index.js | 3 + .../CredentialPluginField.jsx | 103 -- .../Credential/shared/CredentialSubForm.jsx | 74 + .../GoogleComputeEngineSubForm.jsx | 136 -- .../CredentialSubForms/ManualSubForm.jsx | 89 -- .../CredentialSubForms/SharedFields.jsx | 50 - .../SourceControlSubForm.jsx | 25 - .../shared/CredentialSubForms/index.js | 3 - .../shared/data.credentialTypes.json | 1200 ++++++++++++++++- awx/ui_next/src/types.js | 13 + 30 files changed, 2022 insertions(+), 664 deletions(-) create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/BecomeMethodField.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginField.jsx rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginField.test.jsx (85%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.jsx (100%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginPrompt/CredentialPluginPrompt.test.jsx (94%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx (83%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginPrompt/MetadataStep.jsx (89%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginPrompt/index.js (100%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginSelected.jsx (93%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/CredentialPluginSelected.test.jsx (87%) rename awx/ui_next/src/screens/Credential/shared/{ => CredentialFormFields}/CredentialPlugins/index.js (100%) create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/GceFileUploadField.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/index.js delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForm.jsx delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/GoogleComputeEngineSubForm.jsx delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/ManualSubForm.jsx delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx delete mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js 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 ( +