From 8ee595b019d5b100bfe73af378e839237b46828f Mon Sep 17 00:00:00 2001 From: Jiri Tomasek Date: Tue, 2 Jan 2018 09:15:09 +0100 Subject: [PATCH] Fix EnvironmentConfiguration dialog * convert Environment Configuration form to redux-form * add required environments validation * add HorizontalCheckBox and EnvironmentCheckBox * Fix the bug where error is not visible when it is happening in other topic tab by adding general error message * actions and reducer update Partial-Bug: 1743722 Change-Id: Ia056621a847b56913b1dfff57f6e8237f62bd8d7 --- ...onfiguration-updates-1a8c2f64b5252751.yaml | 6 + .../EnvironmentConfigurationActions.tests.js | 13 +- .../EnvironmentConfigurationActions.js | 44 +-- .../EnvironmentCheckBox.js | 76 +++++ .../EnvironmentConfiguration.js | 319 ++++++------------ .../EnvironmentConfigurationForm.js | 164 +++++++++ .../EnvironmentConfigurationSidebar.js | 51 +++ .../EnvironmentConfigurationTopic.js | 51 ++- .../EnvironmentGroup.js | 159 +++------ .../EnvironmentGroupHeading.js | 40 +++ src/js/components/ui/forms/GenericCheckBox.js | 8 +- src/js/components/ui/forms/GroupedCheckBox.js | 8 +- .../ui/reduxForm/HorizontalCheckBox.js | 65 ++++ .../ui/reduxForm/HorizontalInput.js | 2 +- .../ui/reduxForm/HorizontalSelect.js | 2 +- .../ui/reduxForm/HorizontalTextarea.js | 2 +- src/js/components/ui/reduxForm/utils.js | 3 +- .../environmentConfigurationReducer.js | 11 +- 18 files changed, 603 insertions(+), 421 deletions(-) create mode 100644 releasenotes/notes/environment-configuration-updates-1a8c2f64b5252751.yaml create mode 100644 src/js/components/environment_configuration/EnvironmentCheckBox.js create mode 100644 src/js/components/environment_configuration/EnvironmentConfigurationForm.js create mode 100644 src/js/components/environment_configuration/EnvironmentConfigurationSidebar.js create mode 100644 src/js/components/environment_configuration/EnvironmentGroupHeading.js create mode 100644 src/js/components/ui/reduxForm/HorizontalCheckBox.js diff --git a/releasenotes/notes/environment-configuration-updates-1a8c2f64b5252751.yaml b/releasenotes/notes/environment-configuration-updates-1a8c2f64b5252751.yaml new file mode 100644 index 00000000..95f94564 --- /dev/null +++ b/releasenotes/notes/environment-configuration-updates-1a8c2f64b5252751.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Improvements in Deployment configuration -> Overal Settings, Improved + performance, form now shows general error in case when some sub section + contains error \ No newline at end of file diff --git a/src/__tests__/actions/EnvironmentConfigurationActions.tests.js b/src/__tests__/actions/EnvironmentConfigurationActions.tests.js index 26e5813b..09ebbaaf 100644 --- a/src/__tests__/actions/EnvironmentConfigurationActions.tests.js +++ b/src/__tests__/actions/EnvironmentConfigurationActions.tests.js @@ -15,6 +15,7 @@ */ import configureMockStore from 'redux-mock-store'; +import { startSubmit, stopSubmit } from 'redux-form'; import thunkMiddleware from 'redux-thunk'; import EnvironmentConfigurationActions from '../../js/actions/EnvironmentConfigurationActions'; @@ -128,7 +129,7 @@ describe('updateEnvironmentConfiguration', () => { .then(() => { expect(MistralApiService.runAction).toHaveBeenCalled(); expect(store.getActions()).toEqual([ - EnvironmentConfigurationActions.updateEnvironmentConfigurationPending(), + startSubmit('environmentConfigurationForm'), EnvironmentConfigurationActions.updateEnvironmentConfigurationSuccess( [ 'overcloud-resource-registry-puppet.yaml', @@ -136,17 +137,9 @@ describe('updateEnvironmentConfiguration', () => { 'environments/network-isolation.yaml' ] ), + stopSubmit('environmentConfigurationForm'), NotificationActions.notify({ type: 'NOTIFY' }) ]); }); - - // const result = EnvironmentConfigurationActions.updateEnvironmentConfiguration( - // 'myPlan' - // ); - // const mockDispatch = jest.fn(); - // result(mockDispatch, jest.fn(), mockGetIntl).then(() => { - // console.log(mockDispatch); - // expect(mockDispatch.mock.calls).toEqual(true); - // }); }); }); diff --git a/src/js/actions/EnvironmentConfigurationActions.js b/src/js/actions/EnvironmentConfigurationActions.js index 97a17104..b61d44ee 100644 --- a/src/js/actions/EnvironmentConfigurationActions.js +++ b/src/js/actions/EnvironmentConfigurationActions.js @@ -16,6 +16,7 @@ import { defineMessages } from 'react-intl'; import { normalize } from 'normalizr'; +import { startSubmit, stopSubmit } from 'redux-form'; import yaml from 'js-yaml'; import EnvironmentConfigurationConstants from '../constants/EnvironmentConfigurationConstants'; @@ -35,6 +36,10 @@ const messages = defineMessages({ envConfigUpdatedNotificationTitle: { id: 'EnvironmentConfigurationActions.envConfigUpdatedNotificationTitle', defaultMessage: 'Environment Configuration updated' + }, + configurationNotUpdatedError: { + id: 'EnvironmentConfigurationActions.configurationNotUpdatedError', + defaultMessage: 'Deployment configuration could not be updated' } }); @@ -85,10 +90,10 @@ export default { }; }, - updateEnvironmentConfiguration(planName, data, formFields) { + updateEnvironmentConfiguration(planName, data, redirect) { return (dispatch, getState, { getIntl }) => { const { formatMessage } = getIntl(getState()); - dispatch(this.updateEnvironmentConfigurationPending()); + dispatch(startSubmit('environmentConfigurationForm')); return dispatch( MistralApiService.runAction(MistralConstants.CAPABILITIES_UPDATE, { environments: data, @@ -98,6 +103,7 @@ export default { .then(response => { const enabledEnvs = response.environments.map(env => env.path); dispatch(this.updateEnvironmentConfigurationSuccess(enabledEnvs)); + dispatch(stopSubmit('environmentConfigurationForm')); dispatch( NotificationActions.notify({ title: formatMessage(messages.envConfigUpdatedNotificationTitle), @@ -107,34 +113,21 @@ export default { type: 'success' }) ); + redirect && redirect(); }) .catch(error => { dispatch( - handleErrors( - error, - 'Deployment configuration could not be updated', - false - ) - ); - dispatch( - this.updateEnvironmentConfigurationFailed([ - { - title: 'Configuration could not be updated', + stopSubmit('environmentConfigurationForm', { + _error: { + title: formatMessage(messages.configurationNotUpdatedError), message: error.message } - ]) + }) ); }); }; }, - updateEnvironmentConfigurationPending() { - return { - type: - EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_PENDING - }; - }, - updateEnvironmentConfigurationSuccess(enabledEnvironments) { return { type: @@ -143,17 +136,6 @@ export default { }; }, - updateEnvironmentConfigurationFailed(formErrors = [], formFieldErrors = {}) { - return { - type: - EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_FAILED, - payload: { - formErrors, - formFieldErrors - } - }; - }, - fetchEnvironment(planName, environmentPath) { return dispatch => { dispatch(this.fetchEnvironmentPending(environmentPath)); diff --git a/src/js/components/environment_configuration/EnvironmentCheckBox.js b/src/js/components/environment_configuration/EnvironmentCheckBox.js new file mode 100644 index 00000000..68eab959 --- /dev/null +++ b/src/js/components/environment_configuration/EnvironmentCheckBox.js @@ -0,0 +1,76 @@ +/** + * Copyright 2017 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Checkbox, Col, FormGroup } from 'react-bootstrap'; + +import { + getValidationState, + InputDescription, + InputMessage +} from '../ui/reduxForm/utils'; + +/** + * EnvironmentCheckBox differs from HorizontalCheckBox in being considered as + * always touched + */ +const EnvironmentCheckBox = ({ + id, + label, + labelColumns, + inputColumns, + description, + type, + input, + meta, + required, + ...rest +}) => { + return ( + + + + {label} + + + + + + ); +}; +EnvironmentCheckBox.propTypes = { + description: PropTypes.node, + id: PropTypes.string.isRequired, + input: PropTypes.object.isRequired, + inputColumns: PropTypes.number.isRequired, + label: PropTypes.node, + labelColumns: PropTypes.number.isRequired, + meta: PropTypes.object.isRequired, + required: PropTypes.bool.isRequired, + type: PropTypes.string.isRequired +}; +EnvironmentCheckBox.defaultProps = { + labelColumns: 5, + inputColumns: 7, + required: false, + type: 'text' +}; +export default EnvironmentCheckBox; diff --git a/src/js/components/environment_configuration/EnvironmentConfiguration.js b/src/js/components/environment_configuration/EnvironmentConfiguration.js index 6cbcd717..04e0323e 100644 --- a/src/js/components/environment_configuration/EnvironmentConfiguration.js +++ b/src/js/components/environment_configuration/EnvironmentConfiguration.js @@ -14,40 +14,26 @@ * under the License. */ -import * as _ from 'lodash'; +import { camelCase, mapKeys } from 'lodash'; import { connect } from 'react-redux'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import Formsy, { addValidationRule } from 'formsy-react'; +import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import React from 'react'; -import { CloseModalButton } from '../ui/Modals'; import EnvironmentConfigurationActions from '../../actions/EnvironmentConfigurationActions'; +import EnvironmentConfigurationForm from './EnvironmentConfigurationForm'; +import EnvironmentConfigurationSidebar from './EnvironmentConfigurationSidebar'; import EnvironmentConfigurationTopic from './EnvironmentConfigurationTopic'; import { getCurrentPlanName } from '../../selectors/plans'; -import ModalFormErrorList from '../ui/forms/ModalFormErrorList'; import { getEnvironments, getTopicsTree } from '../../selectors/environmentConfiguration'; import { Loader } from '../ui/Loader'; -import Tab from '../ui/Tab'; import TabPane from '../ui/TabPane'; const messages = defineMessages({ - cancel: { - id: 'EnvironmentConfiguration.cancel', - defaultMessage: 'Cancel' - }, - saveChanges: { - id: 'EnvironmentConfiguration.saveChanges', - defaultMessage: 'Save Changes' - }, - saveAndClose: { - id: 'EnvironmentConfiguration.saveAndClose', - defaultMessage: 'Save And Close' - }, loadingEnvironmentConfiguration: { id: 'EnvironmentConfiguration.loadingEnvironmentConfiguration', defaultMessage: 'Loading Deployment Configuration...' @@ -58,8 +44,6 @@ class EnvironmentConfiguration extends React.Component { constructor() { super(); this.state = { - canSubmit: false, - closeOnSubmit: false, activeTab: undefined }; } @@ -75,165 +59,95 @@ class EnvironmentConfiguration extends React.Component { ); } - componentDidUpdate() { - this.invalidateForm(this.props.formFieldErrors.toJS()); - } - - enableButton() { - this.setState({ canSubmit: true }); - } - - disableButton() { - this.setState({ canSubmit: false }); - } - - invalidateForm(formFieldErrors) { - this.refs.environmentConfigurationForm.updateInputsWithError( - formFieldErrors - ); - } - - /* - * Formsy splits data into objects by '.', file names include '.' - * so we need to convert data back to e.g. { filename.yaml: true, ... } - */ - _convertFormData(formData) { - return _.mapValues( - _.mapKeys(formData, (value, key) => { - return key + '.yaml'; - }), - value => { - return value.yaml; - } - ); - } - - handleSubmit(formData, resetForm, invalidateForm) { - const data = this._convertFormData(formData); - this.disableButton(); - this.props.updateEnvironmentConfiguration( - this.props.currentPlanName, - data, - Object.keys(this.refs.environmentConfigurationForm.inputs) - ); - - if (this.state.closeOnSubmit) { - this.setState({ - closeOnSubmit: false - }); - - this.props.history.push(`/plans/${this.props.currentPlanName}`); - } - } - - onSubmitAndClose() { - this.setState( - { - closeOnSubmit: true - }, - this.refs.environmentConfigurationForm.submit - ); - } - - activateTab(tabName, e) { - e.preventDefault(); - this.setState({ activeTab: tabName }); - } - - isTabActive(tabName) { - let firstTabName = _.camelCase( + isTabActive = tabName => { + const firstTabName = camelCase( this.props.environmentConfigurationTopics.first().get('title') ); - let currentTab = this.state.activeTab || firstTabName; + const currentTab = this.state.activeTab || firstTabName; return currentTab === tabName; + }; + + renderTopics = () => { + const { environmentConfigurationTopics } = this.props; + return environmentConfigurationTopics.toList().map((topic, index) => { + const tabName = camelCase(topic.get('title')); + return ( + + + + ); + }); + }; + + handleSubmit = ({ saveAndClose, ...values }, dispatch, props) => { + const data = mapKeys(values, (_, k) => k.replace(':', '.')); + const { + currentPlanName, + history, + updateEnvironmentConfiguration + } = this.props; + if (saveAndClose) { + updateEnvironmentConfiguration(currentPlanName, data, () => + history.push(`/plans/${currentPlanName}`) + ); + } else { + updateEnvironmentConfiguration(currentPlanName, data); + } + }; + + /** + * Initial values are all enabled environments, keys are changed as dots + * in input names cause unwanted splitting into nested objects + */ + getFormInitialValues() { + return this.props.allEnvironments + .mapKeys(k => k.replace('.', ':')) + .filter(e => e.enabled) + .map(e => e.enabled) + .toJS(); } render() { - let topics = this.props.environmentConfigurationTopics - .toList() - .map((topic, index) => { - let tabName = _.camelCase(topic.get('title')); - return ( - - - - ); - }); - - let topicTabs = this.props.environmentConfigurationTopics - .toList() - .map((topic, index) => { - let tabName = _.camelCase(topic.get('title')); - return ( - - - {topic.get('title')} - - - ); - }); + const { + allEnvironments, + environmentConfigurationTopics, + isFetching, + intl: { formatMessage } + } = this.props; return ( - - -
-
-
    - {topicTabs} -
-
+ this.setState({ activeTab: tabName })} + categories={environmentConfigurationTopics.toList().toJS()} + isTabActive={this.isTabActive} + />
-
{topics}
+
{this.renderTopics()}
-
- -
- - - - - -
-
+ + ); } } @@ -252,60 +166,35 @@ EnvironmentConfiguration.propTypes = { updateEnvironmentConfiguration: PropTypes.func }; -function mapStateToProps(state) { - return { - currentPlanName: getCurrentPlanName(state), - allEnvironments: getEnvironments(state), - environmentConfigurationTopics: getTopicsTree(state), - formErrors: state.environmentConfiguration.getIn(['form', 'formErrors']), - formFieldErrors: state.environmentConfiguration.getIn([ - 'form', - 'formFieldErrors' - ]), - isFetching: state.environmentConfiguration.isFetching - }; -} +const mapStateToProps = state => ({ + currentPlanName: getCurrentPlanName(state), + allEnvironments: getEnvironments(state), + environmentConfigurationTopics: getTopicsTree(state), + formErrors: state.environmentConfiguration.getIn(['form', 'formErrors']), + formFieldErrors: state.environmentConfiguration.getIn([ + 'form', + 'formFieldErrors' + ]), + isFetching: state.environmentConfiguration.isFetching +}); -function mapDispatchToProps(dispatch) { - return { - fetchEnvironmentConfiguration: planName => { - dispatch( - EnvironmentConfigurationActions.fetchEnvironmentConfiguration(planName) - ); - }, - updateEnvironmentConfiguration: (planName, data, inputFields) => { - dispatch( - EnvironmentConfigurationActions.updateEnvironmentConfiguration( - planName, - data, - inputFields - ) - ); - } - }; -} +const mapDispatchToProps = dispatch => ({ + fetchEnvironmentConfiguration: planName => { + dispatch( + EnvironmentConfigurationActions.fetchEnvironmentConfiguration(planName) + ); + }, + updateEnvironmentConfiguration: (planName, data, inputFields) => { + dispatch( + EnvironmentConfigurationActions.updateEnvironmentConfiguration( + planName, + data, + inputFields + ) + ); + } +}); export default injectIntl( connect(mapStateToProps, mapDispatchToProps)(EnvironmentConfiguration) ); - -/** - * requiresEnvironments validation - * Invalidates input if it is selected and environment it requires is not. - * example: validations="requiredEnvironments:['some_environment.yaml']" - */ -addValidationRule('requiredEnvironments', function( - values, - value, - requiredEnvironmentFieldNames -) { - if (value) { - return !_.filter( - _.values(_.pick(values, requiredEnvironmentFieldNames)), - function(val) { - return val === false; - } - ).length; - } - return true; -}); diff --git a/src/js/components/environment_configuration/EnvironmentConfigurationForm.js b/src/js/components/environment_configuration/EnvironmentConfigurationForm.js new file mode 100644 index 00000000..04ac7c0e --- /dev/null +++ b/src/js/components/environment_configuration/EnvironmentConfigurationForm.js @@ -0,0 +1,164 @@ +/** + * Copyright 2018 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { Button, Form, ModalFooter } from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { reduxForm } from 'redux-form'; +import { pickBy } from 'lodash'; + +import { CloseModalButton } from '../ui/Modals'; +import ModalFormErrorList from '../ui/forms/ModalFormErrorList'; +import { OverlayLoader } from '../ui/Loader'; + +const messages = defineMessages({ + cancel: { + id: 'EnvironmentConfigurationForm.cancel', + defaultMessage: 'Cancel' + }, + saveChanges: { + id: 'EnvironmentConfigurationForm.saveChanges', + defaultMessage: 'Save Changes' + }, + saveAndClose: { + id: 'EnvironmentConfigurationForm.saveAndClose', + defaultMessage: 'Save And Close' + }, + requiredEnvironments: { + id: 'EnvironmentConfigurationForm.requiredEnvironments', + defaultMessage: 'This option requires {requiredEnvironments} to be enabled.' + }, + missingConfiguration: { + id: 'EnvironmentConfigurationForm.missingConfiguration', + defaultMessage: 'Missing configuration', + description: + 'Title for general error message describing dependent environments need to be enabled' + }, + requiredEnvironmentsGlobalError: { + id: 'EnvironmentConfigurationForm.requiredEnvironmentGlobalError', + defaultMessage: + 'Selected options depend on other options which are not enabled', + description: + 'General error message describing dependent environments need to be enabled' + }, + updatingEnvironmentConfiguration: { + id: 'EnvironmentConfigurationForm.updatingEnvironmentConfiguration', + defaultMessage: 'Updating Environment configuration' + } +}); + +const EnvironmentConfigurationForm = ({ + error, + children, + onSubmit, + handleSubmit, + intl: { formatMessage }, + invalid, + pristine, + submitting, + initialValues +}) => ( +
+ + + {children} + + + + + + + + +
+); +EnvironmentConfigurationForm.propTypes = { + children: PropTypes.node, + error: PropTypes.object, + handleSubmit: PropTypes.func.isRequired, + initialValues: PropTypes.object.isRequired, + intl: PropTypes.object.isRequired, + invalid: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired +}; + +const validate = (values, { allEnvironments, intl: { formatMessage } }) => { + const errors = {}; + + // Get array of environment files currently enabled in the form + const enabledValues = Object.keys(pickBy(values)).map(v => + v.replace(':', '.') + ); + + // For each enabled environment, get its list of required environments. Add + // error if some of them are not enabled + enabledValues.map(e => { + const requires = allEnvironments.getIn([e, 'requires']); + + if ( + !requires + .toSet() + .subtract(enabledValues) + .isEmpty() + ) { + const requiredEnvironmentNames = requires + .map(env => allEnvironments.getIn([env, 'title'], env)) + .toArray(); + + errors[e.replace('.', ':')] = formatMessage( + messages.requiredEnvironments, + { + requiredEnvironments: requiredEnvironmentNames + } + ); + } + }); + + // Add global error message + if (Object.keys(errors).length > 0) { + errors['_error'] = { + title: formatMessage(messages.missingConfiguration), + message: formatMessage(messages.requiredEnvironmentsGlobalError) + }; + } + return errors; +}; + +const form = reduxForm({ + form: 'environmentConfigurationForm', + validate +}); + +export default injectIntl(form(EnvironmentConfigurationForm)); diff --git a/src/js/components/environment_configuration/EnvironmentConfigurationSidebar.js b/src/js/components/environment_configuration/EnvironmentConfigurationSidebar.js new file mode 100644 index 00000000..8b5d56b9 --- /dev/null +++ b/src/js/components/environment_configuration/EnvironmentConfigurationSidebar.js @@ -0,0 +1,51 @@ +/** + * Copyright 2018 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { camelCase } from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Tab from '../ui/Tab'; + +const EnvironmentConfigurationSidebar = ({ + activateTab, + categories, + isTabActive +}) => ( +
+ +
+); +EnvironmentConfigurationSidebar.propTypes = { + activateTab: PropTypes.func.isRequired, + categories: PropTypes.array.isRequired, + isTabActive: PropTypes.func.isRequired +}; +export default EnvironmentConfigurationSidebar; diff --git a/src/js/components/environment_configuration/EnvironmentConfigurationTopic.js b/src/js/components/environment_configuration/EnvironmentConfigurationTopic.js index 75d23275..14e5096a 100644 --- a/src/js/components/environment_configuration/EnvironmentConfigurationTopic.js +++ b/src/js/components/environment_configuration/EnvironmentConfigurationTopic.js @@ -20,39 +20,30 @@ import React from 'react'; import EnvironmentGroup from './EnvironmentGroup'; -export default class EnvironmentConfigurationTopic extends React.Component { - render() { - let environmentGroups = this.props.environmentGroups +const EnvironmentConfigurationTopic = ({ description, environmentGroups }) => ( +
+ {description && ( +

+ {description} +

+ )} + {environmentGroups .toList() - .map((envGroup, index) => { - return ( - - ); - }); - - const { description } = this.props; - return ( -
- {description && ( -

- {description} -

- )} - {environmentGroups} -
- ); - } -} + .map((envGroup, index) => ( + + ))} +
+); EnvironmentConfigurationTopic.propTypes = { - allEnvironments: ImmutablePropTypes.map.isRequired, description: PropTypes.string, environmentGroups: ImmutablePropTypes.list, title: PropTypes.string.isRequired }; + +export default EnvironmentConfigurationTopic; diff --git a/src/js/components/environment_configuration/EnvironmentGroup.js b/src/js/components/environment_configuration/EnvironmentGroup.js index 468cb8c1..05723f82 100644 --- a/src/js/components/environment_configuration/EnvironmentGroup.js +++ b/src/js/components/environment_configuration/EnvironmentGroup.js @@ -14,117 +14,61 @@ * under the License. */ -import { defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import React from 'react'; +import { change, Field } from 'redux-form'; -import GenericCheckBox from '../ui/forms/GenericCheckBox'; -import GroupedCheckBox from '../ui/forms/GroupedCheckBox'; - -const messages = defineMessages({ - requiredEnvironments: { - id: 'EnvironmentGroup.requiredEnvironments', - defaultMessage: 'This option requires {requiredEnvironments} to be enabled.' - } -}); +import EnvironmentGroupHeading from './EnvironmentGroupHeading'; +import EnvironmentCheckBox from './EnvironmentCheckBox'; class EnvironmentGroup extends React.Component { - constructor(props) { - super(props); - this.state = { - checkedEnvironment: null - }; - } - - componentWillMount() { - const firstCheckedEnvironment = this.props.environments - .filter(env => env.get('enabled') === true) - .first(); - this.setState({ - checkedEnvironment: firstCheckedEnvironment - ? firstCheckedEnvironment.get('file') - : null - }); - } - - onGroupedCheckBoxChange(checked, environmentFile) { - this.setState({ checkedEnvironment: checked ? environmentFile : null }); - } - - getRequiredEnvironmentsNames(environment) { - return environment.requires - .map(env => this.props.allEnvironments.getIn([env, 'title'], env)) - .toArray(); - } - - generateInputs() { - const { - environments, - intl: { formatMessage }, - mutuallyExclusive - } = this.props; - - return environments.toList().map((environment, index) => { - const requiredEnvironments = environment.requires.toArray(); - const requiredEnvironmentNames = this.getRequiredEnvironmentsNames( - environment - ); - if (mutuallyExclusive) { - let checkBoxValue = this.state.checkedEnvironment === environment.file; - return ( - - ); - } else { - return ( - - ); - } - }); - } + /** + * When enabling environment in mutually exclusive group disable other + * environments in the group + */ + handleEnablingEnvironment = (event, newValue, previousValue) => { + const { changeValue, environments } = this.props; + if (newValue) { + environments + .delete(event.target.name.replace(':', '.')) + .map(env => changeValue(env.file.replace('.', ':'), false)); + } + }; render() { - let environments = this.generateInputs(); - + const { title, description, environments, mutuallyExclusive } = this.props; return (
- - {environments} + + {environments + .toList() + .map((environment, index) => ( + + ))}
); } } EnvironmentGroup.propTypes = { - allEnvironments: ImmutablePropTypes.map.isRequired, + changeValue: PropTypes.func.isRequired, description: PropTypes.string, environments: ImmutablePropTypes.map, - intl: PropTypes.object, mutuallyExclusive: PropTypes.bool.isRequired, title: PropTypes.string }; @@ -132,26 +76,9 @@ EnvironmentGroup.defaultProps = { mutuallyExclusive: false }; -export default injectIntl(EnvironmentGroup); +const mapDispatchToProps = dispatch => ({ + changeValue: (field, value) => + dispatch(change('environmentConfigurationForm', field, value)) +}); -class EnvironmentGroupHeading extends React.Component { - render() { - if (this.props.title) { - return ( -

- {this.props.title} -
- {this.props.description} -

- ); - } else if (this.props.description) { - return

{this.props.description}

; - } else { - return false; - } - } -} -EnvironmentGroupHeading.propTypes = { - description: PropTypes.string, - title: PropTypes.string -}; +export default connect(null, mapDispatchToProps)(EnvironmentGroup); diff --git a/src/js/components/environment_configuration/EnvironmentGroupHeading.js b/src/js/components/environment_configuration/EnvironmentGroupHeading.js new file mode 100644 index 00000000..93d85eb5 --- /dev/null +++ b/src/js/components/environment_configuration/EnvironmentGroupHeading.js @@ -0,0 +1,40 @@ +/** + * Copyright 2017 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; + +const EnvironmentGroupHeading = ({ title, description }) => { + if (title) { + return ( +

+ {title} +
+ {description} +

+ ); + } else if (description) { + return

{description}

; + } else { + return false; + } +}; +EnvironmentGroupHeading.propTypes = { + description: PropTypes.string, + title: PropTypes.string +}; + +export default EnvironmentGroupHeading; diff --git a/src/js/components/ui/forms/GenericCheckBox.js b/src/js/components/ui/forms/GenericCheckBox.js index 36b278c9..3c9bd02c 100644 --- a/src/js/components/ui/forms/GenericCheckBox.js +++ b/src/js/components/ui/forms/GenericCheckBox.js @@ -22,7 +22,11 @@ import React from 'react'; import InputDescription from './InputDescription'; import InputErrorMessage from './InputErrorMessage'; -class GenericCheckBox extends React.Component { +class GenericCheckBox extends React.PureComponent { + constructor() { + super(); + this.changeValue = this.changeValue.bind(this); + } changeValue(event) { this.props.setValue(event.target.checked); } @@ -43,7 +47,7 @@ class GenericCheckBox extends React.Component { name={this.props.name} ref={this.props.id} id={this.props.id} - onChange={this.changeValue.bind(this)} + onChange={this.changeValue} checked={!!this.props.getValue()} value={this.props.getValue()} /> diff --git a/src/js/components/ui/forms/GroupedCheckBox.js b/src/js/components/ui/forms/GroupedCheckBox.js index 6e02e25e..0e84d6ed 100644 --- a/src/js/components/ui/forms/GroupedCheckBox.js +++ b/src/js/components/ui/forms/GroupedCheckBox.js @@ -22,7 +22,11 @@ import React from 'react'; import InputDescription from './InputDescription'; import InputErrorMessage from './InputErrorMessage'; -class GroupedCheckBox extends React.Component { +class GroupedCheckBox extends React.PureComponent { + constructor() { + super(); + this.changeValue = this.changeValue.bind(this); + } changeValue(event) { this.props.onChange(event.target.checked, this.props.name); this.props.setValue(event.target.checked); @@ -44,7 +48,7 @@ class GroupedCheckBox extends React.Component { name={this.props.name} ref={this.props.id} id={this.props.id} - onChange={this.changeValue.bind(this)} + onChange={this.changeValue} checked={!!this.props.getValue()} value={this.props.getValue()} /> diff --git a/src/js/components/ui/reduxForm/HorizontalCheckBox.js b/src/js/components/ui/reduxForm/HorizontalCheckBox.js new file mode 100644 index 00000000..0280fc63 --- /dev/null +++ b/src/js/components/ui/reduxForm/HorizontalCheckBox.js @@ -0,0 +1,65 @@ +/** + * Copyright 2017 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Checkbox, Col, FormGroup } from 'react-bootstrap'; + +import { getValidationState, InputDescription, InputMessage } from './utils'; + +const HorizontalCheckBox = ({ + id, + label, + labelColumns, + inputColumns, + description, + type, + input, + meta, + required, + ...rest +}) => { + return ( + + + + {label} + + + + + + ); +}; +HorizontalCheckBox.propTypes = { + description: PropTypes.node, + id: PropTypes.string.isRequired, + input: PropTypes.object.isRequired, + inputColumns: PropTypes.number.isRequired, + label: PropTypes.node, + labelColumns: PropTypes.number.isRequired, + meta: PropTypes.object.isRequired, + required: PropTypes.bool.isRequired, + type: PropTypes.string.isRequired +}; +HorizontalCheckBox.defaultProps = { + labelColumns: 5, + inputColumns: 7, + required: false, + type: 'text' +}; +export default HorizontalCheckBox; diff --git a/src/js/components/ui/reduxForm/HorizontalInput.js b/src/js/components/ui/reduxForm/HorizontalInput.js index 5cae6ed6..ee09a5ad 100644 --- a/src/js/components/ui/reduxForm/HorizontalInput.js +++ b/src/js/components/ui/reduxForm/HorizontalInput.js @@ -44,7 +44,7 @@ const HorizontalInput = ({ - + diff --git a/src/js/components/ui/reduxForm/HorizontalSelect.js b/src/js/components/ui/reduxForm/HorizontalSelect.js index 3b3c1157..5f2869cb 100644 --- a/src/js/components/ui/reduxForm/HorizontalSelect.js +++ b/src/js/components/ui/reduxForm/HorizontalSelect.js @@ -52,7 +52,7 @@ const HorizontalSelect = ({ > {children} - + diff --git a/src/js/components/ui/reduxForm/HorizontalTextarea.js b/src/js/components/ui/reduxForm/HorizontalTextarea.js index 88c9e0ff..4f44d01d 100644 --- a/src/js/components/ui/reduxForm/HorizontalTextarea.js +++ b/src/js/components/ui/reduxForm/HorizontalTextarea.js @@ -43,7 +43,7 @@ const HorizontalTextarea = ({ - + diff --git a/src/js/components/ui/reduxForm/utils.js b/src/js/components/ui/reduxForm/utils.js index b7381d08..ce87b54f 100644 --- a/src/js/components/ui/reduxForm/utils.js +++ b/src/js/components/ui/reduxForm/utils.js @@ -35,12 +35,11 @@ InputDescription.propTypes = { description: PropTypes.node }; -export const InputMessage = ({ fieldMeta: { touched, error, warning } }) => +export const InputMessage = ({ touched, error, warning }) => touched ? (error ? {error} : null) || (warning ? {warning} : null) : null; - InputMessage.propTypes = { error: PropTypes.node, touched: PropTypes.bool, diff --git a/src/js/reducers/environmentConfigurationReducer.js b/src/js/reducers/environmentConfigurationReducer.js index 787ff9c5..cf9d5527 100644 --- a/src/js/reducers/environmentConfigurationReducer.js +++ b/src/js/reducers/environmentConfigurationReducer.js @@ -48,9 +48,6 @@ export default function environmentConfigurationReducer( case EnvironmentConfigurationConstants.FETCH_ENVIRONMENT_CONFIGURATION_FAILED: return state.set('isFetching', false).set('loaded', true); - case EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_PENDING: - return state.set('isFetching', true); - case EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_SUCCESS: { const enabledEnvs = fromJS(action.payload); const updatedEnvs = state.environments.map(environment => { @@ -59,15 +56,9 @@ export default function environmentConfigurationReducer( enabledEnvs.includes(environment.get('file')) ); }); - return state.set('environments', updatedEnvs).set('isFetching', false); + return state.set('environments', updatedEnvs); } - case EnvironmentConfigurationConstants.UPDATE_ENVIRONMENT_CONFIGURATION_FAILED: - return state - .set('isFetching', false) - .set('loaded', true) - .set('form', fromJS(action.payload)); - case EnvironmentConfigurationConstants.FETCH_ENVIRONMENT_PENDING: return state.setIn(['environments', action.payload, 'isFetching'], true);