From 4a9d39c3faf6bc61e363958262db61fce55c02bc Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 14 Apr 2020 09:55:30 -0400 Subject: [PATCH] Adds support for GCE credentials in credential form(s) --- .../src/components/FormField/FormField.jsx | 5 + .../CredentialAdd/CredentialAdd.jsx | 14 +- .../CredentialEdit/CredentialEdit.jsx | 4 +- .../Credential/shared/CredentialForm.jsx | 38 +- .../Credential/shared/CredentialForm.test.jsx | 489 +++++++++++++----- .../GoogleComputeEngineSubForm.jsx | 120 +++++ .../CredentialSubForms/SharedFields.jsx | 1 + .../SourceControlSubForm.jsx | 12 +- .../shared/CredentialSubForms/index.js | 3 + 9 files changed, 530 insertions(+), 156 deletions(-) create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialSubForms/GoogleComputeEngineSubForm.jsx diff --git a/awx/ui_next/src/components/FormField/FormField.jsx b/awx/ui_next/src/components/FormField/FormField.jsx index c94c370e47..096960ef39 100644 --- a/awx/ui_next/src/components/FormField/FormField.jsx +++ b/awx/ui_next/src/components/FormField/FormField.jsx @@ -7,6 +7,7 @@ import FieldTooltip from './FieldTooltip'; function FormField(props) { const { id, + helperText, name, label, tooltip, @@ -25,6 +26,7 @@ function FormField(props) { {(type === 'textarea' && ( {}, isRequired: false, diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx index 092abd1e1c..e42b3faec7 100644 --- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx @@ -20,7 +20,9 @@ function CredentialAdd({ me }) { try { const { data: { results: loadedCredentialTypes }, - } = await CredentialTypesAPI.read({ or__kind: ['scm', 'ssh'] }); + } = await CredentialTypesAPI.read({ + or__namespace: ['gce', 'scm', 'ssh'], + }); setCredentialTypes(loadedCredentialTypes); } catch (err) { setError(err); @@ -65,7 +67,15 @@ function CredentialAdd({ me }) { ); } if (isLoading) { - return ; + return ( + + + + + + + + ); } return ( diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx index 636431d3fd..71409638f6 100644 --- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx @@ -20,7 +20,9 @@ function CredentialEdit({ credential, me }) { try { const { data: { results: loadedCredentialTypes }, - } = await CredentialTypesAPI.read({ or__kind: ['scm', 'ssh'] }); + } = await CredentialTypesAPI.read({ + or__namespace: ['gce', 'scm', 'ssh'], + }); setCredentialTypes(loadedCredentialTypes); } catch (err) { setError(err); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx index ef8dc686b5..466dd43bf3 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx @@ -13,12 +13,17 @@ import { FormColumnLayout, SubFormLayout, } from '../../../components/FormLayout'; -import { ManualSubForm, SourceControlSubForm } from './CredentialSubForms'; +import { + GoogleComputeEngineSubForm, + ManualSubForm, + SourceControlSubForm, +} from './CredentialSubForms'; function CredentialFormFields({ i18n, credentialTypes, formik, + gceCredentialTypeId, initialValues, scmCredentialTypeId, sshCredentialTypeId, @@ -106,6 +111,7 @@ function CredentialFormFields({ {i18n._(t`Type Details`)} { { + [gceCredentialTypeId]: , [sshCredentialTypeId]: , [scmCredentialTypeId]: , }[formik.values.credential_type] @@ -130,22 +136,26 @@ function CredentialForm({ organization: credential?.summary_fields?.organization || null, credential_type: credential.credential_type || '', inputs: { - username: credential?.inputs?.username || '', - password: credential?.inputs?.password || '', - ssh_key_data: credential?.inputs?.ssh_key_data || '', - ssh_public_key_data: credential?.inputs?.ssh_public_key_data || '', - ssh_key_unlock: credential?.inputs?.ssh_key_unlock || '', become_method: credential?.inputs?.become_method || '', - become_username: credential?.inputs?.become_username || '', 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 || '', }, }; const scmCredentialTypeId = Object.keys(credentialTypes) - .filter(key => credentialTypes[key].kind === 'scm') + .filter(key => credentialTypes[key].namespace === 'scm') .map(key => credentialTypes[key].id)[0]; const sshCredentialTypeId = Object.keys(credentialTypes) - .filter(key => credentialTypes[key].kind === 'ssh') + .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 ( @@ -168,6 +178,7 @@ function CredentialForm({ '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) { @@ -182,6 +193,14 @@ function CredentialForm({ 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); }} @@ -193,6 +212,7 @@ function CredentialForm({ formik={formik} initialValues={initialValues} credentialTypes={credentialTypes} + gceCredentialTypeId={gceCredentialTypeId} scmCredentialTypeId={scmCredentialTypeId} sshCredentialTypeId={sshCredentialTypeId} {...rest} 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 de441f954a..dd517529c2 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx @@ -4,6 +4,8 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import CredentialForm from './CredentialForm'; +jest.mock('@api'); + const machineCredential = { id: 3, type: 'credential', @@ -180,6 +182,104 @@ const sourceControlCredential = { kubernetes: false, }; +const gceCredential = { + id: 9, + type: 'credential', + url: '/api/v2/credentials/9/', + related: { + named_url: + '/api/v2/credentials/a gce cred++Google Compute Engine+cloud++Default/', + created_by: '/api/v2/users/1/', + modified_by: '/api/v2/users/1/', + organization: '/api/v2/organizations/4/', + activity_stream: '/api/v2/credentials/9/activity_stream/', + access_list: '/api/v2/credentials/9/access_list/', + object_roles: '/api/v2/credentials/9/object_roles/', + owner_users: '/api/v2/credentials/9/owner_users/', + owner_teams: '/api/v2/credentials/9/owner_teams/', + copy: '/api/v2/credentials/9/copy/', + input_sources: '/api/v2/credentials/9/input_sources/', + credential_type: '/api/v2/credential_types/10/', + }, + summary_fields: { + organization: { + id: 4, + name: 'Default', + description: '', + }, + credential_type: { + id: 10, + name: 'Google Compute Engine', + description: '', + }, + created_by: { + id: 1, + username: 'admin', + first_name: '', + last_name: '', + }, + modified_by: { + id: 1, + username: 'admin', + first_name: '', + last_name: '', + }, + object_roles: { + admin_role: { + description: 'Can manage all aspects of the credential', + name: 'Admin', + id: 287, + }, + use_role: { + description: 'Can use the credential in a job template', + name: 'Use', + id: 288, + }, + read_role: { + description: 'May view settings for the credential', + name: 'Read', + id: 289, + }, + }, + user_capabilities: { + edit: true, + delete: true, + copy: true, + use: true, + }, + owners: [ + { + id: 1, + type: 'user', + name: 'admin', + description: ' ', + url: '/api/v2/users/1/', + }, + { + id: 4, + type: 'organization', + name: 'Default', + description: '', + url: '/api/v2/organizations/4/', + }, + ], + }, + created: '2020-04-13T17:33:27.625773Z', + modified: '2020-04-13T17:33:27.625882Z', + name: 'a gce cred', + description: '', + organization: 4, + credential_type: 10, + inputs: { + project: 'test123', + username: 'test123.iam.gserviceaccount.com', + ssh_key_data: '$encrypted$', + }, + kind: 'gce', + cloud: true, + kubernetes: false, +}; + const credentialTypes = [ { id: 2, @@ -313,36 +413,71 @@ const credentialTypes = [ }, injectors: {}, }, + { + id: 10, + type: 'credential_type', + url: '/api/v2/credential_types/10/', + related: { + credentials: '/api/v2/credential_types/10/credentials/', + activity_stream: '/api/v2/credential_types/10/activity_stream/', + }, + summary_fields: { + user_capabilities: { + edit: false, + delete: false, + }, + }, + created: '2020-04-09T19:20:27.090665Z', + modified: '2020-04-09T19:21:11.575214Z', + name: 'Google Compute Engine', + description: '', + kind: 'cloud', + namespace: 'gce', + managed_by_tower: true, + inputs: { + fields: [ + { + id: 'username', + label: 'Service Account Email Address', + type: 'string', + help_text: + 'The email address assigned to the Google Compute Engine service account.', + }, + { + id: 'project', + label: 'Project', + type: 'string', + help_text: + 'The Project ID is the GCE assigned identification. It is often constructed as three words or two words followed by a three-digit number. Examples: project-id-000 and another-project-id', + }, + { + id: 'ssh_key_data', + label: 'RSA Private Key', + type: 'string', + format: 'ssh_private_key', + secret: true, + multiline: true, + help_text: + 'Paste the contents of the PEM file associated with the service account email.', + }, + ], + required: ['username', 'ssh_key_data'], + }, + injectors: {}, + }, ]; describe('', () => { let wrapper; - let onCancel; - let onSubmit; + const onCancel = jest.fn(); + const onSubmit = jest.fn(); const addFieldExpects = () => { + expect(wrapper.find('FormGroup').length).toBe(4); expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); 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(0); - expect(wrapper.find('FormGroup[label="Password"]').length).toBe(0); - expect(wrapper.find('FormGroup[label="SSH Private Key"]').length).toBe(0); - expect( - wrapper.find('FormGroup[label="Signed SSH Certificate"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Private Key Passphrase"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Privelege Escalation Method"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Privilege Escalation Username"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Privilege Escalation Password"]').length - ).toBe(0); }; const machineFieldExpects = () => { @@ -371,6 +506,7 @@ describe('', () => { }; const sourceFieldExpects = () => { + expect(wrapper.find('FormGroup').length).toBe(8); expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1); @@ -378,139 +514,218 @@ describe('', () => { 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="Signed SSH Certificate"]').length - ).toBe(0); expect( wrapper.find('FormGroup[label="Private Key Passphrase"]').length ).toBe(1); - expect( - wrapper.find('FormGroup[label="Privelege Escalation Method"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Privilege Escalation Username"]').length - ).toBe(0); - expect( - wrapper.find('FormGroup[label="Privilege Escalation Password"]').length - ).toBe(0); }; - beforeEach(() => { - onCancel = jest.fn(); - onSubmit = jest.fn(); - wrapper = mountWithContexts( - - ); - }); + const gceFieldExpects = () => { + expect(wrapper.find('FormGroup').length).toBe(8); + expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1); + expect( + wrapper.find('FormGroup[label="Service account JSON file"]').length + ).toBe(1); + expect( + 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); + }; - afterEach(() => { - wrapper.unmount(); - }); - - test('Initially renders successfully', () => { - expect(wrapper.length).toBe(1); - }); - - test('should display form fields on add properly', () => { - wrapper = mountWithContexts( - - ); - addFieldExpects(); - }); - - test('should display form fields for machine credential properly', () => { - wrapper = mountWithContexts( - - ); - machineFieldExpects(); - }); - - test('should display form fields for source control credential properly', () => { - wrapper = mountWithContexts( - - ); - sourceFieldExpects(); - }); - - test('should update form values', async () => { - // name and description change - act(() => { - wrapper.find('input#credential-name').simulate('change', { - target: { value: 'new Foo', name: 'name' }, - }); - wrapper.find('input#credential-description').simulate('change', { - target: { value: 'new Bar', name: 'description' }, + describe('Add', () => { + beforeAll(async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); }); }); - wrapper.update(); - expect(wrapper.find('input#credential-name').prop('value')).toEqual( - 'new Foo' - ); - expect(wrapper.find('input#credential-description').prop('value')).toEqual( - 'new Bar' - ); - // organization change - act(() => { - wrapper.find('OrganizationLookup').invoke('onBlur')(); - wrapper.find('OrganizationLookup').invoke('onChange')({ + afterAll(() => { + wrapper.unmount(); + }); + test('should display form fields on add properly', async () => { + addFieldExpects(); + }); + test('should update form values', async () => { + // name and description change + await act(async () => { + wrapper.find('input#credential-name').simulate('change', { + target: { value: 'new Foo', name: 'name' }, + }); + wrapper.find('input#credential-description').simulate('change', { + target: { value: 'new Bar', name: 'description' }, + }); + }); + wrapper.update(); + expect(wrapper.find('input#credential-name').prop('value')).toEqual( + 'new Foo' + ); + expect( + wrapper.find('input#credential-description').prop('value') + ).toEqual('new Bar'); + // organization change + await act(async () => { + wrapper.find('OrganizationLookup').invoke('onBlur')(); + wrapper.find('OrganizationLookup').invoke('onChange')({ + id: 3, + name: 'organization', + }); + }); + wrapper.update(); + expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ id: 3, name: 'organization', }); }); - wrapper.update(); - expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ - id: 3, - name: 'organization', + test('should display cred type subform when scm type select has a value', async () => { + await act(async () => { + await wrapper + .find('AnsibleSelect[id="credential_type"]') + .invoke('onChange')(null, 1); + }); + wrapper.update(); + machineFieldExpects(); + await act(async () => { + await wrapper + .find('AnsibleSelect[id="credential_type"]') + .invoke('onChange')(null, 2); + }); + wrapper.update(); + sourceFieldExpects(); + }); + test('should update expected fields when gce service account json file uploaded', async () => { + await act(async () => { + await wrapper + .find('AnsibleSelect[id="credential_type"]') + .invoke('onChange')(null, 10); + }); + wrapper.update(); + 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( + '' + ); + await act(async () => { + wrapper.find('FileUpload').invoke('onChange')({ + name: 'foo.json', + text: () => + '{"client_email":"testemail@ansible.com","project_id":"test123","private_key":"-----BEGIN PRIVATE KEY-----\\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n-----END PRIVATE KEY-----\\n"}', + }); + }); + wrapper.update(); + expect(wrapper.find('input#credential-username').prop('value')).toBe( + 'testemail@ansible.com' + ); + expect(wrapper.find('input#credential-project').prop('value')).toBe( + 'test123' + ); + expect(wrapper.find('textarea#credential-sshKeyData').prop('value')).toBe( + '-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n' + ); + }); + test('should clear expected fields when file clear button clicked', async () => { + await act(async () => { + wrapper.find('FileUploadField').invoke('onClearButtonClick')(); + }); + 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( + '' + ); + }); + test('should show error when error thrown parsing JSON', async () => { + expect(wrapper.find('#credential-gce-file-helper').text()).toBe( + 'Select a JSON formatted service account key to autopopulate the following fields.' + ); + await act(async () => { + wrapper.find('FileUpload').invoke('onChange')({ + name: 'foo.json', + text: () => '{not good json}', + }); + }); + wrapper.update(); + expect(wrapper.find('#credential-gce-file-helper').text()).toBe( + 'There was an error parsing the file. Please check the file formatting and try again.' + ); + }); + test('should call handleCancel when Cancel button is clicked', async () => { + expect(onCancel).not.toHaveBeenCalled(); + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + expect(onCancel).toBeCalled(); }); }); - test('should display cred type subform when scm type select has a value', async () => { - wrapper = mountWithContexts( - - ); - addFieldExpects(); - await act(async () => { - await wrapper - .find('AnsibleSelect[id="credential_type"]') - .invoke('onChange')(null, 1); + describe('Edit', () => { + afterEach(() => { + wrapper.unmount(); }); - wrapper.update(); - machineFieldExpects(); - await act(async () => { - await wrapper - .find('AnsibleSelect[id="credential_type"]') - .invoke('onChange')(null, 2); - }); - wrapper.update(); - sourceFieldExpects(); - }); + test('Initially renders successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); - test('should call handleCancel when Cancel button is clicked', async () => { - expect(onCancel).not.toHaveBeenCalled(); - wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); - expect(onCancel).toBeCalled(); + expect(wrapper.length).toBe(1); + }); + + test('should display form fields for machine credential properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + machineFieldExpects(); + }); + + test('should display form fields for source control credential properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + sourceFieldExpects(); + }); + + test('should display form fields for gce credential properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + gceFieldExpects(); + }); }); }); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/GoogleComputeEngineSubForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/GoogleComputeEngineSubForm.jsx new file mode 100644 index 0000000000..d1050b9375 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/GoogleComputeEngineSubForm.jsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useField } from 'formik'; +import { FileUpload, FormGroup } from '@patternfly/react-core'; +import FormField from '@components/FormField'; +import { FormColumnLayout, FormFullWidthLayout } from '@components/FormLayout'; +import { required } from '@util/validators'; + +const GoogleComputeEngineSubForm = ({ i18n }) => { + const [fileError, setFileError] = useState(null); + const [filename, setFilename] = useState(''); + const [file, setFile] = useState(''); + const inputsUsernameHelpers = useField({ + name: 'inputs.username', + })[2]; + const inputsProjectHelpers = useField({ + name: 'inputs.project', + })[2]; + const inputsSSHKeyDataHelpers = useField({ + name: 'inputs.ssh_key_data', + })[2]; + + return ( + + + { + if (value) { + try { + setFile(value); + setFilename(value.name); + const fileText = await value.text(); + const fileJSON = JSON.parse(fileText); + if ( + !fileJSON.client_email && + !fileJSON.project_id && + !fileJSON.private_key + ) { + setFileError( + i18n._( + t`Expected at least one of client_email, project_id or private_key to be present in the file.` + ) + ); + } else { + inputsUsernameHelpers.setValue(fileJSON.client_email || ''); + inputsProjectHelpers.setValue(fileJSON.project_id || ''); + inputsSSHKeyDataHelpers.setValue(fileJSON.private_key || ''); + setFileError(null); + } + } catch { + setFileError( + i18n._( + t`There was an error parsing the file. Please check the file formatting and try again.` + ) + ); + } + } else { + setFile(''); + setFilename(''); + inputsUsernameHelpers.setValue(''); + inputsProjectHelpers.setValue(''); + inputsSSHKeyDataHelpers.setValue(''); + setFileError(null); + } + }} + dropzoneProps={{ + accept: '.json', + onDropRejected: () => { + setFileError( + i18n._( + t`File upload rejected. Please select a single .json file.` + ) + ); + }, + }} + /> + + + + + + + + ); +}; + +export default withI18n()(GoogleComputeEngineSubForm); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx index 0073d30a1f..51a95292bd 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SharedFields.jsx @@ -26,6 +26,7 @@ export const SSHKeyDataField = withI18n()(({ i18n }) => ( label={i18n._(t`SSH Private Key`)} name="inputs.ssh_key_data" type="textarea" + rows={6} /> )); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx index 9bd6cfa19a..49bdc9fcfc 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/SourceControlSubForm.jsx @@ -12,16 +12,14 @@ import { } from './SharedFields'; const SourceControlSubForm = () => ( - <> - - - - - + + + + - + ); export default withI18n()(SourceControlSubForm); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js index 04cad7240e..8c6d7d19f4 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js +++ b/awx/ui_next/src/screens/Credential/shared/CredentialSubForms/index.js @@ -1,2 +1,5 @@ +export { + default as GoogleComputeEngineSubForm, +} from './GoogleComputeEngineSubForm'; export { default as ManualSubForm } from './ManualSubForm'; export { default as SourceControlSubForm } from './SourceControlSubForm';