From 65bdf0baf731e12dfc374be43bd81a5581bd2274 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 19 Feb 2021 14:05:41 -0500 Subject: [PATCH] Add support for replace/revert on secret credential fields --- .../CredentialAdd/CredentialAdd.jsx | 2 +- .../CredentialEdit/CredentialEdit.jsx | 2 +- .../Credential/shared/CredentialForm.jsx | 4 +- .../CredentialFormFields/CredentialField.jsx | 89 ++++++++++++-- .../CredentialField.test.jsx | 110 ++++++++++++++++++ .../CredentialPluginField.jsx | 14 ++- 6 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.test.jsx diff --git a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx index ecffa873c1..e08c8fe470 100644 --- a/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialAdd/CredentialAdd.jsx @@ -127,7 +127,7 @@ function CredentialAdd({ me }) { ); } - if (isLoading && !result) { + if (isLoading) { return ( diff --git a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx index 1b629104d6..00fa904fb7 100644 --- a/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialEdit/CredentialEdit.jsx @@ -178,7 +178,7 @@ function CredentialEdit({ credential }) { return ; } - if (isLoading && !credentialTypes) { + if (isLoading) { return ; } diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx index 8972fadb89..e43f67683c 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { func, shape } from 'prop-types'; +import { shape } from 'prop-types'; import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -315,8 +315,6 @@ function CredentialForm({ } CredentialForm.propTypes = { - handleSubmit: func.isRequired, - handleCancel: func.isRequired, credentialTypes: shape({}).isRequired, credential: shape({}), inputSources: shape({}), diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx index 70016e7068..d0665186a9 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx @@ -5,11 +5,15 @@ import styled from 'styled-components'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { + Button, + ButtonVariant, FileUpload as PFFileUpload, FormGroup, InputGroup, TextInput, + Tooltip, } from '@patternfly/react-core'; +import { PficonHistoryIcon } from '@patternfly/react-icons'; import { PasswordInput } from '../../../../components/FormField'; import AnsibleSelect from '../../../../components/AnsibleSelect'; import Popover from '../../../../components/Popover'; @@ -22,20 +26,79 @@ const FileUpload = styled(PFFileUpload)` flex-grow: 1; `; -function CredentialInput({ fieldOptions, credentialKind, ...rest }) { +function CredentialInput({ i18n, fieldOptions, credentialKind, ...rest }) { const [fileName, setFileName] = useState(''); const [fileIsUploading, setFileIsUploading] = useState(false); const [subFormField, meta, helpers] = useField(`inputs.${fieldOptions.id}`); const isValid = !(meta.touched && meta.error); + + const RevertReplaceButton = ( + <> + {meta.initialValue && + meta.initialValue !== '' && + !meta.initialValue.credential && ( + + + + )} + + ); + if (fieldOptions.multiline) { const handleFileChange = (value, filename) => { helpers.setValue(value); setFileName(filename); }; + if (fieldOptions.secret) { + return ( + <> + {RevertReplaceButton} + setFileIsUploading(true)} + onReadFinished={() => setFileIsUploading(false)} + isLoading={fileIsUploading} + allowEditingUploadedText + validated={isValid ? 'default' : 'error'} + /> + + ); + } + return ( ); } + if (fieldOptions.secret) { const passwordInput = () => ( - + <> + {RevertReplaceButton} + + ); return credentialKind === 'external' ? ( {passwordInput()} @@ -147,8 +214,16 @@ function CredentialField({ credentialType, fieldOptions, i18n }) { validated={isValid ? 'default' : 'error'} > ); @@ -164,7 +239,7 @@ function CredentialField({ credentialType, fieldOptions, i18n }) { isRequired={isRequired} validated={isValid ? 'default' : 'error'} > - + ); } diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.test.jsx new file mode 100644 index 0000000000..623623fc3f --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.test.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import credentialTypes from '../data.credentialTypes.json'; +import CredentialField from './CredentialField'; + +const credentialType = credentialTypes.find(type => type.id === 5); +const fieldOptions = { + id: 'password', + label: 'Secret Key', + type: 'string', + secret: true, +}; + +describe('', () => { + let wrapper; + afterEach(() => { + wrapper.unmount(); + }); + test('renders correctly without initial value', () => { + wrapper = mountWithContexts( + + {() => ( + + )} + + ); + expect(wrapper.find('CredentialField').length).toBe(1); + expect(wrapper.find('PasswordInput').length).toBe(1); + expect(wrapper.find('TextInput').props().isDisabled).toBe(false); + expect(wrapper.find('KeyIcon').length).toBe(1); + expect(wrapper.find('PficonHistoryIcon').length).toBe(0); + }); + test('renders correctly with initial value', () => { + wrapper = mountWithContexts( + + {() => ( + + )} + + ); + expect(wrapper.find('CredentialField').length).toBe(1); + expect(wrapper.find('PasswordInput').length).toBe(1); + expect(wrapper.find('TextInput').props().isDisabled).toBe(true); + expect(wrapper.find('TextInput').props().value).toBe(''); + expect(wrapper.find('TextInput').props().placeholder).toBe('ENCRYPTED'); + expect(wrapper.find('KeyIcon').length).toBe(1); + expect(wrapper.find('PficonHistoryIcon').length).toBe(1); + }); + test('replace/revert button behaves as expected', () => { + wrapper = mountWithContexts( + + {() => ( + + )} + + ); + expect( + wrapper.find('Tooltip#credential-password-replace-tooltip').props() + .content + ).toBe('Replace'); + expect(wrapper.find('TextInput').props().isDisabled).toBe(true); + expect(wrapper.find('PficonHistoryIcon').simulate('click')); + expect( + wrapper.find('Tooltip#credential-password-replace-tooltip').props() + .content + ).toBe('Revert'); + expect(wrapper.find('TextInput').props().isDisabled).toBe(false); + expect(wrapper.find('TextInput').props().value).toBe(''); + expect(wrapper.find('TextInput').props().placeholder).toBe(undefined); + expect(wrapper.find('PficonHistoryIcon').simulate('click')); + expect( + wrapper.find('Tooltip#credential-password-replace-tooltip').props() + .content + ).toBe('Replace'); + expect(wrapper.find('TextInput').props().isDisabled).toBe(true); + expect(wrapper.find('TextInput').props().value).toBe(''); + expect(wrapper.find('TextInput').props().placeholder).toBe('ENCRYPTED'); + }); +}); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx index 62c579e791..8de2872f76 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialPlugins/CredentialPluginField.jsx @@ -27,9 +27,17 @@ function CredentialPluginInput(props) { } = props; const [showPluginWizard, setShowPluginWizard] = useState(false); - const [inputField, , helpers] = useField(`inputs.${fieldOptions.id}`); + const [inputField, meta, helpers] = useField(`inputs.${fieldOptions.id}`); const [passwordPromptField] = useField(`passwordPrompts.${fieldOptions.id}`); + const disableFieldAndButtons = + !!passwordPromptField.value || + !!( + meta.initialValue && + meta.initialValue !== '' && + meta.value === meta.initialValue + ); + return ( <> {inputField?.value?.credential ? ( @@ -44,7 +52,7 @@ function CredentialPluginInput(props) { ...inputField, isRequired, validated: isValid ? 'default' : 'error', - isDisabled: !!passwordPromptField.value, + isDisabled: disableFieldAndButtons, onChange: (_, event) => { inputField.onChange(event); }, @@ -61,7 +69,7 @@ function CredentialPluginInput(props) { t`Populate field from an external secret management system` )} onClick={() => setShowPluginWizard(true)} - isDisabled={isDisabled || !!passwordPromptField.value} + isDisabled={isDisabled || disableFieldAndButtons} >