diff options
Diffstat (limited to 'web/react')
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); } }); |