merging files

This commit is contained in:
=Corey Hulen
2016-03-16 18:13:16 -07:00
4983 changed files with 16487 additions and 1611263 deletions

View File

@@ -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
};

View File

@@ -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>
);
}
}

View File

@@ -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
};

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 users 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);

View File

@@ -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);

View File

@@ -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 youre 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);

View File

@@ -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);

View File

@@ -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>
);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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
};