Merge pull request #7776 from nixocio/ui_add_instance_group

Add feature to add instance group

Reviewed-by: Kersom
             https://github.com/nixocio
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-08-10 17:35:01 +00:00 committed by GitHub
commit fbb9998b68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 342 additions and 2 deletions

View File

@ -1,11 +1,38 @@
import React from 'react';
import React, { useState } from 'react';
import { Card, PageSection } from '@patternfly/react-core';
import { useHistory } from 'react-router-dom';
import InstanceGroupForm from '../shared/InstanceGroupForm';
import { CardBody } from '../../../components/Card';
import { InstanceGroupsAPI } from '../../../api';
function InstanceGroupAdd() {
const history = useHistory();
const [submitError, setSubmitError] = useState(null);
const handleSubmit = async values => {
try {
const { data: response } = await InstanceGroupsAPI.create(values);
history.push(`/instance_groups/${response.id}/details`);
} catch (error) {
setSubmitError(error);
}
};
const handleCancel = () => {
history.push(`/instance_groups`);
};
return (
<PageSection>
<Card>
<div>Add instance group</div>
<CardBody>
<InstanceGroupForm
onSubmit={handleSubmit}
submitError={submitError}
onCancel={handleCancel}
/>
</CardBody>
</Card>
</PageSection>
);

View File

@ -0,0 +1,101 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import { InstanceGroupsAPI } from '../../../api';
import InstanceGroupAdd from './InstanceGroupAdd';
jest.mock('../../../api');
const instanceGroupData = {
id: 42,
type: 'instance_group',
url: '/api/v2/instance_groups/42/',
related: {
jobs: '/api/v2/instance_groups/42/jobs/',
instances: '/api/v2/instance_groups/7/instances/',
},
name: 'Bar',
created: '2020-07-21T18:41:02.818081Z',
modified: '2020-07-24T20:32:03.121079Z',
capacity: 24,
committed_capacity: 0,
consumed_capacity: 0,
percent_capacity_remaining: 100.0,
jobs_running: 0,
jobs_total: 0,
instances: 1,
controller: null,
is_controller: false,
is_isolated: false,
is_containerized: false,
credential: null,
policy_instance_percentage: 46,
policy_instance_minimum: 12,
policy_instance_list: [],
pod_spec_override: '',
summary_fields: {
user_capabilities: {
edit: true,
delete: true,
},
},
};
InstanceGroupsAPI.create.mockResolvedValue({
data: {
id: 42,
},
});
describe('<InstanceGroupAdd/>', () => {
let wrapper;
let history;
beforeEach(async () => {
history = createMemoryHistory({
initialEntries: ['/instance_groups'],
});
await act(async () => {
wrapper = mountWithContexts(<InstanceGroupAdd />, {
context: { router: { history } },
});
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('handleSubmit should call the api and redirect to details page', async () => {
await act(async () => {
wrapper.find('InstanceGroupForm').prop('onSubmit')(instanceGroupData);
});
wrapper.update();
expect(InstanceGroupsAPI.create).toHaveBeenCalledWith(instanceGroupData);
expect(history.location.pathname).toBe('/instance_groups/42/details');
});
test('handleCancel should return the user back to the instance group list', async () => {
wrapper.find('Button[aria-label="Cancel"]').simulate('click');
expect(history.location.pathname).toEqual('/instance_groups');
});
test('failed form submission should show an error message', async () => {
const error = {
response: {
data: { detail: 'An error occurred' },
},
};
InstanceGroupsAPI.create.mockImplementationOnce(() =>
Promise.reject(error)
);
await act(async () => {
wrapper.find('InstanceGroupForm').invoke('onSubmit')(instanceGroupData);
});
wrapper.update();
expect(wrapper.find('FormSubmitError').length).toBe(1);
});
});

View File

@ -0,0 +1,92 @@
import React from 'react';
import { func, shape } from 'prop-types';
import { Formik } from 'formik';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Form } from '@patternfly/react-core';
import FormField, { FormSubmitError } from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup';
import { required, minMaxValue } from '../../../util/validators';
import { FormColumnLayout } from '../../../components/FormLayout';
function InstanceGroupFormFields({ i18n }) {
return (
<>
<FormField
id="instance-group-name"
label={i18n._(t`Name`)}
name="name"
type="text"
validate={required(null, i18n)}
isRequired
/>
<FormField
id="instance-group-policy-instance-minimum"
label={i18n._(t`Policy instance minimum`)}
name="policy_instance_minimum"
type="number"
validate={minMaxValue(0, 2147483647, i18n)}
tooltip={i18n._(
t`Minimum number of instances that will be automatically
assigned to this group when new instances come online.`
)}
/>
<FormField
id="instance-group-policy-instance-percentage"
label={i18n._(t`Policy instance percentage`)}
name="policy_instance_percentage"
type="number"
tooltip={i18n._(
t`Minimum percentage of all instances that will be automatically
assigned to this group when new instances come online.`
)}
validate={minMaxValue(0, 100, i18n)}
/>
</>
);
}
function InstanceGroupForm({
instanceGroup = {},
onSubmit,
onCancel,
submitError,
...rest
}) {
const initialValues = {
name: instanceGroup.name || '',
policy_instance_minimum: instanceGroup.policy_instance_minimum || 0,
policy_instance_percentage: instanceGroup.policy_instance_percentage || 0,
};
return (
<Formik initialValues={initialValues} onSubmit={values => onSubmit(values)}>
{formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<InstanceGroupFormFields {...rest} />
{submitError && <FormSubmitError error={submitError} />}
<FormActionGroup
onCancel={onCancel}
onSubmit={formik.handleSubmit}
/>
</FormColumnLayout>
</Form>
)}
</Formik>
);
}
InstanceGroupForm.propTypes = {
instanceGroup: shape({}),
onCancel: func.isRequired,
onSubmit: func.isRequired,
submitError: shape({}),
};
InstanceGroupForm.defaultProps = {
instanceGroup: {},
submitError: null,
};
export default withI18n()(InstanceGroupForm);

View File

@ -0,0 +1,120 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import InstanceGroupForm from './InstanceGroupForm';
jest.mock('../../../api');
const instanceGroup = {
id: 7,
type: 'instance_group',
url: '/api/v2/instance_groups/7/',
related: {
jobs: '/api/v2/instance_groups/7/jobs/',
instances: '/api/v2/instance_groups/7/instances/',
},
name: 'Bar',
created: '2020-07-21T18:41:02.818081Z',
modified: '2020-07-24T20:32:03.121079Z',
capacity: 24,
committed_capacity: 0,
consumed_capacity: 0,
percent_capacity_remaining: 100.0,
jobs_running: 0,
jobs_total: 0,
instances: 1,
controller: null,
is_controller: false,
is_isolated: false,
is_containerized: false,
credential: null,
policy_instance_percentage: 46,
policy_instance_minimum: 12,
policy_instance_list: [],
pod_spec_override: '',
summary_fields: {
user_capabilities: {
edit: true,
delete: true,
},
},
};
describe('<InstanceGroupForm/>', () => {
let wrapper;
let onCancel;
let onSubmit;
beforeEach(async () => {
onCancel = jest.fn();
onSubmit = jest.fn();
await act(async () => {
wrapper = mountWithContexts(
<InstanceGroupForm
onCancel={onCancel}
onSubmit={onSubmit}
instanceGroup={instanceGroup}
/>
);
});
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('Initially renders successfully', () => {
expect(wrapper.length).toBe(1);
});
test('should display form fields properly', () => {
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Policy instance minimum"]').length
).toBe(1);
expect(
wrapper.find('FormGroup[label="Policy instance percentage"]').length
).toBe(1);
});
test('should call onSubmit when form submitted', async () => {
expect(onSubmit).not.toHaveBeenCalled();
await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
});
expect(onSubmit).toHaveBeenCalledTimes(1);
});
test('should update form values', () => {
act(() => {
wrapper.find('input#instance-group-name').simulate('change', {
target: { value: 'Foo', name: 'name' },
});
wrapper
.find('input#instance-group-policy-instance-minimum')
.simulate('change', {
target: { value: 10, name: 'policy_instance_minimum' },
});
});
wrapper.update();
expect(wrapper.find('input#instance-group-name').prop('value')).toEqual(
'Foo'
);
expect(
wrapper.find('input#instance-group-policy-instance-minimum').prop('value')
).toEqual(10);
expect(
wrapper
.find('input#instance-group-policy-instance-percentage')
.prop('value')
).toEqual(46);
});
test('should call handleCancel when Cancel button is clicked', async () => {
expect(onCancel).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(onCancel).toBeCalled();
});
});