diff options
Diffstat (limited to 'webapp/components/analytics/team_analytics')
-rw-r--r-- | webapp/components/analytics/team_analytics/index.js | 24 | ||||
-rw-r--r-- | webapp/components/analytics/team_analytics/team_analytics.jsx | 366 |
2 files changed, 390 insertions, 0 deletions
diff --git a/webapp/components/analytics/team_analytics/index.js b/webapp/components/analytics/team_analytics/index.js new file mode 100644 index 000000000..270967a1b --- /dev/null +++ b/webapp/components/analytics/team_analytics/index.js @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {getTeams} from 'mattermost-redux/actions/teams'; + +import TeamAnalytics from './team_analytics.jsx'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + getTeams + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(TeamAnalytics); diff --git a/webapp/components/analytics/team_analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics/team_analytics.jsx new file mode 100644 index 000000000..828f29f51 --- /dev/null +++ b/webapp/components/analytics/team_analytics/team_analytics.jsx @@ -0,0 +1,366 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedDate, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +import Banner from 'components/admin_console/banner.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import AdminStore from 'stores/admin_store.jsx'; +import AnalyticsStore from 'stores/analytics_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {StatTypes} from 'utils/constants.jsx'; +import {convertTeamMapToList} from 'utils/team_utils.jsx'; + +import LineChart from 'components/analytics/line_chart.jsx'; +import StatisticCount from 'components/analytics/statistic_count.jsx'; +import TableChart from 'components/analytics/table_chart.jsx'; +import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from 'components/analytics/system_analytics.jsx'; + +const LAST_ANALYTICS_TEAM = 'last_analytics_team'; + +export default class TeamAnalytics extends React.Component { + static propTypes = { + actions: React.PropTypes.shape({ + getTeams: React.PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.onAllTeamsChange = this.onAllTeamsChange.bind(this); + this.handleTeamChange = this.handleTeamChange.bind(this); + + const teams = convertTeamMapToList(AdminStore.getAllTeams()); + const teamId = BrowserStore.getGlobalItem(LAST_ANALYTICS_TEAM, teams.length > 0 ? teams[0].id : ''); + + this.state = { + teams, + teamId, + team: AdminStore.getTeam(teamId), + stats: AnalyticsStore.getAllTeam(teamId) + }; + } + + componentDidMount() { + AnalyticsStore.addChangeListener(this.onChange); + AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange); + + if (this.state.teamId !== '') { + this.getData(this.state.teamId); + } + + if (this.state.teams.length === 0) { + this.props.actions.getTeams(0, 1000); + } + } + + componentWillUpdate(nextProps, nextState) { + if (nextState.teamId !== this.state.teamId) { + this.getData(nextState.teamId); + } + } + + getData(id) { + AsyncClient.getStandardAnalytics(id); + AsyncClient.getPostsPerDayAnalytics(id); + AsyncClient.getUsersPerDayAnalytics(id); + AsyncClient.getRecentAndNewUsersAnalytics(id); + } + + componentWillUnmount() { + AnalyticsStore.removeChangeListener(this.onChange); + AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange); + } + + onChange() { + this.setState({ + stats: AnalyticsStore.getAllTeam(this.state.teamId) + }); + } + + onAllTeamsChange() { + const teams = convertTeamMapToList(AdminStore.getAllTeams()); + + if (teams.length > 0) { + if (this.state.teamId) { + this.setState({ + team: AdminStore.getTeam(this.state.teamId) + }); + } else { + this.setState({ + teamId: teams[0].id, + team: teams[0] + }); + } + } + + this.setState({ + teams + }); + } + + handleTeamChange(e) { + const teamId = e.target.value; + + this.setState({ + teamId, + team: AdminStore.getTeam(teamId) + }); + + BrowserStore.setGlobalItem(LAST_ANALYTICS_TEAM, teamId); + } + + render() { + if (this.state.teams.length === 0 || !this.state.team || !this.state.stats) { + return <LoadingScreen/>; + } + + if (this.state.teamId === '') { + return ( + <Banner + description={ + <FormattedMessage + id='analytics.team.noTeams' + defaultMessage='There are no teams on this server for which to view statistics.' + /> + } + /> + ); + } + + const stats = this.state.stats; + const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]); + const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]); + + let banner; + let totalPostsCount; + let postTotalGraph; + let userActiveGraph; + if (stats[StatTypes.TOTAL_POSTS] === -1) { + banner = ( + <Banner + description={ + <FormattedHTMLMessage + id='analytics.system.skippedIntensiveQueries' + defaultMessage="Some statistics have been omitted because they put too much load on the system to calculate. See <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a> for more details." + /> + } + /> + ); + } else { + totalPostsCount = ( + <StatisticCount + title={ + <FormattedMessage + id='analytics.team.totalPosts' + defaultMessage='Total Posts' + /> + } + icon='fa-comment' + count={stats[StatTypes.TOTAL_POSTS]} + /> + ); + + postTotalGraph = ( + <div className='row'> + <LineChart + key={this.state.team.id} + title={ + <FormattedMessage + id='analytics.team.totalPosts' + defaultMessage='Total Posts' + /> + } + data={postCountsDay} + options={{ + legend: { + display: false + } + }} + width='740' + height='225' + /> + </div> + ); + + userActiveGraph = ( + <div className='row'> + <LineChart + key={this.state.team.id} + title={ + <FormattedMessage + id='analytics.team.activeUsers' + defaultMessage='Active Users With Posts' + /> + } + data={userCountsWithPostsDay} + options={{ + legend: { + display: false + } + }} + width='740' + height='225' + /> + </div> + ); + } + + const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]); + const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]); + + const teams = this.state.teams.map((team) => { + return ( + <option + key={team.id} + value={team.id} + > + {team.display_name} + </option> + ); + }); + + return ( + <div className='wrapper--fixed team_statistics'> + <div className='admin-console-header team-statistics__header-row'> + <div className='team-statistics__header'> + <h3> + <FormattedMessage + id='analytics.team.title' + defaultMessage='Team Statistics for {team}' + values={{ + team: this.state.team.display_name + }} + /> + </h3> + </div> + <div className='team-statistics__team-filter'> + <select + className='form-control team-statistics__team-filter__dropdown' + onChange={this.handleTeamChange} + value={this.state.teamId} + > + {teams} + </select> + </div> + </div> + {banner} + <div className='row'> + <StatisticCount + title={ + <FormattedMessage + id='analytics.team.totalUsers' + defaultMessage='Total Users' + /> + } + icon='fa-user' + count={stats[StatTypes.TOTAL_USERS]} + /> + <StatisticCount + title={ + <FormattedMessage + id='analytics.team.publicChannels' + defaultMessage='Public Channels' + /> + } + icon='fa-users' + count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS]} + /> + <StatisticCount + title={ + <FormattedMessage + id='analytics.team.privateGroups' + defaultMessage='Private Channels' + /> + } + icon='fa-globe' + count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]} + /> + {totalPostsCount} + </div> + {postTotalGraph} + {userActiveGraph} + <div className='row'> + <TableChart + title={ + <FormattedMessage + id='analytics.team.recentUsers' + defaultMessage='Recent Active Users' + /> + } + data={recentActiveUsers} + /> + <TableChart + title={ + <FormattedMessage + id='analytics.team.newlyCreated' + defaultMessage='Newly Created Users' + /> + } + data={newlyCreatedUsers} + /> + </div> + </div> + ); + } +} + +export function formatRecentUsersData(data) { + if (data == null) { + return []; + } + + const formattedData = data.map((user) => { + const item = {}; + item.name = user.username; + item.value = ( + <FormattedDate + value={user.last_activity_at} + day='numeric' + month='long' + year='numeric' + hour12={true} + hour='2-digit' + minute='2-digit' + /> + ); + item.tip = user.email; + + return item; + }); + + return formattedData; +} + +export function formatNewUsersData(data) { + if (data == null) { + return []; + } + + const formattedData = data.map((user) => { + const item = {}; + item.name = user.username; + item.value = ( + <FormattedDate + value={user.create_at} + day='numeric' + month='long' + year='numeric' + hour12={true} + hour='2-digit' + minute='2-digit' + /> + ); + item.tip = user.email; + + return item; + }); + + return formattedData; +} |