summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/activity_log_modal.jsx6
-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/delete_post_modal.jsx12
-rw-r--r--web/react/components/file_info_preview.jsx11
-rw-r--r--web/react/components/suggestion/at_mention_provider.jsx6
-rw-r--r--web/react/components/team_signup_welcome_page.jsx8
-rw-r--r--web/react/pages/admin_console.jsx77
-rw-r--r--web/react/pages/authorize.jsx75
-rw-r--r--web/react/pages/claim_account.jsx71
-rw-r--r--web/react/pages/docs.jsx64
-rw-r--r--web/react/pages/password_reset.jsx71
-rw-r--r--web/react/pages/signup_team.jsx63
-rw-r--r--web/react/pages/signup_team_complete.jsx67
-rw-r--r--web/react/pages/signup_user_complete.jsx73
-rw-r--r--web/react/pages/verify.jsx65
-rw-r--r--web/react/utils/client.jsx18
20 files changed, 1082 insertions, 331 deletions
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index f5341c0bc..6a880f0ee 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -100,8 +100,12 @@ export default class ActivityLogModal extends React.Component {
if (currentSession.props.platform === 'Windows') {
devicePicture = 'fa fa-windows';
- } else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') {
+ } else if (currentSession.props.platform === 'Macintosh' ||
+ currentSession.props.platform === 'iPhone') {
devicePicture = 'fa fa-apple';
+ } else if (currentSession.props.platform.browser.indexOf('Mattermost/') === 0) {
+ devicePicture = 'fa fa-apple';
+ devicePlatform = 'iPhone';
} else if (currentSession.props.platform === 'Linux') {
if (currentSession.props.os.indexOf('Android') >= 0) {
devicePlatform = 'Android';
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/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx
index 827654e1b..4cde5feed 100644
--- a/web/react/components/delete_post_modal.jsx
+++ b/web/react/components/delete_post_modal.jsx
@@ -23,7 +23,7 @@ export default class DeletePostModal extends React.Component {
this.selectedList = null;
this.state = {
- show: true,
+ show: false,
post: null,
commentCount: 0,
error: ''
@@ -40,6 +40,14 @@ export default class DeletePostModal extends React.Component {
ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
}
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.show && !prevState.show) {
+ setTimeout(() => {
+ $(ReactDOM.findDOMNode(this.refs.deletePostBtn)).focus();
+ }, 0);
+ }
+ }
+
handleDelete() {
Client.deletePost(
this.state.post.channel_id,
@@ -149,10 +157,10 @@ export default class DeletePostModal extends React.Component {
{'Cancel'}
</button>
<button
+ ref='deletePostBtn'
type='button'
className='btn btn-danger'
onClick={this.handleDelete}
- autoFocus='autofocus'
>
{'Delete'}
</button>
diff --git a/web/react/components/file_info_preview.jsx b/web/react/components/file_info_preview.jsx
index 4b76cd162..45d89007f 100644
--- a/web/react/components/file_info_preview.jsx
+++ b/web/react/components/file_info_preview.jsx
@@ -5,11 +5,16 @@ import * as Utils from '../utils/utils.jsx';
export default function FileInfoPreview({filename, fileUrl, fileInfo}) {
// non-image files include a section providing details about the file
- let infoString = 'File type ' + fileInfo.extension.toUpperCase();
- if (fileInfo.size > 0) {
- infoString += ', Size ' + Utils.fileSizeToString(fileInfo.size);
+ const infoParts = [];
+
+ if (fileInfo.extension !== '') {
+ infoParts.push('File type ' + fileInfo.extension.toUpperCase());
}
+ infoParts.push('Size ' + Utils.fileSizeToString(fileInfo.size));
+
+ const infoString = infoParts.join(', ');
+
const name = decodeURIComponent(Utils.getFileName(filename));
return (
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_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/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
index cbd2bd80d..3f4c39934 100644
--- a/web/react/pages/admin_console.jsx
+++ b/web/react/pages/admin_console.jsx
@@ -4,25 +4,68 @@
import ErrorBar from '../components/error_bar.jsx';
import SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
import AdminController from '../components/admin_console/admin_controller.jsx';
+import * as Client from '../utils/client.jsx';
-export function setupAdminConsolePage(props) {
- ReactDOM.render(
- <AdminController
- tab={props.ActiveTab}
- teamId={props.TeamId}
- />,
- document.getElementById('admin_controller')
- );
+var IntlProvider = ReactIntl.IntlProvider;
- ReactDOM.render(
- <SelectTeamModal />,
- document.getElementById('select_team_modal')
- );
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
- ReactDOM.render(
- <ErrorBar/>,
- document.getElementById('error_bar')
- );
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <div>
+ <ErrorBar/>
+ <AdminController
+ tab={this.props.map.ActiveTab}
+ teamId={this.props.map.TeamId}
+ />
+ <SelectTeamModal />
+ </div>
+ </IntlProvider>
+ );
+ }
}
-global.window.setup_admin_console_page = setupAdminConsolePage;
+global.window.setup_admin_console_page = function setup(props) {
+ ReactDOM.render(
+ <Root map={props} />,
+ document.getElementById('admin_controller')
+ );
+};
diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx
index 71f17d007..7474332ce 100644
--- a/web/react/pages/authorize.jsx
+++ b/web/react/pages/authorize.jsx
@@ -2,20 +2,69 @@
// See License.txt for license information.
import Authorize from '../components/authorize.jsx';
+import * as Client from '../utils/client.jsx';
-function setupAuthorizePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <Authorize
+ teamName={this.props.map.TeamName}
+ appName={this.props.map.AppName}
+ responseType={this.props.map.ResponseType}
+ clientId={this.props.map.ClientId}
+ redirectUri={this.props.map.RedirectUri}
+ scope={this.props.map.Scope}
+ state={this.props.map.State}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_authorize_page = function setup(props) {
ReactDOM.render(
- <Authorize
- teamName={props.TeamName}
- appName={props.AppName}
- responseType={props.ResponseType}
- clientId={props.ClientId}
- redirectUri={props.RedirectUri}
- scope={props.Scope}
- state={props.State}
- />,
+ <Root map={props} />,
document.getElementById('authorize')
);
-}
-
-global.window.setup_authorize_page = setupAuthorizePage;
+};
diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx
index bca203d96..7c6af73ca 100644
--- a/web/react/pages/claim_account.jsx
+++ b/web/react/pages/claim_account.jsx
@@ -2,18 +2,67 @@
// See License.txt for license information.
import ClaimAccount from '../components/claim/claim_account.jsx';
+import * as Client from '../utils/client.jsx';
-function setupClaimAccountPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <ClaimAccount
+ email={this.props.map.Email}
+ currentType={this.props.map.CurrentType}
+ newType={this.props.map.NewType}
+ teamName={this.props.map.TeamName}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_claim_account_page = function setup(props) {
ReactDOM.render(
- <ClaimAccount
- email={props.Email}
- currentType={props.CurrentType}
- newType={props.NewType}
- teamName={props.TeamName}
- teamDisplayName={props.TeamDisplayName}
- />,
+ <Root map={props} />,
document.getElementById('claim')
);
-}
-
-global.window.setup_claim_account_page = setupClaimAccountPage;
+}; \ No newline at end of file
diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx
index 74d9c2d19..2f5d4db55 100644
--- a/web/react/pages/docs.jsx
+++ b/web/react/pages/docs.jsx
@@ -2,15 +2,63 @@
// See License.txt for license information.
import Docs from '../components/docs.jsx';
+import * as Client from '../utils/client.jsx';
-function setupDocumentationPage(props) {
- ReactDOM.render(
- <Docs
- site={props.Site}
- />,
- document.getElementById('docs')
- );
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <Docs site={this.props.map.Site} />
+ </IntlProvider>
+ );
+ }
}
global.window.mm_user = global.window.mm_user || {};
-global.window.setup_documentation_page = setupDocumentationPage;
+
+global.window.setup_documentation_page = function setup(props) {
+ ReactDOM.render(
+ <Root map={props} />,
+ document.getElementById('docs')
+ );
+};
diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx
index 4a6f1dcb0..23bbf2691 100644
--- a/web/react/pages/password_reset.jsx
+++ b/web/react/pages/password_reset.jsx
@@ -2,18 +2,67 @@
// See License.txt for license information.
import PasswordReset from '../components/password_reset.jsx';
+import * as Client from '../utils/client.jsx';
-function setupPasswordResetPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <PasswordReset
+ isReset={this.props.map.IsReset}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ teamName={this.props.map.TeamName}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_password_reset_page = function setup(props) {
ReactDOM.render(
- <PasswordReset
- isReset={props.IsReset}
- teamDisplayName={props.TeamDisplayName}
- teamName={props.TeamName}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('reset')
);
-}
-
-global.window.setup_password_reset_page = setupPasswordResetPage;
+};
diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx
index 08ea45000..8f4f86a7c 100644
--- a/web/react/pages/signup_team.jsx
+++ b/web/react/pages/signup_team.jsx
@@ -2,8 +2,60 @@
// See License.txt for license information.
import SignupTeam from '../components/signup_team.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupTeamPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired,
+ teams: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupTeam teams={this.props.teams} />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_team_page = function setup(props) {
var teams = [];
for (var prop in props) {
@@ -15,9 +67,10 @@ function setupSignupTeamPage(props) {
}
ReactDOM.render(
- <SignupTeam teams={teams} />,
+ <Root
+ map={props}
+ teams={teams}
+ />,
document.getElementById('signup-team')
);
-}
-
-global.window.setup_signup_team_page = setupSignupTeamPage;
+}; \ No newline at end of file
diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx
index d5ed144a1..1bee4e598 100644
--- a/web/react/pages/signup_team_complete.jsx
+++ b/web/react/pages/signup_team_complete.jsx
@@ -2,16 +2,65 @@
// See License.txt for license information.
import SignupTeamComplete from '../components/signup_team_complete.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupTeamCompletePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupTeamComplete
+ email={this.props.map.Email}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_team_complete_page = function setup(props) {
ReactDOM.render(
- <SignupTeamComplete
- email={props.Email}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('signup-team-complete')
);
-}
-
-global.window.setup_signup_team_complete_page = setupSignupTeamCompletePage;
+}; \ No newline at end of file
diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx
index de2c48443..6c761c1ee 100644
--- a/web/react/pages/signup_user_complete.jsx
+++ b/web/react/pages/signup_user_complete.jsx
@@ -2,19 +2,68 @@
// See License.txt for license information.
import SignupUserComplete from '../components/signup_user_complete.jsx';
+import * as Client from '../utils/client.jsx';
-function setupSignupUserCompletePage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <SignupUserComplete
+ teamId={this.props.map.TeamId}
+ teamName={this.props.map.TeamName}
+ teamDisplayName={this.props.map.TeamDisplayName}
+ email={this.props.map.Email}
+ hash={this.props.map.Hash}
+ data={this.props.map.Data}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setup_signup_user_complete_page = function setup(props) {
ReactDOM.render(
- <SignupUserComplete
- teamId={props.TeamId}
- teamName={props.TeamName}
- teamDisplayName={props.TeamDisplayName}
- email={props.Email}
- hash={props.Hash}
- data={props.Data}
- />,
+ <Root map={props} />,
document.getElementById('signup-user-complete')
);
-}
-
-global.window.setup_signup_user_complete_page = setupSignupUserCompletePage;
+}; \ No newline at end of file
diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx
index d4ce4844d..2fc619e58 100644
--- a/web/react/pages/verify.jsx
+++ b/web/react/pages/verify.jsx
@@ -2,15 +2,66 @@
// See License.txt for license information.
import EmailVerify from '../components/email_verify.jsx';
+import * as Client from '../utils/client.jsx';
-global.window.setupVerifyPage = function setupVerifyPage(props) {
+var IntlProvider = ReactIntl.IntlProvider;
+
+class Root extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ translations: null,
+ loaded: false
+ };
+ }
+
+ static propTypes() {
+ return {
+ map: React.PropTypes.object.isRequired
+ };
+ }
+
+ componentWillMount() {
+ Client.getTranslations(
+ this.props.map.Locale,
+ (data) => {
+ this.setState({
+ translations: data,
+ loaded: true
+ });
+ },
+ () => {
+ this.setState({
+ loaded: true
+ });
+ }
+ );
+ }
+
+ render() {
+ if (!this.state.loaded) {
+ return <div></div>;
+ }
+
+ return (
+ <IntlProvider
+ locale={this.props.map.Locale}
+ messages={this.state.translations}
+ >
+ <EmailVerify
+ isVerified={this.props.map.IsVerified}
+ teamURL={this.props.map.TeamURL}
+ userEmail={this.props.map.UserEmail}
+ resendSuccess={this.props.map.ResendSuccess}
+ />
+ </IntlProvider>
+ );
+ }
+}
+
+global.window.setupVerifyPage = function setup(props) {
ReactDOM.render(
- <EmailVerify
- isVerified={props.IsVerified}
- teamURL={props.TeamURL}
- userEmail={props.UserEmail}
- resendSuccess={props.ResendSuccess}
- />,
+ <Root map={props} />,
document.getElementById('verify')
);
};
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 80b29da4e..855de3fc2 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -399,7 +399,7 @@ export function getConfig(success, error) {
});
}
-export function getAnalytics(teamId, name, success, error) {
+export function getTeamAnalytics(teamId, name, success, error) {
$.ajax({
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
dataType: 'json',
@@ -407,7 +407,21 @@ export function getAnalytics(teamId, name, success, error) {
type: 'GET',
success,
error: (xhr, status, err) => {
- var e = handleError('getAnalytics', xhr, status, err);
+ var e = handleError('getTeamAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function getSystemAnalytics(name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('getSystemAnalytics', xhr, status, err);
error(e);
}
});