summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-05-17 07:21:39 -0400
committerChristopher Speller <crspeller@gmail.com>2016-05-17 07:21:39 -0400
commitfd53e3b7868234af328cd73150318fc8e7a26b89 (patch)
tree48c49af0d6b25bf978430efc61aa5b3b63b3414a
parent5f5f813387a914d6e34945490c438755adfa8505 (diff)
downloadchat-fd53e3b7868234af328cd73150318fc8e7a26b89.tar.gz
chat-fd53e3b7868234af328cd73150318fc8e7a26b89.tar.bz2
chat-fd53e3b7868234af328cd73150318fc8e7a26b89.zip
PLT-2257 Reorganized System Console (#3003)
* Reorganized system console * Fixed the names of some components * Fixed timestamp for BrandImageSetting * Fixed merge issues * Updated push notification settings to match master branch * Removed top level setting pages and moved enable Gitlab/LDAP settings onto their respective pages * Re-added restrictDirectMessage setting to system console * Re-added email connection test and fixed some margins * Fixed ESLint errors * Renamed Authentication > Onboarding to Authentication > Email in the system console * Renamed Customization > Whitelabeling to Customization > Custom Branding in System Console * Re-added EnableOpenServer to system console
-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_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.jsx53
-rw-r--r--webapp/components/admin_console/brand_image_setting.jsx182
-rw-r--r--webapp/components/admin_console/compliance_settings.jsx318
-rw-r--r--webapp/components/admin_console/configuration_settings.jsx74
-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.jsx192
-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.jsx1214
-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.jsx514
-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/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/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.jsx180
-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/users_and_teams_settings.jsx179
-rw-r--r--webapp/components/admin_console/webhook_settings.jsx166
-rw-r--r--webapp/components/analytics/system_analytics.jsx3
-rw-r--r--webapp/components/analytics/team_analytics.jsx52
-rw-r--r--webapp/i18n/en.json49
-rw-r--r--webapp/i18n/es.json27
-rw-r--r--webapp/i18n/fr.json26
-rw-r--r--webapp/i18n/pt.json24
-rw-r--r--webapp/root.jsx189
-rw-r--r--webapp/sass/components/_module.scss1
-rw-r--r--webapp/sass/components/_save-button.scss7
-rw-r--r--webapp/sass/layout/_headers.scss3
-rw-r--r--webapp/sass/routes/_admin-console.scss463
-rw-r--r--webapp/stores/admin_store.jsx6
57 files changed, 5683 insertions, 6560 deletions
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_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 4ffc31815..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..a0bd2aa36 100644
--- a/webapp/components/admin_console/boolean_setting.jsx
+++ b/webapp/components/admin_console/boolean_setting.jsx
@@ -8,16 +8,43 @@ 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}
+ checked={this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.trueText}
</label>
@@ -25,13 +52,12 @@ export default class BooleanSetting extends React.Component {
<input
type='radio'
value='false'
- checked={!this.props.currentValue}
- onChange={this.props.handleChange}
- disabled={this.props.isDisabled}
+ checked={!this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
/>
{this.props.falseText}
</label>
- {this.props.helpText}
</Setting>
);
}
@@ -48,15 +74,18 @@ BooleanSetting.defaultProps = {
id='admin.ldap.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_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..2f80f0be3
--- /dev/null
+++ b/webapp/components/admin_console/configuration_settings.jsx
@@ -0,0 +1,74 @@
+// 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 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'
+ />
+ }
+ >
+ <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..42b3727ec
--- /dev/null
+++ b/webapp/components/admin_console/database_settings.jsx
@@ -0,0 +1,192 @@
+// 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 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}
+ />
+ </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 71add9983..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>
+ renderSettings() {
+ return (
+ <SettingsGroup
+ header={
<FormattedMessage
- id='admin.email.emailFail'
- defaultMessage='Connection unsuccessful: {error}'
- values={{
- error: this.state.emailFail
- }}
+ id='admin.notifications.email'
+ defaultMessage='Email'
/>
- </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}
+ }
+ >
+ <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..a83407cb6
--- /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.email.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 510fd0887..bd3cd8dec 100644
--- a/webapp/components/admin_console/gitlab_settings.jsx
+++ b/webapp/components/admin_console/gitlab_settings.jsx
@@ -1,382 +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'
- >
+ }
+ >
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.gitlab.enableTitle'
+ defaultMessage='Enable Sign Up With GitLab: '
+ />
+ }
+ helpText={
+ <div>
<FormattedMessage
- id='admin.gitlab.clientSecretTitle'
- defaultMessage='Secret:'
+ 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='Secret'
- ref='Secret'
- placeholder={formatMessage(holders.clientSecretExample)}
- defaultValue={this.props.config.GitLabSettings.Secret}
- 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.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'
- >
- <FormattedMessage
- id='admin.gitlab.tokenTitle'
- defaultMessage='Token Endpoint:'
- />
- </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}
- />
- <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 64a7663c6..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
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/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx
index 9229c62bc..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 React from 'react';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+import * as Utils from 'utils/utils.jsx';
-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 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';
-import React from 'react';
-
-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;
- }
-
- if (action === 'console_false') {
- s.consoleEnable = false;
- }
-
- if (action === 'file_true') {
- s.fileEnable = true;
- }
+ this.getConfigFromState = this.getConfigFromState.bind(this);
- if (action === 'file_false') {
- s.fileEnable = false;
- }
+ this.renderSettings = this.renderSettings.bind(this);
- 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>
- <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'>
- <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>
+ }
+ >
+ <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'
- >
- <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')}
- />
- <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')}
- />
+ 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.false'
- defaultMessage='false'
+ id='admin.log.formatTime'
+ defaultMessage='Time (15:04:05 MST)'
/>
- </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
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 8759472a2..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
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 5eb099b8a..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
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 dfd19d057..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 a6e09b4a0..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);
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
new file mode 100644
index 000000000..339876b18
--- /dev/null
+++ b/webapp/components/admin_console/storage_settings.jsx
@@ -0,0 +1,180 @@
+// 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, {
+ 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.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'
+ />
+ }
+ >
+ <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>
+ );
+ }
+} \ No newline at end of file
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/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/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/i18n/en.json b/webapp/i18n/en.json
index 748a1a657..022235879 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -436,29 +436,46 @@
"admin.service.webhooksTitle": "Enable Incoming Webhooks: ",
"admin.sidebar.addTeamSidebar": "Add team from sidebar menu",
"admin.sidebar.audits": "Compliance and Auditing",
- "admin.sidebar.compliance": "Compliance Settings",
- "admin.sidebar.email": "Email Settings",
- "admin.sidebar.file": "File Settings",
- "admin.sidebar.gitlab": "GitLab Settings",
- "admin.sidebar.ldap": "LDAP Settings",
+ "admin.sidebar.authentication": "Authentication",
+ "admin.sidebar.compliance": "Compliance",
+ "admin.sidebar.configuration": "Configuration",
+ "admin.sidebar.connections": "Connections",
+ "admin.sidebar.customBrand": "Custom Branding",
+ "admin.sidebar.customization": "Customization",
+ "admin.sidebar.database": "Database",
+ "admin.sidebar.developer": "Developer",
+ "admin.sidebar.email": "Email",
+ "admin.sidebar.external": "External Services",
+ "admin.sidebar.files": "Files",
+ "admin.sidebar.general": "General",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.integrations": "Integrations",
+ "admin.sidebar.images": "Images",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edition and License",
- "admin.sidebar.loading": "Loading",
- "admin.sidebar.log": "Log Settings",
+ "admin.sidebar.logging": "Logging",
+ "admin.sidebar.login": "Login",
"admin.sidebar.logs": "Logs",
+ "admin.sidebar.notifications": "Notifications",
"admin.sidebar.other": "OTHER",
- "admin.sidebar.privacy": "Privacy Settings",
- "admin.sidebar.rate_limit": "Rate Limit Settings",
+ "admin.sidebar.privacy": "Privacy",
+ "admin.sidebar.publicLinks": "Public Links",
+ "admin.sidebar.push": "Mobile Push",
+ "admin.sidebar.rateLimiting": "Rate Limiting",
"admin.sidebar.reports": "SITE REPORTS",
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
- "admin.sidebar.service": "Service Settings",
+ "admin.sidebar.security": "Security",
+ "admin.sidebar.sign_up": "Sign Up",
+ "admin.sidebar.sessions": "Sessions",
"admin.sidebar.settings": "SETTINGS",
- "admin.sidebar.sql": "SQL Settings",
- "admin.sidebar.statistics": "- Statistics",
- "admin.sidebar.support": "Legal and Support Settings",
- "admin.sidebar.team": "Team Settings",
- "admin.sidebar.teams": "TEAMS ({count})",
- "admin.sidebar.users": "- Users",
+ "admin.sidebar.statistics": "Statistics",
+ "admin.sidebar.storage": "Storage",
+ "admin.sidebar.support": "Legal and Support",
+ "admin.sidebar.teams": "TEAMS ({count, number})",
+ "admin.sidebar.users": "Users",
+ "admin.sidebar.usersAndTeams": "Users and Teams",
"admin.sidebar.view_statistics": "View Statistics",
+ "admin.sidebar.webhooks": "Webhooks and Commands",
"admin.sidebarHeader.systemConsole": "System Console",
"admin.sql.dataSource": "Data Source:",
"admin.sql.driverName": "Driver Name:",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 7aa3071f6..6094ff269 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -435,30 +435,25 @@
"admin.service.webhooksDescription": "Cuando es verdadero, la entradas de webhooks será permitida. Para ayudar a combatir ataques phishing, todos los comentarios de webhooks serán marcados con una etiqueta BOT.",
"admin.service.webhooksTitle": "Habilitar Webhooks de Entrada: ",
"admin.sidebar.addTeamSidebar": "Agregar un equipo el menú lateral",
- "admin.sidebar.audits": "Auditorías",
- "admin.sidebar.compliance": "Configuración de Cumplimiento",
- "admin.sidebar.email": "Configuración de correo",
- "admin.sidebar.file": "Configuracion de archivos",
- "admin.sidebar.gitlab": "Configuración de GitLab",
- "admin.sidebar.ldap": "Configuración LDAP",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edición y Licencia",
- "admin.sidebar.loading": "Cargando",
- "admin.sidebar.log": "Configuracion de log",
"admin.sidebar.logs": "Registros",
"admin.sidebar.other": "OTROS",
- "admin.sidebar.privacy": "Configuración de privacidad",
- "admin.sidebar.rate_limit": "Configuración de velocidad",
"admin.sidebar.reports": "REPORTES DEL SITIO",
"admin.sidebar.rmTeamSidebar": "Remover un equipo del menú lateral",
- "admin.sidebar.service": "Configuración de servicio",
"admin.sidebar.settings": "CONFIGURACIONES",
+ "admin.sidebar.statistics": "Estadísticas",
+ "admin.sidebar.teams": "EQUIPOS ({count, number})",
+ "admin.sidebar.users": "Usuarios",
+ "admin.sidebar.view_statistics": "Ver Estadísticas",
+ "admin.sidebar.file": "Configuracion de archivos",
+ "admin.sidebar.loading": "Cargando",
+ "admin.sidebar.log": "Configuracion de log",
+ "admin.sidebar.rate_limit": "Configuración de velocidad",
+ "admin.sidebar.service": "Configuración de servicio",
"admin.sidebar.sql": "Configuración de SQL",
- "admin.sidebar.statistics": "- Estadísticas",
- "admin.sidebar.support": "Configuración de Soporte",
"admin.sidebar.team": "Configuración de equipo",
- "admin.sidebar.teams": "EQUIPOS ({count})",
- "admin.sidebar.users": "- Usuarios",
- "admin.sidebar.view_statistics": "Ver Estadísticas",
"admin.sidebarHeader.systemConsole": "Consola de sistema",
"admin.sql.dataSource": "Origen de datos:",
"admin.sql.driverName": "Nombre de controlador:",
diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json
index 752d5bea3..f6dba48d1 100644
--- a/webapp/i18n/fr.json
+++ b/webapp/i18n/fr.json
@@ -370,29 +370,27 @@
"admin.service.webhooksTitle": "Activer les webhooks entrants :",
"admin.sidebar.addTeamSidebar": "Afficher l'équipe dans le menu",
"admin.sidebar.audits": "Conformité et vérification",
- "admin.sidebar.compliance": "Paramètres de conformité",
- "admin.sidebar.email": "Configuration messagerie",
- "admin.sidebar.file": "Configuration du fichier",
- "admin.sidebar.gitlab": "Configuration de GitLab",
- "admin.sidebar.ldap": "Paramètres LDAP",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.other": "AUTRES",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Édition et licence",
- "admin.sidebar.loading": "Chargement",
- "admin.sidebar.log": "Configuration du journal (log)",
"admin.sidebar.logs": "Journaux",
- "admin.sidebar.other": "AUTRES",
- "admin.sidebar.privacy": "Paramètres de confidentialité",
- "admin.sidebar.rate_limit": "Configuration des limites de débits",
"admin.sidebar.reports": "RAPPORTS DU SITE",
"admin.sidebar.rmTeamSidebar": "Ne plus afficher l'équipe dans le menu",
+ "admin.sidebar.statistics": "Statistiques",
+ "admin.sidebar.teams": "ÉQUIPES ({count, number})",
+ "admin.sidebar.users": "Utilisateurs",
+ "admin.sidebar.view_statistics": "Voir les statistiques",
+ "admin.sidebar.file": "Configuration du fichier",
+ "admin.sidebar.ldap": "LDAP",
+ "admin.sidebar.loading": "Chargement",
+ "admin.sidebar.log": "Configuration du journal (log)",
+ "admin.sidebar.rate_limit": "Configuration des limites de débits",
"admin.sidebar.service": "Configuration du service",
"admin.sidebar.settings": "REGLAGES",
"admin.sidebar.sql": "Configuration SQL",
- "admin.sidebar.statistics": "- Statistiques",
"admin.sidebar.support": "Paramètres légaux et support",
"admin.sidebar.team": "Configuration de l'équipe",
- "admin.sidebar.teams": "ÉQUIPES ({count})",
- "admin.sidebar.users": "- Utilisateurs",
- "admin.sidebar.view_statistics": "Voir les statistiques",
"admin.sidebarHeader.systemConsole": "Console système",
"admin.sql.dataSource": "Source de données :",
"admin.sql.driverName": "Nom du pilote :",
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index 88e2a59c3..c3a9e0bd4 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -436,28 +436,16 @@
"admin.service.webhooksTitle": "Ativar Webhooks Entrada: ",
"admin.sidebar.addTeamSidebar": "Adicionar equipe do menu lateral",
"admin.sidebar.audits": "Conformidade e Auditoria",
- "admin.sidebar.compliance": "Configurações Compliance",
- "admin.sidebar.email": "Configuração do e-mail",
- "admin.sidebar.file": "Configurações do arquivo",
- "admin.sidebar.gitlab": "Configurações GitLab",
- "admin.sidebar.ldap": "Configurações LDAP",
+ "admin.sidebar.gitlab": "GitLab",
+ "admin.sidebar.other": "OUTROS",
+ "admin.sidebar.ldap": "LDAP",
"admin.sidebar.license": "Edição e Licença",
- "admin.sidebar.loading": "Carregando",
- "admin.sidebar.log": "Configurações de Log",
"admin.sidebar.logs": "Logs",
- "admin.sidebar.other": "OUTROS",
- "admin.sidebar.privacy": "Configurações De Privacidade",
- "admin.sidebar.rate_limit": "Configurações Rate Limit",
"admin.sidebar.reports": "RELATÓRIOS DO SITE",
"admin.sidebar.rmTeamSidebar": "Remover equipe do menu lateral",
- "admin.sidebar.service": "Configurações do Serviço",
- "admin.sidebar.settings": "CONFIGURAÇÕES",
- "admin.sidebar.sql": "Configurações SQL",
- "admin.sidebar.statistics": "- Estátisticas",
- "admin.sidebar.support": "Configurações jurídico e apoio",
- "admin.sidebar.team": "Configurações De Equipe",
- "admin.sidebar.teams": "EQUIPES ({count})",
- "admin.sidebar.users": "- Usuários",
+ "admin.sidebar.statistics": "Estátisticas",
+ "admin.sidebar.teams": "EQUIPES ({count, number})",
+ "admin.sidebar.users": "Usuários",
"admin.sidebar.view_statistics": "Ver Estatísticas",
"admin.sidebarHeader.systemConsole": "Console do Sistema",
"admin.sql.dataSource": "Fonte de Dados:",
diff --git a/webapp/root.jsx b/webapp/root.jsx
index f53109cdd..aef2607ef 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -34,7 +34,6 @@ import * as GlobalActions from 'action_creators/global_actions.jsx';
import SignupUserComplete from 'components/signup_user_complete.jsx';
import ShouldVerifyEmail from 'components/should_verify_email.jsx';
import DoVerifyEmail from 'components/do_verify_email.jsx';
-import AdminConsole from 'components/admin_console/admin_controller.jsx';
import TutorialView from 'components/tutorial/tutorial_view.jsx';
import BackstageNavbar from 'components/backstage/backstage_navbar.jsx';
import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx';
@@ -51,6 +50,38 @@ import AppDispatcher from './dispatcher/app_dispatcher.jsx';
import Constants from './utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
+import AdminConsole from 'components/admin_console/admin_console.jsx';
+import SystemAnalytics from 'components/analytics/system_analytics.jsx';
+import ConfigurationSettings from 'components/admin_console/configuration_settings.jsx';
+import UsersAndTeamsSettings from 'components/admin_console/users_and_teams_settings.jsx';
+import PrivacySettings from 'components/admin_console/privacy_settings.jsx';
+import LogSettings from 'components/admin_console/log_settings.jsx';
+import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx';
+import GitLabSettings from 'components/admin_console/gitlab_settings.jsx';
+import LdapSettings from 'components/admin_console/ldap_settings.jsx';
+import SignupSettings from 'components/admin_console/signup_settings.jsx';
+import LoginSettings from 'components/admin_console/login_settings.jsx';
+import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
+import SessionSettings from 'components/admin_console/session_settings.jsx';
+import ConnectionSettings from 'components/admin_console/connection_settings.jsx';
+import EmailSettings from 'components/admin_console/email_settings.jsx';
+import PushSettings from 'components/admin_console/push_settings.jsx';
+import WebhookSettings from 'components/admin_console/webhook_settings.jsx';
+import ExternalServiceSettings from 'components/admin_console/external_service_settings.jsx';
+import DatabaseSettings from 'components/admin_console/database_settings.jsx';
+import StorageSettings from 'components/admin_console/storage_settings.jsx';
+import ImageSettings from 'components/admin_console/image_settings.jsx';
+import CustomBrandSettings from 'components/admin_console/custom_brand_settings.jsx';
+import LegalAndSupportSettings from 'components/admin_console/legal_and_support_settings.jsx';
+import ComplianceSettings from 'components/admin_console/compliance_settings.jsx';
+import RateSettings from 'components/admin_console/rate_settings.jsx';
+import DeveloperSettings from 'components/admin_console/developer_settings.jsx';
+import TeamUsers from 'components/admin_console/team_users.jsx';
+import TeamAnalytics from 'components/analytics/team_analytics.jsx';
+import LicenseSettings from 'components/admin_console/license_settings.jsx';
+import Audits from 'components/admin_console/audits.jsx';
+import Logs from 'components/admin_console/logs.jsx';
+
import Claim from 'components/claim/claim.jsx';
import EmailToOAuth from 'components/claim/components/email_to_oauth.jsx';
import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx';
@@ -319,7 +350,161 @@ function renderRootComponent() {
<Route
path='admin_console'
component={AdminConsole}
- />
+ >
+ <IndexRedirect to='system_analytics'/>
+ <Route
+ path='system_analytics'
+ component={SystemAnalytics}
+ />
+ <Route path='general'>
+ <IndexRedirect to='configuration'/>
+ <Route
+ path='configuration'
+ component={ConfigurationSettings}
+ />
+ <Route
+ path='users_and_teams'
+ component={UsersAndTeamsSettings}
+ />
+ <Route
+ path='privacy'
+ component={PrivacySettings}
+ />
+ <Route
+ path='logging'
+ component={LogSettings}
+ />
+ </Route>
+ <Route path='authentication'>
+ <IndexRedirect to='email'/>
+ <Route
+ path='email'
+ component={EmailAuthenticationSettings}
+ />
+ <Route
+ path='gitlab'
+ component={GitLabSettings}
+ />
+ <Route
+ path='ldap'
+ component={LdapSettings}
+ />
+ </Route>
+ <Route path='security'>
+ <IndexRedirect to='sign_up'/>
+ <Route
+ path='sign_up'
+ component={SignupSettings}
+ />
+ <Route
+ path='login'
+ component={LoginSettings}
+ />
+ <Route
+ path='public_links'
+ component={PublicLinkSettings}
+ />
+ <Route
+ path='sessions'
+ component={SessionSettings}
+ />
+ <Route
+ path='connections'
+ component={ConnectionSettings}
+ />
+ </Route>
+ <Route path='notifications'>
+ <IndexRedirect to='email'/>
+ <Route
+ path='email'
+ component={EmailSettings}
+ />
+ <Route
+ path='push'
+ component={PushSettings}
+ />
+ </Route>
+ <Route path='integrations'>
+ <IndexRedirect to='webhooks'/>
+ <Route
+ path='webhooks'
+ component={WebhookSettings}
+ />
+ <Route
+ path='external'
+ component={ExternalServiceSettings}
+ />
+ </Route>
+ <Route
+ path='database'
+ component={DatabaseSettings}
+ />
+ <Route path='files'>
+ <IndexRedirect to='storage'/>
+ <Route
+ path='storage'
+ component={StorageSettings}
+ />
+ <Route
+ path='images'
+ component={ImageSettings}
+ />
+ </Route>
+ <Route path='customization'>
+ <IndexRedirect to='custom_brand'/>
+ <Route
+ path='custom_brand'
+ component={CustomBrandSettings}
+ />
+ <Route
+ path='legal_and_support'
+ component={LegalAndSupportSettings}
+ />
+ </Route>
+ <Route
+ path='compliance'
+ component={ComplianceSettings}
+ />
+ <Route
+ path='rate'
+ component={RateSettings}
+ />
+ <Route
+ path='developer'
+ component={DeveloperSettings}
+ />
+ <Route path='team'>
+ <Redirect
+ from=':team'
+ to=':team/users'
+ />
+ <Route
+ path=':team/users'
+ component={TeamUsers}
+ />
+ <Route
+ path=':team/analytics'
+ component={TeamAnalytics}
+ />
+ <Redirect
+ from='*'
+ to='/error'
+ query={notFoundParams}
+ />
+ </Route>
+ <Route
+ path='license'
+ component={LicenseSettings}
+ />
+ <Route
+ path='audits'
+ component={Audits}
+ />
+ <Route
+ path='logs'
+ component={Logs}
+ />
+ </Route>
<Route
path=':team'
component={NeedsTeam}
diff --git a/webapp/sass/components/_module.scss b/webapp/sass/components/_module.scss
index 24488df96..e74404d9c 100644
--- a/webapp/sass/components/_module.scss
+++ b/webapp/sass/components/_module.scss
@@ -11,6 +11,7 @@
@import 'modal';
@import 'oauth';
@import 'popover';
+@import 'save-button';
@import 'scrollbar';
@import 'search';
@import 'suggestion-list';
diff --git a/webapp/sass/components/_save-button.scss b/webapp/sass/components/_save-button.scss
new file mode 100644
index 000000000..12f793aa1
--- /dev/null
+++ b/webapp/sass/components/_save-button.scss
@@ -0,0 +1,7 @@
+@charset 'UTF-8';
+
+.save-button {
+ .glyphicon {
+ margin-right: 10px;
+ }
+} \ No newline at end of file
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 35fef8e08..35b9e5524 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -118,7 +118,8 @@
// Team Header in Sidebar
.sidebar--left,
-.sidebar--menu {
+.sidebar--menu,
+.admin-sidebar {
.team__header {
@include legacy-pie-clearfix;
padding: 9px 10px;
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 0f47e7529..a4c8621c8 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -1,6 +1,12 @@
@charset 'UTF-8';
-.admin-controller {
+.admin-console {
+ color: #333;
+ height: 100%;
+ margin-left: 220px;
+ overflow: auto;
+ padding: 0 20px;
+
> div {
height: 100%;
}
@@ -34,122 +40,6 @@
}
}
- .sidebar--left {
- &.sidebar--collapsable {
- background: #333;
-
- .team__header {
- background: transparent;
- margin-bottom: 5px;
- }
-
- .nav {
- li {
- padding: 0;
- @include opacity(1);
-
- .icon {
- width: 17px;
- }
-
- &.divider {
- @include alpha-property(background, $black, .1);
- }
-
- > a {
- &:hover,
- &:focus {
- @include alpha-property(background, $black, .1);
- }
-
- &.active {
- background-color: transparent;
- }
- }
-
- > h4 {
- background: alpha-color($white, .15);
- color: $white;
- margin: 1px 0 0;
- padding: 10px;
-
- .menu-icon--right {
- right: 12px;
- top: 6px;
- }
- }
- }
-
- .menu-icon--right {
- font-size: 18px;
- font-weight: 600;
- height: 20px;
- line-height: 20px;
- position: absolute;
- right: 10px;
- text-align: center;
- top: 3px;
- width: 20px;
-
- .fa {
- color: $white;
- font-size: 13px;
- position: relative;
- right: -2px;
- }
- }
-
- &.nav__sub-menu {
- @include font-smoothing(initial);
- background: #111;
-
- &.padded {
- padding: 5px 0;
- }
-
- li {
- > a {
- background: transparent;
- color: #bbb;
- font-size: 13px;
- padding: 5px 35px 5px 15px;
-
- &:hover {
- color: lighten($primary-color, 10);
- }
-
- &.active {
- color: $white;
- font-weight: 600;
- }
- }
-
- .nav-more {
- background: transparent;
- color: #bbb;
- cursor: pointer;
- display: block;
- font-size: 13px;
- padding: 5px 15px;
-
- &:hover {
- color: lighten($primary-color, 10);
- }
- }
- }
- }
-
- &.nav__inner-menu {
- li {
- > a {
- padding-left: 20px;
- }
- }
- }
- }
- }
- }
-
.log__panel {
background-color: white;
border: 1px solid #ddd;
@@ -160,168 +50,160 @@
width: 100%;
}
- .app__content {
- color: #333;
+ &.admin {
+ background-color: #f1f1f1;
+ min-height: 600px;
+ overflow: auto;
+ padding: 0 40px 20px;
+ }
- &.admin {
- background-color: #f1f1f1;
- min-height: 600px;
- overflow: auto;
- padding: 0 40px 20px;
- }
+ .wrapper--fixed {
+ max-width: 800px;
+ }
- .wrapper--fixed {
- max-width: 800px;
+ .form-horizontal {
+ margin-top: 40px;
+
+ .control-label {
+ font-weight: 600;
+ padding-right: 0;
+ text-align: left;
}
- .form-horizontal {
- margin-top: 40px;
+ .form-group {
+ margin-bottom: 25px;
+ }
- .control-label {
- font-weight: 600;
- padding-right: 0;
- text-align: left;
+ .file__upload {
+ display: inline-block;
+ margin: 0 10px 10px 0;
+ position: relative;
+
+ input {
+ @include opacity(0);
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: 5;
}
+ }
- .form-group {
- margin-bottom: 25px;
+ .help-text {
+ color: #777;
+ margin: 10px 0 0 15px;
- &.form-group--small {
- margin-bottom: 10px;
- }
+ &.no-margin {
+ margin: 0;
}
- .file__upload {
- display: inline-block;
- margin: 0 10px 10px 0;
- position: relative;
-
- input {
- @include opacity(0);
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: 100%;
- z-index: 5;
- }
+ &.no-margin--top {
+ margin-top: 0;
}
- .help-text {
- color: #777;
- margin: 10px 0 0 15px;
-
- &.no-margin {
- margin: 0;
- }
-
- &.no-margin--top {
- margin-top: 0;
- }
-
- ul,
- ol {
- padding-left: 23px;
- }
-
- .help-link {
- margin-right: 5px;
- }
-
- .btn {
- font-size: 13px;
- }
+ ul,
+ ol {
+ padding-left: 23px;
}
- .alert {
- display: inline-block;
- margin: 1em 0 0;
- padding: 5px 7px;
- position: relative;
- top: 1px;
+ .help-link {
+ margin-right: 5px;
+ }
- .fa {
- margin-right: 5px;
- }
+ .btn {
+ font-size: 13px;
}
}
- .banner {
- background: $white;
- border: 1px solid #ddd;
- font-size: .95em;
- margin: 2em 0;
- padding: .7em 1.5em;
+ .alert {
+ display: inline-block;
+ margin: 1em 0 0;
+ padding: 5px 7px;
+ position: relative;
+ top: 1px;
- .banner__heading {
- font-size: 1.5em;
+ .fa {
+ margin-right: 5px;
}
+ }
+ }
- .banner__content {
- width: 80%;
- }
+ .banner {
+ background: $white;
+ border: 1px solid #ddd;
+ font-size: .95em;
+ margin: 2em 0;
+ padding: .7em 1.5em;
- &.warning {
- background: #e60000;
- }
+ .banner__heading {
+ font-size: 1.5em;
}
- .popover {
- border-radius: 3px;
- font-size: .95em;
- width: 100%;
+ .banner__content {
+ width: 80%;
}
- .panel {
- background-color: transparent;
- border: none;
+ &.warning {
+ background: #e60000;
}
+ }
- .panel-default {
- > .panel-heading {
- background-color: transparent;
- padding: 10px 0;
- }
+ .popover {
+ border-radius: 3px;
+ font-size: .95em;
+ width: 100%;
+ }
- .panel-body {
- padding: 30px 0 10px;
- }
- }
+ .panel {
+ background-color: transparent;
+ border: none;
+ }
- .panel-group {
- margin-bottom: 50px;
+ .panel-default {
+ > .panel-heading {
+ background-color: transparent;
+ padding: 10px 0;
}
- .panel-title {
- font-size: 24px;
- line-height: 1.5;
+ .panel-body {
+ padding: 30px 0 10px;
+ }
+ }
- a {
- @include clearfix;
- display: block;
- text-decoration: none;
+ .panel-group {
+ margin-bottom: 50px;
+ }
- &.collapsed {
- .fa-minus {
- display: none;
- }
+ .panel-title {
+ font-size: 24px;
+ line-height: 1.5;
- .fa-plus {
- display: inline-block;
- }
- }
+ a {
+ @include clearfix;
+ display: block;
+ text-decoration: none;
- .fa {
- color: #aaa;
- float: right;
- font-size: 18px;
- margin-top: 8px;
+ &.collapsed {
+ .fa-minus {
+ display: none;
}
.fa-plus {
- display: none;
+ display: inline-block;
}
}
+
+ .fa {
+ color: #aaa;
+ float: right;
+ font-size: 18px;
+ margin-top: 8px;
+ }
+
+ .fa-plus {
+ display: none;
+ }
}
}
@@ -343,3 +225,108 @@
margin-bottom: 1.5em;
max-width: 150px;
}
+
+.admin-console__disabled-text {
+ color: #777;
+ margin: 10px 0 0 15px;
+}
+
+.admin-sidebar {
+ background: #333;
+ border-right: 1px solid #ddd;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ width: 220px;
+ z-index: 5;
+
+ .team__header {
+ background: transparent;
+ }
+
+ .nav-pills__container {
+ background: #111;
+ @include font-smoothing(initial);
+ height: calc(100% - 80px);
+ margin-top: 1px;
+ position: relative;
+ }
+
+ .sidebar-category {
+ .category-title {
+ background: alpha-color($white, .15);
+ color: $white;
+ line-height: 15.4px;
+ padding: 10px;
+
+ .category-icon {
+ right: 12px;
+ top: 6px;
+ width: 17px;
+ }
+ }
+
+ .sections {
+ padding: 5px 0;
+ }
+ }
+
+ .sidebar-section-title {
+ padding: 5px 35px 5px 15px;
+ }
+
+ .sidebar-subsection-title {
+ padding: 5px 35px 5px 30px;
+ }
+
+ .sidebar-section-title,
+ .sidebar-subsection-title {
+ color: #bbb;
+ display: block;
+ font-size: 13px;
+ position: relative;
+
+ &:focus {
+ text-decoration: none;
+ }
+
+ &:hover {
+ color: lighten($primary-color, 10);
+ text-decoration: none;
+ }
+
+ &--active {
+ color: $white;
+ font-weight: 600;
+ }
+ }
+
+ .menu-icon--right {
+ font-size: 18px;
+ font-weight: 600;
+ height: 20px;
+ line-height: 20px;
+ position: absolute;
+ right: 12px;
+ text-align: center;
+ top: 8px;
+ width: 20px;
+
+ .fa {
+ color: $white;
+ font-size: 13px;
+ position: relative;
+ right: -2px;
+ }
+
+ &.menu__close {
+ cursor: pointer;
+ right: 10px;
+ top: 3px;
+ }
+ }
+}
+
+.email-connection-test {
+ margin-top: -15px;
+} \ No newline at end of file
diff --git a/webapp/stores/admin_store.jsx b/webapp/stores/admin_store.jsx
index ecfbaf85f..b135d9485 100644
--- a/webapp/stores/admin_store.jsx
+++ b/webapp/stores/admin_store.jsx
@@ -22,7 +22,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.audits = null;
this.config = null;
- this.teams = null;
+ this.teams = {};
this.complianceReports = null;
}
@@ -126,6 +126,10 @@ class AdminStoreClass extends EventEmitter {
this.teams = teams;
}
+ getTeam(id) {
+ return this.teams[id];
+ }
+
getSelectedTeams() {
const result = BrowserStore.getItem('seleted_teams');
if (!result) {