Merge branch 'master' into PLT-1429

Этот коммит содержится в:
=Corey Hulen
2016-02-03 10:45:58 -08:00
родитель 581785f504 9bba6c7988
Коммит bdfa4715d6
74 изменённых файлов: 2924 добавлений и 866 удалений

Просмотреть файл

@@ -1,204 +1,24 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import LoadingScreen from './loading_screen.jsx';
import AuditTable from './audit_table.jsx';
import UserStore from '../stores/user_store.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
sessionRevoked: {
id: 'access_history.sessionRevoked',
defaultMessage: 'The session with id {sessionId} was revoked'
},
channelCreated: {
id: 'access_history.channelCreated',
defaultMessage: 'Created the {channelName} channel/group'
},
establishedDM: {
id: 'access_history.establishedDM',
defaultMessage: 'Established a direct message channel with {username}'
},
nameUpdated: {
id: 'access_history.nameUpdated',
defaultMessage: 'Updated the {channelName} channel/group name'
},
headerUpdated: {
id: 'access_history.headerUpdated',
defaultMessage: 'Updated the {channelName} channel/group header'
},
channelDeleted: {
id: 'access_history.channelDeleted',
defaultMessage: 'Deleted the channel/group with the URL {url}'
},
userAdded: {
id: 'access_history.userAdded',
defaultMessage: 'Added {username} to the {channelName} channel/group'
},
userRemoved: {
id: 'access_history.userRemoved',
defaultMessage: 'Removed {username} to the {channelName} channel/group'
},
attemptedRegisterApp: {
id: 'access_history.attemptedRegisterApp',
defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
},
attemptedAllowOAuthAccess: {
id: 'access_history.attemptedAllowOAuthAccess',
defaultMessage: 'Attempted to allow a new OAuth service access'
},
successfullOAuthAccess: {
id: 'access_history.successfullOAuthAccess',
defaultMessage: 'Successfully gave a new OAuth service access'
},
failedOAuthAccess: {
id: 'access_history.failedOAuthAccess',
defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
},
attemptedOAuthToken: {
id: 'access_history.attemptedOAuthToken',
defaultMessage: 'Attempted to get an OAuth access token'
},
successfullOAuthToken: {
id: 'access_history.successfullOAuthToken',
defaultMessage: 'Successfully added a new OAuth service'
},
oauthTokenFailed: {
id: 'access_history.oauthTokenFailed',
defaultMessage: 'Failed to get an OAuth access token - {token}'
},
attemptedLogin: {
id: 'access_history.attemptedLogin',
defaultMessage: 'Attempted to login'
},
successfullLogin: {
id: 'access_history.successfullLogin',
defaultMessage: 'Successfully logged in'
},
failedLogin: {
id: 'access_history.failedLogin',
defaultMessage: 'FAILED login attempt'
},
updatePicture: {
id: 'access_history.updatePicture',
defaultMessage: 'Updated your profile picture'
},
updateGeneral: {
id: 'access_history.updateGeneral',
defaultMessage: 'Updated the general settings of your account'
},
attemptedPassword: {
id: 'access_history.attemptedPassword',
defaultMessage: 'Attempted to change password'
},
successfullPassword: {
id: 'access_history.successfullPassword',
defaultMessage: 'Successfully changed password'
},
failedPassword: {
id: 'access_history.failedPassword',
defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
},
updatedRol: {
id: 'access_history.updatedRol',
defaultMessage: 'Updated user role(s) to '
},
member: {
id: 'access_history.member',
defaultMessage: 'member'
},
accountActive: {
id: 'access_history.accountActive',
defaultMessage: 'Account made active'
},
accountInactive: {
id: 'access_history.accountInactive',
defaultMessage: 'Account made inactive'
},
by: {
id: 'access_history.by',
defaultMessage: ' by {username}'
},
byAdmin: {
id: 'access_history.byAdmin',
defaultMessage: ' by an admin'
},
sentEmail: {
id: 'access_history.sentEmail',
defaultMessage: 'Sent an email to {email} to reset your password'
},
attemptedReset: {
id: 'access_history.attemptedReset',
defaultMessage: 'Attempted to reset password'
},
successfullReset: {
id: 'access_history.successfullReset',
defaultMessage: 'Successfully reset password'
},
updateGlobalNotifications: {
id: 'access_history.updateGlobalNotifications',
defaultMessage: 'Updated your global notification settings'
},
attemptedWebhookCreate: {
id: 'access_history.attemptedWebhookCreate',
defaultMessage: 'Attempted to create a webhook'
},
succcessfullWebhookCreate: {
id: 'access_history.successfullWebhookCreate',
defaultMessage: 'Successfully created a webhook'
},
failedWebhookCreate: {
id: 'access_history.failedWebhookCreate',
defaultMessage: 'Failed to create a webhook - bad channel permissions'
},
attemptedWebhookDelete: {
id: 'access_history.attemptedWebhookDelete',
defaultMessage: 'Attempted to delete a webhook'
},
successfullWebhookDelete: {
id: 'access_history.successfullWebhookDelete',
defaultMessage: 'Successfully deleted a webhook'
},
failedWebhookDelete: {
id: 'access_history.failedWebhookDelete',
defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
},
logout: {
id: 'access_history.logout',
defaultMessage: 'Logged out of your account'
},
verified: {
id: 'access_history.verified',
defaultMessage: 'Sucessfully verified your email address'
},
revokedAll: {
id: 'access_history.revokedAll',
defaultMessage: 'Revoked all current sessions for the team'
},
loginAttempt: {
id: 'access_history.loginAttempt',
defaultMessage: ' (Login attempt)'
},
loginFailure: {
id: 'access_history.loginFailure',
defaultMessage: ' (Login failure)'
}
});
import {intlShape, injectIntl, FormattedMessage} from 'mm-intl';
class AccessHistoryModal extends React.Component {
constructor(props) {
super(props);
this.onAuditChange = this.onAuditChange.bind(this);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
this.formatAuditInfo = this.formatAuditInfo.bind(this);
this.handleRevokedSession = this.handleRevokedSession.bind(this);
const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
@@ -245,359 +65,17 @@ class AccessHistoryModal extends React.Component {
this.setState(newState);
}
}
handleMoreInfo(index) {
var newMoreInfo = this.state.moreInfo;
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
handleRevokedSession(sessionId) {
return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
}
formatAuditInfo(currentAudit) {
const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
const {formatMessage} = this.props.intl;
let currentAuditDesc = '';
if (currentActionURL.indexOf('/channels') === 0) {
const channelInfo = currentAudit.extra_info.split(' ');
const channelNameField = channelInfo[0].split('=');
let channelURL = '';
let channelObj;
let channelName = '';
if (channelNameField.indexOf('name') >= 0) {
channelURL = channelNameField[channelNameField.indexOf('name') + 1];
channelObj = ChannelStore.getByName(channelURL);
if (channelObj) {
channelName = channelObj.display_name;
} else {
channelName = channelURL;
}
}
switch (currentActionURL) {
case '/channels/create':
currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
break;
case '/channels/create_direct':
currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
break;
case '/channels/update':
currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
break;
case '/channels/update_desc': // support the old path
case '/channels/update_header':
currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
break;
default: {
let userIdField = [];
let userId = '';
let username = '';
if (channelInfo[1]) {
userIdField = channelInfo[1].split('=');
if (userIdField.indexOf('user_id') >= 0) {
userId = userIdField[userIdField.indexOf('user_id') + 1];
username = UserStore.getProfile(userId).username;
}
}
if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
} else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
} else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
}
break;
}
}
} else if (currentActionURL.indexOf('/oauth') === 0) {
const oauthInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/oauth/register': {
const clientIdField = oauthInfo[0].split('=');
if (clientIdField[0] === 'client_id') {
currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
}
break;
}
case '/oauth/allow':
if (oauthInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
} else if (oauthInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
} else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
currentAuditDesc = formatMessage(holders.failedOAuthAccess);
}
break;
case '/oauth/access_token':
if (oauthInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
} else if (oauthInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullOAuthToken);
} else {
const oauthTokenFailure = oauthInfo[0].split('-');
if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
}
}
break;
default:
break;
}
} else if (currentActionURL.indexOf('/users') === 0) {
const userInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/users/login':
if (userInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedLogin);
} else if (userInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullLogin);
} else if (userInfo[0]) {
currentAuditDesc = formatMessage(holders.failedLogin);
}
break;
case '/users/revoke_session':
currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
break;
case '/users/newimage':
currentAuditDesc = formatMessage(holders.updatePicture);
break;
case '/users/update':
currentAuditDesc = formatMessage(holders.updateGeneral);
break;
case '/users/newpassword':
if (userInfo[0] === 'attempted') {
currentAuditDesc = formatMessage(holders.attemptedPassword);
} else if (userInfo[0] === 'completed') {
currentAuditDesc = formatMessage(holders.successfullPassword);
} else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
currentAuditDesc = formatMessage(holders.failedPassword);
}
break;
case '/users/update_roles': {
const userRoles = userInfo[0].split('=')[1];
currentAuditDesc = formatMessage(holders.updatedRol);
if (userRoles.trim()) {
currentAuditDesc += userRoles;
} else {
currentAuditDesc += formatMessage(holders.member);
}
break;
}
case '/users/update_active': {
const updateType = userInfo[0].split('=')[0];
const updateField = userInfo[0].split('=')[1];
/* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
if (updateType === 'active') {
if (updateField === 'true') {
currentAuditDesc = formatMessage(holders.accountActive);
} else if (updateField === 'false') {
currentAuditDesc = formatMessage(holders.accountInactive);
}
const actingUserInfo = userInfo[1].split('=');
if (actingUserInfo[0] === 'session_user') {
const actingUser = UserStore.getProfile(actingUserInfo[1]);
const currentUser = UserStore.getCurrentUser();
if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
} else if (currentUser && actingUser) {
currentAuditDesc += formatMessage(holders.byAdmin);
}
}
} else if (updateType === 'session_id') {
currentAuditDesc = this.handleRevokedSession(updateField);
}
break;
}
case '/users/send_password_reset':
currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
break;
case '/users/reset_password':
if (userInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedReset);
} else if (userInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullReset);
}
break;
case '/users/update_notify':
currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
break;
default:
break;
}
} else if (currentActionURL.indexOf('/hooks') === 0) {
const webhookInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/hooks/incoming/create':
if (webhookInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
} else if (webhookInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
} else if (webhookInfo[0] === 'fail - bad channel permissions') {
currentAuditDesc = formatMessage(holders.failedWebhookCreate);
}
break;
case '/hooks/incoming/delete':
if (webhookInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
} else if (webhookInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
} else if (webhookInfo[0] === 'fail - inappropriate conditions') {
currentAuditDesc = formatMessage(holders.failedWebhookDelete);
}
break;
default:
break;
}
} else {
switch (currentActionURL) {
case '/logout':
currentAuditDesc = formatMessage(holders.logout);
break;
case '/verify_email':
currentAuditDesc = formatMessage(holders.verified);
break;
default:
break;
}
}
/* If all else fails... */
if (!currentAuditDesc) {
/* Currently not called anywhere */
if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
currentAuditDesc = formatMessage(holders.revokedAll);
} else {
let currentActionDesc = '';
if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
currentActionDesc = Utils.toTitleCase(currentActionDesc);
}
let currentExtraInfoDesc = '';
if (currentAudit.extra_info) {
currentExtraInfoDesc = currentAudit.extra_info;
if (currentExtraInfoDesc.indexOf('=') !== -1) {
currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
}
}
currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
}
}
const currentDate = new Date(currentAudit.create_at);
const currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' +
currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
return currentAuditInfo;
}
render() {
var accessList = [];
const {formatMessage} = this.props.intl;
for (var i = 0; i < this.state.audits.length; i++) {
const currentAudit = this.state.audits[i];
const currentAuditInfo = this.formatAuditInfo(currentAudit);
var moreInfo = (
<a
href='#'
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
<FormattedMessage
id='access_history.moreInfo'
defaultMessage='More info'
/>
</a>
);
if (this.state.moreInfo[i]) {
if (!currentAudit.session_id) {
currentAudit.session_id = 'N/A';
if (currentAudit.action.search('/users/login') >= 0) {
if (currentAudit.extra_info === 'attempt') {
currentAudit.session_id += formatMessage(holders.loginAttempt);
} else {
currentAudit.session_id += formatMessage(holders.loginFailure);
}
}
}
moreInfo = (
<div>
<div>
<FormattedMessage
id='access_history.ip'
defaultMessage='IP: {ip}'
values={{
ip: currentAudit.ip_address
}}
/>
</div>
<div>
<FormattedMessage
id='access_history.session'
defaultMessage='Session ID: {id}'
values={{
id: currentAudit.session_id
}}
/>
</div>
</div>
);
}
var divider = null;
if (i < this.state.audits.length - 1) {
divider = (<div className='divider-light'></div>);
}
accessList[i] = (
<div
key={'accessHistoryEntryKey' + i}
className='access-history__table'
>
<div className='access__report'>
<div className='report__time'>{currentAuditInfo}</div>
<div className='report__info'>
{moreInfo}
</div>
{divider}
</div>
</div>
);
}
var content;
if (this.state.audits.loading) {
content = (<LoadingScreen />);
} else {
content = (<form role='form'>{accessList}</form>);
content = (
<AuditTable
audits={this.state.audits}
moreInfo={this.state.moreInfo}
/>
);
}
return (
@@ -628,4 +106,4 @@ AccessHistoryModal.propTypes = {
onHide: React.PropTypes.func.isRequired
};
export default injectIntl(AccessHistoryModal);
export default injectIntl(AccessHistoryModal);

Просмотреть файл

@@ -11,6 +11,7 @@ import * as Utils from '../../utils/utils.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';
@@ -138,6 +139,8 @@ export default class AdminController extends React.Component {
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') {

Просмотреть файл

@@ -214,6 +214,24 @@ export default class AdminSidebar extends React.Component {
);
}
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='Audits'
/>
</a>
</li>
);
}
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component {
/>
</a>
</li>
{audits}
</ul>
</li>
</ul>

Просмотреть файл

@@ -4,11 +4,60 @@
import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
import DoughnutChart from './doughnut_chart.jsx';
import StatisticCount from './statistic_count.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
import {FormattedMessage} from 'mm-intl';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
analyticsTotalUsers: {
id: 'admin.analytics.totalUsers',
defaultMessage: 'Total Users'
},
analyticsPublicChannels: {
id: 'admin.analytics.publicChannels',
defaultMessage: 'Public Channels'
},
analyticsPrivateGroups: {
id: 'admin.analytics.privateGroups',
defaultMessage: 'Private Groups'
},
analyticsTotalPosts: {
id: 'admin.analytics.totalPosts',
defaultMessage: 'Total Posts'
},
analyticsFilePosts: {
id: 'admin.analytics.totalFilePosts',
defaultMessage: 'Posts with Files'
},
analyticsHashtagPosts: {
id: 'admin.analytics.totalHashtagPosts',
defaultMessage: 'Posts with Hashtags'
},
analyticsIncomingHooks: {
id: 'admin.analytics.totalIncomingWebhooks',
defaultMessage: 'Incoming Webhooks'
},
analyticsOutgoingHooks: {
id: 'admin.analytics.totalOutgoingWebhooks',
defaultMessage: 'Outgoing Webhooks'
},
analyticsChannelTypes: {
id: 'admin.analytics.channelTypes',
defaultMessage: 'Channel Types'
},
analyticsTextPosts: {
id: 'admin.analytics.textPosts',
defaultMessage: 'Posts with Text-only'
},
analyticsPostTypes: {
id: 'admin.analytics.postTypes',
defaultMessage: 'Posts, Files and Hashtags'
}
});
export default class Analytics extends React.Component {
constructor(props) {
@@ -18,6 +67,8 @@ export default class Analytics extends React.Component {
}
render() { // in the future, break down these into smaller components
const {formatMessage} = this.props.intl;
var serverError = '';
if (this.props.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
@@ -30,77 +81,129 @@ export default class Analytics extends React.Component {
/>
);
var totalCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalUsers'
defaultMessage='Total Users'
/>
<i className='fa fa-users'/></div>
<div className='content'>{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}</div>
let firstRow;
let extraGraphs;
if (this.props.showAdvanced) {
firstRow = (
<div className='row'>
<StatisticCount
title={formatMessage(holders.analyticsTotalUsers)}
icon='fa-users'
count={this.props.uniqueUserCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsTotalPosts)}
icon='fa-comment'
count={this.props.postCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsIncomingHooks)}
icon='fa-arrow-down'
count={this.props.incomingWebhookCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsOutgoingHooks)}
icon='fa-arrow-up'
count={this.props.outgoingWebhookCount}
/>
</div>
</div>
);
);
var openChannelCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.publicChannels'
defaultMessage='Public Channels'
/>
<i className='fa fa-globe'/></div>
<div className='content'>{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}</div>
const channelTypeData = [
{
value: this.props.channelOpenCount,
color: '#46BFBD',
highlight: '#5AD3D1',
label: formatMessage(holders.analyticsPublicChannels)
},
{
value: this.props.channelPrivateCount,
color: '#FDB45C',
highlight: '#FFC870',
label: formatMessage(holders.analyticsPrivateGroups)
}
];
const postTypeData = [
{
value: this.props.filePostCount,
color: '#46BFBD',
highlight: '#5AD3D1',
label: formatMessage(holders.analyticsFilePosts)
},
{
value: this.props.filePostCount,
color: '#F7464A',
highlight: '#FF5A5E',
label: formatMessage(holders.analyticsHashtagPosts)
},
{
value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount,
color: '#FDB45C',
highlight: '#FFC870',
label: formatMessage(holders.analyticsTextPosts)
}
];
extraGraphs = (
<div className='row'>
<DoughnutChart
title={formatMessage(holders.analyticsChannelTypes)}
data={channelTypeData}
width='300'
height='225'
/>
<DoughnutChart
title={formatMessage(holders.analyticsPostTypes)}
data={postTypeData}
width='300'
height='225'
/>
</div>
</div>
);
var openPrivateCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.privateGroups'
defaultMessage='Private Groups'
/>
<i className='fa fa-lock'/></div>
<div className='content'>{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}</div>
);
} else {
firstRow = (
<div className='row'>
<StatisticCount
title={formatMessage(holders.analyticsTotalUsers)}
icon='fa-users'
count={this.props.uniqueUserCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsPublicChannels)}
icon='fa-globe'
count={this.props.channelOpenCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsPrivateGroups)}
icon='fa-lock'
count={this.props.channelPrivateCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsTotalPosts)}
icon='fa-comment'
count={this.props.postCount}
/>
</div>
</div>
);
);
}
var postCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
<i className='fa fa-comment'/></div>
<div className='content'>{this.props.postCount == null ? loading : this.props.postCount}</div>
</div>
</div>
);
var postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
let postCountsByDay;
if (this.props.postCountsDay == null) {
postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
</div>
<div className='content'>{loading}</div>
</div>
<div className='content'>{loading}</div>
</div>
</div>
);
if (this.props.postCountsDay != null) {
);
} else {
let content;
if (this.props.postCountsDay.labels.length === 0) {
content = (
@@ -137,21 +240,22 @@ export default class Analytics extends React.Component {
);
}
var usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.activeUsers'
defaultMessage='Active Users With Posts'
/>
let usersWithPostsByDay;
if (this.props.userCountsWithPostsDay == null) {
usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.activeUsers'
defaultMessage='Active Users With Posts'
/>
</div>
<div className='content'>{loading}</div>
</div>
<div className='content'>{loading}</div>
</div>
</div>
);
if (this.props.userCountsWithPostsDay != null) {
);
} else {
let content;
if (this.props.userCountsWithPostsDay.labels.length === 0) {
content = (
@@ -312,12 +416,8 @@ export default class Analytics extends React.Component {
/>
</h3>
{serverError}
<div className='row'>
{totalCount}
{postCount}
{openChannelCount}
{openPrivateCount}
</div>
{firstRow}
{extraGraphs}
<div className='row'>
{postCountsByDay}
</div>
@@ -347,10 +447,16 @@ Analytics.defaultProps = {
};
Analytics.propTypes = {
intl: intlShape.isRequired,
title: React.PropTypes.string,
channelOpenCount: React.PropTypes.number,
channelPrivateCount: React.PropTypes.number,
postCount: React.PropTypes.number,
showAdvanced: React.PropTypes.bool,
filePostCount: React.PropTypes.number,
hashtagPostCount: React.PropTypes.number,
incomingWebhookCount: React.PropTypes.number,
outgoingWebhookCount: React.PropTypes.number,
postCountsDay: React.PropTypes.object,
userCountsWithPostsDay: React.PropTypes.object,
recentActiveUsers: React.PropTypes.array,
@@ -358,3 +464,5 @@ Analytics.propTypes = {
uniqueUserCount: React.PropTypes.number,
serverError: React.PropTypes.string
};
export default injectIntl(Analytics);

Просмотреть файл

@@ -0,0 +1,94 @@
// 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 AdminStore from '../../stores/admin_store.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
import {FormattedMessage} from 'mm-intl';
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}
oneLine={true}
showUserId={true}
/>
</div>
);
}
return (
<div className='panel'>
<h3>
<FormattedMessage
id='admin.audits.title'
defaultMessage='Server Audits'
/>
</h3>
<button
type='submit'
className='btn btn-primary'
onClick={this.reload}
>
<FormattedMessage
id='admin.audits.reload'
defaultMessage='Reload'
/>
</button>
<div className='log__panel'>
{content}
</div>
</div>
);
}
}

Просмотреть файл

@@ -0,0 +1,77 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
export default class DoughnutChart extends React.Component {
constructor(props) {
super(props);
this.initChart = this.initChart.bind(this);
this.chart = null;
}
componentDidMount() {
this.initChart(this.props);
}
componentWillReceiveProps(nextProps) {
if (this.chart) {
this.chart.destroy();
this.initChart(nextProps);
}
}
componentWillUnmount() {
if (this.chart) {
this.chart.destroy();
}
}
initChart(props) {
var el = ReactDOM.findDOMNode(this.refs.canvas);
var ctx = el.getContext('2d');
this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap
}
render() {
let content;
if (this.props.data == null) {
content = (
<FormattedMessage
id='admin.analytics.loading'
defaultMessage='Loading...'
/>
);
} else {
content = (
<canvas
ref='canvas'
width={this.props.width}
height={this.props.height}
/>
);
}
return (
<div className='col-sm-6'>
<div className='total-count'>
<div className='title'>
{this.props.title}
</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
}
DoughnutChart.propTypes = {
title: React.PropTypes.string,
width: React.PropTypes.string,
height: React.PropTypes.string,
data: React.PropTypes.array,
options: React.PropTypes.object
};

Просмотреть файл

@@ -112,6 +112,8 @@ class EmailSettings extends React.Component {
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;
@@ -317,6 +319,88 @@ class EmailSettings extends React.Component {
</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')}
/>
{'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')}
/>
{'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')}
/>
{'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')}
/>
{'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'

Просмотреть файл

@@ -0,0 +1,37 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
export default class StatisticCount extends React.Component {
constructor(props) {
super(props);
}
render() {
let loading = (
<FormattedMessage
id='admin.analytics.loading'
defaultMessage='Loading...'
/>
);
return (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
{this.props.title}
<i className={'fa ' + this.props.icon}/>
</div>
<div className='content'>{this.props.count == null ? loading : this.props.count}</div>
</div>
</div>
);
}
}
StatisticCount.propTypes = {
title: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
count: React.PropTypes.number
};

Просмотреть файл

@@ -140,6 +140,34 @@ class SystemAnalytics extends React.Component {
this.setState({serverError: err.message});
}
);
if (global.window.mm_license.IsLicensed === 'true') {
Client.getSystemAnalytics(
'extra_counts',
(data) => {
for (var index in data) {
if (data[index].name === 'file_post_count') {
this.setState({file_post_count: data[index].value});
}
if (data[index].name === 'hashtag_post_count') {
this.setState({hashtag_post_count: data[index].value});
}
if (data[index].name === 'incoming_webhook_count') {
this.setState({incoming_webhook_count: data[index].value});
}
if (data[index].name === 'outgoing_webhook_count') {
this.setState({outgoing_webhook_count: data[index].value});
}
}
},
(err) => {
this.setState({serverError: err.message});
}
);
}
}
componentWillReceiveProps() {
@@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component {
return (
<div>
<Analytics
intl={this.props.intl}
title={this.props.intl.formatMessage(labels.title)}
channelOpenCount={this.state.channel_open_count}
channelPrivateCount={this.state.channel_private_count}
postCount={this.state.post_count}
showAdvanced={global.window.mm_license.IsLicensed === 'true'}
filePostCount={this.state.file_post_count}
hashtagPostCount={this.state.hashtag_post_count}
incomingWebhookCount={this.state.incoming_webhook_count}
outgoingWebhookCount={this.state.outgoing_webhook_count}
postCountsDay={this.state.post_counts_day}
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
uniqueUserCount={this.state.unique_user_count}
@@ -179,4 +213,4 @@ SystemAnalytics.propTypes = {
team: React.PropTypes.object
};
export default injectIntl(SystemAnalytics);
export default injectIntl(SystemAnalytics);

Просмотреть файл

@@ -227,6 +227,7 @@ class TeamAnalytics extends React.Component {
return (
<div>
<Analytics
intl={this.props.intl}
title={this.props.team.name}
users={this.state.users}
channelOpenCount={this.state.channel_open_count}
@@ -249,4 +250,4 @@ TeamAnalytics.propTypes = {
team: React.PropTypes.object
};
export default injectIntl(TeamAnalytics);
export default injectIntl(TeamAnalytics);

571
web/react/components/audit_table.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,571 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
sessionRevoked: {
id: 'audit_table.sessionRevoked',
defaultMessage: 'The session with id {sessionId} was revoked'
},
channelCreated: {
id: 'audit_table.channelCreated',
defaultMessage: 'Created the {channelName} channel/group'
},
establishedDM: {
id: 'audit_table.establishedDM',
defaultMessage: 'Established a direct message channel with {username}'
},
nameUpdated: {
id: 'audit_table.nameUpdated',
defaultMessage: 'Updated the {channelName} channel/group name'
},
headerUpdated: {
id: 'audit_table.headerUpdated',
defaultMessage: 'Updated the {channelName} channel/group header'
},
channelDeleted: {
id: 'audit_table.channelDeleted',
defaultMessage: 'Deleted the channel/group with the URL {url}'
},
userAdded: {
id: 'audit_table.userAdded',
defaultMessage: 'Added {username} to the {channelName} channel/group'
},
userRemoved: {
id: 'audit_table.userRemoved',
defaultMessage: 'Removed {username} to the {channelName} channel/group'
},
attemptedRegisterApp: {
id: 'audit_table.attemptedRegisterApp',
defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
},
attemptedAllowOAuthAccess: {
id: 'audit_table.attemptedAllowOAuthAccess',
defaultMessage: 'Attempted to allow a new OAuth service access'
},
successfullOAuthAccess: {
id: 'audit_table.successfullOAuthAccess',
defaultMessage: 'Successfully gave a new OAuth service access'
},
failedOAuthAccess: {
id: 'audit_table.failedOAuthAccess',
defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
},
attemptedOAuthToken: {
id: 'audit_table.attemptedOAuthToken',
defaultMessage: 'Attempted to get an OAuth access token'
},
successfullOAuthToken: {
id: 'audit_table.successfullOAuthToken',
defaultMessage: 'Successfully added a new OAuth service'
},
oauthTokenFailed: {
id: 'audit_table.oauthTokenFailed',
defaultMessage: 'Failed to get an OAuth access token - {token}'
},
attemptedLogin: {
id: 'audit_table.attemptedLogin',
defaultMessage: 'Attempted to login'
},
successfullLogin: {
id: 'audit_table.successfullLogin',
defaultMessage: 'Successfully logged in'
},
failedLogin: {
id: 'audit_table.failedLogin',
defaultMessage: 'FAILED login attempt'
},
updatePicture: {
id: 'audit_table.updatePicture',
defaultMessage: 'Updated your profile picture'
},
updateGeneral: {
id: 'audit_table.updateGeneral',
defaultMessage: 'Updated the general settings of your account'
},
attemptedPassword: {
id: 'audit_table.attemptedPassword',
defaultMessage: 'Attempted to change password'
},
successfullPassword: {
id: 'audit_table.successfullPassword',
defaultMessage: 'Successfully changed password'
},
failedPassword: {
id: 'audit_table.failedPassword',
defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
},
updatedRol: {
id: 'audit_table.updatedRol',
defaultMessage: 'Updated user role(s) to '
},
member: {
id: 'audit_table.member',
defaultMessage: 'member'
},
accountActive: {
id: 'audit_table.accountActive',
defaultMessage: 'Account made active'
},
accountInactive: {
id: 'audit_table.accountInactive',
defaultMessage: 'Account made inactive'
},
by: {
id: 'audit_table.by',
defaultMessage: ' by {username}'
},
byAdmin: {
id: 'audit_table.byAdmin',
defaultMessage: ' by an admin'
},
sentEmail: {
id: 'audit_table.sentEmail',
defaultMessage: 'Sent an email to {email} to reset your password'
},
attemptedReset: {
id: 'audit_table.attemptedReset',
defaultMessage: 'Attempted to reset password'
},
successfullReset: {
id: 'audit_table.successfullReset',
defaultMessage: 'Successfully reset password'
},
updateGlobalNotifications: {
id: 'audit_table.updateGlobalNotifications',
defaultMessage: 'Updated your global notification settings'
},
attemptedWebhookCreate: {
id: 'audit_table.attemptedWebhookCreate',
defaultMessage: 'Attempted to create a webhook'
},
succcessfullWebhookCreate: {
id: 'audit_table.successfullWebhookCreate',
defaultMessage: 'Successfully created a webhook'
},
failedWebhookCreate: {
id: 'audit_table.failedWebhookCreate',
defaultMessage: 'Failed to create a webhook - bad channel permissions'
},
attemptedWebhookDelete: {
id: 'audit_table.attemptedWebhookDelete',
defaultMessage: 'Attempted to delete a webhook'
},
successfullWebhookDelete: {
id: 'audit_table.successfullWebhookDelete',
defaultMessage: 'Successfully deleted a webhook'
},
failedWebhookDelete: {
id: 'audit_table.failedWebhookDelete',
defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
},
logout: {
id: 'audit_table.logout',
defaultMessage: 'Logged out of your account'
},
verified: {
id: 'audit_table.verified',
defaultMessage: 'Sucessfully verified your email address'
},
revokedAll: {
id: 'audit_table.revokedAll',
defaultMessage: 'Revoked all current sessions for the team'
},
loginAttempt: {
id: 'audit_table.loginAttempt',
defaultMessage: ' (Login attempt)'
},
loginFailure: {
id: 'audit_table.loginFailure',
defaultMessage: ' (Login failure)'
},
userId: {
id: 'audit_table.userId',
defaultMessage: 'User ID'
}
});
class AuditTable extends React.Component {
constructor(props) {
super(props);
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.formatAuditInfo = this.formatAuditInfo.bind(this);
this.handleRevokedSession = this.handleRevokedSession.bind(this);
this.state = {moreInfo: []};
}
handleMoreInfo(index) {
var newMoreInfo = this.state.moreInfo;
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
handleRevokedSession(sessionId) {
return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
}
formatAuditInfo(currentAudit) {
const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
const {formatMessage} = this.props.intl;
let currentAuditDesc = '';
if (currentActionURL.indexOf('/channels') === 0) {
const channelInfo = currentAudit.extra_info.split(' ');
const channelNameField = channelInfo[0].split('=');
let channelURL = '';
let channelObj;
let channelName = '';
if (channelNameField.indexOf('name') >= 0) {
channelURL = channelNameField[channelNameField.indexOf('name') + 1];
channelObj = ChannelStore.getByName(channelURL);
if (channelObj) {
channelName = channelObj.display_name;
} else {
channelName = channelURL;
}
}
switch (currentActionURL) {
case '/channels/create':
currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
break;
case '/channels/create_direct':
currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
break;
case '/channels/update':
currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
break;
case '/channels/update_desc': // support the old path
case '/channels/update_header':
currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
break;
default: {
let userIdField = [];
let userId = '';
let username = '';
if (channelInfo[1]) {
userIdField = channelInfo[1].split('=');
if (userIdField.indexOf('user_id') >= 0) {
userId = userIdField[userIdField.indexOf('user_id') + 1];
username = UserStore.getProfile(userId).username;
}
}
if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
} else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
} else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
}
break;
}
}
} else if (currentActionURL.indexOf('/oauth') === 0) {
const oauthInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/oauth/register': {
const clientIdField = oauthInfo[0].split('=');
if (clientIdField[0] === 'client_id') {
currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
}
break;
}
case '/oauth/allow':
if (oauthInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
} else if (oauthInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
} else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
currentAuditDesc = formatMessage(holders.failedOAuthAccess);
}
break;
case '/oauth/access_token':
if (oauthInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
} else if (oauthInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullOAuthToken);
} else {
const oauthTokenFailure = oauthInfo[0].split('-');
if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
}
}
break;
default:
break;
}
} else if (currentActionURL.indexOf('/users') === 0) {
const userInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/users/login':
if (userInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedLogin);
} else if (userInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullLogin);
} else if (userInfo[0]) {
currentAuditDesc = formatMessage(holders.failedLogin);
}
break;
case '/users/revoke_session':
currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
break;
case '/users/newimage':
currentAuditDesc = formatMessage(holders.updatePicture);
break;
case '/users/update':
currentAuditDesc = formatMessage(holders.updateGeneral);
break;
case '/users/newpassword':
if (userInfo[0] === 'attempted') {
currentAuditDesc = formatMessage(holders.attemptedPassword);
} else if (userInfo[0] === 'completed') {
currentAuditDesc = formatMessage(holders.successfullPassword);
} else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
currentAuditDesc = formatMessage(holders.failedPassword);
}
break;
case '/users/update_roles': {
const userRoles = userInfo[0].split('=')[1];
currentAuditDesc = formatMessage(holders.updatedRol);
if (userRoles.trim()) {
currentAuditDesc += userRoles;
} else {
currentAuditDesc += formatMessage(holders.member);
}
break;
}
case '/users/update_active': {
const updateType = userInfo[0].split('=')[0];
const updateField = userInfo[0].split('=')[1];
/* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
if (updateType === 'active') {
if (updateField === 'true') {
currentAuditDesc = formatMessage(holders.accountActive);
} else if (updateField === 'false') {
currentAuditDesc = formatMessage(holders.accountInactive);
}
const actingUserInfo = userInfo[1].split('=');
if (actingUserInfo[0] === 'session_user') {
const actingUser = UserStore.getProfile(actingUserInfo[1]);
const currentUser = UserStore.getCurrentUser();
if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
} else if (currentUser && actingUser) {
currentAuditDesc += formatMessage(holders.byAdmin);
}
}
} else if (updateType === 'session_id') {
currentAuditDesc = this.handleRevokedSession(updateField);
}
break;
}
case '/users/send_password_reset':
currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
break;
case '/users/reset_password':
if (userInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedReset);
} else if (userInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullReset);
}
break;
case '/users/update_notify':
currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
break;
default:
break;
}
} else if (currentActionURL.indexOf('/hooks') === 0) {
const webhookInfo = currentAudit.extra_info.split(' ');
switch (currentActionURL) {
case '/hooks/incoming/create':
if (webhookInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
} else if (webhookInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
} else if (webhookInfo[0] === 'fail - bad channel permissions') {
currentAuditDesc = formatMessage(holders.failedWebhookCreate);
}
break;
case '/hooks/incoming/delete':
if (webhookInfo[0] === 'attempt') {
currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
} else if (webhookInfo[0] === 'success') {
currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
} else if (webhookInfo[0] === 'fail - inappropriate conditions') {
currentAuditDesc = formatMessage(holders.failedWebhookDelete);
}
break;
default:
break;
}
} else {
switch (currentActionURL) {
case '/logout':
currentAuditDesc = formatMessage(holders.logout);
break;
case '/verify_email':
currentAuditDesc = formatMessage(holders.verified);
break;
default:
break;
}
}
/* If all else fails... */
if (!currentAuditDesc) {
/* Currently not called anywhere */
if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
currentAuditDesc = formatMessage(holders.revokedAll);
} else {
let currentActionDesc = '';
if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
currentActionDesc = Utils.toTitleCase(currentActionDesc);
}
let currentExtraInfoDesc = '';
if (currentAudit.extra_info) {
currentExtraInfoDesc = currentAudit.extra_info;
if (currentExtraInfoDesc.indexOf('=') !== -1) {
currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
}
}
currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
}
}
const currentDate = new Date(currentAudit.create_at);
let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
if (this.props.showUserId) {
currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
}
currentAuditInfo += ' | ' + currentAuditDesc;
return currentAuditInfo;
}
render() {
var accessList = [];
const {formatMessage} = this.props.intl;
for (var i = 0; i < this.props.audits.length; i++) {
const currentAudit = this.props.audits[i];
const currentAuditInfo = this.formatAuditInfo(currentAudit);
let moreInfo;
if (!this.props.oneLine) {
moreInfo = (
<a
href='#'
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
<FormattedMessage
id='audit_table.moreInfo'
defaultMessage='More info'
/>
</a>
);
}
if (this.state.moreInfo[i]) {
if (!currentAudit.session_id) {
currentAudit.session_id = 'N/A';
if (currentAudit.action.search('/users/login') >= 0) {
if (currentAudit.extra_info === 'attempt') {
currentAudit.session_id += formatMessage(holders.loginAttempt);
} else {
currentAudit.session_id += formatMessage(holders.loginFailure);
}
}
}
moreInfo = (
<div>
<div>
<FormattedMessage
id='audit_table.ip'
defaultMessage='IP: {ip}'
values={{
ip: currentAudit.ip_address
}}
/>
</div>
<div>
<FormattedMessage
id='audit_table.session'
defaultMessage='Session ID: {id}'
values={{
id: currentAudit.session_id
}}
/>
</div>
</div>
);
}
var divider = null;
if (i < this.props.audits.length - 1) {
divider = (<div className='divider-light'></div>);
}
accessList[i] = (
<div
key={'accessHistoryEntryKey' + i}
className='access-history__table'
>
<div className='access__report'>
<div className='report__time'>{currentAuditInfo}</div>
<div className='report__info'>
{moreInfo}
</div>
{divider}
</div>
</div>
);
}
return <form role='form'>{accessList}</form>;
}
}
AuditTable.propTypes = {
intl: intlShape.isRequired,
audits: React.PropTypes.array.isRequired,
oneLine: React.PropTypes.bool,
showUserId: React.PropTypes.bool
};
export default injectIntl(AuditTable);

