summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/admin_actions.jsx13
-rw-r--r--webapp/actions/file_actions.jsx6
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx16
-rw-r--r--webapp/components/admin_console/elasticsearch_settings.jsx280
-rw-r--r--webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx4
-rw-r--r--webapp/components/admin_console/push_settings.jsx2
-rw-r--r--webapp/components/file_preview.jsx4
-rw-r--r--webapp/components/integrations/components/abstract_outgoing_webhook.jsx131
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook.jsx36
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook/add_outgoing_webhook.jsx69
-rw-r--r--webapp/components/integrations/components/add_outgoing_webhook/index.js25
-rw-r--r--webapp/components/integrations/components/edit_outgoing_webhook.jsx183
-rw-r--r--webapp/components/integrations/components/edit_outgoing_webhook/edit_outgoing_webhook.jsx169
-rw-r--r--webapp/components/integrations/components/edit_outgoing_webhook/index.js30
-rw-r--r--webapp/components/post_view/post_attachment.jsx9
-rw-r--r--webapp/components/post_view/post_body_additional_content.jsx14
-rw-r--r--webapp/components/setting_picture.jsx72
-rw-r--r--webapp/components/sidebar.jsx6
-rw-r--r--webapp/components/signup/components/signup_email.jsx39
-rw-r--r--webapp/components/textbox.jsx4
-rw-r--r--webapp/components/webrtc/components/webrtc_sidebar.jsx4
-rwxr-xr-xwebapp/i18n/en.json21
-rw-r--r--webapp/package.json1
-rw-r--r--webapp/routes/route_admin_console.jsx5
-rw-r--r--webapp/routes/route_integrations.jsx4
-rw-r--r--webapp/tests/components/integrations/__snapshots__/add_outgoing_hook.test.jsx.snap27
-rw-r--r--webapp/tests/components/integrations/__snapshots__/edit_outgoing_hook.test.jsx.snap7
-rw-r--r--webapp/tests/components/integrations/add_outgoing_hook.test.jsx29
-rw-r--r--webapp/tests/components/integrations/edit_outgoing_hook.test.jsx31
-rw-r--r--webapp/utils/utils.jsx4
-rw-r--r--webapp/yarn.lock4
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"