mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 15:09:32 -02:30
Merge pull request #7338 from mabashian/cred-plugin-test-button
Hook up Test button on Metadata step in credential plugin wizard Reviewed-by: John Hill <johill@redhat.com> https://github.com/unlikelyzero
This commit is contained in:
@@ -14,7 +14,7 @@ import { FieldTooltip, PasswordInput } from '../../../../components/FormField';
|
||||
import AnsibleSelect from '../../../../components/AnsibleSelect';
|
||||
import { CredentialType } from '../../../../types';
|
||||
import { required } from '../../../../util/validators';
|
||||
import { CredentialPluginField } from './CredentialPlugins';
|
||||
import { CredentialPluginField } from '../CredentialPlugins';
|
||||
import BecomeMethodField from './BecomeMethodField';
|
||||
|
||||
const FileUpload = styled(PFFileUpload)`
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import MetadataStep from './MetadataStep';
|
||||
|
||||
function CredentialPluginWizard({ i18n, handleSubmit, onClose }) {
|
||||
const [selectedCredential] = useField('credential');
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
name: i18n._(t`Credential`),
|
||||
component: <CredentialsStep />,
|
||||
enableNext: !!selectedCredential.value,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: i18n._(t`Metadata`),
|
||||
component: <MetadataStep />,
|
||||
canJumpTo: !!selectedCredential.value,
|
||||
nextButtonText: i18n._(t`OK`),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title={i18n._(t`External Secret Management System`)}
|
||||
steps={steps}
|
||||
onSave={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CredentialPluginPrompt({ i18n, onClose, onSubmit, initialValues }) {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
credential: initialValues?.credential || null,
|
||||
inputs: initialValues?.inputs || {},
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ handleSubmit }) => (
|
||||
<CredentialPluginWizard
|
||||
handleSubmit={handleSubmit}
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
CredentialPluginPrompt.propTypes = {
|
||||
onClose: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
initialValues: shape({}),
|
||||
};
|
||||
|
||||
CredentialPluginPrompt.defaultProps = {
|
||||
initialValues: {},
|
||||
};
|
||||
|
||||
export default withI18n()(CredentialPluginPrompt);
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { KeyIcon } from '@patternfly/react-icons';
|
||||
import { FieldTooltip } from '../../../../../components/FormField';
|
||||
import FieldWithPrompt from '../../../../../components/FieldWithPrompt';
|
||||
import { FieldTooltip } from '../../../../components/FormField';
|
||||
import FieldWithPrompt from '../../../../components/FieldWithPrompt';
|
||||
import { CredentialPluginPrompt } from './CredentialPluginPrompt';
|
||||
import CredentialPluginSelected from './CredentialPluginSelected';
|
||||
|
||||
@@ -55,6 +55,7 @@ function CredentialPluginInput(props) {
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
id={`credential-${fieldOptions.id}-external-button`}
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={i18n._(
|
||||
t`Populate field from an external secret management system`
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Formik } from 'formik';
|
||||
import { TextInput } from '@patternfly/react-core';
|
||||
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
||||
import CredentialPluginField from './CredentialPluginField';
|
||||
|
||||
const fieldOptions = {
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
Wizard,
|
||||
WizardContextConsumer,
|
||||
WizardFooter,
|
||||
} from '@patternfly/react-core';
|
||||
import CredentialsStep from './CredentialsStep';
|
||||
import MetadataStep from './MetadataStep';
|
||||
import { CredentialsAPI } from '../../../../../api';
|
||||
import useRequest from '../../../../../util/useRequest';
|
||||
import { CredentialPluginTestAlert } from '..';
|
||||
|
||||
function CredentialPluginWizard({ i18n, handleSubmit, onClose }) {
|
||||
const [selectedCredential] = useField('credential');
|
||||
const [inputValues] = useField('inputs');
|
||||
|
||||
const {
|
||||
result: testPluginSuccess,
|
||||
error: testPluginError,
|
||||
request: testPluginMetadata,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async () =>
|
||||
CredentialsAPI.test(selectedCredential.value.id, {
|
||||
metadata: inputValues.value,
|
||||
}),
|
||||
[selectedCredential, inputValues]
|
||||
),
|
||||
null
|
||||
);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
name: i18n._(t`Credential`),
|
||||
key: 'credential',
|
||||
component: <CredentialsStep />,
|
||||
enableNext: !!selectedCredential.value,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: i18n._(t`Metadata`),
|
||||
key: 'metadata',
|
||||
component: <MetadataStep />,
|
||||
canJumpTo: !!selectedCredential.value,
|
||||
},
|
||||
];
|
||||
|
||||
const CustomFooter = (
|
||||
<WizardFooter>
|
||||
<WizardContextConsumer>
|
||||
{({ activeStep, onNext, onBack }) => (
|
||||
<>
|
||||
<Button
|
||||
id="credential-plugin-prompt-next"
|
||||
variant="primary"
|
||||
onClick={onNext}
|
||||
isDisabled={!selectedCredential.value}
|
||||
>
|
||||
{activeStep.key === 'metadata' ? i18n._(t`OK`) : i18n._(t`Next`)}
|
||||
</Button>
|
||||
{activeStep && activeStep.key === 'metadata' && (
|
||||
<>
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Click this button to verify connection to the secret management system using the selected credential and specified inputs.`
|
||||
)}
|
||||
position="right"
|
||||
>
|
||||
<Button
|
||||
id="credential-plugin-prompt-test"
|
||||
variant="secondary"
|
||||
onClick={() => testPluginMetadata()}
|
||||
>
|
||||
{i18n._(t`Test`)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
id="credential-plugin-prompt-back"
|
||||
variant="secondary"
|
||||
onClick={onBack}
|
||||
>
|
||||
{i18n._(t`Back`)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
id="credential-plugin-prompt-cancel"
|
||||
variant="link"
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</WizardContextConsumer>
|
||||
</WizardFooter>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wizard
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title={i18n._(t`External Secret Management System`)}
|
||||
steps={steps}
|
||||
onSave={handleSubmit}
|
||||
footer={CustomFooter}
|
||||
/>
|
||||
{selectedCredential.value && (
|
||||
<CredentialPluginTestAlert
|
||||
credentialName={selectedCredential.value.name}
|
||||
successResponse={testPluginSuccess}
|
||||
errorResponse={testPluginError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CredentialPluginPrompt({ i18n, onClose, onSubmit, initialValues }) {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
credential: initialValues?.credential || null,
|
||||
inputs: initialValues?.inputs || {},
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ handleSubmit }) => (
|
||||
<CredentialPluginWizard
|
||||
handleSubmit={handleSubmit}
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
CredentialPluginPrompt.propTypes = {
|
||||
onClose: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
initialValues: shape({}),
|
||||
};
|
||||
|
||||
CredentialPluginPrompt.defaultProps = {
|
||||
initialValues: {},
|
||||
};
|
||||
|
||||
export default withI18n()(CredentialPluginPrompt);
|
||||
@@ -3,15 +3,17 @@ import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../../../../testUtils/enzymeHelpers';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../../api';
|
||||
import selectedCredential from '../../../data.cyberArkCredential.json';
|
||||
import azureVaultCredential from '../../../data.azureVaultCredential.json';
|
||||
import hashiCorpCredential from '../../../data.hashiCorpCredential.json';
|
||||
} from '../../../../../../testUtils/enzymeHelpers';
|
||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../../../api';
|
||||
import selectedCredential from '../../data.cyberArkCredential.json';
|
||||
import azureVaultCredential from '../../data.azureVaultCredential.json';
|
||||
import hashiCorpCredential from '../../data.hashiCorpCredential.json';
|
||||
import CredentialPluginPrompt from './CredentialPluginPrompt';
|
||||
|
||||
jest.mock('../../../../../../api/models/Credentials');
|
||||
jest.mock('../../../../../../api/models/CredentialTypes');
|
||||
jest.mock('../../../../../api/models/Credentials');
|
||||
jest.mock('../../../../../api/models/CredentialTypes');
|
||||
|
||||
CredentialsAPI.test.mockResolvedValue({});
|
||||
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
@@ -234,5 +236,13 @@ describe('<CredentialPluginPrompt />', () => {
|
||||
wrapper.find('input#credential-secret_version').prop('value')
|
||||
).toBe('9000');
|
||||
});
|
||||
test('clicking Test button makes correct call', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Button[children="Test"]').simulate('click');
|
||||
});
|
||||
expect(CredentialsAPI.test).toHaveBeenCalledWith(1, {
|
||||
metadata: { secret_path: '/foo/bar', secret_version: '9000' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,13 @@ import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { CredentialsAPI } from '../../../../../../api';
|
||||
import CheckboxListItem from '../../../../../../components/CheckboxListItem';
|
||||
import ContentError from '../../../../../../components/ContentError';
|
||||
import DataListToolbar from '../../../../../../components/DataListToolbar';
|
||||
import PaginatedDataList from '../../../../../../components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '../../../../../../util/qs';
|
||||
import useRequest from '../../../../../../util/useRequest';
|
||||
import { CredentialsAPI } from '../../../../../api';
|
||||
import CheckboxListItem from '../../../../../components/CheckboxListItem';
|
||||
import ContentError from '../../../../../components/ContentError';
|
||||
import DataListToolbar from '../../../../../components/DataListToolbar';
|
||||
import PaginatedDataList from '../../../../../components/PaginatedDataList';
|
||||
import { getQSConfig, parseQueryString } from '../../../../../util/qs';
|
||||
import useRequest from '../../../../../util/useRequest';
|
||||
|
||||
const QS_CONFIG = getQSConfig('credential', {
|
||||
page: 1,
|
||||
@@ -1,27 +1,22 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import { Button, Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
||||
import { Form, FormGroup, Tooltip } from '@patternfly/react-core';
|
||||
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
||||
import { CredentialTypesAPI } from '../../../../../../api';
|
||||
import AnsibleSelect from '../../../../../../components/AnsibleSelect';
|
||||
import ContentError from '../../../../../../components/ContentError';
|
||||
import ContentLoading from '../../../../../../components/ContentLoading';
|
||||
import FormField from '../../../../../../components/FormField';
|
||||
import { FormFullWidthLayout } from '../../../../../../components/FormLayout';
|
||||
import useRequest from '../../../../../../util/useRequest';
|
||||
import { required } from '../../../../../../util/validators';
|
||||
import { CredentialTypesAPI } from '../../../../../api';
|
||||
import AnsibleSelect from '../../../../../components/AnsibleSelect';
|
||||
import ContentError from '../../../../../components/ContentError';
|
||||
import ContentLoading from '../../../../../components/ContentLoading';
|
||||
import FormField from '../../../../../components/FormField';
|
||||
import { FormFullWidthLayout } from '../../../../../components/FormLayout';
|
||||
import useRequest from '../../../../../util/useRequest';
|
||||
import { required } from '../../../../../util/validators';
|
||||
|
||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const TestButton = styled(Button)`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
function MetadataStep({ i18n }) {
|
||||
const form = useFormikContext();
|
||||
const [selectedCredential] = useField('credential');
|
||||
@@ -65,10 +60,6 @@ function MetadataStep({ i18n }) {
|
||||
fetchMetadataOptions();
|
||||
}, [fetchMetadataOptions]);
|
||||
|
||||
const testMetadata = () => {
|
||||
// https://github.com/ansible/awx/issues/7126
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
@@ -136,20 +127,6 @@ function MetadataStep({ i18n }) {
|
||||
</FormFullWidthLayout>
|
||||
</Form>
|
||||
)}
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Click this button to verify connection to the secret management system using the selected credential and specified inputs.`
|
||||
)}
|
||||
position="right"
|
||||
>
|
||||
<TestButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
onClick={() => testMetadata()}
|
||||
>
|
||||
{i18n._(t`Test`)}
|
||||
</TestButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import { t, Trans } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core';
|
||||
import { KeyIcon } from '@patternfly/react-icons';
|
||||
import CredentialChip from '../../../../../components/CredentialChip';
|
||||
import { Credential } from '../../../../../types';
|
||||
import CredentialChip from '../../../../components/CredentialChip';
|
||||
import { Credential } from '../../../../types';
|
||||
|
||||
const SelectedCredential = styled.div`
|
||||
display: flex;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../../../../testUtils/enzymeHelpers';
|
||||
import selectedCredential from '../../data.cyberArkCredential.json';
|
||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
||||
import selectedCredential from '../data.cyberArkCredential.json';
|
||||
import CredentialPluginSelected from './CredentialPluginSelected';
|
||||
|
||||
describe('<CredentialPluginSelected />', () => {
|
||||
@@ -16,7 +16,6 @@ function CredentialPluginTestAlert({
|
||||
}) {
|
||||
const [testMessage, setTestMessage] = useState('');
|
||||
const [testVariant, setTestVariant] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorResponse) {
|
||||
if (errorResponse?.response?.data?.inputs) {
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers';
|
||||
import CredentialPluginTestAlert from './CredentialPluginTestAlert';
|
||||
|
||||
describe('<CredentialPluginTestAlert />', () => {
|
||||
let wrapper;
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('renders expected content when test is successful', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialPluginTestAlert
|
||||
credentialName="Foobar"
|
||||
successResponse={{}}
|
||||
errorResponse={null}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar');
|
||||
expect(wrapper.find('p#credential-plugin-test-message').text()).toBe(
|
||||
'Test passed'
|
||||
);
|
||||
});
|
||||
test('renders expected content when test fails with the expected return string formatting', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialPluginTestAlert
|
||||
credentialName="Foobar"
|
||||
successResponse={null}
|
||||
errorResponse={{
|
||||
response: {
|
||||
data: {
|
||||
inputs: `HTTP 404
|
||||
{"errors":["not found"]}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar');
|
||||
expect(wrapper.find('p#credential-plugin-test-message').text()).toBe(
|
||||
'HTTP 404: not found'
|
||||
);
|
||||
});
|
||||
test('renders expected content when test fails without the expected return string formatting', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialPluginTestAlert
|
||||
credentialName="Foobar"
|
||||
successResponse={null}
|
||||
errorResponse={{
|
||||
response: {
|
||||
data: {
|
||||
inputs: 'usernamee is not present at /secret/foo/bar/baz',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('b#credential-plugin-test-name').text()).toBe('Foobar');
|
||||
expect(wrapper.find('p#credential-plugin-test-message').text()).toBe(
|
||||
'usernamee is not present at /secret/foo/bar/baz'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ import FormField from '../../../components/FormField';
|
||||
import { FormFullWidthLayout } from '../../../components/FormLayout';
|
||||
import { required } from '../../../util/validators';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { CredentialPluginTestAlert } from './CredentialFormFields/CredentialPlugins';
|
||||
import { CredentialPluginTestAlert } from './CredentialPlugins';
|
||||
|
||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -39,6 +39,7 @@ export default function useRequest(makeRequest, initialValue) {
|
||||
async (...args) => {
|
||||
setIsLoading(true);
|
||||
if (isMounted.current) {
|
||||
setResult(initialValue);
|
||||
setError(null);
|
||||
}
|
||||
try {
|
||||
@@ -56,6 +57,7 @@ export default function useRequest(makeRequest, initialValue) {
|
||||
}
|
||||
}
|
||||
},
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[makeRequest]
|
||||
),
|
||||
setValue: setResult,
|
||||
|
||||
@@ -96,6 +96,37 @@ describe('useRequest hooks', () => {
|
||||
expect(wrapper.find('TestInner').prop('error')).toEqual(error);
|
||||
});
|
||||
|
||||
test('should reset error/result on each request', async () => {
|
||||
const error = new Error('error');
|
||||
const makeRequest = throwError => {
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: 'foo' };
|
||||
};
|
||||
const wrapper = mount(<Test makeRequest={makeRequest} />);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('TestInner').invoke('request')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('TestInner').prop('result')).toEqual({});
|
||||
expect(wrapper.find('TestInner').prop('error')).toEqual(error);
|
||||
await act(async () => {
|
||||
wrapper.find('TestInner').invoke('request')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('TestInner').prop('result')).toEqual({ data: 'foo' });
|
||||
expect(wrapper.find('TestInner').prop('error')).toEqual(null);
|
||||
await act(async () => {
|
||||
wrapper.find('TestInner').invoke('request')(true);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('TestInner').prop('result')).toEqual({});
|
||||
expect(wrapper.find('TestInner').prop('error')).toEqual(error);
|
||||
});
|
||||
|
||||
test('should not update state after unmount', async () => {
|
||||
const makeRequest = jest.fn();
|
||||
let resolve;
|
||||
|
||||
Reference in New Issue
Block a user