Skip to content

Commit

Permalink
React component for creating Nuage subnet
Browse files Browse the repository at this point in the history
With this commit we implement react component used to capture
user inputs for creating CloudSubnet. Also, we add button to
NetworkRouter details page to trigger the flow.

RFE BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1574972

Signed-off-by: Miha Pleško <miha.plesko@xlab.si>
  • Loading branch information
miha-plesko committed Aug 13, 2018
1 parent 48b1522 commit 056dfef
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 19 deletions.
@@ -0,0 +1,35 @@
module ManageIQ
module Providers
module Nuage
module ToolbarOverrides
class NetworkRouterCenter < ::ApplicationHelper::Toolbar::Override
button_group(
'nuage_network_router',
[
select(
:nuage_network_router,
'fa fa-cog fa-lg',
t = N_('Edit'),
t,
:items => [
button(
:nuage_create_cloud_subnet,
'pficon pficon-add-circle-o fa-lg',
t = N_('Create L3 Cloud Subnet'),
t,
:data => {'function' => 'sendDataWithRx',
'function-data' => {:controller => 'provider_dialogs',
:button => :nuage_create_cloud_subnet,
:modal_title => N_('Create L3 Cloud Subnet'),
:component_name => 'CreateNuageCloudSubnetForm'}.to_json},
:klass => ApplicationHelper::Button::ButtonWithoutRbacCheck
),
]
)
]
)
end
end
end
end
end
53 changes: 53 additions & 0 deletions app/javascript/components/create-nuage-cloud-subnet-form.jsx
@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import NuageCloudSubnetForm from './forms/nuage-cloud-subnet-form'
import { handleApiError, createSubnet, fetchRouter } from '../utils/api.js'

class CreateNuageCloudSubnetForm extends React.Component {
constructor(props) {
super(props);
this.handleFormStateUpdate = this.handleFormStateUpdate.bind(this);
this.state = {
loading: true
}
}

componentDidMount() {
this.props.dispatch({
type: 'FormButtons.init',
payload: {
newRecord: true,
pristine: true,
addClicked: () => createSubnet(this.state.values, this.state.emsId, this.state.routerRef)
}
});
fetchRouter(ManageIQ.record.recordId).then(router => {
this.setState({emsId: router.ems_id, routerRef: router.ems_ref, loading: false});
}, handleApiError(this));
}

handleFormStateUpdate(formState) {
this.props.dispatch({ type: 'FormButtons.saveable', payload: formState.valid });
this.props.dispatch({ type: 'FormButtons.pristine', payload: formState.pristine });
this.setState({ values: formState.values });
}

render() {
if(this.state.error) {
return <p>{this.state.error}</p>
}
return (
<NuageCloudSubnetForm
updateFormState={this.handleFormStateUpdate}
loading={this.state.loading}
/>
);
}
}

CreateNuageCloudSubnetForm.propTypes = {
dispatch: PropTypes.func.isRequired,
};

export default connect()(CreateNuageCloudSubnetForm);
79 changes: 79 additions & 0 deletions app/javascript/components/forms/nuage-cloud-subnet-form.jsx
@@ -0,0 +1,79 @@
import React, { Component } from 'react';
import { Form, Field, FormSpy } from 'react-final-form';
import { Form as PfForm, Grid, Button, Col, Row, Spinner } from 'patternfly-react';
import PropTypes from 'prop-types';
import { required } from 'redux-form-validators';

import { FinalFormField, FinalFormTextArea, FinalFormSelect } from '@manageiq/react-ui-components/dist/forms';
import { ip4Validator, netmaskValidator } from '../../utils/validators'

const NuageCloudSubnetForm = ({loading, updateFormState}) => {
if(loading){
return (
<Spinner loading size="lg" />
);
}

return (
<Form
onSubmit={() => {}} // handled by modal
render={({ handleSubmit }) => (
<PfForm horizontal>
<FormSpy onChange={state => updateFormState({ ...state, values: state.values })} />
<Grid fluid>
<Row>
<Col xs={12}>
<Field
name="name"
component={FinalFormField}
label={__('Name')}
placeholder="Subnet Name"
validate={required({ msg: 'Name is required' })}
/>
</Col>
<Col xs={12}>
<Field
name="address"
component={FinalFormField}
label={__('Address')}
placeholder="100.100.100.0"
validate={ip4Validator()}
/>
</Col>
<Col xs={12}>
<Field
name="netmask"
component={FinalFormField}
label={__('Netmask')}
placeholder="255.255.255.0"
validate={netmaskValidator()}
/>
</Col>
<Col xs={12}>
<Field
name="gateway"
component={FinalFormField}
label={__('Gateway')}
placeholder="100.100.100.1"
validate={ip4Validator()}
/>
</Col>
<hr />
</Row>
</Grid>
</PfForm>
)}
/>
);
};

NuageCloudSubnetForm.propTypes = {
updateFormState: PropTypes.func.isRequired,
loading: PropTypes.bool
};

NuageCloudSubnetForm.defaultProps = {
loading: false,
};

