Merge pull request #2258 from mattermost/plt-1796

PLT-1796 Refactor and modularize analytics on the client
Этот коммит содержится в:
Corey Hulen
2016-03-02 12:22:11 -08:00
родитель cddc9df7c4 fbb71cab4b
Коммит c52153bf60
29 изменённых файлов: 1438 добавлений и 1093 удалений

Просмотреть файл

@@ -21,10 +21,10 @@ import TeamSettingsTab from './team_settings.jsx';
import ServiceSettingsTab from './service_settings.jsx';
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx';
import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
import SystemAnalyticsTab from './system_analytics.jsx';
import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {

Просмотреть файл

@@ -1,489 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
import DoughnutChart from './doughnut_chart.jsx';
import StatisticCount from './statistic_count.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
const holders = defineMessages({
analyticsTotalUsers: {
id: 'admin.analytics.totalUsers',
defaultMessage: 'Total Users'
},
analyticsPublicChannels: {
id: 'admin.analytics.publicChannels',
defaultMessage: 'Public Channels'
},
analyticsPrivateGroups: {
id: 'admin.analytics.privateGroups',
defaultMessage: 'Private Groups'
},
analyticsTotalPosts: {
id: 'admin.analytics.totalPosts',
defaultMessage: 'Total Posts'
},
analyticsFilePosts: {
id: 'admin.analytics.totalFilePosts',
defaultMessage: 'Posts with Files'
},
analyticsHashtagPosts: {
id: 'admin.analytics.totalHashtagPosts',
defaultMessage: 'Posts with Hashtags'
},
analyticsIncomingHooks: {
id: 'admin.analytics.totalIncomingWebhooks',
defaultMessage: 'Incoming Webhooks'
},
analyticsOutgoingHooks: {
id: 'admin.analytics.totalOutgoingWebhooks',
defaultMessage: 'Outgoing Webhooks'
},
analyticsChannelTypes: {
id: 'admin.analytics.channelTypes',
defaultMessage: 'Channel Types'
},
analyticsTextPosts: {
id: 'admin.analytics.textPosts',
defaultMessage: 'Posts with Text-only'
},
analyticsPostTypes: {
id: 'admin.analytics.postTypes',
defaultMessage: 'Posts, Files and Hashtags'
}
});
export default class Analytics extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() { // in the future, break down these into smaller components
const {formatMessage} = this.props.intl;
var serverError = '';
if (this.props.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
}
let loading = (
<h5>
<FormattedMessage
id='admin.analytics.loading'
defaultMessage='Loading...'
/>
</h5>
);
let firstRow;
let extraGraphs;
if (this.props.showAdvanced) {
firstRow = (
<div className='row'>
<StatisticCount
title={formatMessage(holders.analyticsTotalUsers)}
icon='fa-users'
count={this.props.uniqueUserCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsTotalPosts)}
icon='fa-comment'
count={this.props.postCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsIncomingHooks)}
icon='fa-arrow-down'
count={this.props.incomingWebhookCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsOutgoingHooks)}
icon='fa-arrow-up'
count={this.props.outgoingWebhookCount}
/>
</div>
);
const channelTypeData = [
{
value: this.props.channelOpenCount,
color: '#46BFBD',
highlight: '#5AD3D1',
label: formatMessage(holders.analyticsPublicChannels)
},
{
value: this.props.channelPrivateCount,
color: '#FDB45C',
highlight: '#FFC870',
label: formatMessage(holders.analyticsPrivateGroups)
}
];
const postTypeData = [
{
value: this.props.filePostCount,
color: '#46BFBD',
highlight: '#5AD3D1',
label: formatMessage(holders.analyticsFilePosts)
},
{
value: this.props.filePostCount,
color: '#F7464A',
highlight: '#FF5A5E',
label: formatMessage(holders.analyticsHashtagPosts)
},
{
value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount,
color: '#FDB45C',
highlight: '#FFC870',
label: formatMessage(holders.analyticsTextPosts)
}
];
extraGraphs = (
<div className='row'>
<DoughnutChart
title={formatMessage(holders.analyticsChannelTypes)}
data={channelTypeData}
width='300'
height='225'
/>
<DoughnutChart
title={formatMessage(holders.analyticsPostTypes)}
data={postTypeData}
width='300'
height='225'
/>
</div>
);
} else {
firstRow = (
<div className='row'>
<StatisticCount
title={formatMessage(holders.analyticsTotalUsers)}
icon='fa-users'
count={this.props.uniqueUserCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsPublicChannels)}
icon='fa-globe'
count={this.props.channelOpenCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsPrivateGroups)}
icon='fa-lock'
count={this.props.channelPrivateCount}
/>
<StatisticCount
title={formatMessage(holders.analyticsTotalPosts)}
icon='fa-comment'
count={this.props.postCount}
/>
</div>
);
}
let postCountsByDay;
if (this.props.postCountsDay == null) {
postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
</div>
<div className='content'>{loading}</div>
</div>
</div>
);
} else {
let content;
if (this.props.postCountsDay.labels.length === 0) {
content = (
<h5>
<FormattedMessage
id='admin.analytics.meaningful'
defaultMessage='Not enough data for a meaningful representation.'
/>
</h5>
);
} else {
content = (
<LineChart
data={this.props.postCountsDay}
width='740'
height='225'
/>
);
}
postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
let usersWithPostsByDay;
if (this.props.userCountsWithPostsDay == null) {
usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.activeUsers'
defaultMessage='Active Users With Posts'
/>
</div>
<div className='content'>{loading}</div>
</div>
</div>
);
} else {
let content;
if (this.props.userCountsWithPostsDay.labels.length === 0) {
content = (
<h5>
<FormattedMessage
id='admin.analytics.meaningful'
defaultMessage='Not enough data for a meaningful representation.'
/>
</h5>
);
} else {
content = (
<LineChart
data={this.props.userCountsWithPostsDay}
width='740'
height='225'
/>
);
}
usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.activeUsers'
defaultMessage='Active Users With Posts'
/>
</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
let recentActiveUser;
if (this.props.recentActiveUsers != null) {
let content;
if (this.props.recentActiveUsers.length === 0) {
content = loading;
} else {
content = (
<table>
<tbody>
{
this.props.recentActiveUsers.map((user) => {
const tooltip = (
<Tooltip id={'recent-user-email-tooltip-' + user.id}>
{user.email}
</Tooltip>
);
return (
<tr key={'recent-user-table-entry-' + user.id}>
<td>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
overlay={tooltip}
>
<time>
{user.username}
</time>
</OverlayTrigger>
</td>
<td>
<FormattedDate
value={user.last_activity_at}
day='numeric'
month='long'
year='numeric'
hour12={true}
hour='2-digit'
minute='2-digit'
/>
</td>
</tr>
);
})
}
</tbody>
</table>
);
}
recentActiveUser = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>
<FormattedMessage
id='admin.analytics.recentActive'
defaultMessage='Recent Active Users'
/>
</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
let newUsers;
if (this.props.newlyCreatedUsers != null) {
let content;
if (this.props.newlyCreatedUsers.length === 0) {
content = loading;
} else {
content = (
<table>
<tbody>
{
this.props.newlyCreatedUsers.map((user) => {
const tooltip = (
<Tooltip id={'new-user-email-tooltip-' + user.id}>
{user.email}
</Tooltip>
);
return (
<tr key={'new-user-table-entry-' + user.id}>
<td>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
overlay={tooltip}
>
<time>
{user.username}
</time>
</OverlayTrigger>
</td>
<td>
<FormattedDate
value={user.create_at}
day='numeric'
month='long'
year='numeric'
hour12={true}
hour='2-digit'
minute='2-digit'
/>
</td>
</tr>
);
})
}
</tbody>
</table>
);
}
newUsers = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>
<FormattedMessage
id='admin.analytics.newlyCreated'
defaultMessage='Newly Created Users'
/>
</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
return (
<div className='wrapper--fixed team_statistics'>
<h3>
<FormattedMessage
id='admin.analytics.title'
defaultMessage='Statistics for {title}'
values={{
title: this.props.title
}}
/>
</h3>
{serverError}
{firstRow}
{extraGraphs}
<div className='row'>
{postCountsByDay}
</div>
<div className='row'>
{usersWithPostsByDay}
</div>
<div className='row'>
{recentActiveUser}
{newUsers}
</div>
</div>
);
}
}
Analytics.defaultProps = {
title: null,
channelOpenCount: null,
channelPrivateCount: null,
postCount: null,
postCountsDay: null,
userCountsWithPostsDay: null,
recentActiveUsers: null,
newlyCreatedUsers: null,
uniqueUserCount: null,
serverError: null
};
Analytics.propTypes = {
intl: intlShape.isRequired,
title: React.PropTypes.string,
channelOpenCount: React.PropTypes.number,
channelPrivateCount: React.PropTypes.number,
postCount: React.PropTypes.number,
showAdvanced: React.PropTypes.bool,
filePostCount: React.PropTypes.number,
hashtagPostCount: React.PropTypes.number,
incomingWebhookCount: React.PropTypes.number,
outgoingWebhookCount: 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
};
export default injectIntl(Analytics);

Просмотреть файл

@@ -1,50 +0,0 @@
// 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
};

