Generalize analytics server functions and begin componentizing client analytics controls

Этот коммит содержится в:
JoramWilander
2016-01-21 12:14:17 -05:00
родитель 2fdfdaeff7
Коммит 2a26d85757
9 изменённых файлов: 407 добавлений и 256 удалений

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

@@ -27,6 +27,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -153,13 +154,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := params["name"]
if name == "standard" {
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
rows[2] = &model.AnalyticsRow{"post_count", 0}
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
if r := <-openChan; r.Err != nil {
c.Err = r.Err
@@ -182,6 +185,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[2].Value = float64(r.Data.(int64))
}
if r := <-userChan; r.Err != nil {
c.Err = r.Err
return
} else {
rows[3].Value = float64(r.Data.(int64))
}
w.Write([]byte(rows.ToJson()))
} else if name == "post_counts_day" {
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {

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

@@ -66,7 +66,9 @@ func main() {
api.InitApi()
web.InitWeb()
utils.LoadLicense()
if model.BuildEnterpriseReady == "true" {
utils.LoadLicense()
}
if flagRunCmds {
runCmds()

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

@@ -869,15 +869,13 @@ func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) S
go func() {
result := StoreResult{}
v, err := s.GetReplica().SelectInt(
`SELECT
COUNT(Id) AS Value
FROM
Channels
WHERE
TeamId = :TeamId
AND Type = :ChannelType`,
map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType"
if len(teamId) > 0 {
query += " AND TeamId = :TeamId"
}
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
if err != nil {
result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
} else {

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

@@ -805,9 +805,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
AND Posts.CreateAt <= :EndTime
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -824,9 +828,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
AND Posts.CreateAt <= :EndTime
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
ORDER BY Name DESC) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -869,9 +877,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
AND Posts.CreateAt <= :EndTime
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -888,9 +900,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
FROM
Posts, Channels
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId
AND Posts.CreateAt <= :EndTime
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
query += ` AND Posts.CreateAt <= :EndTime
AND Posts.CreateAt >= :StartTime) AS t1
GROUP BY Name
ORDER BY Name DESC
@@ -924,16 +940,20 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
go func() {
result := StoreResult{}
v, err := s.GetReplica().SelectInt(
query :=
`SELECT
COUNT(Posts.Id) AS Value
FROM
Posts,
Channels
WHERE
Posts.ChannelId = Channels.Id
AND Channels.TeamId = :TeamId`,
map[string]interface{}{"TeamId": teamId})
Posts.ChannelId = Channels.Id`
if len(teamId) > 0 {
query += " AND Channels.TeamId = :TeamId"
}
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
if err != nil {
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
} else {

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

@@ -600,3 +600,30 @@ func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
return storeChannel
}
func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
query := "SELECT COUNT(DISTINCT Email) FROM Users"
if len(teamId) > 0 {
query += " WHERE TeamId = :TeamId"
}
v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
if err != nil {
result.Err = model.NewAppError("SqlUserStore.AnalyticsUniqueUserCount", "We couldn't get the unique user count", err.Error())
} else {
result.Data = v
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

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

@@ -125,6 +125,7 @@ type UserStore interface {
GetTotalActiveUsersCount() StoreChannel
GetSystemAdminProfiles() StoreChannel
PermanentDelete(userId string) StoreChannel
AnalyticsUniqueUserCount(teamId string) StoreChannel
}
type SessionStore interface {

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

@@ -0,0 +1,277 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class Analytics extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() { // in the future, break down these into smaller components
var serverError = '';
if (this.props.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
}
var totalCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
<div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div>
</div>
</div>
);
var openChannelCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
<div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div>
</div>
</div>
);
var openPrivateCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
<div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div>
</div>
</div>
);
var postCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'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'>{'Total Posts'}</div>
<div className='content'>{'Loading...'}</div>
</div>
</div>
);
if (this.props.postCountsDay != null) {
let content;
if (this.props.postCountsDay.labels.length === 0) {
content = 'Not enough data for a meaningful representation.';
} 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'>{'Total Posts'}</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
var usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>{'Total Posts'}</div>
<div>{'Loading...'}</div>
</div>
</div>
);
if (this.props.userCountsWithPostsDay != null) {
let content;
if (this.props.userCountsWithPostsDay.labels.length === 0) {
content = 'Not enough data for a meaningful representation.';
} 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'>{'Active Users With Posts'}</div>
<div className='content'>
{content}
</div>
</div>
</div>
);
}
var recentActiveUser = (
<div className='recent-active-users'>
<div>{'Recent Active Users'}</div>
<div>{'Loading...'}</div>
</div>
);
if (this.props.recentActiveUsers != null) {
recentActiveUser = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>{'Recent Active Users'}</div>
<div className='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>{Utils.displayDateTime(user.last_activity_at)}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
);
}
var newUsers = (
<div className='recent-active-users'>
<div>{'Newly Created Users'}</div>
<div>{'Loading...'}</div>
</div>
);
if (this.props.newlyCreatedUsers != null) {
newUsers = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>{'Newly Created Users'}</div>
<div className='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>{Utils.displayDateTime(user.create_at)}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
);
}
return (
<div className='wrapper--fixed team_statistics'>
<h3>{'Statistics for ' + this.props.title}</h3>
<div className='row'>
{totalCount}
{postCount}
{openChannelCount}
{openPrivateCount}
</div>
<div className='row'>
{postCountsByDay}
</div>
<div className='row'>
{usersWithPostsByDay}
</div>
<div className='row'>
{recentActiveUser}
{newUsers}
</div>
</div>
);
}
}
Analytics.defaultProps = {
title: null,
users: null,
channelOpenCount: null,
channelPrivateCount: null,
postCount: null,
postCountsDay: null,
userCountsWithPostsDay: null,
recentActiveUsers: null,
newlyCreatedUsers: null,
uniqueUserCount: null,
serverError: null
};
Analytics.propTypes = {
title: React.PropTypes.string,
users: React.PropTypes.object,
channelOpenCount: React.PropTypes.number,
channelPrivateCount: React.PropTypes.number,
postCount: 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
};

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