Просмотреть файл

@@ -69,7 +69,7 @@ export default class CenterPanel extends React.Component {
onClick={handleClick}
>
<a href=''>
{'You are viewing the Archives. Click here to jump to recent messages. '}
{'Click here to jump to recent messages. '}
{<i className='fa fa-arrow-down'></i>}
</a>
</div>

Просмотреть файл

@@ -24,8 +24,10 @@ import * as TextFormatting from '../utils/text_formatting.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import {FormattedMessage} from 'mm-intl';
const ActionTypes = Constants.ActionTypes;
const Popover = ReactBootstrap.Popover;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
const Tooltip = ReactBootstrap.Tooltip;
@@ -124,7 +126,14 @@ export default class ChannelHeader extends React.Component {
}
const channel = this.state.channel;
const recentMentionsTooltip = <Tooltip id='recentMentionsTooltip'>{'Recent Mentions'}</Tooltip>;
const recentMentionsTooltip = (
<Tooltip id='recentMentionsTooltip'>
<FormattedMessage
id='channel_header.recentMentions'
defaultMessage='Recent Mentions'
/>
</Tooltip>
);
const popoverContent = (
<Popover
id='hader-popover'
@@ -157,9 +166,19 @@ export default class ChannelHeader extends React.Component {
}
}
let channelTerm = 'Channel';
if (channel.type === 'P') {
channelTerm = 'Group';
let channelTerm = (
<FormattedMessage
id='channel_header.channel'
defaultMessage='Channel'
/>
);
if (channel.type === Constants.PRIVATE_CHANNEL) {
channelTerm = (
<FormattedMessage
id='channel_header.group'
defaultMessage='Group'
/>
);
}
const dropdownContents = [];
@@ -174,7 +193,10 @@ export default class ChannelHeader extends React.Component {
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
>
{'Set Channel Header...'}
<FormattedMessage
id='channel_header.channelHeader'
defaultMessage='Set Channel Header...'
/>
</ToggleModalButton>
</li>
);
@@ -189,7 +211,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelInfoModal}
dialogProps={{channel}}
>
{'View Info'}
<FormattedMessage
id='channel_header.viewInfo'
defaultMessage='View Info'
/>
</ToggleModalButton>
</li>
);
@@ -205,7 +230,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelInviteModal}
dialogProps={{channel}}
>
{'Add Members'}
<FormattedMessage
id='chanel_header.addMembers'
defaultMessage='Add Members'
/>
</ToggleModalButton>
</li>
);
@@ -221,7 +249,10 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={() => this.setState({showMembersModal: true})}
>
{'Manage Members'}
<FormattedMessage
id='channel_header.manageMembers'
defaultMessage='Manage Members'
/>
</a>
</li>
);
@@ -238,7 +269,13 @@ export default class ChannelHeader extends React.Component {
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
>
{`Set ${channelTerm} Header...`}
<FormattedMessage
id='channel_header.setHeader'
defaultMessage='Set {term} Header...'
values={{
term: (channelTerm)
}}
/>
</ToggleModalButton>
</li>
);
@@ -252,7 +289,13 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
>
{'Set '}{channelTerm}{' Purpose...'}
<FormattedMessage
id='channel_header.setPurpose'
defaultMessage='Set {term} Purpose...'
values={{
term: (channelTerm)
}}
/>
</a>
</li>
);
@@ -266,7 +309,10 @@ export default class ChannelHeader extends React.Component {
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
>
{'Notification Preferences'}
<FormattedMessage
id='channel_header.notificationPreferences'
defaultMessage='Notification Preferences'
/>
</ToggleModalButton>
</li>
);
@@ -286,7 +332,13 @@ export default class ChannelHeader extends React.Component {
data-name={channel.name}
data-channelid={channel.id}
>
{'Rename '}{channelTerm}{'...'}
<FormattedMessage
id='channel_header.rename'
defaultMessage='Rename {term}...'
values={{
term: (channelTerm)
}}
/>
</a>
</li>
);
@@ -302,7 +354,13 @@ export default class ChannelHeader extends React.Component {
dialogType={DeleteChannelModal}
dialogProps={{channel}}
>
{'Delete '}{channelTerm}{'...'}
<FormattedMessage
id='channel_header.delete'
defaultMessage='Delete {term}...'
values={{
term: (channelTerm)
}}
/>
</ToggleModalButton>
</li>
);
@@ -320,7 +378,13 @@ export default class ChannelHeader extends React.Component {
href='#'
onClick={this.handleLeave}
>
{'Leave '}{channelTerm}
<FormattedMessage
id='channel_header.leave'
defaultMessage='Leave {term}'
values={{
term: (channelTerm)
}}
/>
</a>
</li>
);

Просмотреть файл

@@ -2,17 +2,28 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelInfoModal extends React.Component {
const holders = defineMessages({
notFound: {
id: 'channel_info.notFound',
defaultMessage: 'No Channel Found'
}
});
class ChannelInfoModal extends React.Component {
render() {
const {formatMessage} = this.props.intl;
let channel = this.props.channel;
if (!channel) {
channel = {
display_name: 'No Channel Found',
name: 'No Channel Found',
purpose: 'No Channel Found',
id: 'No Channel Found'
display_name: formatMessage(holders.notFound),
name: formatMessage(holders.notFound),
purpose: formatMessage(holders.notFound),
id: formatMessage(holders.notFound)
};
}
@@ -28,19 +39,39 @@ export default class ChannelInfoModal extends React.Component {
</Modal.Header>
<Modal.Body ref='modalBody'>
<div className='row form-group'>
<div className='col-sm-3 info__label'>{'Channel Name:'}</div>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='channel_info.name'
defaultMessage='Channel Name:'
/>
</div>
<div className='col-sm-9'>{channel.display_name}</div>
</div>
<div className='row form-group'>
<div className='col-sm-3 info__label'>{'Channel URL:'}</div>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='channel_info.url'
defaultMessage='Channel URL:'
/>
</div>
<div className='col-sm-9'>{channelURL}</div>
</div>
<div className='row'>
<div className='col-sm-3 info__label'>{'Channel ID:'}</div>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='channel_info.id'
defaultMessage='Channel ID:'
/>
</div>
<div className='col-sm-9'>{channel.id}</div>
</div>
<div className='row'>
<div className='col-sm-3 info__label'>{'Channel Purpose:'}</div>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='channel_info.purpose'
defaultMessage='Channel Purpose:'
/>
</div>
<div className='col-sm-9'>{channel.purpose}</div>
</div>
</Modal.Body>
@@ -50,7 +81,10 @@ export default class ChannelInfoModal extends React.Component {
className='btn btn-default'
onClick={this.props.onHide}
>
{'Close'}
<FormattedMessage
id='channel_info.close'
defaultMessage='Close'
/>
</button>
</Modal.Footer>
</Modal>
@@ -59,7 +93,10 @@ export default class ChannelInfoModal extends React.Component {
}
ChannelInfoModal.propTypes = {
intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
};
export default injectIntl(ChannelInfoModal);

Просмотреть файл

@@ -11,6 +11,8 @@ import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelInviteModal extends React.Component {
@@ -154,7 +156,13 @@ export default class ChannelInviteModal extends React.Component {
onHide={this.props.onHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Add New Members to '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
<Modal.Title>
<FormattedMessage
id='channel_invite.addNewMembers'
defaultMessage='Add New Members to '
/>
<span className='name'>{this.props.channel.display_name}</span>
</Modal.Title>
</Modal.Header>
<Modal.Body
ref='modalBody'
@@ -168,7 +176,10 @@ export default class ChannelInviteModal extends React.Component {
className='btn btn-default'
onClick={this.props.onHide}
>
{'Close'}
<FormattedMessage
id='channel_invite.close'
defaultMessage='Close'
/>
</button>
</Modal.Footer>
</Modal>

Просмотреть файл

@@ -12,6 +12,8 @@ import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import * as Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelMembersModal extends React.Component {
@@ -191,7 +193,13 @@ export default class ChannelMembersModal extends React.Component {
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
<Modal.Title><span className='name'>{this.props.channel.display_name}</span>{' Members'}</Modal.Title>
<Modal.Title>
<span className='name'>{this.props.channel.display_name}</span>
<FormattedMessage
id='channel_memebers_modal.members'
defaultMessage=' Members'
/>
</Modal.Title>
<a
className='btn btn-md btn-primary'
href='#'
@@ -200,7 +208,11 @@ export default class ChannelMembersModal extends React.Component {
this.props.onModalDismissed();
}}
>
<i className='glyphicon glyphicon-envelope'/>{' Add New Members'}
<i className='glyphicon glyphicon-envelope'/>
<FormattedMessage
id='channel_members_modal.addNew'
defaultMessage=' Add New Members'
/>
</a>
</Modal.Header>
<Modal.Body
@@ -215,7 +227,10 @@ export default class ChannelMembersModal extends React.Component {
className='btn btn-default'
onClick={this.props.onModalDismissed}
>
{'Close'}
<FormattedMessage
id='channel_members_modal.close'
defaultMessage='Close'
/>
</button>
</Modal.Footer>
</Modal>

Просмотреть файл

@@ -9,6 +9,8 @@ import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import {FormattedMessage} from 'mm-intl';
export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
@@ -97,13 +99,35 @@ export default class ChannelNotificationsModal extends React.Component {
let globalNotifyLevelName;
if (globalNotifyLevel === 'all') {
globalNotifyLevelName = 'For all activity';
globalNotifyLevelName = (
<FormattedMessage
id='channel_notifications.allActivity'
defaultMessage='For all activity'
/>
);
} else if (globalNotifyLevel === 'mention') {
globalNotifyLevelName = 'Only for mentions';
globalNotifyLevelName = (
<FormattedMessage
id='channel_notifications.onlyMentions'
defaultMessage='Only for mentions'
/>
);
} else {
globalNotifyLevelName = 'Never';
globalNotifyLevelName = (
<FormattedMessage
id='channel_notifications.never'
defaultMessage='Never'
/>
);
}
const sendDesktop = (
<FormattedMessage
id='channel_notifications.sendDesktop'
defaultMessage='Send desktop notifications'
/>
);
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false, false];
if (this.state.notifyLevel === 'default') {
@@ -127,7 +151,13 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[0]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
/>
{`Global default (${globalNotifyLevelName})`}
<FormattedMessage
id='channel_notifications.globalDefault'
defaultMessage='Global default ({notifyLevel}'
values={{
notifyLevel: (globalNotifyLevelName)
}}
/>
</label>
<br/>
</div>
@@ -138,7 +168,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[1]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
/>
{'For all activity'}
<FormattedMessage id='channel_notifications.allActivity' />
</label>
<br/>
</div>
@@ -149,7 +179,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[2]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
/>
{'Only for mentions'}
<FormattedMessage id='channel_notifications.onlyMentions' />
</label>
<br/>
</div>
@@ -160,7 +190,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
/>
{'Never'}
<FormattedMessage id='channel_notifications.never' />
</label>
</div>
</div>
@@ -174,13 +204,16 @@ export default class ChannelNotificationsModal extends React.Component {
const extraInfo = (
<span>
{'Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'}
<FormattedMessage
id='channel_notifications.override'
defaultMessage='Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'
/>
</span>
);
return (
<SettingItemMax
title='Send desktop notifications'
title={sendDesktop}
inputs={inputs}
submit={this.handleSubmitNotifyLevel}
server_error={serverError}
@@ -192,13 +225,20 @@ export default class ChannelNotificationsModal extends React.Component {
var describe;
if (this.state.notifyLevel === 'default') {
describe = `Global default (${globalNotifyLevelName})`;
describe = (
<FormattedMessage
id='channel_notifications.globalDefault'
values={{
notifyLevel: (globalNotifyLevelName)
}}
/>
);
} else if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
describe = (<FormattedMessage id='channel_notifications.onlyMentions' />);
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
describe = (<FormattedMessage id='channel_notifications.allActivity' />);
} else {
describe = 'Never';
describe = (<FormattedMessage id='channel_notifications.never' />);
}
handleUpdateSection = function updateSection(e) {
@@ -208,7 +248,7 @@ export default class ChannelNotificationsModal extends React.Component {
return (
<SettingItemMin
title='Send desktop notifications'
title={sendDesktop}
describe={describe}
updateSection={handleUpdateSection}
/>
@@ -250,6 +290,12 @@ export default class ChannelNotificationsModal extends React.Component {
createMarkUnreadLevelSection(serverError) {
let content;
const markUnread = (
<FormattedMessage
id='channel_notifications.markUnread'
defaultMessage='Mark Channel Unread'
/>
);
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
<div key='channel-notification-unread-radio'>
@@ -260,7 +306,10 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
{'For all unread messages'}
<FormattedMessage
id='channel_notifications.allUnread'
defaultMessage='For all unread messages'
/>
</label>
<br />
</div>
@@ -271,7 +320,7 @@ export default class ChannelNotificationsModal extends React.Component {
checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
/>
{'Only for mentions'}
<FormattedMessage id='channel_notifications.onlyMentions' />
</label>
<br />
</div>
@@ -284,11 +333,18 @@ export default class ChannelNotificationsModal extends React.Component {
e.preventDefault();
}.bind(this);
const extraInfo = <span>{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'}</span>;
const extraInfo = (
<span>
<FormattedMessage
id='channel_notifications.unreadInfo'
defaultMessage='The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'
/>
</span>
);
content = (
<SettingItemMax
title='Mark Channel Unread'
title={markUnread}
inputs={inputs}
submit={this.handleSubmitMarkUnreadLevel}
server_error={serverError}
@@ -300,9 +356,14 @@ export default class ChannelNotificationsModal extends React.Component {
let describe;
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
describe = 'For all unread messages';
describe = (
<FormattedMessage
id='channel_notifications.allUnread'
defaultMessage='For all unread messages'
/>
);
} else {
describe = 'Only for mentions';
describe = (<FormattedMessage id='channel_notifications.onlyMentions' />);
}
const handleUpdateSection = function handleUpdateSection(e) {
@@ -312,7 +373,7 @@ export default class ChannelNotificationsModal extends React.Component {
content = (
<SettingItemMin
title='Mark Channel Unread'
title={markUnread}
describe={describe}
updateSection={handleUpdateSection}
/>
@@ -335,7 +396,13 @@ export default class ChannelNotificationsModal extends React.Component {
onHide={this.props.onHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Notification Preferences for '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
<Modal.Title>
<FormattedMessage
id='channel_notifications.preferences'
defaultMessage='Notification Preferences for '
/>
<span className='name'>{this.props.channel.display_name}</span>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className='settings-table'>

Просмотреть файл

@@ -5,7 +5,9 @@ import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
const Modal = ReactBootstrap.Modal;
import TeamStore from '../stores/team_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -32,7 +34,20 @@ export default class DeleteChannelModal extends React.Component {
}
render() {
const channelTerm = Utils.getChannelTerm(this.props.channel.type).toLowerCase();
let channelTerm = (
<FormattedMessage
id='delete_channel.channel'
defaultMessage='channel'
/>
);
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
channelTerm = (
<FormattedMessage
id='delete_channel.group'
defaultMessage='group'
/>
);
}
return (
<Modal
@@ -40,10 +55,22 @@ export default class DeleteChannelModal extends React.Component {
onHide={this.props.onHide}
>
<Modal.Header closeButton={true}>
<h4 className='modal-title'>{'Confirm DELETE Channel'}</h4>
<h4 className='modal-title'>
<FormattedMessage
id='delete_channel.confirm'
defaultMessage='Confirm DELETE Channel'
/>
</h4>
</Modal.Header>
<Modal.Body>
{`Are you sure you wish to delete the ${this.props.channel.display_name} ${channelTerm}?`}
<FormattedMessage
id='delete_channel.question'
defaultMessage='Are you sure you wish to delete the {display_name} {term}?'
values={{
display_name: this.props.channel.display_name,
term: (channelTerm)
}}
/>
</Modal.Body>
<Modal.Footer>
<button
@@ -51,7 +78,10 @@ export default class DeleteChannelModal extends React.Component {
className='btn btn-default'
onClick={this.props.onHide}
>
{'Cancel'}
<FormattedMessage
id='delete_channel.cancel'
defaultMessage='Cancel'
/>
</button>
<button
type='button'
@@ -59,7 +89,10 @@ export default class DeleteChannelModal extends React.Component {
data-dismiss='modal'
onClick={this.handleDelete}
>
{'Delete'}
<FormattedMessage
id='delete_channel.del'
defaultMessage='Delete'
/>
</button>
</Modal.Footer>
</Modal>

Просмотреть файл

@@ -9,6 +9,9 @@ import * as Utils from '../utils/utils.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
var ActionTypes = Constants.ActionTypes;
export default class DeletePostModal extends React.Component {
@@ -128,10 +131,28 @@ export default class DeletePostModal extends React.Component {
var commentWarning = '';
if (this.state.commentCount > 0) {
commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
commentWarning = (
<FormattedMessage
id='delete_post.warning'
defaultMessage='This post has {count} comment(s) on it.'
values={{
count: this.state.commentCount
}}
/>
);
}
const postTerm = Utils.getPostTerm(this.state.post);
const postTerm = this.state.post.root_id ? (
<FormattedMessage
id='delete_post.comment'
defaultMessage='Comment'
/>
) : (
<FormattedMessage
id='delete_post.post'
defaultMessage='Post'
/>
);
return (
<Modal
@@ -139,10 +160,24 @@ export default class DeletePostModal extends React.Component {
onHide={this.handleHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>{`Confirm ${postTerm} Delete`}</Modal.Title>
<Modal.Title>
<FormattedMessage
id='delete_post.confirm'
defaultMessage='Confirm {term} Delete'
values={{
term: (postTerm)
}}
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
<FormattedMessage
id='delete_post.question'
defaultMessage='Are you sure you want to delete this ${term}?'
values={{
term: (postTerm)
}}
/>
<br />
<br />
{commentWarning}
@@ -154,7 +189,10 @@ export default class DeletePostModal extends React.Component {
className='btn btn-default'
onClick={this.handleHide}
>
{'Cancel'}
<FormattedMessage
id='delete_post.cancel'
defaultMessage='Cancel'
/>
</button>
<button
ref='deletePostBtn'
@@ -162,7 +200,10 @@ export default class DeletePostModal extends React.Component {
className='btn btn-danger'
onClick={this.handleDelete}
>
{'Delete'}
<FormattedMessage
id='delete_post.del'
defaultMessage='Delete'
/>
</button>
</Modal.Footer>
</Modal>

Просмотреть файл

@@ -6,9 +6,18 @@ import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class EditChannelHeaderModal extends React.Component {
const holders = defineMessages({
error: {
id: 'edit_channel_header_modal.error',
defaultMessage: 'This channel header is too long, please enter a shorter one'
}
});
class EditChannelHeaderModal extends React.Component {
constructor(props) {
super(props);
@@ -64,8 +73,8 @@ export default class EditChannelHeaderModal extends React.Component {
});
},
(err) => {
if (err.message === 'Invalid channel_header parameter') {
this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
if (err.id === 'api.context.invalid_param.app_error') {
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
} else {
this.setState({serverError: err.message});
}
@@ -99,10 +108,23 @@ export default class EditChannelHeaderModal extends React.Component {
onHide={this.onHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Edit Header for ' + this.props.channel.display_name}</Modal.Title>
<Modal.Title>
<FormattedMessage
id='edit_channel_header_modal.title'
defaultMessage='Edit Header for {channel}'
values={{
channel: this.props.channel.display_name
}}
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
<p>
<FormattedMessage
id='edit_channel_header_modal.description'
defaultMessage='Edit the text appearing next to the channel name in the channel header.'
/>
</p>
<textarea
ref='textarea'
className='form-control no-resize'
@@ -120,14 +142,20 @@ export default class EditChannelHeaderModal extends React.Component {
className='btn btn-default'
onClick={this.onHide}
>
{'Cancel'}
<FormattedMessage
id='edit_channel_header_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleSubmit}
>
{'Save'}
<FormattedMessage
id='edit_channel_header_modal.save'
defaultMessage='Save'
/>
</button>
</Modal.Footer>
</Modal>
@@ -136,7 +164,10 @@ export default class EditChannelHeaderModal extends React.Component {
}
EditChannelHeaderModal.propTypes = {
intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
};
export default injectIntl(EditChannelHeaderModal);

Просмотреть файл

@@ -3,10 +3,19 @@
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
const holders = defineMessages({
error: {
id: 'edit_channel_purpose_modal.error',
defaultMessage: 'This channel purpose is too long, please enter a shorter one'
}
});
export default class EditChannelPurposeModal extends React.Component {
constructor(props) {
super(props);
@@ -48,8 +57,8 @@ export default class EditChannelPurposeModal extends React.Component {
this.handleHide();
},
(err) => {
if (err.message === 'Invalid channel_purpose parameter') {
this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
if (err.id === 'api.context.invalid_param.app_error') {
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
} else {
this.setState({serverError: err.message});
}
@@ -72,9 +81,39 @@ export default class EditChannelPurposeModal extends React.Component {
);
}
let title = <span>{'Edit Purpose'}</span>;
let title = (
<span>
<FormattedMessage
id='edit_channel_purpose_modal.title1'
defaultMessage='Edit Purpose'
/>
</span>
);
if (this.props.channel.display_name) {
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
title = (
<span>
<FormattedMessage
id='edit_channel_purpose_modal.title2'
defaultMessage='Edit Purpose for '
/>
<span className='name'>{this.props.channel.display_name}</span>
</span>
);
}
let channelType = (
<FormattedMessage
id='edit_channel_purpose_modal.channel'
defaultMessage='Channel'
/>
);
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
channelType = (
<FormattedMessage
id='edit_channel_purpose_modal.group'
defaultMessage='Group'
/>
);
}
return (
@@ -90,7 +129,15 @@ export default class EditChannelPurposeModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.`}</p>
<p>
<FormattedMessage
id='edit_channel_purpose_modal.body'
defaultMessage='Describe how this {type} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.'
values={{
type: (channelType)
}}
/>
</p>
<textarea
ref='purpose'
className='form-control no-resize'
@@ -106,14 +153,20 @@ export default class EditChannelPurposeModal extends React.Component {
className='btn btn-default'
onClick={this.handleHide}
>
{'Cancel'}
<FormattedMessage
id='edit_channel_purpose_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleSave}
>
{'Save'}
<FormattedMessage
id='edit_channel_purpose_modal.save'
defaultMessage='Save'
/>
</button>
</Modal.Footer>
</Modal>
@@ -122,7 +175,10 @@ export default class EditChannelPurposeModal extends React.Component {
}
EditChannelPurposeModal.propTypes = {
intl: intlShape.isRequired,
show: React.PropTypes.bool.isRequired,
channel: React.PropTypes.object,
onModalDismissed: React.PropTypes.func.isRequired
};
export default injectIntl(EditChannelPurposeModal);

Просмотреть файл

@@ -10,9 +10,19 @@ import PostStore from '../stores/post_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import Constants from '../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
var KeyCodes = Constants.KeyCodes;
export default class EditPostModal extends React.Component {
const holders = defineMessages({
editPost: {
id: 'edit_post.editPost',
defaultMessage: 'Edit the post...'
}
});
class EditPostModal extends React.Component {
constructor() {
super();
@@ -151,7 +161,15 @@ export default class EditPostModal extends React.Component {
>
<span aria-hidden='true'>&times;</span>
</button>
<h4 className='modal-title'>Edit {this.state.title}</h4>
<h4 className='modal-title'>
<FormattedMessage
id='edit_post.edit'
defaultMessage='Edit {title}'
values={{
title: this.state.title
}}
/>
</h4>
</div>
<div className='edit-modal-body modal-body'>
<Textbox
@@ -159,7 +177,7 @@ export default class EditPostModal extends React.Component {
onKeyPress={this.handleEditKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
createMessage={this.props.intl.formatMessage(holders.editPost)}
supportsCommands={false}
id='edit_textbox'
ref='editbox'
@@ -172,14 +190,20 @@ export default class EditPostModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
Cancel
<FormattedMessage
id='edit_post.cancel'
defaultMessage='Cancel'
/>
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleEdit}
>
Save
<FormattedMessage
id='edit_post.save'
defaultMessage='Save'
/>
</button>
</div>
</div>
@@ -188,3 +212,9 @@ export default class EditPostModal extends React.Component {
);
}
}
EditPostModal.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(EditPostModal);

Просмотреть файл

@@ -41,6 +41,8 @@ export default class GetLinkModal extends React.Component {
}
render() {
const userCreationEnabled = global.window.mm_config.EnableUserCreation === 'true';
let helpText = null;
if (this.props.helpText) {
helpText = (
@@ -53,7 +55,7 @@ export default class GetLinkModal extends React.Component {
}
let copyLink = null;
if (document.queryCommandSupported('copy')) {
if (userCreationEnabled && document.queryCommandSupported('copy')) {
copyLink = (
<button
data-copy-btn='true'
@@ -69,6 +71,18 @@ export default class GetLinkModal extends React.Component {
);
}
let linkText = null;
if (userCreationEnabled) {
linkText = (
<textarea
className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.props.link}
/>
);
}
var copyLinkConfirm = null;
if (this.state.copiedLink) {
copyLinkConfirm = (
@@ -92,12 +106,7 @@ export default class GetLinkModal extends React.Component {
</Modal.Header>
<Modal.Body>
{helpText}
<textarea
className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.props.link}
/>
{linkText}
</Modal.Body>
<Modal.Footer>
<button

Просмотреть файл

@@ -16,6 +16,10 @@ const holders = defineMessages({
help: {
id: 'get_team_invite_link_modal.help',
defaultMessage: 'Send teammates the link below for them to sign-up to this team site.'
},
helpDisabled: {
id: 'get_team_invite_link_modal.helpDisabled',
defaultMessage: 'User creation has been disabled for your team. Please ask your team administrator for details.'
}
});
@@ -47,12 +51,18 @@ class GetTeamInviteLinkModal extends React.Component {
render() {
const {formatMessage} = this.props.intl;
let helpText = formatMessage(holders.helpDisabled);
if (global.window.mm_config.EnableUserCreation === 'true') {
helpText = formatMessage(holders.help);
}
return (
<GetLinkModal
show={this.state.show}
onHide={() => this.setState({show: false})}
title={formatMessage(holders.title)}
helpText={formatMessage(holders.help)}
helpText={helpText}
link={TeamStore.getCurrentInviteLink()}
/>
);

Просмотреть файл

@@ -2,6 +2,7 @@
// See License.txt for license information.
import LoginEmail from './login_email.jsx';
import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -35,7 +36,7 @@ export default class Login extends React.Component {
/>
</span>
</a>
);
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
@@ -87,7 +88,7 @@ export default class Login extends React.Component {
}
let emailSignup;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
<LoginEmail
teamName={this.props.teamName}
@@ -189,6 +190,15 @@ export default class Login extends React.Component {
);
}
let usernameLogin = null;
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
usernameLogin = (
<LoginUsername
teamName={this.props.teamName}
/>
);
}
return (
<div className='signup-team__container'>
<h5 className='margin--less'>
@@ -210,6 +220,7 @@ export default class Login extends React.Component {
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{findTeams}

181
web/react/components/login_username.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,181 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
var holders = defineMessages({
badTeam: {
id: 'login_username.badTeam',
defaultMessage: 'Bad team name'
},
usernameReq: {
id: 'login_username.usernameReq',
defaultMessage: 'A username is required'
},
pwdReq: {
id: 'login_username.pwdReq',
defaultMessage: 'A password is required'
},
verifyEmailError: {
id: 'login_username.verifyEmailError',
defaultMessage: 'Please verify your email address. Check your inbox for an email.'
},
userNotFoundError: {
id: 'login_username.userNotFoundError',
defaultMessage: "We couldn't find an existing account matching your username for this team."
},
username: {
id: 'login_username.username',
defaultMessage: 'Username'
},
pwd: {
id: 'login_username.pwd',
defaultMessage: 'Password'
}
});
export default class LoginUsername extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
serverError: ''
};
}
handleSubmit(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
var state = {};
const name = this.props.teamName;
if (!name) {
state.serverError = formatMessage(holders.badTeam);
this.setState(state);
return;
}
const username = this.refs.username.value.trim();
if (!username) {
state.serverError = formatMessage(holders.usernameReq);
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
state.serverError = formatMessage(holders.pwdReq);
this.setState(state);
return;
}
state.serverError = '';
this.setState(state);
Client.loginByUsername(name, username, password,
() => {
UserStore.setLastUsername(username);
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
window.location.href = decodeURIComponent(redirect);
} else {
window.location.href = '/' + name + '/channels/town-square';
}
},
(err) => {
if (err.message === 'api.user.login.not_verified.app_error') {
state.serverError = formatMessage(holders.verifyEmailError);
} else if (err.message === 'store.sql_user.get_by_username.app_error') {
state.serverError = formatMessage(holders.userNotFoundError);
} else {
state.serverError = err.message;
}
this.valid = false;
this.setState(state);
}
);
}
render() {
let serverError;
let errorClass = '';
if (this.state.serverError) {
serverError = <label className='control-label'>{this.state.serverError}</label>;
errorClass = ' has-error';
}
let priorUsername = UserStore.getLastUsername();
let focusUsername = false;
let focusPassword = false;
if (priorUsername === '') {
focusUsername = true;
} else {
focusPassword = true;
}
const emailParam = Utils.getUrlParameter('email');
if (emailParam) {
priorUsername = decodeURIComponent(emailParam);
}
const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
<div className={'form-group' + errorClass}>
{serverError}
</div>
<div className={'form-group' + errorClass}>
<input
autoFocus={focusUsername}
type='username'
className='form-control'
name='username'
defaultValue={priorUsername}
ref='username'
placeholder={formatMessage(holders.username)}
spellCheck='false'
/>
</div>
<div className={'form-group' + errorClass}>
<input
autoFocus={focusPassword}
type='password'
className='form-control'
name='password'
ref='password'
placeholder={formatMessage(holders.pwd)}
spellCheck='false'
/>
</div>
<div className='form-group'>
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='login_username.signin'
defaultMessage='Sign in'
/>
</button>
</div>
</div>
</form>
);
}
}
LoginUsername.defaultProps = {
};
LoginUsername.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string.isRequired
};
export default injectIntl(LoginUsername);

Просмотреть файл

@@ -4,6 +4,8 @@
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
export default class MemberListItem extends React.Component {
constructor(props) {
super(props);
@@ -38,7 +40,10 @@ export default class MemberListItem extends React.Component {
className='btn btn-sm btn-primary'
>
<i className='glyphicon glyphicon-envelope'/>
{' Add'}
<FormattedMessage
id='member_item.add'
defaultMessage=' Add'
/>
</a>
);
} else if (isAdmin && !isMemberAdmin && (member.id !== UserStore.getCurrentId())) {
@@ -53,7 +58,10 @@ export default class MemberListItem extends React.Component {
role='menuitem'
onClick={self.handleMakeAdmin}
>
Make Admin
<FormattedMessage
id='member_item.makeAdmin'
defaultMessage='Make Admin'
/>
</a>
</li>);
}
@@ -67,7 +75,10 @@ export default class MemberListItem extends React.Component {
role='menuitem'
onClick={self.handleRemove}
>
Remove Member
<FormattedMessage
id='member_item.removeMember'
defaultMessage='Remove Member'
/>
</a>
</li>);
}
@@ -82,7 +93,14 @@ export default class MemberListItem extends React.Component {
aria-expanded='true'
>
<span className='fa fa-pencil'></span>
<span className='text-capitalize'>{member.roles || 'Member'} </span>
<span className='text-capitalize'>
{member.roles ||
<FormattedMessage
id='member_item.member'
defaultMessage='Member'
/>
}
</span>
</a>
<ul
className='dropdown-menu member-menu'
@@ -94,7 +112,7 @@ export default class MemberListItem extends React.Component {
</div>
);
} else {
invite = <div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || 'Member'}</div>;
invite = (<div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || <FormattedMessage id='member_item.member' />}</div>);
}
return (

Просмотреть файл

@@ -106,9 +106,9 @@ class MsgTyping extends React.Component {
<FormattedMessage
id='msg_typing.areTyping'
defaultMessage='{users} and {last} are typing...'
vaues={{
users: users.join(', '),
last: last
values={{
users: (users.join(', ')),
last: (last)
}}
/>
);

Просмотреть файл

@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import {FormattedMessage} from 'mm-intl';
export default class PopoverListMembers extends React.Component {
constructor(props) {
super(props);
@@ -92,7 +94,10 @@ export default class PopoverListMembers extends React.Component {
className='btn-message'
onClick={(e) => this.handleShowDirectChannel(m, e)}
>
{'Message'}
<FormattedMessage
id='members_popover.msg'
defaultMessage='Message'
/>
</a>
);
}
@@ -147,6 +152,12 @@ export default class PopoverListMembers extends React.Component {
countText = count.toString();
}
const title = (
<FormattedMessage
id='members_popover.title'
defaultMessage='Members'
/>
);
return (
<div>
<div
@@ -171,7 +182,7 @@ export default class PopoverListMembers extends React.Component {
>
<Popover
ref='memebersPopover'
title='Members'
title={title}
id='member-list-popover'
>
{popoverHtml}

Просмотреть файл

@@ -214,6 +214,7 @@ export default class Post extends React.Component {
commentCount={commentCount}
handleCommentClick={this.handleCommentClick}
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
/>
<PostBody
post={post}

Просмотреть файл

@@ -4,6 +4,9 @@
import UserStore from '../stores/user_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
var ActionTypes = Constants.ActionTypes;
export default class PostDeletedModal extends React.Component {
@@ -65,11 +68,19 @@ export default class PostDeletedModal extends React.Component {
className='modal-title'
id='myModalLabel'
>
{'Comment could not be posted'}
<FormattedMessage
id='post_delete.notPosted'
defaultMessage='Comment could not be posted'
/>
</h4>
</div>
<div className='modal-body'>
<p>{'Someone deleted the message on which you tried to post a comment.'}</p>
<p>
<FormattedMessage
id='post_delete.someone'
defaultMessage='Someone deleted the message on which you tried to post a comment.'
/>
</p>
</div>
<div className='modal-footer'>
<button
@@ -77,7 +88,10 @@ export default class PostDeletedModal extends React.Component {
className='btn btn-primary'
data-dismiss='modal'
>
{'Okay'}
<FormattedMessage
id='post_delete.okay'
defaultMessage='Okay'
/>
</button>
</div>
</div>

Просмотреть файл

@@ -52,6 +52,7 @@ export default class PostHeader extends React.Component {
handleCommentClick={this.props.handleCommentClick}
allowReply='true'
isLastComment={this.props.isLastComment}
sameUser={this.props.sameUser}
/>
</li>
</ul>
@@ -62,11 +63,13 @@ export default class PostHeader extends React.Component {
PostHeader.defaultProps = {
post: null,
commentCount: 0,
isLastComment: false
isLastComment: false,
sameUser: false
};
PostHeader.propTypes = {
post: React.PropTypes.object,
commentCount: React.PropTypes.number,
isLastComment: React.PropTypes.bool,
handleCommentClick: React.PropTypes.func
handleCommentClick: React.PropTypes.func,
sameUser: React.PropTypes.bool
};

Просмотреть файл

@@ -220,6 +220,7 @@ export default class PostInfo extends React.Component {
<li className='col'>
<TimeSince
eventTime={post.create_at}
sameUser={this.props.sameUser}
/>
</li>
<li className='col col__reply'>
@@ -251,12 +252,14 @@ PostInfo.defaultProps = {
post: null,
commentCount: 0,
isLastComment: false,
allowReply: false
allowReply: false,
sameUser: false
};
PostInfo.propTypes = {
post: React.PropTypes.object,
commentCount: React.PropTypes.number,
isLastComment: React.PropTypes.bool,
allowReply: React.PropTypes.string,
handleCommentClick: React.PropTypes.func
handleCommentClick: React.PropTypes.func,
sameUser: React.PropTypes.bool
};

Просмотреть файл

@@ -6,6 +6,8 @@ import UserStore from '../stores/user_store.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import * as utils from '../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
export default class RemovedFromChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -49,12 +51,22 @@ export default class RemovedFromChannelModal extends React.Component {
render() {
var currentUser = UserStore.getCurrentUser();
var channelName = 'the channel';
var channelName = (
<FormattedMessage
id='removed_channel.channelName'
defaultMessage='the channel'
/>
);
if (this.state.channelName) {
channelName = this.state.channelName;
}
var remover = 'Someone';
var remover = (
<FormattedMessage
id='removed_channel.someone'
defaultMessage='Someone'
/>
);
if (this.state.remover) {
remover = this.state.remover;
}
@@ -78,17 +90,36 @@ export default class RemovedFromChannelModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
><span aria-hidden='true'>&times;</span></button>
<h4 className='modal-title'>Removed from <span className='name'>{channelName}</span></h4>
<h4 className='modal-title'>
<FormattedMessage
id='removed_channel.from'
defaultMessage='Removed from '
/>
<span className='name'>{channelName}</span></h4>
</div>
<div className='modal-body'>
<p>{remover} removed you from {channelName}</p>
<p>
<FormattedMessage
id='removed_channel.remover'
defaultMessage='{remover} removed you from {channel}'
values={{
remover: (remover),
channel: (channelName)
}}
/>
</p>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-primary'
data-dismiss='modal'
>Okay</button>
>
<FormattedMessage
id='removed_channel.okay'
defaultMessage='Okay'
/>
</button>
</div>
</div>
</div>

Просмотреть файл

@@ -7,6 +7,39 @@ import * as AsyncClient from '../utils/async_client.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import Constants from '../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
const holders = defineMessages({
required: {
id: 'rename_channel.required',
defaultMessage: 'This field is required'
},
maxLength: {
id: 'rename_channel.maxLength',
defaultMessage: 'This field must be less than 22 characters'
},
lowercase: {
id: 'rename_channel.lowercase',
defaultMessage: 'Must be lowercase alphanumeric characters'
},
handle: {
id: 'rename_channel.handle',
defaultMessage: 'Handle'
},
defaultError: {
id: 'rename_channel.defaultError',
defaultMessage: ' - Cannot be changed for the default channel'
},
displayNameHolder: {
id: 'rename_channel.displayNameHolder',
defaultMessage: 'Enter display name'
},
handleHolder: {
id: 'rename_channel.handleHolder',
defaultMessage: 'lowercase alphanumeric&#39;s only'
}
});
export default class RenameChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -41,13 +74,14 @@ export default class RenameChannelModal extends React.Component {
const oldName = channel.name;
const oldDisplayName = channel.displayName;
const state = {serverError: ''};
const {formatMessage} = this.props.intl;
channel.display_name = this.state.displayName.trim();
if (!channel.display_name) {
state.displayNameError = 'This field is required';
state.displayNameError = formatMessage(holders.required);
state.invalid = true;
} else if (channel.display_name.length > 22) {
state.displayNameError = 'This field must be less than 22 characters';
state.displayNameError = formatMessage(holders.maxLength);
state.invalid = true;
} else {
state.displayNameError = '';
@@ -55,17 +89,17 @@ export default class RenameChannelModal extends React.Component {
channel.name = this.state.channelName.trim();
if (!channel.name) {
state.nameError = 'This field is required';
state.nameError = formatMessage(holders.required);
state.invalid = true;
} else if (channel.name.length > 22) {
state.nameError = 'This field must be less than 22 characters';
state.nameError = formatMessage(holders.maxLength);
state.invalid = true;
} else {
const cleanedName = Utils.cleanUpUrlable(channel.name);
if (cleanedName === channel.name) {
state.nameError = '';
} else {
state.nameError = 'Must be lowercase alphanumeric characters';
state.nameError = formatMessage(holders.lowercase);
state.invalid = true;
}
}
@@ -153,11 +187,13 @@ export default class RenameChannelModal extends React.Component {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
let handleInputLabel = 'Handle';
const {formatMessage} = this.props.intl;
let handleInputLabel = formatMessage(holders.handle);
let handleInputClass = 'form-control';
let readOnlyHandleInput = false;
if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
handleInputLabel += ' - Cannot be changed for the default channel';
handleInputLabel += formatMessage(holders.defaultError);
handleInputClass += ' disabled-input';
readOnlyHandleInput = true;
}
@@ -180,14 +216,29 @@ export default class RenameChannelModal extends React.Component {
data-dismiss='modal'
>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>{'Close'}</span>
<span className='sr-only'>
<FormattedMessage
id='rename_channel.close'
defaultMessage='Close'
/>
</span>
</button>
<h4 className='modal-title'>{'Rename Channel'}</h4>
<h4 className='modal-title'>
<FormattedMessage
id='rename_channel.title'
defaultMessage='Rename Channel'
/>
</h4>
</div>
<form role='form'>
<div className='modal-body'>
<div className={displayNameClass}>
<label className='control-label'>{'Display Name'}</label>
<label className='control-label'>
<FormattedMessage
id='rename_channel.displayName'
defaultMessage='Display Name'
/>
</label>
<input
onKeyUp={this.displayNameKeyUp}
onChange={this.onDisplayNameChange}
@@ -195,7 +246,7 @@ export default class RenameChannelModal extends React.Component {
ref='displayName'
id='display_name'
className='form-control'
placeholder='Enter display name'
placeholder={formatMessage(holders.displayNameHolder)}
value={this.state.displayName}
maxLength='64'
/>
@@ -208,7 +259,7 @@ export default class RenameChannelModal extends React.Component {
type='text'
className={handleInputClass}
ref='channelName'
placeholder='lowercase alphanumeric&#39;s only'
placeholder={formatMessage(holders.handleHolder)}
value={this.state.channelName}
maxLength='64'
readOnly={readOnlyHandleInput}
@@ -223,14 +274,20 @@ export default class RenameChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
{'Cancel'}
<FormattedMessage
id='rename_channel.cancel'
defaultMessage='Cancel'
/>
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
{'Save'}
<FormattedMessage
id='rename_channel.save'
defaultMessage='Save'
/>
</button>
</div>
</form>
@@ -240,3 +297,9 @@ export default class RenameChannelModal extends React.Component {
);
}
}
RenameChannelModal.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(RenameChannelModal);

Просмотреть файл

@@ -85,6 +85,6 @@ SettingItemMax.propTypes = {
extraInfo: React.PropTypes.element,
updateSection: React.PropTypes.func,
submit: React.PropTypes.func,
title: React.PropTypes.string,
title: React.PropTypes.node,
width: React.PropTypes.string
};

Просмотреть файл

@@ -38,8 +38,8 @@ export default class SettingItemMin extends React.Component {
}
SettingItemMin.propTypes = {
title: React.PropTypes.string,
title: React.PropTypes.node,
disableOpen: React.PropTypes.bool,
updateSection: React.PropTypes.func,
describe: React.PropTypes.string
describe: React.PropTypes.node
};

Просмотреть файл

@@ -150,9 +150,18 @@ class SignupUserComplete extends React.Component {
// set up error labels
var emailError = null;
var emailHelpText = (
<span className='help-block'>
<FormattedMessage
id='signup_user_completed.emailHelp'
defaultMessage='Valid email required for sign-up'
/>
</span>
);
var emailDivStyle = 'form-group';
if (this.state.emailError) {
emailError = <label className='control-label'>{this.state.emailError}</label>;
emailHelpText = '';
emailDivStyle += ' has-error';
}
@@ -232,6 +241,7 @@ class SignupUserComplete extends React.Component {
spellCheck='false'
/>
{emailError}
{emailHelpText}
</div>
</div>
);

Просмотреть файл

@@ -575,6 +575,8 @@ class GeneralTab extends React.Component {
</div>
);
const nameExtraInfo = <span>{formatMessage(holders.teamNameInfo)}</span>;
nameSection = (
<SettingItemMax
title={formatMessage({id: 'general_tab.teamName'})}
@@ -583,7 +585,7 @@ class GeneralTab extends React.Component {
server_error={serverError}
client_error={clientError}
updateSection={this.onUpdateNameSection}
extraInfo={formatMessage(holders.teamNameInfo)}
extraInfo={nameExtraInfo}
/>
);
} else {

Просмотреть файл

@@ -129,13 +129,6 @@ export default class Textbox extends React.Component {
this.resize();
}
showHelp(e) {
e.preventDefault();
e.target.blur();
global.window.open('/docs/Messaging');
}
render() {
let previewLink = null;
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
@@ -194,7 +187,8 @@ export default class Textbox extends React.Component {
</div>
{previewLink}
<a
onClick={this.showHelp}
target='_blank'
href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
className='textbox-help-link'
>
<FormattedMessage

Просмотреть файл

@@ -14,7 +14,7 @@ export default class TimeSince extends React.Component {
componentDidMount() {
this.intervalId = setInterval(() => {
this.forceUpdate();
}, 30000);
}, Constants.TIME_SINCE_UPDATE_INTERVAL);
}
componentWillUnmount() {
clearInterval(this.intervalId);
@@ -23,6 +23,14 @@ export default class TimeSince extends React.Component {
const displayDate = Utils.displayDate(this.props.eventTime);
const displayTime = Utils.displayTime(this.props.eventTime);
if (this.props.sameUser) {
return (
<time className='post__time'>
{Utils.displayTime(this.props.eventTime)}
</time>
);
}
const tooltip = (
<Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
{displayDate + ' at ' + displayTime}
@@ -42,10 +50,13 @@ export default class TimeSince extends React.Component {
);
}
}
TimeSince.defaultProps = {
eventTime: 0
eventTime: 0,
sameUser: false
};
TimeSince.propTypes = {
eventTime: React.PropTypes.number.isRequired
eventTime: React.PropTypes.number.isRequired,
sameUser: React.PropTypes.bool
};

Просмотреть файл

@@ -88,7 +88,8 @@ class SecurityTab extends React.Component {
}
if (newPassword !== confirmPassword) {
this.setState({passwordError: formatMessage(holders.passwordMatchError), serverError: ''});
var defaultState = Object.assign(this.getDefaultState(), {passwordError: formatMessage(holders.passwordMatchError), serverError: ''});
this.setState(defaultState);
return;
}

Просмотреть файл

@@ -1,4 +1,4 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -10,6 +10,7 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const LOG_CHANGE_EVENT = 'log_change';
const SERVER_AUDIT_CHANGE_EVENT = 'server_audit_change';
const CONFIG_CHANGE_EVENT = 'config_change';
const ALL_TEAMS_EVENT = 'all_team_change';
@@ -18,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
super();
this.logs = null;
this.audits = null;
this.config = null;
this.teams = null;
@@ -25,6 +27,10 @@ class AdminStoreClass extends EventEmitter {
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
this.emitAuditChange = this.emitAuditChange.bind(this);
this.addAuditChangeListener = this.addAuditChangeListener.bind(this);
this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this);
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
@@ -46,6 +52,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(LOG_CHANGE_EVENT, callback);
}
emitAuditChange() {
this.emit(SERVER_AUDIT_CHANGE_EVENT);
}
addAuditChangeListener(callback) {
this.on(SERVER_AUDIT_CHANGE_EVENT, callback);
}
removeAuditChangeListener(callback) {
this.removeListener(SERVER_AUDIT_CHANGE_EVENT, callback);
}
emitConfigChange() {
this.emit(CONFIG_CHANGE_EVENT);
}
@@ -78,6 +96,14 @@ class AdminStoreClass extends EventEmitter {
this.logs = logs;
}
getAudits() {
return this.audits;
}
saveAudits(audits) {
this.audits = audits;
}
getConfig() {
return this.config;
}
@@ -113,6 +139,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveLogs(action.logs);
AdminStore.emitLogChange();
break;
case ActionTypes.RECIEVED_SERVER_AUDITS:
AdminStore.saveAudits(action.audits);
AdminStore.emitAuditChange();
break;
case ActionTypes.RECIEVED_CONFIG:
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();

Просмотреть файл

@@ -38,6 +38,8 @@ class UserStoreClass extends EventEmitter {
this.setCurrentUser = this.setCurrentUser.bind(this);
this.getLastEmail = this.getLastEmail.bind(this);
this.setLastEmail = this.setLastEmail.bind(this);
this.getLastUsername = this.getLastUsername.bind(this);
this.setLastUsername = this.setLastUsername.bind(this);
this.hasProfile = this.hasProfile.bind(this);
this.getProfile = this.getProfile.bind(this);
this.getProfileByUsername = this.getProfileByUsername.bind(this);
@@ -159,6 +161,14 @@ class UserStoreClass extends EventEmitter {
BrowserStore.setGlobalItem('last_email', email);
}
getLastUsername() {
return BrowserStore.getGlobalItem('last_username', '');
}
setLastUsername(username) {
BrowserStore.setGlobalItem('last_username', username);
}
hasProfile(userId) {
return this.getProfiles()[userId] != null;
}

Просмотреть файл

@@ -312,6 +312,32 @@ export function getLogs() {
);
}
export function getServerAudits() {
if (isCallInProgress('getServerAudits')) {
return;
}
callTracker.getServerAudits = utils.getTimestamp();
client.getServerAudits(
(data, textStatus, xhr) => {
callTracker.getServerAudits = 0;
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_SERVER_AUDITS,
audits: data
});
},
(err) => {
callTracker.getServerAudits = 0;
dispatchError(err, 'getServerAudits');
}
);
}
export function getConfig() {
if (isCallInProgress('getConfig')) {
return;

Просмотреть файл

@@ -305,6 +305,28 @@ export function loginByEmail(name, email, password, success, error) {
});
}
export function loginByUsername(name, username, password, success, error) {
$.ajax({
url: '/api/v1/users/login',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({name, username, password}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_login_success', data.team_id, 'username', data.username);
sessionStorage.removeItem(data.id + '_last_error');
BrowserStore.signalLogin();
success(data, textStatus, xhr);
},
error: function onError(xhr, status, err) {
track('api', 'api_users_login_fail', name, 'username', username);
var e = handleError('loginByUsername', xhr, status, err);
error(e);
}
});
}
export function loginByLdap(teamName, id, password, success, error) {
$.ajax({
url: '/api/v1/users/login_ldap',
@@ -385,6 +407,20 @@ export function getLogs(success, error) {
});
}
export function getServerAudits(success, error) {
$.ajax({
url: '/api/v1/admin/audits',
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success,
error: function onError(xhr, status, err) {
var e = handleError('getServerAudits', xhr, status, err);
error(e);
}
});
}
export function getConfig(success, error) {
$.ajax({
url: '/api/v1/admin/config',

Просмотреть файл

@@ -45,6 +45,7 @@ export default {
RECIEVED_CONFIG: null,
RECIEVED_LOGS: null,
RECIEVED_SERVER_AUDITS: null,
RECIEVED_ALL_TEAMS: null,
SHOW_SEARCH: null,
@@ -84,7 +85,7 @@ export default {
SPECIAL_MENTIONS: ['channel'],
CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
PRESENTATION_TYPES: ['ppt', 'pptx'],
SPREADSHEET_TYPES: ['xlsx', 'csv'],
@@ -462,5 +463,6 @@ export default {
MIN_USERNAME_LENGTH: 3,
MAX_USERNAME_LENGTH: 15,
MIN_PASSWORD_LENGTH: 5,
MAX_PASSWORD_LENGTH: 50
MAX_PASSWORD_LENGTH: 50,
TIME_SINCE_UPDATE_INTERVAL: 30000
};

Просмотреть файл

@@ -134,7 +134,7 @@ class MattermostMarkdownRenderer extends marked.Renderer {
);
} else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
try {
const html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
const html = katex.renderToString(code, {throwOnError: false, displayMode: true});
return '<div class="post-body--code tex">' + html + '</div>';
} catch (e) {