Deployment Plan page updates

Updated deployment steps, added Roles listing to Register and Assign
Nodes step, implemented plan reloading process when plan is switched,
listing of assigned/available/introspected nodes

Change-Id: I1cff92d7d95382013304751ebdee2a2734fb331c
This commit is contained in:
Jiri Tomasek 2016-04-07 11:59:55 +02:00
parent a1d162faa6
commit 04b32c117e
33 changed files with 622 additions and 574 deletions

View File

@ -0,0 +1,36 @@
import RolesActions from '../../js/actions/RolesActions';
import RolesConstants from '../../js/constants/RolesConstants';
describe('Roles actions', () => {
it('should create an action for pending Roles request', () => {
const expectedAction = {
type: RolesConstants.FETCH_ROLES_PENDING
};
expect(RolesActions.fetchRolesPending()).toEqual(expectedAction);
});
it('should create an action for successful Roles retrieval', () => {
const normalizedRolesResponse = {
entities: {
roles: {
1: 'first role',
2: 'second role'
}
},
result: [1, 2]
};
const expectedAction = {
type: RolesConstants.FETCH_ROLES_SUCCESS,
payload: normalizedRolesResponse.entities.roles
};
expect(RolesActions.fetchRolesSuccess(normalizedRolesResponse.entities.roles))
.toEqual(expectedAction);
});
it('should create an action for failed Roles request', () => {
const expectedAction = {
type: RolesConstants.FETCH_ROLES_FAILED
};
expect(RolesActions.fetchRolesFailed()).toEqual(expectedAction);
});
});

View File

