1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-27 00:55:06 +03:00

Merge pull request #7341 from AlexSCorey/7235-ApplicationsAddEdit

7235 applications add

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-06-19 15:36:04 +00:00 committed by GitHub
commit c5addd7c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 632 additions and 5 deletions

View File

@ -1,15 +1,94 @@
import React from 'react';
import { Card, PageSection } from '@patternfly/react-core';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
function ApplicatonAdd() {
import { Card, PageSection } from '@patternfly/react-core';
import useRequest from '../../../util/useRequest';
import ContentError from '../../../components/ContentError';
import ApplicationForm from '../shared/ApplicationForm';
import { ApplicationsAPI } from '../../../api';
import { CardBody } from '../../../components/Card';
function ApplicationAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const {
error,
request: fetchOptions,
result: { authorizationOptions, clientTypeOptions },
} = useRequest(
useCallback(async () => {
const {
data: {
actions: {
GET: {
authorization_grant_type: { choices: authChoices },
client_type: { choices: clientChoices },
},
},
},
} = await ApplicationsAPI.readOptions();
const authorization = authChoices.map(choice => ({
value: choice[0],
label: choice[1],
key: choice[0],
}));
const clientType = clientChoices.map(choice => ({
value: choice[0],
label: choice[1],
key: choice[0],
}));
return {
authorizationOptions: authorization,
clientTypeOptions: clientType,
};
}, []),
{
authorizationOptions: [],
clientTypeOptions: [],
}
);
const handleSubmit = async ({ ...values }) => {
values.organization = values.organization.id;
try {
const {
data: { id },
} = await ApplicationsAPI.create(values);
history.push(`/applications/${id}/details`);
} catch (err) {
setSubmitError(err);
}
};
const handleCancel = () => {
history.push(`/applications`);
};
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
if (error) {
return <ContentError error={error} />;
}
return (
<>
<PageSection>
<Card>
<div>Applications Add</div>
<CardBody>
<ApplicationForm
onSubmit={handleSubmit}
onCancel={handleCancel}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
submitError={submitError}
/>
</CardBody>
</Card>
</PageSection>
</>
);
}
export default ApplicatonAdd;
export default ApplicationAdd;

View File

