Introduce nodes assignment using NodePicker component
This change finishes the ongoing effor to separate Nodes tagging and Nodes assignment into separate proceses * Introduces NodePicker component to allow user to set number of Nodes to deploy as a specific Role * Refactors Nodes assignment related selectors into separate module * Adds selectors which take nodesAssignment form values into account to calculate available Nodes realtime * Uses huge amount of redux-form functionality to achieve expected behavior * Nodes Assignment form is submitted each time an input value changes without re-rendering all form inputs * API call which updates the Node count parameters is debounced, so it is not issued too many times when user clicks fast to increase Nodes count * Adds custom styling for NodePicker component * Updates UpdateParameters action to support nodesAssignment form Closes-Bug: #1640103 Change-Id: If7701190954d54b75421e5c1be6a8382b9f5c746
This commit is contained in:
parent
ad6314526b
commit
bd07eb24c0
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
||||
<Form onSubmit={handleSubmit(this.debouncedUpdate.bind(this))}>
|
||||
<FormErrorList errors={error ? [error] : []}/>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
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));
|
|
@ -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 (
|
||||
<div className={`card-pf card-pf-accented role-card ${identifier}`}>
|
||||
<h2 className="card-pf-title">
|
||||
|
@ -38,10 +52,22 @@ const RoleCard = ({ assignedNodesCountParameter,
|
|||
</Link>
|
||||
</h2>
|
||||
<div className="card-pf-body">
|
||||
<p className="card-pf-utilization-details">
|
||||
<span className="card-pf-utilization-card-details-count">
|
||||
{assignedNodesCountParameter ? assignedNodesCountParameter.default : '-'}
|
||||
</span>
|
||||
<div className="card-pf-utilization-details">
|
||||
<div className="node-picker-cell">
|
||||
{assignedNodesCountParameter
|
||||
? <Field
|
||||
component={NodePickerInput}
|
||||
increment={1}
|
||||
validate={validations}
|
||||
name={assignedNodesCountParameter.name}
|
||||
max={availableNodesCount}/>
|
||||
: <NodePickerInput
|
||||
increment={1}
|
||||
input={{ value: '-' }}
|
||||
meta={{ submitting: true }}
|
||||
max={availableNodesCount}
|
||||
min={0}/>}
|
||||
</div>
|
||||
<span className="card-pf-utilization-card-details-description">
|
||||
<span className="card-pf-utilization-card-details-line-1">
|
||||
<FormattedMessage
|
||||
|
@ -52,7 +78,7 @@ const RoleCard = ({ assignedNodesCountParameter,
|
|||
<FormattedMessage {...messages.nodesAssigned}/>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-pf-footer">
|
||||
<p>
|
||||
|
@ -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);
|
||||
|
|
|
@ -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}>
|
||||
<div className="row-cards-pf">
|
||||
{this.renderRoleCards()}
|
||||
<NodesAssignmentForm>
|
||||
{this.renderRoleCards()}
|
||||
</NodesAssignmentForm>
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<div className="node-picker">
|
||||
<PickerArrow
|
||||
direction="up"
|
||||
onClick={() => onChange(incrementValue(increment))}
|
||||
disabled={submitting || value + increment > max}/>
|
||||
<NodeCount error={error}>
|
||||
<span className="value">{value}</span>
|
||||
</NodeCount>
|
||||
<PickerArrow
|
||||
direction="down"
|
||||
onClick={() => onChange(incrementValue(-increment))}
|
||||
disabled={submitting || value - increment < min}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-default btn-node-picker btn-node-picker-${direction}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}>
|
||||
<span className={`fa fa-angle-${direction}`} aria-hidden="true"/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<div className={classes} title={error}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
NodeCount.propTypes = {
|
||||
children: React.PropTypes.node.isRequired,
|
||||
error: React.PropTypes.string
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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'], ''));
|
||||
|
|
|
@ -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 <RoleName>Count parameters for each Role
|
||||
*/
|
||||
export const getNodeCountParametersByRole = createSelector(
|
||||
[getRoles, getParameters], (roles, parameters) =>
|
||||
roles.map(role => parameters.get(`${role.name}Count`))
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns <RoleName>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 <RoleName>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)
|
||||
);
|
|
@ -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 <RoleName>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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue