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 ( +
+ + {children} + + ); + } +} +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; + } +}