mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
Add support for replace/revert on secret credential fields
This commit is contained in:
parent
6fae6168d9
commit
65bdf0baf7
@ -127,7 +127,7 @@ function CredentialAdd({ me }) {
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
if (isLoading && !result) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
|
||||
@ -178,7 +178,7 @@ function CredentialEdit({ credential }) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading && !credentialTypes) {
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
|
||||
@ -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({}),
|
||||
|
||||
@ -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 && (
|
||||
<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) {
|
||||
const handleFileChange = (value, filename) => {
|
||||
helpers.setValue(value);
|
||||
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 (
|
||||
<FileUpload
|
||||
{...subFormField}
|
||||
{...rest}
|
||||
id={`credential-${fieldOptions.id}`}
|
||||
type="text"
|
||||
filename={fileName}
|
||||
@ -48,13 +111,17 @@ function CredentialInput({ fieldOptions, credentialKind, ...rest }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldOptions.secret) {
|
||||
const passwordInput = () => (
|
||||
<PasswordInput
|
||||
{...subFormField}
|
||||
id={`credential-${fieldOptions.id}`}
|
||||
{...rest}
|
||||
/>
|
||||
<>
|
||||
{RevertReplaceButton}
|
||||
<PasswordInput
|
||||
{...subFormField}
|
||||
id={`credential-${fieldOptions.id}`}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return credentialKind === 'external' ? (
|
||||
<InputGroup>{passwordInput()}</InputGroup>
|
||||
@ -147,8 +214,16 @@ function CredentialField({ credentialType, fieldOptions, i18n }) {
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<CredentialInput
|
||||
i18n={i18n}
|
||||
credentialKind={credentialType.kind}
|
||||
fieldOptions={fieldOptions}
|
||||
isDisabled={
|
||||
!!(
|
||||
meta.initialValue &&
|
||||
meta.initialValue !== '' &&
|
||||
meta.value === meta.initialValue
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
@ -164,7 +239,7 @@ function CredentialField({ credentialType, fieldOptions, i18n }) {
|
||||
isRequired={isRequired}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<CredentialInput fieldOptions={fieldOptions} />
|
||||
<CredentialInput i18n={i18n} fieldOptions={fieldOptions} />
|
||||
</CredentialPluginField>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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}
|
||||
>
|
||||
<KeyIcon />
|
||||
</Button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user