diff options
Diffstat (limited to 'web')
46 files changed, 2363 insertions, 829 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 6319b5681..98b1d7cc1 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -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 LoadingScreen from './loading_screen.jsx'; +import AuditTable from './audit_table.jsx'; + 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 * 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);
\ No newline at end of file +export default injectIntl(AccessHistoryModal); diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index efd163017..360ae3ef3 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -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') { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index d6bae1feb..642bfe9d7 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -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> diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx index a22c26c34..0a159d2e3 100644 --- a/web/react/components/admin_console/analytics.jsx +++ b/web/react/components/admin_console/analytics.jsx @@ -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> - </div> - </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) + } + ]; - 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> - </div> - </div> - ); + 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) + } + ]; - 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> + 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> - ); + ); + } 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> + ); + } - 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); diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx new file mode 100644 index 000000000..866539b3d --- /dev/null +++ b/web/react/components/admin_console/audits.jsx @@ -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> + ); + } +} diff --git a/web/react/components/admin_console/doughnut_chart.jsx b/web/react/components/admin_console/doughnut_chart.jsx new file mode 100644 index 000000000..e2dc01528 --- /dev/null +++ b/web/react/components/admin_console/doughnut_chart.jsx @@ -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 +}; diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index ce3c8cd12..17f25a04c 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -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; @@ -320,6 +322,88 @@ class EmailSettings extends React.Component { <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' htmlFor='sendEmailNotifications' > <FormattedMessage diff --git a/web/react/components/admin_console/statistic_count.jsx b/web/react/components/admin_console/statistic_count.jsx new file mode 100644 index 000000000..57af0ed1b --- /dev/null +++ b/web/react/components/admin_console/statistic_count.jsx @@ -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 +}; diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx index 2dd833fb2..f983db177 100644 --- a/web/react/components/admin_console/system_analytics.jsx +++ b/web/react/components/admin_console/system_analytics.jsx @@ -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);
\ No newline at end of file +export default injectIntl(SystemAnalytics); diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index ee59b0e66..808d8046d 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -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);
\ No newline at end of file +export default injectIntl(TeamAnalytics); diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx new file mode 100644 index 000000000..cdca7e8d6 --- /dev/null +++ b/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); diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index f64834775..005a82209 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -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> ); diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx index 72c7c3daa..5067f5913 100644 --- a/web/react/components/channel_info_modal.jsx +++ b/web/react/components/channel_info_modal.jsx @@ -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);
\ No newline at end of file diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index 8b7485e5f..7dc2c0a11 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -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> diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx index 513a720e7..f3cbef719 100644 --- a/web/react/components/channel_members_modal.jsx +++ b/web/react/components/channel_members_modal.jsx @@ -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> diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx index e70d3a634..59ef8966e 100644 --- a/web/react/components/channel_notifications_modal.jsx +++ b/web/react/components/channel_notifications_modal.jsx @@ -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'> diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index 1255067fd..d9113bc9f 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -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> diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 4cde5feed..34fd724f5 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -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> diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx index e4817f6e4..1066d123e 100644 --- a/web/react/components/edit_channel_header_modal.jsx +++ b/web/react/components/edit_channel_header_modal.jsx @@ -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); diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx index af23342ae..d8354f59d 100644 --- a/web/react/components/edit_channel_purpose_modal.jsx +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -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);
\ No newline at end of file diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index e4e77a943..e54b7d9b8 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -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'>×</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);
\ No newline at end of file diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index c4f530af0..0123a0f3c 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -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} diff --git a/web/react/components/login_username.jsx b/web/react/components/login_username.jsx new file mode 100644 index 000000000..f787490fa --- /dev/null +++ b/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); diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index a7273f280..c50ee5c96 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -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 ( diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index b95b06260..f7a40b54e 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -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) }} /> ); diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index f4cb542e4..f217229ed 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -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} diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 695d7daef..53fe7fb5d 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -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} diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx index 3723bcaba..218f57eb5 100644 --- a/web/react/components/post_deleted_modal.jsx +++ b/web/react/components/post_deleted_modal.jsx @@ -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> diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index f18024343..037b48096 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -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 }; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 2bff675a9..0fb9d7f4a 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -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 }; diff --git a/web/react/components/removed_from_channel_modal.jsx b/web/react/components/removed_from_channel_modal.jsx index 69d038c22..748baa32b 100644 --- a/web/react/components/removed_from_channel_modal.jsx +++ b/web/react/components/removed_from_channel_modal.jsx @@ -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'>×</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> diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index c16216c68..c467c0d87 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -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'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'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);
\ No newline at end of file diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 52f1906c3..537055641 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -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 }; diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx index db5513b14..868b7e1b2 100644 --- a/web/react/components/setting_item_min.jsx +++ b/web/react/components/setting_item_min.jsx @@ -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 }; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 47ec58e98..98a832542 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -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> ); diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index bb383aca1..00e5ace98 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -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 diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx index 32947bd60..0b549b1e6 100644 --- a/web/react/components/time_since.jsx +++ b/web/react/components/time_since.jsx @@ -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 }; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 704e2ced4..8f43091a7 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -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(); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 3e1871180..b97a0d87b 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -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; } diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 0ee89b9fa..d615e02c7 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -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; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 09cd4162a..33eb4cd47 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -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', diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 8486efe72..11a8da669 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -45,6 +45,7 @@ export default { RECIEVED_CONFIG: null, RECIEVED_LOGS: null, + RECIEVED_SERVER_AUDITS: null, RECIEVED_ALL_TEAMS: null, SHOW_SEARCH: null, @@ -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 }; diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index be85ef07b..73c7bd9cb 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -456,11 +456,7 @@ body.ios { &:hover { .post__time { - - &:before { - @include opacity(0.5); - } - + @include opacity(0.5); } } @@ -484,27 +480,15 @@ body.ios { } .post__time { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; + font: normal normal normal FontAwesome; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - font-size: 0; position: absolute; top: -3px; - left: 17px; - width: 30px; - height: 30px; + left: -1.0em; line-height: 37px; - - &:before { - @include opacity(0); - content: "\f017"; - content: "\f017"; - font-size: 19px; - } - + @include opacity(0); } } diff --git a/web/sass-files/sass/partials/_statistics.scss b/web/sass-files/sass/partials/_statistics.scss index edd3c9bf3..f86740270 100644 --- a/web/sass-files/sass/partials/_statistics.scss +++ b/web/sass-files/sass/partials/_statistics.scss @@ -14,10 +14,11 @@ padding: 7px 10px; border-bottom: 1px solid #ddd; text-align: left; + font-size: 13px; .fa { float: right; - margin: 3px 0 0; + margin: 0px 0 0; color: #555; font-size: 16px; } @@ -83,4 +84,4 @@ } } } -}
\ No newline at end of file +} diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 2412a1bae..890e7188d 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -8,53 +8,54 @@ "about.date": "Build Date:", "about.hash": "Build Hash:", "about.close": "Close", - "access_history.sessionRevoked": "The session with id {sessionId} was revoked", - "access_history.channelCreated": "Created the {channelName} channel/group", - "access_history.establishedDM": "Established a direct message channel with {username}", - "access_history.nameUpdated": "Updated the {channelName} channel/group name", - "access_history.headerUpdated": "Updated the {channelName} channel/group header", - "access_history.channelDeleted": "Deleted the channel/group with the URL {url}", - "access_history.userAdded": "Added {username} to the {channelName} channel/group", - "access_history.userRemoved": "Removed {username} to the {channelName} channel/group", - "access_history.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}", - "access_history.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access", - "access_history.successfullOAuthAccess": "Successfully gave a new OAuth service access", - "access_history.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback", - "access_history.attemptedOAuthToken": "Attempted to get an OAuth access token", - "access_history.successfullOAuthToken": "Successfully added a new OAuth service", - "access_history.oauthTokenFailed": "Failed to get an OAuth access token - {token}", - "access_history.attemptedLogin": "Attempted to login", - "access_history.successfullLogin": "Successfully logged in", - "access_history.failedLogin": "FAILED login attempt", - "access_history.updatePicture": "Updated your profile picture", - "access_history.updateGeneral": "Updated the general settings of your account", - "access_history.attemptedPassword": "Attempted to change password", - "access_history.successfullPassword": "Successfully changed password", - "access_history.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth", - "access_history.updatedRol": "Updated user role(s) to ", - "access_history.member": "member", - "access_history.accountActive": "Account made active", - "access_history.accountInactive": "Account made inactive", - "access_history.by": " by {username}", - "access_history.byAdmin": " by an admin", - "access_history.sentEmail": "Sent an email to {email} to reset your password", - "access_history.attemptedReset": "Attempted to reset password", - "access_history.successfullReset": "Successfully reset password", - "access_history.updateGlobalNotifications": "Updated your global notification settings", - "access_history.attemptedWebhookCreate": "Attempted to create a webhook", - "access_history.successfullWebhookCreate": "Successfully created a webhook", - "access_history.failedWebhookCreate": "Failed to create a webhook - bad channel permissions", - "access_history.attemptedWebhookDelete": "Attempted to delete a webhook", - "access_history.successfullWebhookDelete": "Successfully deleted a webhook", - "access_history.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions", - "access_history.logout": "Logged out of your account", - "access_history.verified": "Sucessfully verified your email address", - "access_history.revokedAll": "Revoked all current sessions for the team", - "access_history.loginAttempt": " (Login attempt)", - "access_history.loginFailure": " (Login failure)", - "access_history.moreInfo": "More info", - "access_history.ip": "IP: {ip}", - "access_history.session": "Session ID: {id}", + "audit_table.sessionRevoked": "The session with id {sessionId} was revoked", + "audit_table.channelCreated": "Created the {channelName} channel/group", + "audit_table.establishedDM": "Established a direct message channel with {username}", + "audit_table.nameUpdated": "Updated the {channelName} channel/group name", + "audit_table.headerUpdated": "Updated the {channelName} channel/group header", + "audit_table.channelDeleted": "Deleted the channel/group with the URL {url}", + "audit_table.userAdded": "Added {username} to the {channelName} channel/group", + "audit_table.userRemoved": "Removed {username} to the {channelName} channel/group", + "audit_table.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}", + "audit_table.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access", + "audit_table.successfullOAuthAccess": "Successfully gave a new OAuth service access", + "audit_table.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback", + "audit_table.attemptedOAuthToken": "Attempted to get an OAuth access token", + "audit_table.successfullOAuthToken": "Successfully added a new OAuth service", + "audit_table.oauthTokenFailed": "Failed to get an OAuth access token - {token}", + "audit_table.attemptedLogin": "Attempted to login", + "audit_table.successfullLogin": "Successfully logged in", + "audit_table.failedLogin": "FAILED login attempt", + "audit_table.updatePicture": "Updated your profile picture", + "audit_table.updateGeneral": "Updated the general settings of your account", + "audit_table.attemptedPassword": "Attempted to change password", + "audit_table.successfullPassword": "Successfully changed password", + "audit_table.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth", + "audit_table.updatedRol": "Updated user role(s) to ", + "audit_table.member": "member", + "audit_table.accountActive": "Account made active", + "audit_table.accountInactive": "Account made inactive", + "audit_table.by": " by {username}", + "audit_table.byAdmin": " by an admin", + "audit_table.sentEmail": "Sent an email to {email} to reset your password", + "audit_table.attemptedReset": "Attempted to reset password", + "audit_table.successfullReset": "Successfully reset password", + "audit_table.updateGlobalNotifications": "Updated your global notification settings", + "audit_table.attemptedWebhookCreate": "Attempted to create a webhook", + "audit_table.successfullWebhookCreate": "Successfully created a webhook", + "audit_table.failedWebhookCreate": "Failed to create a webhook - bad channel permissions", + "audit_table.attemptedWebhookDelete": "Attempted to delete a webhook", + "audit_table.successfullWebhookDelete": "Successfully deleted a webhook", + "audit_table.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions", + "audit_table.logout": "Logged out of your account", + "audit_table.verified": "Sucessfully verified your email address", + "audit_table.revokedAll": "Revoked all current sessions for the team", + "audit_table.loginAttempt": " (Login attempt)", + "audit_table.loginFailure": " (Login failure)", + "audit_table.moreInfo": "More info", + "audit_table.ip": "IP: {ip}", + "audit_table.session": "Session ID: {id}", + "audit_table.userId": "User ID", "access_history.title": "Access History", "activity_log_modal.iphoneNativeApp": "iPhone Native App", "activity_log_modal.androidNativeApp": "Android Native App", @@ -96,11 +97,19 @@ "admin.sidebar.teams": "TEAMS ({count})", "admin.sidebar.other": "OTHER", "admin.sidebar.logs": "Logs", + "admin.sidebar.audits": "Audits", "admin.analytics.loading": "Loading...", "admin.analytics.totalUsers": "Total Users", "admin.analytics.publicChannels": "Public Channels", "admin.analytics.privateGroups": "Private Groups", "admin.analytics.totalPosts": "Total Posts", + "admin.analytics.totalFilePosts": "Posts with Files", + "admin.analytics.totalHashtagPosts": "Posts with Hashtags", + "admin.analytics.totalIncomingWebhooks": "Incoming Webhooks", + "admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks", + "admin.analytics.channelTypes": "Channel Types", + "admin.analytics.textPosts": "Posts with Text-only", + "admin.analytics.postTypes": "Posts, Files and Hashtags", "admin.analytics.meaningful": "Not enough data for a meaningful representation.", "admin.analytics.activeUsers": "Active Users With Posts", "admin.analytics.recentActive": "Recent Active Users", @@ -127,6 +136,10 @@ "admin.email.true": "true", "admin.email.false": "false", "admin.email.allowSignupDescription": "When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.", + "admin.email.allowEmailSignInTitle": "Allow Sign In With Email: ", + "admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.", + "admin.email.allowUsernameSignInTitle": "Allow Sign In With Username: ", + "admin.email.allowUsernameSignInDescription": "When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.", "admin.email.notificationsTitle": "Send Email Notifications: ", "admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).", "admin.email.requireVerificationTitle": "Require Email Verification: ", @@ -326,6 +339,8 @@ "admin.log.save": "Save", "admin.logs.title": "Server Logs", "admin.logs.reload": "Reload", + "admin.audits.title": "Server Audits", + "admin.audits.reload": "Reload", "admin.privacy.saving": "Saving Config...", "admin.privacy.title": "Privacy Settings", "admin.privacy.showEmailTitle": "Show Email Address: ", @@ -480,6 +495,40 @@ "change_url.noUnderscore": "Can not contain two underscores in a row.", "change_url.invalidUrl": "Invalid URL", "change_url.close": "Close", + "channel_header.recentMentions": "Recent Mentions", + "channel_header.channel": "Channel", + "channel_header.group": "Group", + "channel_header.channelHeader": "Set Channel Header...", + "channel_header.viewInfo": "View Info", + "chanel_header.addMembers": "Add Members", + "channel_header.manageMembers": "Manage Members", + "channel_header.setHeader": "Set {term} Header...", + "channel_header.setPurpose": "Set {term} Purpose...", + "channel_header.notificationPreferences": "Notification Preferences", + "channel_header.rename": "Rename {term}...", + "channel_header.delete": "Delete {term}...", + "channel_header.leave": "Leave {term}", + "channel_info.notFound": "No Channel Found", + "channel_info.name": "Channel Name:", + "channel_info.url": "Channel URL:", + "channel_info.id": "Channel ID:", + "channel_info.purpose": "Channel Purpose:", + "channel_info.close": "Close", + "channel_invite.addNewMembers": "Add New Members to ", + "channel_invite.close": "Close", + "channel_memebers_modal.members": " Members", + "channel_members_modal.addNew": " Add New Members", + "channel_members_modal.close": "Close", + "channel_notifications.allActivity": "For all activity", + "channel_notifications.onlyMentions": "Only for mentions", + "channel_notifications.never": "Never", + "channel_notifications.sendDesktop": "Send desktop notifications", + "channel_notifications.globalDefault": "Global default ({notifyLevel}", + "channel_notifications.override": "Selecting an option other than \"Default\" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.", + "channel_notifications.markUnread": "Mark Channel Unread", + "channel_notifications.allUnread": "For all unread messages", + "channel_notifications.unreadInfo": "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.", + "channel_notifications.preferences": "Notification Preferences for ", "claim.account.noEmail": "No email specified", "claim.email_to_sso.pwdError": "Please enter your password.", "claim.email_to_sso.pwd": "Password", @@ -502,6 +551,36 @@ "create_comment.commentTitle": "Comment", "create_comment.file": "File uploading", "create_comment.files": "Files uploading", + "delete_channel.channel": "channel", + "delete_channel.group": "group", + "delete_channel.confirm": "Confirm DELETE Channel", + "delete_channel.question": "Are you sure you wish to delete the {display_name} {term}?", + "delete_channel.cancel": "Cancel", + "delete_channel.del": "Delete", + "delete_post.warning": "This post has {count} comment(s) on it.", + "delete_post.comment": "Comment", + "delete_post.post": "Post", + "delete_post.confirm": "Confirm {term} Delete", + "delete_post.question": "Are you sure you want to delete this ${term}?", + "delete_post.cancel": "Cancel", + "delete_post.del": "Delete", + "edit_channel_header_modal.error": "This channel header is too long, please enter a shorter one", + "edit_channel_header_modal.title": "Edit Header for {channel}", + "edit_channel_header_modal.description": "Edit the text appearing next to the channel name in the channel header.", + "edit_channel_header_modal.cancel": "Cancel", + "edit_channel_header_modal.save": "Save", + "edit_channel_purpose_modal.error": "This channel purpose is too long, please enter a shorter one", + "edit_channel_purpose_modal.title1": "Edit Purpose", + "edit_channel_purpose_modal.title2": "Edit Purpose for ", + "edit_channel_purpose_modal.channel": "Channel", + "edit_channel_purpose_modal.group": "Group", + "edit_channel_purpose_modal.body": "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.", + "edit_channel_purpose_modal.cancel": "Cancel", + "edit_channel_purpose_modal.save": "Save", + "edit_post.editPost": "Edit the post...", + "edit_post.edit": "Edit {title}", + "edit_post.cancel": "Cancel", + "edit_post.save": "Save", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>", "email_verify.almost": "{siteName}: You are almost done", @@ -551,6 +630,14 @@ "login_email.email": "Email", "login_email.pwd": "Password", "login_email.signin": "Sign in", + "login_username.badTeam": "Bad team name", + "login_username.usernameReq": "A username is required", + "login_username.pwdReq": "A password is required", + "login_username.verifyEmailError": "Please verify your email address. Check your inbox for an email.", + "login_username.userNotFoundError": "We couldn't find an existing account matching your username for this team.", + "login_username.username": "Username", + "login_username.pwd": "Password", + "login_username.signin": "Sign in", "login_ldap.badTeam": "Bad team name", "login_ldap.idlReq": "An LDAP ID is required", "login_ldap.pwdReq": "An LDAP password is required", @@ -569,6 +656,10 @@ "login.find": "Find your other teams", "login.signTo": "Sign in to:", "login.on": "on {siteName}", + "member_item.add": " Add", + "member_item.makeAdmin": "Make Admin", + "member_item.removeMember": "Remove Member", + "member_item.member": "Member", "member_team_item.member": "Member", "member_team_item.systemAdmin": "System Admin", "member_team_item.teamAdmin": "Team Admin", @@ -644,6 +735,11 @@ "password_send.title": "Password Reset", "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", "password_send.reset": "Reset my password", + "members_popover.msg": "Message", + "members_popover.title": "Members", + "post_delete.notPosted": "Comment could not be posted", + "post_delete.someone": "Someone deleted the message on which you tried to post a comment.", + "post_delete.okay": "Okay", "register_app.required": "Required", "register_app.optional": "Optional", "register_app.nameError": "Application name must be filled in.", @@ -663,6 +759,23 @@ "register_app.credentialsSave": "I have saved both my Client Id and Client Secret somewhere safe", "register_app.close": "Close", "register_app.dev": "Developer Applications", + "removed_channel.channelName": "the channel", + "removed_channel.someone": "Someone", + "removed_channel.from": "Removed from ", + "removed_channel.remover": "{remover} removed you from {channel}", + "removed_channel.okay": "Okay", + "rename_channel.required": "This field is required", + "rename_channel.maxLength": "This field must be less than 22 characters", + "rename_channel.lowercase": "Must be lowercase alphanumeric characters", + "rename_channel.handle": "Handle", + "rename_channel.defaultError": " - Cannot be changed for the default channel", + "rename_channel.displayNameHolder": "Enter display name", + "rename_channel.handleHolder": "lowercase alphanumeric's only", + "rename_channel.close": "Close", + "rename_channel.title": "Rename Channel", + "rename_channel.displayName": "Display Name", + "rename_channel.cancel": "Cancel", + "rename_channel.save": "Save", "rhs_comment.comment": "Comment", "rhs_comment.edit": "Edit", "rhs_comment.del": "Delete", @@ -1045,4 +1158,4 @@ "user.settings.security.title": "Security Settings", "user.settings.security.viewHistory": "View Access History", "user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions" -}
\ No newline at end of file +} diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index cb3e8a199..92f3ba2ea 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -474,6 +474,7 @@ "authorize.app": "La app {appName} quiere tener la abilidad de accesar y modificar tu información básica.", "authorize.deny": "Denegar", "authorize.title": "Una aplicación quiere conectarse con tu cuenta de {teamName}", + "chanel_header.addMembers": "Agregar Miembros", "change_url.close": "Cerrar", "change_url.endWithLetter": "Debe terminar con una letra o número", "change_url.invalidUrl": "URL Inválida", @@ -488,6 +489,29 @@ "channel_flow.group": "Grupo", "channel_flow.invalidName": "Nombre de Canal Inválido", "channel_flow.set_url_title": "Asignar URL de {term}", + "channel_header.channel": "Canal", + "channel_header.channelHeader": "Encabezado del Canal...", + "channel_header.delete": "Eliminar {term}...", + "channel_header.group": "Grupo", + "channel_header.leave": "Abondanar {term}", + "channel_header.manageMembers": "Administrar Miembros", + "channel_header.notificationPreferences": "Preferencias de Notificación", + "channel_header.recentMentions": "Menciones recientes", + "channel_header.rename": "Renombrar {term}...", + "channel_header.setHeader": "Encabezado del {term}...", + "channel_header.setPurpose": "Propósito del {term}...", + "channel_header.viewInfo": "Ver Info", + "channel_info.close": "Cerrar", + "channel_info.id": "ID del Canal:", + "channel_info.name": "Nombre del Canal:", + "channel_info.notFound": "Canal no encontrado", + "channel_info.purpose": "Propósito del Canal:", + "channel_info.url": "URL del Canal:", + "channel_invite.addNewMembers": "Agregar nuevos Miembros a ", + "channel_invite.close": "Cerrar", + "channel_members_modal.addNew": " Agregar nuevos Miembros", + "channel_members_modal.close": "Cerrar", + "channel_memebers_modal.members": " Miembros", "channel_modal.cancel": "Cancelar", "channel_modal.channel": "Canal", "channel_modal.createNew": "Crear Nuevo ", @@ -504,6 +528,16 @@ "channel_modal.publicChannel1": "Crear un canal público", "channel_modal.publicChannel2": "Crear un canal público al que cualquiera puede unirse. ", "channel_modal.purpose": "Propósito", + "channel_notifications.allActivity": "Para toda actividad", + "channel_notifications.allUnread": "Para todos los mensajes sin leer", + "channel_notifications.globalDefault": "Predeterminado global ({notifyLevel})", + "channel_notifications.markUnread": "Marcar Canal como No Leido", + "channel_notifications.never": "Nunca", + "channel_notifications.onlyMentions": "Sólo para menciones", + "channel_notifications.override": "Seleccionar una opción diferente a \"Predeterminada\" anulará las configuraciones globales de notificación. Las notificaciones de Escritorio están disponibles para Firefox, Safari, y Chrome.", + "channel_notifications.preferences": "Preferencias de Notificación de ", + "channel_notifications.sendDesktop": "Enviar notificaciones de escritorio", + "channel_notifications.unreadInfo": "El nombre del canal está en negritas en la barra lateral cuando hay mensajes sin leer. Al elegir \"Sólo para menciones\" sólo lo dejará en negritas cuando seas mencionado.", "choose_auth_page.emailCreate": "Crea un nuevo equipo con tu cuenta de correo", "choose_auth_page.find": "Encontrar mi equipo", "choose_auth_page.gitlabCreate": "Crear un nuevo equipo con una cuenta de GitLab", @@ -531,6 +565,36 @@ "create_comment.commentTitle": "Comentario", "create_comment.file": "Subiendo archivo", "create_comment.files": "Subiendo archivos", + "delete_channel.cancel": "Cancelar", + "delete_channel.channel": "canal", + "delete_channel.confirm": "Confirmar la ELIMINACIÓN del Canal", + "delete_channel.del": "Eliminar", + "delete_channel.group": "grupo", + "delete_channel.question": "¿Estás seguro de querer eliminar el {term} {display_name}?", + "delete_post.cancel": "Cancelar", + "delete_post.comment": "Comentario", + "delete_post.confirm": "Confirmar Eliminación del {term}", + "delete_post.del": "Eliminar", + "delete_post.post": "Mensaje", + "delete_post.question": "¿Estás seguro(a) de querer eliminar este {term}?", + "delete_post.warning": "Este mensaje tiene {count} comentario(s).", + "edit_channel_header_modal.cancel": "Cancelar", + "edit_channel_header_modal.description": "Edita el texto que aparece al lado del nombre del canal en el encabezado del canal.", + "edit_channel_header_modal.error": "Este encabezado es demasiado largo, por favor ingresa uno más corto", + "edit_channel_header_modal.save": "Guardar", + "edit_channel_header_modal.title": "Edita el Encabezado de {channel}", + "edit_channel_purpose_modal.body": "Describe como este {type} debería ser usado. Este texto aparace en la lista de canales dentro del menú de \"Más...\" y ayuda a otros en decidir si unirse.", + "edit_channel_purpose_modal.cancel": "Cancelar", + "edit_channel_purpose_modal.channel": "Canal", + "edit_channel_purpose_modal.error": "El propósito de este canal es muy largo, por favor ingresa uno más corto", + "edit_channel_purpose_modal.group": "Grupo", + "edit_channel_purpose_modal.save": "Guardar", + "edit_channel_purpose_modal.title1": "Editar Propósito", + "edit_channel_purpose_modal.title2": "Editar el Propósito de ", + "edit_post.cancel": "Cancelar", + "edit_post.edit": "Editar {title}", + "edit_post.editPost": "Editar el mensaje...", + "edit_post.save": "Guardar", "email_signup.address": "Correo electrónico", "email_signup.createTeam": "Crear Equipo", "email_signup.emailError": "Por favor ingresa una dirección de correos válida", @@ -619,6 +683,10 @@ "login_ldap.pwdReq": "La contraseña LDAP es obligatoria", "login_ldap.signin": "Entrar", "login_ldap.username": "Usuario LDAP", + "member_item.add": " Agregar", + "member_item.makeAdmin": "Convertir en Admin de Equipo", + "member_item.member": "Miembro", + "member_item.removeMember": "Elminar Miembro", "member_team_item.inactive": "Inactivo", "member_team_item.makeActive": "Activar", "member_team_item.makeAdmin": "Convertir a Admin de Equipo", @@ -627,6 +695,8 @@ "member_team_item.member": "Miembro", "member_team_item.systemAdmin": "Administrador de Sistema", "member_team_item.teamAdmin": "Admin de Equipo", + "members_popover.msg": "Mensaje", + "members_popover.title": "Miembros", "more_channels.close": "Cerrar", "more_channels.create": "Crear Nuevo Canal", "more_channels.createClick": "Pincha 'Crear Nuevo Canal' para crear uno nuevo", @@ -670,6 +740,9 @@ "password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", + "post_delete.notPosted": "No se pudo enviar el comentario", + "post_delete.okay": "Ok", + "post_delete.someone": "Alguien borró el mensaje que querías comentar.", "register_app.callback": "Callback URL", "register_app.callbackError": "Al menos un callback URL debe ser ingresado.", "register_app.cancel": "Cancelar", @@ -689,6 +762,23 @@ "register_app.register": "Registrar", "register_app.required": "Requerido", "register_app.title": "Registra una Nueva Aplicación", + "removed_channel.channelName": "el canal", + "removed_channel.from": "Removido de ", + "removed_channel.okay": "OK", + "removed_channel.remover": "{remover} te removió de {channel}", + "removed_channel.someone": "Alguien", + "rename_channel.cancel": "Cancelar", + "rename_channel.close": "Cerrado", + "rename_channel.defaultError": " - No se puede cambiar el del canal predeterminado", + "rename_channel.displayName": "Nombre a Mostrar", + "rename_channel.displayNameHolder": "Ingresa el nombre a mostrar", + "rename_channel.handle": "Identificador", + "rename_channel.handleHolder": "Sólo caracteres alfanumericos y en minúscula", + "rename_channel.lowercase": "Debe tener caracteres alfanumericos y minúscula", + "rename_channel.maxLength": "Este campo debe tener menos de 22 caracteres", + "rename_channel.required": "Este campo es obligatorio", + "rename_channel.save": "Guardar", + "rename_channel.title": "Renombrar Canal", "rhs_comment.comment": "Comentario", "rhs_comment.del": "Borrar", "rhs_comment.edit": "Editar", @@ -1045,4 +1135,4 @@ "user.settings.security.title": "Configuración de Seguridad", "user.settings.security.viewHistory": "Visualizar historial de acceso", "user_profile.notShared": "Correo no compartido" -}
\ No newline at end of file +} |