Convert Parameters form to use redux-form
Using Formsy in large forms causes performance problems * Introduce ParametersForm and ParametersSidebar * Add NewParameterInputList and NewParameterInput as the old ones are still used in role forms. After those are converted too, we'll rename these components * Validate json parameter inputs * Handle submitting of ParametersForm * Separate updateParameters action to 3 actions: updateParameters, updateNodesAssignment, updateRoleParameters as those 3 dialogs all update parameters using the same mistral action but they handle it differently Implements: blueprint ui-redux-form-migration Closes-Bug: 1745408 Change-Id: I527f3587f8adce89be649c487d9a0df5ce4939d5
This commit is contained in:
parent
8ee595b019
commit
0e7d65228a
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Improved perfomance in 'Deployment configuration' -> 'Parameters' section,
|
||||
a general error is shown when sub-section contains validation error
|
||||
fixes:
|
||||
- Fixes `bug 1745408 <https://bugs.launchpad.net/tripleo/+bug/1745408>`__
|
||||
Parameters form does not load for environments with large amount of parameters
|
|
@ -115,20 +115,13 @@ describe('ParametersActions', () => {
|
|||
}
|
||||
);
|
||||
expect(store.getActions()).toEqual([
|
||||
ReduxFormActions.startSubmit('nodesAssignment'),
|
||||
ParametersActions.updateParametersPending(),
|
||||
ReduxFormActions.stopSubmit('nodesAssignment', {
|
||||
ReduxFormActions.startSubmit('parametersForm'),
|
||||
ReduxFormActions.stopSubmit('parametersForm', {
|
||||
_error: {
|
||||
title: 'Parameters could not be updated',
|
||||
message: error.message
|
||||
}
|
||||
}),
|
||||
ParametersActions.updateParametersFailed([
|
||||
{
|
||||
title: 'Parameters could not be updated',
|
||||
message: error.message
|
||||
}
|
||||
])
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,10 @@ const messages = defineMessages({
|
|||
id: 'ParametersActions.parametersUpdatedNotficationTitle',
|
||||
defaultMessage: 'Parameters updated'
|
||||
},
|
||||
updateParametersFailed: {
|
||||
id: 'ParametersActions.updateParametersFailed',
|
||||
defaultMessage: 'Parameters could not be updated'
|
||||
},
|
||||
parametersUpdatedNotficationMessage: {
|
||||
id: 'ParametersActions.parametersUpdatedNotficationMessage',
|
||||
defaultMessage: 'The Deployment parameters have been successfully updated.'
|
||||
|
@ -108,7 +112,45 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
updateParameters(planName, data, inputFieldNames, redirect) {
|
||||
updateRoleParameters(planName, data, inputFieldNames, redirect) {
|
||||
return (dispatch, getState, { getIntl }) => {
|
||||
const { formatMessage } = getIntl(getState());
|
||||
dispatch(this.updateParametersPending());
|
||||
return dispatch(
|
||||
MistralApiService.runAction(MistralConstants.PARAMETERS_UPDATE, {
|
||||
container: planName,
|
||||
parameters: data
|
||||
})
|
||||
)
|
||||
.then(response => {
|
||||
dispatch(this.updateParametersSuccess(data));
|
||||
dispatch(
|
||||
NotificationActions.notify({
|
||||
title: formatMessage(messages.parametersUpdatedNotficationTitle),
|
||||
message: formatMessage(
|
||||
messages.parametersUpdatedNotficationMessage
|
||||
),
|
||||
type: 'success'
|
||||
})
|
||||
);
|
||||
if (redirect) {
|
||||
redirect();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(
|
||||
this.updateParametersFailed([
|
||||
{
|
||||
title: formatMessage(messages.updateParametersFailed),
|
||||
message: error.message
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
updateNodesAssignment(planName, data) {
|
||||
return (dispatch, getState, { getIntl }) => {
|
||||
const { formatMessage } = getIntl(getState());
|
||||
dispatch(startSubmit('nodesAssignment'));
|
||||
|
@ -131,22 +173,19 @@ export default {
|
|||
type: 'success'
|
||||
})
|
||||
);
|
||||
if (redirect) {
|
||||
redirect();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(
|
||||
handleErrors(
|
||||
error,
|
||||
'Deployment parameters could not be updated',
|
||||
formatMessage(messages.updateParametersFailed),
|
||||
false
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
stopSubmit('nodesAssignment', {
|
||||
_error: {
|
||||
title: 'Parameters could not be updated',
|
||||
title: formatMessage(messages.updateParametersFailed),
|
||||
message: error.message
|
||||
}
|
||||
})
|
||||
|
@ -154,12 +193,42 @@ export default {
|
|||
dispatch(
|
||||
this.updateParametersFailed([
|
||||
{
|
||||
title: 'Parameters could not be updated',
|
||||
title: formatMessage(messages.updateParametersFailed),
|
||||
message: error.message
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
updateParameters(planName, data, redirect) {
|
||||
return (dispatch, getState, { getIntl }) => {
|
||||
const { formatMessage } = getIntl(getState());
|
||||
dispatch(startSubmit('parametersForm'));
|
||||
return dispatch(
|
||||
MistralApiService.runAction(MistralConstants.PARAMETERS_UPDATE, {
|
||||
container: planName,
|
||||
parameters: data
|
||||
})
|
||||
)
|
||||
.then(response => {
|
||||
dispatch(this.updateParametersSuccess(data));
|
||||
dispatch(stopSubmit('parametersForm'));
|
||||
if (redirect) {
|
||||
redirect();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(
|
||||
stopSubmit('parametersForm', {
|
||||
_error: {
|
||||
title: formatMessage(messages.updateParametersFailed),
|
||||
message: error.message
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -41,59 +41,50 @@ const messages = defineMessages({
|
|||
}
|
||||
});
|
||||
|
||||
class DeploymentConfiguration extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { show: true };
|
||||
}
|
||||
render() {
|
||||
const { location, match } = this.props;
|
||||
return (
|
||||
<RoutedModal
|
||||
id="DeploymentConfiguration__ModalDialog"
|
||||
bsSize="xl"
|
||||
redirectPath={`/plans/${match.params.planName}`}
|
||||
const DeploymentConfiguration = ({ location, match }) => (
|
||||
<RoutedModal
|
||||
id="DeploymentConfiguration__ModalDialog"
|
||||
bsSize="xl"
|
||||
redirectPath={`/plans/${match.params.planName}`}
|
||||
>
|
||||
<ModalHeader>
|
||||
<CloseModalXButton />
|
||||
<ModalTitle>
|
||||
<FormattedMessage {...messages.deploymentConfiguration} />
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ul className="nav nav-tabs">
|
||||
<NavTab
|
||||
id="DeploymentConfiguration__OverallSettingsTab"
|
||||
to={`${match.url}/environment`}
|
||||
>
|
||||
<ModalHeader>
|
||||
<CloseModalXButton />
|
||||
<ModalTitle>
|
||||
<FormattedMessage {...messages.deploymentConfiguration} />
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<FormattedMessage {...messages.overallSettings} />
|
||||
</NavTab>
|
||||
<NavTab
|
||||
id="DeploymentConfiguration__ParametersTab"
|
||||
to={`${match.url}/parameters`}
|
||||
>
|
||||
<FormattedMessage {...messages.parameters} />
|
||||
</NavTab>
|
||||
</ul>
|
||||
|
||||
<ul className="nav nav-tabs">
|
||||
<NavTab
|
||||
id="DeploymentConfiguration__OverallSettingsTab"
|
||||
to={`${match.url}/environment`}
|
||||
>
|
||||
<FormattedMessage {...messages.overallSettings} />
|
||||
</NavTab>
|
||||
<NavTab
|
||||
id="DeploymentConfiguration__ParametersTab"
|
||||
to={`${match.url}/parameters`}
|
||||
>
|
||||
<FormattedMessage {...messages.parameters} />
|
||||
</NavTab>
|
||||
</ul>
|
||||
|
||||
<Switch location={location}>
|
||||
<Route
|
||||
path="/plans/:planName/configuration/environment"
|
||||
component={EnvironmentConfiguration}
|
||||
/>
|
||||
<Route
|
||||
path="/plans/:planName/configuration/parameters"
|
||||
component={Parameters}
|
||||
/>
|
||||
<Redirect
|
||||
from="/plans/:planName/configuration"
|
||||
to={`${match.url}/environment`}
|
||||
/>
|
||||
</Switch>
|
||||
</RoutedModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Switch location={location}>
|
||||
<Route
|
||||
path="/plans/:planName/configuration/environment"
|
||||
component={EnvironmentConfiguration}
|
||||
/>
|
||||
<Route
|
||||
path="/plans/:planName/configuration/parameters"
|
||||
component={Parameters}
|
||||
/>
|
||||
<Redirect
|
||||
from="/plans/:planName/configuration"
|
||||
to={`${match.url}/environment`}
|
||||
/>
|
||||
</Switch>
|
||||
</RoutedModal>
|
||||
);
|
||||
DeploymentConfiguration.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
|
|
|
@ -25,7 +25,7 @@ import { getEnvironmentParameters } from '../../selectors/parameters';
|
|||
import { getEnvironment } from '../../selectors/environmentConfiguration';
|
||||
import InlineNotification from '../ui/InlineNotification';
|
||||
import { Loader } from '../ui/Loader';
|
||||
import ParameterInputList from './ParameterInputList';
|
||||
import ParameterInputList from './NewParameterInputList';
|
||||
|
||||
class EnvironmentParameters extends React.Component {
|
||||
componentDidMount() {
|
||||
|
@ -41,7 +41,7 @@ class EnvironmentParameters extends React.Component {
|
|||
<Loader
|
||||
height={120}
|
||||
loaded={!isFetchingEnvironment}
|
||||
content="Fetching Parameters..."
|
||||
content="Loading configuration..."
|
||||
>
|
||||
{environmentError ? (
|
||||
<fieldset>
|
||||
|
@ -70,7 +70,6 @@ function mapStateToProps(state, ownProps) {
|
|||
currentPlanName: getCurrentPlanName(state),
|
||||
environmentError: getEnvironment(state, ownProps.environment).error,
|
||||
parameters: getEnvironmentParameters(state, ownProps.environment),
|
||||
parametersLoaded: state.parameters.loaded,
|
||||
isFetchingEnvironment: getEnvironment(state, ownProps.environment)
|
||||
.isFetching
|
||||
};
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 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 { Field } from 'redux-form';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { isObjectLike } from 'lodash';
|
||||
|
||||
import HorizontalInput from '../ui/reduxForm/HorizontalInput';
|
||||
import HorizontalTextarea from '../ui/reduxForm/HorizontalTextarea';
|
||||
import HorizontalCheckBox from '../ui/reduxForm/HorizontalCheckBox';
|
||||
import HorizontalStaticText from '../ui/forms/HorizontalStaticText';
|
||||
|
||||
// TODO(jtomasek): rename this when original Formsy based ParameterInputList and
|
||||
// ParameterInput are not used
|
||||
class NewParameterInput extends React.Component {
|
||||
render() {
|
||||
const { name, label, description, defaultValue, value, type } = this.props;
|
||||
const commonProps = {
|
||||
component: HorizontalInput,
|
||||
name,
|
||||
id: name,
|
||||
label,
|
||||
description,
|
||||
labelColumns: 4,
|
||||
inputColumns: 8
|
||||
};
|
||||
if (value) {
|
||||
return (
|
||||
<HorizontalStaticText
|
||||
text={isObjectLike(value) ? JSON.stringify(value) : value}
|
||||
title={label}
|
||||
labelColumnClasses="col-sm-4"
|
||||
inputColumnClasses="col-sm-8"
|
||||
/>
|
||||
);
|
||||
} else if (type.toLowerCase() === 'commadelimitedlist') {
|
||||
return (
|
||||
<Field
|
||||
{...commonProps}
|
||||
parse={value => value.split(',')}
|
||||
component={HorizontalTextarea}
|
||||
/>
|
||||
);
|
||||
} else if (type.toLowerCase() === 'json' || isObjectLike(defaultValue)) {
|
||||
return <Field {...commonProps} component={HorizontalTextarea} />;
|
||||
} else if (
|
||||
type.toLowerCase() === 'string' &&
|
||||
/^.*(Key|Cert|Certificate)$/.test(name)
|
||||
) {
|
||||
return <Field {...commonProps} component={HorizontalTextarea} />;
|
||||
} else if (type.toLowerCase() === 'number') {
|
||||
return (
|
||||
<Field
|
||||
{...commonProps}
|
||||
type="number"
|
||||
parse={value =>
|
||||
isNaN(parseInt(value)) ? undefined : parseInt(value)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (type.toLowerCase() === 'boolean') {
|
||||
return (
|
||||
<Field
|
||||
{...commonProps}
|
||||
type="checkbox"
|
||||
component={HorizontalCheckBox}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Field
|
||||
{...commonProps}
|
||||
component={HorizontalInput}
|
||||
description={description}
|
||||
labelColumns={4}
|
||||
inputColumns={8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
NewParameterInput.propTypes = {
|
||||
defaultValue: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
PropTypes.string
|
||||
]),
|
||||
description: PropTypes.string.isRequired,
|
||||
intl: PropTypes.object,
|
||||
label: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
PropTypes.string
|
||||
])
|
||||
};
|
||||
NewParameterInput.defaultProps = {
|
||||
defaultValue: ''
|
||||
};
|
||||
|
||||
export default NewParameterInput;
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* 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, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import InlineNotification from '../ui/InlineNotification';
|
||||
import ParameterInput from './NewParameterInput';
|
||||
|
||||
const messages = defineMessages({
|
||||
noParameters: {
|
||||
id: 'ParameterInputList.noParameters',
|
||||
defaultMessage:
|
||||
'There are currently no parameters to configure in this section.'
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(jtomasek): rename this when original Formsy based ParameterInputList and
|
||||
// ParameterInput are not used
|
||||
class NewParameterInputList extends React.Component {
|
||||
render() {
|
||||
const emptyParametersMessage =
|
||||
this.props.emptyParametersMessage ||
|
||||
this.props.intl.formatMessage(messages.noParameters);
|
||||
|
||||
const parameters = this.props.parameters.map(parameter => {
|
||||
return (
|
||||
<ParameterInput
|
||||
key={parameter.name}
|
||||
name={parameter.name}
|
||||
label={parameter.label}
|
||||
description={parameter.description}
|
||||
defaultValue={parameter.default}
|
||||
value={parameter.value}
|
||||
type={parameter.type}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (parameters.isEmpty()) {
|
||||
return (
|
||||
<fieldset>
|
||||
<InlineNotification type="info">
|
||||
{emptyParametersMessage}
|
||||
</InlineNotification>
|
||||
</fieldset>
|
||||
);
|
||||
} else {
|
||||
return <fieldset>{parameters}</fieldset>;
|
||||
}
|
||||
}
|
||||
}
|
||||
NewParameterInputList.propTypes = {
|
||||
emptyParametersMessage: PropTypes.string,
|
||||
intl: PropTypes.object,
|
||||
mistralParameters: ImmutablePropTypes.map,
|
||||
parameters: ImmutablePropTypes.list.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(NewParameterInputList);
|
|
@ -15,53 +15,28 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import Formsy from 'formsy-react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { isObjectLike, mapValues } from 'lodash';
|
||||
import { fromJS, is } from 'immutable';
|
||||
import { pickBy, isEqual, mapValues } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { CloseModalButton } from '../ui/Modals';
|
||||
import EnvironmentConfigurationActions from '../../actions/EnvironmentConfigurationActions';
|
||||
import EnvironmentParameters from './EnvironmentParameters';
|
||||
import { getCurrentPlanName } from '../../selectors/plans';
|
||||
import { getRootParameters } from '../../selectors/parameters';
|
||||
import { getEnabledEnvironments } from '../../selectors/environmentConfiguration';
|
||||
import { Loader } from '../ui/Loader';
|
||||
import ModalFormErrorList from '../ui/forms/ModalFormErrorList';
|
||||
import ParametersActions from '../../actions/ParametersActions';
|
||||
import ParameterInputList from './ParameterInputList';
|
||||
import Tab from '../ui/Tab';
|
||||
import ParametersForm from './ParametersForm';
|
||||
import ParameterInputList from './NewParameterInputList';
|
||||
import ParametersSidebar from './ParametersSidebar';
|
||||
import TabPane from '../ui/TabPane';
|
||||
|
||||
const messages = defineMessages({
|
||||
saveChanges: {
|
||||
id: 'Parameters.saveChanges',
|
||||
defaultMessage: 'Save Changes'
|
||||
},
|
||||
saveAndClose: {
|
||||
id: 'Parameters.saveAndClose',
|
||||
defaultMessage: 'Save And Close'
|
||||
},
|
||||
cancel: {
|
||||
id: 'Parameters.cancel',
|
||||
defaultMessage: 'Cancel'
|
||||
},
|
||||
general: {
|
||||
id: 'Parameters.general',
|
||||
defaultMessage: 'General'
|
||||
}
|
||||
});
|
||||
|
||||
class Parameters extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
canSubmit: false,
|
||||
closeOnSubmit: false,
|
||||
selectedTab: 'general'
|
||||
activeTab: 'general'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,116 +45,23 @@ class Parameters extends React.Component {
|
|||
currentPlanName,
|
||||
fetchEnvironmentConfiguration,
|
||||
fetchParameters,
|
||||
history,
|
||||
isFetchingParameters
|
||||
} = this.props;
|
||||
fetchEnvironmentConfiguration(currentPlanName);
|
||||
fetchEnvironmentConfiguration(currentPlanName, () =>
|
||||
history.push(`/plans/${currentPlanName}`)
|
||||
);
|
||||
!isFetchingParameters && fetchParameters(currentPlanName);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.invalidateForm(this.props.formFieldErrors.toJS());
|
||||
}
|
||||
|
||||
enableButton() {
|
||||
this.setState({ canSubmit: true });
|
||||
}
|
||||
|
||||
disableButton() {
|
||||
this.setState({ canSubmit: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out non updated parameters, so only parameters which have been actually changed
|
||||
* get sent to updateparameters
|
||||
*/
|
||||
_filterFormData(formData) {
|
||||
return fromJS(formData)
|
||||
.filterNot((value, key) => {
|
||||
return is(fromJS(this.props.allParameters.get(key).default), value);
|
||||
})
|
||||
.toJS();
|
||||
}
|
||||
|
||||
/**
|
||||
* Json parameter values are sent as string, this function parses them and checks if they're object
|
||||
* or array. Also, parameters with undefined value are set to null
|
||||
*/
|
||||
_jsonParseFormData(formData) {
|
||||
return mapValues(formData, value => {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
if (isObjectLike(parsedValue)) {
|
||||
return parsedValue;
|
||||
}
|
||||
return value;
|
||||
} catch (e) {
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invalidateForm(formFieldErrors) {
|
||||
this.refs.parameterConfigurationForm.updateInputsWithError(formFieldErrors);
|
||||
}
|
||||
|
||||
handleSubmit(formData, resetForm, invalidateForm) {
|
||||
this.disableButton();
|
||||
|
||||
this.props.updateParameters(
|
||||
this.props.currentPlanName,
|
||||
this._filterFormData(this._jsonParseFormData(formData)),
|
||||
Object.keys(this.refs.parameterConfigurationForm.inputs)
|
||||
);
|
||||
|
||||
if (this.state.closeOnSubmit) {
|
||||
this.setState({
|
||||
closeOnSubmit: false
|
||||
});
|
||||
|
||||
this.props.history.push(`/plans/${this.props.currentPlanName}`);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitAndClose() {
|
||||
this.setState(
|
||||
{
|
||||
closeOnSubmit: true
|
||||
},
|
||||
this.refs.parameterConfigurationForm.submit
|
||||
);
|
||||
}
|
||||
|
||||
selectTab(tabName) {
|
||||
this.setState({
|
||||
selectedTab: tabName
|
||||
});
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
return this.props.enabledEnvironments.toList().map(environment => {
|
||||
return (
|
||||
<Tab
|
||||
key={environment.file}
|
||||
title={environment.description}
|
||||
isActive={environment.file === this.state.selectedTab}
|
||||
>
|
||||
<a
|
||||
className="link"
|
||||
onClick={this.selectTab.bind(this, environment.file)}
|
||||
>
|
||||
{environment.title}
|
||||
</a>
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
}
|
||||
isTabActive = tabName => tabName === this.state.activeTab;
|
||||
|
||||
renderTabPanes() {
|
||||
if (this.state.selectedTab === 'general') {
|
||||
if (this.state.activeTab === 'general') {
|
||||
return (
|
||||
<TabPane isActive>
|
||||
<ParameterInputList
|
||||
parameters={this.props.parameters.toList()}
|
||||
parameters={this.props.rootParameters.toList()}
|
||||
mistralParameters={this.props.mistralParameters}
|
||||
/>
|
||||
</TabPane>
|
||||
|
@ -188,7 +70,7 @@ class Parameters extends React.Component {
|
|||
return this.props.enabledEnvironments.toList().map(environment => {
|
||||
return (
|
||||
<TabPane
|
||||
isActive={this.state.selectedTab === environment.file}
|
||||
isActive={this.state.activeTab === environment.file}
|
||||
key={environment.file}
|
||||
renderOnlyActive
|
||||
>
|
||||
|
@ -199,75 +81,99 @@ class Parameters extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out non updated parameters, so only parameters which have been actually changed
|
||||
* get sent to updateparameters
|
||||
*/
|
||||
_filterFormData(values, initialValues) {
|
||||
return pickBy(values, (value, key) => !isEqual(value, initialValues[key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Json parameter values are sent as string, this function parses them and checks if they're object
|
||||
* or array. Also, parameters with undefined value are set to null
|
||||
*/
|
||||
_parseJsonTypeValues(values, parameters) {
|
||||
return mapValues(values, (value, key) => {
|
||||
if (parameters.get(key).type.toLowerCase() === 'json') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
}
|
||||
return value === undefined ? null : value;
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = ({ saveAndClose, ...values }, dispatch, { initialValues }) => {
|
||||
const {
|
||||
currentPlanName,
|
||||
history,
|
||||
parameters,
|
||||
updateParameters
|
||||
} = this.props;
|
||||
const updatedValues = this._parseJsonTypeValues(
|
||||
this._filterFormData(values, initialValues),
|
||||
parameters
|
||||
);
|
||||
updateParameters(
|
||||
currentPlanName,
|
||||
updatedValues,
|
||||
saveAndClose && history.push.bind(this, `/plans/${currentPlanName}`)
|
||||
);
|
||||
};
|
||||
|
||||
_convertJsonTypeParameterValueToString(value) {
|
||||
// Heat defaults empty values to empty string also some JSON type parameters
|
||||
// accept empty string as valid value
|
||||
return ['', undefined].includes(value) ? '' : JSON.stringify(value);
|
||||
}
|
||||
|
||||
getFormInitialValues() {
|
||||
return this.props.parameters
|
||||
.map(p => {
|
||||
const value = p.value === undefined ? p.default : p.value;
|
||||
if (p.type.toLowerCase() === 'json') {
|
||||
return this._convertJsonTypeParameterValueToString(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
.toJS();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { enabledEnvironments, parameters, parametersLoaded } = this.props;
|
||||
return (
|
||||
<Formsy
|
||||
ref="parameterConfigurationForm"
|
||||
role="form"
|
||||
className="form form-horizontal"
|
||||
onSubmit={this.handleSubmit.bind(this)}
|
||||
onValid={this.enableButton.bind(this)}
|
||||
onInvalid={this.disableButton.bind(this)}
|
||||
<Loader
|
||||
height={120}
|
||||
content="Fetching Parameters..."
|
||||
loaded={parametersLoaded}
|
||||
>
|
||||
<div className="container-fluid">
|
||||
<div className="row row-eq-height">
|
||||
<div className="col-sm-4 sidebar-pf sidebar-pf-left">
|
||||
<ul className="nav nav-pills nav-stacked nav-arrows">
|
||||
<Tab
|
||||
key="general"
|
||||
title={this.props.intl.formatMessage(messages.general)}
|
||||
isActive={'general' === this.state.selectedTab}
|
||||
>
|
||||
<a
|
||||
className="link"
|
||||
onClick={this.selectTab.bind(this, 'general')}
|
||||
>
|
||||
<FormattedMessage {...messages.general} />
|
||||
</a>
|
||||
</Tab>
|
||||
<li className="spacer" />
|
||||
{this.renderTabs()}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-sm-8">
|
||||
<Loader
|
||||
height={120}
|
||||
content="Fetching Parameters..."
|
||||
loaded={this.props.parametersLoaded}
|
||||
>
|
||||
<ModalFormErrorList errors={this.props.formErrors.toJS()} />
|
||||
<ParametersForm
|
||||
onSubmit={this.handleSubmit}
|
||||
parameters={parameters}
|
||||
initialValues={this.getFormInitialValues()}
|
||||
>
|
||||
<div className="container-fluid">
|
||||
<div className="row row-eq-height">
|
||||
<ParametersSidebar
|
||||
activateTab={tabName => this.setState({ activeTab: tabName })}
|
||||
enabledEnvironments={enabledEnvironments.toList()}
|
||||
isTabActive={this.isTabActive}
|
||||
/>
|
||||
<div className="col-sm-8">
|
||||
<div className="tab-content">{this.renderTabPanes()}</div>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!this.state.canSubmit}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<FormattedMessage {...messages.saveChanges} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!this.state.canSubmit}
|
||||
onClick={this.onSubmitAndClose.bind(this)}
|
||||
className="btn btn-default"
|
||||
>
|
||||
<FormattedMessage {...messages.saveAndClose} />
|
||||
</button>
|
||||
<CloseModalButton>
|
||||
<FormattedMessage {...messages.cancel} />
|
||||
</CloseModalButton>
|
||||
</div>
|
||||
</Formsy>
|
||||
</ParametersForm>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
Parameters.propTypes = {
|
||||
allParameters: ImmutablePropTypes.map.isRequired,
|
||||
currentPlanName: PropTypes.string,
|
||||
enabledEnvironments: ImmutablePropTypes.map.isRequired,
|
||||
fetchEnvironmentConfiguration: PropTypes.func.isRequired,
|
||||
|
@ -280,12 +186,13 @@ Parameters.propTypes = {
|
|||
mistralParameters: ImmutablePropTypes.map.isRequired,
|
||||
parameters: ImmutablePropTypes.map.isRequired,
|
||||
parametersLoaded: PropTypes.bool,
|
||||
rootParameters: ImmutablePropTypes.map.isRequired,
|
||||
updateParameters: PropTypes.func
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
allParameters: state.parameters.parameters,
|
||||
parameters: state.parameters.parameters,
|
||||
enabledEnvironments: getEnabledEnvironments(state),
|
||||
form: state.parameters.form,
|
||||
formErrors: state.parameters.form.get('formErrors'),
|
||||
|
@ -293,7 +200,7 @@ function mapStateToProps(state, ownProps) {
|
|||
currentPlanName: getCurrentPlanName(state),
|
||||
isFetchingParameters: state.parameters.isFetching,
|
||||
mistralParameters: state.parameters.mistralParameters,
|
||||
parameters: getRootParameters(state),
|
||||
rootParameters: getRootParameters(state),
|
||||
parametersLoaded: state.parameters.loaded
|
||||
};
|
||||
}
|
||||
|
@ -315,19 +222,12 @@ function mapDispatchToProps(dispatch, ownProps) {
|
|||
)
|
||||
);
|
||||
},
|
||||
updateParameters: (currentPlanName, data, inputFields, redirect) => {
|
||||
updateParameters: (currentPlanName, data, redirect) => {
|
||||
dispatch(
|
||||
ParametersActions.updateParameters(
|
||||
currentPlanName,
|
||||
data,
|
||||
inputFields,
|
||||
redirect
|
||||
)
|
||||
ParametersActions.updateParameters(currentPlanName, data, redirect)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Parameters)
|
||||
);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Parameters);
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* 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 { Button, Form, ModalFooter } from 'react-bootstrap';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { isEqual, pickBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { reduxForm } from 'redux-form';
|
||||
|
||||
import { CloseModalButton } from '../ui/Modals';
|
||||
import ModalFormErrorList from '../ui/forms/ModalFormErrorList';
|
||||
import { OverlayLoader } from '../ui/Loader';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: {
|
||||
id: 'ParametersForm.cancel',
|
||||
defaultMessage: 'Cancel'
|
||||
},
|
||||
saveChanges: {
|
||||
id: 'ParametersForm.saveChanges',
|
||||
defaultMessage: 'Save Changes'
|
||||
},
|
||||
saveAndClose: {
|
||||
id: 'ParametersForm.saveAndClose',
|
||||
defaultMessage: 'Save And Close'
|
||||
},
|
||||
updatingParameters: {
|
||||
id: 'ParametersForm.updatingParameters',
|
||||
defaultMessage: 'Updating parameters'
|
||||
},
|
||||
enterValidJson: {
|
||||
id: 'ParametersForm.enterValidJson',
|
||||
defaultMessage: 'Please enter a valid JSON.'
|
||||
},
|
||||
invalidParameters: {
|
||||
id: 'ParametersForm.invalidParameters',
|
||||
defaultMessage: 'Some parameter values are invalid:',
|
||||
description: 'Form error notification title'
|
||||
},
|
||||
invalidParametersList: {
|
||||
id: 'ParametersForm.invalidParametersList',
|
||||
defaultMessage: '{parameters}',
|
||||
description: 'A list of invalid parameters in form error message'
|
||||
}
|
||||
});
|
||||
|
||||
const ParametersForm = ({
|
||||
dispatch,
|
||||
error,
|
||||
children,
|
||||
onSubmit,
|
||||
handleSubmit,
|
||||
intl: { formatMessage },
|
||||
invalid,
|
||||
pristine,
|
||||
submitting,
|
||||
initialValues
|
||||
}) => (
|
||||
<Form onSubmit={handleSubmit} horizontal>
|
||||
<OverlayLoader
|
||||
loaded={!submitting}
|
||||
content={formatMessage(messages.updatingParameters)}
|
||||
>
|
||||
<ModalFormErrorList errors={error ? [error] : []} />
|
||||
{children}
|
||||
</OverlayLoader>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={invalid || pristine || submitting}
|
||||
bsStyle="primary"
|
||||
>
|
||||
<FormattedMessage {...messages.saveChanges} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={invalid || pristine || submitting}
|
||||
onClick={handleSubmit(values =>
|
||||
onSubmit({ ...values, saveAndClose: true }, dispatch, {
|
||||
initialValues
|
||||
})
|
||||
)}
|
||||
>
|
||||
<FormattedMessage {...messages.saveAndClose} />
|
||||
</Button>
|
||||
<CloseModalButton>
|
||||
<FormattedMessage {...messages.cancel} />
|
||||
</CloseModalButton>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
);
|
||||
ParametersForm.propTypes = {
|
||||
children: PropTypes.node,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
initialValues: PropTypes.object.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
parameters: ImmutablePropTypes.map.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
submitting: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const isJSON = value => {
|
||||
try {
|
||||
// empty string is considered valid JSON type parameter value
|
||||
return value === '' ? true : !!JSON.parse(value);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (
|
||||
values,
|
||||
{ initialValues, parameters, intl: { formatMessage } }
|
||||
) => {
|
||||
const errors = {};
|
||||
|
||||
// Validate only parameters which were updated by User
|
||||
const updatedValues = pickBy(
|
||||
values,
|
||||
(value, key) => !isEqual(value, initialValues[key])
|
||||
);
|
||||
|
||||
Object.keys(updatedValues).map(key => {
|
||||
if (parameters.getIn([key, 'type']).toLowerCase() === 'json') {
|
||||
if (!isJSON(values[key])) {
|
||||
errors[key] = formatMessage(messages.enterValidJson);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add global error message
|
||||
if (Object.keys(errors).length > 0) {
|
||||
errors['_error'] = {
|
||||
title: formatMessage(messages.invalidParameters),
|
||||
message: formatMessage(messages.invalidParametersList, {
|
||||
parameters: Object.keys(errors).join(', ')
|
||||
})
|
||||
};
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const form = reduxForm({
|
||||
form: 'parametersForm',
|
||||
validate
|
||||
});
|
||||
|
||||
export default injectIntl(form(ParametersForm));
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Tab from '../ui/Tab';
|
||||
|
||||
const messages = defineMessages({
|
||||
general: {
|
||||
id: 'Parameters.general',
|
||||
defaultMessage: 'General'
|
||||
}
|
||||
});
|
||||
|
||||
const ParametersSidebar = ({
|
||||
activateTab,
|
||||
enabledEnvironments,
|
||||
intl: { formatMessage },
|
||||
isTabActive
|
||||
}) => (
|
||||
<div className="col-sm-4 sidebar-pf sidebar-pf-left">
|
||||
<ul
|
||||
id="Parameters__EnvironmentTabsList"
|
||||
className="nav nav-pills nav-stacked nav-arrows"
|
||||
>
|
||||
<Tab
|
||||
key="general"
|
||||
title={formatMessage(messages.general)}
|
||||
isActive={isTabActive('general')}
|
||||
>
|
||||
<a className="link" onClick={() => activateTab('general')}>
|
||||
<FormattedMessage {...messages.general} />
|
||||
</a>
|
||||
</Tab>
|
||||
<li className="spacer" />
|
||||
{enabledEnvironments.map(environment => {
|
||||
return (
|
||||
<Tab key={environment.file} isActive={isTabActive(environment.file)}>
|
||||
<a
|
||||
className="link"
|
||||
onClick={() => activateTab(environment.file)}
|
||||
title={environment.file}
|
||||
>
|
||||
{environment.title}
|
||||
</a>
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
ParametersSidebar.propTypes = {
|
||||
activateTab: PropTypes.func.isRequired,
|
||||
enabledEnvironments: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isTabActive: PropTypes.func.isRequired
|
||||
};
|
||||
export default injectIntl(ParametersSidebar);
|
|
@ -100,15 +100,8 @@ const mapStateToProps = state => ({
|
|||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
updateParameters: (currentPlanName, data, inputFields, redirectPath) => {
|
||||
dispatch(
|
||||
ParametersActions.updateParameters(
|
||||
currentPlanName,
|
||||
data,
|
||||
inputFields,
|
||||
redirectPath
|
||||
)
|
||||
);
|
||||
updateParameters: (currentPlanName, data) => {
|
||||
dispatch(ParametersActions.updateNodesAssignment(currentPlanName, data));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -294,7 +294,7 @@ function mapDispatchToProps(dispatch) {
|
|||
},
|
||||
updateParameters: (currentPlanName, data, inputFields, redirectPath) => {
|
||||
dispatch(
|
||||
ParametersActions.updateParameters(
|
||||
ParametersActions.updateRoleParameters(
|
||||
currentPlanName,
|
||||
data,
|
||||
inputFields,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Checkbox, Col, FormGroup } from 'react-bootstrap';
|
||||
import { Checkbox, Col, ControlLabel, FormGroup } from 'react-bootstrap';
|
||||
|
||||
import { getValidationState, InputDescription, InputMessage } from './utils';
|
||||
|
||||
|
@ -35,10 +35,15 @@ const HorizontalCheckBox = ({
|
|||
}) => {
|
||||
return (
|
||||
<FormGroup controlId={id} validationState={getValidationState(meta)}>
|
||||
<Col smOffset={labelColumns} sm={inputColumns}>
|
||||
<Checkbox {...input} {...rest}>
|
||||
<span className={cx({ 'required-pf': required })}>{label}</span>
|
||||
</Checkbox>
|
||||
<Col
|
||||
componentClass={ControlLabel}
|
||||
sm={labelColumns}
|
||||
className={cx({ 'required-pf': required })}
|
||||
>
|
||||
{label}
|
||||
</Col>
|
||||
<Col sm={inputColumns}>
|
||||
<Checkbox {...input} {...rest} />
|
||||
<InputMessage {...meta} />
|
||||
<InputDescription description={description} />
|
||||
</Col>
|
||||
|
|
|
@ -31,24 +31,22 @@ const HorizontalTextarea = ({
|
|||
meta,
|
||||
required,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<FormGroup controlId={id} validationState={getValidationState(meta)}>
|
||||
<Col
|
||||
componentClass={ControlLabel}
|
||||
sm={labelColumns}
|
||||
className={cx({ 'required-pf': required })}
|
||||
>
|
||||
{label}
|
||||
</Col>
|
||||
<Col sm={inputColumns}>
|
||||
<FormControl componentClass="textarea" {...input} {...rest} />
|
||||
<InputMessage {...meta} />
|
||||
<InputDescription description={description} />
|
||||
</Col>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<FormGroup controlId={id} validationState={getValidationState(meta)}>
|
||||
<Col
|
||||
componentClass={ControlLabel}
|
||||
sm={labelColumns}
|
||||
className={cx({ 'required-pf': required })}
|
||||
>
|
||||
{label}
|
||||
</Col>
|
||||
<Col sm={inputColumns}>
|
||||
<FormControl componentClass="textarea" {...input} {...rest} />
|
||||
<InputMessage {...meta} />
|
||||
<InputDescription description={description} />
|
||||
</Col>
|
||||
</FormGroup>
|
||||
);
|
||||
HorizontalTextarea.propTypes = {
|
||||
description: PropTypes.node,
|
||||
id: PropTypes.string.isRequired,
|
||||
|
|
Loading…
Reference in New Issue