export default NuageCloudSubnetForm;
3 changes: 3 additions & 0 deletions app/javascript/packs/component-definitions-common.js
@@ -0,0 +1,3 @@
import CreateNuageCloudSubnetForm from '../components/create-nuage-cloud-subnet-form';

ManageIQ.component.addReact('CreateNuageCloudSubnetForm', CreateNuageCloudSubnetForm);
18 changes: 18 additions & 0 deletions app/javascript/utils/api.js
@@ -0,0 +1,18 @@
export const createSubnet = (values, emsId, routerRef) => API.post(`/api/providers/${emsId}/cloud_subnets`, {
action: 'create',
resource: {...values, router_ref: routerRef},
}).then(response => {
response['results'].forEach(res => window.add_flash(res.message, res.success ? 'success' : 'error'));
});

export const fetchRouter = (routerId) => API.get(`/api/network_routers/${routerId}?attributes=ems_ref,name,ems_id`);

export const handleApiError = (self) => {
return (err) => {
let msg = __('Unknown API error');
if(err.data && err.data.error && err.data.error.message) {
msg = err.data.error.message
}
self.setState({loading: false, error: msg});
};
};
15 changes: 15 additions & 0 deletions app/javascript/utils/validators.js
@@ -0,0 +1,15 @@
import { addValidator } from 'redux-form-validators';

export const ip4Validator = addValidator({
defaultMessage: __('Must be IPv4 address'),
validator: function(options, value, allValues) {
return (/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/).test(value)
}
});

export const netmaskValidator = addValidator({
defaultMessage: __('Must be netmask'),
validator: function(options, value, allValues) {
return (/^(((128|192|224|240|248|252|254)\.0\.0\.0)|(255\.(0|128|192|224|240|248|252|254)\.0\.0)|(255\.255\.(0|128|192|224|240|248|252|254)\.0)|(255\.255\.255\.(0|128|192|224|240|248|252|254)))$/).test(value)
}
});
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -48,6 +48,6 @@
"jest": {
"testURL": "http://localhost",
"setupTestFrameworkScriptFile": "<rootDir>/spec/javascript/setup.js",
"testPathDirs": ["<rootDir>/spec/javascript"]
"roots": ["<rootDir>/spec/javascript"]
}
}

This file was deleted.

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CreateNuageCloudSubnetForm componentDidMount router fetch fails 1`] = `
<p>
MSG
</p>
`;

exports[`CreateNuageCloudSubnetForm componentDidMount router fetch succeeds 1`] = `
<NuageCloudSubnetForm
loading={false}
updateFormState={[Function]}
/>
`;
11 changes: 0 additions & 11 deletions spec/javascript/components/components.test.jsx

This file was deleted.

55 changes: 55 additions & 0 deletions spec/javascript/components/create-nuage-cloud-subnet-form.test.jsx
@@ -0,0 +1,55 @@
import CreateNuageCloudSubnetForm from '../../../app/javascript/components/create-nuage-cloud-subnet-form'
import * as api from "../../../app/javascript/utils/api";

const fetchRouterMock = jest.spyOn(api, 'fetchRouter').mockResolvedValue('SUCCESS');
const dispatchMock = jest.spyOn(DEFAULT_STORE, 'dispatch');

let renderComponent;
let renderComponentFull;

describe('CreateNuageCloudSubnetForm', () => {
beforeAll(() => {
renderComponent = () => shallowRedux(<CreateNuageCloudSubnetForm />);
renderComponentFull = () => mountRedux(<CreateNuageCloudSubnetForm />);
});

beforeEach(() => {
ManageIQ.record.recordId = 123;
});

describe('componentDidMount', () => {
it('router fetch succeeds', () => {
fetchRouterMock.mockResolvedValue({ ems_id: 111, ems_ref: 222 });
let component = renderComponent();
return fetchRouterMock().then(() => {
component.update();
expect(fetchRouterMock).toHaveBeenCalledWith(123);
expect(component.state()).toEqual({emsId: 111, routerRef: 222, loading: false});
expect(toJson(component)).toMatchSnapshot();
});
});

it('router fetch fails', () => {
fetchRouterMock.mockRejectedValue({data: {error: { message: 'MSG'}}});
let component = renderComponent();
return fetchRouterMock().catch(() => {
component.update();
expect(fetchRouterMock).toHaveBeenCalledWith(123);
expect(component.state()).toEqual({loading: false, error: 'MSG'});
expect(toJson(component)).toMatchSnapshot();
});
});
});

describe('redux bindings', () => {
it('when fully mounted', () => {
fetchRouterMock.mockResolvedValue('SUCCESS');
let component = renderComponentFull();
return fetchRouterMock().then(() => {
expect(dispatchMock).toHaveBeenCalledWith({type: 'FormButtons.init', payload: expect.anything()});
expect(dispatchMock).toHaveBeenCalledWith({type: 'FormButtons.saveable', payload: expect.anything()});
expect(dispatchMock).toHaveBeenCalledWith({type: 'FormButtons.pristine', payload: expect.anything()});
});
});
});
});

0 comments on commit 056dfef

Please sign in to comment.