summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/about_build_modal.jsx10
-rw-r--r--webapp/components/activity_log_modal.jsx12
-rw-r--r--webapp/components/admin_console/admin_console.jsx61
-rw-r--r--webapp/components/admin_console/admin_controller.jsx221
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx28
-rw-r--r--webapp/components/admin_console/admin_settings.jsx115
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx830
-rw-r--r--webapp/components/admin_console/admin_sidebar_category.jsx83
-rw-r--r--webapp/components/admin_console/admin_sidebar_section.jsx80
-rw-r--r--webapp/components/admin_console/admin_sidebar_team.jsx87
-rw-r--r--webapp/components/admin_console/boolean_setting.jsx59
-rw-r--r--webapp/components/admin_console/brand_image_setting.jsx182
-rw-r--r--webapp/components/admin_console/compliance_reports.jsx1
-rw-r--r--webapp/components/admin_console/compliance_settings.jsx318
-rw-r--r--webapp/components/admin_console/configuration_settings.jsx76
-rw-r--r--webapp/components/admin_console/connection_security_dropdown_setting.jsx114
-rw-r--r--webapp/components/admin_console/connection_settings.jsx94
-rw-r--r--webapp/components/admin_console/custom_brand_settings.jsx137
-rw-r--r--webapp/components/admin_console/database_settings.jsx194
-rw-r--r--webapp/components/admin_console/developer_settings.jsx83
-rw-r--r--webapp/components/admin_console/dropdown_setting.jsx33
-rw-r--r--webapp/components/admin_console/email_authentication_settings.jsx109
-rw-r--r--webapp/components/admin_console/email_connection_test.jsx118
-rw-r--r--webapp/components/admin_console/email_settings.jsx1218
-rw-r--r--webapp/components/admin_console/external_service_settings.jsx94
-rw-r--r--webapp/components/admin_console/generated_setting.jsx97
-rw-r--r--webapp/components/admin_console/gitlab_settings.jsx515
-rw-r--r--webapp/components/admin_console/image_settings.jsx820
-rw-r--r--webapp/components/admin_console/ldap_settings.jsx999
-rw-r--r--webapp/components/admin_console/legal_and_support_settings.jsx431
-rw-r--r--webapp/components/admin_console/license_settings.jsx1
-rw-r--r--webapp/components/admin_console/log_settings.jsx586
-rw-r--r--webapp/components/admin_console/login_settings.jsx130
-rw-r--r--webapp/components/admin_console/logs.jsx2
-rw-r--r--webapp/components/admin_console/privacy_settings.jsx261
-rw-r--r--webapp/components/admin_console/public_link_settings.jsx91
-rw-r--r--webapp/components/admin_console/push_settings.jsx235
-rw-r--r--webapp/components/admin_console/rate_settings.jsx479
-rw-r--r--webapp/components/admin_console/recycle_db.jsx96
-rw-r--r--webapp/components/admin_console/reload_config.jsx96
-rw-r--r--webapp/components/admin_console/save_button.jsx61
-rw-r--r--webapp/components/admin_console/service_settings.jsx1042
-rw-r--r--webapp/components/admin_console/session_settings.jsx134
-rw-r--r--webapp/components/admin_console/setting.jsx14
-rw-r--r--webapp/components/admin_console/settings_group.jsx42
-rw-r--r--webapp/components/admin_console/signup_settings.jsx124
-rw-r--r--webapp/components/admin_console/sql_settings.jsx390
-rw-r--r--webapp/components/admin_console/storage_settings.jsx200
-rw-r--r--webapp/components/admin_console/team_settings.jsx735
-rw-r--r--webapp/components/admin_console/team_users.jsx41
-rw-r--r--webapp/components/admin_console/text_setting.jsx83
-rw-r--r--webapp/components/admin_console/user_item.jsx4
-rw-r--r--webapp/components/admin_console/users_and_teams_settings.jsx179
-rw-r--r--webapp/components/admin_console/webhook_settings.jsx166
-rw-r--r--webapp/components/analytics/statistic_count.jsx2
-rw-r--r--webapp/components/analytics/system_analytics.jsx3
-rw-r--r--webapp/components/analytics/team_analytics.jsx52
-rw-r--r--webapp/components/backstage/add_command.jsx51
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx5
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx4
-rw-r--r--webapp/components/channel_header.jsx22
-rw-r--r--webapp/components/channel_notifications_modal.jsx10
-rw-r--r--webapp/components/channel_select.jsx51
-rw-r--r--webapp/components/claim/claim_controller.jsx (renamed from webapp/components/claim/claim.jsx)6
-rw-r--r--webapp/components/claim/components/email_to_ldap.jsx4
-rw-r--r--webapp/components/claim/components/email_to_oauth.jsx2
-rw-r--r--webapp/components/claim/components/ldap_to_email.jsx23
-rw-r--r--webapp/components/claim/components/oauth_to_email.jsx4
-rw-r--r--webapp/components/create_comment.jsx37
-rw-r--r--webapp/components/create_post.jsx21
-rw-r--r--webapp/components/create_team/components/display_name.jsx38
-rw-r--r--webapp/components/create_team/components/team_url.jsx95
-rw-r--r--webapp/components/create_team/create_team_controller.jsx (renamed from webapp/components/create_team/create_team.jsx)4
-rw-r--r--webapp/components/edit_post_modal.jsx2
-rw-r--r--webapp/components/error_bar.jsx9
-rw-r--r--webapp/components/file_attachment.jsx69
-rw-r--r--webapp/components/file_attachment_list.jsx5
-rw-r--r--webapp/components/file_info_preview.jsx1
-rw-r--r--webapp/components/file_upload.jsx5
-rw-r--r--webapp/components/header_footer_template.jsx10
-rw-r--r--webapp/components/invite_member_modal.jsx114
-rw-r--r--webapp/components/logged_in.jsx2
-rw-r--r--webapp/components/login/login_controller.jsx (renamed from webapp/components/login/login.jsx)19
-rw-r--r--webapp/components/more_channels.jsx2
-rw-r--r--webapp/components/more_direct_channels.jsx2
-rw-r--r--webapp/components/navbar.jsx134
-rw-r--r--webapp/components/navbar_dropdown.jsx25
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/password_reset_form.jsx2
-rw-r--r--webapp/components/pending_post_actions.jsx92
-rw-r--r--webapp/components/post.jsx80
-rw-r--r--webapp/components/post_attachment.jsx34
-rw-r--r--webapp/components/post_attachment_oembed.jsx39
-rw-r--r--webapp/components/post_body.jsx21
-rw-r--r--webapp/components/post_body_additional_content.jsx6
-rw-r--r--webapp/components/post_focus_view.jsx2
-rw-r--r--webapp/components/post_header.jsx7
-rw-r--r--webapp/components/post_info.jsx9
-rw-r--r--webapp/components/posts_view.jsx13
-rw-r--r--webapp/components/posts_view_container.jsx2
-rw-r--r--webapp/components/removed_from_channel_modal.jsx26
-rw-r--r--webapp/components/rhs_comment.jsx63
-rw-r--r--webapp/components/rhs_header_post.jsx2
-rw-r--r--webapp/components/rhs_root_post.jsx5
-rw-r--r--webapp/components/rhs_thread.jsx9
-rw-r--r--webapp/components/root.jsx2
-rw-r--r--webapp/components/search_results.jsx5
-rw-r--r--webapp/components/search_results_item.jsx11
-rw-r--r--webapp/components/select_team/select_team.jsx2
-rw-r--r--webapp/components/settings_sidebar.jsx4
-rw-r--r--webapp/components/sidebar.jsx31
-rw-r--r--webapp/components/sidebar_header.jsx2
-rw-r--r--webapp/components/sidebar_right_menu.jsx8
-rw-r--r--webapp/components/signup_user_complete.jsx70
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx2
-rw-r--r--webapp/components/suggestion/suggestion_list.jsx6
-rw-r--r--webapp/components/team_general_tab.jsx12
-rw-r--r--webapp/components/team_import_tab.jsx3
-rw-r--r--webapp/components/team_settings_modal.jsx6
-rw-r--r--webapp/components/textbox.jsx1
-rw-r--r--webapp/components/time_since.jsx5
-rw-r--r--webapp/components/toggle_modal_button.jsx4
-rw-r--r--webapp/components/tutorial/tutorial_intro_screens.jsx3
-rw-r--r--webapp/components/user_settings/custom_theme_chooser.jsx10
-rw-r--r--webapp/components/user_settings/import_theme_modal.jsx2
-rw-r--r--webapp/components/user_settings/manage_languages.jsx2
-rw-r--r--webapp/components/user_settings/premade_theme_chooser.jsx1
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_developer.jsx4
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx127
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx16
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx189
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx29
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx8
-rw-r--r--webapp/components/view_image.jsx3
-rw-r--r--webapp/components/view_image_popover_bar.jsx1
-rw-r--r--webapp/components/youtube_video.jsx71
137 files changed, 6700 insertions, 7600 deletions
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index 4fd946401..2f7b3e781 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -45,6 +45,7 @@ export default class AboutBuildModal extends React.Component {
/>
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://www.mattermost.org/'
>
{'mattermost.org'}
@@ -76,6 +77,7 @@ export default class AboutBuildModal extends React.Component {
/>
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://about.mattermost.com/'
>
{'about.mattermost.com'}
@@ -133,7 +135,7 @@ export default class AboutBuildModal extends React.Component {
id='about.version'
defaultMessage='Version:'
/>
- &nbsp;{config.Version}&nbsp;({config.BuildNumber})
+ {'\u00a0' + config.Version + '\u00a0' + config.BuildNumber}
</div>
</div>
{licensee}
@@ -155,6 +157,12 @@ export default class AboutBuildModal extends React.Component {
defaultMessage='Build Hash:'
/>
&nbsp;{config.BuildHash}
+ <br/>
+ <FormattedMessage
+ id='about.hashee'
+ defaultMessage='EE Build Hash:'
+ />
+ &nbsp;{config.BuildHashEnterprise}
</p>
<p>
<FormattedMessage
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index d3e5ce66d..ab6224906 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -23,7 +23,7 @@ export default class ActivityLogModal extends React.Component {
this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
- let state = this.getStateFromStores();
+ const state = this.getStateFromStores();
state.moreInfo = [];
this.state = state;
@@ -43,14 +43,14 @@ export default class ActivityLogModal extends React.Component {
modalContent.removeClass('animation--highlight');
}, 1500);
Client.revokeSession(altId,
- function handleRevokeSuccess() {
+ () => {
AsyncClient.getSessions();
},
- function handleRevokeError(err) {
- let state = this.getStateFromStores();
+ (err) => {
+ const state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
- }.bind(this)
+ }
);
}
onShow() {
@@ -85,7 +85,7 @@ export default class ActivityLogModal extends React.Component {
}
}
handleMoreInfo(index) {
- let newMoreInfo = this.state.moreInfo;
+ const newMoreInfo = this.state.moreInfo;
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
diff --git a/webapp/components/admin_console/admin_console.jsx b/webapp/components/admin_console/admin_console.jsx
new file mode 100644
index 000000000..e5c528614
--- /dev/null
+++ b/webapp/components/admin_console/admin_console.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+
+import AdminStore from 'stores/admin_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import AdminSidebar from './admin_sidebar.jsx';
+
+export default class AdminConsole extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleConfigChange = this.handleConfigChange.bind(this);
+
+ this.state = {
+ config: AdminStore.getConfig()
+ };
+ }
+
+ componentWillMount() {
+ AdminStore.addConfigChangeListener(this.handleConfigChange);
+ AsyncClient.getConfig();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeConfigChangeListener(this.handleConfigChange);
+ }
+
+ handleConfigChange() {
+ this.setState({
+ config: AdminStore.getConfig()
+ });
+ }
+
+ render() {
+ if ($.isEmptyObject(this.state.config)) {
+ return <div className='admin-console'/>;
+ }
+
+ // not every page in the system console will need the config, but the vast majority will
+ const children = React.cloneElement(this.props.children, {
+ config: this.state.config
+ });
+
+ return (
+ <div className='admin-console'>
+ <AdminSidebar/>
+ {children}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_controller.jsx b/webapp/components/admin_console/admin_controller.jsx
deleted file mode 100644
index aea2a0197..000000000
--- a/webapp/components/admin_console/admin_controller.jsx
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import AdminSidebar from './admin_sidebar.jsx';
-import AdminStore from 'stores/admin_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import LoadingScreen from '../loading_screen.jsx';
-
-import EmailSettingsTab from './email_settings.jsx';
-import LogSettingsTab from './log_settings.jsx';
-import LogsTab from './logs.jsx';
-import AuditsTab from './audits.jsx';
-import FileSettingsTab from './image_settings.jsx';
-import PrivacySettingsTab from './privacy_settings.jsx';
-import RateSettingsTab from './rate_settings.jsx';
-import GitLabSettingsTab from './gitlab_settings.jsx';
-import SqlSettingsTab from './sql_settings.jsx';
-import TeamSettingsTab from './team_settings.jsx';
-import ServiceSettingsTab from './service_settings.jsx';
-import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
-import TeamUsersTab from './team_users.jsx';
-import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
-import LdapSettingsTab from './ldap_settings.jsx';
-import ComplianceSettingsTab from './compliance_settings.jsx';
-import LicenseSettingsTab from './license_settings.jsx';
-import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
-
-import React from 'react';
-
-export default class AdminController extends React.Component {
- constructor(props) {
- super(props);
-
- this.selectTab = this.selectTab.bind(this);
- this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
- this.addSelectedTeam = this.addSelectedTeam.bind(this);
- this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
- this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
-
- var selectedTeams = AdminStore.getSelectedTeams();
- if (selectedTeams == null) {
- selectedTeams = {};
- selectedTeams[TeamStore.getCurrentId()] = 'true';
- AdminStore.saveSelectedTeams(selectedTeams);
- }
-
- this.state = {
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams,
- selected: props.tab || 'system_analytics',
- selectedTeam: props.teamId || null
- };
- }
-
- componentDidMount() {
- AdminStore.addConfigChangeListener(this.onConfigListenerChange);
- AsyncClient.getConfig();
-
- AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
- AsyncClient.getAllTeams();
-
- $('[data-toggle="tooltip"]').tooltip();
- $('[data-toggle="popover"]').popover();
- }
-
- componentWillUnmount() {
- AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
- AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
- }
-
- onConfigListenerChange() {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- onAllTeamsListenerChange() {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
-
- });
- }
-
- selectTab(tab, teamId) {
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: tab,
- selectedTeam: teamId
- });
- }
-
- removeSelectedTeam(teamId) {
- var selectedTeams = AdminStore.getSelectedTeams();
- Reflect.deleteProperty(selectedTeams, teamId);
- AdminStore.saveSelectedTeams(selectedTeams);
-
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- addSelectedTeam(teamId) {
- var selectedTeams = AdminStore.getSelectedTeams();
- selectedTeams[teamId] = 'true';
- AdminStore.saveSelectedTeams(selectedTeams);
-
- this.setState({
- config: AdminStore.getConfig(),
- teams: AdminStore.getAllTeams(),
- selectedTeams: AdminStore.getSelectedTeams(),
- selected: this.state.selected,
- selectedTeam: this.state.selectedTeam
- });
- }
-
- render() {
- var tab = <LoadingScreen/>;
-
- if (this.state.config != null) {
- if (this.state.selected === 'email_settings') {
- tab = <EmailSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'log_settings') {
- tab = <LogSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'logs') {
- tab = <LogsTab/>;
- } else if (this.state.selected === 'audits') {
- tab = <AuditsTab/>;
- } else if (this.state.selected === 'image_settings') {
- tab = <FileSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'privacy_settings') {
- tab = <PrivacySettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'rate_settings') {
- tab = <RateSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'gitlab_settings') {
- tab = <GitLabSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'sql_settings') {
- tab = <SqlSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'team_settings') {
- tab = <TeamSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'service_settings') {
- tab = <ServiceSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'legal_and_support_settings') {
- tab = <LegalAndSupportSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'ldap_settings') {
- tab = <LdapSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'compliance_settings') {
- tab = <ComplianceSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'license') {
- tab = <LicenseSettingsTab config={this.state.config}/>;
- } else if (this.state.selected === 'team_users') {
- if (this.state.teams) {
- tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]}/>;
- }
- } else if (this.state.selected === 'team_analytics') {
- if (this.state.teams) {
- tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]}/>;
- }
- } else if (this.state.selected === 'system_analytics') {
- tab = <SystemAnalyticsTab/>;
- }
- }
-
- return (
- <div
- id='admin_controller'
- className='admin-controller'
- >
- <div
- className='sidebar--menu'
- id='sidebar-menu'
- />
- <AdminSidebar
- selected={this.state.selected}
- selectedTeam={this.state.selectedTeam}
- selectTab={this.selectTab}
- teams={this.state.teams}
- selectedTeams={this.state.selectedTeams}
- removeSelectedTeam={this.removeSelectedTeam}
- addSelectedTeam={this.addSelectedTeam}
- />
- <div className='inner-wrap channel__wrap'>
- <div className='row header'>
- </div>
- <div className='row main'>
- <div
- id='app-content'
- className='app__content admin'
- >
- {tab}
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-AdminController.defaultProps = {
-};
-
-AdminController.propTypes = {
- tab: React.PropTypes.string,
- teamId: React.PropTypes.string
-};
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
index 37b8f2135..65a76a517 100644
--- a/webapp/components/admin_console/admin_navbar_dropdown.jsx
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -53,13 +53,6 @@ export default class AdminNavbarDropdown extends React.Component {
var teams = [];
if (this.state.teamMembers && this.state.teamMembers.length > 0) {
- teams.push(
- <li
- key='teamDiv'
- className='divider'
- ></li>
- );
-
for (var index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
var teamMember = this.state.teamMembers[index];
@@ -69,12 +62,23 @@ export default class AdminNavbarDropdown extends React.Component {
<Link
to={'/' + team.name + '/channels/town-square'}
>
+ <FormattedMessage
+ id='navbar_dropdown.switchTo'
+ defaultMessage='Switch to '
+ />
{team.display_name}
</Link>
</li>
);
}
}
+
+ teams.push(
+ <li
+ key='teamDiv'
+ className='divider'
+ ></li>
+ );
}
return (
@@ -99,20 +103,18 @@ export default class AdminNavbarDropdown extends React.Component {
className='dropdown-menu'
role='menu'
>
+ {teams}
<li>
<Link
to={'/select_team'}
>
+ <i className='fa fa-exchange'/>
<FormattedMessage
id='admin.nav.switch'
- defaultMessage='Switch to {display_name}'
- values={{
- display_name: global.window.mm_config.SiteName
- }}
+ defaultMessage='Team Selection'
/>
</Link>
</li>
- {teams}
<li
key='teamDiv'
className='divider'
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
new file mode 100644
index 000000000..d76e1331a
--- /dev/null
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -0,0 +1,115 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'utils/web_client.jsx';
+
+import FormError from 'components/form_error.jsx';
+import SaveButton from 'components/admin_console/save_button.jsx';
+
+export default class AdminSettings extends React.Component {
+ static get propTypes() {
+ return {
+ config: React.PropTypes.object
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ saving: false,
+ serverError: null
+ };
+ }
+
+ handleChange(id, value) {
+ this.setState({
+ saveNeeded: true,
+ [id]: value
+ });
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ this.setState({
+ saving: true,
+ serverError: null
+ });
+
+ const config = this.getConfigFromState(this.props.config);
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ saveNeeded: false,
+ saving: false
+ });
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ parseInt(str) {
+ const n = parseInt(str, 10);
+
+ if (isNaN(n)) {
+ return 0;
+ }
+
+ return n;
+ }
+
+ parseIntNonZero(str) {
+ const n = parseInt(str, 10);
+
+ if (isNaN(n) || n < 1) {
+ return 1;
+ }
+
+ return n;
+ }
+
+ render() {
+ let saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass += 'btn-primary';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ {this.renderTitle()}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ {this.renderSettings()}
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <FormError error={this.state.serverError}/>
+ <SaveButton
+ saving={this.state.saving}
+ disabled={!this.state.saveNeeded || (this.canSave && !this.canSave())}
+ onClick={this.handleSubmit}
+ />
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index da406e647..cdb7e29d5 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -2,69 +2,80 @@
// See License.txt for license information.
import $ from 'jquery';
+import React from 'react';
-import AdminSidebarHeader from './admin_sidebar_header.jsx';
-import SelectTeamModal from './select_team_modal.jsx';
+import AdminStore from 'stores/admin_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import AdminSidebarHeader from './admin_sidebar_header.jsx';
+import AdminSidebarTeam from './admin_sidebar_team.jsx';
import {FormattedMessage} from 'react-intl';
-
-import {Tooltip, OverlayTrigger} from 'react-bootstrap';
-
-import React from 'react';
+import {browserHistory} from 'react-router';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+import SelectTeamModal from './select_team_modal.jsx';
+import AdminSidebarCategory from './admin_sidebar_category.jsx';
+import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebar extends React.Component {
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
- this.isSelected = this.isSelected.bind(this);
- this.handleClick = this.handleClick.bind(this);
+ this.handleAllTeamsChange = this.handleAllTeamsChange.bind(this);
+
this.removeTeam = this.removeTeam.bind(this);
this.showTeamSelect = this.showTeamSelect.bind(this);
this.teamSelectedModal = this.teamSelectedModal.bind(this);
this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
+ this.renderAddTeamButton = this.renderAddTeamButton.bind(this);
+ this.renderTeams = this.renderTeams.bind(this);
+
this.state = {
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
showSelectModal: false
};
}
+ componentDidMount() {
+ AdminStore.addAllTeamsChangeListener(this.handleAllTeamsChange);
+ AsyncClient.getAllTeams();
+ }
+
componentDidUpdate() {
if (!Utils.isMobile()) {
- $('.sidebar--left .nav-pills__container').perfectScrollbar();
+ $('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
- handleClick(name, teamId, e) {
- e.preventDefault();
- this.props.selectTab(name, teamId);
+ componentWillUnmount() {
+ AdminStore.removeAllTeamsChangeListener(this.handleAllTeamsChange);
}
- isSelected(name, teamId) {
- if (this.props.selected === name) {
- if (name === 'team_users' || name === 'team_analytics') {
- if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
- return 'active';
- }
- } else {
- return 'active';
- }
- }
-
- return '';
+ handleAllTeamsChange() {
+ this.setState({
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams()
+ });
}
- removeTeam(teamId, e) {
- e.preventDefault();
- e.stopPropagation();
- Reflect.deleteProperty(this.props.selectedTeams, teamId);
- this.props.removeSelectedTeam(teamId);
+ removeTeam(team) {
+ const selectedTeams = Object.assign({}, this.state.selectedTeams);
+ Reflect.deleteProperty(selectedTeams, team.id);
+ AdminStore.saveSelectedTeams(selectedTeams);
- if (this.props.selected === 'team_users') {
- if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
- this.props.selectTab('service_settings', null);
- }
+ this.handleAllTeamsChange();
+
+ if (this.context.router.isActive('/admin_console/team/' + team.id)) {
+ browserHistory.push('/admin_console');
}
}
@@ -74,31 +85,23 @@ export default class AdminSidebar extends React.Component {
}
teamSelectedModal(teamId) {
- this.setState({showSelectModal: false});
- this.props.addSelectedTeam(teamId);
- this.forceUpdate();
+ this.setState({
+ showSelectModal: false
+ });
+
+ const selectedTeams = Object.assign({}, this.state.selectedTeams);
+ selectedTeams[teamId] = true;
+
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.handleAllTeamsChange();
}
teamSelectedModalDismissed() {
this.setState({showSelectModal: false});
}
- render() {
- var count = '*';
- var teams = (
- <FormattedMessage
- id='admin.sidebar.loading'
- defaultMessage='Loading'
- />
- );
- const removeTooltip = (
- <Tooltip id='remove-team-tooltip'>
- <FormattedMessage
- id='admin.sidebar.rmTeamSidebar'
- defaultMessage='Remove team from sidebar menu'
- />
- </Tooltip>
- );
+ renderAddTeamButton() {
const addTeamTooltip = (
<Tooltip id='add-team-tooltip'>
<FormattedMessage
@@ -108,393 +111,468 @@ export default class AdminSidebar extends React.Component {
</Tooltip>
);
- if (this.props.teams != null) {
- count = '' + Object.keys(this.props.teams).length;
+ return (
+ <span className='menu-icon--right'>
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={addTeamTooltip}
+ >
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i
+ className='fa fa-plus'
+ ></i>
+ </a>
+ </OverlayTrigger>
+ </span>
+ );
+ }
+
+ renderTeams() {
+ const teams = [];
- teams = [];
- for (var key in this.props.selectedTeams) {
- if (this.props.selectedTeams.hasOwnProperty(key)) {
- var team = this.props.teams[key];
+ for (const key in this.state.selectedTeams) {
+ if (!this.state.selectedTeams.hasOwnProperty(key)) {
+ continue;
+ }
- if (team != null) {
- teams.push(
- <ul
- key={'team_' + team.id}
- className='nav nav__sub-menu'
- >
- <li>
- <a
- href='#'
- onClick={this.handleClick.bind(this, 'team_users', team.id)}
- className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id) + ' ' + this.isSelected('team_analytics', team.id)}
- >
- {team.name}
- <OverlayTrigger
- delayShow={1000}
- placement='top'
- overlay={removeTooltip}
- >
- <span
- className='menu-icon--right menu__close'
- onClick={this.removeTeam.bind(this, team.id)}
- style={{cursor: 'pointer'}}
- >
- {'×'}
- </span>
- </OverlayTrigger>
- </a>
- </li>
- <li>
- <ul className='nav nav__inner-menu'>
- <li>
- <a
- href='#'
- className={this.isSelected('team_users', team.id)}
- onClick={this.handleClick.bind(this, 'team_users', team.id)}
- >
- <FormattedMessage
- id='admin.sidebar.users'
- defaultMessage='- Users'
- />
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('team_analytics', team.id)}
- onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
- >
- <FormattedMessage
- id='admin.sidebar.statistics'
- defaultMessage='- Statistics'
- />
- </a>
- </li>
- </ul>
- </li>
- </ul>
- );
- }
- }
+ const team = this.state.teams[key];
+
+ if (!team) {
+ continue;
}
+
+ teams.push(
+ <AdminSidebarTeam
+ key={team.id}
+ team={team}
+ onRemoveTeam={this.removeTeam}
+ />
+ );
}
- let ldapSettings;
- let complianceSettings;
- let licenseSettings;
- if (global.window.mm_config.BuildEnterpriseReady === 'true') {
- if (global.window.mm_license.IsLicensed === 'true') {
+ return (
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.teams'
+ defaultMessage='TEAMS ({count, number})'
+ values={{
+ count: Object.keys(this.state.teams).length
+ }}
+ />
+ }
+ action={this.renderAddTeamButton()}
+ >
+ {teams}
+ </AdminSidebarCategory>
+ );
+ }
+
+ render() {
+ let ldapSettings = null;
+ let complianceSettings = null;
+
+ let license = null;
+ let audits = null;
+
+ if (window.mm_config.BuildEnterpriseReady === 'true') {
+ if (window.mm_license.IsLicensed === 'true') {
if (global.window.mm_license.LDAP === 'true') {
ldapSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('ldap_settings')}
- onClick={this.handleClick.bind(this, 'ldap_settings', null)}
- >
+ <AdminSidebarSection
+ name='ldap'
+ title={
<FormattedMessage
id='admin.sidebar.ldap'
- defaultMessage='LDAP Settings'
+ defaultMessage='LDAP'
/>
- </a>
- </li>
+ }
+ />
);
}
if (global.window.mm_license.Compliance === 'true') {
complianceSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('compliance_settings')}
- onClick={this.handleClick.bind(this, 'compliance_settings', null)}
- >
+ <AdminSidebarSection
+ name='compliance'
+ title={
<FormattedMessage
id='admin.sidebar.compliance'
- defaultMessage='Compliance Settings'
+ defaultMessage='Compliance'
/>
- </a>
- </li>
+ }
+ />
);
}
}
- licenseSettings = (
- <li>
- <a
- href='#'
- className={this.isSelected('license')}
- onClick={this.handleClick.bind(this, 'license', null)}
- >
+ license = (
+ <AdminSidebarSection
+ name='license'
+ title={
<FormattedMessage
id='admin.sidebar.license'
defaultMessage='Edition and License'
/>
- </a>
- </li>
+ }
+ />
);
}
- let audits;
- if (global.window.mm_license.IsLicensed === 'true') {
+ if (window.mm_license.IsLicensed === 'true') {
audits = (
- <li>
- <a
- href='#'
- className={this.isSelected('audits')}
- onClick={this.handleClick.bind(this, 'audits', null)}
- >
+ <AdminSidebarSection
+ name='audits'
+ title={
<FormattedMessage
id='admin.sidebar.audits'
- defaultMessage='Compliance and Auditing'
+ defaultMessage='Complaince and Auditing'
/>
- </a>
- </li>
+ }
+ />
);
}
return (
- <div className='sidebar--left sidebar--collapsable'>
+ <div className='admin-sidebar'>
<AdminSidebarHeader/>
<div className='nav-pills__container'>
<ul className='nav nav-pills nav-stacked'>
- <li>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.reports'
- defaultMessage='SITE REPORTS'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- <a
- href='#'
- className={this.isSelected('system_analytics')}
- onClick={this.handleClick.bind(this, 'system_analytics', null)}
- >
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.reports'
+ defaultMessage='SITE REPORTS'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='system_analytics'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.view_statistics'
+ defaultMessage='View Statistics'
+ />
+ }
+ />
+ </AdminSidebarCategory>
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.settings'
+ defaultMessage='SETTINGS'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='general'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.general'
+ defaultMessage='General'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='configuration'
+ title={
<FormattedMessage
- id='admin.sidebar.view_statistics'
- defaultMessage='View Statistics'
+ id='admin.sidebar.configuration'
+ defaultMessage='Configuration'
/>
- </a>
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.settings'
- defaultMessage='SETTINGS'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- <a
- href='#'
- className={this.isSelected('service_settings')}
- onClick={this.handleClick.bind(this, 'service_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='users_and_teams'
+ title={
<FormattedMessage
- id='admin.sidebar.service'
- defaultMessage='Service Settings'
+ id='admin.sidebar.usersAndTeams'
+ defaultMessage='Users and Teams'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('team_settings')}
- onClick={this.handleClick.bind(this, 'team_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='privacy'
+ title={
<FormattedMessage
- id='admin.sidebar.team'
- defaultMessage='Team Settings'
+ id='admin.sidebar.privacy'
+ defaultMessage='Privacy'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('sql_settings')}
- onClick={this.handleClick.bind(this, 'sql_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='logging'
+ title={
<FormattedMessage
- id='admin.sidebar.sql'
- defaultMessage='SQL Settings'
+ id='admin.sidebar.logging'
+ defaultMessage='Logging'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings', null)}
- >
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='authentication'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.authentication'
+ defaultMessage='Authentication'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='email'
+ title={
<FormattedMessage
id='admin.sidebar.email'
- defaultMessage='Email Settings'
+ defaultMessage='Email'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('image_settings')}
- onClick={this.handleClick.bind(this, 'image_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='gitlab'
+ title={
<FormattedMessage
- id='admin.sidebar.file'
- defaultMessage='File Settings'
+ id='admin.sidebar.gitlab'
+ defaultMessage='GitLab'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings', null)}
- >
+ }
+ />
+ {ldapSettings}
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='security'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.security'
+ defaultMessage='Security'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='sign_up'
+ title={
<FormattedMessage
- id='admin.sidebar.log'
- defaultMessage='Log Settings'
+ id='admin.sidebar.signUp'
+ defaultMessage='Sign Up'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('rate_settings')}
- onClick={this.handleClick.bind(this, 'rate_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='login'
+ title={
<FormattedMessage
- id='admin.sidebar.rate_limit'
- defaultMessage='Rate Limit Settings'
+ id='admin.sidebar.login'
+ defaultMessage='Login'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('privacy_settings')}
- onClick={this.handleClick.bind(this, 'privacy_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='public_links'
+ title={
<FormattedMessage
- id='admin.sidebar.privacy'
- defaultMessage='Privacy Settings'
+ id='admin.sidebar.publicLinks'
+ defaultMessage='Public Links'
/>
- </a>
- </li>
- <li>
- <a
- href='#'
- className={this.isSelected('gitlab_settings')}
- onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='sessions'
+ title={
<FormattedMessage
- id='admin.sidebar.gitlab'
- defaultMessage='GitLab Settings'
+ id='admin.sidebar.sessions'
+ defaultMessage='Sessions'
/>
- </a>
- </li>
- {ldapSettings}
- {complianceSettings}
- <li>
- <a
- href='#'
- className={this.isSelected('legal_and_support_settings')}
- onClick={this.handleClick.bind(this, 'legal_and_support_settings', null)}
- >
+ }
+ />
+ <AdminSidebarSection
+ name='connections'
+ title={
<FormattedMessage
- id='admin.sidebar.support'
- defaultMessage='Legal and Support Settings'
+ id='admin.sidebar.connections'
+ defaultMessage='Connections'
/>
- </a>
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.teams'
- defaultMessage='TEAMS ({count})'
- values={{
- count: count
- }}
- />
- </span>
- <span className='menu-icon--right'>
- <OverlayTrigger
- delayShow={1000}
- placement='top'
- overlay={addTeamTooltip}
- >
- <a
- href='#'
- onClick={this.showTeamSelect}
- >
- <i
- className='fa fa-plus'
- ></i>
- </a>
- </OverlayTrigger>
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- <li>
- {teams}
- </li>
- </ul>
- <ul className='nav nav__sub-menu'>
- <li>
- <h4>
- <span className='icon fa fa-gear'></span>
- <span>
- <FormattedMessage
- id='admin.sidebar.other'
- defaultMessage='OTHER'
- />
- </span>
- </h4>
- </li>
- </ul>
- <ul className='nav nav__sub-menu padded'>
- {licenseSettings}
- {audits}
- <li>
- <a
- href='#'
- className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs', null)}
- >
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='notifications'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.notifications'
+ defaultMessage='Notifications'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='email'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.email'
+ defaultMessage='Email'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='push'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.push'
+ defaultMessage='Mobile Push'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='integrations'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.integrations'
+ defaultMessage='Integrations'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='webhooks'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.webhooks'
+ defaultMessage='Webhooks and Commands'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='external'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.external'
+ defaultMessage='External Services'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='database'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.database'
+ defaultMessage='Database'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='files'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.files'
+ defaultMessage='Files'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='storage'
+ title={
<FormattedMessage
- id='admin.sidebar.logs'
- defaultMessage='Logs'
+ id='admin.sidebar.storage'
+ defaultMessage='Storage'
/>
- </a>
- </li>
- </ul>
- </li>
+ }
+ />
+ <AdminSidebarSection
+ name='images'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.images'
+ defaultMessage='Images'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ <AdminSidebarSection
+ name='customization'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.customization'
+ defaultMessage='Customization'
+ />
+ }
+ >
+ <AdminSidebarSection
+ name='custom_brand'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.customBrand'
+ defaultMessage='Custom Branding'
+ />
+
+ }
+ />
+ <AdminSidebarSection
+ name='legal_and_support'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.legalAndSupport'
+ defaultMessage='Legal and Support'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ {complianceSettings}
+ <AdminSidebarSection
+ name='rate'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.rate'
+ defaultMessage='Rate Limiting'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='developer'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.developer'
+ defaultMessage='Developer'
+ />
+ }
+ />
+ </AdminSidebarCategory>
+ {this.renderTeams()}
+ <AdminSidebarCategory
+ parentLink='/admin_console'
+ icon='fa-gear'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.other'
+ defaultMessage='OTHER'
+ />
+ }
+ >
+ {license}
+ {audits}
+ <AdminSidebarSection
+ name='logs'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.logs'
+ defaultMessage='Logs'
+ />
+ }
+ />
+ </AdminSidebarCategory>
</ul>
</div>
-
<SelectTeamModal
- teams={this.props.teams}
+ teams={this.state.teams}
show={this.state.showSelectModal}
onModalSubmit={this.teamSelectedModal}
onModalDismissed={this.teamSelectedModalDismissed}
@@ -503,13 +581,3 @@ export default class AdminSidebar extends React.Component {
);
}
}
-
-AdminSidebar.propTypes = {
- teams: React.PropTypes.object,
- selectedTeams: React.PropTypes.object,
- removeSelectedTeam: React.PropTypes.func,
- addSelectedTeam: React.PropTypes.func,
- selected: React.PropTypes.string,
- selectedTeam: React.PropTypes.string,
- selectTab: React.PropTypes.func
-};
diff --git a/webapp/components/admin_console/admin_sidebar_category.jsx b/webapp/components/admin_console/admin_sidebar_category.jsx
new file mode 100644
index 000000000..c31c84ff7
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_category.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AdminSidebarCategory extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string,
+ title: React.PropTypes.node.isRequired,
+ icon: React.PropTypes.string.isRequired,
+ parentLink: React.PropTypes.string,
+ children: React.PropTypes.node,
+ action: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: ''
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ render() {
+ let link = this.props.parentLink;
+ let title = (
+ <div className='category-title category-title--active'>
+ <i className={'category-icon fa ' + this.props.icon}/>
+ <span className='category-title__text'>
+ {this.props.title}
+ </span>
+ {this.props.action}
+ </div>
+ );
+
+ if (this.props.name) {
+ link += '/' + name;
+ title = (
+ <Link
+ to={link}
+ className='category-title'
+ activeClassName='category-title category-title--active'
+ >
+ {title}
+ </Link>
+ );
+ }
+
+ let clonedChildren = null;
+ if (this.props.children && this.context.router.isActive(link)) {
+ clonedChildren = (
+ <ul className='sections'>
+ {
+ React.Children.map(this.props.children, (child) => {
+ if (child === null) {
+ return null;
+ }
+
+ return React.cloneElement(child, {
+ parentLink: link
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ return (
+ <li className='sidebar-category'>
+ {title}
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar_section.jsx b/webapp/components/admin_console/admin_sidebar_section.jsx
new file mode 100644
index 000000000..0492745ca
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_section.jsx
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AdminSidebarSection extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ parentLink: React.PropTypes.string,
+ subsection: React.PropTypes.bool,
+ children: React.PropTypes.arrayOf(React.PropTypes.element),
+ action: React.PropTypes.node,
+ onlyActiveOnIndex: React.PropTypes.bool
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ subsection: false,
+ children: [],
+ onlyActiveOnIndex: true
+ };
+ }
+
+ getLink() {
+ return this.props.parentLink + '/' + this.props.name;
+ }
+
+ render() {
+ const link = this.getLink();
+
+ let clonedChildren = null;
+ if (this.props.children.length > 0) {
+ clonedChildren = (
+ <ul className='nav nav__sub-menu subsections'>
+ {
+ React.Children.map(this.props.children, (child) => {
+ if (child === null) {
+ return null;
+ }
+
+ return React.cloneElement(child, {
+ parentLink: link,
+ subsection: true
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ let className = 'sidebar-section';
+ if (this.props.subsection) {
+ className += ' sidebar-subsection';
+ }
+
+ return (
+ <li className={className}>
+ <Link
+ className={`${className}-title`}
+ activeClassName={`${className}-title ${className}-title--active`}
+ onlyActiveOnIndex={this.props.onlyActiveOnIndex}
+ onClick={this.handleClick}
+ to={link}
+ >
+ <span className={`${className}-title__text`}>
+ {this.props.title}
+ </span>
+ {this.props.action}
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar_team.jsx b/webapp/components/admin_console/admin_sidebar_team.jsx
new file mode 100644
index 000000000..2b85c712c
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_team.jsx
@@ -0,0 +1,87 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+import {OverlayTrigger, Tooltip} from 'react-bootstrap';
+import AdminSidebarSection from './admin_sidebar_section.jsx';
+
+export default class AdminSidebarTeam extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.PropTypes.object.isRequired,
+ onRemoveTeam: React.PropTypes.func.isRequired,
+ parentLink: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleRemoveTeam = this.handleRemoveTeam.bind(this);
+ }
+
+ handleRemoveTeam(e) {
+ e.preventDefault();
+
+ this.props.onRemoveTeam(this.props.team);
+ }
+
+ render() {
+ const team = this.props.team;
+
+ const removeTeamTooltip = (
+ <Tooltip id='remove-team-tooltip'>
+ <FormattedMessage
+ id='admin.sidebar.rmTeamSidebar'
+ defaultMessage='Remove team from sidebar menu'
+ />
+ </Tooltip>
+ );
+
+ const removeTeamButton = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={removeTeamTooltip}
+ >
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.handleRemoveTeam}
+ >
+ {'×'}
+ </span>
+ </OverlayTrigger>
+ );
+
+ return (
+ <AdminSidebarSection
+ key={team.id}
+ name={'team/' + team.id}
+ parentLink={this.props.parentLink}
+ title={team.display_name}
+ action={removeTeamButton}
+ >
+ <AdminSidebarSection
+ name='users'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.users'
+ defaultMessage='- Users'
+ />
+ }
+ />
+ <AdminSidebarSection
+ name='analytics'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.statistics'
+ defaultMessage='- Statistics'
+ />
+ }
+ />
+ </AdminSidebarSection>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/boolean_setting.jsx b/webapp/components/admin_console/boolean_setting.jsx
index 99d508d68..bdc1d79bf 100644
--- a/webapp/components/admin_console/boolean_setting.jsx
+++ b/webapp/components/admin_console/boolean_setting.jsx
@@ -8,16 +8,44 @@ import Setting from './setting.jsx';
import {FormattedMessage} from 'react-intl';
export default class BooleanSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value === 'true');
+ }
+
render() {
+ let helpText;
+ if (this.props.disabled && this.props.disabledText) {
+ helpText = (
+ <div>
+ <span className='admin-console__disabled-text'>
+ {this.props.disabledText}
+ </span>
+ {this.props.helpText}
+ </div>
+ );
+ } else {
+ helpText = this.props.helpText;
+ }
+
return (
- <Setting label={this.props.label}>
+ <Setting
+ label={this.props.label}
+ helpText={helpText}
+ >
<label className='radio-inline'>
<input
type='radio'
value='true'
- checked={this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ name={this.props.id}
+ checked={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.trueText}
</label>
@@ -25,13 +53,13 @@ export default class BooleanSetting extends React.Component {
<input
type='radio'
value='false'
- checked={!this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ name={this.props.id}
+ checked={!this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.falseText}
</label>
- {this.props.helpText}
</Setting>
);
}
@@ -39,24 +67,27 @@ export default class BooleanSetting extends React.Component {
BooleanSetting.defaultProps = {
trueText: (
<FormattedMessage
- id='admin.ldap.true'
+ id='admin.true'
defaultMessage='true'
/>
),
falseText: (
<FormattedMessage
- id='admin.ldap.false'
+ id='admin.false'
defaultMessage='false'
/>
- )
+ ),
+ disabled: false
};
BooleanSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
label: React.PropTypes.node.isRequired,
- currentValue: React.PropTypes.bool.isRequired,
+ value: React.PropTypes.bool.isRequired,
+ onChange: React.PropTypes.func.isRequired,
trueText: React.PropTypes.node,
falseText: React.PropTypes.node,
- isDisabled: React.PropTypes.bool.isRequired,
- handleChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired,
+ disabledText: React.PropTypes.node,
helpText: React.PropTypes.node.isRequired
};
diff --git a/webapp/components/admin_console/brand_image_setting.jsx b/webapp/components/admin_console/brand_image_setting.jsx
new file mode 100644
index 000000000..74f2290af
--- /dev/null
+++ b/webapp/components/admin_console/brand_image_setting.jsx
@@ -0,0 +1,182 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import FormError from 'components/form_error.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+
+export default class BrandImageSetting extends React.Component {
+ static get propTypes() {
+ return {
+ disabled: React.PropTypes.bool.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleImageChange = this.handleImageChange.bind(this);
+ this.handleImageSubmit = this.handleImageSubmit.bind(this);
+
+ this.state = {
+ brandImage: null,
+ brandImageExists: false,
+ brandImageTimestamp: Date.now(),
+ uploading: false,
+ uploadCompleted: false,
+ error: ''
+ };
+ }
+
+ componentWillMount() {
+ $.get(Client.getAdminRoute() + '/get_brand_image?t=' + this.state.brandImageTimestamp).done(() => {
+ this.setState({brandImageExists: true});
+ });
+ }
+
+ handleImageChange() {
+ const element = $(this.refs.fileInput);
+
+ if (element.prop('files').length > 0) {
+ this.setState({
+ brandImage: element.prop('files')[0]
+ });
+ }
+ }
+
+ handleImageSubmit(e) {
+ e.preventDefault();
+
+ if (!this.state.brandImage) {
+ return;
+ }
+
+ if (this.state.uploading) {
+ return;
+ }
+
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('loading');
+
+ this.setState({
+ uploading: true,
+ error: ''
+ });
+
+ Client.uploadBrandImage(
+ this.state.brandImage,
+ () => {
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('complete');
+
+ this.setState({
+ brandImageExists: true,
+ brandImage: null,
+ brandImageTimestamp: Date.now(),
+ uploading: false
+ });
+ },
+ (err) => {
+ $(ReactDOM.findDOMNode(this.refs.upload)).button('reset');
+
+ this.setState({
+ uploading: false,
+ error: err.message
+ });
+ }
+ );
+ }
+
+ render() {
+ let btnClass = 'btn';
+ if (this.state.brandImage) {
+ btnClass += ' btn-primary';
+ }
+
+ let img = null;
+ if (this.state.brandImage) {
+ img = (
+ <img
+ ref='image'
+ className='brand-img'
+ src=''
+ />
+ );
+ } else if (this.state.brandImageExists) {
+ img = (
+ <img
+ className='brand-img'
+ src={Client.getAdminRoute() + '/get_brand_image?t=' + this.state.brandImageTimestamp}
+ />
+ );
+ } else {
+ img = (
+ <p>
+ <FormattedMessage
+ id='admin.team.noBrandImage'
+ defaultMessage='No brand image uploaded'
+ />
+ </p>
+ );
+ }
+
+ return (
+ <div className='form-group'>
+ <label className='control-label col-sm-4'>
+ <FormattedMessage
+ id='admin.team.brandImageTitle'
+ defaultMessage='Custom Brand Image:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ {img}
+ </div>
+ <div className='col-sm-4'/>
+ <div className='col-sm-8'>
+ <div className='file__upload'>
+ <button
+ className='btn btn-default'
+ disabled={this.props.disabled}
+ >
+ <FormattedMessage
+ id='admin.team.chooseImage'
+ defaultMessage='Choose New Image'
+ />
+ </button>
+ <input
+ ref='fileInput'
+ type='file'
+ accept='.jpg,.png,.bmp'
+ onChange={this.handleImageChange}
+ />
+ </div>
+ <button
+ className={btnClass}
+ disabled={this.props.disabled || !this.state.brandImage}
+ onClick={this.handleImageSubmit}
+ id='upload-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.team.uploading', 'Uploading..')}
+ data-complete-text={'<span class=\'glyphicon glyphicon-ok\'></span> ' + Utils.localizeMessage('admin.team.uploaded', 'Uploaded!')}
+ >
+ <FormattedMessage
+ id='admin.team.upload'
+ defaultMessage='Upload'
+ />
+ </button>
+ <br/>
+ <FormError error={this.state.error}/>
+ <p className='help-text no-margin'>
+ <FormattedHTMLMessage
+ id='admin.team.uploadDesc'
+ defaultMessage='Customize your user experience by adding a custom image to your login screen. See examples at <a href="http://docs.mattermost.com/administration/config-settings.html#custom-branding" target="_blank">docs.mattermost.com/administration/config-settings.html#custom-branding</a>.'
+ />
+ </p>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx
index 79b0d2210..a93f7a17c 100644
--- a/webapp/components/admin_console/compliance_reports.jsx
+++ b/webapp/components/admin_console/compliance_reports.jsx
@@ -273,7 +273,6 @@ export default class ComplianceReports extends React.Component {
defaultMessage='Compliance Reports'
/>
</h3>
-
<div className='row'>
<div className='col-sm-6 col-md-4 form-group'>
<label>
diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx
index 53f060e11..d31759150 100644
--- a/webapp/components/admin_console/compliance_settings.jsx
+++ b/webapp/components/admin_console/compliance_settings.jsx
@@ -1,83 +1,51 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from '../../utils/async_client.jsx';
-import * as Utils from '../../utils/utils.jsx';
+import React from 'react';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
-import ReactDOM from 'react-dom';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-export default class ComplianceSettings extends React.Component {
+export default class ComplianceSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.handleEnable = this.handleEnable.bind(this);
- this.handleDisable = this.handleDisable.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null,
- enable: this.props.config.ComplianceSettings.Enable
- };
- }
- handleChange() {
- this.setState({saveNeeded: true});
- }
- handleEnable() {
- this.setState({saveNeeded: true, enable: true});
- }
- handleDisable() {
- this.setState({saveNeeded: true, enable: false});
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enable: props.config.ComplianceSettings.Enable,
+ directory: props.config.ComplianceSettings.Directory,
+ enableDaily: props.config.ComplianceSettings.EnableDaily
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
- const config = this.props.config;
- const oldEnable = config.ComplianceSettings.Enable;
- config.ComplianceSettings.Enable = this.refs.Enable.checked;
- config.ComplianceSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
- config.ComplianceSettings.EnableDaily = this.refs.EnableDaily.checked;
+ getConfigFromState(config) {
+ config.ComplianceSettings.Enable = this.state.enable;
+ config.ComplianceSettings.Directory = this.state.directory;
+ config.ComplianceSettings.EnableDaily = this.state.enableDaily;
- Client.saveConfig(
- config,
- () => {
- $('#save-button').button('reset');
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- if (oldEnable !== config.ComplianceSettings.Enable) {
- window.location.reload();
- }
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
+ return config;
}
- render() {
- let serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.compliance.title'
+ defaultMessage='Compliance Settings'
+ />
+ </h3>
+ );
+ }
+ renderSettings() {
const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Compliance === 'true';
let bannerContent;
@@ -95,170 +63,64 @@ export default class ComplianceSettings extends React.Component {
}
return (
- <div className='wrapper--fixed'>
+ <SettingsGroup>
{bannerContent}
- <h3>
- <FormattedMessage
- id='admin.compliance.title'
- defaultMessage='Compliance Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.compliance.enableTitle'
- defaultMessage='Enable Compliance:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.ComplianceSettings.Enable}
- onChange={this.handleEnable}
- disabled={!licenseEnabled}
- />
- <FormattedMessage
- id='admin.compliance.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.ComplianceSettings.Enable}
- onChange={this.handleDisable}
- />
- <FormattedMessage
- id='admin.compliance.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.enableDesc'
- defaultMessage='When true, Mattermost allows compliance reporting'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Directory'
- >
- <FormattedMessage
- id='admin.compliance.directoryTitle'
- defaultMessage='Compliance Directory Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Directory'
- ref='Directory'
- placeholder={Utils.localizeMessage('admin.compliance.directoryExample', 'Ex "./data/"')}
- defaultValue={this.props.config.ComplianceSettings.Directory}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.directoryDescription'
- defaultMessage='Directory to which compliance reports are written. If blank, will be set to ./data/.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableDaily'
- >
- <FormattedMessage
- id='admin.compliance.enableDailyTitle'
- defaultMessage='Enable Daily Report:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDaily'
- value='true'
- ref='EnableDaily'
- defaultChecked={this.props.config.ComplianceSettings.EnableDaily}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <FormattedMessage
- id='admin.compliance.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDaily'
- value='false'
- defaultChecked={!this.props.config.ComplianceSettings.EnableDaily}
- disabled={!this.state.enable}
- />
- <FormattedMessage
- id='admin.compliance.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.compliance.enableDailyDesc'
- defaultMessage='When true, Mattermost will generate a daily compliance report.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.compliance.saving', 'Saving Config...')}
- >
- <FormattedMessage
- id='admin.compliance.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
- </form>
- </div>
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.compliance.enableTitle'
+ defaultMessage='Enable Compliance:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.enableDesc'
+ defaultMessage='When true, Mattermost allows compliance reporting'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled}
+ />
+ <TextSetting
+ id='directory'
+ label={
+ <FormattedMessage
+ id='admin.compliance.directoryTitle'
+ defaultMessage='Compliance Directory Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxOpenExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.directoryDescription'
+ defaultMessage='Directory to which compliance reports are written. If blank, will be set to ./data/.'
+ />
+ }
+ value={this.state.directory}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled || !this.state.enable}
+ />
+ <BooleanSetting
+ id='enableDaily'
+ label={
+ <FormattedMessage
+ id='admin.compliance.enableDailyTitle'
+ defaultMessage='Enable Daily Report:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.compliance.enableDailyDesc'
+ defaultMessage='When true, Mattermost will generate a daily compliance report.'
+ />
+ }
+ value={this.state.enableDaily}
+ onChange={this.handleChange}
+ disabled={!licenseEnabled || !this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-
-ComplianceSettings.propTypes = {
- config: React.PropTypes.object
-};
-
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/configuration_settings.jsx b/webapp/components/admin_console/configuration_settings.jsx
new file mode 100644
index 000000000..9521ed22c
--- /dev/null
+++ b/webapp/components/admin_console/configuration_settings.jsx
@@ -0,0 +1,76 @@
+// Copyright (c) 2015 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 {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+import ReloadConfigButton from './reload_config.jsx';
+
+export default class ConfigurationSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ listenAddress: props.config.ServiceSettings.ListenAddress
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.ListenAddress = this.state.listenAddress;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.general.configuration'
+ defaultMessage='Configuration'
+ />
+ }
+ >
+ <ReloadConfigButton/>
+ <TextSetting
+ id='listenAddress'
+ label={
+ <FormattedMessage
+ id='admin.service.listenAddress'
+ defaultMessage='Listen Address:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.listenExample', 'Ex ":8065"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.listenDescription'
+ defaultMessage='The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'
+ />
+ }
+ value={this.state.listenAddress}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/connection_security_dropdown_setting.jsx b/webapp/components/admin_console/connection_security_dropdown_setting.jsx
index 02b56b192..b3e9ac31c 100644
--- a/webapp/components/admin_console/connection_security_dropdown_setting.jsx
+++ b/webapp/components/admin_console/connection_security_dropdown_setting.jsx
@@ -8,63 +8,62 @@ import DropdownSetting from './dropdown_setting.jsx';
import {FormattedMessage} from 'react-intl';
const CONNECTION_SECURITY_HELP_TEXT = (
- <div className='help-text'>
- <table
- className='table table-bordered table-margin--none'
- cellPadding='5'
- >
- <tbody>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityNone'
- defaultMessage='None'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityNoneDescription'
- defaultMessage='Mattermost will connect over an unsecure connection.'
- />
- </td>
- </tr>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityTls'
- defaultMessage='TLS'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityTlsDescription'
- defaultMessage='Encrypts the communication between Mattermost and your server.'
- />
- </td>
- </tr>
- <tr>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityStart'
- defaultMessage='STARTTLS'
- />
- </td>
- <td className='help-text'>
- <FormattedMessage
- id='admin.connectionSecurityStartDescription'
- defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
- />
- </td>
- </tr>
- </tbody>
- </table>
- </div>
+ <table
+ className='table table-bordered table-margin--none'
+ cellPadding='5'
+ >
+ <tbody>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityNone'
+ defaultMessage='None'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityNoneDescription'
+ defaultMessage='Mattermost will connect over an unsecure connection.'
+ />
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityTls'
+ defaultMessage='TLS'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityTlsDescription'
+ defaultMessage='Encrypts the communication between Mattermost and your server.'
+ />
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityStart'
+ defaultMessage='STARTTLS'
+ />
+ </td>
+ <td className='help-text'>
+ <FormattedMessage
+ id='admin.connectionSecurityStartDescription'
+ defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
);
export default class ConnectionSecurityDropdownSetting extends React.Component {
render() {
return (
<DropdownSetting
+ id='connectionSecurity'
values={[
{value: '', text: Utils.localizeMessage('admin.connectionSecurityNone', 'None')},
{value: 'TLS', text: Utils.localizeMessage('admin.connectionSecurityTls', 'TLS (Recommended)')},
@@ -76,11 +75,10 @@ export default class ConnectionSecurityDropdownSetting extends React.Component {
defaultMessage='Connection Security:'
/>
}
- currentValue={this.props.currentValue}
- handleChange={this.props.handleChange}
- isDisabled={this.props.isDisabled}
+ value={this.props.value}
+ onChange={this.props.onChange}
+ disabled={this.props.disabled}
helpText={CONNECTION_SECURITY_HELP_TEXT}
- margin='small'
/>
);
}
@@ -89,7 +87,7 @@ ConnectionSecurityDropdownSetting.defaultProps = {
};
ConnectionSecurityDropdownSetting.propTypes = {
- currentValue: React.PropTypes.string.isRequired,
- handleChange: React.PropTypes.func.isRequired,
- isDisabled: React.PropTypes.bool.isRequired
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired
};
diff --git a/webapp/components/admin_console/connection_settings.jsx b/webapp/components/admin_console/connection_settings.jsx
new file mode 100644
index 000000000..59b32ec23
--- /dev/null
+++ b/webapp/components/admin_console/connection_settings.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 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 BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class ConnectionSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ allowCorsFrom: props.config.ServiceSettings.AllowCorsFrom,
+ enableInsecureOutgoingConnections: props.config.ServiceSettings.EnableInsecureOutgoingConnections
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.AllowCorsFrom = this.state.allowCorsFrom;
+ config.ServiceSettings.EnableInsecureOutgoingConnections = this.state.enableInsecureOutgoingConnections;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.connection'
+ defaultMessage='Connections'
+ />
+ }
+ >
+ <TextSetting
+ id='allowCorsFrom'
+ label={
+ <FormattedMessage
+ id='admin.service.corsTitle'
+ defaultMessage='Allow Cross-origin Requests from:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.corsEx', 'http://example.com')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.corsDescription'
+ defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
+ />
+ }
+ value={this.state.allowCorsFrom}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableInsecureOutgoingConnections'
+ label={
+ <FormattedMessage
+ id='admin.service.insecureTlsTitle'
+ defaultMessage='Enable Insecure Outgoing Connections: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.insecureTlsDesc'
+ defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.'
+ />
+ }
+ value={this.state.enableInsecureOutgoingConnections}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx
new file mode 100644
index 000000000..307bbad8c
--- /dev/null
+++ b/webapp/components/admin_console/custom_brand_settings.jsx
@@ -0,0 +1,137 @@
+// Copyright (c) 2015 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 BooleanSetting from './boolean_setting.jsx';
+import BrandImageSetting from './brand_image_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class CustomBrandSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ siteName: props.config.TeamSettings.SiteName,
+ enableCustomBrand: props.config.TeamSettings.EnableCustomBrand,
+ customBrandText: props.config.TeamSettings.CustomBrandText
+ });
+ }
+
+ getConfigFromState(config) {
+ config.TeamSettings.SiteName = this.state.siteName;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
+ config.TeamSettings.EnableCustomBrand = this.state.enableCustomBrand;
+ config.TeamSettings.CustomBrandText = this.state.customBrandText;
+ }
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.customization.title'
+ defaultMessage='Customization Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const enterpriseSettings = [];
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
+ enterpriseSettings.push(
+ <BooleanSetting
+ key='enableCustomBrand'
+ id='enableCustomBrand'
+ label={
+ <FormattedMessage
+ id='admin.team.brandTitle'
+ defaultMessage='Enable Custom Branding: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.brandDesc'
+ defaultMessage='Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.'
+ />
+ }
+ value={this.state.enableCustomBrand}
+ onChange={this.handleChange}
+ />
+ );
+
+ enterpriseSettings.push(
+ <BrandImageSetting
+ key='customBrandImage'
+ disabled={!this.state.enableCustomBrand}
+ />
+ );
+
+ enterpriseSettings.push(
+ <TextSetting
+ key='customBrandText'
+ id='customBrandText'
+ type='textarea'
+ label={
+ <FormattedMessage
+ id='admin.team.brandTextTitle'
+ defaultMessage='Custom Brand Text:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.brandTextDescription'
+ defaultMessage='The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.'
+ />
+ }
+ value={this.state.customBrandText}
+ onChange={this.handleChange}
+ disabled={!this.state.enableCustomBrand}
+ />
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.customization.customBrand'
+ defaultMessage='Custom Branding'
+ />
+ }
+ >
+ <TextSetting
+ id='siteName'
+ label={
+ <FormattedMessage
+ id='admin.team.siteNameTitle'
+ defaultMessage='Site Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.siteNameExample', 'Ex "Mattermost"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.siteNameDescription'
+ defaultMessage='Name of service shown in login screens and UI.'
+ />
+ }
+ value={this.state.siteName}
+ onChange={this.handleChange}
+ />
+ {enterpriseSettings}
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/database_settings.jsx b/webapp/components/admin_console/database_settings.jsx
new file mode 100644
index 000000000..97a6b692c
--- /dev/null
+++ b/webapp/components/admin_console/database_settings.jsx
@@ -0,0 +1,194 @@
+// Copyright (c) 2015 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 BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+import RecycleDbButton from './recycle_db.jsx';
+
+export default class DatabaseSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ driverName: this.props.config.SqlSettings.DriverName,
+ dataSource: this.props.config.SqlSettings.DataSource,
+ dataSourceReplicas: this.props.config.SqlSettings.DataSourceReplicas,
+ maxIdleConns: props.config.SqlSettings.MaxIdleConns,
+ maxOpenConns: props.config.SqlSettings.MaxOpenConns,
+ atRestEncryptKey: props.config.SqlSettings.AtRestEncryptKey,
+ trace: props.config.SqlSettings.Trace
+ });
+ }
+
+ getConfigFromState(config) {
+ // driverName, dataSource, and dataSourceReplicas are read-only from the UI
+
+ config.SqlSettings.MaxIdleConns = this.parseIntNonZero(this.state.maxIdleConns);
+ config.SqlSettings.MaxOpenConns = this.parseIntNonZero(this.state.maxOpenConns);
+ config.SqlSettings.AtRestEncryptKey = this.state.atRestEncryptKey;
+ config.SqlSettings.Trace = this.state.trace;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.database.title'
+ defaultMessage='Database Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const dataSource = '**********' + this.state.dataSource.substring(this.state.dataSource.indexOf('@'));
+
+ let dataSourceReplicas = '';
+ this.state.dataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.state.dataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <SettingsGroup>
+ <p>
+ <FormattedMessage
+ id='admin.sql.noteDescription'
+ defaultMessage='Changing properties in this section will require a server restart before taking effect.'
+ />
+ </p>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ <FormattedMessage
+ id='admin.sql.driverName'
+ defaultMessage='Driver Name:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.state.driverName}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ <FormattedMessage
+ id='admin.sql.dataSource'
+ defaultMessage='Data Source:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ <FormattedMessage
+ id='admin.sql.replicas'
+ defaultMessage='Data Source Replicas:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+ <TextSetting
+ id='maxIdleConns'
+ label={
+ <FormattedMessage
+ id='admin.sql.maxConnectionsTitle'
+ defaultMessage='Maximum Idle Connections:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxConnectionsExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.maxConnectionsDescription'
+ defaultMessage='Maximum number of idle connections held open to the database.'
+ />
+ }
+ value={this.state.maxIdleConns}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='maxOpenConns'
+ label={
+ <FormattedMessage
+ id='admin.sql.maxOpenTitle'
+ defaultMessage='Maximum Open Connections:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.maxOpenExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.maxOpenDescription'
+ defaultMessage='Maximum number of open connections held open to the database.'
+ />
+ }
+ value={this.state.maxOpenConns}
+ onChange={this.handleChange}
+ />
+ <GeneratedSetting
+ id='atRestEncryptKey'
+ label={
+ <FormattedMessage
+ id='admin.sql.keyTitle'
+ defaultMessage='At Rest Encrypt Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.sql.keyExample', 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"')}
+ helpText={
+ <FormattedMessage
+ id='admin.sql.keyDescription'
+ defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
+ />
+ }
+ value={this.state.atRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='trace'
+ label={
+ <FormattedMessage
+ id='admin.sql.traceTitle'
+ defaultMessage='Trace: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.sql.traceDescription'
+ defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
+ />
+ }
+ value={this.state.trace}
+ onChange={this.handleChange}
+ />
+ <RecycleDbButton/>
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/developer_settings.jsx b/webapp/components/admin_console/developer_settings.jsx
new file mode 100644
index 000000000..9b153ed26
--- /dev/null
+++ b/webapp/components/admin_console/developer_settings.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class DeveloperSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableTesting: props.config.ServiceSettings.EnableTesting,
+ enableDeveloper: props.config.ServiceSettings.EnableDeveloper
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.EnableTesting = this.state.enableTesting;
+ config.ServiceSettings.EnableDeveloper = this.state.enableDeveloper;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.developer.title'
+ defaultMessage='Developer Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup>
+ <BooleanSetting
+ id='enableTesting'
+ label={
+ <FormattedMessage
+ id='admin.service.testingTitle'
+ defaultMessage='Enable Testing: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.testingDescription'
+ defaultMessage='(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'
+ />
+ }
+ value={this.state.enableTesting}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableDeveloper'
+ label={
+ <FormattedMessage
+ id='admin.service.developerTitle'
+ defaultMessage='Enable Developer Mode: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.developerDesc'
+ defaultMessage='(Developer Option) When true, extra information around errors will be displayed in the UI.'
+ />
+ }
+ value={this.state.enableDeveloper}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/dropdown_setting.jsx b/webapp/components/admin_console/dropdown_setting.jsx
index fca8dd170..cf733ec90 100644
--- a/webapp/components/admin_console/dropdown_setting.jsx
+++ b/webapp/components/admin_console/dropdown_setting.jsx
@@ -6,6 +6,16 @@ import React from 'react';
import Setting from './setting.jsx';
export default class DropdownSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
render() {
const options = [];
for (const {value, text} of this.props.values) {
@@ -22,30 +32,33 @@ export default class DropdownSetting extends React.Component {
return (
<Setting
label={this.props.label}
- margin={this.props.margin}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
>
<select
className='form-control'
- value={this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ id={this.props.id}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
>
{options}
</select>
- {this.props.helpText}
</Setting>
);
}
}
+
DropdownSetting.defaultProps = {
+ isDisabled: false
};
DropdownSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
values: React.PropTypes.array.isRequired,
label: React.PropTypes.node.isRequired,
- currentValue: React.PropTypes.string.isRequired,
- handleChange: React.PropTypes.func.isRequired,
- isDisabled: React.PropTypes.bool.isRequired,
- helpText: React.PropTypes.node.isRequired,
- margin: React.PropTypes.oneOf(['', 'small'])
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
};
diff --git a/webapp/components/admin_console/email_authentication_settings.jsx b/webapp/components/admin_console/email_authentication_settings.jsx
new file mode 100644
index 000000000..2f5c423bf
--- /dev/null
+++ b/webapp/components/admin_console/email_authentication_settings.jsx
@@ -0,0 +1,109 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class EmailAuthenticationSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableSignUpWithEmail: props.config.EmailSettings.EnableSignUpWithEmail,
+ enableSignInWithEmail: props.config.EmailSettings.EnableSignInWithEmail,
+ enableSignInWithUsername: props.config.EmailSettings.EnableSignInWithUsername
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.EnableSignUpWithEmail = this.state.enableSignUpWithEmail;
+ config.EmailSettings.EnableSignInWithEmail = this.state.enableSignInWithEmail;
+ config.EmailSettings.EnableSignInWithUsername = this.state.enableSignInWithUsername;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.authentication.email'
+ defaultMessage='Email'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableSignUpWithEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.allowSignupTitle'
+ defaultMessage='Allow Sign Up With Email: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowSignupDescription'
+ defaultMessage='When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'
+ />
+ }
+ value={this.state.enableSignUpWithEmail}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableSignInWithEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.allowEmailSignInTitle'
+ defaultMessage='Allow Sign In With Email: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowEmailSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
+ />
+ }
+ value={this.state.enableSignInWithEmail}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableSignInWithUsername'
+ label={
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInTitle'
+ defaultMessage='Allow Sign In With Username: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
+ />
+ }
+ value={this.state.enableSignInWithUsername}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/email_connection_test.jsx b/webapp/components/admin_console/email_connection_test.jsx
new file mode 100644
index 000000000..87612e4d5
--- /dev/null
+++ b/webapp/components/admin_console/email_connection_test.jsx
@@ -0,0 +1,118 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class EmailConnectionTestButton extends React.Component {
+ static get propTypes() {
+ return {
+ config: React.PropTypes.object.isRequired,
+ disabled: React.PropTypes.bool.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleTestConnection = this.handleTestConnection.bind(this);
+
+ this.state = {
+ testing: false,
+ success: false,
+ fail: null
+ };
+ }
+
+ handleTestConnection(e) {
+ e.preventDefault();
+
+ this.setState({
+ testing: true,
+ success: false,
+ fail: null
+ });
+
+ Client.testEmail(
+ this.props.config,
+ () => {
+ this.setState({
+ testing: false,
+ success: true
+ });
+ },
+ (err) => {
+ this.setState({
+ testing: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.success) {
+ testMessage = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>
+ <FormattedMessage
+ id='admin.email.emailSuccess'
+ defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
+ />
+ </div>
+ );
+ } else if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.email.emailFail'
+ defaultMessage='Connection unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.testing) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.email.testing', 'Testing...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.email.connectionSecurityTest'
+ defaultMessage='Test Connection'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group email-connection-test'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div className='help-text'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleTestConnection}
+ disabled={this.props.disabled}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx
index dcdf52486..5067b562b 100644
--- a/webapp/components/admin_console/email_settings.jsx
+++ b/webapp/components/admin_console/email_settings.jsx
@@ -1,1052 +1,232 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import React from 'react';
import * as Utils from 'utils/utils.jsx';
-import Constants from 'utils/constants.jsx';
-var holders = defineMessages({
- notificationDisplayExample: {
- id: 'admin.email.notificationDisplayExample',
- defaultMessage: 'Ex: "Mattermost Notification", "System", "No-Reply"'
- },
- notificationEmailExample: {
- id: 'admin.email.notificationEmailExample',
- defaultMessage: 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
- },
- smtpUsernameExample: {
- id: 'admin.email.smtpUsernameExample',
- defaultMessage: 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
- },
- smtpPasswordExample: {
- id: 'admin.email.smtpPasswordExample',
- defaultMessage: 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- smtpServerExample: {
- id: 'admin.email.smtpServerExample',
- defaultMessage: 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
- },
- smtpPortExample: {
- id: 'admin.email.smtpPortExample',
- defaultMessage: 'Ex: "25", "465"'
- },
- inviteSaltExample: {
- id: 'admin.email.inviteSaltExample',
- defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
- },
- passwordSaltExample: {
- id: 'admin.email.passwordSaltExample',
- defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
- },
- testing: {
- id: 'admin.email.testing',
- defaultMessage: 'Testing...'
- },
- saving: {
- id: 'admin.email.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import EmailConnectionTest from './email_connection_test.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class EmailSettings extends React.Component {
+export default class EmailSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleTestConnection = this.handleTestConnection.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.buildConfig = this.buildConfig.bind(this);
- this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
- this.handleGenerateReset = this.handleGenerateReset.bind(this);
- this.handleSendPushNotificationsChange = this.handleSendPushNotificationsChange.bind(this);
- this.handlePushServerChange = this.handlePushServerChange.bind(this);
- this.handleAgreeChange = this.handleAgreeChange.bind(this);
-
- let sendNotificationValue;
- let agree = false;
- if (!props.config.EmailSettings.SendPushNotifications) {
- sendNotificationValue = 'off';
- } else if (props.config.EmailSettings.PushNotificationServer === Constants.MHPNS && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
- sendNotificationValue = 'mhpns';
- agree = true;
- } else if (props.config.EmailSettings.PushNotificationServer === Constants.MTPNS) {
- sendNotificationValue = 'mtpns';
- } else {
- sendNotificationValue = 'self';
- }
-
- let pushNotificationServer = this.props.config.EmailSettings.PushNotificationServer;
- if (sendNotificationValue === 'mtpns') {
- pushNotificationServer = Constants.MTPNS;
- } else if (sendNotificationValue === 'mhpns') {
- pushNotificationServer = Constants.MHPNS;
- }
-
- this.state = {
- sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
- sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
- saveNeeded: false,
- serverError: null,
- emailSuccess: null,
- emailFail: null,
- pushNotificationContents: this.props.config.EmailSettings.PushNotificationContents,
- connectionSecurity: this.props.config.EmailSettings.ConnectionSecurity,
- sendNotificationValue,
- pushNotificationServer,
- agree
- };
- }
-
- handleChange(action) {
- const s = {saveNeeded: true};
-
- if (action === 'sendEmailNotifications_true') {
- s.sendEmailNotifications = true;
- }
-
- if (action === 'sendEmailNotifications_false') {
- s.sendEmailNotifications = false;
- }
-
- if (action === 'sendPushNotifications_true') {
- s.sendPushNotifications = true;
- }
-
- if (action === 'sendPushNotifications_false') {
- s.sendPushNotifications = false;
- }
-
- this.setState(s);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ sendEmailNotifications: props.config.EmailSettings.SendEmailNotifications,
+ feedbackName: props.config.EmailSettings.FeedbackName,
+ feedbackEmail: props.config.EmailSettings.FeedbackEmail,
+ smtpUsername: props.config.EmailSettings.SMTPUsername,
+ smtpPassword: props.config.EmailSettings.SMTPPassword,
+ smtpServer: props.config.EmailSettings.SMTPServer,
+ smtpPort: props.config.EmailSettings.SMTPPort,
+ connectionSecurity: props.config.EmailSettings.ConnectionSecurity,
+ enableSecurityFixAlert: props.config.ServiceSettings.EnableSecurityFixAlert
+ });
}
- buildConfig() {
- const config = this.props.config;
- config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
- config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
- config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
- config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
- config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
- config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
- config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
- config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
- config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
- config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
- config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
- config.EmailSettings.ConnectionSecurity = this.state.connectionSecurity.trim();
-
- config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim();
- if (config.EmailSettings.InviteSalt === '') {
- config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
- }
-
- config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim();
- if (config.EmailSettings.PasswordResetSalt === '') {
- config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
- }
-
- const sendPushNotifications = this.refs.sendPushNotifications.value;
- if (sendPushNotifications === 'off') {
- config.EmailSettings.SendPushNotifications = false;
- } else {
- config.EmailSettings.SendPushNotifications = true;
- }
-
- if (this.refs.PushNotificationServer) {
- config.EmailSettings.PushNotificationServer = this.refs.PushNotificationServer.value.trim();
- }
-
- if (this.refs.PushNotificationContents) {
- config.EmailSettings.PushNotificationContents = this.refs.PushNotificationContents.value;
- }
+ getConfigFromState(config) {
+ config.EmailSettings.SendEmailNotifications = this.state.sendEmailNotifications;
+ config.EmailSettings.FeedbackName = this.state.feedbackName;
+ config.EmailSettings.FeedbackEmail = this.state.feedbackEmail;
+ config.EmailSettings.SMTPUsername = this.state.smtpUsername;
+ config.EmailSettings.SMTPPassword = this.state.smtpPassword;
+ config.EmailSettings.SMTPServer = this.state.smtpServer;
+ config.EmailSettings.SMTPPort = this.state.smtpPort;
+ config.EmailSettings.ConnectionSecurity = this.state.connectionSecurity;
+ config.ServiceSettings.EnableSecurityFixAlert = this.state.enableSecurityFixAlert;
return config;
}
- handleSendPushNotificationsChange(e) {
- const sendNotificationValue = e.target.value;
- let pushNotificationServer = this.state.pushNotificationServer;
- if (sendNotificationValue === 'mtpns') {
- pushNotificationServer = Constants.MTPNS;
- } else if (sendNotificationValue === 'mhpns') {
- pushNotificationServer = Constants.MHPNS;
- }
- this.setState({saveNeeded: true, sendNotificationValue, pushNotificationServer, agree: false});
- }
-
- handlePushServerChange(e) {
- this.setState({saveNeeded: true, pushNotificationServer: e.target.value});
- }
-
- handleAgreeChange(e) {
- this.setState({agree: e.target.checked});
- }
-
- handleGenerateInvite(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleGenerateReset(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleTestConnection(e) {
- e.preventDefault();
- $('#connection-button').button('loading');
-
- var config = this.buildConfig();
-
- Client.testEmail(
- config,
- () => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: true,
- emailSuccess: true,
- emailFail: null
- });
- $('#connection-button').button('reset');
- },
- (err) => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: true,
- emailSuccess: null,
- emailFail: err.message + ' - ' + err.detailed_error
- });
- $('#connection-button').button('reset');
- }
- );
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.buildConfig();
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: null,
- saveNeeded: false,
- emailSuccess: null,
- emailFail: null
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
- serverError: err.message,
- saveNeeded: true,
- emailSuccess: null,
- emailFail: null
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.notifications.title'
+ defaultMessage='Notification Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var emailSuccess = '';
- if (this.state.emailSuccess) {
- emailSuccess = (
- <div className='alert alert-success'>
- <i className='fa fa-check'></i>
- <FormattedMessage
- id='admin.email.emailSuccess'
- defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
- />
- </div>
- );
- }
-
- var emailFail = '';
- if (this.state.emailFail) {
- emailSuccess = (
- <div className='alert alert-warning'>
- <i className='fa fa-warning'></i>
- <FormattedMessage
- id='admin.email.emailFail'
- defaultMessage='Connection unsuccessful: {error}'
- values={{
- error: this.state.emailFail
- }}
- />
- </div>
- );
- }
-
- let mhpnsOption;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
- mhpnsOption = <option value='mhpns'>{Utils.localizeMessage('admin.email.mhpns', 'Use encrypted, production-quality HPNS connection to iOS and Android apps')}</option>;
- }
-
- let disableSave = !this.state.saveNeeded;
-
- let tosCheckbox;
- if (this.state.sendNotificationValue === 'mhpns') {
- tosCheckbox = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- >
- {''}
- </label>
- <div className='col-sm-8'>
- <input
- type='checkbox'
- ref='agree'
- checked={this.state.agree}
- onChange={this.handleAgreeChange}
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.notifications.email'
+ defaultMessage='Email'
+ />
+ }
+ >
+ <BooleanSetting
+ id='sendEmailNotifications'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationsTitle'
+ defaultMessage='Send Email Notifications: '
/>
+ }
+ helpText={
<FormattedHTMLMessage
- id='admin.email.agreeHPNS'
- defaultMessage=' 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>.'
+ id='admin.email.notificationsDescription'
+ defaultMessage='Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'
/>
- </div>
- </div>
- );
-
- disableSave = disableSave || !this.state.agree;
- }
-
- let sendHelpText;
- let pushServerHelpText;
- if (this.state.sendNotificationValue === 'off') {
- sendHelpText = (
- <FormattedHTMLMessage
- id='admin.email.pushOffHelp'
- defaultMessage='Please see <a href="http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices" target="_blank">documentation on push notifications</a> to learn more about setup options.'
+ }
+ value={this.state.sendEmailNotifications}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='feedbackName'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationDisplayTitle'
+ defaultMessage='Notification Display Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.notificationDisplayExample', 'Ex: "Mattermost Notification", "System", "No-Reply"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.notificationDisplayDescription'
+ defaultMessage='Display name on email account used when sending notification emails from Mattermost.'
+ />
+ }
+ value={this.state.feedbackName}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else if (this.state.sendNotificationValue === 'mhpns') {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.mhpnsHelp'
- defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.'
+ <TextSetting
+ id='feedbackEmail'
+ label={
+ <FormattedMessage
+ id='admin.email.notificationEmailTitle'
+ defaultMessage='Notification Email Address:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.notificationEmailExample', 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.notificationEmailDescription'
+ defaultMessage='Email address displayed on email account used when sending notification emails from Mattermost.'
+ />
+ }
+ value={this.state.feedbackEmail}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else if (this.state.sendNotificationValue === 'mtpns') {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.mtpnsHelp'
- defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.'
+ <TextSetting
+ id='smtpUsername'
+ label={
+ <FormattedMessage
+ id='admin.email.smtpUsernameTitle'
+ defaultMessage='SMTP Username:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpUsernameExample', 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpUsernameDescription'
+ defaultMessage=' Obtain this credential from administrator setting up your email server.'
+ />
+ }
+ value={this.state.smtpUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- } else {
- pushServerHelpText = (
- <FormattedHTMLMessage
- id='admin.email.easHelp'
- defaultMessage='Learn more about compiling and deploying your own mobile apps from an <a href="http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas" target="_blank">Enterprise App Store</a>.'
+ <TextSetting
+ id='smtpPassword'
+ label={
+ <FormattedMessage
+ id='admin.email.smtpPasswordTitle'
+ defaultMessage='SMTP Password:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpPasswordExample', 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpPasswordDescription'
+ defaultMessage=' Obtain this credential from administrator setting up your email server.'
+ />
+ }
+ value={this.state.smtpPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- );
- }
-
- const sendPushNotifications = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='sendPushNotifications'
- >
- <FormattedMessage
- id='admin.email.pushTitle'
- defaultMessage='Send Push Notifications: '
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='sendPushNotifications'
- ref='sendPushNotifications'
- value={this.state.sendNotificationValue}
- onChange={this.handleSendPushNotificationsChange}
- >
- <option value='off'>{Utils.localizeMessage('admin.email.pushOff', 'Do not send push notifications')}</option>
- {mhpnsOption}
- <option value='mtpns'>{Utils.localizeMessage('admin.email.mtpns', 'Use iOS and Android apps on iTunes and Google Play with TPNS')}</option>
- <option value='self'>{Utils.localizeMessage('admin.email.selfPush', 'Manually enter Push Notification Service location')}</option>
- </select>
- <p className='help-text'>
- {sendHelpText}
- </p>
- </div>
- </div>
- );
-
- let pushNotificationServer;
- let pushNotificationContent;
- if (this.state.sendNotificationValue !== 'off') {
- pushNotificationServer = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PushNotificationServer'
- >
+ <TextSetting
+ id='smtpServer'
+ label={
<FormattedMessage
- id='admin.email.pushServerTitle'
- defaultMessage='Push Notification Server:'
+ id='admin.email.smtpServerTitle'
+ defaultMessage='SMTP Server:'
/>
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PushNotificationServer'
- ref='PushNotificationServer'
- placeholder={Utils.localizeMessage('admin.email.pushServerEx', 'E.g.: "http://push-test.mattermost.com"')}
- value={this.state.pushNotificationServer}
- onChange={this.handlePushServerChange}
- disabled={this.state.sendNotificationValue !== 'self'}
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpServerExample', 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpServerDescription'
+ defaultMessage='Location of SMTP email server.'
/>
- <p className='help-text'>
- {pushServerHelpText}
- </p>
- </div>
- </div>
- );
-
- pushNotificationContent = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='pushNotificationContents'
- >
+ }
+ value={this.state.smtpServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <TextSetting
+ id='smtpPort'
+ label={
<FormattedMessage
- id='admin.email.pushContentTitle'
- defaultMessage='Push Notification Contents:'
+ id='admin.email.smtpPortTitle'
+ defaultMessage='SMTP Port:'
/>
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='pushNotificationContents'
- ref='PushNotificationContents'
- defaultValue={this.props.config.EmailSettings.PushNotificationContents}
- onChange={this.handleChange.bind(this, 'pushNotificationContents')}
- >
- <option value='generic'>{Utils.localizeMessage('admin.email.genericPushNotification', 'Send generic description with user and channel names')}</option>
- <option value='full'>{Utils.localizeMessage('admin.email.fullPushNotification', 'Send full message snippet')}</option>
- </select>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.email.pushContentDesc'
- defaultMessage='Selecting "Send generic description with user and channel names" provides push notifications with generic messages, including names of users and channels but no specific details from the message text.<br /><br />
- Selecting "Send full message snippet" sends excerpts from messages triggering notifications with specifics and may include confidential information sent in messages. If your Push Notification Service is outside your firewall, it is HIGHLY RECOMMENDED this option only be used with an "https" protocol to encrypt the connection.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
- <h3>
- <FormattedMessage
- id='admin.email.emailSettings'
- defaultMessage='Email Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignUpWithEmail'
- >
- <FormattedMessage
- id='admin.email.allowSignupTitle'
- defaultMessage='Allow Sign Up With Email: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignUpWithEmail'
- value='true'
- ref='allowSignUpWithEmail'
- defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignUpWithEmail'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowSignupDescription'
- defaultMessage='When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignInWithEmail'
- >
- <FormattedMessage
- id='admin.email.allowEmailSignInTitle'
- defaultMessage='Allow Sign In With Email: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithEmail'
- value='true'
- ref='allowSignInWithEmail'
- defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithEmail'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail}
- onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowEmailSignInDescription'
- defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='allowSignInWithUsername'
- >
- <FormattedMessage
- id='admin.email.allowUsernameSignInTitle'
- defaultMessage='Allow Sign In With Username: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithUsername'
- value='true'
- ref='allowSignInWithUsername'
- defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername}
- onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='allowSignInWithUsername'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername}
- onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.allowUsernameSignInDescription'
- defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='sendEmailNotifications'
- >
- <FormattedMessage
- id='admin.email.notificationsTitle'
- defaultMessage='Send Email Notifications: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='sendEmailNotifications'
- value='true'
- ref='sendEmailNotifications'
- defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
- onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='sendEmailNotifications'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
- onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.email.notificationsDescription'
- defaultMessage='Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='requireEmailVerification'
- >
- <FormattedMessage
- id='admin.email.requireVerificationTitle'
- defaultMessage='Require Email Verification: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='requireEmailVerification'
- value='true'
- ref='requireEmailVerification'
- defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
- onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
- disabled={!this.state.sendEmailNotifications}
- />
- <FormattedMessage
- id='admin.email.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='requireEmailVerification'
- value='false'
- defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
- onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
- disabled={!this.state.sendEmailNotifications}
- />
- <FormattedMessage
- id='admin.email.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.requireVerificationDescription'
- defaultMessage='Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackName'
- >
- <FormattedMessage
- id='admin.email.notificationDisplayTitle'
- defaultMessage='Notification Display Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackName'
- ref='feedbackName'
- placeholder={formatMessage(holders.notificationDisplayExample)}
- defaultValue={this.props.config.EmailSettings.FeedbackName}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.notificationDisplayDescription'
- defaultMessage='Display name on email account used when sending notification emails from Mattermost.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackEmail'
- >
- <FormattedMessage
- id='admin.email.notificationEmailTitle'
- defaultMessage='Notification Email Address:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='email'
- className='form-control'
- id='feedbackEmail'
- ref='feedbackEmail'
- placeholder={formatMessage(holders.notificationEmailExample)}
- defaultValue={this.props.config.EmailSettings.FeedbackEmail}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.notificationEmailDescription'
- defaultMessage='Email address displayed on email account used when sending notification emails from Mattermost.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPUsername'
- >
- <FormattedMessage
- id='admin.email.smtpUsernameTitle'
- defaultMessage='SMTP Username:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPUsername'
- ref='SMTPUsername'
- placeholder={formatMessage(holders.smtpUsernameExample)}
- defaultValue={this.props.config.EmailSettings.SMTPUsername}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpUsernameDescription'
- defaultMessage=' Obtain this credential from administrator setting up your email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPPassword'
- >
- <FormattedMessage
- id='admin.email.smtpPasswordTitle'
- defaultMessage='SMTP Password:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPPassword'
- ref='SMTPPassword'
- placeholder={formatMessage(holders.smtpPasswordExample)}
- defaultValue={this.props.config.EmailSettings.SMTPPassword}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpPasswordDescription'
- defaultMessage=' Obtain this credential from administrator setting up your email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPServer'
- >
- <FormattedMessage
- id='admin.email.smtpServerTitle'
- defaultMessage='SMTP Server:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPServer'
- ref='SMTPServer'
- placeholder={formatMessage(holders.smtpServerExample)}
- defaultValue={this.props.config.EmailSettings.SMTPServer}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpServerDescription'
- defaultMessage='Location of SMTP email server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SMTPPort'
- >
- <FormattedMessage
- id='admin.email.smtpPortTitle'
- defaultMessage='SMTP Port:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SMTPPort'
- ref='SMTPPort'
- placeholder={formatMessage(holders.smtpPortExample)}
- defaultValue={this.props.config.EmailSettings.SMTPPort}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.smtpPortDescription'
- defaultMessage='Port of SMTP email server.'
- />
- </p>
- </div>
- </div>
-
- <ConnectionSecurityDropdownSetting
- currentValue={this.state.connectionSecurity}
- handleChange={(e) => this.setState({connectionSecurity: e.target.value, saveNeeded: true})}
- isDisabled={!this.state.sendEmailNotifications}
- />
- <div className='form-group'>
- <div className='col-sm-offset-4 col-sm-8'>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleTestConnection}
- disabled={!this.state.sendEmailNotifications}
- id='connection-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.testing)}
- >
- <FormattedMessage
- id='admin.email.connectionSecurityTest'
- defaultMessage='Test Connection'
- />
- </button>
- {emailSuccess}
- {emailFail}
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='InviteSalt'
- >
- <FormattedMessage
- id='admin.email.inviteSaltTitle'
- defaultMessage='Invite Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='InviteSalt'
- ref='InviteSalt'
- placeholder={formatMessage(holders.inviteSaltExample)}
- defaultValue={this.props.config.EmailSettings.InviteSalt}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.inviteSaltDescription'
- defaultMessage='32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerateInvite}
- disabled={!this.state.sendEmailNotifications}
- >
- <FormattedMessage
- id='admin.email.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PasswordResetSalt'
- >
- <FormattedMessage
- id='admin.email.passwordSaltTitle'
- defaultMessage='Password Reset Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PasswordResetSalt'
- ref='PasswordResetSalt'
- placeholder={formatMessage(holders.passwordSaltExample)}
- defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
- onChange={this.handleChange}
- disabled={!this.state.sendEmailNotifications}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.email.passwordSaltDescription'
- defaultMessage='32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerateReset}
- disabled={!this.state.sendEmailNotifications}
- >
- <FormattedMessage
- id='admin.email.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- {sendPushNotifications}
- {tosCheckbox}
- {pushNotificationServer}
- {pushNotificationContent}
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={disableSave}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.email.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ placeholder={Utils.localizeMessage('admin.email.smtpPortExample', 'Ex: "25", "465"')}
+ helpText={
+ <FormattedMessage
+ id='admin.email.smtpPortDescription'
+ defaultMessage='Port of SMTP email server.'
+ />
+ }
+ value={this.state.smtpPort}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <ConnectionSecurityDropdownSetting
+ value={this.state.connectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <EmailConnectionTest
+ config={this.getConfigFromState(this.props.config)}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <BooleanSetting
+ id='enableSecurityFixAlert'
+ label={
+ <FormattedMessage
+ id='admin.service.securityTitle'
+ defaultMessage='Enable Security Alerts: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.securityDesc'
+ defaultMessage='When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'
+ />
+ }
+ value={this.state.enableSecurityFixAlert}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-EmailSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(EmailSettings);
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx
new file mode 100644
index 000000000..88c6c28ea
--- /dev/null
+++ b/webapp/components/admin_console/external_service_settings.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 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 {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class ExternalServiceSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ segmentDeveloperKey: props.config.ServiceSettings.SegmentDeveloperKey,
+ googleDeveloperKey: props.config.ServiceSettings.GoogleDeveloperKey
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.SegmentDeveloperKey = this.state.segmentDeveloperKey;
+ config.ServiceSettings.GoogleDeveloperKey = this.state.googleDeveloperKey;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.integration.title'
+ defaultMessage='Integration Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.integrations.external'
+ defaultMessage='External Services'
+ />
+ }
+ >
+ <TextSetting
+ id='segmentDeveloperKey'
+ label={
+ <FormattedMessage
+ id='admin.service.segmentTitle'
+ defaultMessage='Segment Developer Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.segmentExample', 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.segmentDescription'
+ defaultMessage='For users running a SaaS services, sign up for a key at Segment.com to track metrics.'
+ />
+ }
+ value={this.state.segmentDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='googleDeveloperKey'
+ label={
+ <FormattedMessage
+ id='admin.service.googleTitle'
+ defaultMessage='Google Developer Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.googleExample', 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"')}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.service.googleDescription'
+ defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
+ />
+ }
+ value={this.state.googleDeveloperKey}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/generated_setting.jsx b/webapp/components/admin_console/generated_setting.jsx
new file mode 100644
index 000000000..29bb96985
--- /dev/null
+++ b/webapp/components/admin_console/generated_setting.jsx
@@ -0,0 +1,97 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import crypto from 'crypto';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class GeneratedSetting extends React.Component {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ placeholder: React.PropTypes.string,
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool.isRequired,
+ disabledText: React.PropTypes.node,
+ helpText: React.PropTypes.node.isRequired,
+ regenerateText: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ disabled: false,
+ regenerateText: (
+ <FormattedMessage
+ id='admin.regenerate'
+ defaultMessage='Re-Generate'
+ />
+ )
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.regenerate = this.regenerate.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value === 'true');
+ }
+
+ regenerate(e) {
+ e.preventDefault();
+
+ this.props.onChange(this.props.id, crypto.randomBytes(256).toString('base64').substring(0, 32));
+ }
+
+ render() {
+ let disabledText = null;
+ if (this.props.disabled && this.props.disabledText) {
+ disabledText = (
+ <div className='admin-console__disabled-text'>
+ {this.props.disabledText}
+ </div>
+ );
+ }
+
+ return (
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor={this.props.id}
+ >
+ {this.props.label}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id={this.props.id}
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {disabledText}
+ <div className='help-text'>
+ {this.props.helpText}
+ </div>
+ <button
+ className='btn btn-default'
+ onClick={this.regenerate}
+ disabled={this.props.disabled}
+ >
+ {this.props.regenerateText}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx
index 747905ac6..bd3cd8dec 100644
--- a/webapp/components/admin_console/gitlab_settings.jsx
+++ b/webapp/components/admin_console/gitlab_settings.jsx
@@ -1,383 +1,186 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- clientIdExample: {
- id: 'admin.gitlab.clientIdExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- clientSecretExample: {
- id: 'admin.gitlab.clientSecretExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- authExample: {
- id: 'admin.gitlab.authExample',
- defaultMessage: 'Ex ""'
- },
- tokenExample: {
- id: 'admin.gitlab.tokenExample',
- defaultMessage: 'Ex ""'
- },
- userExample: {
- id: 'admin.gitlab.userExample',
- defaultMessage: 'Ex ""'
- },
- saving: {
- id: 'admin.gitlab.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class GitLabSettings extends React.Component {
+export default class GitLabSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- Enable: this.props.config.GitLabSettings.Enable,
- saveNeeded: false,
- serverError: null
- };
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'EnableTrue') {
- s.Enable = true;
- }
-
- if (action === 'EnableFalse') {
- s.Enable = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enable: props.config.GitLabSettings.Enable,
+ id: props.config.GitLabSettings.Id,
+ secret: props.config.GitLabSettings.Secret,
+ userApiEndpoint: props.config.GitLabSettings.UserApiEndpoint,
+ authEndpoint: props.config.GitLabSettings.AuthEndpoint,
+ tokenEndpoint: props.config.GitLabSettings.TokenEndpoint
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.GitLabSettings.Enable = this.state.enable;
+ config.GitLabSettings.Id = this.state.id;
+ config.GitLabSettings.Secret = this.state.secret;
+ config.GitLabSettings.UserApiEndpoint = this.state.userApiEndpoint;
+ config.GitLabSettings.AuthEndpoint = this.state.authEndpoint;
+ config.GitLabSettings.TokenEndpoint = this.state.tokenEndpoint;
- var config = this.props.config;
- config.GitLabSettings.Enable = ReactDOM.findDOMNode(this.refs.Enable).checked;
- config.GitLabSettings.Secret = ReactDOM.findDOMNode(this.refs.Secret).value.trim();
- config.GitLabSettings.Id = ReactDOM.findDOMNode(this.refs.Id).value.trim();
- config.GitLabSettings.AuthEndpoint = ReactDOM.findDOMNode(this.refs.AuthEndpoint).value.trim();
- config.GitLabSettings.TokenEndpoint = ReactDOM.findDOMNode(this.refs.TokenEndpoint).value.trim();
- config.GitLabSettings.UserApiEndpoint = ReactDOM.findDOMNode(this.refs.UserApiEndpoint).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
-
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.gitlab.settingsTitle'
- defaultMessage='GitLab Settings'
+ id='admin.authentication.gitlab'
+ defaultMessage='GitLab'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.gitlab.enableTitle'
- defaultMessage='Enable Sign Up With GitLab: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.GitLabSettings.Enable}
- onChange={this.handleChange.bind(this, 'EnableTrue')}
- />
- <FormattedMessage
- id='admin.gitlab.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.GitLabSettings.Enable}
- onChange={this.handleChange.bind(this, 'EnableFalse')}
- />
- <FormattedMessage
- id='admin.gitlab.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.enableDescription'
- defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
- />
- <br/>
- </p>
- <div className='help-text'>
- <FormattedHTMLMessage
- id='admin.gitlab.EnableHtmlDesc'
- defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
- />
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Id'
- >
- <FormattedMessage
- id='admin.gitlab.clientIdTitle'
- defaultMessage='Id:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Id'
- ref='Id'
- placeholder={formatMessage(holders.clientIdExample)}
- defaultValue={this.props.config.GitLabSettings.Id}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.clientIdDescription'
- defaultMessage='Obtain this value via the instructions above for logging into GitLab'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Secret'
- >
- <FormattedMessage
- id='admin.gitlab.clientSecretTitle'
- defaultMessage='Secret:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Secret'
- ref='Secret'
- placeholder={formatMessage(holders.clientSecretExample)}
- defaultValue={this.props.config.GitLabSettings.Secret}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitab.clientSecretDescription'
- defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AuthEndpoint'
- >
- <FormattedMessage
- id='admin.gitlab.authTitle'
- defaultMessage='Auth Endpoint:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AuthEndpoint'
- ref='AuthEndpoint'
- placeholder={formatMessage(holders.authExample)}
- defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.authDescription'
- defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='TokenEndpoint'
- >
+ }
+ >
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.enableTitle'
+ defaultMessage='Enable Sign Up With GitLab: '
+ />
+ }
+ helpText={
+ <div>
<FormattedMessage
- id='admin.gitlab.tokenTitle'
- defaultMessage='Token Endpoint:'
+ id='admin.gitlab.enableDescription'
+ defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
/>
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='TokenEndpoint'
- ref='TokenEndpoint'
- placeholder={formatMessage(holders.tokenExample)}
- defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
+ <br/>
+ <FormattedHTMLMessage
+ id='admin.gitlab.EnableHtmlDesc'
+ defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
/>
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.tokenDescription'
- defaultMessage='Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
</div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UserApiEndpoint'
- >
- <FormattedMessage
- id='admin.gitlab.userTitle'
- defaultMessage='User API Endpoint:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UserApiEndpoint'
- ref='UserApiEndpoint'
- placeholder={formatMessage(holders.userExample)}
- defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
- onChange={this.handleChange}
- disabled={!this.state.Enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.gitlab.userDescription'
- defaultMessage='Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.gitlab.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='id'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.clientIdTitle'
+ defaultMessage='Id:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.clientIdExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.clientIdDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab'
+ />
+ }
+ value={this.state.id}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='secret'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.clientSecretTitle'
+ defaultMessage='Secret:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.clientSecretExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitab.clientSecretDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
+ />
+ }
+ value={this.state.secret}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='userApiEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.userTitle'
+ defaultMessage='User API Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.userExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.userDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.userApiEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='authEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.authTitle'
+ defaultMessage='Auth Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.authExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.authDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.authEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='tokenEndpoint'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.tokenTitle'
+ defaultMessage='Token Endpoint:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.gitlab.tokenExample', 'Ex ""')}
+ helpText={
+ <FormattedMessage
+ id='admin.gitlab.tokenDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ }
+ value={this.state.tokenEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-
-//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
-// <div className='form-group'>
-// <label
-// className='control-label col-sm-4'
-// htmlFor='Scope'
-// >
-// {'Scope:'}
-// </label>
-// <div className='col-sm-8'>
-// <input
-// type='text'
-// className='form-control'
-// id='Scope'
-// ref='Scope'
-// placeholder='Not currently used by GitLab. Please leave blank'
-// defaultValue={this.props.config.GitLabSettings.Scope}
-// onChange={this.handleChange}
-// disabled={!this.state.Allow}
-// />
-// <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
-// </div>
-// </div>
-
-GitLabSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(GitLabSettings);
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx
index 023e9af3b..86d8795cc 100644
--- a/webapp/components/admin_console/image_settings.jsx
+++ b/webapp/components/admin_console/image_settings.jsx
@@ -1,692 +1,174 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- storeLocal: {
- id: 'admin.image.storeLocal',
- defaultMessage: 'Local File System'
- },
- storeAmazonS3: {
- id: 'admin.image.storeAmazonS3',
- defaultMessage: 'Amazon S3'
- },
- localExample: {
- id: 'admin.image.localExample',
- defaultMessage: 'Ex "./data/"'
- },
- amazonS3IdExample: {
- id: 'admin.image.amazonS3IdExample',
- defaultMessage: 'Ex "AKIADTOVBGERKLCBV"'
- },
- amazonS3SecretExample: {
- id: 'admin.image.amazonS3SecretExample',
- defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
- },
- amazonS3BucketExample: {
- id: 'admin.image.amazonS3BucketExample',
- defaultMessage: 'Ex "mattermost-media"'
- },
- amazonS3RegionExample: {
- id: 'admin.image.amazonS3RegionExample',
- defaultMessage: 'Ex "us-east-1"'
- },
- thumbWidthExample: {
- id: 'admin.image.thumbWidthExample',
- defaultMessage: 'Ex "120"'
- },
- thumbHeightExample: {
- id: 'admin.image.thumbHeightExample',
- defaultMessage: 'Ex "100"'
- },
- previewWidthExample: {
- id: 'admin.image.previewWidthExample',
- defaultMessage: 'Ex "1024"'
- },
- previewHeightExample: {
- id: 'admin.image.previewHeightExample',
- defaultMessage: 'Ex "0"'
- },
- profileWidthExample: {
- id: 'admin.image.profileWidthExample',
- defaultMessage: 'Ex "1024"'
- },
- profileHeightExample: {
- id: 'admin.image.profileHeightExample',
- defaultMessage: 'Ex "0"'
- },
- publicLinkExample: {
- id: 'admin.image.publicLinkExample',
- defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
- },
- saving: {
- id: 'admin.image.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class FileSettings extends React.Component {
+export default class ImageSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleGenerate = this.handleGenerate.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null,
- DriverName: this.props.config.FileSettings.DriverName
- };
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'DriverName') {
- s.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
- }
-
- this.setState(s);
- }
-
- handleGenerate(e) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ thumbnailWidth: props.config.FileSettings.ThumbnailWidth,
+ thumbnailHeight: props.config.FileSettings.ThumbnailHeight,
+ profileWidth: props.config.FileSettings.ProfileWidth,
+ profileHeight: props.config.FileSettings.ProfileHeight,
+ previewWidth: props.config.FileSettings.PreviewWidth,
+ previewHeight: props.config.FileSettings.PreviewHeight
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.FileSettings.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
- config.FileSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
- config.FileSettings.AmazonS3AccessKeyId = ReactDOM.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
- config.FileSettings.AmazonS3SecretAccessKey = ReactDOM.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
- config.FileSettings.AmazonS3Bucket = ReactDOM.findDOMNode(this.refs.AmazonS3Bucket).value;
- config.FileSettings.AmazonS3Region = ReactDOM.findDOMNode(this.refs.AmazonS3Region).value;
- config.FileSettings.EnablePublicLink = ReactDOM.findDOMNode(this.refs.EnablePublicLink).checked;
-
- config.FileSettings.PublicLinkSalt = ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+ getConfigFromState(config) {
+ config.FileSettings.ThumbnailWidth = this.parseInt(this.state.thumbnailWidth);
+ config.FileSettings.ThumbnailHeight = this.parseInt(this.state.thumbnailHeight);
+ config.FileSettings.ProfileWidth = this.parseInt(this.state.profileWidth);
+ config.FileSettings.ProfileHeight = this.parseInt(this.state.profileHeight);
+ config.FileSettings.PreviewWidth = this.parseInt(this.state.previewWidth);
+ config.FileSettings.PreviewHeight = this.parseInt(this.state.previewHeight);
- if (config.FileSettings.PublicLinkSalt === '') {
- config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
- }
-
- var thumbnailWidth = 120;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
- thumbnailWidth = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10);
- }
- config.FileSettings.ThumbnailWidth = thumbnailWidth;
- ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
-
- var thumbnailHeight = 100;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
- thumbnailHeight = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10);
- }
- config.FileSettings.ThumbnailHeight = thumbnailHeight;
- ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
-
- var previewWidth = 1024;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10))) {
- previewWidth = parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10);
- }
- config.FileSettings.PreviewWidth = previewWidth;
- ReactDOM.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
-
- var previewHeight = 0;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10))) {
- previewHeight = parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10);
- }
- config.FileSettings.PreviewHeight = previewHeight;
- ReactDOM.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
-
- var profileWidth = 128;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10))) {
- profileWidth = parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10);
- }
- config.FileSettings.ProfileWidth = profileWidth;
- ReactDOM.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
-
- var profileHeight = 128;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10))) {
- profileHeight = parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10);
- }
- config.FileSettings.ProfileHeight = profileHeight;
- ReactDOM.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.files.title'
+ defaultMessage='File Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var enableFile = false;
- var enableS3 = false;
-
- if (this.state.DriverName === 'local') {
- enableFile = true;
- }
-
- if (this.state.DriverName === 'amazons3') {
- enableS3 = true;
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.image.fileSettings'
- defaultMessage='File Settings'
+ id='admin.files.images'
+ defaultMessage='Images'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DriverName'
- >
- <FormattedMessage
- id='admin.image.storeTitle'
- defaultMessage='Store Files In:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='DriverName'
- ref='DriverName'
- defaultValue={this.props.config.FileSettings.DriverName}
- onChange={this.handleChange.bind(this, 'DriverName')}
- >
- <option value='local'>{formatMessage(holders.storeLocal)}</option>
- <option value='amazons3'>{formatMessage(holders.storeAmazonS3)}</option>
- </select>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Directory'
- >
- <FormattedMessage
- id='admin.image.localTitle'
- defaultMessage='Local Directory Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='Directory'
- ref='Directory'
- placeholder={formatMessage(holders.localExample)}
- defaultValue={this.props.config.FileSettings.Directory}
- onChange={this.handleChange}
- disabled={!enableFile}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.localDescription'
- defaultMessage='Directory to which image files are written. If blank, will be set to ./data/.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3AccessKeyId'
- >
- <FormattedMessage
- id='admin.image.amazonS3IdTitle'
- defaultMessage='Amazon S3 Access Key Id:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3AccessKeyId'
- ref='AmazonS3AccessKeyId'
- placeholder={formatMessage(holders.amazonS3IdExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3IdDescription'
- defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3SecretAccessKey'
- >
- <FormattedMessage
- id='admin.image.amazonS3SecretTitle'
- defaultMessage='Amazon S3 Secret Access Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3SecretAccessKey'
- ref='AmazonS3SecretAccessKey'
- placeholder={formatMessage(holders.amazonS3SecretExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3SecretDescription'
- defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3Bucket'
- >
- <FormattedMessage
- id='admin.image.amazonS3BucketTitle'
- defaultMessage='Amazon S3 Bucket:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3Bucket'
- ref='AmazonS3Bucket'
- placeholder={formatMessage(holders.amazonS3BucketExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3BucketDescription'
- defaultMessage='Name you selected for your S3 bucket in AWS.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AmazonS3Region'
- >
- <FormattedMessage
- id='admin.image.amazonS3RegionTitle'
- defaultMessage='Amazon S3 Region:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AmazonS3Region'
- ref='AmazonS3Region'
- placeholder={formatMessage(holders.amazonS3RegionExample)}
- defaultValue={this.props.config.FileSettings.AmazonS3Region}
- onChange={this.handleChange}
- disabled={!enableS3}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.amazonS3RegionDescription'
- defaultMessage='AWS region you selected for creating your S3 bucket.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ThumbnailWidth'
- >
- <FormattedMessage
- id='admin.image.thumbWidthTitle'
- defaultMessage='Thumbnail Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ThumbnailWidth'
- ref='ThumbnailWidth'
- placeholder={formatMessage(holders.thumbWidthExample)}
- defaultValue={this.props.config.FileSettings.ThumbnailWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.thumbWidthDescription'
- defaultMessage='Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ThumbnailHeight'
- >
- <FormattedMessage
- id='admin.image.thumbHeightTitle'
- defaultMessage='Thumbnail Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ThumbnailHeight'
- ref='ThumbnailHeight'
- placeholder={formatMessage(holders.thumbHeightExample)}
- defaultValue={this.props.config.FileSettings.ThumbnailHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.thumbHeightDescription'
- defaultMessage='Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PreviewWidth'
- >
- <FormattedMessage
- id='admin.image.previewWidthTitle'
- defaultMessage='Preview Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PreviewWidth'
- ref='PreviewWidth'
- placeholder={formatMessage(holders.previewWidthExample)}
- defaultValue={this.props.config.FileSettings.PreviewWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.previewWidthDescription'
- defaultMessage='Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PreviewHeight'
- >
- <FormattedMessage
- id='admin.image.previewHeightTitle'
- defaultMessage='Preview Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PreviewHeight'
- ref='PreviewHeight'
- placeholder={formatMessage(holders.previewHeightExample)}
- defaultValue={this.props.config.FileSettings.PreviewHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.previewHeightDescription'
- defaultMessage='Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ProfileWidth'
- >
- <FormattedMessage
- id='admin.image.profileWidthTitle'
- defaultMessage='Profile Width:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ProfileWidth'
- ref='ProfileWidth'
- placeholder={formatMessage(holders.profileWidthExample)}
- defaultValue={this.props.config.FileSettings.ProfileWidth}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.profileWidthDescription'
- defaultMessage='Width of profile picture.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ProfileHeight'
- >
- <FormattedMessage
- id='admin.image.profileHeightTitle'
- defaultMessage='Profile Height:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ProfileHeight'
- ref='ProfileHeight'
- placeholder={formatMessage(holders.profileHeightExample)}
- defaultValue={this.props.config.FileSettings.ProfileHeight}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.profileHeightDescription'
- defaultMessage='Height of profile picture.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePublicLink'
- >
- <FormattedMessage
- id='admin.image.shareTitle'
- defaultMessage='Share Public File Link: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePublicLink'
- value='true'
- ref='EnablePublicLink'
- defaultChecked={this.props.config.FileSettings.EnablePublicLink}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.image.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePublicLink'
- value='false'
- defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.image.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.shareDescription'
- defaultMessage='Allow users to share public links to files and images.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PublicLinkSalt'
- >
- <FormattedMessage
- id='admin.image.publicLinkTitle'
- defaultMessage='Public Link Salt:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PublicLinkSalt'
- ref='PublicLinkSalt'
- placeholder={formatMessage(holders.publicLinkExample)}
- defaultValue={this.props.config.FileSettings.PublicLinkSalt}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.image.publicLinkDescription'
- defaultMessage='32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerate}
- >
- <FormattedMessage
- id='admin.image.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.image.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <TextSetting
+ id='thumbnailWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.thumbWidthTitle'
+ defaultMessage='Thumbnail Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.thumbWidthExample', 'Ex "120"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.thumbWidthDescription'
+ defaultMessage='Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.thumbnailWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='thumbnailHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.thumbHeightTitle'
+ defaultMessage='Thumbnail Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.thumbHeightExample', 'Ex "100"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.thumbHeightDescription'
+ defaultMessage='Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.thumbnailHeight}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='profileWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.profileWidthTitle'
+ defaultMessage='Profile Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.profileWidthExample', 'Ex "1024"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.profileWidthDescription'
+ defaultMessage='Width of profile picture.'
+ />
+ }
+ value={this.state.profileWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='profileHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.profileHeightTitle'
+ defaultMessage='Profile Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.profileHeightExample', 'Ex "0"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.profileHeightDescription'
+ defaultMessage='Height of profile picture.'
+ />
+ }
+ value={this.state.profileHeight}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='previewWidth'
+ label={
+ <FormattedMessage
+ id='admin.image.previewWidthTitle'
+ defaultMessage='Preview Width:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.previewWidthExample', 'Ex "1024"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.previewWidthDescription'
+ defaultMessage='Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.previewWidth}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='previewHeight'
+ label={
+ <FormattedMessage
+ id='admin.image.previewHeightTitle'
+ defaultMessage='Preview Height:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.previewHeightExample', 'Ex "0"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.previewHeightDescription'
+ defaultMessage='Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'
+ />
+ }
+ value={this.state.previewHeight}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-FileSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(FileSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx
index 3ced65e50..d47a1f8c2 100644
--- a/webapp/components/admin_console/ldap_settings.jsx
+++ b/webapp/components/admin_console/ldap_settings.jsx
@@ -1,116 +1,95 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
+import React from 'react';
+
import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import AdminSettings from './admin_settings.jsx';
import BooleanSetting from './boolean_setting.jsx';
+import ConnectionSecurityDropdownSetting from './connection_security_dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-const DEFAULT_LDAP_PORT = 389;
-const DEFAULT_QUERY_TIMEOUT = 60;
-
-import React from 'react';
-
-class LdapSettings extends React.Component {
+export default class LdapSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.handleEnable = this.handleEnable.bind(this);
- this.handleDisable = this.handleDisable.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null,
- enable: this.props.config.LdapSettings.Enable,
- connectionSecurity: this.props.config.LdapSettings.ConnectionSecurity,
- skipCertificateVerification: this.props.config.LdapSettings.SkipCertificateVerification
- };
- }
- handleChange() {
- this.setState({saveNeeded: true});
- }
- handleEnable() {
- this.setState({saveNeeded: true, enable: true});
- }
- handleDisable() {
- this.setState({saveNeeded: true, enable: false});
- }
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ this.renderSettings = this.renderSettings.bind(this);
- const config = this.props.config;
- config.LdapSettings.Enable = this.refs.Enable.checked;
- config.LdapSettings.LdapServer = this.refs.LdapServer.value.trim();
-
- let LdapPort = DEFAULT_LDAP_PORT;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10))) {
- LdapPort = parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10);
- }
- config.LdapSettings.LdapPort = LdapPort;
+ this.state = Object.assign(this.state, {
+ enable: props.config.LdapSettings.Enable,
+ ldapServer: props.config.LdapSettings.LdapServer,
+ ldapPort: props.config.LdapSettings.LdapPort,
+ connectionSecurity: props.config.LdapSettings.ConnectionSecurity,
+ baseDN: props.config.LdapSettings.BaseDN,
+ bindUsername: props.config.LdapSettings.BindUsername,
+ bindPassword: props.config.LdapSettings.BindPassword,
+ userFilter: props.config.LdapSettings.UserFilter,
+ firstNameAttribute: props.config.LdapSettings.FirstNameAttribute,
+ lastNameAttribute: props.config.LdapSettings.LastNameAttribute,
+ nicknameAttribute: props.config.LdapSettings.NicknameAttribute,
+ emailAttribute: props.config.LdapSettings.EmailAttribute,
+ usernameAttribute: props.config.LdapSettings.UsernameAttribute,
+ idAttribute: props.config.LdapSettings.IdAttribute,
+ skipCertificateVerification: props.config.LdapSettings.SkipCertificateVerification,
+ queryTimeout: props.config.LdapSettings.QueryTimeout,
+ loginFieldName: props.config.LdapSettings.LoginFieldName
+ });
+ }
- config.LdapSettings.BaseDN = this.refs.BaseDN.value.trim();
- config.LdapSettings.BindUsername = this.refs.BindUsername.value.trim();
- config.LdapSettings.BindPassword = this.refs.BindPassword.value.trim();
- config.LdapSettings.FirstNameAttribute = this.refs.FirstNameAttribute.value.trim();
- config.LdapSettings.LastNameAttribute = this.refs.LastNameAttribute.value.trim();
- config.LdapSettings.NicknameAttribute = this.refs.NicknameAttribute.value.trim();
- config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim();
- config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim();
- config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim();
- config.LdapSettings.UserFilter = this.refs.UserFilter.value.trim();
- config.LdapSettings.ConnectionSecurity = this.state.connectionSecurity.trim();
+ getConfigFromState(config) {
+ config.LdapSettings.Enable = this.state.enable;
+ config.LdapSettings.LdapServer = this.state.ldapServer;
+ config.LdapSettings.LdapPort = this.parseIntNonZero(this.state.ldapPort);
+ config.LdapSettings.ConnectionSecurity = this.state.connectionSecurity;
+ config.LdapSettings.BaseDN = this.state.baseDN;
+ config.LdapSettings.BindUsername = this.state.bindUsername;
+ config.LdapSettings.BindPassword = this.state.bindPassword;
+ config.LdapSettings.UserFilter = this.state.userFilter;
+ config.LdapSettings.FirstNameAttribute = this.state.firstNameAttribute;
+ config.LdapSettings.LastNameAttribute = this.state.lastNameAttribute;
+ config.LdapSettings.NicknameAttribute = this.state.nicknameAttribute;
+ config.LdapSettings.EmailAttribute = this.state.emailAttribute;
+ config.LdapSettings.UsernameAttribute = this.state.usernameAttribute;
+ config.LdapSettings.IdAttribute = this.state.idAttribute;
config.LdapSettings.SkipCertificateVerification = this.state.skipCertificateVerification;
- config.LdapSettings.LoginFieldName = this.refs.LoginFieldName.value.trim();
+ config.LdapSettings.QueryTimeout = this.parseIntNonZero(this.state.queryTimeout);
+ config.LdapSettings.LoginFieldName = this.state.loginFieldName;
- let QueryTimeout = DEFAULT_QUERY_TIMEOUT;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) {
- QueryTimeout = parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10);
- }
- config.LdapSettings.QueryTimeout = QueryTimeout;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.authentication.title'
+ defaultMessage='Authentication Settings'
+ />
+ </h3>
);
}
- render() {
- let serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
- let saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
+ renderSettings() {
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+ if (!licenseEnabled) {
+ return null;
}
- const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.authentication.ldap'
+ defaultMessage='LDAP'
+ />
- let bannerContent;
- if (licenseEnabled) {
- bannerContent = (
+ }
+ >
<div className='banner'>
<div className='banner__content'>
<h4 className='banner__heading'>
@@ -127,540 +106,310 @@ class LdapSettings extends React.Component {
</p>
</div>
</div>
- );
- } else {
- bannerContent = (
- <div className='banner warning'>
- <div className='banner__content'>
- <FormattedHTMLMessage
- id='admin.ldap.noLicense'
- defaultMessage='<h4 class="banner__heading">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href="http://mattermost.com"target="_blank">here</a> for information and pricing on enterprise licenses.</p>'
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.ldap.enableTitle'
+ defaultMessage='Enable Login With LDAP:'
/>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
- {bannerContent}
- <h3>
- <FormattedMessage
- id='admin.ldap.title'
- defaultMessage='LDAP Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Enable'
- >
- <FormattedMessage
- id='admin.ldap.enableTitle'
- defaultMessage='Enable Login With LDAP:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='true'
- ref='Enable'
- defaultChecked={this.props.config.LdapSettings.Enable}
- onChange={this.handleEnable}
- disabled={!licenseEnabled}
- />
- <FormattedMessage
- id='admin.ldap.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Enable'
- value='false'
- defaultChecked={!this.props.config.LdapSettings.Enable}
- onChange={this.handleDisable}
- />
- <FormattedMessage
- id='admin.ldap.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.enableDesc'
- defaultMessage='When true, Mattermost allows login using LDAP'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LdapServer'
- >
- <FormattedMessage
- id='admin.ldap.serverTitle'
- defaultMessage='LDAP Server:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LdapServer'
- ref='LdapServer'
- placeholder={Utils.localizeMessage('admin.ldap.serverEx', 'Ex "10.0.0.23"')}
- defaultValue={this.props.config.LdapSettings.LdapServer}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.serverDesc'
- defaultMessage='The domain or IP address of LDAP server.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LdapPort'
- >
- <FormattedMessage
- id='admin.ldap.portTitle'
- defaultMessage='LDAP Port:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='number'
- className='form-control'
- id='LdapPort'
- ref='LdapPort'
- placeholder={Utils.localizeMessage('admin.ldap.portEx', 'Ex "389"')}
- defaultValue={this.props.config.LdapSettings.LdapPort}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.portDesc'
- defaultMessage='The port Mattermost will use to connect to the LDAP server. Default is 389.'
- />
- </p>
- </div>
- </div>
- <ConnectionSecurityDropdownSetting
- currentValue={this.state.connectionSecurity}
- handleChange={(e) => this.setState({connectionSecurity: e.target.value, saveNeeded: true})}
- isDisabled={!this.state.enable}
- />
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BaseDN'
- >
- <FormattedMessage
- id='admin.ldap.baseTitle'
- defaultMessage='BaseDN:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='BaseDN'
- ref='BaseDN'
- placeholder={Utils.localizeMessage('admin.ldap.baseEx', 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"')}
- defaultValue={this.props.config.LdapSettings.BaseDN}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.baseDesc'
- defaultMessage='The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BindUsername'
- >
- <FormattedMessage
- id='admin.ldap.bindUserTitle'
- defaultMessage='Bind Username:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='BindUsername'
- ref='BindUsername'
- placeholder=''
- defaultValue={this.props.config.LdapSettings.BindUsername}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.bindUserDesc'
- defaultMessage='The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='BindPassword'
- >
- <FormattedMessage
- id='admin.ldap.bindPwdTitle'
- defaultMessage='Bind Password:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='password'
- className='form-control'
- id='BindPassword'
- ref='BindPassword'
- placeholder=''
- defaultValue={this.props.config.LdapSettings.BindPassword}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.bindPwdDesc'
- defaultMessage='Password of the user given in "Bind Username".'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UserFilter'
- >
- <FormattedMessage
- id='admin.ldap.userFilterTitle'
- defaultMessage='User Filter:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UserFilter'
- ref='UserFilter'
- placeholder={Utils.localizeMessage('admin.ldap.userFilterEx', 'Ex. "(objectClass=user)"')}
- defaultValue={this.props.config.LdapSettings.UserFilter}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.userFilterDisc'
- defaultMessage='Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='FirstNameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.firstnameAttrTitle'
- defaultMessage='First Name Attrubute'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='FirstNameAttribute'
- ref='FirstNameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.firstnameAttrEx', 'Ex "givenName"')}
- defaultValue={this.props.config.LdapSettings.FirstNameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.firstnameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LastNameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.lastnameAttrTitle'
- defaultMessage='Last Name Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LastNameAttribute'
- ref='LastNameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.lastnameAttrEx', 'Ex "sn"')}
- defaultValue={this.props.config.LdapSettings.LastNameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.lastnameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='NicknameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.nicknameAttrTitle'
- defaultMessage='Nickname Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='NicknameAttribute'
- ref='NicknameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.nicknameAttrEx', 'Ex "nickname"')}
- defaultValue={this.props.config.LdapSettings.NicknameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.nicknameAttrDesc'
- defaultMessage='(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EmailAttribute'
- >
- <FormattedMessage
- id='admin.ldap.emailAttrTitle'
- defaultMessage='Email Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='EmailAttribute'
- ref='EmailAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.emailAttrEx', 'Ex "mail" or "userPrincipalName"')}
- defaultValue={this.props.config.LdapSettings.EmailAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.emailAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='UsernameAttribute'
- >
- <FormattedMessage
- id='admin.ldap.usernameAttrTitle'
- defaultMessage='Username Attribute:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='UsernameAttribute'
- ref='UsernameAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.usernameAttrEx', 'Ex "sAMAccountName"')}
- defaultValue={this.props.config.LdapSettings.UsernameAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.uernameAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'
- />
- </p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='IdAttribute'
- >
- <FormattedMessage
- id='admin.ldap.idAttrTitle'
- defaultMessage='Id Attribute: '
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='IdAttribute'
- ref='IdAttribute'
- placeholder={Utils.localizeMessage('admin.ldap.idAttrEx', 'Ex "sAMAccountName"')}
- defaultValue={this.props.config.LdapSettings.IdAttribute}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.idAttrDesc'
- defaultMessage='The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'
- />
- </p>
- </div>
- </div>
- <BooleanSetting
- label={
- <FormattedMessage
- id='admin.ldap.skipCertificateVerification'
- defaultMessage='Skip Certificate Verification'
- />
- }
- currentValue={this.state.skipCertificateVerification}
- isDisabled={!this.state.enable}
- handleChange={(e) => this.setState({skipCertificateVerification: e.target.value.trim() === 'true', saveNeeded: true})}
- helpText={
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.skipCertificateVerificationDesc'
- defaultMessage='Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.'
- />
- </p>
- }
- />
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='QueryTimeout'
- >
- <FormattedMessage
- id='admin.ldap.queryTitle'
- defaultMessage='Query Timeout (seconds):'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='number'
- className='form-control'
- id='QueryTimeout'
- ref='QueryTimeout'
- placeholder={Utils.localizeMessage('admin.ldap.queryEx', 'Ex "60"')}
- defaultValue={this.props.config.LdapSettings.QueryTimeout}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.queryDesc'
- defaultMessage='The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='LoginFieldName'
- >
- <FormattedMessage
- id='admin.ldap.loginNameTitle'
- defaultMessage='Login Field Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='LoginFieldName'
- ref='LoginFieldName'
- placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')}
- defaultValue={this.props.config.LdapSettings.LoginFieldName}
- onChange={this.handleChange}
- disabled={!this.state.enable}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.ldap.loginNameDesc'
- defaultMessage='The placeholder text that appears in the login field on the login page. Defaults to "LDAP Username".'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.ldap.saving', 'Saving Config...')}
- >
- <FormattedMessage
- id='admin.ldap.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
- </form>
- </div>
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.enableDesc'
+ defaultMessage='When true, Mattermost allows login using LDAP'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='ldapServer'
+ label={
+ <FormattedMessage
+ id='admin.ldap.serverTitle'
+ defaultMessage='LDAP Server:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.serverEx', 'Ex "10.0.0.23"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.serverDesc'
+ defaultMessage='The domain or IP address of LDAP server.'
+ />
+ }
+ value={this.state.ldapServer}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='ldapPort'
+ label={
+ <FormattedMessage
+ id='admin.ldap.portTitle'
+ defaultMessage='LDAP Port:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.portEx', 'Ex "389"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.portDesc'
+ defaultMessage='The port Mattermost will use to connect to the LDAP server. Default is 389.'
+ />
+ }
+ value={this.state.ldapPort}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <ConnectionSecurityDropdownSetting
+ value={this.state.ldapConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='baseDN'
+ label={
+ <FormattedMessage
+ id='admin.ldap.baseTitle'
+ defaultMessage='BaseDN:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.baseEx', 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.baseDesc'
+ defaultMessage='The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'
+ />
+ }
+ value={this.state.baseDN}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='bindUsername'
+ label={
+ <FormattedMessage
+ id='admin.ldap.bindUserTitle'
+ defaultMessage='Bind Username:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.bindUserDesc'
+ defaultMessage='The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'
+ />
+ }
+ value={this.state.bindUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='bindPassword'
+ label={
+ <FormattedMessage
+ id='admin.ldap.bindPwdTitle'
+ defaultMessage='Bind Password:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.bindPwdDesc'
+ defaultMessage='Password of the user given in "Bind Username".'
+ />
+ }
+ value={this.state.bindPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='userFilter'
+ label={
+ <FormattedMessage
+ id='admin.ldap.userFilterTitle'
+ defaultMessage='User Filter:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.userFilterEx', 'Ex. "(objectClass=user)"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.userFilterDisc'
+ defaultMessage='Optionally enter an LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).'
+ />
+ }
+ value={this.state.userFilter}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='firstNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.firstnameAttrTitle'
+ defaultMessage='First Name Attrubute'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.firstnameAttrEx', 'Ex "givenName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.firstnameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'
+ />
+ }
+ value={this.state.firstNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='lastNameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.lastnameAttrTitle'
+ defaultMessage='Last Name Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.lastnameAttrEx', 'Ex "sn"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.lastnameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'
+ />
+ }
+ value={this.state.lastNameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='nicknameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.nicknameAttrTitle'
+ defaultMessage='Nickname Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.nicknameAttrEx', 'Ex "nickname"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.nicknameAttrDesc'
+ defaultMessage='(Optional) The attribute in the LDAP server that will be used to populate the nickname of users in Mattermost.'
+ />
+ }
+ value={this.state.nicknameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='emailAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.emailAttrTitle'
+ defaultMessage='Email Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.emailAttrEx', 'Ex "mail" or "userPrincipalName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.emailAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'
+ />
+ }
+ value={this.state.emailAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='usernameAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.usernameAttrTitle'
+ defaultMessage='Username Attribute:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.usernameAttrEx', 'Ex "sAMAccountName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.uernameAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'
+ />
+ }
+ value={this.state.usernameAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='idAttribute'
+ label={
+ <FormattedMessage
+ id='admin.ldap.idAttrTitle'
+ defaultMessage='Id Attribute: '
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.idAttrEx', 'Ex "sAMAccountName"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.idAttrDesc'
+ defaultMessage='The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'
+ />
+ }
+ value={this.state.idAttribute}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <BooleanSetting
+ id='skipCertificateVerification'
+ label={
+ <FormattedMessage
+ id='admin.ldap.skipCertificateVerification'
+ defaultMessage='Skip Certificate Verification'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.skipCertificateVerificationDesc'
+ defaultMessage='Skips the certificate verification step for TLS or STARTTLS connections. Not recommended for production environments where TLS is required. For testing only.'
+ />
+ }
+ value={this.state.skipCertificateVerification}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='queryTimeout'
+ label={
+ <FormattedMessage
+ id='admin.ldap.queryTitle'
+ defaultMessage='Query Timeout (seconds):'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.queryEx', 'Ex "60"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.queryDesc'
+ defaultMessage='The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'
+ />
+ }
+ value={this.state.queryTimeout}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <TextSetting
+ id='loginFieldName'
+ label={
+ <FormattedMessage
+ id='admin.ldap.loginNameTitle'
+ defaultMessage='Login Field Name:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.ldap.loginNameEx', 'Ex "LDAP Username"')}
+ helpText={
+ <FormattedMessage
+ id='admin.ldap.loginNameDesc'
+ defaultMessage='The placeholder text that appears in the login field on the login page. Defaults to "LDAP Username".'
+ />
+ }
+ value={this.state.loginFieldName}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ </SettingsGroup>
);
}
-}
-LdapSettings.defaultProps = {
-};
-
-LdapSettings.propTypes = {
- config: React.PropTypes.object
-};
-
-export default LdapSettings;
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx
index 9f72f5fdf..cb152e414 100644
--- a/webapp/components/admin_console/legal_and_support_settings.jsx
+++ b/webapp/components/admin_console/legal_and_support_settings.jsx
@@ -1,309 +1,166 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-var holders = defineMessages({
- saving: {
- id: 'admin.support.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
import React from 'react';
-class LegalAndSupportSettings extends React.Component {
+import AdminSettings from './admin_settings.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class LegalAndSupportSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
+ this.renderSettings = this.renderSettings.bind(this);
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ termsOfServiceLink: props.config.SupportSettings.TermsOfServiceLink,
+ privacyPolicyLink: props.config.SupportSettings.PrivacyPolicyLink,
+ aboutLink: props.config.SupportSettings.AboutLink,
+ helpLink: props.config.SupportSettings.HelpLink,
+ reportAProblemLink: props.config.SupportSettings.ReportAProblemLink,
+ supportEmail: props.config.SupportSettings.SupportEmail
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.SupportSettings.TermsOfServiceLink = this.state.termsOfServiceLink;
+ config.SupportSettings.PrivacyPolicyLink = this.state.privacyPolicyLink;
+ config.SupportSettings.AboutLink = this.state.aboutLink;
+ config.SupportSettings.HelpLink = this.state.helpLink;
+ config.SupportSettings.ReportAProblemLink = this.state.reportAProblemLink;
+ config.SupportSettings.SupportEmail = this.state.supportEmail;
- var config = this.props.config;
-
- config.SupportSettings.TermsOfServiceLink = ReactDOM.findDOMNode(this.refs.TermsOfServiceLink).value.trim();
- config.SupportSettings.PrivacyPolicyLink = ReactDOM.findDOMNode(this.refs.PrivacyPolicyLink).value.trim();
- config.SupportSettings.AboutLink = ReactDOM.findDOMNode(this.refs.AboutLink).value.trim();
- config.SupportSettings.HelpLink = ReactDOM.findDOMNode(this.refs.HelpLink).value.trim();
- config.SupportSettings.ReportAProblemLink = ReactDOM.findDOMNode(this.refs.ReportAProblemLink).value.trim();
- config.SupportSettings.SupportEmail = ReactDOM.findDOMNode(this.refs.SupportEmail).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.customization.title'
+ defaultMessage='Customization Settings'
+ />
+ </h3>
);
}
- render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <div className='banner'>
- <div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.support.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.support.noteDescription'
- defaultMessage='If linking to an external site, URLs should begin with http:// or https://.'
- />
- </p>
- </div>
- </div>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.support.title'
- defaultMessage='Legal and Support Settings'
+ id='admin.customization.support'
+ defaultMessage='Legal and Support'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='TermsOfServiceLink'
- >
- <FormattedMessage
- id='admin.support.termsTitle'
- defaultMessage='Terms of Service link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='TermsOfServiceLink'
- ref='TermsOfServiceLink'
- defaultValue={this.props.config.SupportSettings.TermsOfServiceLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.termsDesc'
- defaultMessage='Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PrivacyPolicyLink'
- >
- <FormattedMessage
- id='admin.support.privacyTitle'
- defaultMessage='Privacy Policy link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PrivacyPolicyLink'
- ref='PrivacyPolicyLink'
- defaultValue={this.props.config.SupportSettings.PrivacyPolicyLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.privacyDesc'
- defaultMessage='Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AboutLink'
- >
- <FormattedMessage
- id='admin.support.aboutTitle'
- defaultMessage='About link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AboutLink'
- ref='AboutLink'
- defaultValue={this.props.config.SupportSettings.AboutLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.aboutDesc'
- defaultMessage='Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='HelpLink'
- >
- <FormattedMessage
- id='admin.support.helpTitle'
- defaultMessage='Help link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='HelpLink'
- ref='HelpLink'
- defaultValue={this.props.config.SupportSettings.HelpLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.helpDesc'
- defaultMessage='Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ReportAProblemLink'
- >
- <FormattedMessage
- id='admin.support.problemTitle'
- defaultMessage='Report a Problem link:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ReportAProblemLink'
- ref='ReportAProblemLink'
- defaultValue={this.props.config.SupportSettings.ReportAProblemLink}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.problemDesc'
- defaultMessage='Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SupportEmail'
- >
- <FormattedMessage
- id='admin.support.emailTitle'
- defaultMessage='Support email:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SupportEmail'
- ref='SupportEmail'
- defaultValue={this.props.config.SupportSettings.SupportEmail}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.support.emailHelp'
- defaultMessage='Email shown during tutorial for end users to ask support questions.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.support.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <TextSetting
+ id='termsOfServiceLink'
+ label={
+ <FormattedMessage
+ id='admin.support.termsTitle'
+ defaultMessage='Terms of Service link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.termsDesc'
+ defaultMessage='Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
+ />
+ }
+ value={this.state.termsOfServiceLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='privacyPolicyLink'
+ label={
+ <FormattedMessage
+ id='admin.support.privacyTitle'
+ defaultMessage='Privacy Policy link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.privacyDesc'
+ defaultMessage='Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
+ />
+ }
+ value={this.state.privacyPolicyLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='aboutLink'
+ label={
+ <FormattedMessage
+ id='admin.support.aboutTitle'
+ defaultMessage='About link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.aboutDesc'
+ defaultMessage='Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'
+ />
+ }
+ value={this.state.aboutLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='helpLink'
+ label={
+ <FormattedMessage
+ id='admin.support.helpTitle'
+ defaultMessage='Help link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.helpDesc'
+ defaultMessage='Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'
+ />
+ }
+ value={this.state.helpLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='reportAProblemLink'
+ label={
+ <FormattedMessage
+ id='admin.support.problemTitle'
+ defaultMessage='Report a Problem link:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.problemDesc'
+ defaultMessage='Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'
+ />
+ }
+ value={this.state.reportAProblemLink}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='supportEmail'
+ label={
+ <FormattedMessage
+ id='admin.support.emailTitle'
+ defaultMessage='Support email:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.support.emailHelp'
+ defaultMessage='Email shown during tutorial for end users to ask support questions.'
+ />
+ }
+ value={this.state.supportEmail}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-LegalAndSupportSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(LegalAndSupportSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index f2c511e44..14934a3e5 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -152,6 +152,7 @@ class LicenseSettings extends React.Component {
{'Mattermost Enterprise Edition. Unlock enterprise features in this software through the purchase of a subscription from '}
<a
target='_blank'
+ rel='noopener noreferrer'
href='https://mattermost.com/'
>
{'https://mattermost.com/'}
diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx
index 061c2b6e3..fa29074d8 100644
--- a/webapp/components/admin_console/log_settings.jsx
+++ b/webapp/components/admin_console/log_settings.jsx
@@ -1,418 +1,246 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import React from 'react';
-const holders = defineMessages({
- locationPlaceholder: {
- id: 'admin.log.locationPlaceholder',
- defaultMessage: 'Enter your file location'
- },
- formatPlaceholder: {
- id: 'admin.log.formatPlaceholder',
- defaultMessage: 'Enter your file format'
- },
- saving: {
- id: 'admin.log.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import * as Utils from 'utils/utils.jsx';
-import React from 'react';
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
-class LogSettings extends React.Component {
+export default class LogSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- consoleEnable: this.props.config.LogSettings.EnableConsole,
- fileEnable: this.props.config.LogSettings.EnableFile,
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
-
- if (action === 'console_true') {
- s.consoleEnable = true;
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- if (action === 'console_false') {
- s.consoleEnable = false;
- }
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'file_true') {
- s.fileEnable = true;
- }
-
- if (action === 'file_false') {
- s.fileEnable = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enableConsole: props.config.LogSettings.EnableConsole,
+ consoleLevel: props.config.LogSettings.ConsoleLevel,
+ enableFile: props.config.LogSettings.EnableFile,
+ fileLevel: props.config.LogSettings.FileLevel,
+ fileLocation: props.config.LogSettings.FileLocation,
+ fileFormat: props.config.LogSettings.FileFormat
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.LogSettings.EnableConsole = this.state.enableConsole;
+ config.LogSettings.ConsoleLevel = this.state.consoleLevel;
+ config.LogSettings.EnableFile = this.state.enableFile;
+ config.LogSettings.FileLevel = this.state.fileLevel;
+ config.LogSettings.FileLocation = this.state.fileLocation;
+ config.LogSettings.FileFormat = this.state.fileFormat;
- var config = this.props.config;
- config.LogSettings.EnableConsole = ReactDOM.findDOMNode(this.refs.consoleEnable).checked;
- config.LogSettings.ConsoleLevel = ReactDOM.findDOMNode(this.refs.consoleLevel).value;
- config.LogSettings.EnableFile = ReactDOM.findDOMNode(this.refs.fileEnable).checked;
- config.LogSettings.FileLevel = ReactDOM.findDOMNode(this.refs.fileLevel).value;
- config.LogSettings.FileLocation = ReactDOM.findDOMNode(this.refs.fileLocation).value.trim();
- config.LogSettings.FileFormat = ReactDOM.findDOMNode(this.refs.fileFormat).value.trim();
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- consoleEnable: config.LogSettings.EnableConsole,
- fileEnable: config.LogSettings.EnableFile,
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- consoleEnable: config.LogSettings.EnableConsole,
- fileEnable: config.LogSettings.EnableFile,
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
+ renderSettings() {
+ const logLevels = [
+ {value: 'DEBUG', text: 'DEBUG'},
+ {value: 'INFO', text: 'INFO'},
+ {value: 'ERROR', text: 'ERROR'}
+ ];
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.log.logSettings'
- defaultMessage='Log Settings'
+ id='admin.general.log'
+ defaultMessage='Logging'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='consoleEnable'
- >
- <FormattedMessage
- id='admin.log.consoleTitle'
- defaultMessage='Log To The Console: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='consoleEnable'
- value='true'
- ref='consoleEnable'
- defaultChecked={this.props.config.LogSettings.EnableConsole}
- onChange={this.handleChange.bind(this, 'console_true')}
- />
- <FormattedMessage
- id='admin.log.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='consoleEnable'
- value='false'
- defaultChecked={!this.props.config.LogSettings.EnableConsole}
- onChange={this.handleChange.bind(this, 'console_false')}
- />
- <FormattedMessage
- id='admin.log.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.log.consoleDescription'
- defaultMessage='Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'
- />
- </p>
- </div>
- </div>
+ }
+ >
+ <BooleanSetting
+ id='enableConsole'
+ label={
+ <FormattedMessage
+ id='admin.log.consoleTitle'
+ defaultMessage='Log To The Console: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.log.consoleDescription'
+ defaultMessage='Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'
+ />
+ }
+ value={this.state.enableConsole}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='consoleLevel'
+ values={logLevels}
+ label={
+ <FormattedMessage
+ id='admin.log.levelTitle'
+ defaultMessage='Console Log Level:'
+ />
+ }
+ value={this.state.consoleLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.enableConsole}
+ helpText={
+ <FormattedMessage
+ id='admin.log.levelDescription'
+ defaultMessage='This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ />
+ }
+ />
+ <BooleanSetting
+ id='enableFile'
+ label={
+ <FormattedMessage
+ id='admin.log.fileTitle'
+ defaultMessage='Log To File: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.log.fileDescription'
+ defaultMessage='Typically set to true in production. When true, log files are written to the log file specified in file location field below.'
+ />
+ }
+ value={this.state.enableFile}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='fileLevel'
+ values={logLevels}
+ label={
+ <FormattedMessage
+ id='admin.log.fileLevelTitle'
+ defaultMessage='File Log Level:'
+ />
+ }
+ value={this.state.fileLevel}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ helpText={
+ <FormattedMessage
+ id='admin.log.fileLevelDescription'
+ defaultMessage='This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ />
+ }
+ />
+ <TextSetting
+ id='fileLocation'
+ label={
+ <FormattedMessage
+ id='admin.log.locationTitle'
+ defaultMessage='File Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.log.locationPlaceholder', 'Enter your file location')}
+ helpText={
+ <FormattedMessage
+ id='admin.log.locationDescription'
+ defaultMessage='File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'
+ />
+ }
+ value={this.state.fileLocation}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ />
+ <TextSetting
+ id='fileFormat'
+ label={
+ <FormattedMessage
+ id='admin.log.formatTitle'
+ defaultMessage='File Format:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.log.formatPlaceholder', 'Enter your file format')}
+ helpText={this.renderFileFormatHelpText()}
+ value={this.state.fileFormat}
+ onChange={this.handleChange}
+ disabled={!this.state.enableFile}
+ />
+ </SettingsGroup>
+ );
+ }
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='consoleLevel'
- >
- <FormattedMessage
- id='admin.log.levelTitle'
- defaultMessage='Console Log Level:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='consoleLevel'
- ref='consoleLevel'
- defaultValue={this.props.config.LogSettings.consoleLevel}
- onChange={this.handleChange}
- disabled={!this.state.consoleEnable}
- >
- <option value='DEBUG'>{'DEBUG'}</option>
- <option value='INFO'>{'INFO'}</option>
- <option value='ERROR'>{'ERROR'}</option>
- </select>
- <p className='help-text'>
+ renderFileFormatHelpText() {
+ return (
+ <div>
+ <FormattedMessage
+ id='admin.log.formatDescription'
+ defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
+ />
+ <table
+ className='table table-bordered'
+ cellPadding='5'
+ >
+ <tbody>
+ <tr>
+ <td className='help-text'>{'%T'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.levelDescription'
- defaultMessage='This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- >
- <FormattedMessage
- id='admin.log.fileTitle'
- defaultMessage='Log To File: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='fileEnable'
- ref='fileEnable'
- value='true'
- defaultChecked={this.props.config.LogSettings.EnableFile}
- onChange={this.handleChange.bind(this, 'file_true')}
+ id='admin.log.formatTime'
+ defaultMessage='Time (15:04:05 MST)'
/>
- <FormattedMessage
- id='admin.log.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='fileEnable'
- value='false'
- defaultChecked={!this.props.config.LogSettings.EnableFile}
- onChange={this.handleChange.bind(this, 'file_false')}
- />
- <FormattedMessage
- id='admin.log.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%D'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.fileDescription'
- defaultMessage='Typically set to true in production. When true, log files are written to the log file specified in file location field below.'
+ id='admin.log.formatDateLong'
+ defaultMessage='Date (2006/01/02)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileLevel'
- >
- <FormattedMessage
- id='admin.log.fileLevelTitle'
- defaultMessage='File Log Level:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='fileLevel'
- ref='fileLevel'
- defaultValue={this.props.config.LogSettings.FileLevel}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- >
- <option value='DEBUG'>{'DEBUG'}</option>
- <option value='INFO'>{'INFO'}</option>
- <option value='ERROR'>{'ERROR'}</option>
- </select>
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%d'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.fileLevelDescription'
- defaultMessage='This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
+ id='admin.log.formatDateShort'
+ defaultMessage='Date (01/02/06)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileLocation'
- >
- <FormattedMessage
- id='admin.log.locationTitle'
- defaultMessage='File Location:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='fileLocation'
- ref='fileLocation'
- placeholder={formatMessage(holders.locationPlaceholder)}
- defaultValue={this.props.config.LogSettings.FileLocation}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- />
- <p className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%L'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.locationDescription'
- defaultMessage='File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'
+ id='admin.log.formatLevel'
+ defaultMessage='Level (DEBG, INFO, EROR)'
/>
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='fileFormat'
- >
- <FormattedMessage
- id='admin.log.formatTitle'
- defaultMessage='File Format:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='fileFormat'
- ref='fileFormat'
- placeholder={formatMessage(holders.formatPlaceholder)}
- defaultValue={this.props.config.LogSettings.FileFormat}
- onChange={this.handleChange}
- disabled={!this.state.fileEnable}
- />
- <div className='help-text'>
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%S'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.formatDescription'
- defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
+ id='admin.log.formatSource'
+ defaultMessage='Source'
/>
- <div className='help-text'>
- <table
- className='table table-bordered'
- cellPadding='5'
- >
- <tbody>
- <tr><td className='help-text'>{'%T'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatTime'
- defaultMessage='Time (15:04:05 MST)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%D'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatDateLong'
- defaultMessage='Date (2006/01/02)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%d'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatDateShort'
- defaultMessage='Date (01/02/06)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%L'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatLevel'
- defaultMessage='Level (DEBG, INFO, EROR)'
- />
- </td></tr>
- <tr><td className='help-text'>{'%S'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatSource'
- defaultMessage='Source'
- />
- </td></tr>
- <tr><td className='help-text'>{'%M'}</td><td className='help-text'>
- <FormattedMessage
- id='admin.log.formatMessage'
- defaultMessage='Message'
- />
- </td></tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
+ </td>
+ </tr>
+ <tr>
+ <td className='help-text'>{'%M'}</td><td className='help-text'>
<FormattedMessage
- id='admin.log.save'
- defaultMessage='Save'
+ id='admin.log.formatMessage'
+ defaultMessage='Message'
/>
- </button>
- </div>
- </div>
-
- </form>
+ </td>
+ </tr>
+ </tbody>
+ </table>
</div>
);
}
-}
-
-LogSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(LogSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/login_settings.jsx b/webapp/components/admin_console/login_settings.jsx
new file mode 100644
index 000000000..f473d8f56
--- /dev/null
+++ b/webapp/components/admin_console/login_settings.jsx
@@ -0,0 +1,130 @@
+// Copyright (c) 2015 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 BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class LoginSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ passwordResetSalt: props.config.EmailSettings.PasswordResetSalt,
+ maximumLoginAttempts: props.config.ServiceSettings.MaximumLoginAttempts,
+ enableMultifactorAuthentication: props.config.ServiceSettings.EnableMultifactorAuthentication
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.PasswordResetSalt = this.state.passwordResetSalt;
+ config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts);
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ config.ServiceSettings.EnableMultifactorAuthentication = this.state.enableMultifactorAuthentication;
+ }
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ let mfaSetting = null;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ mfaSetting = (
+ <BooleanSetting
+ id='enableMultifactorAuthentication'
+ label={
+ <FormattedMessage
+ id='admin.service.mfaTitle'
+ defaultMessage='Enable Multi-factor Authentication:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.mfaDesc'
+ defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
+ />
+ }
+ value={this.state.enableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.login'
+ defaultMessage='Login'
+ />
+ }
+ >
+ <GeneratedSetting
+ id='passwordResetSalt'
+ label={
+ <FormattedMessage
+ id='admin.email.passwordSaltTitle'
+ defaultMessage='Password Reset Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.passwordSaltDescription'
+ defaultMessage='32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.passwordResetSalt}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.passwordResetSalt.disabled'
+ defaultMessage='Password reset salt cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <TextSetting
+ id='maximumLoginAttempts'
+ label={
+ <FormattedMessage
+ id='admin.service.attemptTitle'
+ defaultMessage='Maximum Login Attempts:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.attemptExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.attemptDescription'
+ defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
+ />
+ }
+ value={this.state.maximumLoginAttempts}
+ onChange={this.handleChange}
+ />
+ {mfaSetting}
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index f2c6d92c3..ad0277b7f 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -99,4 +99,4 @@ export default class Logs extends React.Component {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx
index 5045a6d31..8905e57ef 100644
--- a/webapp/components/admin_console/privacy_settings.jsx
+++ b/webapp/components/admin_console/privacy_settings.jsx
@@ -1,215 +1,90 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- saving: {
- id: 'admin.privacy.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
import React from 'react';
-class PrivacySettings extends React.Component {
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class PrivacySettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.renderSettings = this.renderSettings.bind(this);
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ showEmailAddress: props.config.PrivacySettings.ShowEmailAddress,
+ showFullName: props.config.PrivacySettings.ShowFullName
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
+ getConfigFromState(config) {
+ config.PrivacySettings.ShowEmailAddress = this.state.showEmailAddress;
+ config.PrivacySettings.ShowFullName = this.state.showFullName;
- var config = this.props.config;
- config.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked;
- config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
);
}
- render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
- <h3>
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.privacy.title'
- defaultMessage='Privacy Settings'
+ id='admin.general.privacy'
+ defaultMessage='Privacy'
/>
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ShowEmailAddress'
- >
- <FormattedMessage
- id='admin.privacy.showEmailTitle'
- defaultMessage='Show Email Address: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowEmailAddress'
- value='true'
- ref='ShowEmailAddress'
- defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowEmailAddress'
- value='false'
- defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.privacy.showEmailDescription'
- defaultMessage='When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ShowFullName'
- >
- <FormattedMessage
- id='admin.privacy.showFullNameTitle'
- defaultMessage='Show Full Name: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowFullName'
- value='true'
- ref='ShowFullName'
- defaultChecked={this.props.config.PrivacySettings.ShowFullName}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='ShowFullName'
- value='false'
- defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.privacy.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.privacy.showFullNameDescription'
- defaultMessage='When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.privacy.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ }
+ >
+ <BooleanSetting
+ id='showEmailAddress'
+ label={
+ <FormattedMessage
+ id='admin.privacy.showEmailTitle'
+ defaultMessage='Show Email Address: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.privacy.showEmailDescription'
+ defaultMessage='When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'
+ />
+ }
+ value={this.state.showEmailAddress}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='showFullName'
+ label={
+ <FormattedMessage
+ id='admin.privacy.showFullNameTitle'
+ defaultMessage='Show Full Name: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.privacy.showFullNameDescription'
+ defaultMessage='When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.'
+ />
+ }
+ value={this.state.showFullName}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
);
}
-}
-
-PrivacySettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(PrivacySettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/public_link_settings.jsx b/webapp/components/admin_console/public_link_settings.jsx
new file mode 100644
index 000000000..9024261fa
--- /dev/null
+++ b/webapp/components/admin_console/public_link_settings.jsx
@@ -0,0 +1,91 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+
+export default class PublicLinkSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enablePublicLink: props.config.FileSettings.EnablePublicLink,
+ publicLinkSalt: props.config.FileSettings.PublicLinkSalt
+ });
+ }
+
+ getConfigFromState(config) {
+ config.FileSettings.EnablePublicLink = this.state.enablePublicLink;
+ config.FileSettings.PublicLinkSalt = this.state.publicLinkSalt;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.public_links'
+ defaultMessage='Public Links'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enablePublicLink'
+ label={
+ <FormattedMessage
+ id='admin.image.shareTitle'
+ defaultMessage='Share Public File Link: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.image.shareDescription'
+ defaultMessage='Allow users to share public links to files and images.'
+ />
+ }
+ value={this.state.enablePublicLink}
+ onChange={this.handleChange}
+ />
+ <GeneratedSetting
+ id='publicLinkSalt'
+ label={
+ <FormattedMessage
+ id='admin.image.publicLinkTitle'
+ defaultMessage='Public Link Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.image.publicLinkDescription'
+ defaultMessage='32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.publicLinkSalt}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx
new file mode 100644
index 000000000..660c23e97
--- /dev/null
+++ b/webapp/components/admin_console/push_settings.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import AdminSettings from './admin_settings.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const PUSH_NOTIFICATIONS_OFF = 'off';
+const PUSH_NOTIFICATIONS_MHPNS = 'mhpns';
+const PUSH_NOTIFICATIONS_MTPNS = 'mtpns';
+const PUSH_NOTIFICATIONS_CUSTOM = 'custom';
+
+export default class PushSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.canSave = this.canSave.bind(this);
+
+ this.handleAgreeChange = this.handleAgreeChange.bind(this);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ let pushNotificationServerType = PUSH_NOTIFICATIONS_CUSTOM;
+ let agree = false;
+ if (!props.config.EmailSettings.SendPushNotifications) {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_OFF;
+ } else if (props.config.EmailSettings.PushNotificationServer === Constants.MHPNS &&
+ global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_MHPNS;
+ agree = true;
+ } else if (props.config.EmailSettings.PushNotificationServer === Constants.MTPNS) {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_MTPNS;
+ } else {
+ pushNotificationServerType = PUSH_NOTIFICATIONS_CUSTOM;
+ }
+
+ let pushNotificationServer = this.props.config.EmailSettings.PushNotificationServer;
+ if (pushNotificationServerType === PUSH_NOTIFICATIONS_MTPNS) {
+ pushNotificationServer = Constants.MTPNS;
+ } else if (pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ pushNotificationServer = Constants.MHPNS;
+ }
+
+ this.state = Object.assign(this.state, {
+ pushNotificationServerType,
+ pushNotificationServer,
+ pushNotificationContents: props.config.EmailSettings.PushNotificationContents,
+ agree
+ });
+ }
+
+ canSave() {
+ return this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_MHPNS || this.state.agree;
+ }
+
+ handleAgreeChange(e) {
+ this.setState({
+ agree: e.target.checked
+ });
+ }
+
+ handleChange(id, value) {
+ if (id === 'pushNotificationServerType') {
+ this.setState({
+ agree: false
+ });
+
+ if (value === PUSH_NOTIFICATIONS_MHPNS) {
+ this.setState({
+ pushNotificationServer: Constants.MHPNS
+ });
+ } else if (value === PUSH_NOTIFICATIONS_MTPNS) {
+ this.setState({
+ pushNotificationServer: Constants.MTPNS
+ });
+ }
+ }
+
+ super.handleChange(id, value);
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.SendPushNotifications = this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_OFF;
+ config.EmailSettings.PushNotificationServer = this.state.pushNotificationServer.trim();
+ config.EmailSettings.PushNotificationContents = this.state.pushNotificationContents;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.notifications.title'
+ defaultMessage='Notification Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const pushNotificationServerTypes = [];
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_OFF, text: Utils.localizeMessage('admin.email.pushOff', 'Do not send push notifications')});
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MHPNS === 'true') {
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MHPNS, text: Utils.localizeMessage('admin.email.mhpns', 'Use encrypted, production-quality HPNS connection to iOS and Android apps')});
+ }
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MTPNS, text: Utils.localizeMessage('admin.email.mtpns', 'Use iOS and Android apps on iTunes and Google Play with TPNS')});
+ pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_CUSTOM, text: Utils.localizeMessage('admin.email.selfPush', 'Manually enter Push Notification Service location')});
+
+ let sendHelpText = null;
+ let pushServerHelpText = null;
+ if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_OFF) {
+ sendHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.pushOffHelp'
+ defaultMessage='Please see <a href="http://docs.mattermost.com/deployment/push.html#push-notifications-and-mobile-devices" target="_blank">documentation on push notifications</a> to learn more about setup options.'
+ />
+ );
+ } else if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.mhpnsHelp'
+ defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.'
+ />
+ );
+ } else if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MTPNS) {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.mtpnsHelp'
+ defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.'
+ />
+ );
+ } else {
+ pushServerHelpText = (
+ <FormattedHTMLMessage
+ id='admin.email.easHelp'
+ defaultMessage='Learn more about compiling and deploying your own mobile apps from an <a href="http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas" target="_blank">Enterprise App Store</a>.'
+ />
+ );
+ }
+
+ let tosCheckbox;
+ if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MHPNS) {
+ tosCheckbox = (
+ <div className='form-group'>
+ <div className='col-sm-4'/>
+ <div className='col-sm-8'>
+ <input
+ type='checkbox'
+ ref='agree'
+ checked={this.state.agree}
+ onChange={this.handleAgreeChange}
+ />
+ <FormattedHTMLMessage
+ id='admin.email.agreeHPNS'
+ defaultMessage=' 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>.'
+ />
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.notifications.push'
+ defaultMessage='Mobile Push'
+ />
+ }
+ >
+ <DropdownSetting
+ id='pushNotificationServerType'
+ values={pushNotificationServerTypes}
+ label={
+ <FormattedMessage
+ id='admin.email.pushTitle'
+ defaultMessage='Send Push Notifications: '
+ />
+ }
+ value={this.state.pushNotificationServerType}
+ onChange={this.handleChange}
+ helpText={sendHelpText}
+ />
+ {tosCheckbox}
+ <TextSetting
+ id='pushNotificationServer'
+ label={
+ <FormattedMessage
+ id='admin.email.pushServerTitle'
+ defaultMessage='Push Notification Server:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.email.pushServerEx', 'E.g.: "http://push-test.mattermost.com"')}
+ helpText={pushServerHelpText}
+ value={this.state.pushNotificationServer}
+ onChange={this.handleChange}
+ disabled={this.state.pushNotificationServerType !== PUSH_NOTIFICATIONS_CUSTOM}
+ />
+ <DropdownSetting
+ id='pushNotificationContents'
+ values={[
+ {value: 'generic', text: Utils.localizeMessage('admin.email.genericPushNotification', 'Send generic description with user and channel names')},
+ {value: 'full', text: Utils.localizeMessage('admin.email.fullPushNotification', 'Send full message snippet')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.email.pushContentTitle'
+ defaultMessage='Push Notification Contents:'
+ />
+ }
+ value={this.state.pushNotificationContents}
+ onChange={this.handleChange}
+ disabled={this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_OFF}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.email.pushContentDesc'
+ defaultMessage='Selecting "Send generic description with user and channel names" provides push notifications with generic messages, including names of users and channels but no specific details from the message text.<br /><br />
+ Selecting "Send full message snippet" sends excerpts from messages triggering notifications with specifics and may include confidential information sent in messages. If your Push Notification Service is outside your firewall, it is HIGHLY RECOMMENDED this option only be used with an "https" protocol to encrypt the connection.'
+ />
+ }
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx
index de7a40e6b..60818aaf9 100644
--- a/webapp/components/admin_console/rate_settings.jsx
+++ b/webapp/components/admin_console/rate_settings.jsx
@@ -1,371 +1,158 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
+import React from 'react';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-const holders = defineMessages({
- queriesExample: {
- id: 'admin.rate.queriesExample',
- defaultMessage: 'Ex "10"'
- },
- memoryExample: {
- id: 'admin.rate.memoryExample',
- defaultMessage: 'Ex "10000"'
- },
- httpHeaderExample: {
- id: 'admin.rate.httpHeaderExample',
- defaultMessage: 'Ex "X-Real-IP", "X-Forwarded-For"'
- },
- saving: {
- id: 'admin.rate.saving',
- defaultMessage: 'Saving Config...'
- }
-});
+import AdminSettings from './admin_settings.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 React from 'react';
-
-class RateSettings extends React.Component {
+export default class RateSettings extends AdminSettings {
constructor(props) {
super(props);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
- VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange(action) {
- var s = {saveNeeded: true, serverError: this.state.serverError};
-
- if (action === 'EnableRateLimiterTrue') {
- s.EnableRateLimiter = true;
- }
-
- if (action === 'EnableRateLimiterFalse') {
- s.EnableRateLimiter = false;
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- if (action === 'VaryByRemoteAddrTrue') {
- s.VaryByRemoteAddr = true;
- }
+ this.renderSettings = this.renderSettings.bind(this);
- if (action === 'VaryByRemoteAddrFalse') {
- s.VaryByRemoteAddr = false;
- }
-
- this.setState(s);
+ this.state = Object.assign(this.state, {
+ enableRateLimiter: props.config.RateLimitSettings.EnableRateLimiter,
+ perSec: props.config.RateLimitSettings.PerSec,
+ memoryStoreSize: props.config.RateLimitSettings.MemoryStoreSize,
+ varyByRemoteAddr: props.config.RateLimitSettings.VaryByRemoteAddr,
+ varyByHeader: props.config.RateLimitSettings.VaryByHeader
+ });
}
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.RateLimitSettings.EnableRateLimiter = ReactDOM.findDOMNode(this.refs.EnableRateLimiter).checked;
- config.RateLimitSettings.VaryByRemoteAddr = ReactDOM.findDOMNode(this.refs.VaryByRemoteAddr).checked;
- config.RateLimitSettings.VaryByHeader = ReactDOM.findDOMNode(this.refs.VaryByHeader).value.trim();
+ getConfigFromState(config) {
+ config.RateLimitSettings.EnableRateLimiter = this.state.enableRateLimiter;
+ config.RateLimitSettings.PerSec = this.parseIntNonZero(this.state.perSec);
+ config.RateLimitSettings.MemoryStoreSize = this.parseIntNonZero(this.state.memoryStoreSize);
+ config.RateLimitSettings.VaryByRemoteAddr = this.state.varyByRemoteAddr;
+ config.RateLimitSettings.VaryByHeader = this.state.varyByHeader;
- var PerSec = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10))) {
- PerSec = parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10);
- }
- config.RateLimitSettings.PerSec = PerSec;
- ReactDOM.findDOMNode(this.refs.PerSec).value = PerSec;
-
- var MemoryStoreSize = 10000;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
- MemoryStoreSize = parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10);
- }
- config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
- ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+ return config;
+ }
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.rate.title'
+ defaultMessage='Rate Limit Settings'
+ />
+ </h3>
);
}
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
+ renderSettings() {
return (
- <div className='wrapper--fixed'>
-
+ <SettingsGroup>
<div className='banner'>
<div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.rate.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.rate.noteDescription'
- defaultMessage='Changing properties in this section will require a server restart before taking effect.'
- />
- </p>
+ <FormattedMessage
+ id='admin.rate.noteDescription'
+ defaultMessage='Changing properties in this section will require a server restart before taking effect.'
+ />
</div>
</div>
-
- <h3>
- <FormattedMessage
- id='admin.rate.title'
- defaultMessage='Rate Limit Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableRateLimiter'
- >
- <FormattedMessage
- id='admin.rate.enableLimiterTitle'
- defaultMessage='Enable Rate Limiter: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableRateLimiter'
- value='true'
- ref='EnableRateLimiter'
- defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
- onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
- />
- <FormattedMessage
- id='admin.rate.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableRateLimiter'
- value='false'
- defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
- onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
- />
- <FormattedMessage
- id='admin.rate.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.enableLimiterDescription'
- defaultMessage='When true, APIs are throttled at rates specified below.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='PerSec'
- >
- <FormattedMessage
- id='admin.rate.queriesTitle'
- defaultMessage='Number Of Queries Per Second:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='PerSec'
- ref='PerSec'
- placeholder={formatMessage(holders.queriesExample)}
- defaultValue={this.props.config.RateLimitSettings.PerSec}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.queriesDescription'
- defaultMessage='Throttles API at this number of requests per second.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MemoryStoreSize'
- >
- <FormattedMessage
- id='admin.rate.memoryTitle'
- defaultMessage='Memory Store Size:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MemoryStoreSize'
- ref='MemoryStoreSize'
- placeholder={formatMessage(holders.memoryExample)}
- defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.memoryDescription'
- defaultMessage='Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='VaryByRemoteAddr'
- >
- <FormattedMessage
- id='admin.rate.remoteTitle'
- defaultMessage='Vary By Remote Address: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='VaryByRemoteAddr'
- value='true'
- ref='VaryByRemoteAddr'
- defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
- onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
- disabled={!this.state.EnableRateLimiter}
- />
- <FormattedMessage
- id='admin.rate.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='VaryByRemoteAddr'
- value='false'
- defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
- onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
- disabled={!this.state.EnableRateLimiter}
- />
- <FormattedMessage
- id='admin.rate.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.remoteDescription'
- defaultMessage='When true, rate limit API access by IP address.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='VaryByHeader'
- >
- <FormattedMessage
- id='admin.rate.httpHeaderTitle'
- defaultMessage='Vary By HTTP Header:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='VaryByHeader'
- ref='VaryByHeader'
- placeholder={formatMessage(holders.httpHeaderExample)}
- defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
- onChange={this.handleChange}
- disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.rate.httpHeaderDescription'
- defaultMessage='When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.rate.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
+ <BooleanSetting
+ id='enableRateLimiter'
+ label={
+ <FormattedMessage
+ id='admin.rate.enableLimiterTitle'
+ defaultMessage='Enable Rate Limiter: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.rate.enableLimiterDescription'
+ defaultMessage='When true, APIs are throttled at rates specified below.'
+ />
+ }
+ value={this.state.enableRateLimiter}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='perSec'
+ label={
+ <FormattedMessage
+ id='admin.rate.queriesTitle'
+ defaultMessage='Number Of Queries Per Second:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.queriesExample', 'Ex "10"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.queriesDescription'
+ defaultMessage='Throttles API at this number of requests per second.'
+ />
+ }
+ value={this.state.perSec}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <TextSetting
+ id='memoryStoreSize'
+ label={
+ <FormattedMessage
+ id='admin.rate.memoryTitle'
+ defaultMessage='Memory Store Size:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.memoryExample', 'Ex "10000"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.memoryDescription'
+ defaultMessage='Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'
+ />
+ }
+ value={this.state.memoryStoreSize}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <BooleanSetting
+ id='varyByRemoteAddr'
+ label={
+ <FormattedMessage
+ id='admin.rate.remoteTitle'
+ defaultMessage='Vary By Remote Address: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.rate.remoteDescription'
+ defaultMessage='When true, rate limit API access by IP address.'
+ />
+ }
+ value={this.state.varyByRemoteAddr}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter}
+ />
+ <TextSetting
+ id='varyByHeader'
+ label={
+ <FormattedMessage
+ id='admin.rate.httpHeaderTitle'
+ defaultMessage='Vary By HTTP Header:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.rate.httpHeaderExample', 'Ex "X-Real-IP", "X-Forwarded-For"')}
+ helpText={
+ <FormattedMessage
+ id='admin.rate.httpHeaderDescription'
+ defaultMessage='When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'
+ />
+ }
+ value={this.state.varyByHeader}
+ onChange={this.handleChange}
+ disabled={!this.state.enableRateLimiter || this.state.varyByRemoteAddr}
+ />
+ </SettingsGroup>
);
}
-}
-
-RateSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(RateSettings); \ No newline at end of file
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/recycle_db.jsx b/webapp/components/admin_console/recycle_db.jsx
new file mode 100644
index 000000000..47ef2b0bf
--- /dev/null
+++ b/webapp/components/admin_console/recycle_db.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class RecycleDbButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleRecycle = this.handleRecycle.bind(this);
+
+ this.state = {
+ loading: false,
+ fail: null
+ };
+ }
+
+ handleRecycle(e) {
+ e.preventDefault();
+
+ this.setState({
+ loading: true,
+ fail: null
+ });
+
+ Client.recycleDatabaseConnection(
+ () => {
+ this.setState({
+ loading: false
+ });
+ },
+ (err) => {
+ this.setState({
+ loading: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.recycle.reloadFail'
+ defaultMessage='Recycling unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.loading) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.recycle.loading', ' Recycling...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.recycle.button'
+ defaultMessage='Recycle Database Connections'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group recycle-db'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div>
+ <button
+ className='btn btn-default'
+ onClick={this.handleRecycle}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/reload_config.jsx b/webapp/components/admin_console/reload_config.jsx
new file mode 100644
index 000000000..c137afaf9
--- /dev/null
+++ b/webapp/components/admin_console/reload_config.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Client from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class ReloadConfigButton extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleReloadConfig = this.handleReloadConfig.bind(this);
+
+ this.state = {
+ loading: false,
+ fail: null
+ };
+ }
+
+ handleReloadConfig(e) {
+ e.preventDefault();
+
+ this.setState({
+ loading: true,
+ fail: null
+ });
+
+ Client.reloadConfig(
+ () => {
+ this.setState({
+ loading: false
+ });
+ },
+ (err) => {
+ this.setState({
+ loading: false,
+ fail: err.message + ' - ' + err.detailed_error
+ });
+ }
+ );
+ }
+
+ render() {
+ let testMessage = null;
+ if (this.state.fail) {
+ testMessage = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.reload.reloadFail'
+ defaultMessage='Reload unsuccessful: {error}'
+ values={{
+ error: this.state.fail
+ }}
+ />
+ </div>
+ );
+ }
+
+ let contents = null;
+ if (this.state.loading) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ {Utils.localizeMessage('admin.reload.loading', ' Loading...')}
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.reload.button'
+ defaultMessage='Reload Configuration From Disk'
+ />
+ );
+ }
+
+ return (
+ <div className='form-group reload-config'>
+ <div className='col-sm-offset-4 col-sm-8'>
+ <div>
+ <button
+ className='btn btn-default'
+ onClick={this.handleReloadConfig}
+ >
+ {contents}
+ </button>
+ {testMessage}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/save_button.jsx b/webapp/components/admin_console/save_button.jsx
new file mode 100644
index 000000000..18bb6e96d
--- /dev/null
+++ b/webapp/components/admin_console/save_button.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class SaveButton extends React.Component {
+ static get propTypes() {
+ return {
+ saving: React.PropTypes.bool.isRequired,
+ disabled: React.PropTypes.bool
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ disabled: false
+ };
+ }
+
+ render() {
+ const {saving, disabled, ...props} = this.props; // eslint-disable-line no-use-before-define
+
+ let contents;
+ if (saving) {
+ contents = (
+ <span>
+ <span className='glyphicon glyphicon-refresh glyphicon-refresh-animate'/>
+ <FormattedMessage
+ id='admin.saving'
+ defaultMessage='Saving Config...'
+ />
+ </span>
+ );
+ } else {
+ contents = (
+ <FormattedMessage
+ id='admin.save'
+ defaultMessage='Save'
+ />
+ );
+ }
+
+ let className = 'save-button btn';
+ if (!disabled) {
+ className += ' btn-primary';
+ }
+
+ return (
+ <button
+ type='submit'
+ className={className}
+ disabled={disabled}
+ {...props}
+ >
+ {contents}
+ </button>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx
deleted file mode 100644
index 90b6a39b4..000000000
--- a/webapp/components/admin_console/service_settings.jsx
+++ /dev/null
@@ -1,1042 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const DefaultSessionLength = 30;
-const DefaultMaximumLoginAttempts = 10;
-const DefaultSessionCacheInMinutes = 10;
-
-var holders = defineMessages({
- listenExample: {
- id: 'admin.service.listenExample',
- defaultMessage: 'Ex ":8065"'
- },
- attemptExample: {
- id: 'admin.service.attemptExample',
- defaultMessage: 'Ex "10"'
- },
- segmentExample: {
- id: 'admin.service.segmentExample',
- defaultMessage: 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
- },
- googleExample: {
- id: 'admin.service.googleExample',
- defaultMessage: 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
- },
- sessionDaysEx: {
- id: 'admin.service.sessionDaysEx',
- defaultMessage: 'Ex "30"'
- },
- corsExample: {
- id: 'admin.service.corsEx',
- defaultMessage: 'http://example.com'
- },
- saving: {
- id: 'admin.service.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
-
-class ServiceSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.ServiceSettings.ListenAddress = ReactDOM.findDOMNode(this.refs.ListenAddress).value.trim();
- if (config.ServiceSettings.ListenAddress === '') {
- config.ServiceSettings.ListenAddress = ':8065';
- ReactDOM.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
- }
-
- config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
- config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
- config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
- config.ServiceSettings.EnableOutgoingWebhooks = ReactDOM.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
- config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
- config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
- config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
- config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
- config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
- config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked;
- config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
- config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
-
- if (this.refs.EnableMultifactorAuthentication) {
- config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked;
- }
-
- //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
-
- var MaximumLoginAttempts = DefaultMaximumLoginAttempts;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
- MaximumLoginAttempts = parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
- }
- if (MaximumLoginAttempts < 1) {
- MaximumLoginAttempts = 1;
- }
- config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
- ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
-
- var SessionLengthWebInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10))) {
- SessionLengthWebInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10);
- }
- if (SessionLengthWebInDays < 1) {
- SessionLengthWebInDays = 1;
- }
- config.ServiceSettings.SessionLengthWebInDays = SessionLengthWebInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value = SessionLengthWebInDays;
-
- var SessionLengthMobileInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10))) {
- SessionLengthMobileInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10);
- }
- if (SessionLengthMobileInDays < 1) {
- SessionLengthMobileInDays = 1;
- }
- config.ServiceSettings.SessionLengthMobileInDays = SessionLengthMobileInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value = SessionLengthMobileInDays;
-
- var SessionLengthSSOInDays = DefaultSessionLength;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10))) {
- SessionLengthSSOInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10);
- }
- if (SessionLengthSSOInDays < 1) {
- SessionLengthSSOInDays = 1;
- }
- config.ServiceSettings.SessionLengthSSOInDays = SessionLengthSSOInDays;
- ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value = SessionLengthSSOInDays;
-
- var SessionCacheInMinutes = DefaultSessionCacheInMinutes;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10))) {
- SessionCacheInMinutes = parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10);
- }
- if (SessionCacheInMinutes < -1) {
- SessionCacheInMinutes = -1;
- }
- config.ServiceSettings.SessionCacheInMinutes = SessionCacheInMinutes;
- ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value = SessionCacheInMinutes;
-
- config.ServiceSettings.AllowCorsFrom = ReactDOM.findDOMNode(this.refs.AllowCorsFrom).value.trim();
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- let mfaSetting;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
- mfaSetting = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableMultifactorAuthentication'
- >
- <FormattedMessage
- id='admin.service.mfaTitle'
- defaultMessage='Enable Multi-factor Authentication:'
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableMultifactorAuthentication'
- value='true'
- ref='EnableMultifactorAuthentication'
- defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableMultifactorAuthentication'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.mfaDesc'
- defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <h3>
- <FormattedMessage
- id='admin.service.title'
- defaultMessage='Service Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='ListenAddress'
- >
- <FormattedMessage
- id='admin.service.listenAddress'
- defaultMessage='Listen Address:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='ListenAddress'
- ref='ListenAddress'
- placeholder={formatMessage(holders.listenExample)}
- defaultValue={this.props.config.ServiceSettings.ListenAddress}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.listenDescription'
- defaultMessage='The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaximumLoginAttempts'
- >
- <FormattedMessage
- id='admin.service.attemptTitle'
- defaultMessage='Maximum Login Attempts:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaximumLoginAttempts'
- ref='MaximumLoginAttempts'
- placeholder={formatMessage(holders.attemptExample)}
- defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.attemptDescription'
- defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SegmentDeveloperKey'
- >
- <FormattedMessage
- id='admin.service.segmentTitle'
- defaultMessage='Segment Developer Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SegmentDeveloperKey'
- ref='SegmentDeveloperKey'
- placeholder={formatMessage(holders.segmentExample)}
- defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.segmentDescription'
- defaultMessage='For users running a SaaS services, sign up for a key at Segment.com to track metrics.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='GoogleDeveloperKey'
- >
- <FormattedMessage
- id='admin.service.googleTitle'
- defaultMessage='Google Developer Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='GoogleDeveloperKey'
- ref='GoogleDeveloperKey'
- placeholder={formatMessage(holders.googleExample)}
- defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.service.googleDescription'
- defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableIncomingWebhooks'
- >
- <FormattedMessage
- id='admin.service.webhooksTitle'
- defaultMessage='Enable Incoming Webhooks: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableIncomingWebhooks'
- value='true'
- ref='EnableIncomingWebhooks'
- defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableIncomingWebhooks'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.webhooksDescription'
- defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOutgoingWebhooks'
- >
- <FormattedMessage
- id='admin.service.outWebhooksTitle'
- defaultMessage='Enable Outgoing Webhooks: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOutgoingWebhooks'
- value='true'
- ref='EnableOutgoingWebhooks'
- defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOutgoingWebhooks'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.outWebhooksDesc'
- defaultMessage='When true, outgoing webhooks will be allowed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableCommands'
- >
- <FormattedMessage
- id='admin.service.cmdsTitle'
- defaultMessage='Enable Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCommands'
- value='true'
- ref='EnableCommands'
- defaultChecked={this.props.config.ServiceSettings.EnableCommands}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCommands'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableCommands}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.cmdsDesc'
- defaultMessage='When true, user created slash commands will be allowed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOnlyAdminIntegrations'
- >
- <FormattedMessage
- id='admin.service.integrationAdmin'
- defaultMessage='Enable Integrations for Admin Only: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOnlyAdminIntegrations'
- value='true'
- ref='EnableOnlyAdminIntegrations'
- defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOnlyAdminIntegrations'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.integrationAdminDesc'
- defaultMessage='When true, user created integrations can only be created by admins.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePostUsernameOverride'
- >
- <FormattedMessage
- id='admin.service.overrideTitle'
- defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostUsernameOverride'
- value='true'
- ref='EnablePostUsernameOverride'
- defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostUsernameOverride'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.overrideDescription'
- defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnablePostIconOverride'
- >
- <FormattedMessage
- id='admin.service.iconTitle'
- defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostIconOverride'
- value='true'
- ref='EnablePostIconOverride'
- defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnablePostIconOverride'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.iconDescription'
- defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableTesting'
- >
- <FormattedMessage
- id='admin.service.testingTitle'
- defaultMessage='Enable Testing: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTesting'
- value='true'
- ref='EnableTesting'
- defaultChecked={this.props.config.ServiceSettings.EnableTesting}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTesting'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.testingDescription'
- defaultMessage='(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableDeveloper'
- >
- <FormattedMessage
- id='admin.service.developerTitle'
- defaultMessage='Enable Developer Mode: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDeveloper'
- value='true'
- ref='EnableDeveloper'
- defaultChecked={this.props.config.ServiceSettings.EnableDeveloper}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableDeveloper'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableDeveloper}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.developerDesc'
- defaultMessage='(Developer Option) When true, extra information around errors will be displayed in the UI.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableSecurityFixAlert'
- >
- <FormattedMessage
- id='admin.service.securityTitle'
- defaultMessage='Enable Security Alerts: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='true'
- ref='EnableSecurityFixAlert'
- defaultChecked={this.props.config.ServiceSettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableSecurityFixAlert'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableSecurityFixAlert}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.securityDesc'
- defaultMessage='When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableInsecureOutgoingConnections'
- >
- <FormattedMessage
- id='admin.service.insecureTlsTitle'
- defaultMessage='Enable Insecure Outgoing Connections: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableInsecureOutgoingConnections'
- value='true'
- ref='EnableInsecureOutgoingConnections'
- defaultChecked={this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableInsecureOutgoingConnections'
- value='false'
- defaultChecked={!this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.service.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.insecureTlsDesc'
- defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.'
- />
- </p>
- </div>
- </div>
-
- {mfaSetting}
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AllowCorsFrom'
- >
- <FormattedMessage
- id='admin.service.corsTitle'
- defaultMessage='Allow Cross-origin Requests from:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AllowCorsFrom'
- ref='AllowCorsFrom'
- placeholder={formatMessage(holders.corsExample)}
- defaultValue={this.props.config.ServiceSettings.AllowCorsFrom}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.corsDescription'
- defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthWebInDays'
- >
- <FormattedMessage
- id='admin.service.webSessionDays'
- defaultMessage='Session Length for Web in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthWebInDays'
- ref='SessionLengthWebInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthWebInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.webSessionDaysDesc'
- defaultMessage='The web session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthMobileInDays'
- >
- <FormattedMessage
- id='admin.service.mobileSessionDays'
- defaultMessage='Session Length for Mobile Device in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthMobileInDays'
- ref='SessionLengthMobileInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthMobileInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.mobileSessionDaysDesc'
- defaultMessage='The native mobile session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionLengthSSOInDays'
- >
- <FormattedMessage
- id='admin.service.ssoSessionDays'
- defaultMessage='Session Length for SSO in Days:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionLengthSSOInDays'
- ref='SessionLengthSSOInDays'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionLengthSSOInDays}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.ssoSessionDaysDesc'
- defaultMessage='The SSO session will expire after the number of days specified and will require a user to login again.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SessionCacheInMinutes'
- >
- <FormattedMessage
- id='admin.service.sessionCache'
- defaultMessage='Session Cache in Minutes:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SessionCacheInMinutes'
- ref='SessionCacheInMinutes'
- placeholder={formatMessage(holders.sessionDaysEx)}
- defaultValue={this.props.config.ServiceSettings.SessionCacheInMinutes}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.service.sessionCacheDesc'
- defaultMessage='The number of minutes to cache a session in memory.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.service.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-// <div className='form-group'>
-// <label
-// className='control-label col-sm-4'
-// htmlFor='EnableOAuthServiceProvider'
-// >
-// {'Enable OAuth Service Provider: '}
-// </label>
-// <div className='col-sm-8'>
-// <label className='radio-inline'>
-// <input
-// type='radio'
-// name='EnableOAuthServiceProvider'
-// value='true'
-// ref='EnableOAuthServiceProvider'
-// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
-// onChange={this.handleChange}
-// />
-// {'true'}
-// </label>
-// <label className='radio-inline'>
-// <input
-// type='radio'
-// name='EnableOAuthServiceProvider'
-// value='false'
-// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
-// onChange={this.handleChange}
-// />
-// {'false'}
-// </label>
-// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
-// </div>
-// </div>
-
-ServiceSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(ServiceSettings);
diff --git a/webapp/components/admin_console/session_settings.jsx b/webapp/components/admin_console/session_settings.jsx
new file mode 100644
index 000000000..79f3c7ee5
--- /dev/null
+++ b/webapp/components/admin_console/session_settings.jsx
@@ -0,0 +1,134 @@
+// Copyright (c) 2015 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 {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+export default class SessionSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ sessionLengthWebInDays: props.config.ServiceSettings.SessionLengthWebInDays,
+ sessionLengthMobileInDays: props.config.ServiceSettings.SessionLengthMobileInDays,
+ sessionLengthSSOInDays: props.config.ServiceSettings.SessionLengthSSOInDays,
+ sessionCacheInMinutes: props.config.ServiceSettings.SessionCacheInMinutes
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.SessionLengthWebInDays = this.parseIntNonZero(this.state.sessionLengthWebInDays);
+ config.ServiceSettings.SessionLengthMobileInDays = this.parseIntNonZero(this.state.sessionLengthMobileInDays);
+ config.ServiceSettings.SessionLengthSSOInDays = this.parseIntNonZero(this.state.sessionLengthSSOInDays);
+ config.ServiceSettings.SessionCacheInMinutes = this.parseIntNonZero(this.state.sessionCacheInMinutes);
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.session'
+ defaultMessage='Sessions'
+ />
+ }
+ >
+ <TextSetting
+ id='sessionLengthWebInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.webSessionDays'
+ defaultMessage='Session Length for Web in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.webSessionDaysDesc'
+ defaultMessage='The web session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthWebInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionLengthMobileInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.mobileSessionDays'
+ defaultMessage='Session Length for Mobile Device in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.mobileSessionDaysDesc'
+ defaultMessage='The native mobile session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthMobileInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionLengthSSOInDays'
+ label={
+ <FormattedMessage
+ id='admin.service.ssoSessionDays'
+ defaultMessage='Session Length for SSO in Days:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.ssoSessionDaysDesc'
+ defaultMessage='The SSO session will expire after the number of days specified and will require a user to login again.'
+ />
+ }
+ value={this.state.sessionLengthSSOInDays}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='sessionCacheInMinutes'
+ label={
+ <FormattedMessage
+ id='admin.service.sessionCache'
+ defaultMessage='Session Cache in Minutes:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.service.sessionDaysEx', 'Ex "30"')}
+ helpText={
+ <FormattedMessage
+ id='admin.service.sessionCacheDesc'
+ defaultMessage='The number of minutes to cache a session in memory.'
+ />
+ }
+ value={this.state.sessionCacheInMinutes}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/setting.jsx b/webapp/components/admin_console/setting.jsx
index 7dee6c8dc..024111fa5 100644
--- a/webapp/components/admin_console/setting.jsx
+++ b/webapp/components/admin_console/setting.jsx
@@ -5,20 +5,19 @@ import React from 'react';
export default class Setting extends React.Component {
render() {
- let marginClass = '';
- if (this.props.margin === 'small') {
- marginClass = ' form-group--small';
- }
-
return (
- <div className={'form-group' + marginClass}>
+ <div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor={this.props.inputId}
>
{this.props.label}
</label>
<div className='col-sm-8'>
{this.props.children}
+ <div className='help-text'>
+ {this.props.helpText}
+ </div>
</div>
</div>
);
@@ -28,7 +27,8 @@ Setting.defaultProps = {
};
Setting.propTypes = {
+ inputId: React.PropTypes.string,
label: React.PropTypes.node.isRequired,
children: React.PropTypes.node.isRequired,
- margin: React.PropTypes.oneOf(['', 'small'])
+ helpText: React.PropTypes.node
};
diff --git a/webapp/components/admin_console/settings_group.jsx b/webapp/components/admin_console/settings_group.jsx
new file mode 100644
index 000000000..10b3444d8
--- /dev/null
+++ b/webapp/components/admin_console/settings_group.jsx
@@ -0,0 +1,42 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+export default class SettingsGroup extends React.Component {
+ static get propTypes() {
+ return {
+ show: React.PropTypes.bool.isRequired,
+ header: React.PropTypes.node,
+ children: React.PropTypes.node
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ show: true
+ };
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let header = null;
+ if (this.props.header) {
+ header = (
+ <h4>
+ {this.props.header}
+ </h4>
+ );
+ }
+
+ return (
+ <div className='admin-settings__group'>
+ {header}
+ {this.props.children}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/signup_settings.jsx b/webapp/components/admin_console/signup_settings.jsx
new file mode 100644
index 000000000..fd64e4ea5
--- /dev/null
+++ b/webapp/components/admin_console/signup_settings.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import GeneratedSetting from './generated_setting.jsx';
+import SettingsGroup from './settings_group.jsx';
+
+export default class SignupSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ requireEmailVerification: props.config.EmailSettings.RequireEmailVerification,
+ inviteSalt: props.config.EmailSettings.InviteSalt,
+ enableOpenServer: props.config.TeamSettings.EnableOpenServer
+ });
+ }
+
+ getConfigFromState(config) {
+ config.EmailSettings.RequireEmailVerification = this.state.requireEmailVerification;
+ config.EmailSettings.InviteSalt = this.state.inviteSalt;
+ config.TeamSettings.EnableOpenServer = this.state.enableOpenServer;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.security.title'
+ defaultMessage='Security Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.security.signup'
+ defaultMessage='Signup'
+ />
+ }
+ >
+ <BooleanSetting
+ id='requireEmailVerification'
+ label={
+ <FormattedMessage
+ id='admin.email.requireVerificationTitle'
+ defaultMessage='Require Email Verification: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.requireVerificationDescription'
+ defaultMessage='Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'
+ />
+ }
+ value={this.state.requireEmailVerification}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.requireEmailVerification.disabled'
+ defaultMessage='Email verification cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <GeneratedSetting
+ id='inviteSalt'
+ label={
+ <FormattedMessage
+ id='admin.email.inviteSaltTitle'
+ defaultMessage='Invite Salt:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.email.inviteSaltDescription'
+ defaultMessage='32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'
+ />
+ }
+ value={this.state.inviteSalt}
+ onChange={this.handleChange}
+ disabled={this.state.sendEmailNotifications}
+ disabledText={
+ <FormattedMessage
+ id='admin.security.inviteSalt.disabled'
+ defaultMessage='Invite salt cannot be changed while sending emails is disabled.'
+ />
+ }
+ />
+ <BooleanSetting
+ id='enableOpenServer'
+ label={
+ <FormattedMessage
+ id='admin.team.openServerTitle'
+ defaultMessage='Enable Open Server: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.openServerDescription'
+ defaultMessage='When true, anyone can signup for a user account on this server without the need to be invited.'
+ />
+ }
+ value={this.state.enableOpenServer}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/sql_settings.jsx b/webapp/components/admin_console/sql_settings.jsx
deleted file mode 100644
index f2e005b83..000000000
--- a/webapp/components/admin_console/sql_settings.jsx
+++ /dev/null
@@ -1,390 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import crypto from 'crypto';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-const holders = defineMessages({
- warning: {
- id: 'admin.sql.warning',
- defaultMessage: 'Warning: re-generating this salt may cause some columns in the database to return empty results.'
- },
- maxConnectionsExample: {
- id: 'admin.sql.maxConnectionsExample',
- defaultMessage: 'Ex "10"'
- },
- maxOpenExample: {
- id: 'admin.sql.maxOpenExample',
- defaultMessage: 'Ex "10"'
- },
- keyExample: {
- id: 'admin.sql.keyExample',
- defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
- },
- saving: {
- id: 'admin.sql.saving',
- defaultMessage: 'Saving Config...'
- }
-});
-
-import React from 'react';
-
-class SqlSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleGenerate = this.handleGenerate.bind(this);
-
- this.state = {
- saveNeeded: false,
- serverError: null
- };
- }
-
- handleChange() {
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked;
- config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
-
- if (config.SqlSettings.AtRestEncryptKey === '') {
- config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
- ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
- }
-
- var MaxOpenConns = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
- MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10);
- }
- config.SqlSettings.MaxOpenConns = MaxOpenConns;
- ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
-
- var MaxIdleConns = 10;
- if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
- MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10);
- }
- config.SqlSettings.MaxIdleConns = MaxIdleConns;
- ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- handleGenerate(e) {
- e.preventDefault();
-
- var cfm = global.window.confirm(this.props.intl.formatMessage(holders.warning));
- if (cfm === false) {
- return;
- }
-
- ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
- var s = {saveNeeded: true, serverError: this.state.serverError};
- this.setState(s);
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
-
- var dataSourceReplicas = '';
- this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
- dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
- });
-
- if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
- dataSourceReplicas = 'none';
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <div className='banner'>
- <div className='banner__content'>
- <h4 className='banner__heading'>
- <FormattedMessage
- id='admin.sql.noteTitle'
- defaultMessage='Note:'
- />
- </h4>
- <p>
- <FormattedMessage
- id='admin.sql.noteDescription'
- defaultMessage='Changing properties in this section will require a server restart before taking effect.'
- />
- </p>
- </div>
- </div>
-
- <h3>
- <FormattedMessage
- id='admin.sql.title'
- defaultMessage='SQL Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DriverName'
- >
- <FormattedMessage
- id='admin.sql.driverName'
- defaultMessage='Driver Name:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DataSource'
- >
- <FormattedMessage
- id='admin.sql.dataSource'
- defaultMessage='Data Source:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{dataSource}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='DataSourceReplicas'
- >
- <FormattedMessage
- id='admin.sql.replicas'
- defaultMessage='Data Source Replicas:'
- />
- </label>
- <div className='col-sm-8'>
- <p className='help-text'>{dataSourceReplicas}</p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxIdleConns'
- >
- <FormattedMessage
- id='admin.sql.maxConnectionsTitle'
- defaultMessage='Maximum Idle Connections:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxIdleConns'
- ref='MaxIdleConns'
- placeholder={formatMessage(holders.maxConnectionsExample)}
- defaultValue={this.props.config.SqlSettings.MaxIdleConns}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.maxConnectionsDescription'
- defaultMessage='Maximum number of idle connections held open to the database.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxOpenConns'
- >
- <FormattedMessage
- id='admin.sql.maxOpenTitle'
- defaultMessage='Maximum Open Connections:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxOpenConns'
- ref='MaxOpenConns'
- placeholder={formatMessage(holders.maxOpenExample)}
- defaultValue={this.props.config.SqlSettings.MaxOpenConns}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.maxOpenDescription'
- defaultMessage='Maximum number of open connections held open to the database.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='AtRestEncryptKey'
- >
- <FormattedMessage
- id='admin.sql.keyTitle'
- defaultMessage='At Rest Encrypt Key:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='AtRestEncryptKey'
- ref='AtRestEncryptKey'
- placeholder={formatMessage(holders.keyExample)}
- defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.keyDescription'
- defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
- />
- </p>
- <div className='help-text'>
- <button
- className='btn btn-default'
- onClick={this.handleGenerate}
- >
- <FormattedMessage
- id='admin.sql.regenerate'
- defaultMessage='Re-Generate'
- />
- </button>
- </div>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='Trace'
- >
- <FormattedMessage
- id='admin.sql.traceTitle'
- defaultMessage='Trace: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Trace'
- value='true'
- ref='Trace'
- defaultChecked={this.props.config.SqlSettings.Trace}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.sql.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='Trace'
- value='false'
- defaultChecked={!this.props.config.SqlSettings.Trace}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.sql.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.sql.traceDescription'
- defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.sql.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-SqlSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(SqlSettings); \ No newline at end of file
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
new file mode 100644
index 000000000..7cfa9cf3b
--- /dev/null
+++ b/webapp/components/admin_console/storage_settings.jsx
@@ -0,0 +1,200 @@
+// Copyright (c) 2015 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 DropdownSetting from './dropdown_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const DRIVER_LOCAL = 'local';
+const DRIVER_S3 = 'amazons3';
+
+export default class StorageSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ maxFileSize: props.config.FileSettings.MaxFileSize,
+ driverName: props.config.FileSettings.DriverName,
+ directory: props.config.FileSettings.Directory,
+ amazonS3AccessKeyId: props.config.FileSettings.AmazonS3AccessKeyId,
+ amazonS3SecretAccessKey: props.config.FileSettings.AmazonS3SecretAccessKey,
+ amazonS3Bucket: props.config.FileSettings.AmazonS3Bucket,
+ amazonS3Region: props.config.FileSettings.AmazonS3Region
+ });
+ }
+
+ getConfigFromState(config) {
+ config.FileSettings.MaxFileSize = this.parseInt(this.state.maxFileSize);
+ config.FileSettings.DriverName = this.state.driverName;
+ config.FileSettings.Directory = this.state.directory;
+ config.FileSettings.AmazonS3AccessKeyId = this.state.amazonS3AccessKeyId;
+ config.FileSettings.AmazonS3SecretAccessKey = this.state.amazonS3SecretAccessKey;
+ config.FileSettings.AmazonS3Bucket = this.state.amazonS3Bucket;
+ config.FileSettings.AmazonS3Region = this.state.amazonS3Region;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.files.title'
+ defaultMessage='File Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.files.storage'
+ defaultMessage='Storage'
+ />
+ }
+ >
+ <TextSetting
+ id='maxFileSize'
+ label={
+ <FormattedMessage
+ id='admin.image.maxFileSizeTitle'
+ defaultMessage='Max File Size:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.maxFileSizeExample', 'Ex "52428800"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.maxFileSizeDescription'
+ defaultMessage='Max File Size in bytes. If blank, will be set to 52428800 (50MB).'
+ />
+ }
+ value={this.state.maxFileSize}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='driverName'
+ values={[
+ {value: DRIVER_LOCAL, text: Utils.localizeMessage('admin.image.storeLocal', 'Local File System')},
+ {value: DRIVER_S3, text: Utils.localizeMessage('admin.image.storeAmazonS3', 'Amazon S3')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.image.storeTitle'
+ defaultMessage='Store Files In:'
+ />
+ }
+ value={this.state.driverName}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='directory'
+ label={
+ <FormattedMessage
+ id='admin.image.localTitle'
+ defaultMessage='Local Directory Location:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.localExample', 'Ex "./data/"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.localDescription'
+ defaultMessage='Directory to which image files are written. If blank, will be set to ./data/.'
+ />
+ }
+ value={this.state.directory}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_LOCAL}
+ />
+ <TextSetting
+ id='amazonS3AccessKeyId'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3IdTitle'
+ defaultMessage='Amazon S3 Access Key Id:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3IdExample', 'Ex "AKIADTOVBGERKLCBV"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3IdDescription'
+ defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
+ />
+ }
+ value={this.state.amazonS3AccessKeyId}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3SecretAccessKey'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3SecretTitle'
+ defaultMessage='Amazon S3 Secret Access Key:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3SecretExample', 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3SecretDescription'
+ defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
+ />
+ }
+ value={this.state.amazonS3SecretAccessKey}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3Bucket'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3BucketTitle'
+ defaultMessage='Amazon S3 Bucket:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3BucketExample', 'Ex "mattermost-media"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3BucketDescription'
+ defaultMessage='Name you selected for your S3 bucket in AWS.'
+ />
+ }
+ value={this.state.amazonS3Bucket}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ <TextSetting
+ id='amazonS3Region'
+ label={
+ <FormattedMessage
+ id='admin.image.amazonS3RegionTitle'
+ defaultMessage='Amazon S3 Region:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.image.amazonS3RegionExample', 'Ex "us-east-1"')}
+ helpText={
+ <FormattedMessage
+ id='admin.image.amazonS3RegionDescription'
+ defaultMessage='AWS region you selected for creating your S3 bucket.'
+ />
+ }
+ value={this.state.amazonS3Region}
+ onChange={this.handleChange}
+ disabled={this.state.driverName !== DRIVER_S3}
+ />
+ </SettingsGroup>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx
deleted file mode 100644
index e7bfcd74a..000000000
--- a/webapp/components/admin_console/team_settings.jsx
+++ /dev/null
@@ -1,735 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as Utils from 'utils/utils.jsx';
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const holders = defineMessages({
- siteNameExample: {
- id: 'admin.team.siteNameExample',
- defaultMessage: 'Ex "Mattermost"'
- },
- maxUsersExample: {
- id: 'admin.team.maxUsersExample',
- defaultMessage: 'Ex "25"'
- },
- restrictExample: {
- id: 'admin.team.restrictExample',
- defaultMessage: 'Ex "corp.mattermost.com, mattermost.org"'
- },
- saving: {
- id: 'admin.team.saving',
- defaultMessage: 'Saving Config...'
- },
- restrictDirectMessageAny: {
- id: 'admin.team.restrict_direct_message_any',
- defaultMessage: 'Any user on the Mattermost server'
- },
- restrictDirectMessageTeam: {
- id: 'admin.team.restrict_direct_message_team',
- defaultMessage: 'Any member of the team'
- }
-});
-
-import React from 'react';
-
-const ENABLE_BRAND_ACTION = 'enable_brand_action';
-const DISABLE_BRAND_ACTION = 'disable_brand_action';
-
-class TeamSettings extends React.Component {
- constructor(props) {
- super(props);
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleImageChange = this.handleImageChange.bind(this);
- this.handleImageSubmit = this.handleImageSubmit.bind(this);
-
- this.uploading = false;
- this.timestamp = 0;
-
- this.state = {
- saveNeeded: false,
- brandImageExists: false,
- enableCustomBrand: this.props.config.TeamSettings.EnableCustomBrand,
- restrictDirectMessage: this.props.config.TeamSettings.RestrictDirectMessage,
- serverError: null
- };
- }
-
- componentWillMount() {
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
- $.get(Client.getAdminRoute() + '/get_brand_image').done(() => this.setState({brandImageExists: true}));
- }
- }
-
- componentDidUpdate() {
- if (this.refs.image) {
- const reader = new FileReader();
-
- const img = this.refs.image;
- reader.onload = (e) => {
- $(img).attr('src', e.target.result);
- };
-
- reader.readAsDataURL(this.state.brandImage);
- }
- }
-
- handleChange(action) {
- var s = {saveNeeded: true};
-
- if (action === ENABLE_BRAND_ACTION) {
- s.enableCustomBrand = true;
- }
-
- if (action === DISABLE_BRAND_ACTION) {
- s.enableCustomBrand = false;
- }
-
- this.setState(s);
- }
-
- handleImageChange() {
- const element = $(this.refs.fileInput);
- if (element.prop('files').length > 0) {
- this.setState({fileSelected: true, brandImage: element.prop('files')[0]});
- }
- $('#upload-button').button('reset');
- }
-
- handleSubmit(e) {
- e.preventDefault();
- $('#save-button').button('loading');
-
- var config = this.props.config;
- config.TeamSettings.SiteName = this.refs.SiteName.value.trim();
- config.TeamSettings.RestrictCreationToDomains = this.refs.RestrictCreationToDomains.value.trim();
- config.TeamSettings.EnableTeamCreation = this.refs.EnableTeamCreation.checked;
- config.TeamSettings.EnableUserCreation = this.refs.EnableUserCreation.checked;
- config.TeamSettings.EnableOpenServer = this.refs.EnableOpenServer.checked;
- config.TeamSettings.RestrictTeamNames = this.refs.RestrictTeamNames.checked;
- config.TeamSettings.RestrictDirectMessage = this.refs.RestrictDirectMessage.value.trim();
-
- if (this.refs.EnableCustomBrand) {
- config.TeamSettings.EnableCustomBrand = this.refs.EnableCustomBrand.checked;
- }
-
- if (this.refs.CustomBrandText) {
- config.TeamSettings.CustomBrandText = this.refs.CustomBrandText.value;
- }
-
- var MaxUsersPerTeam = 50;
- if (!isNaN(parseInt(this.refs.MaxUsersPerTeam.value, 10))) {
- MaxUsersPerTeam = parseInt(this.refs.MaxUsersPerTeam.value, 10);
- }
- config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
- this.refs.MaxUsersPerTeam.value = MaxUsersPerTeam;
-
- Client.saveConfig(
- config,
- () => {
- AsyncClient.getConfig();
- this.setState({
- serverError: null,
- saveNeeded: false
- });
- $('#save-button').button('reset');
- },
- (err) => {
- this.setState({
- serverError: err.message,
- saveNeeded: true
- });
- $('#save-button').button('reset');
- }
- );
- }
-
- handleImageSubmit(e) {
- e.preventDefault();
-
- if (!this.state.brandImage) {
- return;
- }
-
- if (this.uploading) {
- return;
- }
-
- $('#upload-button').button('loading');
- this.uploading = true;
-
- Client.uploadBrandImage(this.state.brandImage,
- () => {
- $('#upload-button').button('complete');
- this.timestamp = Utils.getTimestamp();
- this.setState({brandImageExists: true, brandImage: null});
- this.uploading = false;
- },
- (err) => {
- $('#upload-button').button('reset');
- this.uploading = false;
- this.setState({serverImageError: err.message});
- }
- );
- }
-
- createBrandSettings() {
- var btnClass = 'btn';
- if (this.state.fileSelected) {
- btnClass = 'btn btn-primary';
- }
-
- var serverImageError = '';
- if (this.state.serverImageError) {
- serverImageError = <div className='form-group has-error'><label className='control-label'>{this.state.serverImageError}</label></div>;
- }
-
- let uploadImage;
- let uploadText;
- if (this.state.enableCustomBrand) {
- let img;
- if (this.state.brandImage) {
- img = (
- <img
- ref='image'
- className='brand-img'
- src=''
- />
- );
- } else if (this.state.brandImageExists) {
- img = (
- <img
- className='brand-img'
- src={Client.getAdminRoute() + '/get_brand_image?t=' + this.timestamp}
- />
- );
- } else {
- img = (
- <p>
- <FormattedMessage
- id='admin.team.noBrandImage'
- defaultMessage='No brand image uploaded'
- />
- </p>
- );
- }
-
- uploadImage = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='CustomBrandImage'
- >
- <FormattedMessage
- id='admin.team.brandImageTitle'
- defaultMessage='Custom Brand Image:'
- />
- </label>
- <div className='col-sm-8'>
- {img}
- </div>
- <div className='col-sm-4'/>
- <div className='col-sm-8'>
- <div className='file__upload'>
- <button className='btn btn-default'>
- <FormattedMessage
- id='admin.team.chooseImage'
- defaultMessage='Choose New Image'
- />
- </button>
- <input
- ref='fileInput'
- type='file'
- accept='.jpg,.png,.bmp'
- onChange={this.handleImageChange}
- />
- </div>
- <button
- className={btnClass}
- disabled={!this.state.fileSelected}
- onClick={this.handleImageSubmit}
- id='upload-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.team.uploading', 'Uploading..')}
- data-complete-text={'<span class=\'glyphicon glyphicon-ok\'></span> ' + Utils.localizeMessage('admin.team.uploaded', 'Uploaded!')}
- >
- <FormattedMessage
- id='admin.team.upload'
- defaultMessage='Upload'
- />
- </button>
- <br/>
- {serverImageError}
- <p className='help-text no-margin'>
- <FormattedHTMLMessage
- id='admin.team.uploadDesc'
- defaultMessage='Customize your user experience by adding a custom image to your login screen. See examples at <a href="http://docs.mattermost.com/administration/config-settings.html#custom-branding" target="_blank">docs.mattermost.com/administration/config-settings.html#custom-branding</a>.'
- />
- </p>
- </div>
- </div>
- );
-
- uploadText = (
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='CustomBrandText'
- >
- <FormattedMessage
- id='admin.team.brandTextTitle'
- defaultMessage='Custom Brand Text:'
- />
- </label>
- <div className='col-sm-8'>
- <textarea
- type='text'
- rows='5'
- maxLength='1024'
- className='form-control admin-textarea'
- id='CustomBrandText'
- ref='CustomBrandText'
- onChange={this.handleChange}
- >
- {this.props.config.TeamSettings.CustomBrandText}
- </textarea>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.brandTextDescription'
- defaultMessage='The custom branding Markdown-formatted text you would like to appear below your custom brand image on your login sreen.'
- />
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableCustomBrand'
- >
- <FormattedMessage
- id='admin.team.brandTitle'
- defaultMessage='Enable Custom Branding: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCustomBrand'
- value='true'
- ref='EnableCustomBrand'
- defaultChecked={this.props.config.TeamSettings.EnableCustomBrand}
- onChange={this.handleChange.bind(this, ENABLE_BRAND_ACTION)}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableCustomBrand'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableCustomBrand}
- onChange={this.handleChange.bind(this, DISABLE_BRAND_ACTION)}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.brandDesc'
- defaultMessage='Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.'
- />
- </p>
- </div>
- </div>
-
- {uploadImage}
- {uploadText}
- </div>
- );
- }
-
- render() {
- const {formatMessage} = this.props.intl;
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var saveClass = 'btn';
- if (this.state.saveNeeded) {
- saveClass = 'btn btn-primary';
- }
-
- let brand;
- if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.CustomBrand === 'true') {
- brand = this.createBrandSettings();
- }
-
- return (
- <div className='wrapper--fixed'>
-
- <h3>
- <FormattedMessage
- id='admin.team.title'
- defaultMessage='Team Settings'
- />
- </h3>
- <form
- className='form-horizontal'
- role='form'
- >
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='SiteName'
- >
- <FormattedMessage
- id='admin.team.siteNameTitle'
- defaultMessage='Site Name:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='SiteName'
- ref='SiteName'
- placeholder={formatMessage(holders.siteNameExample)}
- defaultValue={this.props.config.TeamSettings.SiteName}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.siteNameDescription'
- defaultMessage='Name of service shown in login screens and UI.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='MaxUsersPerTeam'
- >
- <FormattedMessage
- id='admin.team.maxUsersTitle'
- defaultMessage='Max Users Per Team:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='MaxUsersPerTeam'
- ref='MaxUsersPerTeam'
- placeholder={formatMessage(holders.maxUsersExample)}
- defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.maxUsersDescription'
- defaultMessage='Maximum total number of users per team, including both active and inactive users.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableTeamCreation'
- >
- <FormattedMessage
- id='admin.team.teamCreationTitle'
- defaultMessage='Enable Team Creation: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTeamCreation'
- value='true'
- ref='EnableTeamCreation'
- defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableTeamCreation'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.teamCreationDescription'
- defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableUserCreation'
- >
- <FormattedMessage
- id='admin.team.userCreationTitle'
- defaultMessage='Enable User Creation: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableUserCreation'
- value='true'
- ref='EnableUserCreation'
- defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableUserCreation'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.userCreationDescription'
- defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='EnableOpenServer'
- >
- <FormattedMessage
- id='admin.team.openServerTitle'
- defaultMessage='Enable Open Server: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOpenServer'
- value='true'
- ref='EnableOpenServer'
- defaultChecked={this.props.config.TeamSettings.EnableOpenServer}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='EnableOpenServer'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.EnableOpenServer}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.openServerDescription'
- defaultMessage='When true, anyone can signup for a user account on this server without the need to be invited.'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='RestrictCreationToDomains'
- >
- <FormattedMessage
- id='admin.team.restrictTitle'
- defaultMessage='Restrict Creation To Domains:'
- />
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='RestrictCreationToDomains'
- ref='RestrictCreationToDomains'
- placeholder={formatMessage(holders.restrictExample)}
- defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
- onChange={this.handleChange}
- />
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.restrictDescription'
- defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='RestrictTeamNames'
- >
- <FormattedMessage
- id='admin.team.restrictNameTitle'
- defaultMessage='Restrict Team Names: '
- />
- </label>
- <div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='RestrictTeamNames'
- value='true'
- ref='RestrictTeamNames'
- defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.true'
- defaultMessage='true'
- />
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='RestrictTeamNames'
- value='false'
- defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
- onChange={this.handleChange}
- />
- <FormattedMessage
- id='admin.team.false'
- defaultMessage='false'
- />
- </label>
- <p className='help-text'>
- <FormattedMessage
- id='admin.team.restrictNameDesc'
- defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
- />
- </p>
- </div>
- </div>
-
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='restrictDirectMessage'
- >
- <FormattedMessage
- id='admin.team.restrictDirectMessage'
- defaultMessage='Enable users to open Direct Message channels with:'
- />
- </label>
- <div className='col-sm-8'>
- <select
- className='form-control'
- id='restrictDirectMessage'
- ref='RestrictDirectMessage'
- defaultValue={this.props.config.TeamSettings.RestrictDirectMessage}
- onChange={this.handleChange.bind(this, 'restrictDirectMessage')}
- >
- <option value='any'>{formatMessage(holders.restrictDirectMessageAny)}</option>
- <option value='team'>{formatMessage(holders.restrictDirectMessageTeam)}</option>
- </select>
- <p className='help-text'>
- <FormattedHTMLMessage
- id='admin.team.restrictDirectMessageDesc'
- defaultMessage='"Any user on the Mattermost server" enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. "Any member of the team" limits the ability to open Direct Message channels to only users who are in the same team.'
- />
- </p>
- </div>
- </div>
-
- {brand}
-
- <div className='form-group'>
- <div className='col-sm-12'>
- {serverError}
- <button
- disabled={!this.state.saveNeeded}
- type='submit'
- className={saveClass}
- onClick={this.handleSubmit}
- id='save-button'
- data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
- >
- <FormattedMessage
- id='admin.team.save'
- defaultMessage='Save'
- />
- </button>
- </div>
- </div>
-
- </form>
- </div>
- );
- }
-}
-
-TeamSettings.propTypes = {
- intl: intlShape.isRequired,
- config: React.PropTypes.object
-};
-
-export default injectIntl(TeamSettings);
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 00aa1a832..89fbd0e3a 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -1,7 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import AdminStore from 'stores/admin_store.jsx';
import Client from 'utils/web_client.jsx';
+import FormError from 'components/form_error.jsx';
import LoadingScreen from '../loading_screen.jsx';
import UserItem from './user_item.jsx';
import ResetPasswordModal from './reset_password_modal.jsx';
@@ -11,9 +13,17 @@ import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class UserList extends React.Component {
+ static get propTypes() {
+ return {
+ params: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
+ this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
+
this.getTeamProfiles = this.getTeamProfiles.bind(this);
this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
@@ -22,7 +32,7 @@ export default class UserList extends React.Component {
this.getTeamMemberForUser = this.getTeamMemberForUser.bind(this);
this.state = {
- teamId: props.team.id,
+ team: AdminStore.getTeam(this.props.params.team),
users: null,
teamMembers: null,
serverError: null,
@@ -35,8 +45,14 @@ export default class UserList extends React.Component {
this.getCurrentTeamProfiles();
}
+ onAllTeamsChange() {
+ this.setState({
+ team: AdminStore.getTeam(this.props.params.team)
+ });
+ }
+
getCurrentTeamProfiles() {
- this.getTeamProfiles(this.props.team.id);
+ this.getTeamProfiles(this.props.params.team);
}
getTeamProfiles(teamId) {
@@ -133,9 +149,8 @@ export default class UserList extends React.Component {
}
render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ if (!this.state.team) {
+ return null;
}
if (this.state.users == null || this.state.teamMembers == null) {
@@ -146,11 +161,11 @@ export default class UserList extends React.Component {
id='admin.userList.title'
defaultMessage='Users for {team}'
values={{
- team: this.props.team.name
+ team: this.state.team.name
}}
/>
</h3>
- {serverError}
+ <FormError error={this.state.serverError}/>
<LoadingScreen/>
</div>
);
@@ -161,7 +176,7 @@ export default class UserList extends React.Component {
return (
<UserItem
- team={this.props.team}
+ team={this.state.team}
key={'user_' + user.id}
user={user}
teamMember={teamMember}
@@ -177,12 +192,12 @@ export default class UserList extends React.Component {
id='admin.userList.title2'
defaultMessage='Users for {team} ({count})'
values={{
- team: this.props.team.name,
+ team: this.state.team.name,
count: this.state.users.length
}}
/>
</h3>
- {serverError}
+ <FormError error={this.state.serverError}/>
<form
className='form-horizontal'
role='form'
@@ -194,7 +209,7 @@ export default class UserList extends React.Component {
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
- team={this.props.team}
+ team={this.state.team}
onModalSubmit={this.doPasswordResetSubmit}
onModalDismissed={this.doPasswordResetDismiss}
/>
@@ -202,7 +217,3 @@ export default class UserList extends React.Component {
);
}
}
-
-UserList.propTypes = {
- team: React.PropTypes.object
-};
diff --git a/webapp/components/admin_console/text_setting.jsx b/webapp/components/admin_console/text_setting.jsx
new file mode 100644
index 000000000..bb37f8e29
--- /dev/null
+++ b/webapp/components/admin_console/text_setting.jsx
@@ -0,0 +1,83 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+export default class TextSetting extends React.Component {
+ static get propTypes() {
+ return {
+ id: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ placeholder: React.PropTypes.string,
+ helpText: React.PropTypes.node,
+ value: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.number
+ ]).isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ type: React.PropTypes.oneOf([
+ 'input',
+ 'textarea'
+ ])
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ type: 'input'
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ render() {
+ let input = null;
+ if (this.props.type === 'input') {
+ input = (
+ <input
+ id={this.props.id}
+ className='form-control'
+ type='text'
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ );
+ } else if (this.props.type === 'textarea') {
+ input = (
+ <textarea
+ id={this.props.id}
+ className='form-control'
+ rows='5'
+ maxLength='1024'
+ placeholder={this.props.placeholder}
+ value={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ );
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ helpText={this.props.helpText}
+ inputId={this.props.id}
+ >
+ {input}
+ </Setting>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index ef6bd9f45..affd4b5a4 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -224,8 +224,8 @@ export default class UserItem extends React.Component {
let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
let showMakeActive = false;
let showMakeNotActive = user.roles !== 'system_admin';
- let mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
- let showMfaReset = mfaEnabled && user.mfa_active;
+ const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
+ const showMfaReset = mfaEnabled && user.mfa_active;
if (user.delete_at > 0) {
currentRoles = (
diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx
new file mode 100644
index 000000000..a7f703820
--- /dev/null
+++ b/webapp/components/admin_console/users_and_teams_settings.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2015 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 BooleanSetting from './boolean_setting.jsx';
+import DropdownSetting from './dropdown_setting.jsx';
+import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+import TextSetting from './text_setting.jsx';
+
+const RESTRICT_DIRECT_MESSAGE_ANY = 'any';
+const RESTRICT_DIRECT_MESSAGE_TEAM = 'team';
+
+export default class UsersAndTeamsSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableUserCreation: props.config.TeamSettings.EnableUserCreation,
+ enableTeamCreation: props.config.TeamSettings.EnableTeamCreation,
+ maxUsersPerTeam: props.config.TeamSettings.MaxUsersPerTeam,
+ restrictCreationToDomains: props.config.TeamSettings.RestrictCreationToDomains,
+ restrictTeamNames: props.config.TeamSettings.RestrictTeamNames,
+ restrictDirectMessage: props.config.TeamSettings.RestrictDirectMessage
+ });
+ }
+
+ getConfigFromState(config) {
+ config.TeamSettings.EnableUserCreation = this.state.enableUserCreation;
+ config.TeamSettings.EnableTeamCreation = this.state.enableTeamCreation;
+ config.TeamSettings.MaxUsersPerTeam = this.parseIntNonZero(this.state.maxUsersPerTeam);
+ config.TeamSettings.RestrictCreationToDomains = this.state.restrictCreationToDomains;
+ config.TeamSettings.RestrictTeamNames = this.state.restrictTeamNames;
+ config.TeamSettings.RestrictDirectMessage = this.state.restrictDirectMessage;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.general.title'
+ defaultMessage='General Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.general.usersAndTeams'
+ defaultMessage='Users and Teams'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableUserCreation'
+ label={
+ <FormattedMessage
+ id='admin.team.userCreationTitle'
+ defaultMessage='Enable User Creation: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.userCreationDescription'
+ defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
+ />
+ }
+ value={this.state.enableUserCreation}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableTeamCreation'
+ label={
+ <FormattedMessage
+ id='admin.team.teamCreationTitle'
+ defaultMessage='Enable Team Creation: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.teamCreationDescription'
+ defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
+ />
+ }
+ value={this.state.enableTeamCreation}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='maxUsersPerTeam'
+ label={
+ <FormattedMessage
+ id='admin.team.maxUsersTitle'
+ defaultMessage='Max Users Per Team:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.maxUsersExample', 'Ex "25"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.maxUsersDescription'
+ defaultMessage='Maximum total number of users per team, including both active and inactive users.'
+ />
+ }
+ value={this.state.maxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='restrictCreationToDomains'
+ label={
+ <FormattedMessage
+ id='admin.team.restrictTitle'
+ defaultMessage='Restrict Creation To Domains:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.restrictExample', 'Ex "corp.mattermost.com, mattermost.org"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.restrictDescription'
+ defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
+ />
+ }
+ value={this.state.restrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='restrictTeamNames'
+ label={
+ <FormattedMessage
+ id='admin.team.restrictNameTitle'
+ defaultMessage='Restrict Team Names: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.team.restrictNameDesc'
+ defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
+ />
+ }
+ value={this.state.restrictTeamNames}
+ onChange={this.handleChange}
+ />
+ <DropdownSetting
+ id='restrictDirectMessage'
+ values={[
+ {value: RESTRICT_DIRECT_MESSAGE_ANY, text: Utils.localizeMessage('admin.team.restrict_direct_message_any', 'Any user on the Mattermost server')},
+ {value: RESTRICT_DIRECT_MESSAGE_TEAM, text: Utils.localizeMessage('admin.team.restrict_direct_message_team', 'Any member of the team')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.team.restrictDirectMessage'
+ defaultMessage='Enable users to open Direct Message channels with:'
+ />
+ }
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.team.restrictDirectMessageDesc'
+ defaultMessage='"Any user on the Mattermost server" enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. "Any member of the team" limits the ability to open Direct Message channels to only users who are in the same team.'
+ />
+ }
+ value={this.state.restrictDirectMessage}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/admin_console/webhook_settings.jsx b/webapp/components/admin_console/webhook_settings.jsx
new file mode 100644
index 000000000..1c125cd0f
--- /dev/null
+++ b/webapp/components/admin_console/webhook_settings.jsx
@@ -0,0 +1,166 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import {FormattedMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+export default class WebhookSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+
+ this.renderSettings = this.renderSettings.bind(this);
+
+ this.state = Object.assign(this.state, {
+ enableIncomingWebhooks: props.config.ServiceSettings.EnableIncomingWebhooks,
+ enableOutgoingWebhooks: props.config.ServiceSettings.EnableOutgoingWebhooks,
+ enableCommands: props.config.ServiceSettings.EnableCommands,
+ enableOnlyAdminIntegrations: props.config.ServiceSettings.EnableOnlyAdminIntegrations,
+ enablePostUsernameOverride: props.config.ServiceSettings.EnablePostUsernameOverride,
+ enablePostIconOverride: props.config.ServiceSettings.EnablePostIconOverride
+ });
+ }
+
+ getConfigFromState(config) {
+ config.ServiceSettings.EnableIncomingWebhooks = this.state.enableIncomingWebhooks;
+ config.ServiceSettings.EnableOutgoingWebhooks = this.state.enableOutgoingWebhooks;
+ config.ServiceSettings.EnableCommands = this.state.enableCommands;
+ config.ServiceSettings.EnableOnlyAdminIntegrations = this.state.enableOnlyAdminIntegrations;
+ config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride;
+ config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride;
+
+ return config;
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.integration.title'
+ defaultMessage='Integration Settings'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
+ <FormattedMessage
+ id='admin.integrations.webhook'
+ defaultMessage='Webhooks and Commands'
+ />
+ }
+ >
+ <BooleanSetting
+ id='enableIncomingWebhooks'
+ label={
+ <FormattedMessage
+ id='admin.service.webhooksTitle'
+ defaultMessage='Enable Incoming Webhooks: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.webhooksDescription'
+ defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
+ />
+ }
+ value={this.state.enableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableOutgoingWebhooks'
+ label={
+ <FormattedMessage
+ id='admin.service.outWebhooksTitle'
+ defaultMessage='Enable Outgoing Webhooks: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.outWebhooksDesc'
+ defaultMessage='When true, outgoing webhooks will be allowed.'
+ />
+ }
+ value={this.state.enableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableCommands'
+ label={
+ <FormattedMessage
+ id='admin.service.cmdsTitle'
+ defaultMessage='Enable Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.cmdsDesc'
+ defaultMessage='When true, user created slash commands will be allowed.'
+ />
+ }
+ value={this.state.enableCommands}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enableOnlyAdminIntegrations'
+ label={
+ <FormattedMessage
+ id='admin.service.integrationAdmin'
+ defaultMessage='Enable Integrations for Admin Only: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.integrationAdminDesc'
+ defaultMessage='When true, user created integrations can only be created by admins.'
+ />
+ }
+ value={this.state.enableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enablePostUsernameOverride'
+ label={
+ <FormattedMessage
+ id='admin.service.overrideTitle'
+ defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.overrideDescription'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
+ />
+ }
+ value={this.state.enablePostUsernameOverride}
+ onChange={this.handleChange}
+ />
+ <BooleanSetting
+ id='enablePostIconOverride'
+ label={
+ <FormattedMessage
+ id='admin.service.iconTitle'
+ defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.service.iconDescription'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
+ />
+ }
+ value={this.state.enablePostIconOverride}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/analytics/statistic_count.jsx b/webapp/components/analytics/statistic_count.jsx
index cbb8935dd..89e0cc8df 100644
--- a/webapp/components/analytics/statistic_count.jsx
+++ b/webapp/components/analytics/statistic_count.jsx
@@ -7,7 +7,7 @@ import React from 'react';
export default class StatisticCount extends React.Component {
render() {
- let loading = (
+ const loading = (
<FormattedMessage
id='analytics.chart.loading'
defaultMessage='Loading...'
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
index 77f5efaa6..1625a919e 100644
--- a/webapp/components/analytics/system_analytics.jsx
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -245,8 +245,7 @@ class SystemAnalytics extends React.Component {
}
SystemAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
+ intl: intlShape.isRequired
};
export default injectIntl(SystemAnalytics);
diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx
index 9b4eb1f94..ffca9199a 100644
--- a/webapp/components/analytics/team_analytics.jsx
+++ b/webapp/components/analytics/team_analytics.jsx
@@ -5,6 +5,7 @@ import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
+import AdminStore from 'stores/admin_store.jsx';
import AnalyticsStore from 'stores/analytics_store.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -13,23 +14,34 @@ import Constants from 'utils/constants.jsx';
const StatTypes = Constants.StatTypes;
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
-import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'react-intl';
+import {FormattedMessage, FormattedDate} from 'react-intl';
import React from 'react';
-class TeamAnalytics extends React.Component {
+export default class TeamAnalytics extends React.Component {
+ static get propTypes() {
+ return {
+ params: React.PropTypes.object.isRequired
+ };
+ }
+
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
+ this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
- this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
+ this.state = {
+ team: AdminStore.getTeam(this.props.params.team),
+ stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ };
}
componentDidMount() {
AnalyticsStore.addChangeListener(this.onChange);
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
- this.getData(this.props.team.id);
+ this.getData(this.props.params.team);
}
getData(id) {
@@ -41,11 +53,14 @@ class TeamAnalytics extends React.Component {
componentWillUnmount() {
AnalyticsStore.removeChangeListener(this.onChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
}
componentWillReceiveProps(nextProps) {
- this.getData(nextProps.team.id);
- this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
+ this.getData(nextProps.params.team);
+ this.setState({
+ stats: AnalyticsStore.getAllTeam(nextProps.params.team)
+ });
}
shouldComponentUpdate(nextProps, nextState) {
@@ -53,7 +68,7 @@ class TeamAnalytics extends React.Component {
return true;
}
- if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
+ if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) {
return true;
}
@@ -61,10 +76,22 @@ class TeamAnalytics extends React.Component {
}
onChange() {
- this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
+ this.setState({
+ stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ });
+ }
+
+ onAllTeamsChange() {
+ this.setState({
+ team: AdminStore.getTeam(this.props.params.team)
+ });
}
render() {
+ if (!this.state.team || !this.state.stats) {
+ return null;
+ }
+
const stats = this.state.stats;
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
@@ -78,7 +105,7 @@ class TeamAnalytics extends React.Component {
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
- team: this.props.team.name
+ team: this.state.team.name
}}
/>
</h3>
@@ -175,13 +202,6 @@ class TeamAnalytics extends React.Component {
}
}
-TeamAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object.isRequired
-};
-
-export default injectIntl(TeamAnalytics);
-
export function formatRecentUsersData(data) {
if (data == null) {
return [];
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx
index ba9ac4e79..c817764aa 100644
--- a/webapp/components/backstage/add_command.jsx
+++ b/webapp/components/backstage/add_command.jsx
@@ -4,14 +4,14 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
+import Constants from 'utils/constants.jsx';
const REQUEST_POST = 'P';
const REQUEST_GET = 'G';
@@ -93,6 +93,51 @@ export default class AddCommand extends React.Component {
return;
}
+ if (command.trigger.indexOf('/') === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSlash'
+ defaultMessage='A trigger word cannot begin with a /'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf(' ') !== -1) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSpace'
+ defaultMessage='A trigger word must not contain spaces'
+ />
+ )
+ });
+ return;
+ }
+
+ if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidLength'
+ defaultMessage='A trigger word must contain between {min} and {max} characters'
+ values={{
+ min: Constants.MIN_TRIGGER_LENGTH,
+ max: Constants.MAX_TRIGGER_LENGTH
+ }}
+ />
+ )
+ });
+
+ return;
+ }
+
if (!command.url) {
this.setState({
saving: false,
@@ -324,7 +369,7 @@ export default class AddCommand extends React.Component {
<input
id='trigger'
type='text'
- maxLength='128'
+ maxLength={Constants.MAX_TRIGGER_LENGTH}
className='form-control'
value={this.state.trigger}
onChange={this.updateTrigger}
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx
index 445d370b5..f698f2b13 100644
--- a/webapp/components/backstage/add_incoming_webhook.jsx
+++ b/webapp/components/backstage/add_incoming_webhook.jsx
@@ -4,14 +4,13 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
export default class AddIncomingWebhook extends React.Component {
@@ -173,6 +172,8 @@ export default class AddIncomingWebhook extends React.Component {
id='channelId'
value={this.state.channelId}
onChange={this.updateChannelId}
+ selectOpen={true}
+ selectPrivate={true}
/>
</div>
</div>
diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx
index 245df1604..2fefd5965 100644
--- a/webapp/components/backstage/add_outgoing_webhook.jsx
+++ b/webapp/components/backstage/add_outgoing_webhook.jsx
@@ -4,14 +4,13 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {Link} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
export default class AddOutgoingWebhook extends React.Component {
@@ -225,6 +224,7 @@ export default class AddOutgoingWebhook extends React.Component {
id='channelId'
value={this.state.channelId}
onChange={this.updateChannelId}
+ selectOpen={true}
/>
</div>
</div>
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 992244915..91060f583 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -47,6 +47,7 @@ export default class ChannelHeader extends React.Component {
this.searchMentions = this.searchMentions.bind(this);
this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
+ this.openRecentMentions = this.openRecentMentions.bind(this);
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
@@ -82,6 +83,7 @@ export default class ChannelHeader extends React.Component {
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
$('.sidebar--left .dropdown-menu').perfectScrollbar();
+ document.addEventListener('keydown', this.openRecentMentions);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -89,6 +91,7 @@ export default class ChannelHeader extends React.Component {
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
+ document.removeEventListener('keydown', this.openRecentMentions);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -139,6 +142,12 @@ export default class ChannelHeader extends React.Component {
is_mention_search: true
});
}
+ openRecentMentions(e) {
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) {
+ e.preventDefault();
+ this.searchMentions(e);
+ }
+ }
showRenameChannelModal(e) {
e.preventDefault();
@@ -409,7 +418,8 @@ export default class ChannelHeader extends React.Component {
}
}
- if (!ChannelStore.isDefault(channel)) {
+ const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true;
+ if (!ChannelStore.isDefault(channel) && canLeave) {
dropdownContents.push(
<li
key='leave_channel'
@@ -469,11 +479,11 @@ export default class ChannelHeader extends React.Component {
overlay={popoverContent}
ref='headerOverlay'
>
- <div
- onClick={TextFormatting.handleClick}
- className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
- />
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ />
</OverlayTrigger>
</div>
</th>
diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx
index 112c07ad0..74ec2376b 100644
--- a/webapp/components/channel_notifications_modal.jsx
+++ b/webapp/components/channel_notifications_modal.jsx
@@ -182,7 +182,6 @@ export default class ChannelNotificationsModal extends React.Component {
const handleUpdateSection = function updateSection(e) {
this.updateSection('');
- this.onListenerChange();
e.preventDefault();
}.bind(this);
@@ -289,10 +288,10 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.unreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
- <FormattedMessage
- id='channel_notifications.allUnread'
- defaultMessage='For all unread messages'
- />
+ <FormattedMessage
+ id='channel_notifications.allUnread'
+ defaultMessage='For all unread messages'
+ />
</label>
<br/>
</div>
@@ -312,7 +311,6 @@ export default class ChannelNotificationsModal extends React.Component {
const handleUpdateSection = function handleUpdateSection(e) {
this.updateSection('');
- this.onListenerChange();
e.preventDefault();
}.bind(this);
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx
index 238cfa1ae..59bf2f15a 100644
--- a/webapp/components/channel_select.jsx
+++ b/webapp/components/channel_select.jsx
@@ -6,12 +6,24 @@ import React from 'react';
import Constants from 'utils/constants.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
export default class ChannelSelect extends React.Component {
static get propTypes() {
return {
onChange: React.PropTypes.func,
- value: React.PropTypes.string
+ value: React.PropTypes.string,
+ selectOpen: React.PropTypes.bool.isRequired,
+ selectPrivate: React.PropTypes.bool.isRequired,
+ selectDm: React.PropTypes.bool.isRequired
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ selectOpen: false,
+ selectPrivate: false,
+ selectDm: false
};
}
@@ -19,17 +31,16 @@ export default class ChannelSelect extends React.Component {
super(props);
this.handleChannelChange = this.handleChannelChange.bind(this);
+ this.compareByDisplayName = this.compareByDisplayName.bind(this);
+
+ AsyncClient.getMoreChannels(true);
this.state = {
- channels: []
+ channels: ChannelStore.getAll().sort(this.compareByDisplayName)
};
}
- componentWillMount() {
- this.setState({
- channels: ChannelStore.getAll()
- });
-
+ componentDidMount() {
ChannelStore.addChangeListener(this.handleChannelChange);
}
@@ -39,10 +50,14 @@ export default class ChannelSelect extends React.Component {
handleChannelChange() {
this.setState({
- channels: ChannelStore.getAll()
+ channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).sort(this.compareByDisplayName)
});
}
+ compareByDisplayName(channelA, channelB) {
+ return channelA.display_name.localeCompare(channelB.display_name);
+ }
+
render() {
const options = [
<option
@@ -54,7 +69,25 @@ export default class ChannelSelect extends React.Component {
];
this.state.channels.forEach((channel) => {
- if (channel.type === Constants.OPEN_CHANNEL) {
+ if (channel.type === Constants.OPEN_CHANNEL && this.props.selectOpen) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ } else if (channel.type === Constants.PRIVATE_CHANNEL && this.props.selectPrivate) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ } else if (channel.type === Constants.DM_CHANNEL && this.props.selectDm) {
options.push(
<option
key={channel.id}
diff --git a/webapp/components/claim/claim.jsx b/webapp/components/claim/claim_controller.jsx
index 0197e1677..dbb944bb9 100644
--- a/webapp/components/claim/claim.jsx
+++ b/webapp/components/claim/claim_controller.jsx
@@ -7,7 +7,7 @@ import {Link} from 'react-router';
import logoImage from 'images/logo.png';
-export default class Claim extends React.Component {
+export default class ClaimController extends React.Component {
constructor(props) {
super(props);
@@ -51,9 +51,9 @@ export default class Claim extends React.Component {
}
}
-Claim.defaultProps = {
+ClaimController.defaultProps = {
};
-Claim.propTypes = {
+ClaimController.propTypes = {
location: React.PropTypes.object.isRequired,
children: React.PropTypes.node
};
diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx
index fbf26cade..f7bb02c6e 100644
--- a/webapp/components/claim/components/email_to_ldap.jsx
+++ b/webapp/components/claim/components/email_to_ldap.jsx
@@ -20,7 +20,7 @@ export default class EmailToLDAP extends React.Component {
e.preventDefault();
var state = {};
- const password = ReactDOM.findDOMNode(this.refs.emailpassword).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.emailpassword).value;
if (!password) {
state.error = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.');
this.setState(state);
@@ -34,7 +34,7 @@ export default class EmailToLDAP extends React.Component {
return;
}
- const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim();
+ const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value;
if (!ldapPassword) {
state.error = Utils.localizeMessage('claim.email_to_ldap.ldapPasswordError', 'Please enter your LDAP password.');
this.setState(state);
diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx
index 1fd284bed..171ebe8a4 100644
--- a/webapp/components/claim/components/email_to_oauth.jsx
+++ b/webapp/components/claim/components/email_to_oauth.jsx
@@ -20,7 +20,7 @@ export default class EmailToOAuth extends React.Component {
e.preventDefault();
var state = {};
- var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ var password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.email_to_oauth.pwdError', 'Please enter your password.');
this.setState(state);
diff --git a/webapp/components/claim/components/ldap_to_email.jsx b/webapp/components/claim/components/ldap_to_email.jsx
index a10cefd6f..fbc8bcebf 100644
--- a/webapp/components/claim/components/ldap_to_email.jsx
+++ b/webapp/components/claim/components/ldap_to_email.jsx
@@ -2,7 +2,8 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
+
+import {switchFromLdapToEmail} from 'actions/user_actions.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -16,25 +17,26 @@ export default class LDAPToEmail extends React.Component {
this.state = {};
}
+
submit(e) {
e.preventDefault();
var state = {};
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.ldap_to_email.pwdError', 'Please enter your password.');
this.setState(state);
return;
}
- const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
+ const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value;
if (!confirmPassword || password !== confirmPassword) {
state.error = Utils.localizeMessage('claim.ldap_to_email.pwdNotMatch', 'Passwords do not match.');
this.setState(state);
return;
}
- const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value.trim();
+ const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value;
if (!ldapPassword) {
state.error = Utils.localizeMessage('claim.ldap_to_email.ldapPasswordError', 'Please enter your LDAP password.');
this.setState(state);
@@ -44,20 +46,15 @@ export default class LDAPToEmail extends React.Component {
state.error = null;
this.setState(state);
- Client.ldapToEmail(
+ switchFromLdapToEmail(
this.props.email,
password,
ldapPassword,
- (data) => {
- if (data.follow_link) {
- window.location.href = data.follow_link;
- }
- },
- (error) => {
- this.setState({error});
- }
+ null,
+ (err) => this.setState({error: err.message})
);
}
+
render() {
var error = null;
if (this.state.error) {
diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx
index 7fd18aaa6..1a3b962a2 100644
--- a/webapp/components/claim/components/oauth_to_email.jsx
+++ b/webapp/components/claim/components/oauth_to_email.jsx
@@ -21,14 +21,14 @@ export default class OAuthToEmail extends React.Component {
e.preventDefault();
const state = {};
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password) {
state.error = Utils.localizeMessage('claim.oauth_to_email.enterPwd', 'Please enter a password.');
this.setState(state);
return;
}
- const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
+ const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value;
if (!confirmPassword || password !== confirmPassword) {
state.error = Utils.localizeMessage('claim.oauth_to_email.pwdNotMatch', 'Password do not match.');
this.setState(state);
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 30e89e500..616257f37 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -16,7 +16,7 @@ import MsgTyping from './msg_typing.jsx';
import FileUpload from './file_upload.jsx';
import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -72,6 +72,7 @@ class CreateComment extends React.Component {
const draft = PostStore.getCommentDraft(this.props.rootId);
this.state = {
messageText: draft.message,
+ lastMessage: '',
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
@@ -111,7 +112,7 @@ class CreateComment extends React.Component {
return;
}
- let post = {};
+ const post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -144,7 +145,7 @@ class CreateComment extends React.Component {
AsyncClient.getPosts(this.props.channelId);
const channel = ChannelStore.get(this.props.channelId);
- let member = ChannelStore.getMember(this.props.channelId);
+ const member = ChannelStore.getMember(this.props.channelId);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
@@ -172,6 +173,7 @@ class CreateComment extends React.Component {
this.setState({
messageText: '',
+ lastMessage: this.state.messageText,
submitting: false,
postError: null,
previews: [],
@@ -190,7 +192,7 @@ class CreateComment extends React.Component {
GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
handleUserInput(messageText) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.message = messageText;
PostStore.storeCommentDraft(this.props.rootId, draft);
@@ -203,7 +205,7 @@ class CreateComment extends React.Component {
return;
}
- if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId);
@@ -221,12 +223,25 @@ class CreateComment extends React.Component {
comments: PostStore.getCommentCount(lastPost)
});
}
+
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP) {
+ e.preventDefault();
+ const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId);
+ if (!lastPost) {
+ return;
+ }
+ let message = lastPost.message;
+ if (this.state.lastMessage !== '') {
+ message = this.state.lastMessage;
+ }
+ this.setState({messageText: message});
+ }
}
handleUploadClick() {
this.focusTextbox();
}
handleUploadStart(clientIds) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeCommentDraft(this.props.rootId, draft);
@@ -238,7 +253,7 @@ class CreateComment extends React.Component {
this.focusTextbox();
}
handleFileUploadComplete(filenames, clientIds) {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -258,7 +273,7 @@ class CreateComment extends React.Component {
if (clientId === -1) {
this.setState({serverError: err});
} else {
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
@@ -271,8 +286,8 @@ class CreateComment extends React.Component {
}
}
removePreview(id) {
- let previews = this.state.previews;
- let uploadsInProgress = this.state.uploadsInProgress;
+ const previews = this.state.previews;
+ const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
@@ -287,7 +302,7 @@ class CreateComment extends React.Component {
previews.splice(index, 1);
}
- let draft = PostStore.getCommentDraft(this.props.rootId);
+ const draft = PostStore.getCommentDraft(this.props.rootId);
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCommentDraft(this.props.rootId, draft);
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index d173fe42b..75c75f09d 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -10,7 +10,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -77,6 +77,7 @@ class CreatePost extends React.Component {
this.state = {
channelId: ChannelStore.getCurrentId(),
messageText: draft.messageText,
+ lastMessage: '',
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
@@ -126,7 +127,7 @@ class CreatePost extends React.Component {
}
this.setState({submitting: true, serverError: null});
-
+ this.setState({lastMessage: this.state.messageText});
if (post.message.indexOf('/') === 0) {
Client.executeCommand(
this.state.channelId,
@@ -350,7 +351,7 @@ class CreatePost extends React.Component {
return;
}
- if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const channelId = ChannelStore.getCurrentId();
@@ -371,6 +372,20 @@ class CreatePost extends React.Component {
comments: PostStore.getCommentCount(lastPost)
});
}
+
+ if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP) {
+ e.preventDefault();
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
+ if (!lastPost) {
+ return;
+ }
+ let message = lastPost.message;
+ if (this.state.lastMessage !== '') {
+ message = this.state.lastMessage;
+ }
+ this.setState({messageText: message});
+ }
}
showPostDeletedModal() {
this.setState({
diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx
index e8f1717bb..e6dcd221a 100644
--- a/webapp/components/create_team/components/display_name.jsx
+++ b/webapp/components/create_team/components/display_name.jsx
@@ -1,29 +1,19 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
-import * as utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
-import {Link} from 'react-router';
+import {track} from 'actions/analytics_actions.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
+import Constants from 'utils/constants.jsx';
import logoImage from 'images/logo.png';
-const holders = defineMessages({
- required: {
- id: 'create_team.display_name.required',
- defaultMessage: 'This field is required'
- },
- charLength: {
- id: 'create_team.display_name.charLength',
- defaultMessage: 'Name must be 4 or more characters up to a maximum of 15'
- }
-});
-
import React from 'react';
+import ReactDOM from 'react-dom';
+import {Link} from 'react-router';
+import {FormattedMessage} from 'react-intl';
-class TeamSignupDisplayNamePage extends React.Component {
+export default class TeamSignupDisplayNamePage extends React.Component {
constructor(props) {
super(props);
@@ -35,19 +25,18 @@ class TeamSignupDisplayNamePage extends React.Component {
submitNext(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!displayName) {
- this.setState({nameError: formatMessage(holders.required)});
+ this.setState({nameError: Utils.localizeMessage('create_team.display_name.required', 'This field is required')});
return;
- } else if (displayName.length < 4 || displayName.length > 15) {
- this.setState({nameError: formatMessage(holders.charLength)});
+ } else if (displayName.length < Constants.MIN_TEAMNAME_LENGTH || displayName.length > Constants.MAX_TEAMNAME_LENGTH) {
+ this.setState({nameError: Utils.localizeMessage('create_team.display_name.charLength', 'Name must be 4 or more characters up to a maximum of 15')});
return;
}
this.props.state.wizard = 'team_url';
this.props.state.team.display_name = displayName;
- this.props.state.team.name = utils.cleanUpUrlable(displayName);
+ this.props.state.team.name = Utils.cleanUpUrlable(displayName);
this.props.updateParent(this.props.state);
}
@@ -57,7 +46,7 @@ class TeamSignupDisplayNamePage extends React.Component {
}
render() {
- Client.track('signup', 'signup_team_02_name');
+ track('signup', 'signup_team_02_name');
var nameError = null;
var nameDivClass = 'form-group';
@@ -128,9 +117,6 @@ class TeamSignupDisplayNamePage extends React.Component {
}
TeamSignupDisplayNamePage.propTypes = {
- intl: intlShape.isRequired,
state: React.PropTypes.object,
updateParent: React.PropTypes.func
};
-
-export default injectIntl(TeamSignupDisplayNamePage);
diff --git a/webapp/components/create_team/components/team_url.jsx b/webapp/components/create_team/components/team_url.jsx
index 34e696938..b6c634816 100644
--- a/webapp/components/create_team/components/team_url.jsx
+++ b/webapp/components/create_team/components/team_url.jsx
@@ -2,45 +2,20 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
+
import * as Utils from 'utils/utils.jsx';
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-import Constants from 'utils/constants.jsx';
-import {browserHistory} from 'react-router';
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {checkIfTeamExists, createTeam} from 'actions/team_actions.jsx';
+import {track} from 'actions/analytics_actions.jsx';
+import Constants from 'utils/constants.jsx';
import logoImage from 'images/logo.png';
-const holders = defineMessages({
- required: {
- id: 'create_team.team_url.required',
- defaultMessage: 'This field is required'
- },
- regex: {
- id: 'create_team.team_url.regex',
- defaultMessage: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."
- },
- charLength: {
- id: 'create_team.team_url.charLength',
- defaultMessage: 'Name must be 4 or more characters up to a maximum of 15'
- },
- taken: {
- id: 'create_team.team_url.taken',
- defaultMessage: 'URL is taken or contains a reserved word'
- },
- unavailable: {
- id: 'create_team.team_url.unavailable',
- defaultMessage: 'This URL is unavailable. Please try another.'
- }
-});
-
import React from 'react';
+import ReactDOM from 'react-dom';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-class TeamUrl extends React.Component {
+export default class TeamUrl extends React.Component {
constructor(props) {
super(props);
@@ -58,10 +33,9 @@ class TeamUrl extends React.Component {
submitNext(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
const name = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!name) {
- this.setState({nameError: formatMessage(holders.required)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.required', 'This field is required')});
return;
}
@@ -69,17 +43,17 @@ class TeamUrl extends React.Component {
const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
if (cleanedName !== name || !urlRegex.test(name)) {
- this.setState({nameError: formatMessage(holders.regex)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.regex', "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.")});
return;
- } else if (cleanedName.length < 4 || cleanedName.length > 15) {
- this.setState({nameError: formatMessage(holders.charLength)});
+ } else if (cleanedName.length < Constants.MIN_TEAMNAME_LENGTH || cleanedName.length > Constants.MAX_TEAMNAME_LENGTH) {
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.charLength', 'Name must be 4 or more characters up to a maximum of 15')});
return;
}
if (global.window.mm_config.RestrictTeamNames === 'true') {
for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: formatMessage(holders.taken)});
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.taken', 'URL is taken or contains a reserved word')});
return;
}
}
@@ -90,30 +64,24 @@ class TeamUrl extends React.Component {
teamSignup.team.type = 'O';
teamSignup.team.name = name;
- Client.findTeamByName(name,
- (findTeam) => {
- if (findTeam) {
- this.setState({nameError: formatMessage(holders.unavailable)});
- $('#finish-button').button('reset');
- } else {
- Client.createTeam(teamSignup.team,
- (team) => {
- Client.track('signup', 'signup_team_08_complete');
- $('#sign-up-button').button('reset');
- AsyncClient.getDirectProfiles();
- TeamStore.saveTeam(team);
- TeamStore.appendTeamMember({team_id: team.id, user_id: UserStore.getCurrentId(), roles: 'admin'});
- TeamStore.emitChange();
- browserHistory.push('/' + team.name + '/channels/town-square');
- },
- (err) => {
- this.setState({nameError: err.message});
- $('#finish-button').button('reset');
- }
- );
-
+ checkIfTeamExists(name,
+ (foundTeam) => {
+ if (foundTeam) {
+ this.setState({nameError: Utils.localizeMessage('create_team.team_url.unavailable', 'This URL is unavailable. Please try another.')});
$('#finish-button').button('reset');
+ return;
}
+
+ createTeam(teamSignup.team,
+ () => {
+ track('signup', 'signup_team_08_complete');
+ $('#sign-up-button').button('reset');
+ },
+ (err) => {
+ this.setState({nameError: err.message});
+ $('#finish-button').button('reset');
+ }
+ );
},
(err) => {
this.setState({nameError: err.message});
@@ -121,15 +89,17 @@ class TeamUrl extends React.Component {
}
);
}
+
handleFocus(e) {
e.preventDefault();
e.currentTarget.select();
}
+
render() {
$('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
- Client.track('signup', 'signup_team_03_url');
+ track('signup', 'signup_team_03_url');
let nameError = null;
let nameDivClass = 'form-group';
@@ -223,9 +193,6 @@ class TeamUrl extends React.Component {
}
TeamUrl.propTypes = {
- intl: intlShape.isRequired,
state: React.PropTypes.object,
updateParent: React.PropTypes.func
};
-
-export default injectIntl(TeamUrl);
diff --git a/webapp/components/create_team/create_team.jsx b/webapp/components/create_team/create_team_controller.jsx
index 8a119a122..ad2a008bd 100644
--- a/webapp/components/create_team/create_team.jsx
+++ b/webapp/components/create_team/create_team_controller.jsx
@@ -8,7 +8,7 @@ import {browserHistory, Link} from 'react-router';
import React from 'react';
-export default class CreateTeam extends React.Component {
+export default class CreateTeamController extends React.Component {
constructor(props) {
super(props);
@@ -67,6 +67,6 @@ export default class CreateTeam extends React.Component {
}
}
-CreateTeam.propTypes = {
+CreateTeamController.propTypes = {
children: React.PropTypes.node
};
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index bc67a34f9..92b16f925 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -5,7 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Textbox from './textbox.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PostStore from 'stores/post_store.jsx';
diff --git a/webapp/components/error_bar.jsx b/webapp/components/error_bar.jsx
index d28be671d..c13d0dd6a 100644
--- a/webapp/components/error_bar.jsx
+++ b/webapp/components/error_bar.jsx
@@ -58,7 +58,12 @@ export default class ErrorBar extends React.Component {
e.preventDefault();
}
- ErrorStore.clearLastError();
+ if (ErrorStore.getLastError() && ErrorStore.getLastError().email_preview) {
+ ErrorStore.clearPreviewError();
+ } else {
+ ErrorStore.clearLastError();
+ }
+
this.setState({message: null});
}
@@ -81,7 +86,7 @@ export default class ErrorBar extends React.Component {
className='error-bar__close'
onClick={this.handleClose}
>
- &times;
+ {'×'}
</a>
</div>
);
diff --git a/webapp/components/file_attachment.jsx b/webapp/components/file_attachment.jsx
index 4a040a35b..d5bbae4c9 100644
--- a/webapp/components/file_attachment.jsx
+++ b/webapp/components/file_attachment.jsx
@@ -8,6 +8,7 @@ import Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages} from 'react-intl';
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
const holders = defineMessages({
download: {
@@ -24,6 +25,7 @@ class FileAttachment extends React.Component {
this.loadFiles = this.loadFiles.bind(this);
this.addBackgroundImage = this.addBackgroundImage.bind(this);
+ this.onAttachmentClick = this.onAttachmentClick.bind(this);
this.canSetState = false;
this.state = {fileSize: -1};
@@ -126,6 +128,10 @@ class FileAttachment extends React.Component {
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
}
}
+ onAttachmentClick(e) {
+ e.preventDefault();
+ this.props.handleImageClick(this.props.index);
+ }
render() {
var filename = this.props.filename;
@@ -149,12 +155,12 @@ class FileAttachment extends React.Component {
if (this.state.fileSize < 0) {
Client.getFileInfo(
filename,
- function success(data) {
+ (data) => {
if (this.canSetState) {
this.setState({fileSize: parseInt(data.size, 10)});
}
- }.bind(this),
- function error() {
+ },
+ () => {
// Do nothing
}
);
@@ -169,35 +175,64 @@ class FileAttachment extends React.Component {
} else {
trimmedFilename = filenameString;
}
+ var filenameOverlay = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={<Tooltip id='file-name__tooltip'>{this.props.intl.formatMessage(holders.download) + ' "' + filenameString + '"'}</Tooltip>}
+ >
+ <a
+ href={fileUrl}
+ download={filenameString}
+ className='post-image__name'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ {trimmedFilename}
+ </a>
+ </OverlayTrigger>
+ );
+
+ if (this.props.compactDisplay) {
+ filenameOverlay = (
+ <OverlayTrigger
+ delayShow={1000}
+ placement='top'
+ overlay={<Tooltip id='file-name__tooltip'>{filenameString}</Tooltip>}
+ >
+ <a
+ href='#'
+ onClick={this.onAttachmentClick}
+ className='post-image__name'
+ rel='noopener noreferrer'
+ >
+ <i className='glyphicon glyphicon-paperclip'/>{trimmedFilename}
+ </a>
+ </OverlayTrigger>
+ );
+ }
return (
<div
className='post-image__column'
key={filename}
>
- <a className='post-image__thumbnail'
+ <a
+ className='post-image__thumbnail'
href='#'
- onClick={() => this.props.handleImageClick(this.props.index)}
+ onClick={this.onAttachmentClick}
>
{thumbnail}
</a>
<div className='post-image__details'>
- <a
- href={fileUrl}
- download={filenameString}
- data-toggle='tooltip'
- title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
- className='post-image__name'
- target='_blank'
- >
- {trimmedFilename}
- </a>
+ {filenameOverlay}
<div>
<a
href={fileUrl}
download={filenameString}
className='post-image__download'
target='_blank'
+ rel='noopener noreferrer'
>
<span
className='fa fa-download'
@@ -222,7 +257,9 @@ FileAttachment.propTypes = {
index: React.PropTypes.number.isRequired,
// handler for when the thumbnail is clicked passed the index above
- handleImageClick: React.PropTypes.func
+ handleImageClick: React.PropTypes.func,
+
+ compactDisplay: React.PropTypes.bool
};
export default injectIntl(FileAttachment);
diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list.jsx
index 59fd56bc3..e4b841769 100644
--- a/webapp/components/file_attachment_list.jsx
+++ b/webapp/components/file_attachment_list.jsx
@@ -29,6 +29,7 @@ export default class FileAttachmentList extends React.Component {
filename={filenames[i]}
index={i}
handleImageClick={this.handleImageClick}
+ compactDisplay={this.props.compactDisplay}
/>
);
}
@@ -60,5 +61,7 @@ FileAttachmentList.propTypes = {
channelId: React.PropTypes.string,
// the user that owns the post that this is attached to
- userId: React.PropTypes.string
+ userId: React.PropTypes.string,
+
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/file_info_preview.jsx b/webapp/components/file_info_preview.jsx
index d5dcd75eb..fe4e76f91 100644
--- a/webapp/components/file_info_preview.jsx
+++ b/webapp/components/file_info_preview.jsx
@@ -38,6 +38,7 @@ export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMess
className={'file-details__preview'}
to={fileUrl}
target='_blank'
+ rel='noopener noreferrer'
>
<span className='file-details__preview-helper'/>
<img src={Utils.getPreviewImagePath(filename)}/>
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 05f1701a8..829e580b9 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -284,7 +284,10 @@ class FileUpload extends React.Component {
keyUpload(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.U) {
- $(this.refs.input).focus().trigger('click');
+ e.preventDefault();
+ if (this.props.postType === 'post' && document.activeElement.id === 'post_textbox' || this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox') {
+ $(this.refs.fileInput).focus().trigger('click');
+ }
}
}
diff --git a/webapp/components/header_footer_template.jsx b/webapp/components/header_footer_template.jsx
index 71716c4fb..8267c73c5 100644
--- a/webapp/components/header_footer_template.jsx
+++ b/webapp/components/header_footer_template.jsx
@@ -28,13 +28,13 @@ export default class NotLoggedIn extends React.Component {
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
</div>
<div className='col-xs-12'>
- <span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
+ <span className='pull-right footer-link copyright'>{'© 2015-2016 Mattermost, Inc.'}</span>
<a
id='help_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.help'/>
</a>
@@ -42,8 +42,8 @@ export default class NotLoggedIn extends React.Component {
id='terms_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.TermsOfServiceLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.terms'/>
</a>
@@ -51,8 +51,8 @@ export default class NotLoggedIn extends React.Component {
id='privacy_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.PrivacyPolicyLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.privacy'/>
</a>
@@ -60,8 +60,8 @@ export default class NotLoggedIn extends React.Component {
id='about_link'
className='pull-right footer-link'
target='_blank'
+ rel='noopener noreferrer'
href={global.window.mm_config.AboutLink}
- rel='noreferrer'
>
<FormattedMessage id='web.footer.about'/>
</a>
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index 4ac620f08..96a9eb75d 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -6,7 +6,7 @@ import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import Client from 'utils/web_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
@@ -258,15 +258,17 @@ class InviteMemberModal extends React.Component {
var removeButton = null;
if (index) {
- removeButton = (<div>
- <button
- type='button'
- className='btn btn-link remove__member'
- onClick={this.removeInviteFields.bind(this, index)}
- >
- <span className='fa fa-trash'></span>
- </button>
- </div>);
+ removeButton = (
+ <div>
+ <button
+ type='button'
+ className='btn btn-link remove__member'
+ onClick={this.removeInviteFields.bind(this, index)}
+ >
+ <span className='fa fa-trash'></span>
+ </button>
+ </div>
+ );
}
var emailClass = 'form-group invite';
if (emailError) {
@@ -283,54 +285,56 @@ class InviteMemberModal extends React.Component {
if (lastNameError) {
lastNameClass += ' has-error';
}
- nameFields = (<div className='row--invite'>
- <div className='col-sm-6'>
- <div className={firstNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'first_name' + index}
- placeholder={formatMessage(holders.firstname)}
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {firstNameError}
- </div>
- </div>
- <div className='col-sm-6'>
- <div className={lastNameClass}>
- <input
- type='text'
- className='form-control'
- ref={'last_name' + index}
- placeholder={formatMessage(holders.lastname)}
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {lastNameError}
- </div>
- </div>
- </div>);
+ nameFields = (
+ <div className='row--invite'>
+ <div className='col-sm-6'>
+ <div className={firstNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'first_name' + index}
+ placeholder={formatMessage(holders.firstname)}
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {firstNameError}
+ </div>
+ </div>
+ <div className='col-sm-6'>
+ <div className={lastNameClass}>
+ <input
+ type='text'
+ className='form-control'
+ ref={'last_name' + index}
+ placeholder={formatMessage(holders.lastname)}
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {lastNameError}
+ </div>
+ </div>
+ </div>
+ );
inviteSections[index] = (
<div key={'key' + index}>
- {removeButton}
- <div className={emailClass}>
- <input
- onKeyUp={this.displayNameKeyUp}
- type='text'
- ref={'email' + index}
- className='form-control'
- placeholder='email@domain.com'
- maxLength='64'
- disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
- spellCheck='false'
- />
- {emailError}
- </div>
- {nameFields}
+ {removeButton}
+ <div className={emailClass}>
+ <input
+ onKeyUp={this.displayNameKeyUp}
+ type='text'
+ ref={'email' + index}
+ className='form-control'
+ placeholder='email@domain.com'
+ maxLength='64'
+ disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
+ spellCheck='false'
+ />
+ {emailError}
+ </div>
+ {nameFields}
</div>
);
}
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 9f1fac6bc..0c37d62cb 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -8,7 +8,7 @@ import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as Websockets from 'action_creators/websocket_actions.jsx';
+import * as Websockets from 'actions/websocket_actions.jsx';
import Constants from 'utils/constants.jsx';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/login/login.jsx b/webapp/components/login/login_controller.jsx
index e0969001a..1b1f65436 100644
--- a/webapp/components/login/login.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -5,10 +5,11 @@ import LoginMfa from './components/login_mfa.jsx';
import ErrorBar from 'components/error_bar.jsx';
import FormError from 'components/form_error.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import Client from 'utils/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -20,7 +21,7 @@ import {browserHistory, Link} from 'react-router';
import React from 'react';
import logoImage from 'images/logo.png';
-export default class Login extends React.Component {
+export default class LoginController extends React.Component {
constructor(props) {
super(props);
@@ -44,13 +45,15 @@ export default class Login extends React.Component {
if (UserStore.getCurrentUser()) {
browserHistory.push('/select_team');
}
+
+ AsyncClient.checkVersion();
}
preSubmit(e) {
e.preventDefault();
const loginId = this.state.loginId.trim();
- const password = this.state.password.trim();
+ const password = this.state.password;
if (global.window.mm_config.EnableMultifactorAuthentication === 'true') {
Client.checkMfa(
@@ -257,6 +260,7 @@ export default class Login extends React.Component {
onChange={this.handleLoginIdChange}
placeholder={this.createLoginPlaceholder(emailSigninEnabled, usernameSigninEnabled, ldapEnabled)}
spellCheck='false'
+ autoCapitalize='off'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -289,7 +293,10 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableOpenServer === 'true') {
loginControls.push(
- <div key='signup'>
+ <div
+ className='form-group'
+ key='signup'
+ >
<span>
<FormattedMessage
id='login.noAccount'
@@ -440,8 +447,8 @@ export default class Login extends React.Component {
}
}
-Login.defaultProps = {
+LoginController.defaultProps = {
};
-Login.propTypes = {
+LoginController.propTypes = {
params: React.PropTypes.object.isRequired
};
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index 087db68e6..34cad32d2 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -9,7 +9,7 @@ import ChannelStore from 'stores/channel_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index 2895047b5..de61bcf98 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -6,7 +6,7 @@ import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 21ca53649..d4968986e 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -17,6 +17,7 @@ import ToggleModalButton from './toggle_modal_button.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -34,6 +35,8 @@ import {Link, browserHistory} from 'react-router';
import React from 'react';
+import * as GlobalActions from 'actions/global_actions.jsx';
+
export default class Navbar extends React.Component {
constructor(props) {
super(props);
@@ -50,6 +53,12 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
+ this.navigateChannelShortcut = this.navigateChannelShortcut.bind(this);
+ this.navigateUnreadChannelShortcut = this.navigateUnreadChannelShortcut.bind(this);
+ this.getDisplayedChannels = this.getDisplayedChannels.bind(this);
+ this.compareByName = this.compareByName.bind(this);
+ this.compareByDisplayName = this.compareByDisplayName.bind(this);
+
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false;
@@ -72,10 +81,14 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars);
+ document.addEventListener('keydown', this.navigateChannelShortcut);
+ document.addEventListener('keydown', this.navigateUnreadChannelShortcut);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeExtraInfoChangeListener(this.onChange);
+ document.removeEventListener('keydown', this.navigateChannelShortcut);
+ document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
}
handleSubmit(e) {
e.preventDefault();
@@ -150,6 +163,96 @@ export default class Navbar extends React.Component {
showRenameChannelModal: false
});
}
+ navigateChannelShortcut(e) {
+ if (e.altKey && !e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
+ e.preventDefault();
+ const allChannels = this.getDisplayedChannels();
+ const curChannel = this.state.channel;
+ let curIndex = -1;
+ for (let i = 0; i < allChannels.length; i++) {
+ if (allChannels[i].id === curChannel.id) {
+ curIndex = i;
+ }
+ }
+ let nextChannel = curChannel;
+ 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)];
+ GlobalActions.emitChannelClickEvent(nextChannel);
+ }
+ }
+ navigateUnreadChannelShortcut(e) {
+ if (e.altKey && e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
+ e.preventDefault();
+ const allChannels = this.getDisplayedChannels();
+ const curChannel = this.state.channel;
+ let curIndex = -1;
+ for (let i = 0; i < allChannels.length; i++) {
+ if (allChannels[i].id === curChannel.id) {
+ curIndex = i;
+ }
+ }
+ let nextChannel = curChannel;
+ let nextIndex = curIndex;
+ let count = 0;
+ let increment = 0;
+ if (e.keyCode === Constants.KeyCodes.UP) {
+ increment = -1;
+ } else if (e.keyCode === Constants.KeyCodes.DOWN) {
+ increment = 1;
+ }
+ let unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
+ while (count < allChannels.length && unreadCounts.msgs === 0 && unreadCounts.mentions === 0) {
+ nextIndex += increment;
+ count++;
+ nextIndex = Utils.mod(nextIndex, allChannels.length);
+ unreadCounts = ChannelStore.getUnreadCount(allChannels[nextIndex].id);
+ }
+ if (unreadCounts.msgs !== 0 || unreadCounts.mentions !== 0) {
+ nextChannel = allChannels[nextIndex];
+ GlobalActions.emitChannelClickEvent(nextChannel);
+ }
+ }
+ }
+ getDisplayedChannels() {
+ const allChannels = ChannelStore.getChannels().sort(this.compareByName);
+ const publicChannels = allChannels.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
+ const privateChannels = allChannels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
+
+ const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
+
+ const directChannels = [];
+ const directNonTeamChannels = [];
+ for (const [name, value] of preferences) {
+ if (value !== 'true') {
+ continue;
+ }
+
+ const directChannel = allChannels.find(Utils.isDirectChannelForUser.bind(null, name));
+ directChannel.display_name = Utils.displayUsername(name);
+
+ if (UserStore.hasTeamProfile(name)) {
+ directChannels.push(directChannel);
+ } else {
+ directNonTeamChannels.push(directChannel);
+ }
+ }
+
+ directChannels.sort(this.compareByDisplayName);
+ directNonTeamChannels.sort(this.compareByDisplayName);
+
+ return publicChannels.concat(privateChannels).concat(directChannels).concat(directNonTeamChannels);
+ }
+ compareByName(a, b) {
+ return a.name.localeCompare(b.name);
+ }
+ compareByDisplayName(a, b) {
+ return a.display_name.localeCompare(b.display_name);
+ }
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) {
var viewInfoOption = (
@@ -218,20 +321,23 @@ export default class Navbar extends React.Component {
</li>
);
- leaveChannelOption = (
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.handleLeave}
- >
- <FormattedMessage
- id='navbar.leave'
- defaultMessage='Leave Channel'
- />
- </a>
- </li>
- );
+ const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true;
+ if (canLeave) {
+ leaveChannelOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleLeave}
+ >
+ <FormattedMessage
+ id='navbar.leave'
+ defaultMessage='Leave Channel'
+ />
+ </a>
+ </li>
+ );
+ }
}
var manageMembersOption;
diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx
index 7f1cfce7c..87df3848d 100644
--- a/webapp/components/navbar_dropdown.jsx
+++ b/webapp/components/navbar_dropdown.jsx
@@ -4,7 +4,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -28,6 +28,7 @@ export default class NavbarDropdown extends React.Component {
this.handleAboutModal = this.handleAboutModal.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
+ this.openAccountSettings = this.openAccountSettings.bind(this);
this.state = {
showUserSettingsModal: false,
@@ -53,6 +54,7 @@ export default class NavbarDropdown extends React.Component {
});
TeamStore.addChangeListener(this.onTeamChange);
+ document.addEventListener('keydown', this.openAccountSettings);
}
onTeamChange() {
@@ -65,14 +67,19 @@ export default class NavbarDropdown extends React.Component {
componentWillUnmount() {
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
TeamStore.removeChangeListener(this.onTeamChange);
+ document.removeEventListener('keydown', this.openAccountSettings);
+ }
+ openAccountSettings(e) {
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.A) {
+ e.preventDefault();
+ this.setState({showUserSettingsModal: true});
+ }
}
-
render() {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
- var adminDivider = '';
var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
@@ -126,8 +133,6 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- adminDivider = (<li className='divider'></li>);
-
teamSettings = (
<li>
<a
@@ -213,6 +218,10 @@ export default class NavbarDropdown extends React.Component {
<Link
to={'/' + team.name + '/channels/town-square'}
>
+ <FormattedMessage
+ id='navbar_dropdown.switchTo'
+ defaultMessage='Switch to '
+ />
{team.display_name}
</Link>
</li>
@@ -228,8 +237,8 @@ export default class NavbarDropdown extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<FormattedMessage
id='navbar_dropdown.help'
@@ -246,8 +255,8 @@ export default class NavbarDropdown extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.ReportAProblemLink}
- rel='noreferrer'
>
<FormattedMessage
id='navbar_dropdown.report'
@@ -304,7 +313,7 @@ export default class NavbarDropdown extends React.Component {
/>
</a>
</li>
- {adminDivider}
+ <li className='divider'></li>
{teamSettings}
{integrationsLink}
{manageLink}
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index c2f450f98..955758237 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.jsx
@@ -12,7 +12,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
const TutorialSteps = Constants.TutorialSteps;
const Preferences = Constants.Preferences;
diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx
index 23b8952cc..887bc0c8e 100644
--- a/webapp/components/password_reset_form.jsx
+++ b/webapp/components/password_reset_form.jsx
@@ -22,7 +22,7 @@ class PasswordResetForm extends React.Component {
handlePasswordReset(e) {
e.preventDefault();
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.password).value;
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
this.setState({
error: (
diff --git a/webapp/components/pending_post_actions.jsx b/webapp/components/pending_post_actions.jsx
new file mode 100644
index 000000000..7528ef207
--- /dev/null
+++ b/webapp/components/pending_post_actions.jsx
@@ -0,0 +1,92 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import PostStore from 'stores/post_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+
+import Client from 'utils/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class PendingPostActions extends React.Component {
+ constructor(props) {
+ super(props);
+ this.retryPost = this.retryPost.bind(this);
+ this.cancelPost = this.cancelPost.bind(this);
+ this.state = {};
+ }
+ retryPost(e) {
+ e.preventDefault();
+
+ var post = this.props.post;
+ Client.createPost(post,
+ (data) => {
+ AsyncClient.getPosts(post.channel_id);
+
+ var channel = ChannelStore.get(post.channel_id);
+ var member = ChannelStore.getMember(post.channel_id);
+ member.msg_count = channel.total_msg_count;
+ member.last_viewed_at = (new Date()).getTime();
+ ChannelStore.setChannelMember(member);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST,
+ post: data
+ });
+ },
+ () => {
+ post.state = Constants.POST_FAILED;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }
+ );
+
+ post.state = Constants.POST_LOADING;
+ PostStore.updatePendingPost(post);
+ this.forceUpdate();
+ }
+ cancelPost(e) {
+ e.preventDefault();
+
+ var post = this.props.post;
+ PostStore.removePendingPost(post.channel_id, post.pending_post_id);
+ this.forceUpdate();
+ }
+ render() {
+ return (<span className='pending-post-actions'>
+ <a
+ className='post-retry'
+ href='#'
+ onClick={this.retryPost}
+ >
+ <FormattedMessage
+ id='pending_post_actions.retry'
+ defaultMessage='Retry'
+ />
+ </a>
+ {' - '}
+ <a
+ className='post-cancel'
+ href='#'
+ onClick={this.cancelPost}
+ >
+ <FormattedMessage
+ id='pending_post_actions.cancel'
+ defaultMessage='Cancel'
+ />
+ </a>
+ </span>);
+ }
+}
+
+PendingPostActions.propTypes = {
+ post: React.PropTypes.object
+};
diff --git a/webapp/components/post.jsx b/webapp/components/post.jsx
index ae3fa9c98..2b28d442c 100644
--- a/webapp/components/post.jsx
+++ b/webapp/components/post.jsx
@@ -4,14 +4,9 @@
import PostHeader from './post_header.jsx';
import PostBody from './post_body.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -23,7 +18,6 @@ export default class Post extends React.Component {
this.handleCommentClick = this.handleCommentClick.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
- this.retryPost = this.retryPost.bind(this);
this.state = {};
}
@@ -44,36 +38,6 @@ export default class Post extends React.Component {
this.refs.info.forceUpdate();
this.refs.header.forceUpdate();
}
- retryPost(e) {
- e.preventDefault();
-
- var post = this.props.post;
- Client.createPost(post,
- (data) => {
- AsyncClient.getPosts();
-
- var channel = ChannelStore.get(post.channel_id);
- var member = ChannelStore.getMember(post.channel_id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = Utils.getTimestamp();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST,
- post: data
- });
- },
- () => {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
- );
-
- post.state = Constants.POST_LOADING;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
shouldComponentUpdate(nextProps) {
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
@@ -103,6 +67,10 @@ export default class Post extends React.Component {
return true;
}
+ if (nextProps.compactDisplay !== this.props.compactDisplay) {
+ return true;
+ }
+
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
@@ -187,24 +155,21 @@ export default class Post extends React.Component {
systemMessageClass = 'post--system';
}
- let profilePic = null;
- if (!this.props.hideProfilePic) {
+ let profilePic = (
+ <img
+ src={Utils.getProfilePicSrcForPost(post, timestamp)}
+ height='36'
+ width='36'
+ />
+ );
+
+ if (Utils.isSystemMessage(post)) {
profilePic = (
- <img
- src={Utils.getProfilePicSrcForPost(post, timestamp)}
- height='36'
- width='36'
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: mattermostLogo}}
/>
);
-
- if (Utils.isSystemMessage(post)) {
- profilePic = (
- <span
- className='icon'
- dangerouslySetInnerHTML={{__html: mattermostLogo}}
- />
- );
- }
}
let centerClass = '';
@@ -212,11 +177,16 @@ export default class Post extends React.Component {
centerClass = 'center';
}
+ let compactClass = '';
+ if (this.props.compactDisplay) {
+ compactClass = 'post--compact';
+ }
+
return (
<div>
<div
id={'post_' + post.id}
- className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
+ className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
>
<div className={'post__content ' + centerClass}>
<div className='post__img'>{profilePic}</div>
@@ -231,6 +201,7 @@ export default class Post extends React.Component {
sameUser={this.props.sameUser}
user={this.props.user}
currentUser={this.props.currentUser}
+ compactDisplay={this.props.compactDisplay}
/>
<PostBody
post={post}
@@ -238,7 +209,7 @@ export default class Post extends React.Component {
parentPost={parentPost}
posts={posts}
handleCommentClick={this.handleCommentClick}
- retryPost={this.retryPost}
+ compactDisplay={this.props.compactDisplay}
/>
</div>
</div>
@@ -261,5 +232,6 @@ Post.propTypes = {
displayNameType: React.PropTypes.string,
hasProfiles: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired,
- center: React.PropTypes.bool
+ center: React.PropTypes.bool,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_attachment.jsx b/webapp/components/post_attachment.jsx
index 1c3df6ea2..8b5ff91f2 100644
--- a/webapp/components/post_attachment.jsx
+++ b/webapp/components/post_attachment.jsx
@@ -59,7 +59,7 @@ class PostAttachment extends React.Component {
toggleCollapseState(e) {
e.preventDefault();
- let state = this.state;
+ const state = this.state;
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
state.collapsed = !state.collapsed;
this.setState(state);
@@ -142,22 +142,22 @@ class PostAttachment extends React.Component {
});
if (headerCols.length > 0) { // Flush last fields
fieldTables.push(
- <table
- className='attachment___fields'
- key={'attachment__table__' + nrTables}
- >
- <thead>
- <tr>
+ <table
+ className='attachment___fields'
+ key={'attachment__table__' + nrTables}
+ >
+ <thead>
+ <tr>
{headerCols}
- </tr>
- </thead>
- <tbody>
- <tr>
- {bodyCols}
- </tr>
- </tbody>
- </table>
- );
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ {bodyCols}
+ </tr>
+ </tbody>
+ </table>
+ );
}
return (
<div>
@@ -209,6 +209,7 @@ class PostAttachment extends React.Component {
<a
href={data.author_link}
target='_blank'
+ rel='noopener noreferrer'
>
{author}
</a>
@@ -226,6 +227,7 @@ class PostAttachment extends React.Component {
className='attachment__title-link'
href={data.title_link}
target='_blank'
+ rel='noopener noreferrer'
>
{data.title}
</a>
diff --git a/webapp/components/post_attachment_oembed.jsx b/webapp/components/post_attachment_oembed.jsx
index a4e4ce001..359c7cc35 100644
--- a/webapp/components/post_attachment_oembed.jsx
+++ b/webapp/components/post_attachment_oembed.jsx
@@ -72,30 +72,31 @@ export default class PostAttachmentOEmbed extends React.Component {
className='attachment attachment--oembed'
ref='attachment'
>
- <div className='attachment__content'>
- <div
- className={'clearfix attachment__container'}
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <h1
+ className='attachment__title'
>
- <h1
- className='attachment__title'
+ <a
+ className='attachment__title-link'
+ href={data.url}
+ target='_blank'
+ rel='noopener noreferrer'
>
- <a
- className='attachment__title-link'
- href={data.url}
- target='_blank'
- >
- {data.title}
- </a>
- </h1>
- <div >
- <div
- className={'attachment__body attachment__body--no_thumb'}
- >
- {content}
- </div>
+ {data.title}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ {content}
</div>
</div>
</div>
+ </div>
</div>
);
}
diff --git a/webapp/components/post_body.jsx b/webapp/components/post_body.jsx
index 6c4e97d8e..415052d96 100644
--- a/webapp/components/post_body.jsx
+++ b/webapp/components/post_body.jsx
@@ -7,6 +7,7 @@ import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import PendingPostActions from './pending_post_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -24,7 +25,7 @@ export default class PostBody extends React.Component {
return true;
}
- if (nextProps.retryPost.toString() !== this.props.retryPost.toString()) {
+ if (!Utils.areObjectsEqual(nextProps.compactDisplay, this.props.compactDisplay)) {
return true;
}
@@ -110,18 +111,7 @@ export default class PostBody extends React.Component {
let loading;
if (post.state === Constants.POST_FAILED) {
postClass += ' post--fail';
- loading = (
- <a
- className='theme post-retry pull-right'
- href='#'
- onClick={this.props.retryPost}
- >
- <FormattedMessage
- id='post_body.retry'
- defaultMessage='Retry'
- />
- </a>
- );
+ loading = <PendingPostActions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting';
loading = (
@@ -136,9 +126,11 @@ export default class PostBody extends React.Component {
if (filenames && filenames.length > 0) {
fileAttachmentHolder = (
<FileAttachmentList
+
filenames={filenames}
channelId={post.channel_id}
userId={post.user_id}
+ compactDisplay={this.props.compactDisplay}
/>
);
}
@@ -189,5 +181,6 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_body_additional_content.jsx b/webapp/components/post_body_additional_content.jsx
index 452597dde..cdb735b47 100644
--- a/webapp/components/post_body_additional_content.jsx
+++ b/webapp/components/post_body_additional_content.jsx
@@ -120,7 +120,8 @@ export default class PostBodyAdditionalContent extends React.Component {
let toggle;
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
toggle = (
- <a className='post__embed-visibility'
+ <a
+ className='post__embed-visibility'
data-expanded={this.state.embedVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleEmbedVisibility}
@@ -131,7 +132,8 @@ export default class PostBodyAdditionalContent extends React.Component {
return (
<div>
{toggle}
- <div className='post__embed-container'
+ <div
+ className='post__embed-container'
hidden={!this.state.embedVisible}
>
{generateEmbed}
diff --git a/webapp/components/post_focus_view.jsx b/webapp/components/post_focus_view.jsx
index 0655a9916..30a2f9d72 100644
--- a/webapp/components/post_focus_view.jsx
+++ b/webapp/components/post_focus_view.jsx
@@ -5,7 +5,7 @@ import PostsView from './posts_view.jsx';
import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import React from 'react';
diff --git a/webapp/components/post_header.jsx b/webapp/components/post_header.jsx
index 9161d37f9..6fae092e5 100644
--- a/webapp/components/post_header.jsx
+++ b/webapp/components/post_header.jsx
@@ -14,6 +14,7 @@ export default class PostHeader extends React.Component {
super(props);
this.state = {};
}
+
render() {
const post = this.props.post;
@@ -31,7 +32,7 @@ export default class PostHeader extends React.Component {
);
}
- botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
} else if (Utils.isSystemMessage(post)) {
userProfile = (
<UserProfile
@@ -56,6 +57,7 @@ export default class PostHeader extends React.Component {
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
currentUser={this.props.currentUser}
+ compactDisplay={this.props.compactDisplay}
/>
</li>
</ul>
@@ -76,5 +78,6 @@ PostHeader.propTypes = {
commentCount: React.PropTypes.number.isRequired,
isLastComment: React.PropTypes.bool.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
- sameUser: React.PropTypes.bool.isRequired
+ sameUser: React.PropTypes.bool.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/post_info.jsx b/webapp/components/post_info.jsx
index 50b03c0be..e3b80e45c 100644
--- a/webapp/components/post_info.jsx
+++ b/webapp/components/post_info.jsx
@@ -4,7 +4,7 @@
import $ from 'jquery';
import * as Utils from 'utils/utils.jsx';
import TimeSince from './time_since.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -33,6 +33,7 @@ export default class PostInfo extends React.Component {
var post = this.props.post;
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
return '';
@@ -108,7 +109,7 @@ export default class PostInfo extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
key='editPost'
@@ -219,6 +220,7 @@ export default class PostInfo extends React.Component {
<TimeSince
eventTime={post.create_at}
sameUser={this.props.sameUser}
+ compactDisplay={this.props.compactDisplay}
/>
</li>
<li className='col col__reply'>
@@ -250,5 +252,6 @@ PostInfo.propTypes = {
allowReply: React.PropTypes.string.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
sameUser: React.PropTypes.bool.isRequired,
- currentUser: React.PropTypes.object.isRequired
+ currentUser: React.PropTypes.object.isRequired,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx
index cc9e738bc..64da4e67c 100644
--- a/webapp/components/posts_view.jsx
+++ b/webapp/components/posts_view.jsx
@@ -6,7 +6,7 @@ import $ from 'jquery';
import Post from './post.jsx';
import FloatingTimestamp from './floating_timestamp.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -55,6 +55,7 @@ export default class PostsView extends React.Component {
this.state = {
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
+ compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
isScrolling: false,
topPostId: null,
currentUser: UserStore.getCurrentUser(),
@@ -79,7 +80,8 @@ export default class PostsView extends React.Component {
updateState() {
this.setState({
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
- centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED
+ centerPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
+ compactPosts: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
});
}
onUserChange() {
@@ -274,6 +276,7 @@ export default class PostsView extends React.Component {
user={profile}
currentUser={this.state.currentUser}
center={this.state.centerPosts}
+ compactDisplay={this.state.compactPosts}
/>
);
@@ -479,6 +482,9 @@ export default class PostsView extends React.Component {
if (this.state.centerPosts !== nextState.centerPosts) {
return true;
}
+ if (this.state.compactPosts !== nextState.compactPosts) {
+ return true;
+ }
if (!Utils.areObjectsEqual(this.state.profiles, nextState.profiles)) {
return true;
}
@@ -592,7 +598,8 @@ PostsView.propTypes = {
showMoreMessagesBottom: React.PropTypes.bool,
channel: React.PropTypes.object,
messageSeparatorTime: React.PropTypes.number,
- postsToHighlight: React.PropTypes.object
+ postsToHighlight: React.PropTypes.object,
+ compactDisplay: React.PropTypes.bool
};
function ScrollToBottomArrows({isScrolling, atBottom, onClick}) {
diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx
index d1d8a2093..3f8a44cc3 100644
--- a/webapp/components/posts_view_container.jsx
+++ b/webapp/components/posts_view_container.jsx
@@ -9,7 +9,7 @@ import LoadingScreen from './loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx
index d037c089d..2199dbbec 100644
--- a/webapp/components/removed_from_channel_modal.jsx
+++ b/webapp/components/removed_from_channel_modal.jsx
@@ -98,7 +98,11 @@ export default class RemovedFromChannelModal extends React.Component {
className='close'
data-dismiss='modal'
aria-label='Close'
- ><span aria-hidden='true'>&times;</span></button>
+ >
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
+ </button>
<h4 className='modal-title'>
<FormattedMessage
id='removed_channel.from'
@@ -107,16 +111,16 @@ export default class RemovedFromChannelModal extends React.Component {
<span className='name'>{channelName}</span></h4>
</div>
<div className='modal-body'>
- <p>
- <FormattedMessage
- id='removed_channel.remover'
- defaultMessage='{remover} removed you from {channel}'
- values={{
- remover: (remover),
- channel: (channelName)
- }}
- />
- </p>
+ <p>
+ <FormattedMessage
+ id='removed_channel.remover'
+ defaultMessage='{remover} removed you from {channel}'
+ values={{
+ remover: (remover),
+ channel: (channelName)
+ }}
+ />
+ </p>
</div>
<div className='modal-footer'>
<button
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 5097e0573..a771803b8 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -3,22 +3,18 @@
import UserProfile from './user_profile.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
+import PendingPostActions from './pending_post_actions.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
-const ActionTypes = Constants.ActionTypes;
import {FormattedMessage, FormattedDate} from 'react-intl';
@@ -30,41 +26,10 @@ export default class RhsComment extends React.Component {
constructor(props) {
super(props);
- this.retryComment = this.retryComment.bind(this);
this.handlePermalink = this.handlePermalink.bind(this);
this.state = {};
}
- retryComment(e) {
- e.preventDefault();
-
- var post = this.props.post;
- Client.createPost(post,
- (data) => {
- AsyncClient.getPosts(post.channel_id);
-
- var channel = ChannelStore.get(post.channel_id);
- var member = ChannelStore.getMember(post.channel_id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = (new Date()).getTime();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST,
- post: data
- });
- },
- () => {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
- );
-
- post.state = Constants.POST_LOADING;
- PostStore.updatePendingPost(post);
- this.forceUpdate();
- }
handlePermalink(e) {
e.preventDefault();
GlobalActions.showGetPostLinkModal(this.props.post);
@@ -85,6 +50,7 @@ export default class RhsComment extends React.Component {
const isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var dropdownContents = [];
@@ -107,7 +73,7 @@ export default class RhsComment extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
role='presentation'
@@ -185,6 +151,11 @@ export default class RhsComment extends React.Component {
var timestamp = this.props.currentUser.update_at;
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
+ }
let loading;
let postClass = '';
let message = (
@@ -197,18 +168,7 @@ export default class RhsComment extends React.Component {
if (post.state === Constants.POST_FAILED) {
postClass += ' post-fail';
- loading = (
- <a
- className='theme post-retry pull-right'
- href='#'
- onClick={this.retryComment}
- >
- <FormattedMessage
- id='rhs_comment.retry'
- defaultMessage='Retry'
- />
- </a>
- );
+ loading = <PendingPostActions post={this.props.post}/>;
} else if (post.state === Constants.POST_LOADING) {
postClass += ' post-waiting';
loading = (
@@ -251,9 +211,10 @@ export default class RhsComment extends React.Component {
</div>
<div>
<ul className='post__header'>
- <li className='col__name'>
+ <li className='col col__name'>
<strong><UserProfile user={this.props.user}/></strong>
</li>
+ {botIndicator}
<li className='col'>
<time className='post__time'>
<FormattedDate
diff --git a/webapp/components/rhs_header_post.jsx b/webapp/components/rhs_header_post.jsx
index 493040800..6e0d9276e 100644
--- a/webapp/components/rhs_header_post.jsx
+++ b/webapp/components/rhs_header_post.jsx
@@ -3,7 +3,7 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 849971864..02fc4fc59 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -9,7 +9,7 @@ import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -41,6 +41,7 @@ export default class RhsRootPost extends React.Component {
const user = this.props.user;
var isOwner = this.props.currentUser.id === post.user_id;
var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -94,7 +95,7 @@ export default class RhsRootPost extends React.Component {
);
}
- if (isOwner) {
+ if (isOwner && !isSystemMessage) {
dropdownContents.push(
<li
key='rhs-root-edit'
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index 2398e5e69..ca5ed2dc8 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -17,6 +17,14 @@ import Scrollbars from 'react-custom-scrollbars';
import React from 'react';
+export function renderView(props) {
+ return (
+ <div
+ {...props}
+ className='scrollbar--view'
+ />);
+}
+
export function renderThumbHorizontal(props) {
return (
<div
@@ -211,6 +219,7 @@ export default class RhsThread extends React.Component {
autoHideDuration={500}
renderThumbHorizontal={renderThumbHorizontal}
renderThumbVertical={renderThumbVertical}
+ renderView={renderView}
>
<div className='post-right__scroll'>
<RootPost
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index 6a5af75c6..c96499392 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -4,7 +4,7 @@
//import $ from 'jquery';
//import Client from 'utils/web_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import LocalizationStore from 'stores/localization_store.jsx';
import {IntlProvider} from 'react-intl';
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index c5baf50ef..4daa94d8a 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -30,7 +30,8 @@ function getStateFromStores() {
return {
results,
- channels
+ channels,
+ searchTerm: SearchStore.getSearchTerm()
};
}
@@ -119,7 +120,7 @@ export default class SearchResults extends React.Component {
searchForm = <SearchBox/>;
}
var noResults = (!results || !results.order || !results.order.length);
- var searchTerm = SearchStore.getSearchTerm();
+ const searchTerm = this.state.searchTerm;
const profiles = this.state.profiles || {};
var ctls = null;
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 0c25e23bc..708b148d8 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -6,7 +6,7 @@ import UserProfile from './user_profile.jsx';
import UserStore from 'stores/user_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -67,6 +67,12 @@ export default class SearchResultsItem extends React.Component {
disableProfilePopover = true;
}
+ let botIndicator;
+
+ if (post.props && post.props.from_webhook) {
+ botIndicator = <li className='col col__name bot-indicator'>{Constants.BOT_NAME}</li>;
+ }
+
return (
<div className='search-item__container'>
<div className='date-separator'>
@@ -94,13 +100,14 @@ export default class SearchResultsItem extends React.Component {
</div>
<div>
<ul className='post__header'>
- <li className='col__name'><strong>
+ <li className='col col__name'><strong>
<UserProfile
user={user}
overwriteName={overrideUsername}
disablePopover={disableProfilePopover}
/>
</strong></li>
+ {botIndicator}
<li className='col'>
<time className='search-item-time'>
<FormattedDate
diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx
index 45a708d8c..a04961d5b 100644
--- a/webapp/components/select_team/select_team.jsx
+++ b/webapp/components/select_team/select_team.jsx
@@ -7,7 +7,7 @@ import * as Utils from 'utils/utils.jsx';
import ErrorBar from 'components/error_bar.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {Link} from 'react-router';
diff --git a/webapp/components/settings_sidebar.jsx b/webapp/components/settings_sidebar.jsx
index d55eb5366..dc59409a0 100644
--- a/webapp/components/settings_sidebar.jsx
+++ b/webapp/components/settings_sidebar.jsx
@@ -23,7 +23,7 @@ export default class SettingsSidebar extends React.Component {
}
}
render() {
- let tabList = this.props.tabs.map(function makeTab(tab) {
+ let tabList = this.props.tabs.map((tab) => {
let key = `${tab.name}_li`;
let className = '';
if (this.props.activeTab === tab.name) {
@@ -44,7 +44,7 @@ export default class SettingsSidebar extends React.Component {
</a>
</li>
);
- }.bind(this));
+ });
return (
<div>
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index a4d85f4ff..be0fb205b 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -169,6 +169,9 @@ export default class Sidebar extends React.Component {
$('.sidebar--left .nav-pills__container').perfectScrollbar();
}
+ this.refs.container.scrollTop = 0;
+ $('.nav-pills__container').perfectScrollbar('update');
+
// close the LHS on mobile when you change channels
if (this.state.activeId !== prevState.activeId) {
$('.app__body .inner-wrap').removeClass('move--right');
@@ -640,13 +643,13 @@ export default class Sidebar extends React.Component {
placement='top'
overlay={createChannelTootlip}
>
- <a
- className='add-channel-btn'
- href='#'
- onClick={this.showNewChannelModal.bind(this, 'O')}
- >
- {'+'}
- </a>
+ <a
+ className='add-channel-btn'
+ href='#'
+ onClick={this.showNewChannelModal.bind(this, 'O')}
+ >
+ {'+'}
+ </a>
</OverlayTrigger>
</h4>
</li>
@@ -677,13 +680,13 @@ export default class Sidebar extends React.Component {
placement='top'
overlay={createGroupTootlip}
>
- <a
- className='add-channel-btn'
- href='#'
- onClick={this.showNewChannelModal.bind(this, 'P')}
- >
- {'+'}
- </a>
+ <a
+ className='add-channel-btn'
+ href='#'
+ onClick={this.showNewChannelModal.bind(this, 'P')}
+ >
+ {'+'}
+ </a>
</OverlayTrigger>
</h4>
</li>
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index 143a3458a..76d9cf214 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -89,7 +89,7 @@ export default class SidebarHeader extends React.Component {
overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
ref='descriptionOverlay'
>
- <div className='team__name'>{this.props.teamDisplayName}</div>
+ <div className='team__name'>{this.props.teamDisplayName}</div>
</OverlayTrigger>
</div>
</a>
diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx
index b36d84b79..b24b8e4fb 100644
--- a/webapp/components/sidebar_right_menu.jsx
+++ b/webapp/components/sidebar_right_menu.jsx
@@ -10,7 +10,7 @@ import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -174,8 +174,8 @@ export default class SidebarRightMenu extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.HelpLink}
- rel='noreferrer'
>
<i className='fa fa-question'></i>
<FormattedMessage
@@ -193,8 +193,8 @@ export default class SidebarRightMenu extends React.Component {
<li>
<Link
target='_blank'
+ rel='noopener noreferrer'
to={global.window.mm_config.ReportAProblemLink}
- rel='noreferrer'
>
<i className='fa fa-phone'></i>
<FormattedMessage
@@ -250,7 +250,7 @@ export default class SidebarRightMenu extends React.Component {
<i className='fa fa-exchange'></i>
<FormattedMessage
id='sidebar_right_menu.switch_team'
- defaultMessage='Switch Team'
+ defaultMessage='Team Selection'
/>
</Link>
</li>
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index 5c06cefed..ad8b94722 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -3,7 +3,9 @@
import FormError from 'components/form_error.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+
+import * as GlobalActions from 'actions/global_actions.jsx';
+import {track} from 'actions/analytics_actions.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -12,11 +14,10 @@ import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
import Constants from 'utils/constants.jsx';
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-import {browserHistory, Link} from 'react-router';
-
import React from 'react';
import ReactDOM from 'react-dom';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {browserHistory, Link} from 'react-router';
import logoImage from 'images/logo.png';
@@ -198,6 +199,33 @@ export default class SignupUserComplete extends React.Component {
);
}
+ handleUserCreated(user, data) {
+ track('signup', 'signup_user_02_complete');
+ Client.loginById(
+ data.id,
+ user.password,
+ '',
+ () => {
+ if (this.state.hash > 0) {
+ BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
+ }
+
+ GlobalActions.emitInitialLoad(
+ () => {
+ browserHistory.push('/select_team');
+ }
+ );
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
handleSubmit(e) {
e.preventDefault();
@@ -260,7 +288,7 @@ export default class SignupUserComplete extends React.Component {
return;
}
- const providedPassword = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const providedPassword = ReactDOM.findDOMNode(this.refs.password).value;
if (!providedPassword || providedPassword.length < Constants.MIN_PASSWORD_LENGTH) {
this.setState({
nameError: '',
@@ -296,32 +324,7 @@ export default class SignupUserComplete extends React.Component {
this.state.data,
this.state.hash,
this.state.inviteId,
- (data) => {
- Client.track('signup', 'signup_user_02_complete');
- Client.loginById(
- data.id,
- user.password,
- '',
- () => {
- if (this.state.hash > 0) {
- BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
- }
-
- GlobalActions.emitInitialLoad(
- () => {
- browserHistory.push('/select_team');
- }
- );
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
- } else {
- this.setState({serverError: err.message});
- }
- }
- );
- },
+ this.handleUserCreated.bind(this, user),
(err) => {
this.setState({serverError: err.message});
}
@@ -371,6 +374,7 @@ export default class SignupUserComplete extends React.Component {
onChange={this.handleLdapIdChange}
placeholder={ldapIdPlaceholder}
spellCheck='false'
+ autoCapitalize='off'
/>
</div>
<div className={'form-group' + errorClass}>
@@ -402,7 +406,7 @@ export default class SignupUserComplete extends React.Component {
}
render() {
- Client.track('signup', 'signup_user_01_welcome');
+ track('signup', 'signup_user_01_welcome');
// If we have been used then just display a message
if (this.state.usedBefore) {
@@ -511,6 +515,7 @@ export default class SignupUserComplete extends React.Component {
maxLength='128'
autoFocus={true}
spellCheck='false'
+ autoCapitalize='off'
/>
{emailError}
{emailHelpText}
@@ -594,6 +599,7 @@ export default class SignupUserComplete extends React.Component {
placeholder=''
maxLength={Constants.MAX_USERNAME_LENGTH}
spellCheck='false'
+ autoCapitalize='off'
/>
{nameError}
{nameHelpText}
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index 998c17340..86d349a1a 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -5,7 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import Constants from 'utils/constants.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import * as Utils from 'utils/utils.jsx';
diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx
index ce950e4f4..91f7443cb 100644
--- a/webapp/components/suggestion/suggestion_list.jsx
+++ b/webapp/components/suggestion/suggestion_list.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import SuggestionStore from 'stores/suggestion_store.jsx';
import React from 'react';
@@ -62,6 +62,10 @@ export default class SuggestionList extends React.Component {
scrollToItem(term) {
const content = this.getContent();
+ if (!content) {
+ return;
+ }
+
const visibleContentHeight = content[0].clientHeight;
const actualContentHeight = content[0].scrollHeight;
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 1f783fe9f..70d52740e 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -9,7 +9,7 @@ import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import TeamStore from 'stores/team_store.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const holders = defineMessages({
dirDisabled: {
@@ -378,9 +378,9 @@ class GeneralTab extends React.Component {
</div>
</div>
<div className='setting-list__hint'>
- <FormattedMessage
+ <FormattedHTMLMessage
id='general_tab.codeLongDesc'
- defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by **Get Team Invite Link** in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
/>
</div>
</div>
@@ -473,7 +473,9 @@ class GeneralTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
</button>
<h4
className='modal-title'
@@ -518,4 +520,4 @@ GeneralTab.propTypes = {
activeSection: React.PropTypes.string.isRequired
};
-export default injectIntl(GeneralTab); \ No newline at end of file
+export default injectIntl(GeneralTab);
diff --git a/webapp/components/team_import_tab.jsx b/webapp/components/team_import_tab.jsx
index 782273c5a..f724a789a 100644
--- a/webapp/components/team_import_tab.jsx
+++ b/webapp/components/team_import_tab.jsx
@@ -123,7 +123,8 @@ class TeamImportTab extends React.Component {
return (
<div>
<div className='modal-header'>
- <button type='button'
+ <button
+ type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 657643367..fedf34ab5 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -92,7 +92,9 @@ class TeamSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>
+ {'×'}
+ </span>
</button>
<h4
className='modal-title'
@@ -133,4 +135,4 @@ TeamSettingsModal.propTypes = {
intl: intlShape.isRequired
};
-export default injectIntl(TeamSettingsModal); \ No newline at end of file
+export default injectIntl(TeamSettingsModal);
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 4aa88d267..7f5ecea09 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -211,6 +211,7 @@ export default class Textbox extends React.Component {
{previewLink}
<a
target='_blank'
+ rel='noopener noreferrer'
href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
className='textbox-help-link'
>
diff --git a/webapp/components/time_since.jsx b/webapp/components/time_since.jsx
index f715193e2..50a0f7d04 100644
--- a/webapp/components/time_since.jsx
+++ b/webapp/components/time_since.jsx
@@ -26,7 +26,7 @@ export default class TimeSince extends React.Component {
clearInterval(this.intervalId);
}
render() {
- if (this.props.sameUser) {
+ if (this.props.sameUser || this.props.compactDisplay) {
return (
<time className='post__time'>
{Utils.displayTimeFormatted(this.props.eventTime)}
@@ -69,5 +69,6 @@ TimeSince.defaultProps = {
TimeSince.propTypes = {
eventTime: React.PropTypes.number.isRequired,
- sameUser: React.PropTypes.bool
+ sameUser: React.PropTypes.bool,
+ compactDisplay: React.PropTypes.bool
};
diff --git a/webapp/components/toggle_modal_button.jsx b/webapp/components/toggle_modal_button.jsx
index 69bdbbda0..6082901de 100644
--- a/webapp/components/toggle_modal_button.jsx
+++ b/webapp/components/toggle_modal_button.jsx
@@ -25,7 +25,7 @@ export default class ModalToggleButton extends React.Component {
}
render() {
- const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-use-before-define
+ const {children, dialogType, dialogProps, onClick, ...props} = this.props;
// allow callers to provide an onClick which will be called before the modal is shown
let clickHandler = this.show;
@@ -38,7 +38,7 @@ export default class ModalToggleButton extends React.Component {
}
// this assumes that all modals will have a show property and an onHide event
- const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, {
+ const dialog = React.createElement(dialogType, Object.assign({}, dialogProps, {
show: this.state.show,
onHide: () => {
this.hide();
diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx
index 95c26edca..277ff967f 100644
--- a/webapp/components/tutorial/tutorial_intro_screens.jsx
+++ b/webapp/components/tutorial/tutorial_intro_screens.jsx
@@ -6,7 +6,7 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -149,6 +149,7 @@ export default class TutorialIntroScreens extends React.Component {
<a
href={'mailto:' + global.window.mm_config.SupportEmail}
target='_blank'
+ rel='noopener noreferrer'
>
{global.window.mm_config.SupportEmail}
</a>
diff --git a/webapp/components/user_settings/custom_theme_chooser.jsx b/webapp/components/user_settings/custom_theme_chooser.jsx
index 9fbdd1251..e77ea1d30 100644
--- a/webapp/components/user_settings/custom_theme_chooser.jsx
+++ b/webapp/components/user_settings/custom_theme_chooser.jsx
@@ -230,11 +230,11 @@ class CustomThemeChooser extends React.Component {
overlay={popoverContent}
ref='headerOverlay'
>
- <span className='input-group-addon'>
- <img
- src={codeThemeURL}
- />
- </span>
+ <span className='input-group-addon'>
+ <img
+ src={codeThemeURL}
+ />
+ </span>
</OverlayTrigger>
</div>
</div>
diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx
index 32da296bf..f743feee6 100644
--- a/webapp/components/user_settings/import_theme_modal.jsx
+++ b/webapp/components/user_settings/import_theme_modal.jsx
@@ -81,7 +81,7 @@ class ImportThemeModal extends React.Component {
theme.mentionHighlightLink = '#2f81b7';
theme.codeTheme = 'github';
- let user = UserStore.getCurrentUser();
+ const user = UserStore.getCurrentUser();
user.theme_props = theme;
Client.updateUser(user,
diff --git a/webapp/components/user_settings/manage_languages.jsx b/webapp/components/user_settings/manage_languages.jsx
index bbf3a2e40..269181922 100644
--- a/webapp/components/user_settings/manage_languages.jsx
+++ b/webapp/components/user_settings/manage_languages.jsx
@@ -5,7 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx';
import Client from 'utils/web_client.jsx';
import * as I18n from 'i18n/i18n.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx
index 4b0faf865..9552c686d 100644
--- a/webapp/components/user_settings/premade_theme_chooser.jsx
+++ b/webapp/components/user_settings/premade_theme_chooser.jsx
@@ -59,6 +59,7 @@ export default class PremadeThemeChooser extends React.Component {
<a
href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-theme-examples'
target='_blank'
+ rel='noopener noreferrer'
>
<FormattedMessage
id='user.settings.display.theme.otherThemes'
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 61e0e1dad..dc5bd1c0e 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -173,6 +173,7 @@ class AdvancedSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='sendOnCtrlEnter'
checked={ctrlSendActive[0]}
onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
/>
@@ -187,6 +188,7 @@ class AdvancedSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='sendOnCtrlEnter'
checked={ctrlSendActive[1]}
onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
/>
diff --git a/webapp/components/user_settings/user_settings_developer.jsx b/webapp/components/user_settings/user_settings_developer.jsx
index cabb021cb..ae6d60362 100644
--- a/webapp/components/user_settings/user_settings_developer.jsx
+++ b/webapp/components/user_settings/user_settings_developer.jsx
@@ -3,7 +3,7 @@
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -135,4 +135,4 @@ DeveloperTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(DeveloperTab); \ No newline at end of file
+export default injectIntl(DeveloperTab);
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index c4af57d4c..16175d4de 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -23,7 +23,8 @@ function getDisplayStateFromStores() {
militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'),
nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'),
selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT),
- channelDisplayMode: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT)
+ channelDisplayMode: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT),
+ messageDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT)
};
}
@@ -70,8 +71,14 @@ export default class UserSettingsDisplay extends React.Component {
name: Preferences.CHANNEL_DISPLAY_MODE,
value: this.state.channelDisplayMode
};
+ const messageDisplayPreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: Preferences.MESSAGE_DISPLAY,
+ value: this.state.messageDisplay
+ };
- AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference],
+ AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference, messageDisplayPreference],
() => {
this.updateSection('');
},
@@ -89,6 +96,9 @@ export default class UserSettingsDisplay extends React.Component {
handleChannelDisplayModeRadio(channelDisplayMode) {
this.setState({channelDisplayMode});
}
+ handlemessageDisplayRadio(messageDisplay) {
+ this.setState({messageDisplay});
+ }
handleFont(selectedFont) {
Utils.applyFont(selectedFont);
this.setState({selectedFont});
@@ -115,6 +125,7 @@ export default class UserSettingsDisplay extends React.Component {
let channelDisplayModeSection;
let fontSection;
let languagesSection;
+ let messageDisplaySection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
@@ -135,6 +146,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='clockFormat'
checked={clockFormat[0]}
onChange={this.handleClockRadio.bind(this, 'false')}
/>
@@ -149,6 +161,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='clockFormat'
checked={clockFormat[1]}
onChange={this.handleClockRadio.bind(this, 'true')}
/>
@@ -253,6 +266,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[1]}
onChange={this.handleNameRadio.bind(this, 'username')}
/>
@@ -264,6 +278,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[0]}
onChange={this.handleNameRadio.bind(this, 'nickname_full_name')}
/>
@@ -275,6 +290,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='nameFormat'
checked={nameFormat[2]}
onChange={this.handleNameRadio.bind(this, 'full_name')}
/>
@@ -350,6 +366,107 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === Preferences.MESSAGE_DISPLAY) {
+ const messageDisplay = [false, false];
+ if (this.state.messageDisplay === Preferences.MESSAGE_DISPLAY_CLEAN) {
+ messageDisplay[0] = true;
+ } else {
+ messageDisplay[1] = true;
+ }
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='messageDisplay'
+ checked={messageDisplay[0]}
+ onChange={this.handlemessageDisplayRadio.bind(this, Preferences.MESSAGE_DISPLAY_CLEAN)}
+ />
+ <FormattedMessage
+ id='user.settings.display.messageDisplayClean'
+ defaultMessage='Clean'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='messageDisplay'
+ checked={messageDisplay[1]}
+ onChange={this.handlemessageDisplayRadio.bind(this, Preferences.MESSAGE_DISPLAY_COMPACT)}
+ />
+ <FormattedMessage
+ id='user.settings.display.messageDisplayCompact'
+ defaultMessage='Compact'
+ />
+ </label>
+ <br/>
+ </div>
+ <div>
+ <br/>
+ <FormattedMessage
+ id='user.settings.display.messageDisplayDescription'
+ defaultMessage='Select how messages in a channel should be displayed.'
+ />
+ </div>
+ </div>
+ ];
+
+ messageDisplaySection = (
+ <SettingItemMax
+ title={
+ <FormattedMessage
+ id='user.settings.display.messageDisplayTitle'
+ defaultMessage='Message Display'
+ />
+ }
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe;
+ if (this.state.messageDisplay === Preferences.MESSAGE_DISPLAY_CLEAN) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.messageDisplayClean'
+ defaultMessage='Clean'
+ />
+ );
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.display.messageDisplayCompact'
+ defaultMessage='Compact'
+ />
+ );
+ }
+
+ messageDisplaySection = (
+ <SettingItemMin
+ title={
+ <FormattedMessage
+ id='user.settings.display.messageDisplayTitle'
+ defaultMessage='Message Display'
+ />
+ }
+ describe={describe}
+ updateSection={() => {
+ this.props.updateSection(Preferences.MESSAGE_DISPLAY);
+ }}
+ />
+ );
+ }
+
if (this.props.activeSection === Preferences.CHANNEL_DISPLAY_MODE) {
const channelDisplayMode = [false, false];
if (this.state.channelDisplayMode === Preferences.CHANNEL_DISPLAY_MODE_CENTERED) {
@@ -364,6 +481,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='channelDisplayMode'
checked={channelDisplayMode[0]}
onChange={this.handleChannelDisplayModeRadio.bind(this, Preferences.CHANNEL_DISPLAY_MODE_CENTERED)}
/>
@@ -378,6 +496,7 @@ export default class UserSettingsDisplay extends React.Component {
<label>
<input
type='radio'
+ name='channelDisplayMode'
checked={channelDisplayMode[1]}
onChange={this.handleChannelDisplayModeRadio.bind(this, Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN)}
/>
@@ -392,7 +511,7 @@ export default class UserSettingsDisplay extends React.Component {
<br/>
<FormattedMessage
id='user.settings.display.channeldisplaymode'
- defaultMessage='Select how text in a channel is displayed.'
+ defaultMessage='Select the width of the center channel.'
/>
</div>
</div>
@@ -601,6 +720,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark'/>
{nameFormatSection}
<div className='divider-dark'/>
+ {messageDisplaySection}
+ <div className='divider-dark'/>
{channelDisplayModeSection}
<div className='divider-dark'/>
{languagesSection}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index be1d1e6c5..6149b1630 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -160,6 +160,11 @@ class UserSettingsGeneralTab extends React.Component {
const email = this.state.email.trim().toLowerCase();
const confirmEmail = this.state.confirmEmail.trim().toLowerCase();
+ if (user.email === email) {
+ this.updateSection('');
+ return;
+ }
+
const {formatMessage} = this.props.intl;
if (email === '' || !Utils.isEmail(email)) {
this.setState({emailError: formatMessage(holders.validEmail), clientError: '', serverError: ''});
@@ -171,11 +176,6 @@ class UserSettingsGeneralTab extends React.Component {
return;
}
- if (user.email === email) {
- this.updateSection('');
- return;
- }
-
user.email = email;
this.submitUser(user, true);
}
@@ -342,7 +342,7 @@ class UserSettingsGeneralTab extends React.Component {
<div className='col-sm-7'>
<input
className='form-control'
- type='text'
+ type='email'
onChange={this.updateEmail}
value={this.state.email}
/>
@@ -363,7 +363,7 @@ class UserSettingsGeneralTab extends React.Component {
<div className='col-sm-7'>
<input
className='form-control'
- type='text'
+ type='email'
onChange={this.updateConfirmEmail}
value={this.state.confirmEmail}
/>
@@ -681,6 +681,7 @@ class UserSettingsGeneralTab extends React.Component {
type='text'
onChange={this.updateNickname}
value={this.state.nickname}
+ autoCapitalize='off'
/>
</div>
</div>
@@ -764,6 +765,7 @@ class UserSettingsGeneralTab extends React.Component {
type='text'
onChange={this.updateUsername}
value={this.state.username}
+ autoCapitalize='off'
/>
</div>
</div>
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index fa84ce2d6..410ce1a4e 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -30,6 +30,10 @@ function getNotificationsStateFromStores() {
if (user.notify_props && user.notify_props.email) {
email = user.notify_props.email;
}
+ var push = 'mention';
+ if (user.notify_props && user.notify_props.push) {
+ push = user.notify_props.push;
+ }
var usernameKey = false;
var mentionKey = false;
@@ -72,9 +76,20 @@ function getNotificationsStateFromStores() {
}
}
- return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound,
- usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0,
- firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey};
+ return {
+ notifyLevel: desktop,
+ notifyPushLevel: push,
+ enableEmail: email,
+ soundNeeded,
+ enableSound: sound,
+ usernameKey,
+ mentionKey,
+ customKeys,
+ customKeysChecked: customKeys.length > 0,
+ firstNameKey,
+ allKey,
+ channelKey
+ };
}
const holders = defineMessages({
@@ -121,6 +136,7 @@ class NotificationsTab extends React.Component {
this.updateChannelKey = this.updateChannelKey.bind(this);
this.updateCustomMentionKeys = this.updateCustomMentionKeys.bind(this);
this.onCustomChange = this.onCustomChange.bind(this);
+ this.createPushNotificationSection = this.createPushNotificationSection.bind(this);
this.state = getNotificationsStateFromStores();
}
@@ -130,6 +146,7 @@ class NotificationsTab extends React.Component {
data.email = this.state.enableEmail;
data.desktop_sound = this.state.enableSound;
data.desktop = this.state.notifyLevel;
+ data.push = this.state.notifyPushLevel;
var mentionKeys = [];
if (this.state.usernameKey) {
@@ -150,13 +167,13 @@ class NotificationsTab extends React.Component {
data.channel = this.state.channelKey.toString();
Client.updateUserNotifyProps(data,
- function success() {
+ () => {
this.props.updateSection('');
AsyncClient.getMe();
- }.bind(this),
- function failure(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
handleCancel(e) {
@@ -185,15 +202,21 @@ class NotificationsTab extends React.Component {
this.updateState();
}
handleNotifyRadio(notifyLevel) {
- this.setState({notifyLevel: notifyLevel});
+ this.setState({notifyLevel});
+ ReactDOM.findDOMNode(this.refs.wrapper).focus();
+ }
+
+ handlePushRadio(notifyPushLevel) {
+ this.setState({notifyPushLevel});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
+
handleEmailRadio(enableEmail) {
- this.setState({enableEmail: enableEmail});
+ this.setState({enableEmail});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
handleSoundRadio(enableSound) {
- this.setState({enableSound: enableSound});
+ this.setState({enableSound});
ReactDOM.findDOMNode(this.refs.wrapper).focus();
}
updateUsernameKey(val) {
@@ -227,12 +250,129 @@ class NotificationsTab extends React.Component {
ReactDOM.findDOMNode(this.refs.customcheck).checked = true;
this.updateCustomMentionKeys();
}
+ createPushNotificationSection() {
+ var handleUpdateDesktopSection;
+ if (this.props.activeSection === 'push') {
+ var notifyActive = [false, false, false];
+ if (this.state.notifyPushLevel === 'all') {
+ notifyActive[0] = true;
+ } else if (this.state.notifyPushLevel === 'none') {
+ notifyActive[2] = true;
+ } else {
+ notifyActive[1] = true;
+ }
+
+ let inputs = [];
+
+ inputs.push(
+ <div key='userNotificationLevelOption'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[0]}
+ onChange={this.handlePushRadio.bind(this, 'all')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notification.allActivity'
+ defaultMessage='For all activity'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[1]}
+ onChange={this.handlePushRadio.bind(this, 'mention')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notifications.onlyMentions'
+ defaultMessage='For mentions and direct messages'
+ />
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ name='pushNotificationLevel'
+ checked={notifyActive[2]}
+ onChange={this.handlePushRadio.bind(this, 'none')}
+ />
+ <FormattedMessage
+ id='user.settings.push_notifications.off'
+ defaultMessage='Off'
+ />
+ </label>
+ </div>
+ </div>
+ );
+
+ const extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.push_notifications.info'
+ defaultMessage='Notification alerts are pushed to your mobile device when there is activity in Mattermost.'
+ />
+ </span>
+ );
+
+ return (
+ <SettingItemMax
+ title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
+ extraInfo={extraInfo}
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={this.state.serverError}
+ updateSection={this.handleCancel}
+ />
+ );
+ }
+
+ let describe = '';
+ if (this.state.notifyPushLevel === 'all') {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notification.allActivity'
+ defaultMessage='For all activity'
+ />
+ );
+ } else if (this.state.notifyPushLevel === 'none') {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notifications.off'
+ defaultMessage='Off'
+ />
+ );
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.push_notifications.onlyMentions'
+ defaultMessage='For mentions and direct messages'
+ />
+ );
+ }
+
+ handleUpdateDesktopSection = function updateDesktopSection() {
+ this.props.updateSection('push');
+ }.bind(this);
+
+ return (
+ <SettingItemMin
+ title={Utils.localizeMessage('user.settings.notifications.push', 'Mobile push notifications')}
+ describe={describe}
+ updateSection={handleUpdateDesktopSection}
+ />
+ );
+ }
render() {
const {formatMessage} = this.props.intl;
- var serverError = null;
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
+ const serverError = this.state.serverError;
var user = this.props.user;
@@ -254,7 +394,9 @@ class NotificationsTab extends React.Component {
<div key='userNotificationLevelOption'>
<div className='radio'>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[0]}
onChange={this.handleNotifyRadio.bind(this, 'all')}
/>
@@ -269,6 +411,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
/>
@@ -283,6 +426,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='desktopNotificationLevel'
checked={notifyActive[2]}
onChange={this.handleNotifyRadio.bind(this, 'none')}
/>
@@ -370,6 +514,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='notificationSounds'
checked={soundActive[0]}
onChange={this.handleSoundRadio.bind(this, 'true')}
/>
@@ -384,6 +529,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='notificationSounds'
checked={soundActive[1]}
onChange={this.handleSoundRadio.bind(this, 'false')}
/>
@@ -393,8 +539,8 @@ class NotificationsTab extends React.Component {
/>
</label>
<br/>
- </div>
- </div>
+ </div>
+ </div>
);
const extraInfo = (
@@ -473,6 +619,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='emailNotifications'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
/>
@@ -487,6 +634,7 @@ class NotificationsTab extends React.Component {
<label>
<input
type='radio'
+ name='emailNotifications'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
/>
@@ -763,6 +911,11 @@ class NotificationsTab extends React.Component {
);
}
+ let pushNotificationSection;
+ if (global.window.mm_config.SendPushNotifications === 'true') {
+ pushNotificationSection = this.createPushNotificationSection();
+ }
+
return (
<div>
<div className='modal-header'>
@@ -808,6 +961,8 @@ class NotificationsTab extends React.Component {
<div className='divider-light'/>
{emailSection}
<div className='divider-light'/>
+ {pushNotificationSection}
+ <div className='divider-light'/>
{keysSection}
<div className='divider-dark'/>
</div>
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 700aa295a..47a762442 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -61,6 +61,7 @@ class SecurityTab extends React.Component {
this.state = this.getDefaultState();
}
+
getDefaultState() {
return {
currentPassword: '',
@@ -71,6 +72,7 @@ class SecurityTab extends React.Component {
mfaToken: ''
};
}
+
submitPassword(e) {
e.preventDefault();
@@ -117,6 +119,7 @@ class SecurityTab extends React.Component {
}
);
}
+
activateMfa() {
Client.updateMfa(
this.state.mfaToken,
@@ -138,6 +141,7 @@ class SecurityTab extends React.Component {
}
);
}
+
deactivateMfa() {
Client.updateMfa(
'',
@@ -159,22 +163,28 @@ class SecurityTab extends React.Component {
}
);
}
+
updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value});
}
+
updateNewPassword(e) {
this.setState({newPassword: e.target.value});
}
+
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
+
updateMfaToken(e) {
this.setState({mfaToken: e.target.value});
}
+
showQrCode(e) {
e.preventDefault();
this.setState({mfaShowQr: true});
}
+
createMfaSection() {
let updateSectionStatus;
let submit;
@@ -329,6 +339,7 @@ class SecurityTab extends React.Component {
/>
);
}
+
createPasswordSection() {
let updateSectionStatus;
@@ -519,6 +530,7 @@ class SecurityTab extends React.Component {
/>
);
}
+
createSignInSection() {
let updateSectionStatus;
const user = this.props.user;
@@ -608,11 +620,11 @@ class SecurityTab extends React.Component {
const inputs = [];
inputs.push(
<div key='userSignInOption'>
- {emailOption}
- {gitlabOption}
- <br/>
- {ldapOption}
- {googleOption}
+ {emailOption}
+ {gitlabOption}
+ <br/>
+ {ldapOption}
+ {googleOption}
</div>
);
@@ -676,7 +688,10 @@ class SecurityTab extends React.Component {
/>
);
}
+
render() {
+ const user = this.props.user;
+
const passwordSection = this.createPasswordSection();
let numMethods = 0;
@@ -690,7 +705,9 @@ class SecurityTab extends React.Component {
}
let mfaSection;
- if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') {
+ if (global.window.mm_config.EnableMultifactorAuthentication === 'true' &&
+ global.window.mm_license.IsLicensed === 'true' &&
+ (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) {
mfaSection = this.createMfaSection();
}
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index f19538f71..811f4d8e4 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -212,7 +212,9 @@ export default class ThemeSetting extends React.Component {
key='premadeThemeColorLabel'
>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='theme'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
/>
@@ -233,7 +235,9 @@ export default class ThemeSetting extends React.Component {
key='customThemeColorLabel'
>
<label>
- <input type='radio'
+ <input
+ type='radio'
+ name='theme'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
/>
diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx
index c4d7cb4aa..7b827ac0e 100644
--- a/webapp/components/view_image.jsx
+++ b/webapp/components/view_image.jsx
@@ -3,7 +3,7 @@
import $ from 'jquery';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import AudioVideoPreview from './audio_video_preview.jsx';
import Constants from 'utils/constants.jsx';
@@ -404,6 +404,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) {
<a
href={fileUrl}
target='_blank'
+ rel='noopener noreferrer'
download={true}
>
<img
diff --git a/webapp/components/view_image_popover_bar.jsx b/webapp/components/view_image_popover_bar.jsx
index 5b9b2362f..3554ae3f8 100644
--- a/webapp/components/view_image_popover_bar.jsx
+++ b/webapp/components/view_image_popover_bar.jsx
@@ -54,6 +54,7 @@ export default class ViewImagePopoverBar extends React.Component {
download={this.props.filename}
className='text'
target='_blank'
+ rel='noopener noreferrer'
>
<FormattedMessage
id='view_image_popover.download'
diff --git a/webapp/components/youtube_video.jsx b/webapp/components/youtube_video.jsx
index 6083fd8a1..dc2d368d7 100644
--- a/webapp/components/youtube_video.jsx
+++ b/webapp/components/youtube_video.jsx
@@ -1,13 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
import ChannelStore from 'stores/channel_store.jsx';
+import WebClient from 'utils/web_client.jsx';
+import * as Utils from 'utils/utils.jsx';
-const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#\&\?]*)/;
+const ytRegex = /(?:http|https):\/\/(?:www\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(\/u\/\w\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&\?]*)/;
import React from 'react';
-import {Link} from 'react-router';
export default class YoutubeVideo extends React.Component {
constructor(props) {
@@ -15,12 +15,16 @@ export default class YoutubeVideo extends React.Component {
this.updateStateFromProps = this.updateStateFromProps.bind(this);
this.handleReceivedMetadata = this.handleReceivedMetadata.bind(this);
+ this.handleMetadataError = this.handleMetadataError.bind(this);
+ this.loadWithoutKey = this.loadWithoutKey.bind(this);
this.play = this.play.bind(this);
this.stop = this.stop.bind(this);
this.stopOnChannelChange = this.stopOnChannelChange.bind(this);
this.state = {
+ loaded: false,
+ failed: false,
playing: false,
title: ''
};
@@ -78,23 +82,39 @@ export default class YoutubeVideo extends React.Component {
}
componentDidMount() {
- if (global.window.mm_config.GoogleDeveloperKey) {
- $.ajax({
- async: true,
- url: 'https://www.googleapis.com/youtube/v3/videos',
- type: 'GET',
- data: {part: 'snippet', id: this.state.videoId, key: global.window.mm_config.GoogleDeveloperKey},
- success: this.handleReceivedMetadata
- });
+ const key = global.window.mm_config.GoogleDeveloperKey;
+ if (key) {
+ WebClient.getYoutubeVideoInfo(key, this.state.videoId,
+ this.handleReceivedMetadata, this.handleMetadataError);
+ } else {
+ this.loadWithoutKey();
}
}
+ loadWithoutKey() {
+ this.setState({loaded: true});
+ }
+
+ handleMetadataError() {
+ this.setState({
+ failed: true,
+ loaded: true,
+ title: Utils.localizeMessage('youtube_video.notFound', 'Video not found')
+ });
+ }
+
handleReceivedMetadata(data) {
- if (!data.items.length || !data.items[0].snippet) {
+ if (!data || !data.items || !data.items.length || !data.items[0].snippet) {
+ this.setState({
+ failed: true,
+ loaded: true,
+ title: Utils.localizeMessage('youtube_video.notFound', 'Video not found')
+ });
return null;
}
var metadata = data.items[0].snippet;
this.setState({
+ loaded: true,
receivedYoutubeData: true,
title: metadata.title
});
@@ -120,13 +140,28 @@ export default class YoutubeVideo extends React.Component {
}
render() {
+ if (!this.state.loaded) {
+ return <div className='video-loading'/>;
+ }
+
let header = 'Youtube';
if (this.state.title) {
header = header + ' - ';
}
let content;
- if (this.state.playing) {
+ if (this.state.failed) {
+ content = (
+ <div>
+ <div className='video-thumbnail__container'>
+ <div className='video-thumbnail__error'>
+ <div><i className='fa fa-warning fa-2x'/></div>
+ <div>{Utils.localizeMessage('youtube_video.notFound', 'Video not found')}</div>
+ </div>
+ </div>
+ </div>
+ );
+ } else if (this.state.playing) {
content = (
<iframe
src={'https://www.youtube.com/embed/' + this.state.videoId + '?autoplay=1&autohide=1&border=0&wmode=opaque&fs=1&enablejsapi=1' + this.state.time}
@@ -157,7 +192,15 @@ export default class YoutubeVideo extends React.Component {
<div>
<h4>
<span className='video-type'>{header}</span>
- <span className='video-title'><Link to={this.props.link}>{this.state.title}</Link></span>
+ <span className='video-title'>
+ <a
+ href={this.props.link}
+ target='blank'
+ rel='noopener noreferrer'
+ >
+ {this.state.title}
+ </a>
+ </span>
</h4>
<div
className='video-div embed-responsive-item'