@ -3,17 +3,9 @@ import TestUtils from 'react-addons-test-utils';
import Login from '../../js/components/Login';
import LoginActions from '../../js/actions/LoginActions';
import LoginStore from '../../js/stores/LoginStore';
let loginInstance;
let loggedInState = {
token: '123123123',
user: 'admin',
serviceCatalog: 'some service catalog',
metadata: 'some metadata'
};
describe('Login component', () => {
describe('When user is not logged in', () => {
beforeEach(() => {
@ -49,10 +41,6 @@ describe('Login component', () => {
});
describe('When user is logged in', () => {
beforeEach(() => {
LoginStore.getState = LoginStore.getState.mockReturnValue(loggedInState);
LoginStore.isLoggedIn = LoginStore.isLoggedIn.mockReturnValue(true);
});
xit('redirects to nextPath when user is logged in and visits login page', () => {
loginInstance = new Login();
loginInstance.context = {

View File

@ -1,20 +0,0 @@
import { List, Map } from 'immutable';
import { mapStateToProps } from '../../../js/components/deployment-plan/DeploymentPlan.js';
describe('DeploymentPlan mapStateToProps', () => {
describe('hasPlans flag', () => {
it('returns ``hasPlans`` as `false`', () => {
let props = mapStateToProps({ plans: Map({
all: List()
})});
expect(props.hasPlans).toBe(false);
});
it('returns ``hasPlans`` as `false`', () => {
let props = mapStateToProps({ plans: Map({
all: List(['foo', 'bar'])
})});
expect(props.hasPlans).toBe(true);
});
});
});

View File

@ -0,0 +1,42 @@
import { List, Map } from 'immutable';
import { mapStateToProps } from '../../../js/components/deployment-plan/DeploymentPlan.js';
describe('DeploymentPlan mapStateToProps', () => {
describe('hasPlans flag', () => {
it('returns ``hasPlans`` as `false`', () => {
let props = mapStateToProps(
{
plans: Map({ all: List() }),
roles: Map({
loaded: false,
isFetching: false,
roles: Map()
}),
nodes: Map({
isFetching: false,
all: Map()
})
}
);
expect(props.hasPlans).toBe(false);
});
it('returns ``hasPlans`` as `false`', () => {
let props = mapStateToProps(
{
plans: Map({ all: List(['foo', 'bar']) }),
roles: Map({
loaded: false,
isFetching: false,
roles: Map()
}),
nodes: Map({
isFetching: false,
all: Map()
})
}
);
expect(props.hasPlans).toBe(true);
});
});
});

View File

@ -0,0 +1,76 @@
import matchers from 'jasmine-immutable-matchers';
import { Map } from 'immutable';
import { Role } from '../../js/immutableRecords/roles';
import PlansConstants from '../../js/constants/PlansConstants';
import RolesConstants from '../../js/constants/RolesConstants';
import rolesReducer from '../../js/reducers/rolesReducer';
describe('rolesReducer', () => {
beforeEach(() => {
jasmine.addMatchers(matchers);
});
const initialState = Map({
loaded: false,
isFetching: false,
roles: Map()
});
const updatedState = Map({
loaded: true,
isFetching: false,
roles: Map({
control: new Role({
title: 'Controller',
name: 'control'
})
})
});
it('should return initial state', () => {
expect(rolesReducer(initialState, {})).toEqual(initialState);
});
it('should handle FETCH_ROLES_PENDING', () => {
const action = {
type: RolesConstants.FETCH_ROLES_PENDING
};
const newState = rolesReducer(initialState, action);
expect(newState.get('isFetching')).toEqual(true);
});
it('should handle FETCH_ROLES_SUCCESS', () => {
const action = {
type: RolesConstants.FETCH_ROLES_SUCCESS,
payload: {
control: {
name: 'control',
title: 'Controller'
}
}
};
const newState = rolesReducer(initialState, action);
expect(newState.get('roles')).toEqualImmutable(
updatedState.get('roles')
);
expect(newState.get('loaded')).toEqual(true);
});
it('should handle FETCH_ROLES_FAILED', () => {
const action = {
type: RolesConstants.FETCH_ROLES_FAILED
};
const newState = rolesReducer(initialState, action);
expect(newState.get('loaded')).toEqual(true);
expect(newState.get('roles')).toEqual(Map());
});
it('should handle PLAN_CHOSEN', () => {
const action = {
type: PlansConstants.PLAN_CHOSEN
};
const newState = rolesReducer(updatedState, action);
expect(newState.get('loaded')).toEqual(false);
});
});

View File

@ -0,0 +1,53 @@
import { fromJS, Map } from 'immutable';
import matchers from 'jasmine-immutable-matchers';
import * as selectors from '../../js/selectors/nodes';
describe('Nodes selectors', () => {
beforeEach(() => {
jasmine.addMatchers(matchers);
});
const state = {
nodes: Map({
isFetching: false,
dataOperationInProgress: false,
allFilter: '',
registeredFilter: '',
introspectedFilter: '',
provisionedFilter: '',
maintenanceFilter: '',
all: fromJS([
{
provision_state: 'available',
provision_updated_at: '12-12-2016',
properties: { capabilities: 'boot_option:local' }
},
{
provision_state: 'available',
provision_updated_at: '12-12-2016',
properties: { capabilities: 'boot_option:local,profile:control' }
},
{
provision_state: 'available',
provision_updated_at: '12-12-2016',
properties: { capabilities: 'profile:control,boot_option:local' }
},
{
provision_state: 'available',
provision_updated_at: '12-12-2016',
properties: { capabilities: 'profile:compute,boot_option:local' }
},
{
provision_state: 'available',
provision_updated_at: '12-12-2016',
properties: { capabilities: '' }
}
])
})
};
it('provides selector to list Introspected Nodes unassigned to a Role', () => {
expect(selectors.getUnassignedIntrospectedNodes(state).size).toEqual(2);
});
});

View File

@ -1,10 +1,51 @@
import AppDispatcher from '../dispatchers/AppDispatcher.js';
import { normalize, arrayOf } from 'normalizr';
// import NotificationActions from './NotificationActions';
import RolesConstants from '../constants/RolesConstants';
import roles from '../mockData/roles';
import { roleSchema } from '../normalizrSchemas/roles';
export default {
updateRole(role) {
AppDispatcher.dispatch({
actionType: 'UPDATE_FLAVOR_ROLE',
role: role
});
fetchRoles() {
return (dispatch, getState) => {
dispatch(this.fetchRolesPending());
// TODO(jtomasek): Replace this with an actual action which fetches Roles from HEAT
// fake the reques/response delay
const normalizedRoles = normalize(roles, arrayOf(roleSchema)).entities.roles;
setTimeout(() => dispatch(this.fetchRolesSuccess(normalizedRoles)), 500);
// TODO(jtomasek): Use this when roles are fetched from Heat
// HeatApiService.getRoles().then((response) => {
// response = normalize(response, arrayOf(roleSchema));
// dispatch(this.fetchRolesSuccess(response));
// }).catch((error) => {
// console.error('Error in RolesAction.fetchRoles', error.stack || error); //eslint-disable-line no-console
// dispatch(this.fetchRolesFailed());
// let errorHandler = new HeatApiErrorHandler(error);
// errorHandler.errors.forEach((error) => {
// dispatch(NotificationActions.notify(error));
// });
// });
};
},
fetchRolesPending() {
return {
type: RolesConstants.FETCH_ROLES_PENDING
};
},
fetchRolesSuccess(roles) {
return {
type: RolesConstants.FETCH_ROLES_SUCCESS,
payload: roles
};
},
fetchRolesFailed() {
return {
type: RolesConstants.FETCH_ROLES_FAILED
};
}
};

View File

@ -40,7 +40,6 @@ export default class NavBar extends React.Component {
</ul>
<ul className="nav navbar-nav navbar-primary">
<NavTab to="/deployment-plan">Deployment Plan</NavTab>
<NavTab to="/roles">Roles</NavTab>
<NavTab to="/nodes">Nodes</NavTab>
</ul>
</div>

View File

@ -4,13 +4,16 @@ import { Link } from 'react-router';
import React from 'react';
import { getAllPlansButCurrent } from '../../selectors/plans';
import { getIntrospectedNodes, getUnassignedIntrospectedNodes } from '../../selectors/nodes';
import DeploymentStep from './DeploymentStep';
import PlansDropdown from './PlansDropdown';
import FlavorStore from '../../stores/FlavorStore';
import Loader from '../ui/Loader';
import NodesActions from '../../actions/NodesActions';
import NoPlans from './NoPlans';
import NotificationActions from '../../actions/NotificationActions';
import PlansActions from '../../actions/PlansActions';
import Roles from './Roles';
import RolesActions from '../../actions/RolesActions';
import TripleOApiService from '../../services/TripleOApiService';
import TripleOApiErrorHandler from '../../services/TripleOApiErrorHandler';
@ -18,15 +21,10 @@ class DeploymentPlan extends React.Component {
constructor() {
super();
this.state = {
readyToDeploy: false,
flavors: []
readyToDeploy: false
};
}
componentDidMount() {
this.setState({flavors: FlavorStore.getState().flavors});
}
handleDeploy() {
TripleOApiService.deployPlan(this.props.currentPlanName).then((response) => {
this.setState({ parameters: response.parameters });
@ -44,16 +42,23 @@ class DeploymentPlan extends React.Component {
}
render() {
let deploymentConfigLinks = [
<Link className="btn btn-link" key="1" to={'/deployment-plan/configuration'}>
const deploymentConfigLinks = [
<Link className="btn btn-link"
key="deploymentConfiguration"
to={'/deployment-plan/configuration'}>
Edit Configuration
</Link>
];
let roleConfigLinks = [
<Link className="btn btn-link" key="2" to={'/deployment-plan/configuration/parameters'}>
Edit Parameters
</Link>
const registerAndAssignLinks = [
<Link className="btn btn-default" key="registerNodes" to={'/nodes/registered/register'}>
<span className="fa fa-plus"/> Register Nodes
</Link>,
<span key="space">&nbsp;</span>,
<Loader key="rolesLoader"
loaded={!(this.props.rolesLoaded && this.props.isFetchingRoles)}
content="Loading Deployment Roles..."
inline/>
];
let children;
@ -87,12 +92,16 @@ class DeploymentPlan extends React.Component {
<DeploymentStep title="Specify Deployment Configuration"
subTitle={deploymentConfigDescription}
links={deploymentConfigLinks}/>
<DeploymentStep title="Create Flavors and Register Nodes"
subTitle={this.state.flavors.length > 0 ?
'' : 'There are no flavors or nodes currently.'} />
<DeploymentStep title="Configure and Assign Roles"
subTitle="Parameters for all roles can be configured."
links={roleConfigLinks}/>
<DeploymentStep title="Register and Assign Nodes"
links={registerAndAssignLinks}>
<Roles roles={this.props.roles.toList().toJS()}
introspectedNodes={this.props.introspectedNodes}
unassignedIntrospectedNodes={this.props.unassignedIntrospectedNodes}
fetchRoles={this.props.fetchRoles}
fetchNodes={this.props.fetchNodes}
isFetchingNodes={this.props.isFetchingNodes}
loaded={this.props.rolesLoaded}/>
</DeploymentStep>
<DeploymentStep title="Deploy">
<div className="actions pull-left">
<a className={'link btn btn-primary btn-lg ' +
@ -120,26 +129,40 @@ DeploymentPlan.propTypes = {
children: React.PropTypes.node,
choosePlan: React.PropTypes.func,
currentPlanName: React.PropTypes.string,
fetchNodes: React.PropTypes.func,
fetchRoles: React.PropTypes.func,
hasPlans: React.PropTypes.bool,
inactivePlans: ImmutablePropTypes.map,
introspectedNodes: ImmutablePropTypes.list,
isFetchingNodes: React.PropTypes.bool,
isFetchingPlans: React.PropTypes.bool,
route: React.PropTypes.object
isFetchingRoles: React.PropTypes.bool,
roles: ImmutablePropTypes.map,
rolesLoaded: React.PropTypes.bool,
route: React.PropTypes.object,
unassignedIntrospectedNodes: ImmutablePropTypes.list
};
export function mapStateToProps(state) {
return {
currentPlanName: state.plans.get('currentPlanName'),
isFetchingNodes: state.nodes.get('isFetching'),
isFetchingPlans: state.plans.get('isFetchingPlans'),
isFetchingRoles: state.roles.get('isFetching'),
hasPlans: !state.plans.get('all').isEmpty(),
inactivePlans: getAllPlansButCurrent(state)
inactivePlans: getAllPlansButCurrent(state),
introspectedNodes: getIntrospectedNodes(state),
roles: state.roles.get('roles'),
rolesLoaded: state.roles.get('loaded'),
unassignedIntrospectedNodes: getUnassignedIntrospectedNodes(state)
};
}
function mapDispatchToProps(dispatch) {
return {
choosePlan: planName => {
dispatch(PlansActions.choosePlan(planName));
}
choosePlan: planName => dispatch(PlansActions.choosePlan(planName)),
fetchRoles: () => dispatch(RolesActions.fetchRoles()),
fetchNodes: () => dispatch(NodesActions.fetchNodes())
};
}

View File

@ -10,8 +10,10 @@ export default class DeploymentStep extends React.Component {
</h3>
<div className="row">
<div className="col-sm-12">
<span className="deployment-step-subtitle">{this.props.subTitle}</span>
{this.props.links}
<div className="deployment-step-subtitle">
<span>{this.props.subTitle}</span>
{this.props.links}
</div>
</div>
<div className="col-sm-12">
{this.props.children}

View File

@ -0,0 +1,46 @@
import React from 'react';
import Loader from '../ui/Loader';
export default class RoleCard extends React.Component {
render() {
return (
<div className={`card-pf card-pf-accented role-card ${this.props.name}`}>
<h2 className="card-pf-title">
{this.props.title}
</h2>
<div className="card-pf-body">
<Loader loaded={!this.props.isFetchingNodes}
content="Loading Nodes..."
inline>
<p className="card-pf-utilization-details">
<span className="card-pf-utilization-card-details-count">
{this.props.assignedNodesCount}
</span>
<span className="card-pf-utilization-card-details-description">
<span className="card-pf-utilization-card-details-line-1">Nodes assigned</span>
<span className="card-pf-utilization-card-details-line-2">
of {this.props.availableNodesCount} available
</span>
</span>
</p>
</Loader>
</div>
<div className="card-pf-footer">
<p>
<a href="#" className="card-pf-link-with-icon">
<span className="pficon pficon-add-circle-o"></span>Assign Nodes
</a>
</p>
</div>
</div>
);
}
}
RoleCard.propTypes = {
assignedNodesCount: React.PropTypes.number.isRequired,
availableNodesCount: React.PropTypes.number.isRequired,
isFetchingNodes: React.PropTypes.bool,
name: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired
};

View File

@ -0,0 +1,74 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Loader from '../ui/Loader';
import RoleCard from './RoleCard';
export default class Roles extends React.Component {
componentDidMount() {
this.props.fetchRoles();
this.props.fetchNodes();
}
componentDidUpdate() {
if(!this.props.loaded) {
this.props.fetchRoles();
this.props.fetchNodes();
}
}
getAssignedNodes(roleName) {
return this.props.introspectedNodes.filter(
node => node.getIn(['properties', 'capabilities']).includes(`profile:${roleName}`)
);
}
renderRoleCards() {
return this.props.roles.map(role => {
return (
<div className="col-xs-6 col-sm-4 col-md-3 col-lg-2" key={role.name}>
<RoleCard name={role.name}
title={role.title}
isFetchingNodes={this.props.isFetchingNodes}
assignedNodesCount={this.getAssignedNodes(role.name).size}
availableNodesCount={this.props.introspectedNodes.size}/>
</div>
);
});
}
render() {
return (
<Loader loaded={this.props.loaded}
content="Loading Deployment Roles..."
size="lg"
inline>
<Loader loaded={!this.props.isFetchingNodes}
content="Loading Nodes..."
inline>
<p>
There are <strong>{this.props.unassignedIntrospectedNodes.size}</strong> of <strong>
{this.props.introspectedNodes.size}</strong> Introspected
Nodes available to assign
</p>
</Loader>
<div className="panel panel-default roles-panel">
<div className="panel-body">
<div className="row-cards-pf">
{this.renderRoleCards()}
</div>
</div>
</div>
</Loader>
);
}
}
Roles.propTypes = {
fetchNodes: React.PropTypes.func.isRequired,
fetchRoles: React.PropTypes.func.isRequired,
introspectedNodes: ImmutablePropTypes.list,
isFetchingNodes: React.PropTypes.bool,
loaded: React.PropTypes.bool.isRequired,
roles: React.PropTypes.array.isRequired,
unassignedIntrospectedNodes: ImmutablePropTypes.list
};

View File

@ -6,6 +6,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import NavTab from '../ui/NavTab';
import NodesActions from '../../actions/NodesActions';
import { getRegisteredNodes,
getIntrospectedNodes,
getProvisionedNodes,
getMaintenanceNodes } from '../../selectors/nodes';
class Nodes extends React.Component {
componentDidMount() {
@ -88,13 +92,10 @@ function mapStateToProps(state) {
return {
nodes: state.nodes.merge(
Map({
registered: state.nodes.get('all').filter( node => node.provision_state === 'available' &&
!node.provision_updated_at ||
node.provision_state === 'manageable' ),
introspected: state.nodes.get('all').filter( node => node.provision_state === 'available' &&
!!node.provision_updated_at ),
provisioned: state.nodes.get('all').filter( node => node.instance_uuid ),
maintenance: state.nodes.get('all').filter( node => node.maintenance )
registered: getRegisteredNodes(state),
introspected: getIntrospectedNodes(state),
provisioned: getProvisionedNodes(state),
maintenance: getMaintenanceNodes(state)
})
)
};

View File

@ -1,44 +0,0 @@
import React from 'react';
import NodeStack from './NodeStack';
export default class NodePicker extends React.Component {
/*
Component that implements Node Count Picker expects onIncrement function
(that expects increment parameter) passed through props from owner.
*/
render() {
return (
<div className="node-picker">
<PickerArrow direction="left"
increment={this.props.onIncrement.bind(this, -this.props.incrementValue)}/>
<NodeStack count={this.props.nodeCount}/>
<PickerArrow direction="right"
increment={this.props.onIncrement.bind(this, this.props.incrementValue)}/>
</div>
);
}
}
NodePicker.propTypes = {
incrementValue: React.PropTypes.number,
nodeCount: React.PropTypes.number.isRequired,
onIncrement: React.PropTypes.func.isRequired
};
NodePicker.defaultProps = { incrementValue: 1 };
export class PickerArrow extends React.Component {
render() {
return (
<button className="picker-arrow" onClick={this.props.increment}>
<span className={'fa fa-angle-' + this.props.direction}
aria-hidden="true"></span>
</button>
);
}
}
PickerArrow.propTypes = {
direction: React.PropTypes.oneOf(['left', 'right']).isRequired,
increment: React.PropTypes.func.isRequired
};

View File

@ -1,21 +0,0 @@
import ClassNames from 'classnames';
import React from 'react';
export default class NodeStack extends React.Component {
render() {
let classes = ClassNames({
'stack': true,
'single-stack': this.props.count == 2,
'double-stack': this.props.count > 2
});
return (
<div className="node-stack">
<div className={classes}>{this.props.count}</div>
</div>
);
}
}
NodeStack.propTypes = {
count: React.PropTypes.number.isRequired
};

View File

@ -1,192 +0,0 @@
import React from 'react';
import RolesActions from '../../actions/RolesActions';
import FlavorStore from '../../stores/FlavorStore';
import NodePicker from './NodePicker';
import NodeStack from './NodeStack';
export default class Roles extends React.Component {
constructor(props) {
super(props);
this.state = {
flavors: []
};
this.changeListener = this._onChange.bind(this);
}
componentDidMount() {
this.setState(FlavorStore.getState());
FlavorStore.addChangeListener(this.changeListener);
}
componentWillUnmount() {
FlavorStore.removeChangeListener(this.changeListener);
}
_onChange() {
this.setState(FlavorStore.getState());
}
render() {
return (
<div className="row">
<div className="col-sm-12">
<div className="page-header">
<h1>Roles</h1>
</div>
<div className="row">
<div className="col-sm-12">
<h3>Hardware</h3>
<FlavorPanelList flavors={this.state.flavors}/>
</div>
</div>
</div>
</div>
);
}
}
// export class FreeRolesList extends React.Component {
// render() {
// let freeRoles = this.props.data.filter((role, index) => {
// return role;
// });
// return (
// <div className="row">
// <RoleList roles={freeRoles}/>
// </div>
// );
// }
// }
export class FlavorPanelList extends React.Component {
render() {
let flavors = this.props.flavors.map((flavor, index) => {
return (
<FlavorPanel flavor={flavor} key={index}/>
);
});
return (
<div>
{flavors}
</div>
);
}
}
FlavorPanelList.propTypes = {
flavors: React.PropTypes.array.isRequired
};
export class FlavorPanel extends React.Component {
render() {
return (
<div className="panel panel-default flavor-panel">
<div className="panel-heading">
<h3 className="panel-title">
<strong>{this.props.flavor.name}</strong>
<small className='subheader'> {this.props.flavor.hwSpecs}</small>
</h3>
</div>
<div className="panel-body">
<div className="row">
<div className="col-sm-4 col-md-3">
<FreeNodesPanel nodeCount={this.props.flavor.freeNodeCount} />
</div>
<RoleList roles={this.props.flavor.roles}/>
<div className="col-sm-4 col-md-3">
<DropZonePanel />
</div>
</div>
</div>
</div>
);
}
}
FlavorPanel.propTypes = {
flavor: React.PropTypes.object.isRequired
};
export class RoleList extends React.Component {
render() {
let roles = this.props.roles.map((role, index) => {
return (
<div className="col-sm-4 col-md-3" key={index}>
<RolePanel role={role}/>
</div>
);
});
return (
<div>
{roles}
</div>
);
}
}
RoleList.propTypes = {
roles: React.PropTypes.array.isRequired
};
export class FreeNodesPanel extends React.Component {
render() {
return (
<div className="panel panel-default role-panel free-nodes-panel">
<div className="panel-heading">
<h3 className="panel-title">Available Nodes</h3>
</div>
<div className="panel-body clearfix">
<NodeStack count={this.props.nodeCount} />
</div>
</div>
);
}
}
FreeNodesPanel.propTypes = {
nodeCount: React.PropTypes.number.isRequired
};
export class DropZonePanel extends React.Component {
render() {
return (
<div className="panel panel-default role-panel drop-zone-panel">
<div className="panel-heading">
<h3 className="panel-title">Add Role</h3>
</div>
<div className="panel-body clearfix">
<span className="glyphicon glyphicon-plus"></span>
</div>
</div>
);
}
}
export class RolePanel extends React.Component {
updateCount(increment) {
let updatedRole = this.props.role;
updatedRole.nodeCount = this.props.role.nodeCount + increment;
RolesActions.updateRole(updatedRole);
}
render() {
return (
<div className={'panel panel-default role-panel ' + this.props.role.name.toLowerCase()}>
<div className="panel-heading">
<h3 className="panel-title">{this.props.role.name}</h3>
</div>
<div className="panel-body clearfix">
<NodePicker nodeCount={this.props.role.nodeCount}
onIncrement={this.updateCount.bind(this)}/>
</div>
</div>
);
}
}
RolePanel.propTypes = {
role: React.PropTypes.object.isRequired
};

View File

@ -2,6 +2,44 @@ import ClassNames from 'classnames';
import React from 'react';
export default class Loader extends React.Component {
renderGlobalLoader(classes) {
return (
<div className={this.props.className}>
<div className="modal modal-routed in" role="loading">
<div className="modal-dialog modal-sm">
<div className="modal-content">
<div className="modal-body loader">
<div className={classes}/>
<div className="text-center">{this.props.content}</div>
</div>
</div>
</div>
</div>
<div className="modal-backdrop in"></div>
</div>
);
}
renderInlineLoader(classes) {
return (
<span className={this.props.className}>
<span className={classes}></span>
{this.props.content}
</span>
);
}
renderDefaultLoader(classes) {
return (
<div style={{marginTop: `${this.props.height/2}px`,
marginBottom: `${this.props.height/2}px`}}
className={this.props.className}>
<div className={classes}/>
<div className="text-center">{this.props.content}</div>
</div>
);
}
render() {
let classes = ClassNames({
'spinner': true,
@ -14,36 +52,11 @@ export default class Loader extends React.Component {
if(!this.props.loaded) {
if(this.props.global) {
return (
<div>
<div className="modal modal-routed in" role="loading">
<div className="modal-dialog modal-sm">
<div className="modal-content">
<div className="modal-body loader">
<div className={classes}/>
<div className="text-center">{this.props.content}</div>
</div>
</div>
</div>
</div>
<div className="modal-backdrop in"></div>
</div>
);
return this.renderGlobalLoader(classes);
} else if(this.props.inline) {
return (
<span>
<span className={classes}></span>
{this.props.content}
</span>
);
return this.renderInlineLoader(classes);
} else {
return (
<div style={{marginTop: `${this.props.height/2}px`,
marginBottom: `${this.props.height/2}px`}}>
<div className={classes}/>
<div className="text-center">{this.props.content}</div>
</div>
);
return this.renderDefaultLoader(classes);
}
}
return React.createElement(this.props.component, {}, this.props.children);
@ -54,6 +67,7 @@ Loader.propTypes = {
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node
]),
className: React.PropTypes.string,
component: React.PropTypes.any, // Component to wrap children when loaded
content: React.PropTypes.string,
global: React.PropTypes.bool,

View File

@ -0,0 +1,7 @@
import keyMirror from 'keymirror';
export default keyMirror({
FETCH_ROLES_PENDING: null,
FETCH_ROLES_SUCCESS: null,
FETCH_ROLES_FAILED: null
});

View File

@ -1,23 +0,0 @@
export default [
{
name: 'Baremetal',
hwSpecs: '1CPU, 40GB RAM, HDD 500GB',
roles: [
{
name: 'Controller',
nodeCount: 2
},
{
name: 'Compute',
nodeCount: 0
}
],
nodeCount: 20
},
{
name: 'Flavor2',
hwSpecs: '1CPU, 20GB RAM, HDD 250GB',
roles: [],
nodeCount: 10
}
];

View File

@ -1,22 +0,0 @@
export default [
{
name: 'Controller',
flavor: null
},
{
name: 'Compute',
flavor: null
},
{
name: 'Ceph-Storage',
flavor: null
},
{
name: 'Cinder-Storage',
flavor: null
},
{
name: 'Swift-Storage',
flavor: null
}
];

View File

@ -0,0 +1,6 @@
import { Record } from 'immutable';
export const Role = Record({
name: '',
title: ''
});

View File

@ -27,7 +27,6 @@ import Plans from './components/plan/Plans.js';
import ProvisionedNodesTabPane from './components/nodes/ProvisionedNodesTabPane';
import RegisterNodesDialog from './components/nodes/RegisterNodesDialog';
import RegisteredNodesTabPane from './components/nodes/RegisteredNodesTabPane';
import Roles from './components/roles/Roles.js';
import TempStorage from './services/TempStorage.js';
import store from './store';
@ -65,8 +64,6 @@ TempStorage.initialized.then(() => {
</Route>
</Route>
<Route path="roles" component={Roles}/>
<Redirect from="nodes" to="nodes/registered"/>
<Route path="nodes" component={Nodes}>
<Route path="registered" component={RegisteredNodesTabPane}>

22
src/js/mockData/roles.js Normal file
View File

@ -0,0 +1,22 @@
export default [
{
name: 'control',
title: 'Controller'
},
{
name: 'compute',
title: 'Compute'
},
{
name: 'blockStorage',
title: 'Block Storage'
},
{
name: 'objectStorage',
title: 'Object Storage'
},
{
name: 'cephStorage',
title: 'Ceph Storage'
}
];

View File

@ -0,0 +1,3 @@
import { Schema } from 'normalizr';
export const roleSchema = new Schema('roles', { idAttribute: 'name' });

View File

@ -6,6 +6,7 @@ import notificationsReducer from './notificationsReducer';
import parametersReducer from './parametersReducer';
import plansReducer from './plansReducer';
import registerNodesReducer from './registerNodesReducer';
import rolesReducer from './rolesReducer';
import validationsReducer from './validationsReducer';
const appReducer = combineReducers({
@ -16,6 +17,7 @@ const appReducer = combineReducers({
parameters: parametersReducer,
plans: plansReducer,
registerNodes: registerNodesReducer,
roles: rolesReducer,
validations: validationsReducer
});

View File

@ -1,4 +1,4 @@
import { List, Map } from 'immutable';
import { fromJS, List, Map } from 'immutable';
import NodesConstants from '../constants/NodesConstants';
@ -21,7 +21,7 @@ export default function nodesReducer(state = initialState, action) {
case NodesConstants.RECEIVE_NODES:
return state
.set('all', List(action.payload))
.set('all', fromJS(action.payload))
.set('isFetching', false);
case NodesConstants.START_NODES_OPERATION:

View File

@ -0,0 +1,37 @@
import { fromJS, Map } from 'immutable';
import PlansConstants from '../constants/PlansConstants';
import RolesConstants from '../constants/RolesConstants';
import { Role } from '../immutableRecords/roles';
const initialState = Map({
loaded: false,
isFetching: false,
roles: Map()
});
export default function rolesReducer(state = initialState, action) {
switch(action.type) {
case RolesConstants.FETCH_ROLES_PENDING:
return state.set('isFetching', true);
case RolesConstants.FETCH_ROLES_SUCCESS:
const roles = action.payload || {};
return state.set('roles', fromJS(roles).map(role => new Role(role)))
.set('isFetching', false)
.set('loaded', true);
case RolesConstants.FETCH_ROLES_FAILED:
return state.set('roles', Map())
.set('isFetching', false)
.set('loaded', true);
case PlansConstants.PLAN_CHOSEN:
return state.set('loaded', false);
default:
return state;
}
}

38
src/js/selectors/nodes.js Normal file
View File

@ -0,0 +1,38 @@
import { createSelector } from 'reselect';
const nodes = state => state.nodes.get('all');
export const getRegisteredNodes = createSelector(
nodes, (nodes) => {
return nodes.filter( node => node.get('provision_state') === 'available' &&
!node.get('provision_updated_at') ||
node.get('provision_state') === 'manageable' );
}
);
export const getIntrospectedNodes = createSelector(
nodes, (nodes) => {
return nodes.filter( node => node.get('provision_state') === 'available' &&
!!node.get('provision_updated_at') );
}
);
export const getProvisionedNodes = createSelector(
nodes, (nodes) => {
return nodes.filter( node => node.get('instance_uuid') );
}
);
export const getMaintenanceNodes = createSelector(
nodes, (nodes) => {
return nodes.filter( node => node.get('maintenance') );
}
);
export const getUnassignedIntrospectedNodes = createSelector(
getIntrospectedNodes, (introspectedNodes) => {
return introspectedNodes.filterNot(
node => node.getIn(['properties', 'capabilities']).match(/.*profile:.+(,|$)/)
);
}
);

View File

@ -1,28 +0,0 @@
import { EventEmitter } from 'events';
import AppDispatcher from '../dispatchers/AppDispatcher';
export default class BaseStore extends EventEmitter {
constructor() {
super();
}
subscribe(actionSubscribe) {
this._dispatchToken = AppDispatcher.register(actionSubscribe());
}
get dispatchToken() {
return this._dispatchToken;
}
emitChange() {
this.emit('CHANGE');
}
addChangeListener(cb) {
this.on('CHANGE', cb);
}
removeChangeListener(cb) {
this.removeListener('CHANGE', cb);
}
}

View File

@ -1,43 +0,0 @@
import BaseStore from './BaseStore';
import Flavors from '../data/Flavors';
class FlavorStore extends BaseStore {
constructor() {
super();
this.subscribe(() => this._registerToActions.bind(this));
this.state = {
flavors: Flavors
};
}
_registerToActions(payload) {
switch(payload.actionType) {
case 'UPDATE_FLAVOR_ROLE':
this.updateFlavorRole(payload.role);
break;
default:
break;
}
}
updateFlavorRole(role) {
this.state.flavors[0].roles.filter((r) => { r.name == role.name; })[0] = role;
this.state.flavors[0].freeNodeCount = this._calculateFreeNodes(this.state.flavors[0]);
this.emitChange();
}
_calculateFreeNodes(flavor) {
let reserved = 0;
flavor.roles.forEach((role) => { reserved += role.nodeCount; });
return flavor.nodeCount - reserved;
}
getState() {
this.state.flavors.forEach((flavor) => {
flavor.freeNodeCount = this._calculateFreeNodes(flavor);
});
return this.state;
}
}
export default new FlavorStore();

View File

@ -1,60 +0,0 @@
import * as _ from 'lodash';
import BaseStore from './BaseStore';
import LoginConstants from '../constants/LoginConstants';
class LoginStore extends BaseStore {
constructor() {
super();
this.subscribe(() => this._registerToActions.bind(this));
this.state = {};
}
_registerToActions(payload) {
switch(payload.actionType) {
case LoginConstants.USER_AUTH_STARTED:
break;
case LoginConstants.LOGIN_USER:
this.onLoginUser(payload.keystoneAccess);
break;
case LoginConstants.LOGOUT_USER:
this.onLogoutUser();
break;
default:
break;
}
}
onLoginUser(keystoneAccess) {
this.state = {
token: keystoneAccess.token,
user: keystoneAccess.user,
serviceCatalog: keystoneAccess.serviceCatalog,
metadata: keystoneAccess.metadata
};
this.emitChange();
}
onLogoutUser() {
this.state = {};
this.emitChange();
}
getState() {
return this.state;
}
isLoggedIn() {
return !!this.state.user;
}
/**
* Returns the public url of an openstack API,
* determined by the service's name.
*/
getServiceUrl(name) {
return _.result(_.find(this.state.serviceCatalog, 'name', name), 'endpoints[0].publicURL');
}
}
export default new LoginStore();

View File

@ -1,34 +0,0 @@
import BaseStore from './BaseStore';
import ValidationsConstants from '../constants/ValidationsConstants';
class ValidationsStore extends BaseStore {
constructor() {
super();
this.subscribe(() => this._registerToActions.bind(this));
this.state = {
stages: []
};
}
_registerToActions(payload) {
switch(payload.actionType) {
case ValidationsConstants.LIST_STAGES:
this.onListStages(payload.stages);
break;
default:
break;
}
}
onListStages(stages) {
this.state.stages = stages;
this.emitChange();
}
getState() {
return this.state;
}
}
export default new ValidationsStore();

View File

@ -27,8 +27,36 @@ ol.deployment-step-list {
}
.deployment-step-subtitle {
margin-left: 10px;
vertical-align: middle;
margin-bottom: 10px;
& > span {
vertical-align: middle;
}
}
}
}
.roles-panel .panel-body {
padding-bottom: 0;
background: #f5f5f5;
}
.role-color-mixin(@colour) {
border-top-color: @colour;
}
.card-pf.role-card {
.card-pf-body {
margin: 0;
.card-pf-utilization-details {
border-bottom: 0;
padding: 0;
}
}
&.card-pf-accented {
&.control{ .role-color-mixin(#f0ab00); }
&.compute{ .role-color-mixin(#007a87); }
&.blockStorage{ .role-color-mixin(#3b0083); }
&.objectStorage{ .role-color-mixin(#0088ce); }
&.cephStorage{ .role-color-mixin(#b35c00); }
}
}