@ -0,0 +1,190 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import { ApplicationsAPI } from '../../../api';
import ApplicationAdd from './ApplicationAdd';
jest.mock('../../../api/models/Applications');
jest.mock('../../../api/models/Organizations');
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
history: () => ({
location: '/applications/add',
}),
}));
const options = {
data: {
actions: {
GET: {
client_type: {
choices: [
['confidential', 'Confidential'],
['public', 'Public'],
],
},
authorization_grant_type: {
choices: [
['authorization-code', 'Authorization code'],
['password', 'Resource owner password-based'],
],
},
},
},
},
};
describe('<ApplicationAdd/>', () => {
let wrapper;
test('should render properly', async () => {
ApplicationsAPI.readOptions.mockResolvedValue(options);
await act(async () => {
wrapper = mountWithContexts(<ApplicationAdd />);
});
expect(wrapper.find('ApplicationAdd').length).toBe(1);
expect(wrapper.find('ApplicationForm').length).toBe(1);
expect(ApplicationsAPI.readOptions).toBeCalled();
});
test('expect values to be updated and submitted properly', async () => {
const history = createMemoryHistory({
initialEntries: ['/applications/add'],
});
ApplicationsAPI.readOptions.mockResolvedValue(options);
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
await act(async () => {
wrapper = mountWithContexts(<ApplicationAdd />, {
context: { router: { history } },
});
});
await act(async () => {
wrapper.find('input#name').simulate('change', {
target: { value: 'new foo', name: 'name' },
});
wrapper.find('input#description').simulate('change', {
target: { value: 'new bar', name: 'description' },
});
wrapper
.find('AnsibleSelect[name="authorization_grant_type"]')
.prop('onChange')({}, 'authorization code');
wrapper.find('input#redirect_uris').simulate('change', {
target: { value: 'https://www.google.com', name: 'redirect_uris' },
});
wrapper.find('AnsibleSelect[name="client_type"]').prop('onChange')(
{},
'confidential'
);
wrapper.find('OrganizationLookup').invoke('onChange')({
id: 1,
name: 'organization',
});
});
wrapper.update();
expect(wrapper.find('input#name').prop('value')).toBe('new foo');
expect(wrapper.find('input#description').prop('value')).toBe('new bar');
expect(wrapper.find('InnerChipGroup').length).toBe(1);
expect(wrapper.find('InnerChipGroup').text()).toBe('organization');
expect(
wrapper
.find('AnsibleSelect[name="authorization_grant_type"]')
.prop('value')
).toBe('authorization code');
expect(
wrapper.find('AnsibleSelect[name="client_type"]').prop('value')
).toBe('confidential');
expect(wrapper.find('input#redirect_uris').prop('value')).toBe(
'https://www.google.com'
);
await act(async () => {
wrapper.find('Formik').prop('onSubmit')({
authorization_grant_type: 'authorization-code',
client_type: 'confidential',
description: 'bar',
name: 'foo',
organization: { id: 1 },
redirect_uris: 'http://www.google.com',
});
});
expect(ApplicationsAPI.create).toBeCalledWith({
authorization_grant_type: 'authorization-code',
client_type: 'confidential',
description: 'bar',
name: 'foo',
organization: 1,
redirect_uris: 'http://www.google.com',
});
expect(history.location.pathname).toBe('/applications/8/details');
});
test('should cancel form properly', async () => {
const history = createMemoryHistory({
initialEntries: ['/applications/add'],
});
ApplicationsAPI.readOptions.mockResolvedValue(options);
ApplicationsAPI.create.mockResolvedValue({ data: { id: 8 } });
await act(async () => {
wrapper = mountWithContexts(<ApplicationAdd />, {
context: { router: { history } },
});
});
await act(async () => {
wrapper.find('Button[aria-label="Cancel"]').prop('onClick')();
});
expect(history.location.pathname).toBe('/applications');
});
test('should throw error on submit', async () => {
const error = {
response: {
config: {
method: 'patch',
url: '/api/v2/applications/',
},
data: { detail: 'An error occurred' },
},
};
ApplicationsAPI.create.mockRejectedValue(error);
ApplicationsAPI.readOptions.mockResolvedValue(options);
await act(async () => {
wrapper = mountWithContexts(<ApplicationAdd />);
});
await act(async () => {
wrapper.find('Formik').prop('onSubmit')({
id: 1,
organization: { id: 1 },
});
});
waitForElement(wrapper, 'FormSubmitError', el => el.length > 0);
});
test('should render content error on failed read options request', async () => {
ApplicationsAPI.readOptions.mockRejectedValue(
new Error({
response: {
config: {
method: 'options',
},
data: 'An error occurred',
status: 403,
},
})
);
await act(async () => {
wrapper = mountWithContexts(<ApplicationAdd />);
});
wrapper.update();
expect(wrapper.find('ContentError').length).toBe(1);
});
});

View File

