mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 15:36:04 -03:30
Add feature to add instance group
Add feature to add instance group. See: https://github.com/ansible/awx/issues/7744
This commit is contained in:
@@ -1,11 +1,38 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
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() {
|
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 (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<div>Add instance group</div>
|
<CardBody>
|
||||||
|
<InstanceGroupForm
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitError={submitError}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user