Просмотреть файл

@@ -1,216 +0,0 @@
// 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 {injectIntl, intlShape, defineMessages} from 'mm-intl';
const labels = defineMessages({
totalPosts: {
id: 'admin.system_analytics.totalPosts',
defaultMessage: 'Total Posts'
},
activeUsers: {
id: 'admin.system_analytics.activeUsers',
defaultMessage: 'Active Users With Posts'
},
title: {
id: 'admin.system_analytics.title',
defaultMessage: 'the System'
}
});
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
const {formatMessage} = this.props.intl;
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: formatMessage(labels.totalPosts),
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: formatMessage(labels.activeUsers),
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});
}
);
if (global.window.mm_license.IsLicensed === 'true') {
Client.getSystemAnalytics(
'extra_counts',
(data) => {
for (var index in data) {
if (data[index].name === 'file_post_count') {
this.setState({file_post_count: data[index].value});
}
if (data[index].name === 'hashtag_post_count') {
this.setState({hashtag_post_count: data[index].value});
}
if (data[index].name === 'incoming_webhook_count') {
this.setState({incoming_webhook_count: data[index].value});
}
if (data[index].name === 'outgoing_webhook_count') {
this.setState({outgoing_webhook_count: data[index].value});
}
}
},
(err) => {
this.setState({serverError: err.message});
}
);
}
}
componentWillReceiveProps() {
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 (
<div>
<Analytics
intl={this.props.intl}
title={this.props.intl.formatMessage(labels.title)}
channelOpenCount={this.state.channel_open_count}
channelPrivateCount={this.state.channel_private_count}
postCount={this.state.post_count}
showAdvanced={global.window.mm_license.IsLicensed === 'true'}
filePostCount={this.state.file_post_count}
hashtagPostCount={this.state.hashtag_post_count}
incomingWebhookCount={this.state.incoming_webhook_count}
outgoingWebhookCount={this.state.outgoing_webhook_count}
postCountsDay={this.state.post_counts_day}
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
uniqueUserCount={this.state.unique_user_count}
serverError={this.state.serverError}
/>
</div>
);
}
}
SystemAnalytics.propTypes = {
intl: intlShape.isRequired,
team: React.PropTypes.object
};
export default injectIntl(SystemAnalytics);