@ -0,0 +1,177 @@
import React from 'react';
import { useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik, useField } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core';
import PropTypes from 'prop-types';
import { required } from '../../../util/validators';
import FormField, {
FormSubmitError,
FieldTooltip,
} from '../../../components/FormField';
import { FormColumnLayout } from '../../../components/FormLayout';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
import AnsibleSelect from '../../../components/AnsibleSelect';
function ApplicationFormFields({
i18n,
authorizationOptions,
clientTypeOptions,
}) {
const match = useRouteMatch();
const [organizationField, organizationMeta, organizationHelpers] = useField({
name: 'organization',
validate: required(null, i18n),
});
const [
authorizationTypeField,
authorizationTypeMeta,
authorizationTypeHelpers,
] = useField({
name: 'authorization_grant_type',
validate: required(null, i18n),
});
const [clientTypeField, clientTypeMeta, clientTypeHelpers] = useField({
name: 'client_type',
validate: required(null, i18n),
});
return (
<>
<FormField
id="name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="description"
label={i18n._(t`Description`)}
name="description"
type="text"
/>
<OrganizationLookup
helperTextInvalid={organizationMeta.error}
isValid={!organizationMeta.touched || !organizationMeta.error}
onBlur={() => organizationHelpers.setTouched()}
onChange={value => {
organizationHelpers.setValue(value);
}}
value={organizationField.value}
required
/>
<FormGroup
fieldId="authType"
helperTextInvalid={authorizationTypeMeta.error}
isRequired
isValid={!authorizationTypeMeta.touched || !authorizationTypeMeta.error}
label={i18n._(t`Authorization grant type`)}
>
<FieldTooltip
content={i18n._(
t`The Grant type the user must use for acquire tokens for this application`
)}
/>
<AnsibleSelect
{...authorizationTypeField}
isDisabled={match.url.endsWith('edit')}
id="authType"
data={[{ label: '', key: 1, value: '' }, ...authorizationOptions]}
onChange={(event, value) => {
authorizationTypeHelpers.setValue(value);
}}
/>
</FormGroup>
<FormField
id="redirect_uris"
label={i18n._(t`Redirect URIs`)}
name="redirect_uris"
type="text"
isRequired={Boolean(
authorizationTypeField.value === 'authorization-code'
)}
validate={
authorizationTypeField.value === 'authorization-code'
? required(null, i18n)
: null
}
tooltip={i18n._(t`Allowed URIs list, space separated`)}
/>
<FormGroup
fieldId="clientType"
helperTextInvalid={clientTypeMeta.error}
isRequired
isValid={!clientTypeMeta.touched || !clientTypeMeta.error}
label={i18n._(t`Client type`)}
>
<FieldTooltip
content={i18n._(
t`Set to Public or Confidential depending on how secure the client device is.`
)}
/>
<AnsibleSelect
{...clientTypeField}
id="clientType"
data={[{ label: '', key: 1, value: '' }, ...clientTypeOptions]}
onChange={(event, value) => {
clientTypeHelpers.setValue(value);
}}
/>
</FormGroup>
</>
);
}
function ApplicationForm({
onCancel,
onSubmit,
i18n,
submitError,
application,
authorizationOptions,
clientTypeOptions,
}) {
const initialValues = {
name: application?.name || '',
description: application?.description || '',
organization: application?.summary_fields?.organization || null,
authorization_grant_type: application?.authorization_grant_type || '',
redirect_uris: application?.redirect_uris || '',
client_type: application?.client_type || '',
};
return (
<Formik initialValues={initialValues} onSubmit={values => onSubmit(values)}>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<ApplicationFormFields
formik={formik}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
i18n={i18n}
/>
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={onCancel}
onSubmit={formik.handleSubmit}
/>
</FormColumnLayout>
</Form>
)}
</Formik>
);
}
ApplicationForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
authorizationOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
clientTypeOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default withI18n()(ApplicationForm);

View File

