Added 'Max Hosts' field in the Add/Edit Organization view

* max hosts field is enabled is user is superuser, otherwise it is disabled and default is 0
 * OrganizationForm tests added for max hosts input
 * minMaxValue added in validators to validate user input for max hosts

Signed-off-by: catjones9 <catjones@redhat.com>
This commit is contained in:
catjones9
2019-06-10 16:54:05 -04:00
parent 5874becb00
commit 91b8aa90ff
7 changed files with 160 additions and 61 deletions

View File

@@ -8,11 +8,16 @@ jest.mock('../../../../src/api');
describe('<OrganizationForm />', () => { describe('<OrganizationForm />', () => {
const network = {}; const network = {};
const meConfig = {
me: {
is_superuser: false
}
};
const mockData = { const mockData = {
id: 1, id: 1,
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz', custom_virtualenv: 'Fizz',
related: { related: {
instance_groups: '/api/v2/organizations/1/instance_groups' instance_groups: '/api/v2/organizations/1/instance_groups'
@@ -30,6 +35,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
), { ), {
context: { network }, context: { network },
@@ -55,6 +61,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
), { ), {
context: { network }, context: { network },
@@ -72,6 +79,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
); );
@@ -98,6 +106,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
); );
@@ -110,6 +119,10 @@ describe('<OrganizationForm />', () => {
target: { value: 'new bar', name: 'description' } target: { value: 'new bar', name: 'description' }
}); });
expect(form.state('values').description).toEqual('new bar'); expect(form.state('values').description).toEqual('new bar');
wrapper.find('input#org-max_hosts').simulate('change', {
target: { value: '134', name: 'max_hosts' }
});
expect(form.state('values').max_hosts).toEqual('134');
}); });
test('AnsibleSelect component renders if there are virtual environments', () => { test('AnsibleSelect component renders if there are virtual environments', () => {
@@ -122,6 +135,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
), { ), {
context: { config }, context: { config },
@@ -138,6 +152,7 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
); );
expect(handleSubmit).not.toHaveBeenCalled(); expect(handleSubmit).not.toHaveBeenCalled();
@@ -146,6 +161,7 @@ describe('<OrganizationForm />', () => {
expect(handleSubmit).toHaveBeenCalledWith({ expect(handleSubmit).toHaveBeenCalledWith({
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz', custom_virtualenv: 'Fizz',
}, [], []); }, [], []);
}); });
@@ -163,6 +179,7 @@ describe('<OrganizationForm />', () => {
const mockDataForm = { const mockDataForm = {
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz', custom_virtualenv: 'Fizz',
}; };
const handleSubmit = jest.fn(); const handleSubmit = jest.fn();
@@ -175,14 +192,13 @@ describe('<OrganizationForm />', () => {
organization={mockData} organization={mockData}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleCancel={jest.fn()} handleCancel={jest.fn()}
me={meConfig.me}
/> />
), { ), {
context: { network }, context: { network }
} }
); );
await sleep(0); await sleep(0);
wrapper.find('InstanceGroupsLookup').prop('onChange')([ wrapper.find('InstanceGroupsLookup').prop('onChange')([
{ name: 'One', id: 1 }, { name: 'One', id: 1 },
{ name: 'Three', id: 3 } { name: 'Three', id: 3 }
@@ -193,13 +209,95 @@ describe('<OrganizationForm />', () => {
expect(handleSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]); expect(handleSubmit).toHaveBeenCalledWith(mockDataForm, [3], [2]);
}); });
test('handleSubmit is called with max_hosts value if it is in range', async () => {
const handleSubmit = jest.fn();
// normal mount
const wrapper = mountWithContexts(
<OrganizationForm
organization={mockData}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
wrapper.find('button[aria-label="Save"]').simulate('click');
await sleep(0);
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Foo',
description: 'Bar',
max_hosts: 1,
custom_virtualenv: 'Fizz',
}, [], []);
});
test('handleSubmit does not get called if max_hosts value is out of range', async () => {
const handleSubmit = jest.fn();
// not mount with Negative value
const mockDataNegative = JSON.parse(JSON.stringify(mockData));
mockDataNegative.max_hosts = -5;
const wrapper1 = mountWithContexts(
<OrganizationForm
organization={mockDataNegative}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
wrapper1.find('button[aria-label="Save"]').simulate('click');
await sleep(0);
expect(handleSubmit).not.toHaveBeenCalled();
// not mount with Out of Range value
const mockDataOoR = JSON.parse(JSON.stringify(mockData));
mockDataOoR.max_hosts = 999999999999;
const wrapper2 = mountWithContexts(
<OrganizationForm
organization={mockDataOoR}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
wrapper2.find('button[aria-label="Save"]').simulate('click');
await sleep(0);
expect(handleSubmit).not.toHaveBeenCalled();
});
test('handleSubmit is called and max_hosts value defaults to 0 if input is not a number', async () => {
const handleSubmit = jest.fn();
// mount with String value (default to zero)
const mockDataString = JSON.parse(JSON.stringify(mockData));
mockDataString.max_hosts = 'Bee';
const wrapper = mountWithContexts(
<OrganizationForm
organization={mockDataString}
handleSubmit={handleSubmit}
handleCancel={jest.fn()}
me={meConfig.me}
/>
);
wrapper.find('button[aria-label="Save"]').simulate('click');
await sleep(0);
expect(handleSubmit).toHaveBeenCalledWith({
name: 'Foo',
description: 'Bar',
max_hosts: 0,
custom_virtualenv: 'Fizz',
}, [], []);
});
test('calls "handleCancel" when Cancel button is clicked', () => { test('calls "handleCancel" when Cancel button is clicked', () => {
const handleCancel = jest.fn(); const handleCancel = jest.fn();
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<OrganizationForm <OrganizationForm
organization={mockData} organization={mockData}
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={handleCancel} handleCancel={handleCancel}
me={meConfig.me}
/> />
); );
expect(handleCancel).not.toHaveBeenCalled(); expect(handleCancel).not.toHaveBeenCalled();

View File

@@ -10,6 +10,7 @@ describe('<OrganizationDetail />', () => {
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
custom_virtualenv: 'Fizz', custom_virtualenv: 'Fizz',
max_hosts: '0',
created: 'Bat', created: 'Bat',
modified: 'Boo', modified: 'Boo',
summary_fields: { summary_fields: {
@@ -71,11 +72,12 @@ describe('<OrganizationDetail />', () => {
); );
const detailWrapper = wrapper.find('Detail'); const detailWrapper = wrapper.find('Detail');
expect(detailWrapper.length).toBe(5); expect(detailWrapper.length).toBe(6);
const nameDetail = detailWrapper.findWhere(node => node.props().label === 'Name'); const nameDetail = detailWrapper.findWhere(node => node.props().label === 'Name');
const descriptionDetail = detailWrapper.findWhere(node => node.props().label === 'Description'); const descriptionDetail = detailWrapper.findWhere(node => node.props().label === 'Description');
const custom_virtualenvDetail = detailWrapper.findWhere(node => node.props().label === 'Ansible Environment'); const custom_virtualenvDetail = detailWrapper.findWhere(node => node.props().label === 'Ansible Environment');
const max_hostsDetail = detailWrapper.findWhere(node => node.props().label === 'Max Hosts');
const createdDetail = detailWrapper.findWhere(node => node.props().label === 'Created'); const createdDetail = detailWrapper.findWhere(node => node.props().label === 'Created');
const modifiedDetail = detailWrapper.findWhere(node => node.props().label === 'Last Modified'); const modifiedDetail = detailWrapper.findWhere(node => node.props().label === 'Last Modified');
expect(nameDetail.find('dt').text()).toBe('Name'); expect(nameDetail.find('dt').text()).toBe('Name');
@@ -92,6 +94,9 @@ describe('<OrganizationDetail />', () => {
expect(modifiedDetail.find('dt').text()).toBe('Last Modified'); expect(modifiedDetail.find('dt').text()).toBe('Last Modified');
expect(modifiedDetail.find('dd').text()).toBe('Boo'); expect(modifiedDetail.find('dd').text()).toBe('Boo');
expect(max_hostsDetail.find('dt').text()).toBe('Max Hosts');
expect(max_hostsDetail.find('dd').text()).toBe('0');
}); });
test('should show edit button for users with edit permission', () => { test('should show edit button for users with edit permission', () => {

View File

@@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { Formik, Field } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
Tooltip, Tooltip,
Form, Form,
@@ -19,7 +20,7 @@ import FormField from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
import AnsibleSelect from '../../../components/AnsibleSelect'; import AnsibleSelect from '../../../components/AnsibleSelect';
import InstanceGroupsLookup from './InstanceGroupsLookup'; import InstanceGroupsLookup from './InstanceGroupsLookup';
import { OrganizationsAPI } from '../../../api';
import { required, minMaxValue } from '../../../util/validators'; import { required, minMaxValue } from '../../../util/validators';
class OrganizationForm extends Component { class OrganizationForm extends Component {
@@ -28,9 +29,7 @@ class OrganizationForm extends Component {
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this); this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this); this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this);
this.maxHostsChange = this.maxHostsChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.readUsers = this.readUsers.bind(this);
this.state = { this.state = {
instanceGroups: [], instanceGroups: [],
@@ -64,14 +63,6 @@ class OrganizationForm extends Component {
return data.results; return data.results;
} }
async readUsers (queryParams) {
const { api } = this.props;
console.log(api.readUsers((queryParams, is_superuser)));
console.log(api.readUsers((queryParams)));
return true;
// return api.readUsers((queryParams));
}
isEditingNewOrganization () { isEditingNewOrganization () {
const { organization } = this.props; const { organization } = this.props;
return !organization.id; return !organization.id;
@@ -81,10 +72,6 @@ class OrganizationForm extends Component {
this.setState({ instanceGroups }); this.setState({ instanceGroups });
} }
maxHostsChange (event) {
console.log('boop');
}
handleSubmit (values) { handleSubmit (values) {
const { handleSubmit } = this.props; const { handleSubmit } = this.props;
const { instanceGroups, initialInstanceGroups } = this.state; const { instanceGroups, initialInstanceGroups } = this.state;
@@ -96,23 +83,25 @@ class OrganizationForm extends Component {
const groupsToDisassociate = [...initialIds] const groupsToDisassociate = [...initialIds]
.filter(x => !updatedIds.includes(x)); .filter(x => !updatedIds.includes(x));
if (typeof values.max_hosts !== 'number' || values.max_hosts === 'undefined') {
values.max_hosts = 0;
}
handleSubmit(values, groupsToAssociate, groupsToDisassociate); handleSubmit(values, groupsToAssociate, groupsToDisassociate);
} }
render () { render () {
const { organization, handleCancel, i18n, is_superuser } = this.props; const { organization, handleCancel, i18n, me } = this.props;
const { instanceGroups, formIsValid, error } = this.state; const { instanceGroups, formIsValid, error } = this.state;
const defaultVenv = '/venv/ansible/'; const defaultVenv = '/venv/ansible/';
console.log(organization);
return ( return (
<Formik <Formik
initialValues={{ initialValues={{
name: organization.name, name: organization.name,
description: organization.description, description: organization.description,
custom_virtualenv: organization.custom_virtualenv || '', custom_virtualenv: organization.custom_virtualenv || '',
max_hosts: organization.max_hosts || 0 max_hosts: organization.max_hosts || '0',
}} }}
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
render={formik => ( render={formik => (
@@ -136,25 +125,26 @@ class OrganizationForm extends Component {
id="org-max_hosts" id="org-max_hosts"
name="max_hosts" name="max_hosts"
type="number" type="number"
label={<Fragment> label={
{i18n._(t`Max Hosts`)} (
<Fragment>
{i18n._(t`Max Hosts`)}
{' '} {' '}
{( {(
<Tooltip <Tooltip
position="right" position="right"
content="The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details." content="The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details."
> >
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
) )}
} </Fragment>
</Fragment>} )
}
validate={minMaxValue(0, 2147483647, i18n)} validate={minMaxValue(0, 2147483647, i18n)}
onChange={(evt) => this.maxHostsChange(evt)} me={me || {}}
// isDisabled={!is_superuser + console.log(is_superuser)} isDisabled={!me.is_superuser}
// isDisabled={this.readUsers} />
isDisabled={this.readUsers? true: false}
/>
<Config> <Config>
{({ custom_virtualenvs }) => ( {({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && ( custom_virtualenvs && custom_virtualenvs.length > 1 && (
@@ -197,20 +187,14 @@ class OrganizationForm extends Component {
} }
FormField.propTypes = { FormField.propTypes = {
//consider changing this in FormField.jsx, as many fields may need tooltips in the label label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired
label: PropTypes.oneOfType ([ };
PropTypes.object,
PropTypes.string
])
}
console.log()
OrganizationForm.propTypes = { OrganizationForm.propTypes = {
organization: PropTypes.shape(), organization: PropTypes.shape(),
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired,
}; };
OrganizationForm.defaultProps = { OrganizationForm.defaultProps = {
organization: { organization: {
@@ -218,7 +202,7 @@ OrganizationForm.defaultProps = {
description: '', description: '',
max_hosts: '0', max_hosts: '0',
custom_virtualenv: '', custom_virtualenv: '',
} },
}; };
OrganizationForm.contextTypes = { OrganizationForm.contextTypes = {

View File

@@ -78,7 +78,7 @@ class OrganizationDetail extends Component {
/> />
<Detail <Detail
label={i18n._(t`Max Hosts`)} label={i18n._(t`Max Hosts`)}
value={''+max_hosts} value={`${max_hosts}`}
/> />
<Detail <Detail
label={i18n._(t`Ansible Environment`)} label={i18n._(t`Ansible Environment`)}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { CardBody } from '@patternfly/react-core'; import { CardBody } from '@patternfly/react-core';
import OrganizationForm from '../../components/OrganizationForm'; import OrganizationForm from '../../components/OrganizationForm';
import { Config } from '../../../../contexts/Config';
import { withNetwork } from '../../../../contexts/Network'; import { withNetwork } from '../../../../contexts/Network';
import { OrganizationsAPI } from '../../../../api'; import { OrganizationsAPI } from '../../../../api';
@@ -64,11 +65,16 @@ class OrganizationEdit extends Component {
return ( return (
<CardBody> <CardBody>
<OrganizationForm <Config>
organization={organization} {({ me }) => (
handleSubmit={this.handleSubmit} <OrganizationForm
handleCancel={this.handleCancel} organization={organization}
/> handleSubmit={this.handleSubmit}
handleCancel={this.handleCancel}
me={me || {}}
/>
)}
</Config>
{error ? <div>error</div> : null} {error ? <div>error</div> : null}
</CardBody> </CardBody>
); );

View File

@@ -11,6 +11,7 @@ import {
Tooltip, Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { Config } from '../../../contexts/Config';
import { withNetwork } from '../../../contexts/Network'; import { withNetwork } from '../../../contexts/Network';
import CardCloseButton from '../../../components/CardCloseButton'; import CardCloseButton from '../../../components/CardCloseButton';
import OrganizationForm from '../components/OrganizationForm'; import OrganizationForm from '../components/OrganizationForm';
@@ -71,10 +72,15 @@ class OrganizationAdd extends React.Component {
</Tooltip> </Tooltip>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<OrganizationForm <Config>
handleSubmit={this.handleSubmit} {({ me }) => (
handleCancel={this.handleCancel} <OrganizationForm
/> handleSubmit={this.handleSubmit}
handleCancel={this.handleCancel}
me={me || {}}
/>
)}
</Config>
{error ? <div>error</div> : ''} {error ? <div>error</div> : ''}
</CardBody> </CardBody>
</Card> </Card>

View File

@@ -21,7 +21,7 @@ export function maxLength (max, i18n) {
export function minMaxValue (min, max, i18n) { export function minMaxValue (min, max, i18n) {
return value => { return value => {
if (typeof value !== 'number' || value > max || value < min) { if (value < min || value > max) {
return i18n._(t`This field must be a number and have a value between ${min} and ${max}`); return i18n._(t`This field must be a number and have a value between ${min} and ${max}`);
} }
return undefined; return undefined;