diff options
author | George Goldberg <george@gberg.me> | 2017-07-04 08:00:17 +0100 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2017-07-04 00:00:17 -0700 |
commit | 0a3bb8fdb10f2ce72e5e975a35fc7d22637265f9 (patch) | |
tree | 3a3c7dfed0830d9e3a945f862c60d99f15074ca1 /webapp | |
parent | f54aee1ef5466fdf11803cd75be3b7267e68540f (diff) | |
download | chat-0a3bb8fdb10f2ce72e5e975a35fc7d22637265f9.tar.gz chat-0a3bb8fdb10f2ce72e5e975a35fc7d22637265f9.tar.bz2 chat-0a3bb8fdb10f2ce72e5e975a35fc7d22637265f9.zip |
Refactor system console buttons into RequestButton component. (#6808)
Since I was going to make yet another button for the ElasticSearch test
config button, I refactored all of them to use a single common component
and tidied that component up and gave it some unit tests.
Diffstat (limited to 'webapp')
12 files changed, 1121 insertions, 604 deletions
diff --git a/webapp/components/admin_console/configuration_settings.jsx b/webapp/components/admin_console/configuration_settings.jsx index 449b4f549..72bd0e330 100644 --- a/webapp/components/admin_console/configuration_settings.jsx +++ b/webapp/components/admin_console/configuration_settings.jsx @@ -9,12 +9,12 @@ import ErrorStore from 'stores/error_store.jsx'; import {ErrorBarTypes} from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; +import {invalidateAllCaches, reloadConfig} from 'actions/admin_actions.jsx'; import AdminSettings from './admin_settings.jsx'; import BooleanSetting from './boolean_setting.jsx'; import {ConnectionSecurityDropdownSettingWebserver} from './connection_security_dropdown_setting.jsx'; -import PurgeCachesButton from './purge_caches.jsx'; -import ReloadConfigButton from './reload_config.jsx'; import SettingsGroup from './settings_group.jsx'; +import RequestButton from './request_button/request_button'; import TextSetting from './text_setting.jsx'; import WebserverModeDropdownSetting from './webserver_mode_dropdown_setting.jsx'; @@ -83,6 +83,54 @@ export default class ConfigurationSettings extends AdminSettings { } renderSettings() { + const reloadConfigurationHelpText = ( + <FormattedMessage + id='admin.reload.reloadDescription' + defaultMessage='Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating "config.json" to the new desired configuration and using the {featureName} feature to load the new settings while the server is running. The administrator should then use the {recycleDatabaseConnections} feature to recycle the database connections based on the new settings.' + values={{ + featureName: ( + <b> + <FormattedMessage + id='admin.reload.reloadDescription.featureName' + defaultMessage='Reload Configuration from Disk' + /> + </b> + ), + recycleDatabaseConnections: ( + <a href='../advanced/database'> + <b> + <FormattedMessage + id='admin.reload.reloadDescription.recycleDatabaseConnections' + defaultMessage='Database > Recycle Database Connections' + /> + </b> + </a> + ) + }} + /> + ); + + let reloadConfigButton = <div/>; + if (global.window.mm_license.IsLicensed === 'true') { + reloadConfigButton = ( + <RequestButton + requestAction={reloadConfig} + helpText={reloadConfigurationHelpText} + buttonText={ + <FormattedMessage + id='admin.reload.button' + defaultMessage='Reload Configuration From Disk' + /> + } + showSuccessMessage={false} + errorMessage={{ + id: 'admin.reload.reloadFail', + defaultMessage: 'Reload unsuccessful: {error}' + }} + /> + ); + } + return ( <SettingsGroup> <div className='banner'> @@ -261,8 +309,28 @@ export default class ConfigurationSettings extends AdminSettings { onChange={this.handleChange} disabled={false} /> - <ReloadConfigButton/> - <PurgeCachesButton/> + {reloadConfigButton} + <RequestButton + requestAction={invalidateAllCaches} + helpText={ + <FormattedMessage + id='admin.purge.purgeDescription' + defaultMessage='This will purge all the in-memory caches for things like sessions, accounts, channels, etc. Deployments using High Availability will attempt to purge all the servers in the cluster. Purging the caches may adversely impact performance.' + /> + } + buttonText={ + <FormattedMessage + id='admin.purge.button' + defaultMessage='Purge All Caches' + /> + } + showSuccessMessage={false} + includeDetailedError={true} + errorMessage={{ + id: 'admin.purge.purgeFail', + defaultMessage: 'Purging unsuccessful: {error}' + }} + /> </SettingsGroup> ); } diff --git a/webapp/components/admin_console/database_settings.jsx b/webapp/components/admin_console/database_settings.jsx index e182db70e..303865d91 100644 --- a/webapp/components/admin_console/database_settings.jsx +++ b/webapp/components/admin_console/database_settings.jsx @@ -3,6 +3,7 @@ import React from 'react'; +import {recycleDatabaseConnection} from 'actions/admin_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import AdminSettings from './admin_settings.jsx'; @@ -11,7 +12,7 @@ import {FormattedMessage} from 'react-intl'; import GeneratedSetting from './generated_setting.jsx'; import SettingsGroup from './settings_group.jsx'; import TextSetting from './text_setting.jsx'; -import RecycleDbButton from './recycle_db.jsx'; +import RequestButton from './request_button/request_button.jsx'; export default class DatabaseSettings extends AdminSettings { constructor(props) { @@ -58,6 +59,53 @@ export default class DatabaseSettings extends AdminSettings { renderSettings() { const dataSource = '**********' + this.state.dataSource.substring(this.state.dataSource.indexOf('@')); + let recycleDbButton = <div/>; + if (global.window.mm_license.IsLicensed === 'true') { + recycleDbButton = ( + <RequestButton + requestAction={recycleDatabaseConnection} + helpText={ + <FormattedMessage + id='admin.recycle.recycleDescription' + defaultMessage='Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating "config.json" to the new desired configuration and using the {reloadConfiguration} feature to load the new settings while the server is running. The administrator should then use {featureName} feature to recycle the database connections based on the new settings.' + values={{ + featureName: ( + <b> + <FormattedMessage + id='admin.recycle.recycleDescription.featureName' + defaultMessage='Recycle Database Connections' + /> + </b> + ), + reloadConfiguration: ( + <a href='../general/configuration'> + <b> + <FormattedMessage + id='admin.recycle.recycleDescription.reloadConfiguration' + defaultMessage='Configuration > Reload Configuration from Disk' + /> + </b> + </a> + ) + }} + /> + } + buttonText={ + <FormattedMessage + id='admin.recycle.button' + defaultMessage='Recycle Database Connections' + /> + } + showSuccessMessage={false} + errorMessage={{ + id: 'admin.recycle.reloadFail', + defaultMessage: 'Recycling unsuccessful: {error}' + }} + includeDetailedError={true} + /> + ); + } + return ( <SettingsGroup> <p> @@ -183,7 +231,7 @@ export default class DatabaseSettings extends AdminSettings { value={this.state.trace} onChange={this.handleChange} /> - <RecycleDbButton/> + {recycleDbButton} </SettingsGroup> ); } diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx index 87eab8004..9ffbe3b0e 100644 --- a/webapp/components/admin_console/ldap_settings.jsx +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -7,13 +7,13 @@ import {ConnectionSecurityDropdownSettingLdap} from './connection_security_dropd import SettingsGroup from './settings_group.jsx'; import TextSetting from './text_setting.jsx'; -import SyncNowButton from './sync_now_button.jsx'; -import LdapTestButton from './ldap_test_button.jsx'; +import {ldapSyncNow, ldapTest} from 'actions/admin_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import React from 'react'; import {FormattedMessage} from 'react-intl'; +import RequestButton from './request_button/request_button.jsx'; export default class LdapSettings extends AdminSettings { constructor(props) { @@ -450,13 +450,53 @@ export default class LdapSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enable} /> - <SyncNowButton + <RequestButton + requestAction={ldapSyncNow} + helpText={ + <FormattedMessage + id='admin.ldap.syncNowHelpText' + defaultMessage='Initiates an AD/LDAP synchronization immediately.' + /> + } + buttonText={ + <FormattedMessage + id='admin.ldap.sync_button' + defaultMessage='AD/LDAP Synchronize Now' + /> + } disabled={!this.state.enable} + showSuccessMessage={false} + errorMessage={{ + id: 'admin.ldap.syncFailure', + defaultMessage: 'Sync Failure: {error}' + }} + includeDetailedError={true} /> - <LdapTestButton + <RequestButton + requestAction={ldapTest} + helpText={ + <FormattedMessage + id='admin.ldap.testHelpText' + defaultMessage='Tests if the Mattermost server can connect to the AD/LDAP server specified. See log file for more detailed error messages.' + /> + } + buttonText={ + <FormattedMessage + id='admin.ldap.ldap_test_button' + defaultMessage='AD/LDAP Test' + /> + } disabled={!this.state.enable} - submitFunction={this.doSubmit} saveNeeded={this.state.saveNeeded} + saveConfigAction={this.doSubmit} + errorMessage={{ + id: 'admin.ldap.testFailure', + defaultMessage: 'AD/LDAP Test Failure: {error}' + }} + successMessage={{ + id: 'admin.ldap.testSuccess', + defaultMessage: 'AD/LDAP Test Successful' + }} /> </SettingsGroup> ); diff --git a/webapp/components/admin_console/ldap_test_button.jsx b/webapp/components/admin_console/ldap_test_button.jsx deleted file mode 100644 index e785d0f78..000000000 --- a/webapp/components/admin_console/ldap_test_button.jsx +++ /dev/null @@ -1,146 +0,0 @@ -import PropTypes from 'prop-types'; - -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import {ldapTest} from 'actions/admin_actions.jsx'; - -export default class LdapTestButton extends React.Component { - static get propTypes() { - return { - disabled: PropTypes.bool, - submitFunction: PropTypes.func, - saveNeeded: PropTypes.bool - }; - } - constructor(props) { - super(props); - - this.handleLdapTest = this.handleLdapTest.bind(this); - - this.state = { - buisy: false, - fail: null, - success: false - }; - } - - handleLdapTest(e) { - e.preventDefault(); - - this.setState({ - buisy: true, - fail: null, - success: false - }); - - const doRequest = () => { //eslint-disable-line func-style - ldapTest( - () => { - this.setState({ - buisy: false, - success: true - }); - }, - (err) => { - this.setState({ - buisy: false, - fail: err.message - }); - } - ); - }; - - // If we need to run the save function then run it with our request function as callback - if (this.props.saveNeeded) { - this.props.submitFunction(doRequest); - } else { - doRequest(); - } - } - - render() { - let message = null; - if (this.state.fail) { - message = ( - <div> - <div className='alert alert-warning'> - <i className='fa fa-warning'/> - <FormattedMessage - id='admin.ldap.testFailure' - defaultMessage='AD/LDAP Test Failure: {error}' - values={{ - error: this.state.fail - }} - /> - </div> - </div> - ); - } else if (this.state.success) { - message = ( - <div> - <div className='alert alert-success'> - <i className='fa fa-success'/> - <FormattedMessage - id='admin.ldap.testSuccess' - defaultMessage='AD/LDAP Test Successful' - values={{ - error: this.state.fail - }} - /> - </div> - </div> - ); - } - - const helpText = ( - <FormattedHTMLMessage - id='admin.ldap.testHelpText' - defaultMessage='Tests if the Mattermost server can connect to the AD/LDAP server specified. See log file for more detailed error messages.' - /> - ); - - let contents = null; - if (this.state.loading) { - contents = ( - <span> - <span className='fa fa-refresh icon--rotate'/> - {Utils.localizeMessage('admin.reload.loading', ' Loading...')} - </span> - ); - } else { - contents = ( - <FormattedMessage - id='admin.ldap.ldap_test_button' - defaultMessage='AD/LDAP Test' - /> - ); - } - - return ( - <div className='form-group reload-config'> - <div className='col-sm-offset-4 col-sm-8'> - <div> - <button - className='btn btn-default' - onClick={this.handleLdapTest} - disabled={this.props.disabled} - > - {contents} - </button> - {message} - </div> - <div className='help-text'> - {helpText} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/admin_console/purge_caches.jsx b/webapp/components/admin_console/purge_caches.jsx deleted file mode 100644 index d2337d587..000000000 --- a/webapp/components/admin_console/purge_caches.jsx +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import {FormattedMessage} from 'react-intl'; - -import {invalidateAllCaches} from 'actions/admin_actions.jsx'; - -export default class PurgeCachesButton extends React.Component { - constructor(props) { - super(props); - - this.handlePurge = this.handlePurge.bind(this); - - this.state = { - loading: false, - fail: null - }; - } - - handlePurge(e) { - e.preventDefault(); - - this.setState({ - loading: true, - fail: null - }); - - invalidateAllCaches( - () => { - this.setState({ - loading: false - }); - }, - (err) => { - this.setState({ - loading: false, - fail: err.message + ' - ' + err.detailed_error - }); - } - ); - } - - render() { - let testMessage = null; - if (this.state.fail) { - testMessage = ( - <div className='alert alert-warning'> - <i className='fa fa-warning'/> - <FormattedMessage - id='admin.purge.purgeFail' - defaultMessage='Purging unsuccessful: {error}' - values={{ - error: this.state.fail - }} - /> - </div> - ); - } - - const helpText = ( - <FormattedMessage - id='admin.purge.purgeDescription' - defaultMessage='This will purge all the in-memory caches for things like sessions, accounts, channels, etc. Deployments using High Availability will attempt to purge all the servers in the cluster. Purging the caches may adversly impact performance.' - /> - ); - - let contents = null; - if (this.state.loading) { - contents = ( - <span> - <span className='fa fa-refresh icon--rotate'/> - <FormattedMessage - id='admin.purge.loading' - defaultMessage='Loading...' - /> - </span> - ); - } else { - contents = ( - <FormattedMessage - id='admin.purge.button' - defaultMessage='Purge All Caches' - /> - ); - } - - return ( - <div className='form-group reload-config'> - <div className='col-sm-offset-4 col-sm-8'> - <div> - <button - className='btn btn-default' - onClick={this.handlePurge} - > - {contents} - </button> - {testMessage} - </div> - <div className='help-text'> - {helpText} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/admin_console/recycle_db.jsx b/webapp/components/admin_console/recycle_db.jsx deleted file mode 100644 index 5e536d908..000000000 --- a/webapp/components/admin_console/recycle_db.jsx +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import {recycleDatabaseConnection} from 'actions/admin_actions.jsx'; - -export default class RecycleDbButton extends React.Component { - constructor(props) { - super(props); - - this.handleRecycle = this.handleRecycle.bind(this); - - this.state = { - loading: false, - fail: null - }; - } - - handleRecycle(e) { - e.preventDefault(); - - this.setState({ - loading: true, - fail: null - }); - - recycleDatabaseConnection( - () => { - this.setState({ - loading: false - }); - }, - (err) => { - this.setState({ - loading: false, - fail: err.message + ' - ' + err.detailed_error - }); - } - ); - } - - render() { - if (global.window.mm_license.IsLicensed !== 'true') { - return <div/>; - } - - let testMessage = null; - if (this.state.fail) { - testMessage = ( - <div className='alert alert-warning'> - <i className='fa fa-warning'/> - <FormattedMessage - id='admin.recycle.reloadFail' - defaultMessage='Recycling unsuccessful: {error}' - values={{ - error: this.state.fail - }} - /> - </div> - ); - } - - const helpText = ( - <FormattedHTMLMessage - id='admin.recycle.recycleDescription' - defaultMessage='Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating "config.json" to the new desired configuration and using the <a href="../general/configuration"><b>Configuration > Reload Configuration from Disk</b></a> feature to load the new settings while the server is running. The administrator should then use <b>Recycle Database Connections</b> feature to recycle the database connections based on the new settings.' - /> - ); - - let contents = null; - if (this.state.loading) { - contents = ( - <span> - <span className='fa fa-refresh icon--rotate'/> - {Utils.localizeMessage('admin.recycle.loading', ' Recycling...')} - </span> - ); - } else { - contents = ( - <FormattedMessage - id='admin.recycle.button' - defaultMessage='Recycle Database Connections' - /> - ); - } - - return ( - <div className='form-group recycle-db'> - <div className='col-sm-offset-4 col-sm-8'> - <div> - <button - className='btn btn-default' - onClick={this.handleRecycle} - > - {contents} - </button> - {testMessage} - </div> - <div className='help-text'> - {helpText} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/admin_console/reload_config.jsx b/webapp/components/admin_console/reload_config.jsx deleted file mode 100644 index ad3d9cca7..000000000 --- a/webapp/components/admin_console/reload_config.jsx +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import {reloadConfig} from 'actions/admin_actions.jsx'; - -export default class ReloadConfigButton extends React.Component { - constructor(props) { - super(props); - - this.handleReloadConfig = this.handleReloadConfig.bind(this); - - this.state = { - loading: false, - fail: null - }; - } - - handleReloadConfig(e) { - e.preventDefault(); - - this.setState({ - loading: true, - fail: null - }); - - reloadConfig( - () => { - this.setState({ - loading: false - }); - }, - (err) => { - this.setState({ - loading: false, - fail: err.message + ' - ' + err.detailed_error - }); - } - ); - } - - render() { - if (global.window.mm_license.IsLicensed !== 'true') { - return <div/>; - } - - let testMessage = null; - if (this.state.fail) { - testMessage = ( - <div className='alert alert-warning'> - <i className='fa fa-warning'/> - <FormattedMessage - id='admin.reload.reloadFail' - defaultMessage='Reload unsuccessful: {error}' - values={{ - error: this.state.fail - }} - /> - </div> - ); - } - - const helpText = ( - <FormattedHTMLMessage - id='admin.reload.reloadDescription' - defaultMessage='Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating "config.json" to the new desired configuration and using the <b>Reload Configuration from Disk</b> feature to load the new settings while the server is running. The administrator should then use the <a href="../advanced/database"><b>Database > Recycle Database Connections</b></a> feature to recycle the database connections based on the new settings.' - /> - ); - - let contents = null; - if (this.state.loading) { - contents = ( - <span> - <span className='fa fa-refresh icon--rotate'/> - {Utils.localizeMessage('admin.reload.loading', ' Loading...')} - </span> - ); - } else { - contents = ( - <FormattedMessage - id='admin.reload.button' - defaultMessage='Reload Configuration From Disk' - /> - ); - } - - return ( - <div className='form-group reload-config'> - <div className='col-sm-offset-4 col-sm-8'> - <div> - <button - className='btn btn-default' - onClick={this.handleReloadConfig} - > - {contents} - </button> - {testMessage} - </div> - <div className='help-text'> - {helpText} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/admin_console/request_button/request_button.jsx b/webapp/components/admin_console/request_button/request_button.jsx new file mode 100644 index 000000000..4aad287d2 --- /dev/null +++ b/webapp/components/admin_console/request_button/request_button.jsx @@ -0,0 +1,234 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; + +import * as Utils from 'utils/utils.jsx'; + +/** + * A button which, when clicked, performs an action and displays + * its outcome as either success, or failure accompanied by the + * `message` property of the `err` object. + */ +export default class RequestButton extends React.Component { + static propTypes = { + + /** + * The action to be called to carry out the request. + */ + requestAction: PropTypes.func.isRequired, + + /** + * A component that displays help text for the request button. + * + * Typically, this will be a <FormattedMessage/>. + */ + helpText: PropTypes.element.isRequired, + + /** + * A component to be displayed on the button. + * + * Typically, this will be a <FormattedMessage/> + */ + buttonText: PropTypes.element.isRequired, + + /** + * True if the button form control should be disabled, otherwise false. + */ + disabled: PropTypes.bool, + + /** + * True if the config needs to be saved before running the request, otherwise false. + * + * If set to true, the action provided in the `saveConfigAction` property will be + * called before the action provided in the `requestAction` property, with the later + * only being called if the former is successful. + */ + saveNeeded: PropTypes.bool, + + /** + * Action to be called to save the config, if saveNeeded is set to true. + */ + saveConfigAction: PropTypes.func, + + /** + * True if the success message should be show when the request completes successfully, + * otherwise false. + */ + showSuccessMessage: PropTypes.bool, + + /** + * The message to show when the request completes successfully. + */ + successMessage: PropTypes.shape({ + + /** + * The i18n string ID for the success message. + */ + id: PropTypes.string.isRequired, + + /** + * The i18n default value for the success message. + */ + defaultMessage: PropTypes.string.isRequired + }), + + /** + * The message to show when the request returns an error. + */ + errorMessage: PropTypes.shape({ + + /** + * The i18n string ID for the error message. + */ + id: PropTypes.string.isRequired, + + /** + * The i18n default value for the error message. + * + * The placeholder {error} may be used to include the error message returned + * by the server in response to the failed request. + */ + defaultMessage: PropTypes.string.isRequired + }), + + /** + * True if the {error} placeholder for the `errorMessage` property should include both + * the `message` and `detailed_error` properties of the error returned from the server, + * otherwise false to include only the `message` property. + */ + includeDetailedError: PropTypes.bool + } + + static defaultProps = { + disabled: false, + saveNeeded: false, + showSuccessMessage: true, + includeDetailedError: false, + successMessage: { + id: 'admin.requestButton.requestSuccess', + defaultMessage: 'Test Successful' + }, + errorMessage: { + id: 'admin.requestButton.requestFailure', + defaultMessage: 'Test Failure: {error}' + } + } + + constructor(props) { + super(props); + + this.handleRequest = this.handleRequest.bind(this); + + this.state = { + busy: false, + fail: null, + success: false + }; + } + + handleRequest(e) { + e.preventDefault(); + + this.setState({ + busy: true, + fail: null, + success: false + }); + + const doRequest = () => { //eslint-disable-line func-style + this.props.requestAction( + () => { + this.setState({ + busy: false, + success: true + }); + }, + (err) => { + let errMsg = err.message; + if (this.props.includeDetailedError) { + errMsg += ' - ' + err.detailed_error; + } + + this.setState({ + busy: false, + fail: errMsg + }); + } + ); + }; + + if (this.props.saveNeeded) { + this.props.saveConfigAction(doRequest); + } else { + doRequest(); + } + } + + render() { + let message = null; + if (this.state.fail) { + message = ( + <div> + <div className='alert alert-warning'> + <i className='fa fa-warning'/> + <FormattedMessage + id={this.props.errorMessage.id} + defaultMessage={this.props.errorMessage.defaultMessage} + values={{ + error: this.state.fail + }} + /> + </div> + </div> + ); + } else if (this.state.success && this.props.showSuccessMessage) { + message = ( + <div> + <div className='alert alert-success'> + <i className='fa fa-success'/> + <FormattedMessage + id={this.props.successMessage.id} + defaultMessage={this.props.successMessage.defaultMessage} + /> + </div> + </div> + ); + } + + let contents = null; + if (this.state.busy) { + contents = ( + <span> + <span className='fa fa-refresh icon--rotate'/> + {Utils.localizeMessage('admin.requestButton.loading', ' Loading...')} + </span> + ); + } else { + contents = this.props.buttonText; + } + + return ( + <div className='form-group reload-config'> + <div className='col-sm-offset-4 col-sm-8'> + <div> + <button + className='btn btn-default' + onClick={this.handleRequest} + disabled={this.props.disabled} + > + {contents} + </button> + {message} + </div> + <div className='help-text'> + {this.props.helpText} + </div> + </div> + </div> + ); + } +} diff --git a/webapp/components/admin_console/sync_now_button.jsx b/webapp/components/admin_console/sync_now_button.jsx deleted file mode 100644 index b2a5a001d..000000000 --- a/webapp/components/admin_console/sync_now_button.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from 'prop-types'; - -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import {ldapSyncNow} from 'actions/admin_actions.jsx'; - -export default class SyncNowButton extends React.Component { - static get propTypes() { - return { - disabled: PropTypes.bool - }; - } - constructor(props) { - super(props); - - this.handleSyncNow = this.handleSyncNow.bind(this); - - this.state = { - buisy: false, - fail: null - }; - } - - handleSyncNow(e) { - e.preventDefault(); - - this.setState({ - buisy: true, - fail: null - }); - - ldapSyncNow( - () => { - this.setState({ - buisy: false - }); - }, - (err) => { - this.setState({ - buisy: false, - fail: err.message + ' - ' + err.detailed_error - }); - } - ); - } - - render() { - let failMessage = null; - if (this.state.fail) { - failMessage = ( - <div className='alert alert-warning'> - <i className='fa fa-warning'/> - <FormattedMessage - id='admin.ldap.syncFailure' - defaultMessage='Sync Failure: {error}' - values={{ - error: this.state.fail - }} - /> - </div> - ); - } - - const helpText = ( - <FormattedHTMLMessage - id='admin.ldap.syncNowHelpText' - defaultMessage='Initiates an AD/LDAP synchronization immediately.' - /> - ); - - let contents = null; - if (this.state.loading) { - contents = ( - <span> - <span className='fa fa-refresh icon--rotate'/> - {Utils.localizeMessage('admin.reload.loading', ' Loading...')} - </span> - ); - } else { - contents = ( - <FormattedMessage - id='admin.ldap.sync_button' - defaultMessage='AD/LDAP Synchronize Now' - /> - ); - } - - return ( - <div className='form-group reload-config'> - <div className='col-sm-offset-4 col-sm-8'> - <div> - <button - className='btn btn-default' - onClick={this.handleSyncNow} - disabled={this.props.disabled} - > - {contents} - </button> - {failMessage} - </div> - <div className='help-text'> - {helpText} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 5416cd206..037809d1a 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -601,13 +601,20 @@ "admin.rate.title": "Rate Limit Settings", "admin.recycle.button": "Recycle Database Connections", "admin.recycle.loading": " Recycling...", - "admin.recycle.recycleDescription": "Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating \"config.json\" to the new desired configuration and using the <a href=\"../general/configuration\"><b>Configuration > Reload Configuration from Disk</b></a> feature to load the new settings while the server is running. The administrator should then use <b>Recycle Database Connections</b> feature to recycle the database connections based on the new settings.", + "admin.recycle.recycleDescription": "Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating \"config.json\" to the new desired configuration and using the {reloadConfiguration} feature to load the new settings while the server is running. The administrator should then use {featureName} feature to recycle the database connections based on the new settings.", + "admin.recycle.recycleDescription.featureName": "Recycle Database Connections", + "admin.recycle.recycleDescription.reloadConfiguration": "Configuration > Reload Configuration from Disk", "admin.recycle.reloadFail": "Recycling unsuccessful: {error}", "admin.regenerate": "Regenerate", "admin.reload.button": "Reload Configuration From Disk", "admin.reload.loading": " Loading...", - "admin.reload.reloadDescription": "Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating \"config.json\" to the new desired configuration and using the <b>Reload Configuration from Disk</b> feature to load the new settings while the server is running. The administrator should then use the <a href=\"../advanced/database\"><b>Database > Recycle Database Connections</b></a> feature to recycle the database connections based on the new settings.", + "admin.reload.reloadDescription": "Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating \"config.json\" to the new desired configuration and using the {featureName} feature to load the new settings while the server is running. The administrator should then use the {recycleDatabaseConnections} feature to recycle the database connections based on the new settings.", + "admin.reload.reloadDescription.featureName": "Reload Configuration from Disk", + "admin.reload.reloadDescription.recycleDatabaseConnections": "Database > Recycle Database Connections", "admin.reload.reloadFail": "Reloading unsuccessful: {error}", + "admin.requestButton.loading": " Loading...", + "admin.requestButton.requestSuccess": "Test Successful", + "admin.requestButton.requestFailure": "Test Failure: {error}", "admin.reset_password.close": "Close", "admin.reset_password.newPassword": "New Password", "admin.reset_password.select": "Select", diff --git a/webapp/tests/components/admin_console/request_button/__snapshots__/request_button.test.jsx.snap b/webapp/tests/components/admin_console/request_button/__snapshots__/request_button.test.jsx.snap new file mode 100644 index 000000000..108384950 --- /dev/null +++ b/webapp/tests/components/admin_console/request_button/__snapshots__/request_button.test.jsx.snap @@ -0,0 +1,496 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/admin_console/request_button/request_button.jsx should match snapshot 1`] = ` +<div + className="form-group reload-config" +> + <div + className="col-sm-offset-4 col-sm-8" + > + <div> + <button + className="btn btn-default" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + /> + </button> + </div> + <div + className="help-text" + > + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + /> + </div> + </div> +</div> +`; + +exports[`components/admin_console/request_button/request_button.jsx should match snapshot with request error 1`] = ` +<RequestButton + buttonText={ + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + /> + } + disabled={false} + errorMessage={ + Object { + "defaultMessage": "Error Message: {error}", + "id": "error.message", + } + } + helpText={ + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + /> + } + includeDetailedError={true} + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "textComponent": "span", + } + } + requestAction={[Function]} + saveNeeded={false} + showSuccessMessage={true} + successMessage={ + Object { + "defaultMessage": "Test Successful", + "id": "admin.requestButton.requestSuccess", + } + } +> + <div + className="form-group reload-config" + > + <div + className="col-sm-offset-4 col-sm-8" + > + <div> + <button + className="btn btn-default" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + > + <span> + Button Text + </span> + </FormattedMessage> + </button> + <div> + <div + className="alert alert-warning" + > + <i + className="fa fa-warning" + /> + <FormattedMessage + defaultMessage="Error Message: {error}" + id="error.message" + values={ + Object { + "error": "__message__ - __detailed_error__", + } + } + > + <span> + Error Message: __message__ - __detailed_error__ + </span> + </FormattedMessage> + </div> + </div> + </div> + <div + className="help-text" + > + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + > + <span> + Help Text + </span> + </FormattedMessage> + </div> + </div> + </div> +</RequestButton> +`; + +exports[`components/admin_console/request_button/request_button.jsx should match snapshot with request error 2`] = ` +<RequestButton + buttonText={ + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + /> + } + disabled={false} + errorMessage={ + Object { + "defaultMessage": "Error Message: {error}", + "id": "error.message", + } + } + helpText={ + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + /> + } + includeDetailedError={false} + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "textComponent": "span", + } + } + requestAction={[Function]} + saveNeeded={false} + showSuccessMessage={true} + successMessage={ + Object { + "defaultMessage": "Test Successful", + "id": "admin.requestButton.requestSuccess", + } + } +> + <div + className="form-group reload-config" + > + <div + className="col-sm-offset-4 col-sm-8" + > + <div> + <button + className="btn btn-default" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + > + <span> + Button Text + </span> + </FormattedMessage> + </button> + <div> + <div + className="alert alert-warning" + > + <i + className="fa fa-warning" + /> + <FormattedMessage + defaultMessage="Error Message: {error}" + id="error.message" + values={ + Object { + "error": "__message__", + } + } + > + <span> + Error Message: __message__ + </span> + </FormattedMessage> + </div> + </div> + </div> + <div + className="help-text" + > + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + > + <span> + Help Text + </span> + </FormattedMessage> + </div> + </div> + </div> +</RequestButton> +`; + +exports[`components/admin_console/request_button/request_button.jsx should match snapshot with successMessage 1`] = ` +<RequestButton + buttonText={ + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + /> + } + disabled={false} + errorMessage={ + Object { + "defaultMessage": "Test Failure: {error}", + "id": "admin.requestButton.requestFailure", + } + } + helpText={ + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + /> + } + includeDetailedError={false} + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "textComponent": "span", + } + } + requestAction={[Function]} + saveNeeded={false} + showSuccessMessage={true} + successMessage={ + Object { + "defaultMessage": "Success Message", + "id": "success.message", + } + } +> + <div + className="form-group reload-config" + > + <div + className="col-sm-offset-4 col-sm-8" + > + <div> + <button + className="btn btn-default" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + > + <span> + Button Text + </span> + </FormattedMessage> + </button> + <div> + <div + className="alert alert-success" + > + <i + className="fa fa-success" + /> + <FormattedMessage + defaultMessage="Success Message" + id="success.message" + values={Object {}} + > + <span> + Success Message + </span> + </FormattedMessage> + </div> + </div> + </div> + <div + className="help-text" + > + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + > + <span> + Help Text + </span> + </FormattedMessage> + </div> + </div> + </div> +</RequestButton> +`; + +exports[`components/admin_console/request_button/request_button.jsx should match snapshot with successMessage 2`] = ` +<RequestButton + buttonText={ + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + /> + } + disabled={false} + errorMessage={ + Object { + "defaultMessage": "Test Failure: {error}", + "id": "admin.requestButton.requestFailure", + } + } + helpText={ + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + /> + } + includeDetailedError={false} + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object {}, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "textComponent": "span", + } + } + requestAction={[Function]} + saveNeeded={false} + showSuccessMessage={false} + successMessage={ + Object { + "defaultMessage": "Success Message", + "id": "success.message", + } + } +> + <div + className="form-group reload-config" + > + <div + className="col-sm-offset-4 col-sm-8" + > + <div> + <button + className="btn btn-default" + disabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Button Text" + id="test" + values={Object {}} + > + <span> + Button Text + </span> + </FormattedMessage> + </button> + </div> + <div + className="help-text" + > + <FormattedMessage + defaultMessage="Help Text" + id="test" + values={Object {}} + > + <span> + Help Text + </span> + </FormattedMessage> + </div> + </div> + </div> +</RequestButton> +`; diff --git a/webapp/tests/components/admin_console/request_button/request_button.test.jsx b/webapp/tests/components/admin_console/request_button/request_button.test.jsx new file mode 100644 index 000000000..3b3f4b40b --- /dev/null +++ b/webapp/tests/components/admin_console/request_button/request_button.test.jsx @@ -0,0 +1,215 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {shallow} from 'enzyme'; +import {mountWithIntl} from 'tests/helpers/intl-test-helper.jsx'; + +import RequestButton from 'components/admin_console/request_button/request_button.jsx'; + +describe('components/admin_console/request_button/request_button.jsx', () => { + test('should match snapshot', () => { + const emptyFunction = jest.fn(); + + const wrapper = shallow( + <RequestButton + requestAction={emptyFunction} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should call saveConfig and request actions when saveNeeded is true', () => { + const requestActionSuccess = jest.fn((success) => success()); + const saveConfigActionSuccess = jest.fn((success) => success()); + + const wrapper = mountWithIntl( + <RequestButton + requestAction={requestActionSuccess} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + saveNeeded={false} + saveConfigAction={saveConfigActionSuccess} + /> + ); + + wrapper.find('button').first().simulate('click'); + + expect(requestActionSuccess.mock.calls.length).toBe(1); + expect(saveConfigActionSuccess.mock.calls.length).toBe(0); + }); + + test('should call only request action when saveNeeded is false', () => { + const requestActionSuccess = jest.fn((success) => success()); + const saveConfigActionSuccess = jest.fn((success) => success()); + + const wrapper = mountWithIntl( + <RequestButton + requestAction={requestActionSuccess} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + saveNeeded={true} + saveConfigAction={saveConfigActionSuccess} + /> + ); + + wrapper.find('button').first().simulate('click'); + + expect(requestActionSuccess.mock.calls.length).toBe(1); + expect(saveConfigActionSuccess.mock.calls.length).toBe(1); + }); + + test('should match snapshot with successMessage', () => { + const requestActionSuccess = jest.fn((success) => success()); + + // Success & showSuccessMessage=true + const wrapper1 = mountWithIntl( + <RequestButton + requestAction={requestActionSuccess} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + showSuccessMessage={true} + successMessage={{ + id: 'success.message', + defaultMessage: 'Success Message' + }} + /> + ); + + wrapper1.find('button').first().simulate('click'); + expect(wrapper1).toMatchSnapshot(); + + // Success & showSuccessMessage=false + const wrapper2 = mountWithIntl( + <RequestButton + requestAction={requestActionSuccess} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + showSuccessMessage={false} + successMessage={{ + id: 'success.message', + defaultMessage: 'Success Message' + }} + /> + ); + + wrapper2.find('button').first().simulate('click'); + + expect(wrapper2).toMatchSnapshot(); + }); + + test('should match snapshot with request error', () => { + const requestActionFailure = jest.fn((success, error) => error({ + message: '__message__', + detailed_error: '__detailed_error__' + })); + + // Error & includeDetailedError=true + const wrapper1 = mountWithIntl( + <RequestButton + requestAction={requestActionFailure} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + includeDetailedError={true} + errorMessage={{ + id: 'error.message', + defaultMessage: 'Error Message: {error}' + }} + /> + ); + + wrapper1.find('button').first().simulate('click'); + expect(wrapper1).toMatchSnapshot(); + + // Error & includeDetailedError=false + const wrapper2 = mountWithIntl( + <RequestButton + requestAction={requestActionFailure} + helpText={ + <FormattedMessage + id='test' + defaultMessage='Help Text' + /> + } + buttonText={ + <FormattedMessage + id='test' + defaultMessage='Button Text' + /> + } + errorMessage={{ + id: 'error.message', + defaultMessage: 'Error Message: {error}' + }} + /> + ); + + wrapper2.find('button').first().simulate('click'); + + expect(wrapper2).toMatchSnapshot(); + }); +}); |