@ -0,0 +1,181 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import { OrganizationsAPI } from '../../../api';
import ApplicationForm from './ApplicationForm';
jest.mock('../../../api');
const authorizationOptions = [
{
key: 'authorization-code',
label: 'Authorization code',
value: 'authorization-code',
},
{
key: 'password',
label: 'Resource owner password-based',
value: 'password',
},
];
const clientTypeOptions = [
{ key: 'confidential', label: 'Confidential', value: 'confidential' },
{ key: 'public', label: 'Public', value: 'public' },
];
describe('<ApplicationForm', () => {
let wrapper;
test('should mount properly', async () => {
await act(async () => {
wrapper = mountWithContexts(
<ApplicationForm
onSubmit={() => {}}
application={{}}
onCancel={() => {}}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
/>
);
});
expect(wrapper.find('ApplicationForm').length).toBe(1);
});
test('all fields should render successsfully', async () => {
OrganizationsAPI.read.mockResolvedValue({
results: [{ id: 1 }, { id: 2 }],
});
await act(async () => {
wrapper = mountWithContexts(
<ApplicationForm
onSubmit={() => {}}
application={{}}
onCancel={() => {}}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
/>
);
});
expect(wrapper.find('input#name').length).toBe(1);
expect(wrapper.find('input#description').length).toBe(1);
expect(
wrapper.find('AnsibleSelect[name="authorization_grant_type"]').length
).toBe(1);
expect(wrapper.find('input#redirect_uris').length).toBe(1);
expect(wrapper.find('AnsibleSelect[name="client_type"]').length).toBe(1);
expect(wrapper.find('OrganizationLookup').length).toBe(1);
});
test('should update field values', async () => {
OrganizationsAPI.read.mockResolvedValue({
results: [{ id: 1 }, { id: 2 }],
});
await act(async () => {
wrapper = mountWithContexts(
<ApplicationForm
onSubmit={() => {}}
application={{}}
onCancel={() => {}}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
/>
);
await act(async () => {
wrapper.find('input#name').simulate('change', {
target: { value: 'new foo', name: 'name' },
});
wrapper.find('input#description').simulate('change', {
target: { value: 'new bar', name: 'description' },
});
wrapper
.find('AnsibleSelect[name="authorization_grant_type"]')
.prop('onChange')({}, 'authorization-code');
wrapper.find('input#redirect_uris').simulate('change', {
target: { value: 'https://www.google.com', name: 'redirect_uris' },
});
wrapper.find('AnsibleSelect[name="client_type"]').prop('onChange')(
{},
'confidential'
);
wrapper.find('OrganizationLookup').invoke('onChange')({
id: 3,
name: 'organization',
});
});
});
wrapper.update();
expect(wrapper.find('input#name').prop('value')).toBe('new foo');
expect(wrapper.find('input#description').prop('value')).toBe('new bar');
expect(wrapper.find('InnerChipGroup').length).toBe(1);
expect(wrapper.find('InnerChipGroup').text()).toBe('organization');
expect(
wrapper
.find('AnsibleSelect[name="authorization_grant_type"]')
.prop('value')
).toBe('authorization-code');
expect(
wrapper.find('AnsibleSelect[name="client_type"]').prop('value')
).toBe('confidential');
expect(
wrapper.find('FormField[label="Redirect URIs"]').prop('isRequired')
).toBe(true);
expect(wrapper.find('input#redirect_uris').prop('value')).toBe(
'https://www.google.com'
);
});
test('should call onCancel', async () => {
OrganizationsAPI.read.mockResolvedValue({
results: [{ id: 1 }, { id: 2 }],
});
const onSubmit = jest.fn();
const onCancel = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<ApplicationForm
onSubmit={onSubmit}
application={{}}
onCancel={onCancel}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
/>
);
});
wrapper.find('Button[aria-label="Cancel"]').prop('onClick')();
expect(onCancel).toBeCalled();
});
test('should call onSubmit', async () => {
OrganizationsAPI.read.mockResolvedValue({
results: [{ id: 1 }, { id: 2 }],
});
const onSubmit = jest.fn();
const onCancel = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<ApplicationForm
onSubmit={onSubmit}
application={{}}
onCancel={onCancel}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
/>
);
});
wrapper.find('Formik').prop('onSubmit')({
authorization_grant_type: 'authorization-code',
client_type: 'confidential',
description: 'bar',
name: 'foo',
organization: 1,
redirect_uris: 'http://www.google.com',
});
expect(onSubmit).toBeCalledWith({
authorization_grant_type: 'authorization-code',
client_type: 'confidential',
description: 'bar',
name: 'foo',
organization: 1,
redirect_uris: 'http://www.google.com',
});
});
});