summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/admin_console/admin_controller.jsx7
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx19
-rw-r--r--web/react/components/admin_console/analytics.jsx279
-rw-r--r--web/react/components/admin_console/system_analytics.jsx161
-rw-r--r--web/react/components/admin_console/team_analytics.jsx260
-rw-r--r--web/react/components/create_comment.jsx2
-rw-r--r--web/react/components/create_post.jsx2
-rw-r--r--web/react/components/login.jsx17
-rw-r--r--web/react/components/sidebar.jsx4
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx6
-rw-r--r--web/react/components/team_general_tab.jsx2
-rw-r--r--web/react/components/team_signup_welcome_page.jsx8
-rw-r--r--web/react/components/user_settings/manage_languages.jsx101
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx46
-rw-r--r--web/react/components/view_image.jsx3
15 files changed, 679 insertions, 238 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 0f85c238d..efd163017 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.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 AdminSidebar from './admin_sidebar.jsx';
@@ -23,6 +23,7 @@ import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
+import SystemAnalyticsTab from './system_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {
@@ -45,7 +46,7 @@ export default class AdminController extends React.Component {
config: AdminStore.getConfig(),
teams: AdminStore.getAllTeams(),
selectedTeams,
- selected: props.tab || 'service_settings',
+ selected: props.tab || 'system_analytics',
selectedTeam: props.teamId || null
};
@@ -165,6 +166,8 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'system_analytics') {
+ tab = <SystemAnalyticsTab />;
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 5a5eaa055..66f82c55b 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -196,6 +196,25 @@ export default class AdminSidebar extends React.Component {
<li>
<h4>
<span className='icon fa fa-gear'></span>
+ <span>{'SITE REPORTS'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('system_analytics')}
+ onClick={this.handleClick.bind(this, 'system_analytics', null)}
+ >
+ {'View Statistics'}
+ </a>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
<span>{'SETTINGS'}</span>
</h4>
</li>
diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx
new file mode 100644
index 000000000..70ef1ecab
--- /dev/null
+++ b/web/react/components/admin_console/analytics.jsx
@@ -0,0 +1,279 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../../utils/utils.jsx';
+import Constants from '../../utils/constants.jsx';
+import LineChart from './line_chart.jsx';
+
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class Analytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+
+ render() { // in the future, break down these into smaller components
+ var serverError = '';
+ if (this.props.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
+ <div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div>
+ </div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
+ <div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div>
+ </div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
+ <div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div>
+ </div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
+ <div className='content'>{this.props.postCount == null ? 'Loading...' : this.props.postCount}</div>
+ </div>
+ </div>
+ );
+
+ var postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.props.postCountsDay != null) {
+ let content;
+ if (this.props.postCountsDay.labels.length === 0) {
+ content = 'Not enough data for a meaningful representation.';
+ } else {
+ content = (
+ <LineChart
+ data={this.props.postCountsDay}
+ width='740'
+ height='225'
+ />
+ );
+ }
+ postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.props.userCountsWithPostsDay != null) {
+ let content;
+ if (this.props.userCountsWithPostsDay.labels.length === 0) {
+ content = 'Not enough data for a meaningful representation.';
+ } else {
+ content = (
+ <LineChart
+ data={this.props.userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ );
+ }
+ usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ let recentActiveUser;
+ if (this.props.recentActiveUsers != null) {
+ let content;
+ if (this.props.recentActiveUsers.length === 0) {
+ content = 'Loading...';
+ } else {
+ content = (
+ <table>
+ <tbody>
+ {
+ this.props.recentActiveUsers.map((user) => {
+ const tooltip = (
+ <Tooltip id={'recent-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'recent-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ );
+ }
+ recentActiveUser = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Recent Active Users'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ let newUsers;
+ if (this.props.newlyCreatedUsers != null) {
+ let content;
+ if (this.props.newlyCreatedUsers.length === 0) {
+ content = 'Loading...';
+ } else {
+ content = (
+ <table>
+ <tbody>
+ {
+ this.props.newlyCreatedUsers.map((user) => {
+ const tooltip = (
+ <Tooltip id={'new-user-email-tooltip-' + user.id}>
+ {user.email}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'new-user-table-entry-' + user.id}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {user.username}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ );
+ }
+ newUsers = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Newly Created Users'}</div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>{'Statistics for ' + this.props.title}</h3>
+ {serverError}
+ <div className='row'>
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ <div className='row'>
+ {postCountsByDay}
+ </div>
+ <div className='row'>
+ {usersWithPostsByDay}
+ </div>
+ <div className='row'>
+ {recentActiveUser}
+ {newUsers}
+ </div>
+ </div>
+ );
+ }
+}
+
+Analytics.defaultProps = {
+ title: null,
+ channelOpenCount: null,
+ channelPrivateCount: null,
+ postCount: null,
+ postCountsDay: null,
+ userCountsWithPostsDay: null,
+ recentActiveUsers: null,
+ newlyCreatedUsers: null,
+ uniqueUserCount: null,
+ serverError: null
+};
+
+Analytics.propTypes = {
+ title: React.PropTypes.string,
+ channelOpenCount: React.PropTypes.number,
+ channelPrivateCount: React.PropTypes.number,
+ postCount: React.PropTypes.number,
+ postCountsDay: React.PropTypes.object,
+ userCountsWithPostsDay: React.PropTypes.object,
+ recentActiveUsers: React.PropTypes.array,
+ newlyCreatedUsers: React.PropTypes.array,
+ uniqueUserCount: React.PropTypes.number,
+ serverError: React.PropTypes.string
+};
diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx
new file mode 100644
index 000000000..f54813a94
--- /dev/null
+++ b/web/react/components/admin_console/system_analytics.jsx
@@ -0,0 +1,161 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Analytics from './analytics.jsx';
+import * as Client from '../../utils/client.jsx';
+
+export default class SystemAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = { // most of this state should be from a store in the future
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null,
+ unique_user_count: null
+ };
+ }
+
+ componentDidMount() {
+ this.getData();
+ }
+
+ getData() { // should be moved to an action creator eventually
+ Client.getSystemAnalytics(
+ 'standard',
+ (data) => {
+ for (var index in data) {
+ if (data[index].name === 'channel_open_count') {
+ this.setState({channel_open_count: data[index].value});
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ this.setState({channel_private_count: data[index].value});
+ }
+
+ if (data[index].name === 'post_count') {
+ this.setState({post_count: data[index].value});
+ }
+
+ if (data[index].name === 'unique_user_count') {
+ this.setState({unique_user_count: data[index].value});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getSystemAnalytics(
+ 'post_counts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Total Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({post_counts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getSystemAnalytics(
+ 'user_counts_with_posts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Active Users With Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({user_counts_with_posts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ componentWillReceiveProps() {
+ this.setState({
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ unique_user_count: null
+ });
+
+ this.getData();
+ }
+
+ render() {
+ return (
+ <div>
+ <Analytics
+ title={'the System'}
+ channelOpenCount={this.state.channel_open_count}
+ channelPrivateCount={this.state.channel_private_count}
+ postCount={this.state.post_count}
+ postCountsDay={this.state.post_counts_day}
+ userCountsWithPostsDay={this.state.user_counts_with_posts_day}
+ uniqueUserCount={this.state.unique_user_count}
+ serverError={this.state.serverError}
+ />
+ </div>
+ );
+ }
+}
+
+SystemAnalytics.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
index fe7230946..c164dd98c 100644
--- a/web/react/components/admin_console/team_analytics.jsx
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -1,13 +1,8 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import Analytics from './analytics.jsx';
import * as Client from '../../utils/client.jsx';
-import * as Utils from '../../utils/utils.jsx';
-import Constants from '../../utils/constants.jsx';
-import LineChart from './line_chart.jsx';
-
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class TeamAnalytics extends React.Component {
constructor(props) {
@@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component {
this.getData = this.getData.bind(this);
- this.state = {
+ this.state = { // most of this state should be from a store in the future
users: null,
serverError: null,
channel_open_count: null,
@@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
- newly_created_users: null
+ newly_created_users: null,
+ unique_user_count: null
};
}
@@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component {
this.getData(this.props.team.id);
}
- getData(teamId) {
- Client.getAnalytics(
+ getData(teamId) { // should be moved to an action creator eventually
+ Client.getTeamAnalytics(
teamId,
'standard',
(data) => {
@@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component {
if (data[index].name === 'post_count') {
this.setState({post_count: data[index].value});
}
+
+ if (data[index].name === 'unique_user_count') {
+ this.setState({unique_user_count: data[index].value});
+ }
}
},
(err) => {
@@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component {
}
);
- Client.getAnalytics(
+ Client.getTeamAnalytics(
teamId,
'post_counts_day',
(data) => {
@@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component {
}
);
- Client.getAnalytics(
+ Client.getTeamAnalytics(
teamId,
'user_counts_with_posts_day',
(data) => {
@@ -152,6 +152,10 @@ export default class TeamAnalytics extends React.Component {
var recentActive = [];
for (let i = 0; i < usersList.length; i++) {
+ if (usersList[i].last_activity_at == null) {
+ continue;
+ }
+
recentActive.push(usersList[i]);
if (i > 19) {
break;
@@ -198,227 +202,29 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
- newly_created_users: null
+ newly_created_users: null,
+ unique_user_count: null
});
this.getData(newProps.team.id);
}
- componentWillUnmount() {
- }
-
render() {
- var serverError = '';
- if (this.state.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
- }
-
- var totalCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
- <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
- </div>
- </div>
- );
-
- var openChannelCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
- <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
- </div>
- </div>
- );
-
- var openPrivateCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
- <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
- </div>
- </div>
- );
-
- var postCount = (
- <div className='col-sm-3'>
- <div className='total-count'>
- <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
- <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
- </div>
- </div>
- );
-
- var postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div className='content'>{'Loading...'}</div>
- </div>
- </div>
- );
-
- if (this.state.post_counts_day != null) {
- postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div className='content'>
- <LineChart
- data={this.state.post_counts_day}
- width='740'
- height='225'
- />
- </div>
- </div>
- </div>
- );
- }
-
- var usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Total Posts'}</div>
- <div>{'Loading...'}</div>
- </div>
- </div>
- );
-
- if (this.state.user_counts_with_posts_day != null) {
- usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>{'Active Users With Posts'}</div>
- <div className='content'>
- <LineChart
- data={this.state.user_counts_with_posts_day}
- width='740'
- height='225'
- />
- </div>
- </div>
- </div>
- );
- }
-
- var recentActiveUser = (
- <div className='recent-active-users'>
- <div>{'Recent Active Users'}</div>
- <div>{'Loading...'}</div>
- </div>
- );
-
- if (this.state.recent_active_users != null) {
- recentActiveUser = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>{'Recent Active Users'}</div>
- <div className='content'>
- <table>
- <tbody>
- {
- this.state.recent_active_users.map((user) => {
- const tooltip = (
- <Tooltip id={'recent-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'recent-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>{Utils.displayDateTime(user.last_activity_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- </div>
- </div>
- </div>
- );
- }
-
- var newUsers = (
- <div className='recent-active-users'>
- <div>{'Newly Created Users'}</div>
- <div>{'Loading...'}</div>
- </div>
- );
-
- if (this.state.newly_created_users != null) {
- newUsers = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>{'Newly Created Users'}</div>
- <div className='content'>
- <table>
- <tbody>
- {
- this.state.newly_created_users.map((user) => {
- const tooltip = (
- <Tooltip id={'new-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'new-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>{Utils.displayDateTime(user.create_at)}</td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- </div>
- </div>
- </div>
- );
- }
-
return (
- <div className='wrapper--fixed team_statistics'>
- <h3>{'Statistics for ' + this.props.team.name}</h3>
- {serverError}
- <div className='row'>
- {totalCount}
- {postCount}
- {openChannelCount}
- {openPrivateCount}
- </div>
- <div className='row'>
- {postCountsByDay}
- </div>
- <div className='row'>
- {usersWithPostsByDay}
- </div>
- <div className='row'>
- {recentActiveUser}
- {newUsers}
- </div>
+ <div>
+ <Analytics
+ title={this.props.team.name}
+ users={this.state.users}
+ channelOpenCount={this.state.channel_open_count}
+ channelPrivateCount={this.state.channel_private_count}
+ postCount={this.state.post_count}
+ postCountsDay={this.state.post_counts_day}
+ userCountsWithPostsDay={this.state.user_counts_with_posts_day}
+ recentActiveUsers={this.state.recent_active_users}
+ newlyCreatedUsers={this.state.newly_created_users}
+ uniqueUserCount={this.state.unique_user_count}
+ serverError={this.state.serverError}
+ />
</div>
);
}
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index cae94429c..aa7ab6a7b 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -129,7 +129,7 @@ export default class CreateComment extends React.Component {
function handlePostError(err) {
let state = {};
- if (err.message === 'Invalid RootId parameter') {
+ if (err.id === 'api.post.create_post.root_id.app_error') {
PostStore.removePendingPost(post.channel_id, post.pending_post_id);
if ($('#post_deleted').length > 0) {
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index a476863a3..de971c43f 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -191,7 +191,7 @@ export default class CreatePost extends React.Component {
(err) => {
const state = {};
- if (err.message === 'Invalid RootId parameter') {
+ if (err.id === 'api.post.create_post.root_id.app_error') {
if ($('#post_deleted').length > 0) {
$('#post_deleted').modal('show');
}
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 1d9b3e906..6887489a7 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -7,6 +7,8 @@ import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
+var FormattedMessage = ReactIntl.FormattedMessage;
+
export default class Login extends React.Component {
constructor(props) {
super(props);
@@ -86,7 +88,12 @@ export default class Login extends React.Component {
if (emailSignup) {
forgotPassword = (
<div className='form-group'>
- <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a>
+ <a href={'/' + teamName + '/reset_password'}>
+ <FormattedMessage
+ id='login.forgot_password'
+ defaultMessage='I forgot my password'
+ />
+ </a>
</div>
);
}
@@ -141,7 +148,13 @@ export default class Login extends React.Component {
{ldapLogin}
{userSignUp}
<div className='form-group margin--extra form-group--small'>
- <span><a href='/find_team'>{'Find your other teams'}</a></span>
+ <span>
+ <a href='/find_team'>
+ <FormattedMessage
+ id='login.find_teams'
+ defaultMessage='Find your other teams'
+ />
+ </a></span>
</div>
{forgotPassword}
{teamSignUp}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index eaeb7bb91..c902731c9 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -506,9 +506,9 @@ export default class Sidebar extends React.Component {
link.rel = 'shortcut icon';
link.id = 'favicon';
if (this.badgesActive) {
- link.href = '/static/images/redfavicon.ico';
+ link.href = '/static/images/favicon/redfavicon-16x16.png';
} else {
- link.href = '/static/images/favicon.ico';
+ link.href = '/static/images/favicon/favicon-16x16.png';
}
var head = document.getElementsByTagName('head')[0];
var oldLink = document.getElementById('favicon');
diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx
index 8c2893448..e502c981d 100644
--- a/web/react/components/suggestion/at_mention_provider.jsx
+++ b/web/react/components/suggestion/at_mention_provider.jsx
@@ -5,6 +5,8 @@ import SuggestionStore from '../../stores/suggestion_store.jsx';
import UserStore from '../../stores/user_store.jsx';
import * as Utils from '../../utils/utils.jsx';
+const MaxUserSuggestions = 40;
+
class AtMentionSuggestion extends React.Component {
render() {
const {item, isSelection, onClick} = this.props;
@@ -78,6 +80,10 @@ export default class AtMentionProvider {
if (user.username.startsWith(usernamePrefix)) {
filtered.push(user);
}
+
+ if (filtered.length >= MaxUserSuggestions) {
+ break;
+ }
}
// add dummy users to represent the @all and @channel special mentions
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index cc06a940e..b6fb3389f 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -66,7 +66,7 @@ export default class GeneralTab extends React.Component {
handleTeamListingRadio(listing) {
if (global.window.mm_config.EnableTeamListing !== 'true' && listing) {
- this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'});
+ this.setState({clientError: 'Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.'});
} else {
this.setState({allow_team_listing: listing});
}
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index aa91a1329..a374dd363 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -59,7 +59,13 @@ export default class TeamSignupWelcomePage extends React.Component {
}
}.bind(this),
function error(err) {
- this.setState({serverError: err.message});
+ let errorMsg = err.message;
+
+ if (err.detailed_error.indexOf('Invalid RCPT TO address provided') >= 0) {
+ errorMsg = 'Please enter a valid email address';
+ }
+
+ this.setState({emailError: '', serverError: errorMsg});
}.bind(this)
);
}
diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx
new file mode 100644
index 000000000..123165b76
--- /dev/null
+++ b/web/react/components/user_settings/manage_languages.jsx
@@ -0,0 +1,101 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Client from '../../utils/client.jsx';
+import * as Utils from '../../utils/utils.jsx';
+
+export default class ManageLanguage extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.setupInitialState = this.setupInitialState.bind(this);
+ this.setLanguage = this.setLanguage.bind(this);
+ this.changeLanguage = this.changeLanguage.bind(this);
+ this.submitUser = this.submitUser.bind(this);
+ this.state = this.setupInitialState(props);
+ }
+ setupInitialState(props) {
+ var user = props.user;
+ return {
+ languages: Utils.languages(),
+ locale: user.locale
+ };
+ }
+ setLanguage(e) {
+ this.setState({locale: e.target.value});
+ }
+ changeLanguage(e) {
+ e.preventDefault();
+
+ var user = this.props.user;
+ var locale = this.state.locale;
+
+ user.locale = locale;
+
+ this.submitUser(user);
+ }
+ submitUser(user) {
+ Client.updateUser(user,
+ () => {
+ window.location.reload(true);
+ },
+ (err) => {
+ let serverError;
+ if (err.message) {
+ serverError = err.message;
+ } else {
+ serverError = err;
+ }
+ this.setState({serverError});
+ }
+ );
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const options = [];
+ this.state.languages.forEach((lang) => {
+ options.push(
+ <option
+ key={lang.value}
+ value={lang.value}
+ >
+ {lang.name}
+ </option>);
+ });
+
+ return (
+ <div key='changeLanguage'>
+ <br/>
+ <label className='control-label'>{'Change interface language'}</label>
+ <div className='padding-top'>
+ <select
+ ref='language'
+ className='form-control'
+ value={this.state.locale}
+ onChange={this.setLanguage}
+ >
+ {options}
+ </select>
+ {serverError}
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ onClick={this.changeLanguage}
+ >
+ {'Set language'}
+ </a>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+ManageLanguage.propTypes = {
+ user: React.PropTypes.object
+}; \ No newline at end of file
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 1ff0a2913..f2c2502fb 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -5,7 +5,9 @@ import {savePreferences} from '../../utils/client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
+const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
import PreferenceStore from '../../stores/preference_store.jsx';
+import ManageLanguages from './manage_languages.jsx';
import * as Utils from '../../utils/utils.jsx';
function getDisplayStateFromStores() {
@@ -78,6 +80,7 @@ export default class UserSettingsDisplay extends React.Component {
let clockSection;
let nameFormatSection;
let fontSection;
+ let languagesSection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
@@ -292,6 +295,48 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (Utils.isFeatureEnabled(PreReleaseFeatures.LOC_PREVIEW)) {
+ if (this.props.activeSection === 'languages') {
+ var inputs = [];
+ inputs.push(
+ <ManageLanguages
+ user={this.props.user}
+ key='languages-ui'
+ />
+ );
+
+ languagesSection = (
+ <SettingItemMax
+ title={'Language'}
+ width='medium'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ var locale = 'English';
+ Utils.languages().forEach((l) => {
+ if (l.value === this.props.user.locale) {
+ locale = l.name;
+ }
+ });
+
+ languagesSection = (
+ <SettingItemMin
+ title={'Language'}
+ width='medium'
+ describe={locale}
+ updateSection={() => {
+ this.updateSection('languages');
+ }}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -324,6 +369,7 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark'/>
{nameFormatSection}
<div className='divider-dark'/>
+ {languagesSection}
</div>
</div>
);
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index 31ec91248..d11f8a21c 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -211,7 +211,7 @@ export default class ViewImageModal extends React.Component {
}
const filename = this.props.filenames[this.state.imgId];
- const fileUrl = Utils.getFileUrl(filename);
+ const fileUrl = Utils.getFileUrl(filename, true);
var content;
if (this.state.loaded[this.state.imgId]) {
@@ -377,6 +377,7 @@ function ImagePreview({filename, fileUrl, fileInfo, maxHeight}) {
<a
href={fileUrl}
target='_blank'
+ download={true}
>
<img
style={{maxHeight}}