Просмотреть файл

@@ -1,253 +0,0 @@
// 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 {injectIntl, intlShape, defineMessages} from 'mm-intl';
const labels = defineMessages({
totalPosts: {
id: 'admin.team_analytics.totalPosts',
defaultMessage: 'Total Posts'
},
activeUsers: {
id: 'admin.team_analytics.activeUsers',
defaultMessage: 'Active Users With Posts'
}
});
class TeamAnalytics 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(this.props.team.id);
}
getData(teamId) { // should be moved to an action creator eventually
const {formatMessage} = this.props.intl;
Client.getTeamAnalytics(
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});
}
if (data[index].name === 'unique_user_count') {
this.setState({unique_user_count: data[index].value});
}
}
},
(err) => {
this.setState({serverError: err.message});
}
);
Client.getTeamAnalytics(
teamId,
'post_counts_day',
(data) => {
data.reverse();
var chartData = {
labels: [],
datasets: [{
label: formatMessage(labels.totalPosts),
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.getTeamAnalytics(
teamId,
'user_counts_with_posts_day',
(data) => {
data.reverse();
var chartData = {
labels: [],
datasets: [{
label: formatMessage(labels.activeUsers),
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++) {
if (usersList[i].last_activity_at == null) {
continue;
}
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,
unique_user_count: null
});
this.getData(newProps.team.id);
}
render() {
return (
<div>
<Analytics
intl={this.props.intl}
title={this.props.team.name}
users={this.state.users}
channelOpenCount={this.state.channel_open_count}
channelPrivateCount={this.state.channel_private_count}
postCount={this.state.post_count}
postCountsDay={this.state.post_counts_day}
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
recentActiveUsers={this.state.recent_active_users}
newlyCreatedUsers={this.state.newly_created_users}
uniqueUserCount={this.state.unique_user_count}
serverError={this.state.serverError}
/>
</div>
);
}
}
TeamAnalytics.propTypes = {
intl: intlShape.isRequired,
team: React.PropTypes.object
};
export default injectIntl(TeamAnalytics);

Просмотреть файл

@@ -39,7 +39,7 @@ export default class DoughnutChart extends React.Component {
if (this.props.data == null) {
content = (
<FormattedMessage
id='admin.analytics.loading'
id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -69,7 +69,7 @@ export default class DoughnutChart extends React.Component {
}
DoughnutChart.propTypes = {
title: React.PropTypes.string,
title: React.PropTypes.node,
width: React.PropTypes.string,
height: React.PropTypes.string,
data: React.PropTypes.array,

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -7,7 +7,7 @@ export default class StatisticCount extends React.Component {
render() {
let loading = (
<FormattedMessage
id='admin.analytics.loading'
id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -27,7 +27,7 @@ export default class StatisticCount extends React.Component {
}
StatisticCount.propTypes = {
title: React.PropTypes.string.isRequired,
title: React.PropTypes.node.isRequired,
icon: React.PropTypes.string.isRequired,
count: React.PropTypes.number
};

Просмотреть файл

@@ -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;
}

Просмотреть файл

@@ -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
};

Просмотреть файл

@@ -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;
}