diff options
Diffstat (limited to 'web/react')
-rw-r--r-- | web/react/.eslintrc | 3 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_controller.jsx | 5 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_sidebar.jsx | 11 | ||||
-rw-r--r-- | web/react/components/admin_console/line_chart.jsx | 50 | ||||
-rw-r--r-- | web/react/components/admin_console/team_analytics.jsx | 353 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 14 |
6 files changed, 434 insertions, 2 deletions
diff --git a/web/react/.eslintrc b/web/react/.eslintrc index 6a35d3123..d78068882 100644 --- a/web/react/.eslintrc +++ b/web/react/.eslintrc @@ -20,7 +20,8 @@ "globals": { "React": false, "ReactDOM": false, - "ReactBootstrap": false + "ReactBootstrap": false, + "Chart": false }, "rules": { "comma-dangle": [2, "never"], diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index f770d166c..d309ced2e 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx'); var TeamSettingsTab = require('./team_settings.jsx'); var ServiceSettingsTab = require('./service_settings.jsx'); var TeamUsersTab = require('./team_users.jsx'); +var TeamAnalyticsTab = require('./team_analytics.jsx'); export default class AdminController extends React.Component { constructor(props) { @@ -149,6 +150,10 @@ export default class AdminController extends React.Component { if (this.state.teams) { tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; } + } else if (this.state.selected === 'team_analytics') { + if (this.state.teams) { + tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />; + } } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index b0e01ff17..f2fb1c96d 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component { handleClick(name, teamId, e) { e.preventDefault(); this.props.selectTab(name, teamId); - history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`); + history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`); } isSelected(name, teamId) { @@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component { {'- Users'} </a> </li> + <li> + <a + href='#' + className={this.isSelected('team_analytics', team.id)} + onClick={this.handleClick.bind(this, 'team_analytics', team.id)} + > + {'- Statistics'} + </a> + </li> </ul> </li> </ul> diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx new file mode 100644 index 000000000..7e2f95c84 --- /dev/null +++ b/web/react/components/admin_console/line_chart.jsx @@ -0,0 +1,50 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +export default class LineChart extends React.Component { + constructor(props) { + super(props); + + this.initChart = this.initChart.bind(this); + this.chart = null; + } + + componentDidMount() { + this.initChart(this.props); + } + + componentWillReceiveProps(nextProps) { + if (this.chart) { + this.chart.destroy(); + this.initChart(nextProps); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + initChart(props) { + var el = ReactDOM.findDOMNode(this); + var ctx = el.getContext('2d'); + this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap + } + + render() { + return ( + <canvas + width={this.props.width} + height={this.props.height} + /> + ); + } +} + +LineChart.propTypes = { + width: React.PropTypes.string, + height: React.PropTypes.string, + data: React.PropTypes.object, + options: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx new file mode 100644 index 000000000..dd8812ad0 --- /dev/null +++ b/web/react/components/admin_console/team_analytics.jsx @@ -0,0 +1,353 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); +var LineChart = require('./line_chart.jsx'); + +export default class TeamAnalytics extends React.Component { + constructor(props) { + super(props); + + this.getData = this.getData.bind(this); + + this.state = { + 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 + }; + } + + componentDidMount() { + this.getData(this.props.team.id); + } + + getData(teamId) { + Client.getAnalytics( + teamId, + '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}); + } + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + + Client.getAnalytics( + teamId, + 'post_counts_day', + (data) => { + 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.getAnalytics( + teamId, + 'user_counts_with_posts_day', + (data) => { + 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}); + } + ); + + Client.getProfilesForTeam( + teamId, + (users) => { + this.setState({users}); + + var usersList = []; + for (var id in users) { + if (users.hasOwnProperty(id)) { + usersList.push(users[id]); + } + } + + usersList.sort((a, b) => { + if (a.last_activity_at < b.last_activity_at) { + return 1; + } + + if (a.last_activity_at > b.last_activity_at) { + return -1; + } + + return 0; + }); + + var recentActive = []; + for (let i = 0; i < usersList.length; i++) { + recentActive.push(usersList[i]); + if (i > 19) { + break; + } + } + + this.setState({recent_active_users: recentActive}); + + usersList.sort((a, b) => { + if (a.create_at < b.create_at) { + return 1; + } + + if (a.create_at > b.create_at) { + return -1; + } + + return 0; + }); + + var newlyCreated = []; + for (let i = 0; i < usersList.length; i++) { + newlyCreated.push(usersList[i]); + if (i > 19) { + break; + } + } + + this.setState({newly_created_users: newlyCreated}); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + componentWillReceiveProps(newProps) { + this.setState({ + 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 + }); + + 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='total-count text-center'> + <div>{'Total Users'}</div> + <div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div> + </div> + ); + + var openChannelCount = ( + <div className='total-count text-center'> + <div>{'Public Groups'}</div> + <div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div> + </div> + ); + + var openPrivateCount = ( + <div className='total-count text-center'> + <div>{'Private Groups'}</div> + <div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div> + </div> + ); + + var postCount = ( + <div className='total-count text-center'> + <div>{'Total Posts'}</div> + <div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div> + </div> + ); + + var postCountsByDay = ( + <div className='total-count-by-day'> + <div>{'Total Posts'}</div> + <div>{'Loading...'}</div> + </div> + ); + + if (this.state.post_counts_day != null) { + postCountsByDay = ( + <div className='total-count-by-day'> + <div>{'Total Posts'}</div> + <LineChart + data={this.state.post_counts_day} + width='740' + height='225' + /> + </div> + ); + } + + var usersWithPostsByDay = ( + <div className='total-count-by-day'> + <div>{'Total Posts'}</div> + <div>{'Loading...'}</div> + </div> + ); + + if (this.state.user_counts_with_posts_day != null) { + usersWithPostsByDay = ( + <div className='total-count-by-day'> + <div>{'Active Users With Posts'}</div> + <LineChart + data={this.state.user_counts_with_posts_day} + width='740' + height='225' + /> + </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='recent-active-users'> + <div>{'Recent Active Users'}</div> + <table width='90%'> + <tbody> + { + this.state.recent_active_users.map((user) => { + return ( + <tr key={user.id}> + <td className='recent-active-users-td'>{user.email}</td> + <td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + </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='recent-active-users'> + <div>{'Newly Created Users'}</div> + <table width='90%'> + <tbody> + { + this.state.newly_created_users.map((user) => { + return ( + <tr key={user.id}> + <td className='recent-active-users-td'>{user.email}</td> + <td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + </div> + ); + } + + return ( + <div className='wrapper--fixed'> + <h2>{'Statistics for ' + this.props.team.name}</h2> + {serverError} + {totalCount} + {postCount} + {openChannelCount} + {openPrivateCount} + {postCountsByDay} + {usersWithPostsByDay} + {recentActiveUser} + {newUsers} + </div> + ); + } +} + +TeamAnalytics.propTypes = { + team: React.PropTypes.object +}; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index a93257dd2..fe5797769 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -328,6 +328,20 @@ export function getConfig(success, error) { }); } +export function getAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', |