summaryrefslogtreecommitdiffstats
path: root/webapp/components/admin_console
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2016-03-16 18:13:16 -0700
committer=Corey Hulen <corey@hulen.com>2016-03-16 18:13:16 -0700
commitb9d5b4e5dcc1585397f1e1d2e53c5f040ee76220 (patch)
tree85d2c293aa3456182a754fefe6646162b516eb6c /webapp/components/admin_console
parente101b2cf7c172d1c4ff20e0df63917b5b8f923ed (diff)
parentcba59d4eb6ef0f65304bc72339c676ebfd653e2b (diff)
downloadchat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.tar.gz
chat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.tar.bz2
chat-b9d5b4e5dcc1585397f1e1d2e53c5f040ee76220.zip
merging files
Diffstat (limited to 'webapp/components/admin_console')
-rw-r--r--webapp/components/admin_console/admin_controller.jsx221
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx114
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx504
-rw-r--r--webapp/components/admin_console/admin_sidebar_header.jsx70
-rw-r--r--webapp/components/admin_console/audits.jsx102
-rw-r--r--webapp/components/admin_console/email_settings.jsx961
-rw-r--r--webapp/components/admin_console/gitlab_settings.jsx383
-rw-r--r--webapp/components/admin_console/image_settings.jsx692
-rw-r--r--webapp/components/admin_console/ldap_settings.jsx588
-rw-r--r--webapp/components/admin_console/legal_and_support_settings.jsx294
-rw-r--r--webapp/components/admin_console/license_settings.jsx295
-rw-r--r--webapp/components/admin_console/log_settings.jsx418
-rw-r--r--webapp/components/admin_console/logs.jsx102
-rw-r--r--webapp/components/admin_console/privacy_settings.jsx215
-rw-r--r--webapp/components/admin_console/rate_settings.jsx371
-rw-r--r--webapp/components/admin_console/reset_password_modal.jsx162
-rw-r--r--webapp/components/admin_console/select_team_modal.jsx115
-rw-r--r--webapp/components/admin_console/service_settings.jsx984
-rw-r--r--webapp/components/admin_console/sql_settings.jsx390
-rw-r--r--webapp/components/admin_console/team_settings.jsx420
-rw-r--r--webapp/components/admin_console/team_users.jsx188
-rw-r--r--webapp/components/admin_console/user_item.jsx423
22 files changed, 8012 insertions, 0 deletions
diff --git a/webapp/components/admin_console/admin_controller.jsx b/webapp/components/admin_console/admin_controller.jsx
new file mode 100644
index 000000000..aea2a0197
--- /dev/null
+++ b/webapp/components/admin_console/admin_controller.jsx
@@ -0,0 +1,221 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import AdminSidebar from './admin_sidebar.jsx';
+import AdminStore from 'stores/admin_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+
+import EmailSettingsTab from './email_settings.jsx';
+import LogSettingsTab from './log_settings.jsx';
+import LogsTab from './logs.jsx';
+import AuditsTab from './audits.jsx';
+import FileSettingsTab from './image_settings.jsx';
+import PrivacySettingsTab from './privacy_settings.jsx';
+import RateSettingsTab from './rate_settings.jsx';
+import GitLabSettingsTab from './gitlab_settings.jsx';
+import SqlSettingsTab from './sql_settings.jsx';
+import TeamSettingsTab from './team_settings.jsx';
+import ServiceSettingsTab from './service_settings.jsx';
+import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
+import TeamUsersTab from './team_users.jsx';
+import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
+import LdapSettingsTab from './ldap_settings.jsx';
+import ComplianceSettingsTab from './compliance_settings.jsx';
+import LicenseSettingsTab from './license_settings.jsx';
+import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
+
+import React from 'react';
+
+export default class AdminController extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
+ this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
+
+ this.state = {
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: props.tab || 'system_analytics',
+ selectedTeam: props.teamId || null
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addConfigChangeListener(this.onConfigListenerChange);
+ AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
+
+ $('[data-toggle="tooltip"]').tooltip();
+ $('[data-toggle="popover"]').popover();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ }
+
+ onConfigListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ render() {
+ var tab = <LoadingScreen/>;
+
+ if (this.state.config != null) {
+ if (this.state.selected === 'email_settings') {
+ tab = <EmailSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'log_settings') {
+ tab = <LogSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'logs') {
+ tab = <LogsTab/>;
+ } else if (this.state.selected === 'audits') {
+ tab = <AuditsTab/>;
+ } else if (this.state.selected === 'image_settings') {
+ tab = <FileSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'privacy_settings') {
+ tab = <PrivacySettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'rate_settings') {
+ tab = <RateSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'gitlab_settings') {
+ tab = <GitLabSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'sql_settings') {
+ tab = <SqlSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'team_settings') {
+ tab = <TeamSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'service_settings') {
+ tab = <ServiceSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'legal_and_support_settings') {
+ tab = <LegalAndSupportSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'ldap_settings') {
+ tab = <LdapSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'compliance_settings') {
+ tab = <ComplianceSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'license') {
+ tab = <LicenseSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'team_users') {
+ if (this.state.teams) {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]}/>;
+ }
+ } else if (this.state.selected === 'team_analytics') {
+ if (this.state.teams) {
+ tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]}/>;
+ }
+ } else if (this.state.selected === 'system_analytics') {
+ tab = <SystemAnalyticsTab/>;
+ }
+ }
+
+ return (
+ <div
+ id='admin_controller'
+ className='admin-controller'
+ >
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ />
+ <AdminSidebar
+ selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
+ selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
+ />
+ <div className='inner-wrap channel__wrap'>
+ <div className='row header'>
+ </div>
+ <div className='row main'>
+ <div
+ id='app-content'
+ className='app__content admin'
+ >
+ {tab}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+AdminController.defaultProps = {
+};
+
+AdminController.propTypes = {
+ tab: React.PropTypes.string,
+ teamId: React.PropTypes.string
+};
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
new file mode 100644
index 000000000..56b78448a
--- /dev/null
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -0,0 +1,114 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Utils from 'utils/utils.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
+import Constants from 'utils/constants.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import {Link} from 'react-router';
+
+function getStateFromStores() {
+ return {currentTeam: TeamStore.getCurrent()};
+}
+
+import React from 'react';
+
+export default class AdminNavbarDropdown extends React.Component {
+ constructor(props) {
+ super(props);
+ this.blockToggle = false;
+
+ this.state = getStateFromStores();
+ }
+
+ componentDidMount() {
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ this.blockToggle = true;
+ setTimeout(() => {
+ this.blockToggle = false;
+ }, 100);
+ });
+ }
+
+ componentWillUnmount() {
+ $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
+ }
+
+ render() {
+ return (
+ <ul className='nav navbar-nav navbar-right'>
+ <li
+ ref='dropdown'
+ className='dropdown'
+ >
+ <a
+ href='#'
+ className='dropdown-toggle'
+ data-toggle='dropdown'
+ role='button'
+ aria-expanded='false'
+ >
+ <span
+ className='dropdown__icon'
+ dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}
+ />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ >
+ <li>
+ <a
+ href={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name}
+ >
+ <FormattedMessage
+ id='admin.nav.switch'
+ defaultMessage='Switch to {display_name}'
+ values={{
+ display_name: this.state.currentTeam.display_name
+ }}
+ />
+ </a>
+ </li>
+ <li>
+ <Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
+ <FormattedMessage
+ id='admin.nav.logout'
+ defaultMessage='Logout'
+ />
+ </Link>
+ </li>
+ <li className='divider'></li>
+ <li>
+ <a
+ target='_blank'
+ href='/static/help/help.html'
+ >
+ <FormattedMessage
+ id='admin.nav.help'
+ defaultMessage='Help'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ target='_blank'
+ href='/static/help/report_problem.html'
+ >
+ <FormattedMessage
+ id='admin.nav.report'
+ defaultMessage='Report a Problem'
+ />
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
new file mode 100644
index 000000000..8ee75e2ef
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -0,0 +1,504 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AdminSidebarHeader from './admin_sidebar_header.jsx';
+import SelectTeamModal from './select_team_modal.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
+
+import React from 'react';
+
+export default class AdminSidebar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.isSelected = this.isSelected.bind(this);
+ this.handleClick = this.handleClick.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.state = {
+ showSelectModal: false
+ };
+ }
+
+ handleClick(name, teamId, e) {
+ e.preventDefault();
+ this.props.selectTab(name, teamId);
+ }
+
+ 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 '';
+ }
+
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ e.stopPropagation();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ 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>
+ );
+ const addTeamTooltip = (
+ <Tooltip id='add-team-tooltip'>
+ <FormattedMessage
+ id='admin.sidebar.addTeamSidebar'
+ defaultMessage='Add team from sidebar menu'
+ />
+ </Tooltip>
+ );
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ 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>
+ );
+ }
+ }
+ }
+ }
+
+ let ldapSettings;
+ let complianceSettings;
+ let licenseSettings;
+ if (global.window.mm_config.BuildEnterpriseReady === 'true') {
+ if (global.window.mm_license.IsLicensed === 'true') {
+ ldapSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('ldap_settings')}
+ onClick={this.handleClick.bind(this, 'ldap_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.ldap'
+ defaultMessage='LDAP Settings'
+ />
+ </a>
+ </li>
+ );
+
+ complianceSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('compliance_settings')}
+ onClick={this.handleClick.bind(this, 'compliance_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.compliance'
+ defaultMessage='Compliance Settings'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ licenseSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('license')}
+ onClick={this.handleClick.bind(this, 'license', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.license'
+ defaultMessage='Edition and License'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let audits;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ audits = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('audits')}
+ onClick={this.handleClick.bind(this, 'audits', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.audits'
+ defaultMessage='Compliance and Auditing'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='sidebar--left sidebar--collapsable'>
+ <div>
+ <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)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.view_statistics'
+ defaultMessage='View Statistics'
+ />
+ </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)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.service'
+ defaultMessage='Service Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.team'
+ defaultMessage='Team Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.sql'
+ defaultMessage='SQL Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.email'
+ defaultMessage='Email Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.file'
+ defaultMessage='File Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.log'
+ defaultMessage='Log Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.rate_limit'
+ defaultMessage='Rate Limit Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.privacy'
+ defaultMessage='Privacy Settings'
+ />
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.gitlab'
+ defaultMessage='GitLab Settings'
+ />
+ </a>
+ </li>
+ {ldapSettings}
+ {complianceSettings}
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('legal_and_support_settings')}
+ onClick={this.handleClick.bind(this, 'legal_and_support_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.support'
+ defaultMessage='Legal and Support Settings'
+ />
+ </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)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.logs'
+ defaultMessage='Logs'
+ />
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
+ </div>
+ );
+ }
+}
+
+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_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx
new file mode 100644
index 000000000..2e6252075
--- /dev/null
+++ b/webapp/components/admin_console/admin_sidebar_header.jsx
@@ -0,0 +1,70 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class SidebarHeader extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.toggleDropdown = this.toggleDropdown.bind(this);
+
+ this.state = {};
+ }
+
+ toggleDropdown(e) {
+ e.preventDefault();
+
+ if (this.refs.dropdown.blockToggle) {
+ this.refs.dropdown.blockToggle = false;
+ return;
+ }
+
+ $('.team__header').find('.dropdown-toggle').dropdown('toggle');
+ }
+
+ render() {
+ var me = UserStore.getCurrentUser();
+ var profilePicture = null;
+
+ if (!me) {
+ return null;
+ }
+
+ if (me.last_picture_update) {
+ profilePicture = (
+ <img
+ className='user__picture'
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
+ />
+ );
+ }
+
+ return (
+ <div className='team__header theme'>
+ <a
+ href='#'
+ onClick={this.toggleDropdown}
+ >
+ {profilePicture}
+ <div className='header__info'>
+ <div className='user__name'>{'@' + me.username}</div>
+ <div className='team__name'>
+ <FormattedMessage
+ id='admin.sidebarHeader.systemConsole'
+ defaultMessage='System Console'
+ />
+ </div>
+ </div>
+ </a>
+ <AdminNavbarDropdown ref='dropdown'/>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/audits.jsx b/webapp/components/admin_console/audits.jsx
new file mode 100644
index 000000000..1f94de7da
--- /dev/null
+++ b/webapp/components/admin_console/audits.jsx
@@ -0,0 +1,102 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+import AuditTable from '../audit_table.jsx';
+import ComplianceReports from './compliance_reports.jsx';
+
+import AdminStore from 'stores/admin_store.jsx';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class Audits extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onAuditListenerChange = this.onAuditListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ audits: AdminStore.getAudits()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addAuditChangeListener(this.onAuditListenerChange);
+ AsyncClient.getServerAudits();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeAuditChangeListener(this.onAuditListenerChange);
+ }
+
+ onAuditListenerChange() {
+ this.setState({
+ audits: AdminStore.getAudits()
+ });
+ }
+
+ reload() {
+ AdminStore.saveAudits(null);
+ this.setState({
+ audits: null
+ });
+
+ AsyncClient.getServerAudits();
+ }
+
+ render() {
+ var content = null;
+
+ if (global.window.mm_license.IsLicensed !== 'true') {
+ return <div/>;
+ }
+
+ if (this.state.audits === null) {
+ content = <LoadingScreen/>;
+ } else {
+ content = (
+ <div style={{margin: '10px'}}>
+ <AuditTable
+ audits={this.state.audits}
+ showUserId={true}
+ showIp={true}
+ showSession={true}
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <ComplianceReports/>
+
+ <div className='panel'>
+ <h3>
+ <FormattedMessage
+ id='admin.audits.title'
+ defaultMessage='User Activity'
+ />
+ </h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ <FormattedMessage
+ id='admin.audits.reload'
+ defaultMessage='Reload'
+ />
+ </button>
+ <div className='audit__panel'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx
new file mode 100644
index 000000000..1decdae91
--- /dev/null
+++ b/webapp/components/admin_console/email_settings.jsx
@@ -0,0 +1,961 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import crypto from 'crypto';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+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"'
+ },
+ connectionSecurityNone: {
+ id: 'admin.email.connectionSecurityNone',
+ defaultMessage: 'None'
+ },
+ connectionSecurityTls: {
+ id: 'admin.email.connectionSecurityTls',
+ defaultMessage: 'TLS (Recommended)'
+ },
+ connectionSecurityStart: {
+ id: 'admin.email.connectionSecurityStart',
+ defaultMessage: 'STARTTLS'
+ },
+ inviteSaltExample: {
+ id: 'admin.email.inviteSaltExample',
+ defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ },
+ passwordSaltExample: {
+ id: 'admin.email.passwordSaltExample',
+ defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ },
+ pushServerEx: {
+ id: 'admin.email.pushServerEx',
+ defaultMessage: 'E.g.: "http://push-test.mattermost.com"'
+ },
+ testing: {
+ id: 'admin.email.testing',
+ defaultMessage: 'Testing...'
+ },
+ saving: {
+ id: 'admin.email.saving',
+ defaultMessage: 'Saving Config...'
+ }
+});
+
+import React from 'react';
+
+class EmailSettings extends React.Component {
+ 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.state = {
+ sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
+ saveNeeded: false,
+ serverError: null,
+ emailSuccess: null,
+ emailFail: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ 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);
+ }
+
+ buildConfig() {
+ var 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.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).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.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).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 = ReactDOM.findDOMNode(this.refs.ConnectionSecurity).value.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;
+ }
+
+ return config;
+ }
+
+ 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');
+ }
+ );
+ }
+
+ render() {
+ const {formatMessage} = this.props.intl;
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var emailSuccess = '';
+ if (this.state.emailSuccess) {
+ emailSuccess = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>
+ <FormattedMessage
+ id='admin.email.emailSuccess'
+ defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
+ />
+ </div>
+ );
+ }
+
+ var emailFail = '';
+ if (this.state.emailFail) {
+ emailSuccess = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>
+ <FormattedMessage
+ id='admin.email.emailFail'
+ defaultMessage='Connection unsuccessful: {error}'
+ values={{
+ error: this.state.emailFail
+ }}
+ />
+ </div>
+ );
+ }
+
+ 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>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ConnectionSecurity'
+ >
+ <FormattedMessage
+ id='admin.email.connectionSecurityTitle'
+ defaultMessage='Connection Security:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='ConnectionSecurity'
+ ref='ConnectionSecurity'
+ defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ <option value=''>{formatMessage(holders.connectionSecurityNone)}</option>
+ <option value='TLS'>{formatMessage(holders.connectionSecurityTls)}</option>
+ <option value='STARTTLS'>{formatMessage(holders.connectionSecurityStart)}</option>
+ </select>
+ <div className='help-text'>
+ <table
+ className='table table-bordered'
+ cellPadding='5'
+ >
+ <tbody>
+ <tr><td className='help-text'>
+ <FormattedMessage
+ id='admin.email.connectionSecurityNone'
+ defaultMessage='None'
+ />
+ </td><td className='help-text'>
+ <FormattedMessage
+ id='admin.email.connectionSecurityNoneDescription'
+ defaultMessage='Mattermost will send email over an unsecure connection.'
+ />
+ </td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>
+ <FormattedMessage
+ id='admin.email.connectionSecurityTlsDescription'
+ defaultMessage='Encrypts the communication between Mattermost and your email server.'
+ />
+ </td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>
+ <FormattedMessage
+ id='admin.email.connectionSecurityStartDescription'
+ defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
+ />
+ </td></tr>
+ </tbody>
+ </table>
+ </div>
+ <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>
+
+ <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'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='true'
+ ref='sendPushNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_true')}
+ />
+ <FormattedMessage
+ id='admin.email.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendPushNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendPushNotifications}
+ onChange={this.handleChange.bind(this, 'sendPushNotifications_false')}
+ />
+ <FormattedMessage
+ id='admin.email.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.email.pushDesc'
+ defaultMessage='Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PushNotificationServer'
+ >
+ <FormattedMessage
+ id='admin.email.pushServerTitle'
+ defaultMessage='Push Notification Server:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PushNotificationServer'
+ ref='PushNotificationServer'
+ placeholder={formatMessage(holders.pushServerEx)}
+ defaultValue={this.props.config.EmailSettings.PushNotificationServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendPushNotifications}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.email.pushServerDesc'
+ defaultMessage='Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use http://push-test.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'
+ />
+ </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.email.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+EmailSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(EmailSettings);
diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx
new file mode 100644
index 000000000..7fdedde13
--- /dev/null
+++ b/webapp/components/admin_console/gitlab_settings.jsx
@@ -0,0 +1,383 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+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 React from 'react';
+
+class GitLabSettings extends React.Component {
+ 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
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'EnableTrue') {
+ s.Enable = true;
+ }
+
+ if (action === 'EnableFalse') {
+ s.Enable = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ 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();
+
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>
+ <FormattedMessage
+ id='admin.gitlab.settingsTitle'
+ defaultMessage='GitLab Settings'
+ />
+ </h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Enable'
+ >
+ <FormattedMessage
+ id='admin.gitlab.enableTitle'
+ defaultMessage='Enable Sign Up With GitLab: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='true'
+ ref='Enable'
+ defaultChecked={this.props.config.GitLabSettings.Enable}
+ onChange={this.handleChange.bind(this, 'EnableTrue')}
+ />
+ <FormattedMessage
+ id='admin.gitlab.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='false'
+ defaultChecked={!this.props.config.GitLabSettings.Enable}
+ onChange={this.handleChange.bind(this, 'EnableFalse')}
+ />
+ <FormattedMessage
+ id='admin.gitlab.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.gitlab.enableDescription'
+ defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
+ />
+ <br/>
+ </p>
+ <div className='help-text'>
+ <FormattedHTMLMessage
+ id='admin.gitlab.EnableHtmlDesc'
+ defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
+ />
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Id'
+ >
+ <FormattedMessage
+ id='admin.gitlab.clientIdTitle'
+ defaultMessage='Id:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Id'
+ ref='Id'
+ placeholder={formatMessage(holders.clientIdExample)}
+ defaultValue={this.props.config.GitLabSettings.Id}
+ onChange={this.handleChange}
+ disabled={!this.state.Enable}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.gitlab.clientIdDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Secret'
+ >
+ <FormattedMessage
+ id='admin.gitlab.clientSecretTitle'
+ defaultMessage='Secret:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Secret'
+ ref='Secret'
+ placeholder={formatMessage(holders.clientSecretExample)}
+ defaultValue={this.props.config.GitLabSettings.Secret}
+ onChange={this.handleChange}
+ disabled={!this.state.Enable}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.gitab.clientSecretDescription'
+ defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AuthEndpoint'
+ >
+ <FormattedMessage
+ id='admin.gitlab.authTitle'
+ defaultMessage='Auth Endpoint:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AuthEndpoint'
+ ref='AuthEndpoint'
+ placeholder={formatMessage(holders.authExample)}
+ defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Enable}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.gitlab.authDescription'
+ defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='TokenEndpoint'
+ >
+ <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>
+ );
+ }
+}
+
+//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);
diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx
new file mode 100644
index 000000000..576ff18fd
--- /dev/null
+++ b/webapp/components/admin_console/image_settings.jsx
@@ -0,0 +1,692 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/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({
+ 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 React from 'react';
+
+class FileSettings 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,
+ DriverName: this.props.config.FileSettings.DriverName
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ 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);
+ }
+
+ 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();
+
+ 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;
+
+ 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';
+ }
+
+ var enableFile = false;
+ var enableS3 = false;
+
+ if (this.state.DriverName === 'local') {
+ enableFile = true;
+ }
+
+ if (this.state.DriverName === 'amazons3') {
+ enableS3 = true;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.image.fileSettings'
+ defaultMessage='File Settings'
+ />
+ </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>
+ );
+ }
+}
+
+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
new file mode 100644
index 000000000..7996a3aab
--- /dev/null
+++ b/webapp/components/admin_console/ldap_settings.jsx
@@ -0,0 +1,588 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+const DEFAULT_LDAP_PORT = 389;
+const DEFAULT_QUERY_TIMEOUT = 60;
+
+var holders = defineMessages({
+ serverEx: {
+ id: 'admin.ldap.serverEx',
+ defaultMessage: 'Ex "10.0.0.23"'
+ },
+ portEx: {
+ id: 'admin.ldap.portEx',
+ defaultMessage: 'Ex "389"'
+ },
+ baseEx: {
+ id: 'admin.ldap.baseEx',
+ defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
+ },
+ firstnameAttrEx: {
+ id: 'admin.ldap.firstnameAttrEx',
+ defaultMessage: 'Ex "givenName"'
+ },
+ lastnameAttrEx: {
+ id: 'admin.ldap.lastnameAttrEx',
+ defaultMessage: 'Ex "sn"'
+ },
+ emailAttrEx: {
+ id: 'admin.ldap.emailAttrEx',
+ defaultMessage: 'Ex "mail" or "userPrincipalName"'
+ },
+ usernameAttrEx: {
+ id: 'admin.ldap.usernameAttrEx',
+ defaultMessage: 'Ex "sAMAccountName"'
+ },
+ idAttrEx: {
+ id: 'admin.ldap.idAttrEx',
+ defaultMessage: 'Ex "sAMAccountName"'
+ },
+ queryEx: {
+ id: 'admin.ldap.queryEx',
+ defaultMessage: 'Ex "60"'
+ },
+ saving: {
+ id: 'admin.ldap.saving',
+ defaultMessage: 'Saving Config...'
+ }
+});
+
+import React from 'react';
+
+class LdapSettings extends React.Component {
+ 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.state = {
+ saveNeeded: false,
+ serverError: null,
+ enable: this.props.config.LdapSettings.Enable
+ };
+ }
+ 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');
+
+ 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;
+
+ 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.EmailAttribute = this.refs.EmailAttribute.value.trim();
+ config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim();
+ config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim();
+
+ 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;
+
+ 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;
+ 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';
+ }
+
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
+
+ let bannerContent;
+ if (licenseEnabled) {
+ bannerContent = (
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>
+ <FormattedMessage
+ id='admin.ldap.bannerHeading'
+ defaultMessage='Note:'
+ />
+ </h4>
+ <p>
+ <FormattedMessage
+ id='admin.ldap.bannerDesc'
+ defaultMessage='If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'
+ />
+ </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>'
+ />
+ </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={formatMessage(holders.serverEx)}
+ 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={formatMessage(holders.portEx)}
+ 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>
+ <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={formatMessage(holders.baseEx)}
+ 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='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={formatMessage(holders.firstnameAttrEx)}
+ 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={formatMessage(holders.lastnameAttrEx)}
+ 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='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={formatMessage(holders.emailAttrEx)}
+ 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={formatMessage(holders.usernameAttrEx)}
+ 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={formatMessage(holders.idAttrEx)}
+ 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>
+ <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={formatMessage(holders.queryEx)}
+ 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'>
+ <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.ldap.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
+LdapSettings.defaultProps = {
+};
+
+LdapSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(LdapSettings);
diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx
new file mode 100644
index 000000000..4997a1385
--- /dev/null
+++ b/webapp/components/admin_console/legal_and_support_settings.jsx
@@ -0,0 +1,294 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/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 {
+ 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.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();
+
+ 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() {
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>
+ <FormattedMessage
+ id='admin.support.title'
+ defaultMessage='Legal and Support Settings'
+ />
+ </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>
+ );
+ }
+}
+
+LegalAndSupportSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(LegalAndSupportSettings); \ No newline at end of file
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
new file mode 100644
index 000000000..5aa0dba7e
--- /dev/null
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -0,0 +1,295 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Utils from 'utils/utils.jsx';
+import * as Client from 'utils/client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+const holders = defineMessages({
+ removing: {
+ id: 'admin.license.removing',
+ defaultMessage: 'Removing License...'
+ },
+ uploading: {
+ id: 'admin.license.uploading',
+ defaultMessage: 'Uploading License...'
+ }
+});
+
+import React from 'react';
+
+class LicenseSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleRemove = this.handleRemove.bind(this);
+
+ this.state = {
+ fileSelected: false,
+ fileName: null,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
+ if (element.prop('files').length > 0) {
+ this.setState({fileSelected: true, fileName: element.prop('files')[0].name});
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
+ if (element.prop('files').length === 0) {
+ return;
+ }
+ const file = element.prop('files')[0];
+
+ $('#upload-button').button('loading');
+
+ const formData = new FormData();
+ formData.append('license', file, file.name);
+
+ Client.uploadLicenseFile(formData,
+ () => {
+ Utils.clearFileInput(element[0]);
+ $('#upload-button').button('reset');
+ this.setState({fileSelected: false, fileName: null, serverError: null});
+ window.location.reload(true);
+ },
+ (error) => {
+ Utils.clearFileInput(element[0]);
+ $('#upload-button').button('reset');
+ this.setState({fileSelected: false, fileName: null, serverError: error.message});
+ }
+ );
+ }
+
+ handleRemove(e) {
+ e.preventDefault();
+
+ $('#remove-button').button('loading');
+
+ Client.removeLicenseFile(
+ () => {
+ $('#remove-button').button('reset');
+ this.setState({fileSelected: false, fileName: null, serverError: null});
+ window.location.reload(true);
+ },
+ (error) => {
+ $('#remove-button').button('reset');
+ this.setState({fileSelected: false, fileName: null, serverError: error.message});
+ }
+ );
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var btnClass = 'btn';
+ if (this.state.fileSelected) {
+ btnClass = 'btn btn-primary';
+ }
+
+ let edition;
+ let licenseType;
+ let licenseKey;
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ edition = (
+ <FormattedMessage
+ id='admin.license.enterpriseEdition'
+ defaultMessage='Mattermost Enterprise Edition. Designed for enterprise-scale communication.'
+ />
+ );
+ licenseType = (
+ <FormattedHTMLMessage
+ id='admin.license.enterpriseType'
+ values={{
+ terms: global.window.mm_config.TermsOfServiceLink,
+ name: global.window.mm_license.Name,
+ company: global.window.mm_license.Company,
+ users: global.window.mm_license.Users,
+ issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
+ start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
+ expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
+ ldap: global.window.mm_license.LDAP
+ }}
+ defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
+ <p>Your subscription details are as follows:</p>
+ Name: {name}<br />
+ Company or organization name: {company}<br/>
+ Number of users: {users}<br/>
+ License issued: {issued}<br/>
+ Start date of license: {start}<br/>
+ Expiry date of license: {expires}<br/>
+ LDAP: {ldap}<br/></div>'
+ />
+ );
+
+ licenseKey = (
+ <div className='col-sm-8'>
+ <button
+ disabled={this.props.config.LdapSettings.Enable}
+ className='btn btn-danger'
+ onClick={this.handleRemove}
+ id='remove-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.removing)}
+ >
+ <FormattedMessage
+ id='admin.license.keyRemove'
+ defaultMessage='Remove Enterprise License and Downgrade Server'
+ />
+ </button>
+ <br/>
+ <br/>
+ <p className='help-text'>
+ <FormattedHTMLMessage
+ id='admin.licence.keyMigration'
+ defaultMessage='If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href="http://mattermost.com" target="_blank">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.'
+ />
+ </p>
+ </div>
+ );
+ } else {
+ edition = (
+ <FormattedMessage
+ id='admin.license.teamEdition'
+ defaultMessage='Mattermost Team Edition. Designed for teams from 5 to 50 users.'
+ />
+ );
+
+ licenseType = (
+ <FormattedHTMLMessage
+ id='admin.license.teamType'
+ defaultMessage='<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p>
+ <p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>'
+ />
+ );
+
+ let fileName;
+ if (this.state.fileName) {
+ fileName = this.state.fileName;
+ } else {
+ fileName = (
+ <FormattedMessage
+ id='admin.license.noFile'
+ defaultMessage='No file uploaded'
+ />
+ );
+ }
+
+ licenseKey = (
+ <div className='col-sm-8'>
+ <div className='file__upload'>
+ <button className='btn btn-default'>
+ <FormattedMessage
+ id='admin.license.choose'
+ defaultMessage='Choose File'
+ />
+ </button>
+ <input
+ ref='fileInput'
+ type='file'
+ accept='.mattermost-license'
+ onChange={this.handleChange}
+ />
+ </div>
+ <button
+ className={btnClass}
+ disabled={!this.state.fileSelected}
+ onClick={this.handleSubmit}
+ id='upload-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.uploading)}
+ >
+ <FormattedMessage
+ id='admin.license.upload'
+ defaultMessage='Upload'
+ />
+ </button>
+ <div className='help-text no-margin'>
+ {fileName}
+ </div>
+ <br/>
+ {serverError}
+ <p className='help-text no-margin'>
+ <FormattedHTMLMessage
+ id='admin.license.uploadDesc'
+ defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'
+ />
+ </p>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.license.title'
+ defaultMessage='Edition and License'
+ />
+ </h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ <FormattedMessage
+ id='admin.license.edition'
+ defaultMessage='Edition: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ {edition}
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ <FormattedMessage
+ id='admin.license.type'
+ defaultMessage='License: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ {licenseType}
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ >
+ <FormattedMessage
+ id='admin.license.key'
+ defaultMessage='License Key: '
+ />
+ </label>
+ {licenseKey}
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
+
+LicenseSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(LicenseSettings);
diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx
new file mode 100644
index 000000000..5aa3ca1e0
--- /dev/null
+++ b/webapp/components/admin_console/log_settings.jsx
@@ -0,0 +1,418 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+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 React from 'react';
+
+class LogSettings extends React.Component {
+ 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;
+ }
+
+ if (action === 'file_false') {
+ s.fileEnable = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ 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();
+
+ 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');
+ }
+ );
+ }
+
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.log.logSettings'
+ defaultMessage='Log Settings'
+ />
+ </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>
+
+ <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')}
+ />
+ <FormattedMessage
+ id='admin.log.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p 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.'
+ />
+ </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'>
+ <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.'
+ />
+ </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'>
+ <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.'
+ />
+ </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'>
+ <FormattedMessage
+ id='admin.log.formatDescription'
+ defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
+ />
+ <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)}
+ >
+ <FormattedMessage
+ id='admin.log.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </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/logs.jsx b/webapp/components/admin_console/logs.jsx
new file mode 100644
index 000000000..f2c6d92c3
--- /dev/null
+++ b/webapp/components/admin_console/logs.jsx
@@ -0,0 +1,102 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AdminStore from 'stores/admin_store.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class Logs extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onLogListenerChange = this.onLogListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ logs: AdminStore.getLogs()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addLogChangeListener(this.onLogListenerChange);
+ AsyncClient.getLogs();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeLogChangeListener(this.onLogListenerChange);
+ }
+
+ onLogListenerChange() {
+ this.setState({
+ logs: AdminStore.getLogs()
+ });
+ }
+
+ reload() {
+ AdminStore.saveLogs(null);
+ this.setState({
+ logs: null
+ });
+
+ AsyncClient.getLogs();
+ }
+
+ render() {
+ var content = null;
+
+ if (this.state.logs === null) {
+ content = <LoadingScreen/>;
+ } else {
+ content = [];
+
+ for (var i = 0; i < this.state.logs.length; i++) {
+ var style = {
+ whiteSpace: 'nowrap',
+ fontFamily: 'monospace'
+ };
+
+ if (this.state.logs[i].indexOf('[EROR]') > 0) {
+ style.color = 'red';
+ }
+
+ content.push(<br key={'br_' + i}/>);
+ content.push(
+ <span
+ key={'log_' + i}
+ style={style}
+ >
+ {this.state.logs[i]}
+ </span>
+ );
+ }
+ }
+
+ return (
+ <div className='panel'>
+ <h3>
+ <FormattedMessage
+ id='admin.logs.title'
+ defaultMessage='Server Logs'
+ />
+ </h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ <FormattedMessage
+ id='admin.logs.reload'
+ defaultMessage='Reload'
+ />
+ </button>
+ <div className='log__panel'>
+ {content}
+ </div>
+ </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
new file mode 100644
index 000000000..a312dddca
--- /dev/null
+++ b/webapp/components/admin_console/privacy_settings.jsx
@@ -0,0 +1,215 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/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 {
+ 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.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked;
+ config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked;
+
+ 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() {
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.privacy.title'
+ defaultMessage='Privacy Settings'
+ />
+ </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>
+ );
+ }
+}
+
+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/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx
new file mode 100644
index 000000000..f3fb1742c
--- /dev/null
+++ b/webapp/components/admin_console/rate_settings.jsx
@@ -0,0 +1,371 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+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 React from 'react';
+
+class RateSettings extends React.Component {
+ 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;
+ }
+
+ if (action === 'VaryByRemoteAddrTrue') {
+ s.VaryByRemoteAddr = true;
+ }
+
+ if (action === 'VaryByRemoteAddrFalse') {
+ s.VaryByRemoteAddr = false;
+ }
+
+ this.setState(s);
+ }
+
+ 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();
+
+ 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;
+
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <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>
+ </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>
+ );
+ }
+}
+
+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/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..f80c740e3
--- /dev/null
+++ b/webapp/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,162 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import Constants from 'utils/constants.jsx';
+import {Modal} from 'react-bootstrap';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+var holders = defineMessages({
+ submit: {
+ id: 'admin.reset_password.submit',
+ defaultMessage: 'Please enter at least {chars} characters.'
+ }
+});
+
+import React from 'react';
+
+class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = ReactDOM.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
+ this.setState({serverError: this.props.intl.formatMessage(holders.submit, {chars: Constants.MIN_PASSWORD_LENGTH})});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ <FormattedMessage
+ id='admin.reset_password.title'
+ defaultMessage='Reset Password'
+ />
+ </Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ <FormattedMessage
+ id='admin.reset_password.newPassword'
+ defaultMessage='New Password'
+ />
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ <FormattedMessage
+ id='admin.reset_password.close'
+ defaultMessage='Close'
+ />
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ <FormattedMessage
+ id='admin.reset_password.select'
+ defaultMessage='Select'
+ />
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ intl: intlShape.isRequired,
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
+
+export default injectIntl(ResetPasswordModal); \ No newline at end of file
diff --git a/webapp/components/admin_console/select_team_modal.jsx b/webapp/components/admin_console/select_team_modal.jsx
new file mode 100644
index 000000000..ed6d33056
--- /dev/null
+++ b/webapp/components/admin_console/select_team_modal.jsx
@@ -0,0 +1,115 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ReactDOM from 'react-dom';
+import {FormattedMessage} from 'react-intl';
+
+import {Modal} from 'react-bootstrap';
+
+import React from 'react';
+
+export default class SelectTeamModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
+ render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ <FormattedMessage
+ id='admin.select_team.selectTeam'
+ defaultMessage='Select Team'
+ />
+ </Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ className='form-control'
+ >
+ {options}
+ </select>
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ <FormattedMessage
+ id='admin.select_team.close'
+ defaultMessage='Close'
+ />
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ <FormattedMessage
+ id='admin.select_team.select'
+ defaultMessage='Select'
+ />
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+}; \ No newline at end of file
diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx
new file mode 100644
index 000000000..881d22d76
--- /dev/null
+++ b/webapp/components/admin_console/service_settings.jsx
@@ -0,0 +1,984 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/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;
+
+ //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';
+ }
+
+ 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>
+
+ <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/sql_settings.jsx b/webapp/components/admin_console/sql_settings.jsx
new file mode 100644
index 000000000..33bb2cece
--- /dev/null
+++ b/webapp/components/admin_console/sql_settings.jsx
@@ -0,0 +1,390 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import crypto from 'crypto';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+const holders = defineMessages({
+ warning: {
+ id: 'admin.sql.warning',
+ defaultMessage: 'Warning: re-generating this salt may cause some columns in the database to return empty results.'
+ },
+ maxConnectionsExample: {
+ id: 'admin.sql.maxConnectionsExample',
+ defaultMessage: 'Ex "10"'
+ },
+ maxOpenExample: {
+ id: 'admin.sql.maxOpenExample',
+ defaultMessage: 'Ex "10"'
+ },
+ keyExample: {
+ id: 'admin.sql.keyExample',
+ defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ },
+ saving: {
+ id: 'admin.sql.saving',
+ defaultMessage: 'Saving Config...'
+ }
+});
+
+import React from 'react';
+
+class SqlSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange() {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked;
+ config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
+
+ if (config.SqlSettings.AtRestEncryptKey === '') {
+ config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
+ }
+
+ var MaxOpenConns = 10;
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
+ MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10);
+ }
+ config.SqlSettings.MaxOpenConns = MaxOpenConns;
+ ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
+
+ var MaxIdleConns = 10;
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
+ MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10);
+ }
+ config.SqlSettings.MaxIdleConns = MaxIdleConns;
+ ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+
+ var cfm = global.window.confirm(this.props.intl.formatMessage(holders.warning));
+ if (cfm === false) {
+ return;
+ }
+
+ ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ render() {
+ const {formatMessage} = this.props.intl;
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var saveClass = 'btn';
+ if (this.state.saveNeeded) {
+ saveClass = 'btn btn-primary';
+ }
+
+ var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
+
+ var dataSourceReplicas = '';
+ this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>
+ <FormattedMessage
+ id='admin.sql.noteTitle'
+ defaultMessage='Note:'
+ />
+ </h4>
+ <p>
+ <FormattedMessage
+ id='admin.sql.noteDescription'
+ defaultMessage='Changing properties in this section will require a server restart before taking effect.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <h3>
+ <FormattedMessage
+ id='admin.sql.title'
+ defaultMessage='SQL Settings'
+ />
+ </h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ <FormattedMessage
+ id='admin.sql.driverName'
+ defaultMessage='Driver Name:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ <FormattedMessage
+ id='admin.sql.dataSource'
+ defaultMessage='Data Source:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ <FormattedMessage
+ id='admin.sql.replicas'
+ defaultMessage='Data Source Replicas:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxIdleConns'
+ >
+ <FormattedMessage
+ id='admin.sql.maxConnectionsTitle'
+ defaultMessage='Maximum Idle Connections:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxIdleConns'
+ ref='MaxIdleConns'
+ placeholder={formatMessage(holders.maxConnectionsExample)}
+ defaultValue={this.props.config.SqlSettings.MaxIdleConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.sql.maxConnectionsDescription'
+ defaultMessage='Maximum number of idle connections held open to the database.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxOpenConns'
+ >
+ <FormattedMessage
+ id='admin.sql.maxOpenTitle'
+ defaultMessage='Maximum Open Connections:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxOpenConns'
+ ref='MaxOpenConns'
+ placeholder={formatMessage(holders.maxOpenExample)}
+ defaultValue={this.props.config.SqlSettings.MaxOpenConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.sql.maxOpenDescription'
+ defaultMessage='Maximum number of open connections held open to the database.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AtRestEncryptKey'
+ >
+ <FormattedMessage
+ id='admin.sql.keyTitle'
+ defaultMessage='At Rest Encrypt Key:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AtRestEncryptKey'
+ ref='AtRestEncryptKey'
+ placeholder={formatMessage(holders.keyExample)}
+ defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.sql.keyDescription'
+ defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
+ />
+ </p>
+ <div className='help-text'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleGenerate}
+ >
+ <FormattedMessage
+ id='admin.sql.regenerate'
+ defaultMessage='Re-Generate'
+ />
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Trace'
+ >
+ <FormattedMessage
+ id='admin.sql.traceTitle'
+ defaultMessage='Trace: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='true'
+ ref='Trace'
+ defaultChecked={this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.sql.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='false'
+ defaultChecked={!this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.sql.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.sql.traceDescription'
+ defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
+ >
+ <FormattedMessage
+ id='admin.sql.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+SqlSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(SqlSettings); \ No newline at end of file
diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx
new file mode 100644
index 000000000..654f0085d
--- /dev/null
+++ b/webapp/components/admin_console/team_settings.jsx
@@ -0,0 +1,420 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import ReactDOM from 'react-dom';
+import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+const holders = defineMessages({
+ siteNameExample: {
+ id: 'admin.team.siteNameExample',
+ defaultMessage: 'Ex "Mattermost"'
+ },
+ maxUsersExample: {
+ id: 'admin.team.maxUsersExample',
+ defaultMessage: 'Ex "25"'
+ },
+ restrictExample: {
+ id: 'admin.team.restrictExample',
+ defaultMessage: 'Ex "corp.mattermost.com, mattermost.org"'
+ },
+ saving: {
+ id: 'admin.team.saving',
+ defaultMessage: 'Saving Config...'
+ }
+});
+
+import React from 'react';
+
+class TeamSettings 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.TeamSettings.SiteName = ReactDOM.findDOMNode(this.refs.SiteName).value.trim();
+ config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
+ config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
+ config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
+ config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
+ config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked;
+
+ var MaxUsersPerTeam = 50;
+ if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
+ MaxUsersPerTeam = parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
+ }
+ config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
+ ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ serverError: null,
+ saveNeeded: false
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message,
+ saveNeeded: true
+ });
+ $('#save-button').button('reset');
+ }
+ );
+ }
+
+ 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';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <h3>
+ <FormattedMessage
+ id='admin.team.title'
+ defaultMessage='Team Settings'
+ />
+ </h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SiteName'
+ >
+ <FormattedMessage
+ id='admin.team.siteNameTitle'
+ defaultMessage='Site Name:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SiteName'
+ ref='SiteName'
+ placeholder={formatMessage(holders.siteNameExample)}
+ defaultValue={this.props.config.TeamSettings.SiteName}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.siteNameDescription'
+ defaultMessage='Name of service shown in login screens and UI.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxUsersPerTeam'
+ >
+ <FormattedMessage
+ id='admin.team.maxUsersTitle'
+ defaultMessage='Max Users Per Team:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxUsersPerTeam'
+ ref='MaxUsersPerTeam'
+ placeholder={formatMessage(holders.maxUsersExample)}
+ defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.maxUsersDescription'
+ defaultMessage='Maximum total number of users per team, including both active and inactive users.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamCreation'
+ >
+ <FormattedMessage
+ id='admin.team.teamCreationTitle'
+ defaultMessage='Enable Team Creation: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='true'
+ ref='EnableTeamCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.teamCreationDescription'
+ defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableUserCreation'
+ >
+ <FormattedMessage
+ id='admin.team.userCreationTitle'
+ defaultMessage='Enable User Creation: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='true'
+ ref='EnableUserCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.userCreationDescription'
+ defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictCreationToDomains'
+ >
+ <FormattedMessage
+ id='admin.team.restrictTitle'
+ defaultMessage='Restrict Creation To Domains:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='RestrictCreationToDomains'
+ ref='RestrictCreationToDomains'
+ placeholder={formatMessage(holders.restrictExample)}
+ defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.restrictDescription'
+ defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictTeamNames'
+ >
+ <FormattedMessage
+ id='admin.team.restrictNameTitle'
+ defaultMessage='Restrict Team Names: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='true'
+ ref='RestrictTeamNames'
+ defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.restrictNameDesc'
+ defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamListing'
+ >
+ <FormattedMessage
+ id='admin.team.dirTitle'
+ defaultMessage='Enable Team Directory: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='true'
+ ref='EnableTeamListing'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.team.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.team.dirDesc'
+ defaultMessage='When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'
+ />
+ </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.team.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ );
+ }
+}
+
+TeamSettings.propTypes = {
+ intl: intlShape.isRequired,
+ config: React.PropTypes.object
+};
+
+export default injectIntl(TeamSettings); \ No newline at end of file
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..1bf3f785b
--- /dev/null
+++ b/webapp/components/admin_console/team_users.jsx
@@ -0,0 +1,188 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from 'utils/client.jsx';
+import LoadingScreen from '../loading_screen.jsx';
+import UserItem from './user_item.jsx';
+import ResetPasswordModal from './reset_password_modal.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ 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.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.userList.title'
+ defaultMessage='Users for {team}'
+ values={{
+ team: this.props.team.name
+ }}
+ />
+ </h3>
+ {serverError}
+ <LoadingScreen/>
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>
+ <FormattedMessage
+ id='admin.userList.title2'
+ defaultMessage='Users for {team} ({count})'
+ values={{
+ team: this.props.team.name,
+ count: this.state.users.length
+ }}
+ />
+ </h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <table className='more-modal__list member-list-holder'>
+ <tbody>
+ {memberList}
+ </tbody>
+ </table>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..c6498eafc
--- /dev/null
+++ b/webapp/components/admin_console/user_item.jsx
@@ -0,0 +1,423 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from 'utils/client.jsx';
+import * as Utils from 'utils/utils.jsx';
+import UserStore from 'stores/user_store.jsx';
+import ConfirmModal from '../confirm_modal.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+ this.handleDemote = this.handleDemote.bind(this);
+ this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this);
+ this.handleDemoteCancel = this.handleDemoteCancel.bind(this);
+
+ this.state = {
+ serverError: null,
+ showDemoteModal: false,
+ user: null,
+ role: null
+ };
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const me = UserStore.getCurrentUser();
+ if (this.props.user.id === me.id) {
+ this.handleDemote(this.props.user, '');
+ } else {
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const me = UserStore.getCurrentUser();
+ if (this.props.user.id === me.id) {
+ this.handleDemote(this.props.user, 'admin');
+ } else {
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ handleDemote(user, role) {
+ this.setState({
+ serverError: this.state.serverError,
+ showDemoteModal: true,
+ user,
+ role
+ });
+ }
+
+ handleDemoteCancel() {
+ this.setState({
+ serverError: null,
+ showDemoteModal: false,
+ user: null,
+ role: null
+ });
+ }
+
+ handleDemoteSubmit() {
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: this.state.role
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.setState({
+ serverError: null,
+ showDemoteModal: false,
+ user: null,
+ role: null
+ });
+
+ const teamUrl = TeamStore.getCurrentTeamUrl();
+ if (teamUrl) {
+ window.location.href = teamUrl;
+ } else {
+ window.location.href = '/';
+ }
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.member'
+ defaultMessage='Member'
+ />
+ );
+ if (user.roles.length > 0) {
+ if (Utils.isSystemAdmin(user.roles)) {
+ currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.sysAdmin'
+ defaultMessage='System Admin'
+ />
+ );
+ } else if (Utils.isAdmin(user.roles)) {
+ currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.teamAdmin'
+ defaultMessage='Team Admin'
+ />
+ );
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.inactive'
+ defaultMessage='Inactive'
+ />
+ );
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeSysAdmin'
+ defaultMessage='Make System Admin'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeTeamAdmin'
+ defaultMessage='Make Team Admin'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeMember'
+ defaultMessage='Make Member'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeActive'
+ defaultMessage='Make Active'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeInactive'
+ defaultMessage='Make Inactive'
+ />
+ </a>
+ </li>
+ );
+ }
+ const me = UserStore.getCurrentUser();
+ let makeDemoteModal = null;
+ if (this.props.user.id === me.id) {
+ const title = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteRoleTitle'
+ defaultMessage='Confirm demotion from System Admin role'
+ />
+ );
+
+ const message = (
+ <div>
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteDescription'
+ defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
+ />
+ <br/>
+ <br/>
+ <FormattedMessage
+ id='admin.user_item.confirmDemotionCmd'
+ defaultMessage='platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
+ />
+ {serverError}
+ </div>
+ );
+
+ const confirmButton = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemotion'
+ defaultMessage='Confirm Demotion'
+ />
+ );
+
+ makeDemoteModal = (
+ <ConfirmModal
+ show={this.state.showDemoteModal}
+ title={title}
+ message={message}
+ confirmButton={confirmButton}
+ onConfirm={this.handleDemoteSubmit}
+ onCancel={this.handleDemoteCancel}
+ />
+ );
+ }
+
+ return (
+ <tr>
+ <td className='more-modal__row'>
+ <img
+ className='more-modal__image pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='more-modal__name'>{Utils.getDisplayName(user)}</span>
+ <span className='more-modal__description'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ <FormattedMessage
+ id='admin.user_item.resetPwd'
+ defaultMessage='Reset Password'
+ />
+ </a>
+ </li>
+ </ul>
+ </div>
+ {makeDemoteModal}
+ {serverError}
+ </td>
+ </tr>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};