Added extra system-wide statistics for EE

Этот коммит содержится в:
JoramWilander
2016-02-02 08:41:02 -05:00
родитель f28486c455
Коммит 3d03bdf2f1
17 изменённых файлов: 646 добавлений и 96 удалений

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

@@ -4,11 +4,60 @@
import * as Utils from '../../utils/utils.jsx';
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 {FormattedMessage} from 'mm-intl';
import {injectIntl, intlShape, defineMessages, FormattedMessage} 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) {
@@ -18,6 +67,8 @@ export default class Analytics extends React.Component {
}
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>;
@@ -30,77 +81,129 @@ export default class Analytics extends React.Component {
/>
);
var totalCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalUsers'
defaultMessage='Total Users'
/>
<i className='fa fa-users'/></div>
<div className='content'>{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}</div>
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>
</div>
);
);
var openChannelCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.publicChannels'
defaultMessage='Public Channels'
/>
<i className='fa fa-globe'/></div>
<div className='content'>{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}</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>
</div>
);
var openPrivateCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.privateGroups'
defaultMessage='Private Groups'
/>
<i className='fa fa-lock'/></div>
<div className='content'>{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}</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>
</div>
);
);
}
var postCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
<i className='fa fa-comment'/></div>
<div className='content'>{this.props.postCount == null ? loading : this.props.postCount}</div>
</div>
</div>
);
var postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>
<FormattedMessage
id='admin.analytics.totalPosts'
defaultMessage='Total Posts'
/>
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 className='content'>{loading}</div>
</div>
</div>
);
if (this.props.postCountsDay != null) {
);
} else {
let content;
if (this.props.postCountsDay.labels.length === 0) {
content = (
@@ -137,21 +240,22 @@ export default class Analytics extends React.Component {
);
}
var 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'
/>
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 className='content'>{loading}</div>
</div>
</div>
);
if (this.props.userCountsWithPostsDay != null) {
);
} else {
let content;
if (this.props.userCountsWithPostsDay.labels.length === 0) {
content = (
@@ -312,12 +416,8 @@ export default class Analytics extends React.Component {
/>
</h3>
{serverError}
<div className='row'>
{totalCount}
{postCount}
{openChannelCount}
{openPrivateCount}
</div>
{firstRow}
{extraGraphs}
<div className='row'>
{postCountsByDay}
</div>
@@ -347,10 +447,16 @@ Analytics.defaultProps = {
};
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,
@@ -358,3 +464,5 @@ Analytics.propTypes = {
uniqueUserCount: React.PropTypes.number,
serverError: React.PropTypes.string
};
export default injectIntl(Analytics);

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

@@ -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='admin.analytics.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.string,
width: React.PropTypes.string,
height: React.PropTypes.string,
data: React.PropTypes.array,
options: React.PropTypes.object
};

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

@@ -0,0 +1,37 @@
// 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 {
constructor(props) {
super(props);
}
render() {
let loading = (
<FormattedMessage
id='admin.analytics.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.string.isRequired,
icon: React.PropTypes.string.isRequired,
count: React.PropTypes.number
};

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

@@ -140,6 +140,34 @@ class SystemAnalytics extends React.Component {
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() {
@@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component {
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}
@@ -179,4 +213,4 @@ SystemAnalytics.propTypes = {
team: React.PropTypes.object
};
export default injectIntl(SystemAnalytics);
export default injectIntl(SystemAnalytics);

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

@@ -227,6 +227,7 @@ class TeamAnalytics extends React.Component {
return (
<div>
<Analytics
intl={this.props.intl}
title={this.props.team.name}
users={this.state.users}
channelOpenCount={this.state.channel_open_count}
@@ -249,4 +250,4 @@ TeamAnalytics.propTypes = {
team: React.PropTypes.object
};
export default injectIntl(TeamAnalytics);
export default injectIntl(TeamAnalytics);

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

@@ -14,10 +14,11 @@
padding: 7px 10px;
border-bottom: 1px solid #ddd;
text-align: left;
font-size: 13px;
.fa {
float: right;
margin: 3px 0 0;
margin: 0px 0 0;
color: #555;
font-size: 16px;
}
@@ -83,4 +84,4 @@
}
}
}
}
}

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

@@ -101,6 +101,13 @@
"admin.analytics.publicChannels": "Public Channels",
"admin.analytics.privateGroups": "Private Groups",
"admin.analytics.totalPosts": "Total Posts",
"admin.analytics.totalFilePosts": "Posts with Files",
"admin.analytics.totalHashtagPosts": "Posts with Hashtags",
"admin.analytics.totalIncomingWebhooks": "Incoming Webhooks",
"admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks",
"admin.analytics.channelTypes": "Channel Types",
"admin.analytics.textPosts": "Posts with Text-only",
"admin.analytics.postTypes": "Posts, Files and Hashtags",
"admin.analytics.meaningful": "Not enough data for a meaningful representation.",
"admin.analytics.activeUsers": "Active Users With Posts",
"admin.analytics.recentActive": "Recent Active Users",
@@ -1044,4 +1051,4 @@
"user.settings.security.title": "Security Settings",
"user.settings.security.viewHistory": "View Access History",
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions"
}
}

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

@@ -78,6 +78,13 @@
"admin.analytics.title": "Estadísticas para {title}",
"admin.analytics.totalPosts": "Total de Mensajes",
"admin.analytics.totalUsers": "Total de Usuarios",
"admin.analytics.totalFilePosts": "Posts with Files",
"admin.analytics.totalHashtagPosts": "Posts with Hashtags",
"admin.analytics.totalIncomingWebhooks": "Incoming Webhooks",
"admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks",
"admin.analytics.channelTypes": "Channel Types",
"admin.analytics.textPosts": "Posts with Text-only",
"admin.analytics.postTypes": "Posts, Files and Hashtags",
"admin.email.allowSignupDescription": "Cuando está en verdadero, Mattermost permite la creación de equipos y cuentas utilizando el correo electrónico y contraseña. Este valor debe estar en falso sólo cuando quieres limitar el inicio de sesión a través de servicios tipo OAuth o LDAP.",
"admin.email.allowSignupTitle": "Permitir inicio de sesión con correo:",
"admin.email.connectionSecurityNone": "Ninguno",
@@ -1045,4 +1052,4 @@
"user.settings.security.title": "Configuración de Seguridad",
"user.settings.security.viewHistory": "Visualizar historial de acceso",
"user_profile.notShared": "Correo no compartido"
}
}