mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Refactor and modularize analytics on the client
This commit is contained in:
77
web/react/components/analytics/doughnut_chart.jsx
Normal file
77
web/react/components/analytics/doughnut_chart.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
export default class DoughnutChart 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.refs.canvas);
|
||||
var ctx = el.getContext('2d');
|
||||
this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.props.data == null) {
|
||||
content = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<canvas
|
||||
ref='canvas'
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DoughnutChart.propTypes = {
|
||||
title: React.PropTypes.node,
|
||||
width: React.PropTypes.string,
|
||||
height: React.PropTypes.string,
|
||||
data: React.PropTypes.array,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
90
web/react/components/analytics/line_chart.jsx
Normal file
90
web/react/components/analytics/line_chart.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
export default class LineChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.initChart = this.initChart.bind(this);
|
||||
this.chart = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
initChart() {
|
||||
if (!this.refs.canvas) {
|
||||
return;
|
||||
}
|
||||
var el = ReactDOM.findDOMNode(this.refs.canvas);
|
||||
var ctx = el.getContext('2d');
|
||||
this.chart = new Chart(ctx).Line(this.props.data, this.props.options || {}); //eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.props.data == null) {
|
||||
content = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.data.labels.length === 0) {
|
||||
content = (
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id='analytics.chart.meaningful'
|
||||
defaultMessage='Not enough data for a meaningful representation.'
|
||||
/>
|
||||
</h5>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<canvas
|
||||
ref='canvas'
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.propTypes = {
|
||||
title: React.PropTypes.node.isRequired,
|
||||
width: React.PropTypes.string.isRequired,
|
||||
height: React.PropTypes.string.isRequired,
|
||||
data: React.PropTypes.object,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
|
||||
33
web/react/components/analytics/statistic_count.jsx
Normal file
33
web/react/components/analytics/statistic_count.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'mm-intl';
|
||||
|
||||
export default class StatisticCount extends React.Component {
|
||||
render() {
|
||||
let loading = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
<i className={'fa ' + this.props.icon}/>
|
||||
</div>
|
||||
<div className='content'>{this.props.count == null ? loading : this.props.count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatisticCount.propTypes = {
|
||||
title: React.PropTypes.node.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
count: React.PropTypes.number
|
||||
};
|
||||
346
web/react/components/analytics/system_analytics.jsx
Normal file
346
web/react/components/analytics/system_analytics.jsx
Normal file
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LineChart from './line_chart.jsx';
|
||||
import DoughnutChart from './doughnut_chart.jsx';
|
||||
import StatisticCount from './statistic_count.jsx';
|
||||
|
||||
import AnalyticsStore from '../../stores/analytics_store.jsx';
|
||||
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
const StatTypes = Constants.StatTypes;
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
analyticsPublicChannels: {
|
||||
id: 'analytics.system.publicChannels',
|
||||
defaultMessage: 'Public Channels'
|
||||
},
|
||||
analyticsPrivateGroups: {
|
||||
id: 'analytics.system.privateGroups',
|
||||
defaultMessage: 'Private Groups'
|
||||
},
|
||||
analyticsFilePosts: {
|
||||
id: 'analytics.system.totalFilePosts',
|
||||
defaultMessage: 'Posts with Files'
|
||||
},
|
||||
analyticsHashtagPosts: {
|
||||
id: 'analytics.system.totalHashtagPosts',
|
||||
defaultMessage: 'Posts with Hashtags'
|
||||
},
|
||||
analyticsTextPosts: {
|
||||
id: 'analytics.system.textPosts',
|
||||
defaultMessage: 'Posts with Text-only'
|
||||
}
|
||||
});
|
||||
|
||||
class SystemAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.state = {stats: AnalyticsStore.getAllSystem()};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AnalyticsStore.addChangeListener(this.onChange);
|
||||
|
||||
AsyncClient.getStandardAnalytics();
|
||||
AsyncClient.getPostsPerDayAnalytics();
|
||||
AsyncClient.getUsersPerDayAnalytics();
|
||||
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
AsyncClient.getAdvancedAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AnalyticsStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({stats: AnalyticsStore.getAllSystem()});
|
||||
}
|
||||
|
||||
render() {
|
||||
const stats = this.state.stats;
|
||||
|
||||
let advancedCounts;
|
||||
let advancedGraphs;
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
advancedCounts = (
|
||||
<div className='row'>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalSessions'
|
||||
defaultMessage='Total Sessions'
|
||||
/>
|
||||
}
|
||||
icon='fa-signal'
|
||||
count={stats[StatTypes.TOTAL_SESSIONS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalCommands'
|
||||
defaultMessage='Total Commands'
|
||||
/>
|
||||
}
|
||||
icon='fa-terminal'
|
||||
count={stats[StatTypes.TOTAL_COMMANDS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalIncomingWebhooks'
|
||||
defaultMessage='Incoming Webhooks'
|
||||
/>
|
||||
}
|
||||
icon='fa-arrow-down'
|
||||
count={stats[StatTypes.TOTAL_IHOOKS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalOutgoingWebhooks'
|
||||
defaultMessage='Outgoing Webhooks'
|
||||
/>
|
||||
}
|
||||
icon='fa-arrow-up'
|
||||
count={stats[StatTypes.TOTAL_OHOOKS]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl);
|
||||
const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl);
|
||||
|
||||
advancedGraphs = (
|
||||
<div className='row'>
|
||||
<DoughnutChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.channelTypes'
|
||||
defaultMessage='Channel Types'
|
||||
/>
|
||||
}
|
||||
data={channelTypeData}
|
||||
width='300'
|
||||
height='225'
|
||||
/>
|
||||
<DoughnutChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.postTypes'
|
||||
defaultMessage='Posts, Files and Hashtags'
|
||||
/>
|
||||
}
|
||||
data={postTypeData}
|
||||
width='300'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
|
||||
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='analytics.system.title'
|
||||
defaultMessage='System Statistics'
|
||||
/>
|
||||
</h3>
|
||||
<div className='row'>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalUsers'
|
||||
defaultMessage='Total Users'
|
||||
/>
|
||||
}
|
||||
icon='fa-user'
|
||||
count={stats[StatTypes.TOTAL_USERS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalTeams'
|
||||
defaultMessage='Total Teams'
|
||||
/>
|
||||
}
|
||||
icon='fa-users'
|
||||
count={stats[StatTypes.TOTAL_TEAMS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
icon='fa-comment'
|
||||
count={stats[StatTypes.TOTAL_POSTS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalChannels'
|
||||
defaultMessage='Total Channels'
|
||||
/>
|
||||
}
|
||||
icon='fa-globe'
|
||||
count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS] + stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
|
||||
/>
|
||||
</div>
|
||||
{advancedCounts}
|
||||
{advancedGraphs}
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
data={postCountsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.activeUsers'
|
||||
defaultMessage='Active Users With Posts'
|
||||
/>
|
||||
}
|
||||
data={userCountsWithPostsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SystemAnalytics.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(SystemAnalytics);
|
||||
|
||||
export function formatChannelDoughtnutData(totalPublic, totalPrivate, intl) {
|
||||
const {formatMessage} = intl;
|
||||
const channelTypeData = [
|
||||
{
|
||||
value: totalPublic,
|
||||
color: '#46BFBD',
|
||||
highlight: '#5AD3D1',
|
||||
label: formatMessage(holders.analyticsPublicChannels)
|
||||
},
|
||||
{
|
||||
value: totalPrivate,
|
||||
color: '#FDB45C',
|
||||
highlight: '#FFC870',
|
||||
label: formatMessage(holders.analyticsPrivateGroups)
|
||||
}
|
||||
];
|
||||
|
||||
return channelTypeData;
|
||||
}
|
||||
|
||||
export function formatPostDoughtnutData(filePosts, hashtagPosts, totalPosts, intl) {
|
||||
const {formatMessage} = intl;
|
||||
const postTypeData = [
|
||||
{
|
||||
value: filePosts,
|
||||
color: '#46BFBD',
|
||||
highlight: '#5AD3D1',
|
||||
label: formatMessage(holders.analyticsFilePosts)
|
||||
},
|
||||
{
|
||||
value: hashtagPosts,
|
||||
color: '#F7464A',
|
||||
highlight: '#FF5A5E',
|
||||
label: formatMessage(holders.analyticsHashtagPosts)
|
||||
},
|
||||
{
|
||||
value: totalPosts - filePosts - hashtagPosts,
|
||||
color: '#FDB45C',
|
||||
highlight: '#FFC870',
|
||||
label: formatMessage(holders.analyticsTextPosts)
|
||||
}
|
||||
];
|
||||
|
||||
return postTypeData;
|
||||
}
|
||||
|
||||
export function formatPostsPerDayData(data) {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
export function formatUsersWithPostsPerDayData(data) {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return chartData;
|
||||
}
|
||||
60
web/react/components/analytics/table_chart.jsx
Normal file
60
web/react/components/analytics/table_chart.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
|
||||
const Tooltip = ReactBootstrap.Tooltip;
|
||||
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||
|
||||
export default class TableChart extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
this.props.data.map((item) => {
|
||||
const tooltip = (
|
||||
<Tooltip id={'tip-table-entry-' + item.name}>
|
||||
{item.tip}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<tr key={'table-entry-' + item.name}>
|
||||
<td>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={tooltip}
|
||||
>
|
||||
<time>
|
||||
{item.name}
|
||||
</time>
|
||||
</OverlayTrigger>
|
||||
</td>
|
||||
<td>
|
||||
{item.value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableChart.propTypes = {
|
||||
title: React.PropTypes.node,
|
||||
data: React.PropTypes.array
|
||||
};
|
||||
235
web/react/components/analytics/team_analytics.jsx
Normal file
235
web/react/components/analytics/team_analytics.jsx
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LineChart from './line_chart.jsx';
|
||||
import StatisticCount from './statistic_count.jsx';
|
||||
import TableChart from './table_chart.jsx';
|
||||
|
||||
import AnalyticsStore from '../../stores/analytics_store.jsx';
|
||||
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
const StatTypes = Constants.StatTypes;
|
||||
|
||||
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
|
||||
import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'mm-intl';
|
||||
|
||||
class TeamAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AnalyticsStore.addChangeListener(this.onChange);
|
||||
|
||||
this.getData(this.props.team.id);
|
||||
}
|
||||
|
||||
getData(id) {
|
||||
AsyncClient.getStandardAnalytics(id);
|
||||
AsyncClient.getPostsPerDayAnalytics(id);
|
||||
AsyncClient.getUsersPerDayAnalytics(id);
|
||||
AsyncClient.getRecentAndNewUsersAnalytics(id);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AnalyticsStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.getData(nextProps.team.id);
|
||||
this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
|
||||
}
|
||||
|
||||
render() {
|
||||
const stats = this.state.stats;
|
||||
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
|
||||
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
|
||||
const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
|
||||
const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='analytics.team.title'
|
||||
defaultMessage='Team Statistics for {team}'
|
||||
values={{
|
||||
team: this.props.team.name
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
<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 Groups'
|
||||
/>
|
||||
}
|
||||
icon='fa-globe'
|
||||
count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
icon='fa-comment'
|
||||
count={stats[StatTypes.TOTAL_POSTS]}
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
data={postCountsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.activeUsers'
|
||||
defaultMessage='Active Users With Posts'
|
||||
/>
|
||||
}
|
||||
data={userCountsWithPostsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<TableChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.activeUsers'
|
||||
defaultMessage='Recent Active Users'
|
||||
/>
|
||||
}
|
||||
data={recentActiveUsers}
|
||||
/>
|
||||
<TableChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.newlyCreated'
|
||||
defaultMessage='Newly Created Users'
|
||||
/>
|
||||
}
|
||||
data={newlyCreatedUsers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamAnalytics.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
team: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(TeamAnalytics);
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user