diff options
Diffstat (limited to 'webapp')
31 files changed, 925 insertions, 324 deletions
diff --git a/webapp/actions/admin_actions.jsx b/webapp/actions/admin_actions.jsx index fdaeb8732..ac02ac058 100644 --- a/webapp/actions/admin_actions.jsx +++ b/webapp/actions/admin_actions.jsx @@ -383,3 +383,16 @@ export function getPostsPerDayAnalytics(teamId) { export function getUsersPerDayAnalytics(teamId) { AdminActions.getUsersPerDayAnalytics(teamId)(dispatch, getState); } + +export function elasticsearchTest(config, success, error) { + AdminActions.testElasticsearch(config)(dispatch, getState).then( + (data) => { + if (data && success) { + success(data); + } else if (data == null && error) { + const serverError = getState().requests.admin.testElasticsearch.error; + error({id: serverError.server_error_id, ...serverError}); + } + } + ); +} diff --git a/webapp/actions/file_actions.jsx b/webapp/actions/file_actions.jsx index 9a565a07c..1d9617901 100644 --- a/webapp/actions/file_actions.jsx +++ b/webapp/actions/file_actions.jsx @@ -6,6 +6,8 @@ import request from 'superagent'; import store from 'stores/redux_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + import {FileTypes} from 'mattermost-redux/action_types'; import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers'; import {getLogErrorAction} from 'mattermost-redux/actions/errors'; @@ -20,9 +22,9 @@ export function uploadFile(file, name, channelId, clientId, successCallback, err if (res && res.body && res.body.id) { e = res.body; } else if (err.status === 0 || !err.status) { - e = {message: this.translations.connectionError}; + e = {message: Utils.localizeMessage('channel_loader.connection_error', 'There appears to be a problem with your internet connection.')}; } else { - e = {message: this.translations.unknownError + ' (' + err.status + ')'}; + e = {message: Utils.localizeMessage('channel_loader.unknown_error', 'We received an unexpected status code from the server.') + ' (' + err.status + ')'}; } forceLogoutIfNecessary(err, dispatch); diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 1dbbff2f2..9b27ab81e 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -258,6 +258,21 @@ export default class AdminSidebar extends React.Component { /> ); + let elasticSearchSettings = null; + if (window.mm_license.IsLicensed === 'true') { + elasticSearchSettings = ( + <AdminSidebarSection + name='elasticsearch' + title={ + <FormattedMessage + id='admin.sidebar.elasticsearch' + defaultMessage='Elasticsearch' + /> + } + /> + ); + } + return ( <div className='admin-sidebar'> <AdminSidebarHeader/> @@ -618,6 +633,7 @@ export default class AdminSidebar extends React.Component { /> } /> + {elasticSearchSettings} <AdminSidebarSection name='developer' title={ diff --git a/webapp/components/admin_console/elasticsearch_settings.jsx b/webapp/components/admin_console/elasticsearch_settings.jsx new file mode 100644 index 000000000..23ba14b25 --- /dev/null +++ b/webapp/components/admin_console/elasticsearch_settings.jsx @@ -0,0 +1,280 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as Utils from 'utils/utils.jsx'; + +import AdminSettings from './admin_settings.jsx'; +import {elasticsearchTest} from 'actions/admin_actions.jsx'; +import BooleanSetting from './boolean_setting.jsx'; +import {FormattedMessage} from 'react-intl'; +import SettingsGroup from './settings_group.jsx'; +import TextSetting from './text_setting.jsx'; +import RequestButton from './request_button/request_button.jsx'; + +export default class ElasticsearchSettings extends AdminSettings { + constructor(props) { + super(props); + + this.getConfigFromState = this.getConfigFromState.bind(this); + + this.doTestConfig = this.doTestConfig.bind(this); + this.handleChange = this.handleChange.bind(this); + + this.renderSettings = this.renderSettings.bind(this); + } + + getConfigFromState(config) { + config.ElasticSearchSettings.ConnectionUrl = this.state.connectionUrl; + config.ElasticSearchSettings.Username = this.state.username; + config.ElasticSearchSettings.Password = this.state.password; + config.ElasticSearchSettings.Sniff = this.state.sniff; + config.ElasticSearchSettings.EnableIndexing = this.state.enableIndexing; + config.ElasticSearchSettings.EnableSearching = this.state.enableSearching; + + return config; + } + + getStateFromConfig(config) { + return { + connectionUrl: config.ElasticSearchSettings.ConnectionUrl, + username: config.ElasticSearchSettings.Username, + password: config.ElasticSearchSettings.Password, + sniff: config.ElasticSearchSettings.Sniff, + enableIndexing: config.ElasticSearchSettings.EnableIndexing, + enableSearching: config.ElasticSearchSettings.EnableSearching, + configTested: true, + canSave: true + }; + } + + handleChange(id, value) { + if (id === 'enableIndexing') { + if (value === false) { + this.setState({ + enableSearching: false + }); + } else { + this.setState({ + canSave: false, + configTested: false + }); + } + } + + if (id === 'connectionUrl' || id === 'username' || id === 'password' || id === 'sniff') { + this.setState({ + configTested: false, + canSave: false + }); + } + + super.handleChange(id, value); + } + + canSave() { + return this.state.canSave; + } + + doTestConfig(success, error) { + const config = JSON.parse(JSON.stringify(this.props.config)); + this.getConfigFromState(config); + + elasticsearchTest( + config, + () => { + this.setState({ + configTested: true, + canSave: true + }); + success(); + }, + (err) => { + this.setState({ + configTested: false, + canSave: false + }); + error(err); + } + ); + } + + renderTitle() { + return ( + <FormattedMessage + id='admin.elasticsearch.title' + defaultMessage='Elasticsearch Settings' + /> + ); + } + + renderSettings() { + return ( + <SettingsGroup> + <div className='banner'> + <div className='banner__content'> + <FormattedMessage + id='admin.elasticsearch.noteDescription' + defaultMessage='Changing properties in this section will require a server restart before taking effect.' + /> + </div> + </div> + <BooleanSetting + id='enableIndexing' + label={ + <FormattedMessage + id='admin.elasticsearch.enableIndexingTitle' + defaultMessage='Enable Elasticsearch Indexing:' + /> + } + helpText={ + <FormattedMessage + id='admin.elasticsearch.enableIndexingDescription' + defaultMessage='When true, indexing of new posts occurs automatically. Search queries will use database search until "Enable Elasticsearch for search queries" is enabled. {documentationLink}' + values={{ + documentationLink: ( + <a + href='http://www.mattermost.com' + rel='noopener noreferrer' + target='_blank' + > + <FormattedMessage + id='admin.elasticsearch.enableIndexingDescription.documentationLinkText' + defaultMessage='Learn more about Elasticsearch in our documentation.' + /> + </a> + ) + }} + /> + } + value={this.state.enableIndexing} + onChange={this.handleChange} + /> + <TextSetting + id='connectionUrl' + label={ + <FormattedMessage + id='admin.elasticsearch.connectionUrlTitle' + defaultMessage='Server Connection Address:' + /> + } + placeholder={Utils.localizeMessage('admin.elasticsearch.connectionUrlExample', 'E.g.: "https://elasticsearch.example.org:9200"')} + helpText={ + <FormattedMessage + id='admin.elasticsearch.connectionUrlDescription' + defaultMessage='The address of the Elasticsearch server. {documentationLink}' + values={{ + documentationLink: ( + <a + href='http://www.mattermost.com' + rel='noopener noreferrer' + target='_blank' + > + <FormattedMessage + id='admin.elasticsearch.connectionUrlExample.documentationLinkText' + defaultMessage='Please see documentation with server setup instructions.' + /> + </a> + ) + }} + /> + } + value={this.state.connectionUrl} + disabled={!this.state.enableIndexing} + onChange={this.handleChange} + /> + <TextSetting + id='username' + label={ + <FormattedMessage + id='admin.elasticsearch.usernameTitle' + defaultMessage='Server Username:' + /> + } + placeholder={Utils.localizeMessage('admin.elasticsearch.usernameExample', 'E.g.: "elastic"')} + helpText={ + <FormattedMessage + id='admin.elasticsearch.usernameDescription' + defaultMessage='(Optional) The username to authenticate to the Elasticsearch server.' + /> + } + value={this.state.username} + disabled={!this.state.enableIndexing} + onChange={this.handleChange} + /> + <TextSetting + id='password' + label={ + <FormattedMessage + id='admin.elasticsearch.passwordTitle' + defaultMessage='Server Password:' + /> + } + placeholder={Utils.localizeMessage('admin.elasticsearch.password', 'E.g.: "yourpassword"')} + helpText={ + <FormattedMessage + id='admin.elasticsearch.passwordDescription' + defaultMessage='(Optional) The password to authenticate to the Elasticsearch server.' + /> + } + value={this.state.password} + disabled={!this.state.enableIndexing} + onChange={this.handleChange} + /> + <BooleanSetting + id='sniff' + label={ + <FormattedMessage + id='admin.elasticsearch.sniffTitle' + defaultMessage='Enable Cluster Sniffing:' + /> + } + helpText={ + <FormattedMessage + id='admin.elasticsearch.sniffDescription' + defaultMessage='When true, sniffing finds and connects to all data nodes in your cluster automatically.' + /> + } + value={this.state.sniff} + disabled={!this.state.enableIndexing} + onChange={this.handleChange} + /> + <RequestButton + requestAction={this.doTestConfig} + helpText={ + <FormattedMessage + id='admin.elasticsearch.testHelpText' + defaultMessage='Tests if the Mattermost server can connect to the Elasticsearch server specified. Testing the connection does not save the configuration. See log file for more detailed error messages.' + /> + } + buttonText={ + <FormattedMessage + id='admin.elasticsearch.elasticsearch_test_button' + defaultMessage='Test Connection' + /> + } + disabled={!this.state.enableIndexing} + /> + <BooleanSetting + id='enableSearching' + label={ + <FormattedMessage + id='admin.elasticsearch.enableSearchingTitle' + defaultMessage='Enable Elasticsearch for search queries:' + /> + } + helpText={ + <FormattedMessage + id='admin.elasticsearch.enableSearchingDescription' + defaultMessage='Requires a successful connection to the Elasticsearch server. When true, Elasticsearch will be used for all search queries using the latest index. Search results may be incomplete until a bulk index of the existing post database is finished. When false, database search is used.' + /> + } + value={this.state.enableSearching} + disabled={!this.state.enableIndexing || !this.state.configTested} + onChange={this.handleChange} + /> + </SettingsGroup> + ); + } +} diff --git a/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx b/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx index e58a2c43d..4ee3c11cd 100644 --- a/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx +++ b/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx @@ -38,8 +38,8 @@ export default class ManageTeamsDropdown extends React.Component { } toggleDropdown() { - this.setState({ - show: !this.state.show + this.setState((prevState) => { + return {show: !prevState.show}; }); } diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx index c0ce64f8a..3b21f727a 100644 --- a/webapp/components/admin_console/push_settings.jsx +++ b/webapp/components/admin_console/push_settings.jsx @@ -79,8 +79,6 @@ export default class PushSettings extends AdminSettings { agree = true; } else if (config.EmailSettings.PushNotificationServer === Constants.MTPNS) { pushNotificationServerType = PUSH_NOTIFICATIONS_MTPNS; - } else { - pushNotificationServerType = PUSH_NOTIFICATIONS_CUSTOM; } let pushNotificationServer = config.EmailSettings.PushNotificationServer; diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx index 65a71c047..0606c1b31 100644 --- a/webapp/components/file_preview.jsx +++ b/webapp/components/file_preview.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import * as Utils from 'utils/utils.jsx'; -import {getFileUrl} from 'mattermost-redux/utils/file_utils'; +import {getFileThumbnailUrl} from 'mattermost-redux/utils/file_utils'; import PropTypes from 'prop-types'; @@ -39,7 +39,7 @@ export default class FilePreview extends React.Component { previewImage = ( <img className='file-preview__image' - src={getFileUrl(info.id)} + src={getFileThumbnailUrl(info.id)} /> ); } else { diff --git a/webapp/components/integrations/components/abstract_outgoing_webhook.jsx b/webapp/components/integrations/components/abstract_outgoing_webhook.jsx index 912ad3bdf..397423395 100644 --- a/webapp/components/integrations/components/abstract_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/abstract_outgoing_webhook.jsx @@ -16,61 +16,81 @@ import {Link} from 'react-router/es6'; import SpinnerButton from 'components/spinner_button.jsx'; export default class AbstractOutgoingWebhook extends React.Component { - static get propTypes() { - return { - team: PropTypes.object - }; + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The header text to render, has id and defaultMessage + */ + header: PropTypes.object.isRequired, + + /** + * The footer text to render, has id and defaultMessage + */ + footer: PropTypes.object.isRequired, + + /** + * Any extra component/node to render + */ + renderExtra: PropTypes.node.isRequired, + + /** + * The server error text after a failed action + */ + serverError: PropTypes.string.isRequired, + + /** + * The hook used to set the initial state + */ + initialHook: PropTypes.object, + + /** + * The async function to run when the action button is pressed + */ + action: PropTypes.func.isRequired } constructor(props) { super(props); - this.handleSubmit = this.handleSubmit.bind(this); - - this.updateDisplayName = this.updateDisplayName.bind(this); - this.updateDescription = this.updateDescription.bind(this); - this.updateContentType = this.updateContentType.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - this.updateTriggerWords = this.updateTriggerWords.bind(this); - this.updateTriggerWhen = this.updateTriggerWhen.bind(this); - this.updateCallbackUrls = this.updateCallbackUrls.bind(this); - - this.state = { - displayName: '', - description: '', - contentType: 'application/x-www-form-urlencoded', - channelId: '', - triggerWords: '', - triggerWhen: 0, - callbackUrls: '', - saving: false, - serverError: '', - clientError: null - }; - - if (typeof this.performAction === 'undefined') { - throw new TypeError('Subclasses must override performAction'); - } - - if (typeof this.header === 'undefined') { - throw new TypeError('Subclasses must override header'); - } + this.state = this.getStateFromHook(this.props.initialHook || {}); + } - if (typeof this.footer === 'undefined') { - throw new TypeError('Subclasses must override footer'); + getStateFromHook = (hook) => { + let triggerWords = ''; + if (hook.trigger_words) { + let i = 0; + for (i = 0; i < hook.trigger_words.length; i++) { + triggerWords += hook.trigger_words[i] + '\n'; + } } - if (typeof this.renderExtra === 'undefined') { - throw new TypeError('Subclasses must override renderExtra'); + let callbackUrls = ''; + if (hook.callback_urls) { + let i = 0; + for (i = 0; i < hook.callback_urls.length; i++) { + callbackUrls += hook.callback_urls[i] + '\n'; + } } - this.performAction = this.performAction.bind(this); - this.header = this.header.bind(this); - this.footer = this.footer.bind(this); - this.renderExtra = this.renderExtra.bind(this); + return { + displayName: hook.display_name || '', + description: hook.description || '', + contentType: hook.content_type || 'application/x-www-form-urlencoded', + channelId: hook.channel_id || '', + triggerWords, + triggerWhen: hook.trigger_when || 0, + callbackUrls, + saving: false, + clientError: null + }; } - handleSubmit(e) { + handleSubmit = (e) => { e.preventDefault(); if (this.state.saving) { @@ -79,7 +99,6 @@ export default class AbstractOutgoingWebhook extends React.Component { this.setState({ saving: true, - serverError: '', clientError: '' }); @@ -142,46 +161,46 @@ export default class AbstractOutgoingWebhook extends React.Component { description: this.state.description }; - this.performAction(hook); + this.props.action(hook).then(() => this.setState({saving: false})); } - updateDisplayName(e) { + updateDisplayName = (e) => { this.setState({ displayName: e.target.value }); } - updateDescription(e) { + updateDescription = (e) => { this.setState({ description: e.target.value }); } - updateContentType(e) { + updateContentType = (e) => { this.setState({ contentType: e.target.value }); } - updateChannelId(e) { + updateChannelId = (e) => { this.setState({ channelId: e.target.value }); } - updateTriggerWords(e) { + updateTriggerWords = (e) => { this.setState({ triggerWords: e.target.value }); } - updateTriggerWhen(e) { + updateTriggerWhen = (e) => { this.setState({ triggerWhen: e.target.value }); } - updateCallbackUrls(e) { + updateCallbackUrls = (e) => { this.setState({ callbackUrls: e.target.value }); @@ -191,9 +210,9 @@ export default class AbstractOutgoingWebhook extends React.Component { const contentTypeOption1 = 'application/x-www-form-urlencoded'; const contentTypeOption2 = 'application/json'; - var headerToRender = this.header(); - var footerToRender = this.footer(); - var renderExtra = this.renderExtra(); + var headerToRender = this.props.header; + var footerToRender = this.props.footer; + var renderExtra = this.props.renderExtra; return ( <div className='backstage-content'> @@ -432,7 +451,7 @@ export default class AbstractOutgoingWebhook extends React.Component { <div className='backstage-form__footer'> <FormError type='backstage' - errors={[this.state.serverError, this.state.clientError]} + errors={[this.props.serverError, this.state.clientError]} /> <Link className='btn btn-sm' diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx deleted file mode 100644 index d7f338587..000000000 --- a/webapp/components/integrations/components/add_outgoing_webhook.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {addOutgoingHook} from 'actions/integration_actions.jsx'; -import {browserHistory} from 'react-router/es6'; - -import AbstractOutgoingWebhook from './abstract_outgoing_webhook.jsx'; - -export default class AddOutgoingWebhook extends AbstractOutgoingWebhook { - performAction(hook) { - addOutgoingHook( - hook, - (data) => { - browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=outgoing_webhooks&id=${data.id}`); - }, - (err) => { - this.setState({ - saving: false, - serverError: err.message - }); - } - ); - } - - header() { - return {id: 'integrations.add', defaultMessage: 'Add'}; - } - - footer() { - return {id: 'add_outgoing_webhook.save', defaultMessage: 'Save'}; - } - - renderExtra() { - return ''; - } -} diff --git a/webapp/components/integrations/components/add_outgoing_webhook/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook/add_outgoing_webhook.jsx new file mode 100644 index 000000000..41ab8a073 --- /dev/null +++ b/webapp/components/integrations/components/add_outgoing_webhook/add_outgoing_webhook.jsx @@ -0,0 +1,69 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AbstractOutgoingWebhook from 'components/integrations/components/abstract_outgoing_webhook.jsx'; + +import React from 'react'; +import {browserHistory} from 'react-router/es6'; +import PropTypes from 'prop-types'; + +const HEADER = {id: 'integrations.add', defaultMessage: 'Add'}; +const FOOTER = {id: 'add_outgoing_webhook.save', defaultMessage: 'Save'}; + +export default class AddOutgoingWebhook extends React.PureComponent { + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The request state for createOutgoingHook action. Contains status and error + */ + createOutgoingHookRequest: PropTypes.object.isRequired, + + actions: PropTypes.shape({ + + /** + * The function to call to add a new outgoing webhook + */ + createOutgoingHook: PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.state = { + serverError: '' + }; + } + + addOutgoingHook = async (hook) => { + this.setState({serverError: ''}); + + const data = await this.props.actions.createOutgoingHook(hook); + if (data) { + browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=outgoing_webhooks&id=${data.id}`); + return; + } + + if (this.props.createOutgoingHookRequest.error) { + this.setState({serverError: this.props.createOutgoingHookRequest.error.message}); + } + } + + render() { + return ( + <AbstractOutgoingWebhook + team={this.props.team} + header={HEADER} + footer={FOOTER} + renderExtra={''} + action={this.addOutgoingHook} + serverError={this.state.serverError} + /> + ); + } +} diff --git a/webapp/components/integrations/components/add_outgoing_webhook/index.js b/webapp/components/integrations/components/add_outgoing_webhook/index.js new file mode 100644 index 000000000..f930ac81f --- /dev/null +++ b/webapp/components/integrations/components/add_outgoing_webhook/index.js @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {createOutgoingHook} from 'mattermost-redux/actions/integrations'; + +import AddOutgoingWebhook from './add_outgoing_webhook.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + createOutgoingHookRequest: state.requests.integrations.createOutgoingHook + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + createOutgoingHook + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(AddOutgoingWebhook); diff --git a/webapp/components/integrations/components/edit_outgoing_webhook.jsx b/webapp/components/integrations/components/edit_outgoing_webhook.jsx deleted file mode 100644 index 2f56d1eae..000000000 --- a/webapp/components/integrations/components/edit_outgoing_webhook.jsx +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import {browserHistory} from 'react-router/es6'; -import IntegrationStore from 'stores/integration_store.jsx'; -import {loadOutgoingHooks, updateOutgoingHook} from 'actions/integration_actions.jsx'; - -import AbstractOutgoingWebhook from './abstract_outgoing_webhook.jsx'; -import ConfirmModal from 'components/confirm_modal.jsx'; -import {FormattedMessage} from 'react-intl'; -import TeamStore from 'stores/team_store.jsx'; - -export default class EditOutgoingWebhook extends AbstractOutgoingWebhook { - constructor(props) { - super(props); - - this.handleIntegrationChange = this.handleIntegrationChange.bind(this); - this.handleConfirmModal = this.handleConfirmModal.bind(this); - this.handleUpdate = this.handleUpdate.bind(this); - this.submitCommand = this.submitCommand.bind(this); - this.confirmModalDismissed = this.confirmModalDismissed.bind(this); - this.originalOutgoingHook = null; - - this.state = { - showConfirmModal: false - }; - } - - componentDidMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableOutgoingWebhooks === 'true') { - loadOutgoingHooks(); - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const teamId = TeamStore.getCurrentId(); - - const hooks = IntegrationStore.getOutgoingWebhooks(teamId); - const loading = !IntegrationStore.hasReceivedOutgoingWebhooks(teamId); - - if (!loading) { - this.originalOutgoingHook = hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; - - var triggerWords = ''; - if (this.originalOutgoingHook.trigger_words) { - let i = 0; - for (i = 0; i < this.originalOutgoingHook.trigger_words.length; i++) { - triggerWords += this.originalOutgoingHook.trigger_words[i] + '\n'; - } - } - - var callbackUrls = ''; - if (this.originalOutgoingHook.callback_urls) { - let i = 0; - for (i = 0; i < this.originalOutgoingHook.callback_urls.length; i++) { - callbackUrls += this.originalOutgoingHook.callback_urls[i] + '\n'; - } - } - - this.setState({ - displayName: this.originalOutgoingHook.display_name, - description: this.originalOutgoingHook.description, - channelId: this.originalOutgoingHook.channel_id, - contentType: this.originalOutgoingHook.content_type, - triggerWhen: this.originalOutgoingHook.trigger_when, - triggerWords, - callbackUrls - }); - } - } - - performAction(hook) { - this.newHook = hook; - - if (this.originalOutgoingHook.id) { - hook.id = this.originalOutgoingHook.id; - } - - if (this.originalOutgoingHook.token) { - hook.token = this.originalOutgoingHook.token; - } - - var triggerWordsSame = (this.originalOutgoingHook.trigger_words.length === hook.trigger_words.length) && - this.originalOutgoingHook.trigger_words.every((v, i) => v === hook.trigger_words[i]); - - var callbackUrlsSame = (this.originalOutgoingHook.callback_urls.length === hook.callback_urls.length) && - this.originalOutgoingHook.callback_urls.every((v, i) => v === hook.callback_urls[i]); - - if (this.originalOutgoingHook.content_type !== hook.content_type || - !triggerWordsSame || !callbackUrlsSame) { - this.handleConfirmModal(); - this.setState({ - saving: false - }); - } else { - this.submitCommand(); - } - } - - handleUpdate() { - this.setState({ - saving: true, - serverError: '', - clientError: '' - }); - - this.submitCommand(); - } - - handleConfirmModal() { - this.setState({showConfirmModal: true}); - } - - confirmModalDismissed() { - this.setState({showConfirmModal: false}); - } - - submitCommand() { - updateOutgoingHook( - this.newHook, - () => { - browserHistory.push(`/${this.props.team.name}/integrations/outgoing_webhooks`); - }, - (err) => { - this.setState({ - saving: false, - showConfirmModal: false, - serverError: err.message - }); - } - ); - } - - header() { - return {id: 'integrations.edit', defaultMessage: 'Edit'}; - } - - footer() { - return {id: 'update_outgoing_webhook.update', defaultMessage: 'Update'}; - } - - renderExtra() { - const confirmButton = ( - <FormattedMessage - id='update_outgoing_webhook.update' - defaultMessage='Update' - /> - ); - - const confirmTitle = ( - <FormattedMessage - id='update_outgoing_webhook.confirm' - defaultMessage='Edit Outgoing Webhook' - /> - ); - - const confirmMessage = ( - <FormattedMessage - id='update_outgoing_webhook.question' - defaultMessage='Your changes may break the existing outgoing webhook. Are you sure you would like to update it?' - /> - ); - - return ( - <ConfirmModal - title={confirmTitle} - message={confirmMessage} - confirmButtonText={confirmButton} - show={this.state.showConfirmModal} - onConfirm={this.handleUpdate} - onCancel={this.confirmModalDismissed} - /> - ); - } -} diff --git a/webapp/components/integrations/components/edit_outgoing_webhook/edit_outgoing_webhook.jsx b/webapp/components/integrations/components/edit_outgoing_webhook/edit_outgoing_webhook.jsx new file mode 100644 index 000000000..9b2dbff0a --- /dev/null +++ b/webapp/components/integrations/components/edit_outgoing_webhook/edit_outgoing_webhook.jsx @@ -0,0 +1,169 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AbstractOutgoingWebhook from 'components/integrations/components/abstract_outgoing_webhook.jsx'; +import ConfirmModal from 'components/confirm_modal.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import {browserHistory} from 'react-router/es6'; +import {FormattedMessage} from 'react-intl'; + +const HEADER = {id: 'integrations.edit', defaultMessage: 'Edit'}; +const FOOTER = {id: 'update_outgoing_webhook.update', defaultMessage: 'Update'}; + +export default class EditOutgoingWebhook extends React.PureComponent { + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The outgoing webhook to edit + */ + hook: PropTypes.object, + + /** + * The id of the outgoing webhook to edit + */ + hookId: PropTypes.string.isRequired, + + /** + * The request state for updateOutgoingHook action. Contains status and error + */ + updateOutgoingHookRequest: PropTypes.object.isRequired, + + actions: PropTypes.shape({ + + /** + * The function to call to update an outgoing webhook + */ + updateOutgoingHook: PropTypes.func.isRequired, + + /** + * The function to call to get an outgoing webhook + */ + getOutgoingHook: PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.state = { + showConfirmModal: false, + serverError: '' + }; + } + + componentDidMount() { + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + this.props.actions.getOutgoingHook(this.props.hookId); + } + } + + editOutgoingHook = async (hook) => { + this.newHook = hook; + + if (this.props.hook.id) { + hook.id = this.props.hook.id; + } + + if (this.props.hook.token) { + hook.token = this.props.hook.token; + } + + const triggerWordsSame = (this.props.hook.trigger_words.length === hook.trigger_words.length) && + this.props.hook.trigger_words.every((v, i) => v === hook.trigger_words[i]); + + const callbackUrlsSame = (this.props.hook.callback_urls.length === hook.callback_urls.length) && + this.props.hook.callback_urls.every((v, i) => v === hook.callback_urls[i]); + + if (this.props.hook.content_type !== hook.content_type || + !triggerWordsSame || !callbackUrlsSame) { + this.handleConfirmModal(); + } else { + await this.submitHook(); + } + } + + handleConfirmModal = () => { + this.setState({showConfirmModal: true}); + } + + confirmModalDismissed = () => { + this.setState({showConfirmModal: false}); + } + + submitHook = async () => { + this.setState({serverError: ''}); + + const data = await this.props.actions.updateOutgoingHook(this.newHook); + + if (data) { + browserHistory.push(`/${this.props.team.name}/integrations/outgoing_webhooks`); + return; + } + + this.setState({showConfirmModal: false}); + + if (this.props.updateOutgoingHookRequest.error) { + this.setState({serverError: this.props.updateOutgoingHookRequest.error.message}); + } + } + + renderExtra = () => { + const confirmButton = ( + <FormattedMessage + id='update_outgoing_webhook.update' + defaultMessage='Update' + /> + ); + + const confirmTitle = ( + <FormattedMessage + id='update_outgoing_webhook.confirm' + defaultMessage='Edit Outgoing Webhook' + /> + ); + + const confirmMessage = ( + <FormattedMessage + id='update_outgoing_webhook.question' + defaultMessage='Your changes may break the existing outgoing webhook. Are you sure you would like to update it?' + /> + ); + + return ( + <ConfirmModal + title={confirmTitle} + message={confirmMessage} + confirmButtonText={confirmButton} + show={this.state.showConfirmModal} + onConfirm={this.submitHook} + onCancel={this.confirmModalDismissed} + /> + ); + } + + render() { + if (!this.props.hook) { + return <LoadingScreen/>; + } + + return ( + <AbstractOutgoingWebhook + team={this.props.team} + header={HEADER} + footer={FOOTER} + renderExtra={this.renderExtra()} + action={this.editOutgoingHook} + serverError={this.state.serverError} + initialHook={this.props.hook} + /> + ); + } +} diff --git a/webapp/components/integrations/components/edit_outgoing_webhook/index.js b/webapp/components/integrations/components/edit_outgoing_webhook/index.js new file mode 100644 index 000000000..a526ac76c --- /dev/null +++ b/webapp/components/integrations/components/edit_outgoing_webhook/index.js @@ -0,0 +1,30 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {updateOutgoingHook, getOutgoingHook} from 'mattermost-redux/actions/integrations'; + +import EditOutgoingWebhook from './edit_outgoing_webhook.jsx'; + +function mapStateToProps(state, ownProps) { + const hookId = ownProps.location.query.id; + + return { + ...ownProps, + hookId, + hook: state.entities.integrations.outgoingHooks[hookId], + updateOutgoingHookRequest: state.requests.integrations.createOutgoingHook + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + updateOutgoingHook, + getOutgoingHook + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(EditOutgoingWebhook); diff --git a/webapp/components/post_view/post_attachment.jsx b/webapp/components/post_view/post_attachment.jsx index b7bd1ade9..d7b1ee774 100644 --- a/webapp/components/post_view/post_attachment.jsx +++ b/webapp/components/post_view/post_attachment.jsx @@ -55,10 +55,11 @@ export default class PostAttachment extends React.PureComponent { toggleCollapseState(e) { e.preventDefault(); - - this.setState({ - text: this.state.collapsed ? this.state.uncollapsedText : this.state.collapsedText, - collapsed: !this.state.collapsed + this.setState((prevState) => { + return { + text: prevState.collapsed ? prevState.uncollapsedText : prevState.collapsedText, + collapsed: !prevState.collapsed + }; }); } diff --git a/webapp/components/post_view/post_body_additional_content.jsx b/webapp/components/post_view/post_body_additional_content.jsx index 485e63967..be9e37827 100644 --- a/webapp/components/post_view/post_body_additional_content.jsx +++ b/webapp/components/post_view/post_body_additional_content.jsx @@ -55,14 +55,18 @@ export default class PostBodyAdditionalContent extends React.PureComponent { } componentWillReceiveProps(nextProps) { - this.setState({ - embedVisible: nextProps.previewCollapsed.startsWith('false'), - link: Utils.extractFirstLink(nextProps.post.message) - }); + if (nextProps.previewCollapsed !== this.props.previewCollapsed || nextProps.post.message !== this.props.post.message) { + this.setState({ + embedVisible: nextProps.previewCollapsed.startsWith('false'), + link: Utils.extractFirstLink(nextProps.post.message) + }); + } } toggleEmbedVisibility() { - this.setState({embedVisible: !this.state.embedVisible}); + this.setState((prevState) => { + return {embedVisible: !prevState.embedVisible}; + }); } getSlackAttachment() { diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx index faa463cc7..ec6dfbd20 100644 --- a/webapp/components/setting_picture.jsx +++ b/webapp/components/setting_picture.jsx @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import React, {Component} from 'react'; import {FormattedMessage} from 'react-intl'; +import exif2css from 'exif2css'; import FormError from 'components/form_error.jsx'; import loadingGif from 'images/load.gif'; @@ -41,26 +42,89 @@ export default class SettingPicture extends Component { } } + componentWillUnmount() { + if (this.previewBlob) { + URL.revokeObjectURL(this.previewBlob); + } + } + setPicture = (file) => { if (file) { - var reader = new FileReader(); + this.previewBlob = URL.createObjectURL(file); + var reader = new FileReader(); reader.onload = (e) => { + const orientation = this.getExifOrientation(e.target.result); + const orientationStyles = this.getOrientationStyles(orientation); + this.setState({ - image: e.target.result + image: this.previewBlob, + orientationStyles }); }; - reader.readAsDataURL(file); + reader.readAsArrayBuffer(file); + } + } + + // based on https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603 + getExifOrientation(data) { + var view = new DataView(data); + + if (view.getUint16(0, false) !== 0xFFD8) { + return -2; + } + + var length = view.byteLength; + var offset = 2; + + while (offset < length) { + var marker = view.getUint16(offset, false); + offset += 2; + + if (marker === 0xFFE1) { + if (view.getUint32(offset += 2, false) !== 0x45786966) { + return -1; + } + + var little = view.getUint16(offset += 6, false) === 0x4949; + offset += view.getUint32(offset + 4, little); + var tags = view.getUint16(offset, little); + offset += 2; + + for (var i = 0; i < tags; i++) { + if (view.getUint16(offset + (i * 12), little) === 0x0112) { + return view.getUint16(offset + (i * 12) + 8, little); + } + } + } else if ((marker & 0xFF00) === 0xFF00) { + offset += view.getUint16(offset, false); + } else { + break; + } } + return -1; + } + + getOrientationStyles(orientation) { + const { + transform, + 'transform-origin': transformOrigin + } = exif2css(orientation); + return {transform, transformOrigin}; } render() { let img; if (this.props.file) { + const imageStyles = { + backgroundImage: 'url(' + this.state.image + ')', + ...this.state.orientationStyles + }; + img = ( <div className='profile-img-preview' - style={{backgroundImage: 'url(' + this.state.image + ')'}} + style={imageStyles} /> ); } else { diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 16c96f1b6..798ce5691 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -309,14 +309,13 @@ export default class Sidebar extends React.Component { curIndex = i; } } - let nextChannel = allChannels[curIndex]; let nextIndex = curIndex; if (e.keyCode === Constants.KeyCodes.DOWN) { nextIndex = curIndex + 1; } else if (e.keyCode === Constants.KeyCodes.UP) { nextIndex = curIndex - 1; } - nextChannel = allChannels[Utils.mod(nextIndex, allChannels.length)]; + const nextChannel = allChannels[Utils.mod(nextIndex, allChannels.length)]; ChannelActions.goToChannel(nextChannel); this.updateScrollbarOnChannelChange(nextChannel); this.isSwitchingChannel = false; @@ -342,7 +341,6 @@ export default class Sidebar extends React.Component { curIndex = i; } } - let nextChannel = allChannels[curIndex]; let nextIndex = curIndex; let count = 0; let increment = 0; @@ -359,7 +357,7 @@ export default class Sidebar extends React.Component { unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id); } if (unreadCounts.msgs !== 0 || unreadCounts.mentions !== 0) { - nextChannel = allChannels[nextIndex]; + const nextChannel = allChannels[nextIndex]; ChannelActions.goToChannel(nextChannel); this.updateScrollbarOnChannelChange(nextChannel); } diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx index 25d2c25bd..872439eda 100644 --- a/webapp/components/signup/components/signup_email.jsx +++ b/webapp/components/signup/components/signup_email.jsx @@ -52,9 +52,9 @@ export default class SignupEmail extends React.Component { let teamDisplayName = ''; let teamName = ''; let teamId = ''; - let loading = true; - let serverError = ''; - let noOpenServerError = false; + let loading = false; + const serverError = ''; + const noOpenServerError = false; if (hash && hash.length > 0) { const parsedData = JSON.parse(data); @@ -62,37 +62,40 @@ export default class SignupEmail extends React.Component { teamDisplayName = parsedData.display_name; teamName = parsedData.name; teamId = parsedData.id; - loading = false; } else if (inviteId && inviteId.length > 0) { loading = true; getInviteInfo( inviteId, (inviteData) => { if (!inviteData) { + this.setState({loading: false}); return; } - serverError = ''; - teamDisplayName = inviteData.display_name; - teamName = inviteData.name; - teamId = inviteData.id; + this.setState({ + loading: false, + serverError: '', + teamDisplayName: inviteData.display_name, + teamName: inviteData.name, + teamId: inviteData.id + }); }, () => { - noOpenServerError = true; - serverError = ( - <FormattedMessage - id='signup_user_completed.invalid_invite' - defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.' - /> - ); + this.setState({ + loading: false, + noOpenServerError: true, + serverError: ( + <FormattedMessage + id='signup_user_completed.invalid_invite' + defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.' + /> + ) + }); } ); - loading = false; data = null; hash = null; - } else { - loading = false; } return { diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index 55980d331..536b1a115 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -157,7 +157,9 @@ export default class Textbox extends React.Component { showPreview(e) { e.preventDefault(); e.target.blur(); - this.setState({preview: !this.state.preview}); + this.setState((prevState) => { + return {preview: prevState.preview}; + }); } hidePreview() { diff --git a/webapp/components/webrtc/components/webrtc_sidebar.jsx b/webapp/components/webrtc/components/webrtc_sidebar.jsx index c207ab489..82ac2d98a 100644 --- a/webapp/components/webrtc/components/webrtc_sidebar.jsx +++ b/webapp/components/webrtc/components/webrtc_sidebar.jsx @@ -76,7 +76,9 @@ export default class SidebarRight extends React.Component { if (e) { e.preventDefault(); } - this.setState({expanded: !this.state.expanded}); + this.setState((prevState) => { + return {expanded: !prevState.expanded}; + }); } onInitializeVideoCall(userId, isCaller) { diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 87ec7486b..4a0f41a25 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -235,6 +235,27 @@ "admin.customization.support": "Legal and Support", "admin.database.title": "Database Settings", "admin.developer.title": "Developer Settings", + "admin.elasticsearch.title": "Elasticsearch Settings", + "admin.elasticsearch.noteDescription": "Changing properties in this section will require a server restart before taking effect.", + "admin.elasticsearch.enableIndexingTitle": "Enable Elasticsearch Indexing:", + "admin.elasticsearch.enableIndexingDescription": "When true, indexing of new posts occurs automatically. Search queries will use database search until \"Enable Elasticsearch for search queries\" is enabled. {documentationLink}", + "admin.elasticsearch.enableIndexingDescription.documentationLinkText": "Learn more about Elasticsearch in our documentation.", + "admin.elasticsearch.connectionUrlTitle": "Server Connection Address:", + "admin.elasticsearch.connectionUrlDescription": "The address of the Elasticsearch server. {documentationLink}", + "admin.elasticsearch.connectionUrlExample.documentationLinkText": "Please see documentation with server setup instructions.", + "admin.elasticsearch.usernameTitle": "Server Username:", + "admin.elasticsearch.usernameDescription": "(Optional) The username to authenticate to the Elasticsearch server.", + "admin.elasticsearch.passwordTitle": "Server Password:", + "admin.elasticsearch.passwordDescription": "(Optional) The password to authenticate to the Elasticsearch server.", + "admin.elasticsearch.sniffTitle": "Enable Cluster Sniffing:", + "admin.elasticsearch.sniffDescription": "When true, sniffing finds and connects to all data nodes in your cluster automatically.", + "admin.elasticsearch.enableSearchingTitle": "Enable Elasticsearch for search queries:", + "admin.elasticsearch.enableSearchingDescription": "Requires a successful connection to the Elasticsearch server. When true, Elasticsearch will be used for all search queries using the latest index. Search results may be incomplete until a bulk index of the existing post database is finished. When false, database search is used.", + "admin.elasticsearch.connectionUrlExample": "E.g.: \"https://elasticsearch.example.org:9200\"", + "admin.elasticsearch.usernameExample": "E.g.: \"elastic\"", + "admin.elasticsearch.password": "E.g.: \"yourpassword\"", + "admin.elasticsearch.testHelpText": "Tests if the Mattermost server can connect to the Elasticsearch server specified. Testing the connection does not save the configuration. See log file for more detailed error messages.", + "admin.elasticsearch.elasticsearch_test_button": "Test Connection", "admin.email.agreeHPNS": " I understand and accept the Mattermost Hosted Push Notification Service <a href=\"https://about.mattermost.com/hpns-terms/\" target='_blank'>Terms of Service</a> and <a href=\"https://about.mattermost.com/hpns-privacy/\" target='_blank'>Privacy Policy</a>.", "admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.", "admin.email.allowEmailSignInTitle": "Enable sign-in with email: ", diff --git a/webapp/package.json b/webapp/package.json index 4870c5ce8..c083b80f4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -11,6 +11,7 @@ "bootstrap-colorpicker": "2.5.1", "chart.js": "2.5.0", "compass-mixins": "0.12.10", + "exif2css": "1.2.0", "fastclick": "1.0.6", "flux": "3.1.2", "font-awesome": "4.7.0", diff --git a/webapp/routes/route_admin_console.jsx b/webapp/routes/route_admin_console.jsx index b0b6ebf62..17e0290c2 100644 --- a/webapp/routes/route_admin_console.jsx +++ b/webapp/routes/route_admin_console.jsx @@ -45,6 +45,7 @@ import TeamAnalytics from 'components/analytics/team_analytics'; import LicenseSettings from 'components/admin_console/license_settings.jsx'; import Audits from 'components/admin_console/audits'; import Logs from 'components/admin_console/server_logs'; +import ElasticsearchSettings from 'components/admin_console/elasticsearch_settings.jsx'; export default ( <Route> @@ -200,6 +201,10 @@ export default ( component={DatabaseSettings} /> <Route + path='elasticsearch' + component={ElasticsearchSettings} + /> + <Route path='developer' component={DeveloperSettings} /> diff --git a/webapp/routes/route_integrations.jsx b/webapp/routes/route_integrations.jsx index dd3ebe663..37b33ed40 100644 --- a/webapp/routes/route_integrations.jsx +++ b/webapp/routes/route_integrations.jsx @@ -47,13 +47,13 @@ export default { { path: 'add', getComponents: (location, callback) => { - System.import('components/integrations/components/add_outgoing_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + System.import('components/integrations/components/add_outgoing_webhook').then(RouteUtils.importComponentSuccess(callback)); } }, { path: 'edit', getComponents: (location, callback) => { - System.import('components/integrations/components/edit_outgoing_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + System.import('components/integrations/components/edit_outgoing_webhook').then(RouteUtils.importComponentSuccess(callback)); } } ] diff --git a/webapp/tests/components/integrations/__snapshots__/add_outgoing_hook.test.jsx.snap b/webapp/tests/components/integrations/__snapshots__/add_outgoing_hook.test.jsx.snap new file mode 100644 index 000000000..a55f5db5e --- /dev/null +++ b/webapp/tests/components/integrations/__snapshots__/add_outgoing_hook.test.jsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/integrations/AddOutgoingWebhook should match snapshot 1`] = ` +<AbstractOutgoingWebhook + action={[Function]} + footer={ + Object { + "defaultMessage": "Save", + "id": "add_outgoing_webhook.save", + } + } + header={ + Object { + "defaultMessage": "Add", + "id": "integrations.add", + } + } + renderExtra="" + serverError="" + team={ + Object { + "id": "testteamid", + "name": "test", + } + } +/> +`; diff --git a/webapp/tests/components/integrations/__snapshots__/edit_outgoing_hook.test.jsx.snap b/webapp/tests/components/integrations/__snapshots__/edit_outgoing_hook.test.jsx.snap new file mode 100644 index 000000000..d7656b08f --- /dev/null +++ b/webapp/tests/components/integrations/__snapshots__/edit_outgoing_hook.test.jsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/integrations/EditOutgoingWebhook should match snapshot 1`] = ` +<LoadingScreen + position="relative" +/> +`; diff --git a/webapp/tests/components/integrations/add_outgoing_hook.test.jsx b/webapp/tests/components/integrations/add_outgoing_hook.test.jsx new file mode 100644 index 000000000..0c92a7c83 --- /dev/null +++ b/webapp/tests/components/integrations/add_outgoing_hook.test.jsx @@ -0,0 +1,29 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import AddOutgoingWebhook from 'components/integrations/components/add_outgoing_webhook/add_outgoing_webhook.jsx'; + +describe('components/integrations/AddOutgoingWebhook', () => { + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + const teamId = 'testteamid'; + + const wrapper = shallow( + <AddOutgoingWebhook + team={{ + id: teamId, + name: 'test' + }} + createOutgoingHookRequest={{ + status: 'not_started', + error: null + }} + actions={{createOutgoingHook: emptyFunction}} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/webapp/tests/components/integrations/edit_outgoing_hook.test.jsx b/webapp/tests/components/integrations/edit_outgoing_hook.test.jsx new file mode 100644 index 000000000..c2a5020a6 --- /dev/null +++ b/webapp/tests/components/integrations/edit_outgoing_hook.test.jsx @@ -0,0 +1,31 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import EditOutgoingWebhook from 'components/integrations/components/edit_outgoing_webhook/edit_outgoing_webhook.jsx'; + +describe('components/integrations/EditOutgoingWebhook', () => { + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + const teamId = 'testteamid'; + + const wrapper = shallow( + <EditOutgoingWebhook + team={{ + id: teamId, + name: 'test' + }} + hookId={'somehookid'} + updateOutgoingHookRequest={{ + status: 'not_started', + error: null + }} + actions={{updateOutgoingHook: emptyFunction, getOutgoingHook: emptyFunction}} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); + diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 83e538605..b14bdaf11 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -656,14 +656,14 @@ export function applyTheme(theme) { changeCss('@media(min-width: 768px){.app__body .post:hover, .app__body .more-modal__list .more-modal__row:hover, .app__body .modal .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.08)); changeCss('.app__body .more-modal__row.more-modal__row--selected, .app__body .date-separator.hovered--before:after, .app__body .date-separator.hovered--after:before, .app__body .new-separator.hovered--after:before, .app__body .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07)); changeCss('@media(min-width: 768px){.app__body .suggestion-list__content .command:hover, .app__body .mentions__name:hover, .app__body .dropdown-menu>li>a:focus, .app__body .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15)); - changeCss('.app__body .suggestion--selected, .app__body .emoticon-suggestion:hover, .app__body .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); + changeCss('.app__body .suggestion--selected, .app__body .emoticon-suggestion:hover, .app__body .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15)); changeCss('code, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1)); changeCss('@media(min-width: 960px){.app__body .post.current--user:hover .post__body ', 'background: none;'); changeCss('.app__body .sidebar--right', 'color:' + theme.centerChannelColor); changeCss('.app__body .search-help-popover .search-autocomplete__item:hover, .app__body .modal .settings-modal .settings-table .settings-content .appearance-section .theme-elements__body', 'background:' + changeOpacity(theme.centerChannelColor, 0.05)); changeCss('.app__body .search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.centerChannelColor, 0.15)); if (!UserAgent.isFirefox() && !UserAgent.isInternetExplorer() && !UserAgent.isEdge()) { - changeCss('body.app__body ::-webkit-scrollbar-thumb', 'background:' + changeOpacity(theme.centerChannelColor, 0.4), 1); + changeCss('body.app__body ::-webkit-scrollbar-thumb', 'background:' + changeOpacity(theme.centerChannelColor, 0.4)); } changeCss('body', 'scrollbar-arrow-color:' + theme.centerChannelColor); changeCss('.app__body .post-create__container .post-create-body .btn-file svg, .app__body .post.post--compact .post-image__column .post-image__details svg, .app__body .modal .about-modal .about-modal__logo svg, .app__body .post .post__img svg', 'fill:' + theme.centerChannelColor); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 7583c4312..a311ccaa0 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2720,6 +2720,10 @@ executable@^1.0.0: dependencies: meow "^3.1.0" +exif2css@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/exif2css/-/exif2css-1.2.0.tgz#8438e116921508e3dcc30cbe2407b1d5535e1b45" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" |