diff --git a/src/__tests__/selectors/nodes.tests.js b/src/__tests__/selectors/nodesAssignment.tests.js
similarity index 90%
rename from src/__tests__/selectors/nodes.tests.js
rename to src/__tests__/selectors/nodesAssignment.tests.js
index 2227cd1f..07ebfb2b 100644
--- a/src/__tests__/selectors/nodes.tests.js
+++ b/src/__tests__/selectors/nodesAssignment.tests.js
@@ -1,11 +1,11 @@
import { fromJS, Map } from 'immutable';
-import * as selectors from '../../js/selectors/nodes';
+import * as selectors from '../../js/selectors/nodesAssignment';
import { Port } from '../../js/immutableRecords/nodes';
import { Role, RolesState } from '../../js/immutableRecords/roles';
-import { Parameter } from '../../js/immutableRecords/parameters';
+import { Parameter, ParametersDefaultState } from '../../js/immutableRecords/parameters';
-describe('Nodes selectors', () => {
+describe('Nodes Assignment selectors', () => {
const state = {
nodes: Map({
isFetching: false,
@@ -113,6 +113,18 @@ describe('Nodes selectors', () => {
identifier: 'block-storage'
})
})
+ }),
+ parameters: new ParametersDefaultState({
+ parameters: Map({
+ ControllerCount: new Parameter({
+ name: 'ControllerCount',
+ default: 2
+ }),
+ ComputeCount: new Parameter({
+ name: 'ComputeCount',
+ default: 1
+ })
+ })
})
};
@@ -286,4 +298,15 @@ describe('Nodes selectors', () => {
expect(result.get('block-storage')).toEqual(0);
});
});
+
+ it('getRoleCountParameterByRole', () => {
+ const nodeCountParametersByRole = selectors.getNodeCountParametersByRole(state);
+ expect(nodeCountParametersByRole.get('control').default).toEqual(2);
+ expect(nodeCountParametersByRole.get('compute').default).toEqual(1);
+ });
+
+ it('getTotalAssignedNodesCount', () => {
+ const totalAssignedNodesCount = selectors.getTotalAssignedNodesCount(state);
+ expect(totalAssignedNodesCount).toEqual(3);
+ });
});
diff --git a/src/__tests__/selectors/parameters.tests.js b/src/__tests__/selectors/parameters.tests.js
index 0e9cb6e1..3d8a7cef 100644
--- a/src/__tests__/selectors/parameters.tests.js
+++ b/src/__tests__/selectors/parameters.tests.js
@@ -89,14 +89,6 @@ describe(' validations selectors', () => {
}),
Service1NestedResourceParameter1: new Parameter({
name: 'Service1NestedResourceParameter1'
- }),
- ControllerCount: new Parameter({
- name: 'ControllerCount',
- default: 2
- }),
- ComputeCount: new Parameter({
- name: 'ComputeCount',
- default: 1
})
}),
mistralParameters: Map()
@@ -104,7 +96,7 @@ describe(' validations selectors', () => {
};
it('getParametersExclInternal', () => {
- expect(selectors.getParametersExclInternal(state).size).toEqual(7);
+ expect(selectors.getParametersExclInternal(state).size).toEqual(5);
});
it('getRootParameters', () => {
@@ -130,15 +122,4 @@ describe(' validations selectors', () => {
it('getRoleNetworkConfig', () => {
expect(selectors.getRoleNetworkConfig(state, 'control').name).toEqual('NetworkConfigResource');
});
-
- it('getRoleCountParameterByRole', () => {
- const nodeCountParametersByRole = selectors.getNodeCountParametersByRole(state);
- expect(nodeCountParametersByRole.get('control').default).toEqual(2);
- expect(nodeCountParametersByRole.get('compute').default).toEqual(1);
- });
-
- it('getTotalAssignedNodesCount', () => {
- const totalAssignedNodesCount = selectors.getTotalAssignedNodesCount(state);
- expect(totalAssignedNodesCount).toEqual(3);
- });
});
diff --git a/src/js/actions/ParametersActions.js b/src/js/actions/ParametersActions.js
index fd87af90..d0a16c64 100644
--- a/src/js/actions/ParametersActions.js
+++ b/src/js/actions/ParametersActions.js
@@ -3,6 +3,7 @@ import { normalize } from 'normalizr';
import { browserHistory } from 'react-router';
import uuid from 'node-uuid';
import * as _ from 'lodash';
+import { startSubmit, stopSubmit } from 'redux-form';
import NotificationActions from '../actions/NotificationActions';
import ParametersConstants from '../constants/ParametersConstants';
@@ -90,11 +91,13 @@ export default {
updateParameters(planName, data, inputFieldNames, url) {
return (dispatch, getState, { getIntl }) => {
const { formatMessage } = getIntl(getState());
+ dispatch(startSubmit('nodesAssignment'));
dispatch(this.updateParametersPending());
MistralApiService.runAction(MistralConstants.PARAMETERS_UPDATE,
{ container: planName, parameters: data })
.then(response => {
dispatch(this.updateParametersSuccess(data));
+ dispatch(stopSubmit('nodesAssignment'));
dispatch(NotificationActions.notify({
title: formatMessage(messages.parametersUpdatedNotficationTitle),
message: formatMessage(messages.parametersUpdatedNotficationMessage),
@@ -104,6 +107,7 @@ export default {
}).catch(error => {
logger.error('Error in ParametersActions.updateParameters', error);
let errorHandler = new MistralApiErrorHandler(error, inputFieldNames);
+ dispatch(stopSubmit('nodesAssignment', { _error: error }));
dispatch(this.updateParametersFailed(errorHandler.errors, errorHandler.formFieldErrors));
});
};
diff --git a/src/js/components/deployment_plan/DeploymentPlan.js b/src/js/components/deployment_plan/DeploymentPlan.js
index 4aa7c849..76cc2979 100644
--- a/src/js/components/deployment_plan/DeploymentPlan.js
+++ b/src/js/components/deployment_plan/DeploymentPlan.js
@@ -8,9 +8,9 @@ import { getCurrentStack,
getCurrentStackDeploymentProgress,
getCurrentStackDeploymentInProgress } from '../../selectors/stacks';
import { getAvailableNodes,
- getAvailableNodesCountsByRole } from '../../selectors/nodes';
-import { getNodeCountParametersByRole,
- getTotalAssignedNodesCount } from '../../selectors/parameters';
+ getAvailableNodesCountsByRole,
+ getNodeCountParametersByRole,
+ getTotalAssignedNodesCount } from '../../selectors/nodesAssignment';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans';
import { getRoles } from '../../selectors/roles';
diff --git a/src/js/components/deployment_plan/NodesAssignment.js b/src/js/components/deployment_plan/NodesAssignment.js
index efd99113..2fc8516b 100644
--- a/src/js/components/deployment_plan/NodesAssignment.js
+++ b/src/js/components/deployment_plan/NodesAssignment.js
@@ -8,8 +8,8 @@ import React from 'react';
import { List, Map } from 'immutable';
import { getAvailableNodesByRole,
- getUntaggedAvailableNodes,
- getNodesOperationInProgress } from '../../selectors/nodes';
+ getUntaggedAvailableNodes } from '../../selectors/nodesAssignment';
+import { getNodesOperationInProgress } from '../../selectors/nodes';
import { getRoles } from '../../selectors/roles';
import { getCurrentPlan } from '../../selectors/plans';
import FormErrorList from '../ui/forms/FormErrorList';
diff --git a/src/js/components/deployment_plan/NodesAssignmentForm.js b/src/js/components/deployment_plan/NodesAssignmentForm.js
new file mode 100644
index 00000000..13129287
--- /dev/null
+++ b/src/js/components/deployment_plan/NodesAssignmentForm.js
@@ -0,0 +1,74 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { debounce } from 'lodash';
+import { Form, reduxForm, submit } from 'redux-form';
+import React from 'react';
+
+import { getAssignedNodesCountsByRole } from '../../selectors/nodesAssignment';
+import { getCurrentPlan } from '../../selectors/plans';
+import ParametersActions from '../../actions/ParametersActions';
+import FormErrorList from '../ui/forms/FormErrorList';
+
+class NodesAssignmentForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.debouncedUpdate = debounce(this.update.bind(this), 1000);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ // TODO(jtomasek): update this to happen as onChange when this is released https://github.com/erikras/redux-form/pull/2576
+ // and combine it with calling to this.props.submit() rather than calling nextProps.handleSubmit(....)
+ // make sure debouncing works properly
+ if (nextProps.dirty && nextProps.valid && !nextProps.submitting) {
+ // nextProps.submit();
+ nextProps.handleSubmit(this.debouncedUpdate.bind(this))();
+ } else if (nextProps.invalid || nextProps.submitting) {
+ this.debouncedUpdate.cancel();
+ }
+ }
+
+ update(data) {
+ this.props.updateParameters(this.props.currentPlan.name, data);
+ }
+
+ render() {
+ const { error, handleSubmit, children } = this.props;
+ return (
+
+ );
+ }
+}
+NodesAssignmentForm.propTypes = {
+ children: React.PropTypes.node,
+ currentPlan: ImmutablePropTypes.record.isRequired,
+ error: React.PropTypes.object,
+ handleSubmit: React.PropTypes.func.isRequired,
+ pristine: React.PropTypes.bool.isRequired,
+ updateParameters: React.PropTypes.func.isRequired
+};
+
+const mapStateToProps = (state, ownProps) => ({
+ currentPlan: getCurrentPlan(state),
+ initialValues: getAssignedNodesCountsByRole(state).toJS()
+});
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ updateParameters: (currentPlanName, data, inputFields, redirectPath) => {
+ dispatch(ParametersActions.updateParameters(
+ currentPlanName, data, inputFields, redirectPath));
+ },
+ submit: () => dispatch(submit('nodesAssignment'))
+ };
+};
+
+const form = reduxForm({
+ enableReinitialize: true,
+ keepDirtyOnReinitialize: true,
+ form: 'nodesAssignment'
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(form(NodesAssignmentForm));
diff --git a/src/js/components/deployment_plan/RoleCard.js b/src/js/components/deployment_plan/RoleCard.js
index 13af6add..804320eb 100644
--- a/src/js/components/deployment_plan/RoleCard.js
+++ b/src/js/components/deployment_plan/RoleCard.js
@@ -1,8 +1,14 @@
-import { defineMessages, FormattedMessage } from 'react-intl';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import { Field } from 'redux-form';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import Link from '../ui/Link';
+import NodePickerInput from '../ui/reduxForm/NodePickerInput';
+import { maxValue,
+ minValue,
+ number,
+ messages as validationMessages } from '../ui/reduxForm/validations';
const messages = defineMessages({
nodesAssigned: {
@@ -23,10 +29,18 @@ const messages = defineMessages({
const RoleCard = ({ assignedNodesCountParameter,
availableNodesCount,
identifier,
+ intl,
name,
title }) => {
const disabled = !assignedNodesCountParameter
|| !availableNodesCount && !assignedNodesCountParameter.default;
+ const validations = [
+ maxValue(availableNodesCount, intl.formatMessage(validationMessages.maxValue,
+ { max: availableNodesCount.toString() })),
+ minValue(0, intl.formatMessage(validationMessages.minValue, { min: '0' })),
+ number(intl.formatMessage(validationMessages.number))
+ ];
+
return (
@@ -38,10 +52,22 @@ const RoleCard = ({ assignedNodesCountParameter,
-
-
- {assignedNodesCountParameter ? assignedNodesCountParameter.default : '-'}
-
+
+
+ {assignedNodesCountParameter
+ ?
+ : }
+
-
+
@@ -72,8 +98,9 @@ RoleCard.propTypes = {
assignedNodesCountParameter: ImmutablePropTypes.record,
availableNodesCount: React.PropTypes.number.isRequired,
identifier: React.PropTypes.string.isRequired,
+ intl: React.PropTypes.object,
name: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired
};
-export default RoleCard;
+export default injectIntl(RoleCard);
diff --git a/src/js/components/deployment_plan/Roles.js b/src/js/components/deployment_plan/Roles.js
index e884e59a..d1bc3d72 100644
--- a/src/js/components/deployment_plan/Roles.js
+++ b/src/js/components/deployment_plan/Roles.js
@@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import Loader from '../ui/Loader';
+import NodesAssignmentForm from './NodesAssignmentForm';
import RoleCard from './RoleCard';
const messages = defineMessages({
@@ -49,7 +50,9 @@ class Roles extends React.Component {
content={this.props.intl.formatMessage(messages.loadingDeploymentRoles)}
height={40}>
- {this.renderRoleCards()}
+
+ {this.renderRoleCards()}
+
diff --git a/src/js/components/ui/reduxForm/NodePickerInput.js b/src/js/components/ui/reduxForm/NodePickerInput.js
new file mode 100644
index 00000000..c27a00e4
--- /dev/null
+++ b/src/js/components/ui/reduxForm/NodePickerInput.js
@@ -0,0 +1,77 @@
+import ClassNames from 'classnames';
+import React from 'react';
+
+const NodePickerInput = props => {
+ const { increment, input: { value, onChange }, max, meta: { error, submitting }, min } = props;
+
+ const incrementValue = increment => {
+ const incrementedValue = value + increment;
+ if (incrementedValue <= max && incrementedValue >= min) {
+ return incrementedValue;
+ } else if (incrementedValue > max) {
+ return max;
+ } else if (incrementedValue < min) {
+ return min;
+ }
+ };
+
+ return (
+
+
onChange(incrementValue(increment))}
+ disabled={submitting || value + increment > max}/>
+
+ {value}
+
+ onChange(incrementValue(-increment))}
+ disabled={submitting || value - increment < min}/>
+
+ );
+};
+NodePickerInput.propTypes = {
+ increment: React.PropTypes.number.isRequired,
+ input: React.PropTypes.object.isRequired,
+ max: React.PropTypes.number.isRequired,
+ meta: React.PropTypes.object.isRequired,
+ min: React.PropTypes.number.isRequired
+};
+NodePickerInput.defaultProps = {
+ min: 0
+};
+export default NodePickerInput;
+
+const PickerArrow = ({ direction, disabled, onClick }) => {
+ return (
+
+ );
+};
+PickerArrow.propTypes = {
+ direction: React.PropTypes.oneOf(['up', 'down']).isRequired,
+ disabled: React.PropTypes.bool.isRequired,
+ onClick: React.PropTypes.func.isRequired
+};
+
+const NodeCount = ({ children, error }) => {
+ const classes = ClassNames({
+ 'node-count': true,
+ 'text-danger': error
+ });
+ return (
+
+ {children}
+
+ );
+};
+NodeCount.propTypes = {
+ children: React.PropTypes.node.isRequired,
+ error: React.PropTypes.string
+};
diff --git a/src/js/components/ui/reduxForm/validations.js b/src/js/components/ui/reduxForm/validations.js
new file mode 100644
index 00000000..9c09ea8b
--- /dev/null
+++ b/src/js/components/ui/reduxForm/validations.js
@@ -0,0 +1,25 @@
+import { defineMessages } from 'react-intl';
+
+export const messages = defineMessages({
+ maxValue: {
+ id: 'reduxForm.validations.maxValue',
+ defaultMessage: 'Maximum value is {max}.'
+ },
+ minValue: {
+ id: 'reduxForm.validations.minValue',
+ defaultMessage: 'Minimum value is {min}.'
+ },
+ number: {
+ id: 'reduxForm.validations.number',
+ defaultMessage: 'Value needs to be a number.'
+ }
+});
+
+export const minValue = (min, message) => value =>
+ value && value < min ? message : undefined;
+
+export const maxValue = (max, message) => value =>
+ value && value > max ? message : undefined;
+
+export const number = message => value =>
+ value && isNaN(Number(value)) ? message : undefined;
diff --git a/src/js/reducers/appReducer.js b/src/js/reducers/appReducer.js
index c17ce3be..c5ca39db 100644
--- a/src/js/reducers/appReducer.js
+++ b/src/js/reducers/appReducer.js
@@ -1,4 +1,6 @@
import { combineReducers } from 'redux';
+import { reducer as formReducer } from 'redux-form';
+
import currentPlanReducer from './currentPlanReducer';
import environmentConfigurationReducer from './environmentConfigurationReducer';
import i18nReducer from './i18nReducer';
@@ -26,7 +28,8 @@ const appReducer = combineReducers({
registerNodes: registerNodesReducer,
roles: rolesReducer,
stacks: stacksReducer,
- validations: validationsReducer
+ validations: validationsReducer,
+ form: formReducer
});
export default appReducer;
diff --git a/src/js/selectors/nodes.js b/src/js/selectors/nodes.js
index 7e56998d..5f8eb5ca 100644
--- a/src/js/selectors/nodes.js
+++ b/src/js/selectors/nodes.js
@@ -3,7 +3,6 @@ import { List, Set } from 'immutable';
import { parseNodeCapabilities } from '../utils/nodes';
import { getRoles } from './roles';
-import { getNodeCountParametersByRole } from './parameters';
export const getNodes = state => state.nodes.get('all').sortBy(n => n.get('uuid'));
export const getNodesByIds = (state, nodeIds) =>
@@ -34,7 +33,7 @@ export const getRegisteredNodes = createSelector(
*/
export const getProfilesList = createSelector(
getNodes, nodes => nodes.reduce((profiles, v, k) => {
- const profile = _getNodeCapabilities(v).profile;
+ const profile = getNodeCapabilities(v).profile;
return profile ? profiles.push(profile) : profiles;
}, List()).sort()
);
@@ -47,58 +46,6 @@ export const getAvailableNodeProfiles = createSelector(
Set.fromKeys(roles).union(profiles).toList().sort()
);
-export const getAvailableNodes = createSelector(
- getRegisteredNodes, (nodes) => nodes.filter(node => node.get('provision_state') === 'available')
-);
-
-export const getUntaggedAvailableNodes = createSelector(
- getAvailableNodes, (availableNodes) =>
- availableNodes.filterNot(node => _getNodeCapabilities(node).profile)
-);
-
-/**
- * Returns Nodes available for assignment for each Role (tagged to specific role
- * + untagged nodes)
- * TODO(jtomasek): remove this when NodesAssignment component is no longer used
- */
-export const getAvailableNodesByRole = createSelector(
- [getAvailableNodes, getUntaggedAvailableNodes, getRoles], (nodes, untaggedNodes, roles) =>
- roles.map(role => nodes.filter(node => _getNodeCapabilities(node).profile === role.identifier)
- .merge(untaggedNodes))
-);
-
-/**
- * Returns sum of untagged assigned Nodes counts across all Roles
- */
-export const getTotalUntaggedAssignedNodesCount = createSelector(
- [getAvailableNodes, getRoles, getNodeCountParametersByRole],
- (nodes, roles, parametersByRole) =>
- roles.reduce((total, role) => {
- const taggedCount =
- nodes.filter(node => _getNodeCapabilities(node).profile === role.identifier).size;
- const assignedCount = parametersByRole.getIn([role.identifier, 'default'], 0);
- const remainder = Math.max(0, assignedCount - taggedCount);
- return total + remainder;
- }, 0)
-);
-
-/**
- * Returns maximum Nodes count available to assign by each Role
- */
-export const getAvailableNodesCountsByRole = createSelector(
- [getAvailableNodes, getUntaggedAvailableNodes, getRoles, getNodeCountParametersByRole,
- getTotalUntaggedAssignedNodesCount],
- (nodes, untaggedNodes, roles, parametersByRole, untaggedAssignedCount) =>
- roles.map(role => {
- const taggedCount
- = nodes.filter(node => _getNodeCapabilities(node).profile === role.identifier).size;
- const assignedCount = parametersByRole.getIn([role.identifier, 'default'], 0);
- const untaggedCount = untaggedNodes.size;
- return taggedCount
- + Math.max(0, untaggedCount - (untaggedAssignedCount - (assignedCount - taggedCount)));
- })
-);
-
export const getDeployedNodes = createSelector(
getNodesWithMacs, (nodes) =>
nodes.filter( node => node.get('provision_state') === 'active' )
@@ -129,5 +76,5 @@ const filterPorts = (ports) =>
* @param node
* @returns capabilities object
*/
-const _getNodeCapabilities = (node) =>
+export const getNodeCapabilities = (node) =>
parseNodeCapabilities(node.getIn(['properties', 'capabilities'], ''));
diff --git a/src/js/selectors/nodesAssignment.js b/src/js/selectors/nodesAssignment.js
new file mode 100644
index 00000000..aedbd0e1
--- /dev/null
+++ b/src/js/selectors/nodesAssignment.js
@@ -0,0 +1,97 @@
+import { createSelector } from 'reselect';
+import { getFormValues } from 'redux-form';
+
+import { getParameters } from './parameters';
+import { getRegisteredNodes, getNodeCapabilities } from './nodes';
+import { getRoles } from './roles';
+
+export const getAvailableNodes = createSelector(
+ getRegisteredNodes, (nodes) => nodes.filter(node => node.get('provision_state') === 'available')
+);
+
+export const getUntaggedAvailableNodes = createSelector(
+ getAvailableNodes, (availableNodes) =>
+ availableNodes.filterNot(node => getNodeCapabilities(node).profile)
+);
+
+/**
+ * Returns Nodes available for assignment for each Role (tagged to specific role
+ * + untagged nodes)
+ * TODO(jtomasek): remove this when NodesAssignment component is no longer used
+ */
+export const getAvailableNodesByRole = createSelector(
+ [getAvailableNodes, getUntaggedAvailableNodes, getRoles], (nodes, untaggedNodes, roles) =>
+ roles.map(role => nodes.filter(node => getNodeCapabilities(node).profile === role.identifier)
+ .merge(untaggedNodes))
+);
+
+/**
+ * Returns
Count parameters for each Role
+ */
+export const getNodeCountParametersByRole = createSelector(
+ [getRoles, getParameters], (roles, parameters) =>
+ roles.map(role => parameters.get(`${role.name}Count`))
+);
+
+/**
+ * Returns Count parameters for each Role combined with values from
+ * nodesAssignment form
+ */
+export const getNodeCountParametersByRoleFromFormValues = createSelector(
+ [getNodeCountParametersByRole, getFormValues('nodesAssignment')],
+ (parametersByRole, formValues) =>
+ parametersByRole.map(parameter =>
+ parameter &&
+ parameter.set('default', formValues && formValues[parameter.name]
+ ? formValues[parameter.name]
+ : parameter.default))
+);
+
+/**
+ * Returns sum of untagged assigned Nodes counts across all Roles
+ */
+export const getTotalUntaggedAssignedNodesCount = createSelector(
+ [getAvailableNodes, getRoles, getNodeCountParametersByRoleFromFormValues],
+ (nodes, roles, parametersByRole) =>
+ roles.reduce((total, role) => {
+ const taggedCount =
+ nodes.filter(node => getNodeCapabilities(node).profile === role.identifier).size;
+ const assignedCount = parametersByRole.getIn([role.identifier, 'default'], 0);
+ const remainder = Math.max(0, assignedCount - taggedCount);
+ return total + remainder;
+ }, 0)
+);
+
+/**
+ * Returns maximum Nodes count available to assign by each Role
+ */
+export const getAvailableNodesCountsByRole = createSelector(
+ [getAvailableNodes, getUntaggedAvailableNodes, getRoles,
+ getNodeCountParametersByRoleFromFormValues, getTotalUntaggedAssignedNodesCount],
+ (nodes, untaggedNodes, roles, parametersByRole, untaggedAssignedCount) =>
+ roles.map(role => {
+ const taggedCount
+ = nodes.filter(node => getNodeCapabilities(node).profile === role.identifier).size;
+ const assignedCount = parametersByRole.getIn([role.identifier, 'default'], 0);
+ const untaggedCount = untaggedNodes.size;
+ return taggedCount
+ + Math.max(0, untaggedCount - untaggedAssignedCount
+ + Math.max(0, assignedCount - taggedCount));
+ })
+);
+
+/**
+ * Returns 'default' value of Count parameter for each Role
+ * { ControllerCount: 1, ComputeCount: 0, ... }
+ */
+export const getAssignedNodesCountsByRole = createSelector(
+ getNodeCountParametersByRole, nodeCountParametersByRole =>
+ nodeCountParametersByRole.mapEntries(([role, parameter]) =>
+ parameter ? [parameter.name, parameter.default] : undefined)
+);
+
+export const getTotalAssignedNodesCount = createSelector(
+ getNodeCountParametersByRole, countParamsByRole =>
+ countParamsByRole.reduce((total, parameter) =>
+ parameter ? total + parseInt(parameter.default) : total, 0)
+);
diff --git a/src/js/selectors/parameters.js b/src/js/selectors/parameters.js
index 580ec604..ea42a3b8 100644
--- a/src/js/selectors/parameters.js
+++ b/src/js/selectors/parameters.js
@@ -2,7 +2,7 @@ import { createSelector } from 'reselect';
import { List, Set } from 'immutable';
import { internalParameters } from '../constants/ParametersConstants';
-import { getRole, getRoles } from './roles';
+import { getRole } from './roles';
import { getEnvironment } from './environmentConfiguration';
import { Resource } from '../immutableRecords/parameters';
@@ -111,20 +111,6 @@ export const getResourceParameters = createSelector(
resource.parameters.update(filterParameters(parameters))
);
-/**
- * Returns Count parameters for each Role
- */
-export const getNodeCountParametersByRole = createSelector(
- [getRoles, getParameters], (roles, parameters) =>
- roles.map(role => parameters.get(`${role.name}Count`))
-);
-
-export const getTotalAssignedNodesCount = createSelector(
- getNodeCountParametersByRole, countParamsByRole =>
- countParamsByRole.reduce((total, parameter) =>
- parameter ? total + parseInt(parameter.default) : total, 0)
-);
-
/**
* Helper function to convert list of resource uuids into map of actual resources
* @param resources - Map of resources to filter on
diff --git a/src/less/base.less b/src/less/base.less
index c805296a..6a46e24b 100644
--- a/src/less/base.less
+++ b/src/less/base.less
@@ -95,9 +95,9 @@
@import "components/EnvironmentConfiguration";
@import "components/DeploymentDetail";
@import "components/DeploymentPlan";
-@import "components/NodeStack";
-@import "components/NodePicker";
+@import "components/NodePickerInput";
@import "components/Plans";
+@import "components/RoleCard";
@import "components/Validations";
.wrapper-fixed-body {
diff --git a/src/less/components/NodePicker.less b/src/less/components/NodePicker.less
deleted file mode 100644
index db164a95..00000000
--- a/src/less/components/NodePicker.less
+++ /dev/null
@@ -1,17 +0,0 @@
-// NodePicker component
-.node-picker {
- display: inline-block;
- button.picker-arrow {
- border: none;
- background: transparent;
- outline: none;
- text-shadow: 1px 1px 2px gray;
- &:hover {
- cursor: pointer;
- }
- &:active {
- text-shadow: none;
- color: lighten(gray, 1%);
- }
- }
-}
diff --git a/src/less/components/NodePickerInput.less b/src/less/components/NodePickerInput.less
new file mode 100644
index 00000000..ea795676
--- /dev/null
+++ b/src/less/components/NodePickerInput.less
@@ -0,0 +1,30 @@
+// NodePicker component
+.node-picker {
+ .btn-node-picker {
+ width: 100%;
+ padding: 0;
+ display: block;
+ line-height: 1;
+ &.btn-node-picker-up {
+ border-radius: 3px 3px 0 0;
+ border-bottom: none;
+ }
+ &.btn-node-picker-down {
+ border-radius: 0 0 3px 3px;
+ border-top: none;
+ }
+ }
+}
+
+.node-count {
+ min-width: 45px;
+ text-align: center;
+ border: 1px solid @btn-default-border;
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.2) inset;
+ background: @color-pf-black-200;
+ padding: 0 12px;
+ span.value {
+ font-size: 26px;
+ font-weight: 300;
+ }
+}
diff --git a/src/less/components/NodeStack.less b/src/less/components/NodeStack.less
deleted file mode 100644
index 56567467..00000000
--- a/src/less/components/NodeStack.less
+++ /dev/null
@@ -1,43 +0,0 @@
-// NodeStack component
-.node-stack {
- // this element is required to achieve correct stack effect
- // http://stackoverflow.com/questions/3032856/z-index-of-before-or-after-to-be-below-the-element-is-that-possible
- display: inline-block;
- position: relative;
- z-index: 1;
- .stack {
- @node-size: 40px;
- position: relative;
- display: inline-block;
- width: @node-size;
- height: @node-size;
- line-height: @node-size;
- text-align: center;
- border: 1px solid gray;
- background: white;
- margin: 0 18px;
- &:before, &:after {
- display: none;
- content: "";
- position: absolute;
- width: 100%;
- height: 100%;
- border: inherit;
- background: inherit;
- }
- &:before {
- left: -4px;
- top: -4px;
- z-index: -1;
- }
- &:after {
- left: -7px;
- top: -7px;
- z-index: -2;
- }
- &.single-stack:before { display: block; }
- &.double-stack {
- &:before, &:after { display: block; }
- }
- }
-}
diff --git a/src/less/components/RoleCard.less b/src/less/components/RoleCard.less
new file mode 100644
index 00000000..098dee7f
--- /dev/null
+++ b/src/less/components/RoleCard.less
@@ -0,0 +1,12 @@
+.role-card {
+ .node-picker-cell {
+ display: table-cell;
+ vertical-align: middle;
+ width: 1px;
+ padding: 0 12px 0 0;
+ }
+
+ .card-pf-utilization-card-details-description {
+ float: none;
+ }
+}