mirror of
https://github.com/ansible/awx.git
synced 2026-04-07 02:59:21 -02:30
Add support for replace/revert on secret credential fields
This commit is contained in:
@@ -127,7 +127,7 @@ function CredentialAdd({ me }) {
|
|||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isLoading && !result) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ function CredentialEdit({ credential }) {
|
|||||||
return <ContentError error={error} />;
|
return <ContentError error={error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading && !credentialTypes) {
|
if (isLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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({}),
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
} = 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user