From 2a26d857574f2160e3ee5538ad3a84ec47082f86 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 21 Jan 2016 12:14:17 -0500 Subject: Generalize analytics server functions and begin componentizing client analytics controls --- web/react/components/admin_console/analytics.jsx | 277 +++++++++++++++++++++ .../components/admin_console/team_analytics.jsx | 256 +++---------------- 2 files changed, 306 insertions(+), 227 deletions(-) create mode 100644 web/react/components/admin_console/analytics.jsx (limited to 'web/react/components/admin_console') diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx new file mode 100644 index 000000000..4349719c1 --- /dev/null +++ b/web/react/components/admin_console/analytics.jsx @@ -0,0 +1,277 @@ +// 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 =
; + } + + var totalCount = ( +
+
+
{'Total Users'}
+
{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}
+
+
+ ); + + var openChannelCount = ( +
+
+
{'Public Channels'}
+
{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}
+
+
+ ); + + var openPrivateCount = ( +
+
+
{'Private Groups'}
+
{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}
+
+
+ ); + + var postCount = ( +
+
+
{'Total Posts'}
+
{this.props.postCount == null ? 'Loading...' : this.props.postCount}
+
+
+ ); + + var postCountsByDay = ( +
+
+
{'Total Posts'}
+
{'Loading...'}
+
+
+ ); + + if (this.props.postCountsDay != null) { + let content; + if (this.props.postCountsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + + ); + } + postCountsByDay = ( +
+
+
{'Total Posts'}
+
+ {content} +
+
+
+ ); + } + + var usersWithPostsByDay = ( +
+
+
{'Total Posts'}
+
{'Loading...'}
+
+
+ ); + + if (this.props.userCountsWithPostsDay != null) { + let content; + if (this.props.userCountsWithPostsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + + ); + } + usersWithPostsByDay = ( +
+
+
{'Active Users With Posts'}
+
+ {content} +
+
+
+ ); + } + + var recentActiveUser = ( +
+
{'Recent Active Users'}
+
{'Loading...'}
+
+ ); + + if (this.props.recentActiveUsers != null) { + recentActiveUser = ( +
+
+
{'Recent Active Users'}
+
+ + + { + this.props.recentActiveUsers.map((user) => { + const tooltip = ( + + {user.email} + + ); + + return ( + + + + + ); + }) + } + +
+ + + + {Utils.displayDateTime(user.last_activity_at)}
+
+
+
+ ); + } + + var newUsers = ( +
+
{'Newly Created Users'}
+
{'Loading...'}
+
+ ); + + if (this.props.newlyCreatedUsers != null) { + newUsers = ( +
+
+
{'Newly Created Users'}
+
+ + + { + this.props.newlyCreatedUsers.map((user) => { + const tooltip = ( + + {user.email} + + ); + + return ( + + + + + ); + }) + } + +
+ + + + {Utils.displayDateTime(user.create_at)}
+
+
+
+ ); + } + + return ( +
+

{'Statistics for ' + this.props.title}

+
+ {totalCount} + {postCount} + {openChannelCount} + {openPrivateCount} +
+
+ {postCountsByDay} +
+
+ {usersWithPostsByDay} +
+
+ {recentActiveUser} + {newUsers} +
+
+ ); + } +} + + +Analytics.defaultProps = { + title: null, + users: 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, + users: React.PropTypes.object, + 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/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index fe7230946..baa041bac 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) => { @@ -198,227 +198,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 =
; - } - - var totalCount = ( -
-
-
{'Total Users'}
-
{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}
-
-
- ); - - var openChannelCount = ( -
-
-
{'Public Channels'}
-
{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}
-
-
- ); - - var openPrivateCount = ( -
-
-
{'Private Groups'}
-
{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}
-
-
- ); - - var postCount = ( -
-
-
{'Total Posts'}
-
{this.state.post_count == null ? 'Loading...' : this.state.post_count}
-
-
- ); - - var postCountsByDay = ( -
-
-
{'Total Posts'}
-
{'Loading...'}
-
-
- ); - - if (this.state.post_counts_day != null) { - postCountsByDay = ( -
-
-
{'Total Posts'}
-
- -
-
-
- ); - } - - var usersWithPostsByDay = ( -
-
-
{'Total Posts'}
-
{'Loading...'}
-
-
- ); - - if (this.state.user_counts_with_posts_day != null) { - usersWithPostsByDay = ( -
-
-
{'Active Users With Posts'}
-
- -
-
-
- ); - } - - var recentActiveUser = ( -
-
{'Recent Active Users'}
-
{'Loading...'}
-
- ); - - if (this.state.recent_active_users != null) { - recentActiveUser = ( -
-
-
{'Recent Active Users'}
-
- - - { - this.state.recent_active_users.map((user) => { - const tooltip = ( - - {user.email} - - ); - - return ( - - - - - ); - }) - } - -
- - - - {Utils.displayDateTime(user.last_activity_at)}
-
-
-
- ); - } - - var newUsers = ( -
-
{'Newly Created Users'}
-
{'Loading...'}
-
- ); - - if (this.state.newly_created_users != null) { - newUsers = ( -
-
-
{'Newly Created Users'}
-
- - - { - this.state.newly_created_users.map((user) => { - const tooltip = ( - - {user.email} - - ); - - return ( - - - - - ); - }) - } - -
- - - - {Utils.displayDateTime(user.create_at)}
-
-
-
- ); - } - return ( -
-

{'Statistics for ' + this.props.team.name}

- {serverError} -
- {totalCount} - {postCount} - {openChannelCount} - {openPrivateCount} -
-
- {postCountsByDay} -
-
- {usersWithPostsByDay} -
-
- {recentActiveUser} - {newUsers} -
+
+
); } -- cgit v1.2.3-1-g7c22 From cefdad6d8c71ff6adf0ae919bd9f9139e02a6caa Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 21 Jan 2016 12:59:32 -0500 Subject: Add system analytics page --- .../components/admin_console/admin_controller.jsx | 5 +- .../components/admin_console/admin_sidebar.jsx | 19 +++ web/react/components/admin_console/analytics.jsx | 158 ++++++++++---------- .../components/admin_console/system_analytics.jsx | 161 +++++++++++++++++++++ 4 files changed, 264 insertions(+), 79 deletions(-) create mode 100644 web/react/components/admin_console/system_analytics.jsx (limited to 'web/react/components/admin_console') diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 0f85c238d..db98d8f35 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) { @@ -165,6 +166,8 @@ export default class AdminController extends React.Component { if (this.state.teams) { tab = ; } + } else if (this.state.selected === 'system_analytics') { + tab = ; } } 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 @@ -192,6 +192,25 @@ export default class AdminSidebar extends React.Component {
  • +
      +
    • +

      + + {'SITE REPORTS'} +

      +
    • +
    +
    • diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx index 4349719c1..8fdf538f8 100644 --- a/web/react/components/admin_console/analytics.jsx +++ b/web/react/components/admin_console/analytics.jsx @@ -94,8 +94,8 @@ export default class Analytics extends React.Component { var usersWithPostsByDay = (
      -
      {'Total Posts'}
      -
      {'Loading...'}
      +
      {'Active Users With Posts'}
      +
      {'Loading...'}
      ); @@ -125,98 +125,102 @@ export default class Analytics extends React.Component { ); } - var recentActiveUser = ( -
      -
      {'Recent Active Users'}
      -
      {'Loading...'}
      -
      - ); - + let recentActiveUser; if (this.props.recentActiveUsers != null) { + let content; + if (this.props.recentActiveUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + + + { + this.props.recentActiveUsers.map((user) => { + const tooltip = ( + + {user.email} + + ); + + return ( + + + + + ); + }) + } + +
      + + + + {Utils.displayDateTime(user.last_activity_at)}
      + ); + } recentActiveUser = (
      {'Recent Active Users'}
      - - - { - this.props.recentActiveUsers.map((user) => { - const tooltip = ( - - {user.email} - - ); - - return ( - - - - - ); - }) - } - -
      - - - - {Utils.displayDateTime(user.last_activity_at)}
      + {content}
      ); } - var newUsers = ( -
      -
      {'Newly Created Users'}
      -
      {'Loading...'}
      -
      - ); - + let newUsers; if (this.props.newlyCreatedUsers != null) { + let content; + if (this.props.newlyCreatedUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + + + { + this.props.newlyCreatedUsers.map((user) => { + const tooltip = ( + + {user.email} + + ); + + return ( + + + + + ); + }) + } + +
      + + + + {Utils.displayDateTime(user.create_at)}
      + ); + } newUsers = (
      {'Newly Created Users'}
      - - - { - this.props.newlyCreatedUsers.map((user) => { - const tooltip = ( - - {user.email} - - ); - - return ( - - - - - ); - }) - } - -
      - - - - {Utils.displayDateTime(user.create_at)}
      + {content}
      @@ -250,7 +254,6 @@ export default class Analytics extends React.Component { Analytics.defaultProps = { title: null, - users: null, channelOpenCount: null, channelPrivateCount: null, postCount: null, @@ -264,7 +267,6 @@ Analytics.defaultProps = { Analytics.propTypes = { title: React.PropTypes.string, - users: React.PropTypes.object, channelOpenCount: React.PropTypes.number, channelPrivateCount: React.PropTypes.number, postCount: React.PropTypes.number, 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..640f17ff0 --- /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(newProps) { + 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 ( +
      + +
      + ); + } +} + +SystemAnalytics.propTypes = { + team: React.PropTypes.object +}; -- cgit v1.2.3-1-g7c22 From bbcf00f02e469bcf04a45c1cdf8a7932e30ccfc0 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 22 Jan 2016 09:53:17 -0500 Subject: Add create_at back to profile fields to fix analytics --- web/react/components/admin_console/system_analytics.jsx | 2 +- web/react/components/admin_console/team_analytics.jsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'web/react/components/admin_console') diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx index 640f17ff0..fffe7cc53 100644 --- a/web/react/components/admin_console/system_analytics.jsx +++ b/web/react/components/admin_console/system_analytics.jsx @@ -142,7 +142,7 @@ export default class SystemAnalytics extends React.Component { return (
      19) { break; -- cgit v1.2.3-1-g7c22 From 0d239a1a9e82a1279685b8962920eaf5c1b8f571 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 22 Jan 2016 10:19:02 -0500 Subject: Added unit test and fixed errors --- web/react/components/admin_console/analytics.jsx | 4 ++-- web/react/components/admin_console/system_analytics.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'web/react/components/admin_console') diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx index 8fdf538f8..70ef1ecab 100644 --- a/web/react/components/admin_console/analytics.jsx +++ b/web/react/components/admin_console/analytics.jsx @@ -180,7 +180,7 @@ export default class Analytics extends React.Component { if (this.props.newlyCreatedUsers != null) { let content; if (this.props.newlyCreatedUsers.length === 0) { - content = 'Loading...'; + content = 'Loading...'; } else { content = ( @@ -230,6 +230,7 @@ export default class Analytics extends React.Component { return (

      {'Statistics for ' + this.props.title}

      + {serverError}
      {totalCount} {postCount} @@ -251,7 +252,6 @@ export default class Analytics extends React.Component { } } - Analytics.defaultProps = { title: null, channelOpenCount: null, diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx index fffe7cc53..f54813a94 100644 --- a/web/react/components/admin_console/system_analytics.jsx +++ b/web/react/components/admin_console/system_analytics.jsx @@ -124,7 +124,7 @@ export default class SystemAnalytics extends React.Component { ); } - componentWillReceiveProps(newProps) { + componentWillReceiveProps() { this.setState({ serverError: null, channel_open_count: null, -- cgit v1.2.3-1-g7c22 From 60a73ebabba6798d2b45fa8c8ac0f2bfa6144689 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 22 Jan 2016 10:24:37 -0500 Subject: Change default system console page to statistics --- web/react/components/admin_console/admin_controller.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'web/react/components/admin_console') diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index db98d8f35..efd163017 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -46,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 }; -- cgit v1.2.3-1-g7c22