Submit select roles

* Change FormErrorList to render standard alert
* Add SelectRolesForm validation
* Add actions for selecting roles and handling zaqar message

Implements: blueprint tripleo-ui-select-roles
Depends-On: I42d66da37dcb07c1be112623d89769377bb23284
Change-Id: I1305b5cb1522ca18f03c4c5113b822a86aff5d97
This commit is contained in:
Jiri Tomasek 2017-11-16 16:19:39 +01:00
parent e1adf4c78e
commit 5cde2a4f79
11 changed files with 116 additions and 49 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
Custom deployment roles selection. Roles section of Deployment plan page now
includes 'Manage Roles'. Clicking the link provides a list of all available
roles and lets user select roles intended for deployment.

View File

@ -95,6 +95,12 @@ export default {
dispatch(RolesActions.fetchAvailableRolesFinished(payload, history));
break;
}
// TODO(jtomasek): change this back once underlining tripleo-common patch is fixed
case MistralConstants.SELECT_ROLES: {
// case 'tripleo.roles.v1.select_roles': {
dispatch(RolesActions.selectRolesFinished(payload, history));
break;
}
default:
break;

View File

@ -26,7 +26,7 @@ const AvailableRoleInput = ({
input: { value, name, onChange },
style
}) => (
<Col xs={6} sm={4} md={3} lg={2} style={style}>
<Col xs={12} sm={4} lg={3} style={style}>
<div
className={cx(
'card-pf card-pf-view card-pf-view-select card-pf-view-multi-select',

View File

@ -20,6 +20,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ModalHeader, ModalTitle } from 'react-bootstrap';
import React from 'react';
import { pickBy } from 'lodash';
import PropTypes from 'prop-types';
import AvailableRoleInput from './AvailableRoleInput';
@ -46,10 +47,16 @@ class SelectRolesDialog extends React.Component {
this.props.fetchAvailableRoles();
}
handleFormSubmit = (values, dispatch, formProps) => {
const roleNames = Object.keys(pickBy(values));
this.props.selectRoles(this.props.currentPlanName, roleNames);
};
render() {
const {
availableRoles,
availableRolesLoaded,
fetchingAvailableRoles,
intl: { formatMessage },
currentPlanName,
roles
@ -62,28 +69,31 @@ class SelectRolesDialog extends React.Component {
<FormattedMessage {...messages.selectRolesTitle} />
</ModalTitle>
</ModalHeader>
<SelectRolesForm
initialValues={availableRoles.map(r => roles.includes(r)).toJS()}
currentPlanName={currentPlanName}
<Loader
height={100}
loaded={availableRolesLoaded && !fetchingAvailableRoles}
content={formatMessage(messages.loadingAvailableRoles)}
>
<Loader
loaded={availableRolesLoaded}
content={formatMessage(messages.loadingAvailableRoles)}
<SelectRolesForm
onSubmit={this.handleFormSubmit}
initialValues={availableRoles
.map((_, key) => roles.keySeq().includes(key))
.toJS()}
availableRoles={availableRoles}
currentPlanName={currentPlanName}
>
<div className="row row-cards-pf">
{availableRoles
.toList()
.map(role => (
<Field
component={AvailableRoleInput}
role={role}
name={role.name}
key={role.name}
/>
))}
</div>
</Loader>
</SelectRolesForm>
{availableRoles
.toList()
.map(role => (
<Field
component={AvailableRoleInput}
role={role}
name={role.name}
key={role.name}
/>
))}
</SelectRolesForm>
</Loader>
</RoutedModal>
);
}
@ -93,20 +103,25 @@ SelectRolesDialog.propTypes = {
availableRolesLoaded: PropTypes.bool.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchAvailableRoles: PropTypes.func.isRequired,
fetchingAvailableRoles: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
roles: ImmutablePropTypes.map.isRequired
roles: ImmutablePropTypes.map.isRequired,
selectRoles: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
availableRoles: getMergedRoles(state),
availableRolesLoaded: state.availableRoles.loaded,
fetchingAvailableRoles: state.availableRoles.isFetching,
currentPlanName: getCurrentPlanName(state),
roles: getRoles(state)
});
const mapDispatchToProps = dispatch => ({
fetchAvailableRoles: planName =>
dispatch(RolesActions.fetchAvailableRoles(planName))
dispatch(RolesActions.fetchAvailableRoles(planName)),
selectRoles: (planName, roleNames) =>
dispatch(RolesActions.selectRoles(planName, roleNames))
});
export default injectIntl(

View File

@ -16,37 +16,61 @@
import { Button } from 'react-bootstrap';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { ModalBody, ModalFooter } from 'react-bootstrap';
import { ModalFooter } from 'react-bootstrap';
import { pickBy } from 'lodash';
import { OverlayLoader } from '../ui/Loader';
import PropTypes from 'prop-types';
import React from 'react';
import { reduxForm } from 'redux-form';
import { CloseModalButton } from '../ui/Modals';
import FormErrorList from '../ui/forms/FormErrorList';
import ModalFormErrorList from '../ui/forms/ModalFormErrorList';
const messages = defineMessages({
saveChanges: {
id: 'SelectRolesDialog.saveChanges',
id: 'SelectRolesForm.saveChanges',
defaultMessage: 'Save Changes'
},
cancel: {
id: 'SelectRolesDialog.cancel',
id: 'SelectRolesForm.cancel',
defaultMessage: 'Cancel'
},
updatingRoles: {
id: 'SelectRolesForm.updatingRoles',
defaultMessage: 'Updating Roles...'
},
primaryRoleValidationError: {
id: 'SelectRolesForm.primaryRoleValidationError',
defaultMessage:
'Please select one role tagged as "primary" and "controller"'
}
});
class SelectRolesForm extends React.Component {
render() {
const { children, error, handleSubmit, invalid, pristine } = this.props;
const {
children,
error,
handleSubmit,
invalid,
intl: { formatMessage },
pristine,
submitting
} = this.props;
return (
<form onSubmit={handleSubmit}>
<ModalBody>
<FormErrorList errors={error ? [error] : []} />
{children}
</ModalBody>
<OverlayLoader
loaded={!submitting}
content={formatMessage(messages.updatingRoles)}
>
<ModalFormErrorList errors={error ? [error] : []} />
<div className="cards-pf">
<div className="row row-cards-pf">{children}</div>
</div>
</OverlayLoader>
<ModalFooter>
<Button
disabled={invalid || pristine}
disabled={invalid || pristine || submitting}
bsStyle="primary"
type="submit"
>
@ -65,14 +89,29 @@ SelectRolesForm.propTypes = {
currentPlanName: PropTypes.string.isRequired,
error: PropTypes.object,
handleSubmit: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
invalid: PropTypes.bool.isRequired,
pristine: PropTypes.bool.isRequired
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired
};
const validateForm = (values, { availableRoles }) => {
const errors = {};
const selectedRoleNames = Object.keys(pickBy(values));
const selectedRoles = availableRoles.filter((r, k) =>
selectedRoleNames.includes(k)
);
if (!selectedRoles.some(r => r.tags.includes('primary'))) {
errors._error = {
message: <FormattedMessage {...messages.primaryRoleValidationError} />
};
}
return errors;
};
const form = reduxForm({
enableReinitialize: true,
form: 'selectRoles',
keepDirtyOnReinitialize: true
validate: validateForm
});
export default injectIntl(form(SelectRolesForm));

View File

@ -38,8 +38,7 @@ export default class FormErrorList extends React.Component {
} else {
return (
<p>
<strong>{errors[0].title}</strong>
<br />
{errors[0].title && <strong>{errors[0].title}</strong>}{' '}
{errors[0].message}
</p>
);
@ -51,10 +50,7 @@ export default class FormErrorList extends React.Component {
return null;
} else {
return (
<div
className="toast-pf form-error-list alert alert-danger"
role="alert"
>
<div className="form-error-list alert alert-danger" role="alert">
<span className="pficon pficon-error-circle-o" aria-hidden="true" />
{this.renderErrors()}
</div>

View File

@ -35,6 +35,7 @@ export default {
PLAN_EXPORT: 'tripleo.plan_management.v1.export_deployment_plan',
ROLE_LIST: 'tripleo.role.list',
LIST_AVAILABLE_ROLES: 'tripleo.plan_management.v1.list_available_roles',
SELECT_ROLES: 'tripleo.plan_management.v1.select_roles',
VALIDATIONS_LIST: 'tripleo.validations.list_validations',
VALIDATIONS_RUN: 'tripleo.validations.v1.run_validation',
VALIDATIONS_RUN_GROUPS: 'tripleo.validations.v1.run_groups'

View File

@ -22,5 +22,6 @@ export default keyMirror({
FETCH_AVAILABLE_ROLES_FAILED: null,
FETCH_ROLES_PENDING: null,
FETCH_ROLES_SUCCESS: null,
FETCH_ROLES_FAILED: null
FETCH_ROLES_FAILED: null,
SELECT_ROLES_SUCCESS: null
});

View File

@ -60,6 +60,7 @@ const rolesReducer = (state = initialState, action) => {
case RolesConstants.FETCH_ROLES_PENDING:
return state.set('isFetching', true);
case RolesConstants.SELECT_ROLES_SUCCESS:
case RolesConstants.FETCH_ROLES_SUCCESS: {
const roles = fromJS(action.payload).map(role =>
new Role(role).update(role =>
@ -79,6 +80,9 @@ const rolesReducer = (state = initialState, action) => {
.set('isFetching', false)
.set('loaded', true);
case RolesConstants.SELECT_ROLES_SUCCESS:
return state.set('roles', roles);
case PlansConstants.PLAN_CHOSEN:
return state.set('loaded', false);

View File

@ -30,6 +30,6 @@ form.form, form.form-horizontal {
}
}
.form-error-list.toast-pf {
display: block;
.form-error-list {
margin-top: 20px;
}

View File

@ -49,7 +49,6 @@
border-bottom: 1px solid @modal-header-border-color ;
}
.modal-footer {
margin-top: 0px;
background-color: #f8f8f8;
@ -59,7 +58,7 @@
.modal-form-error-list {
&:extend(.modal-header, .modal .modal-header);
.toast-pf.form-error-list {
margin-bottom: 0;
.form-error-list {
margin: 0;
}
}