Add support for replace/revert on secret credential fields

This commit is contained in:
mabashian
2021-02-19 14:05:41 -05:00
parent 6fae6168d9
commit 65bdf0baf7
6 changed files with 206 additions and 15 deletions

View File

@@ -127,7 +127,7 @@ function CredentialAdd({ me }) {
</PageSection> </PageSection>
); );
} }
if (isLoading && !result) { if (isLoading) {
return ( return (
<PageSection> <PageSection>
<Card> <Card>

View File

@@ -178,7 +178,7 @@ function CredentialEdit({ credential }) {
return <ContentError error={error} />; return <ContentError error={error} />;
} }
if (isLoading && !credentialTypes) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
} }

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { func, shape } from 'prop-types'; import { shape } from 'prop-types';
import { Formik, useField, useFormikContext } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -315,8 +315,6 @@ function CredentialForm({
} }
CredentialForm.propTypes = { CredentialForm.propTypes = {
handleSubmit: func.isRequired,
handleCancel: func.isRequired,
credentialTypes: shape({}).isRequired, credentialTypes: shape({}).isRequired,
credential: shape({}), credential: shape({}),
inputSources: shape({}), inputSources: shape({}),

View File

@@ -5,11 +5,15 @@ import styled from 'styled-components';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
Button,
ButtonVariant,
FileUpload as PFFileUpload, FileUpload as PFFileUpload,
FormGroup, FormGroup,
InputGroup, InputGroup,
TextInput, TextInput,
Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { PficonHistoryIcon } from '@patternfly/react-icons';
import { PasswordInput } from '../../../../components/FormField'; import { PasswordInput } from '../../../../components/FormField';
import AnsibleSelect from '../../../../components/AnsibleSelect'; import AnsibleSelect from '../../../../components/AnsibleSelect';
import Popover from '../../../../components/Popover'; import Popover from '../../../../components/Popover';
@@ -22,20 +26,79 @@ const FileUpload = styled(PFFileUpload)`
flex-grow: 1; flex-grow: 1;
`; `;
function CredentialInput({ fieldOptions, credentialKind, ...rest }) { function CredentialInput({ i18n, fieldOptions, credentialKind, ...rest }) {
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
const [fileIsUploading, setFileIsUploading] = useState(false); const [fileIsUploading, setFileIsUploading] = useState(false);
const [subFormField, meta, helpers] = useField(`inputs.${fieldOptions.id}`); const [subFormField, meta, helpers] = useField(`inputs.${fieldOptions.id}`);
const isValid = !(meta.touched && meta.error); const isValid = !(meta.touched && meta.error);
const RevertReplaceButton = (
<>
{meta.initialValue &&
meta.initialValue !== '' &&
!meta.initialValue.credential && (
<Tooltip
id={`credential-${fieldOptions.id}-replace-tooltip`}
content={
meta.value !== meta.initialValue
? i18n._(t`Revert`)
: i18n._(t`Replace`)
}
>
<Button
id={`credential-${fieldOptions.id}-replace-button`}
variant={ButtonVariant.control}
aria-label={
meta.touched
? i18n._(t`Revert field to previously saved value`)
: i18n._(t`Replace field with new value`)
}
onClick={() => {
if (meta.value !== meta.initialValue) {
helpers.setValue(meta.initialValue);
} else {
helpers.setValue('', false);
}
}}
>
<PficonHistoryIcon />
</Button>
</Tooltip>
)}
</>
);
if (fieldOptions.multiline) { if (fieldOptions.multiline) {
const handleFileChange = (value, filename) => { const handleFileChange = (value, filename) => {
helpers.setValue(value); helpers.setValue(value);
setFileName(filename); setFileName(filename);
}; };
if (fieldOptions.secret) {
return (
<>
{RevertReplaceButton}
<FileUpload
{...subFormField}
{...rest}
id={`credential-${fieldOptions.id}`}
type="text"
filename={fileName}
onChange={handleFileChange}
onReadStarted={() => setFileIsUploading(true)}
onReadFinished={() => setFileIsUploading(false)}
isLoading={fileIsUploading}
allowEditingUploadedText
validated={isValid ? 'default' : 'error'}
/>
</>
);
}
return ( return (
<FileUpload <FileUpload
{...subFormField} {...subFormField}
{...rest}
id={`credential-${fieldOptions.id}`} id={`credential-${fieldOptions.id}`}
type="text" type="text"
filename={fileName} filename={fileName}
@@ -48,13 +111,17 @@ function CredentialInput({ fieldOptions, credentialKind, ...rest }) {
/> />
); );
} }
if (fieldOptions.secret) { if (fieldOptions.secret) {
const passwordInput = () => ( const passwordInput = () => (
<PasswordInput <>
{...subFormField} {RevertReplaceButton}
id={`credential-${fieldOptions.id}`} <PasswordInput
{...rest} {...subFormField}
/> id={`credential-${fieldOptions.id}`}
{...rest}
/>
</>
); );
return credentialKind === 'external' ? ( return credentialKind === 'external' ? (
<InputGroup>{passwordInput()}</InputGroup> <InputGroup>{passwordInput()}</InputGroup>
@@ -147,8 +214,16 @@ function CredentialField({ credentialType, fieldOptions, i18n }) {
validated={isValid ? 'default' : 'error'} validated={isValid ? 'default' : 'error'}
> >
<CredentialInput <CredentialInput
i18n={i18n}
credentialKind={credentialType.kind} credentialKind={credentialType.kind}
fieldOptions={fieldOptions} fieldOptions={fieldOptions}
isDisabled={
!!(
meta.initialValue &&
meta.initialValue !== '' &&
meta.value === meta.initialValue
)
}
/> />
</FormGroup> </FormGroup>
); );
@@ -164,7 +239,7 @@ function CredentialField({ credentialType, fieldOptions, i18n }) {
isRequired={isRequired} isRequired={isRequired}
validated={isValid ? 'default' : 'error'} validated={isValid ? 'default' : 'error'}
> >
<CredentialInput fieldOptions={fieldOptions} /> <CredentialInput i18n={i18n} fieldOptions={fieldOptions} />
</CredentialPluginField> </CredentialPluginField>
); );
} }

View File

@@ -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('<CredentialField />', () => {
let wrapper;
afterEach(() => {
wrapper.unmount();
});
test('renders correctly without initial value', () => {
wrapper = mountWithContexts(
<Formik
initialValues={{
passwordPrompts: {},
inputs: {
password: '',
},
}}
>
{() => (
<CredentialField
fieldOptions={fieldOptions}
credentialType={credentialType}
/>
)}
</Formik>
);
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(
<Formik
initialValues={{
passwordPrompts: {},
inputs: {
password: '$encrypted$',
},
}}
>
{() => (
<CredentialField
fieldOptions={fieldOptions}
credentialType={credentialType}
/>
)}
</Formik>
);
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(
<Formik
initialValues={{
passwordPrompts: {},
inputs: {
password: '$encrypted$',
},
}}
>
{() => (
<CredentialField
fieldOptions={fieldOptions}
credentialType={credentialType}
/>
)}
</Formik>
);
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');
});
});

View File

@@ -27,9 +27,17 @@ function CredentialPluginInput(props) {
} = props; } = props;
const [showPluginWizard, setShowPluginWizard] = useState(false); 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 [passwordPromptField] = useField(`passwordPrompts.${fieldOptions.id}`);
const disableFieldAndButtons =
!!passwordPromptField.value ||
!!(
meta.initialValue &&
meta.initialValue !== '' &&
meta.value === meta.initialValue
);
return ( return (
<> <>
{inputField?.value?.credential ? ( {inputField?.value?.credential ? (
@@ -44,7 +52,7 @@ function CredentialPluginInput(props) {
...inputField, ...inputField,
isRequired, isRequired,
validated: isValid ? 'default' : 'error', validated: isValid ? 'default' : 'error',
isDisabled: !!passwordPromptField.value, isDisabled: disableFieldAndButtons,
onChange: (_, event) => { onChange: (_, event) => {
inputField.onChange(event); inputField.onChange(event);
}, },
@@ -61,7 +69,7 @@ function CredentialPluginInput(props) {
t`Populate field from an external secret management system` t`Populate field from an external secret management system`
)} )}
onClick={() => setShowPluginWizard(true)} onClick={() => setShowPluginWizard(true)}
isDisabled={isDisabled || !!passwordPromptField.value} isDisabled={isDisabled || disableFieldAndButtons}
> >
<KeyIcon /> <KeyIcon />
</Button> </Button>