summaryrefslogtreecommitdiffstats
path: root/webapp/components/analytics
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2016-03-14 08:50:46 -0400
committerChristopher Speller <crspeller@gmail.com>2016-03-16 18:02:55 -0400
commit12896bd23eeba79884245c1c29fdc568cf21a7fa (patch)
tree4e7f83d3e2564b9b89d669e9f7905ff11768b11a /webapp/components/analytics
parent29fe6a3d13c9c7aa490fffebbe5d1b5fdf1e3090 (diff)
downloadchat-12896bd23eeba79884245c1c29fdc568cf21a7fa.tar.gz
chat-12896bd23eeba79884245c1c29fdc568cf21a7fa.tar.bz2
chat-12896bd23eeba79884245c1c29fdc568cf21a7fa.zip
Converting to Webpack. Stage 1.
Diffstat (limited to 'webapp/components/analytics')
-rw-r--r--webapp/components/analytics/doughnut_chart.jsx81
-rw-r--r--webapp/components/analytics/line_chart.jsx94
-rw-r--r--webapp/components/analytics/statistic_count.jsx35
-rw-r--r--webapp/components/analytics/system_analytics.jsx348
-rw-r--r--webapp/components/analytics/table_chart.jsx61
-rw-r--r--webapp/components/analytics/team_analytics.jsx237
6 files changed, 856 insertions, 0 deletions
diff --git a/webapp/components/analytics/doughnut_chart.jsx b/webapp/components/analytics/doughnut_chart.jsx
new file mode 100644
index 000000000..169ac3105
--- /dev/null
+++ b/webapp/components/analytics/doughnut_chart.jsx
@@ -0,0 +1,81 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ReactDOM from 'react-dom';
+import {FormattedMessage} from 'react-intl';
+import Chart from 'chart.js';
+
+import React from 'react';
+
+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
+};
diff --git a/webapp/components/analytics/line_chart.jsx b/webapp/components/analytics/line_chart.jsx
new file mode 100644
index 000000000..6a3b8c0f0
--- /dev/null
+++ b/webapp/components/analytics/line_chart.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ReactDOM from 'react-dom';
+import {FormattedMessage} from 'react-intl';
+import Chart from 'chart.js';
+
+import React from 'react';
+
+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
+};
+
diff --git a/webapp/components/analytics/statistic_count.jsx b/webapp/components/analytics/statistic_count.jsx
new file mode 100644
index 000000000..cbb8935dd
--- /dev/null
+++ b/webapp/components/analytics/statistic_count.jsx
@@ -0,0 +1,35 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+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
+};
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
new file mode 100644
index 000000000..77f5efaa6
--- /dev/null
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -0,0 +1,348 @@
+// 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 'react-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'
+ }
+});
+
+import React from 'react';
+
+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;
+}
diff --git a/webapp/components/analytics/table_chart.jsx b/webapp/components/analytics/table_chart.jsx
new file mode 100644
index 000000000..18ed54f96
--- /dev/null
+++ b/webapp/components/analytics/table_chart.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from 'utils/constants.jsx';
+
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
+
+import React from 'react';
+
+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
+};
diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx
new file mode 100644
index 000000000..efc965f24
--- /dev/null
+++ b/webapp/components/analytics/team_analytics.jsx
@@ -0,0 +1,237 @@
+// 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 'react-intl';
+
+import React from 'react';
+
+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;
+}