@@ -1,13 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// 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 * as Utils from '../../utils/utils.jsx';
import Constants from '../../utils/constants.jsx';
import LineChart from './line_chart.jsx';
var Tooltip = ReactBootstrap.Tooltip;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class TeamAnalytics extends React.Component {
constructor(props) {
@@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component {
this.getData = this.getData.bind(this);
this.state = {
this.state = { // most of this state should be from a store in the future
users: null,
serverError: null,
channel_open_count: null,
@@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
newly_created_users: null
newly_created_users: null,
unique_user_count: null
};
}
@@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component {
this.getData(this.props.team.id);
}
getData(teamId) {
Client.getAnalytics(
getData(teamId) { // should be moved to an action creator eventually
Client.getTeamAnalytics(
teamId,
'standard',
(data) => {
@@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component {
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) => {
@@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component {
}
);
Client.getAnalytics(
Client.getTeamAnalytics(
teamId,
'post_counts_day',
(data) => {
@@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component {
}
);
Client.getAnalytics(
Client.getTeamAnalytics(
teamId,
'user_counts_with_posts_day',
(data) => {
@@ -198,227 +198,29 @@ export default class TeamAnalytics extends React.Component {
post_counts_day: null,
user_counts_with_posts_day: null,
recent_active_users: null,
newly_created_users: null
newly_created_users: null,
unique_user_count: null
});
this.getData(newProps.team.id);
}
componentWillUnmount() {
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var totalCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
<div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
</div>
</div>
);
var openChannelCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
<div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
</div>
</div>
);
var openPrivateCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
<div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
</div>
</div>
);
var postCount = (
<div className='col-sm-3'>
<div className='total-count'>
<div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
<div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
</div>
</div>
);
var postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>{'Total Posts'}</div>
<div className='content'>{'Loading...'}</div>
</div>
</div>
);
if (this.state.post_counts_day != null) {
postCountsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>{'Total Posts'}</div>
<div className='content'>
<LineChart
data={this.state.post_counts_day}
width='740'
height='225'
/>
</div>
</div>
</div>
);
}
var usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>{'Total Posts'}</div>
<div>{'Loading...'}</div>
</div>
</div>
);
if (this.state.user_counts_with_posts_day != null) {
usersWithPostsByDay = (
<div className='col-sm-12'>
<div className='total-count by-day'>
<div className='title'>{'Active Users With Posts'}</div>
<div className='content'>
<LineChart
data={this.state.user_counts_with_posts_day}
width='740'
height='225'
/>
</div>
</div>
</div>
);
}
var recentActiveUser = (
<div className='recent-active-users'>
<div>{'Recent Active Users'}</div>
<div>{'Loading...'}</div>
</div>
);
if (this.state.recent_active_users != null) {
recentActiveUser = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>{'Recent Active Users'}</div>
<div className='content'>
<table>
<tbody>
{
this.state.recent_active_users.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>{Utils.displayDateTime(user.last_activity_at)}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
);
}
var newUsers = (
<div className='recent-active-users'>
<div>{'Newly Created Users'}</div>
<div>{'Loading...'}</div>
</div>
);
if (this.state.newly_created_users != null) {
newUsers = (
<div className='col-sm-6'>
<div className='total-count recent-active-users'>
<div className='title'>{'Newly Created Users'}</div>
<div className='content'>
<table>
<tbody>
{
this.state.newly_created_users.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>{Utils.displayDateTime(user.create_at)}</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
</div>
);
}
return (
<div className='wrapper--fixed team_statistics'>
<h3>{'Statistics for ' + this.props.team.name}</h3>
{serverError}
<div className='row'>
{totalCount}
{postCount}
{openChannelCount}
{openPrivateCount}
</div>
<div className='row'>
{postCountsByDay}
</div>
<div className='row'>
{usersWithPostsByDay}
</div>
<div className='row'>
{recentActiveUser}
{newUsers}
</div>
<div>
<Analytics
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>
);
}

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

@@ -387,7 +387,7 @@ export function getConfig(success, error) {
});
}
export function getAnalytics(teamId, name, success, error) {
export function getTeamAnalytics(teamId, name, success, error) {
$.ajax({
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
dataType: 'json',
@@ -395,7 +395,21 @@ export function getAnalytics(teamId, name, success, error) {
type: 'GET',
success,
error: (xhr, status, err) => {
var e = handleError('getAnalytics', xhr, status, err);
var e = handleError('getTeamAnalytics', xhr, status, err);
error(e);
}
});
}
export function getServerAnalytics(name, success, error) {
$.ajax({
url: '/api/v1/admin/analytics/' + name,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success,
error: (xhr, status, err) => {
var e = handleError('getServerAnalytics', xhr, status, err);
error(e);
}
});