Merge pull request #2045 from mattermost/plt-1849
PLT-1849 Added extra system-wide statistics for EE
Этот коммит содержится в:
44
api/admin.go
44
api/admin.go
@@ -161,9 +161,10 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||||
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||||
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
|
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
|
||||||
|
|
||||||
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
||||||
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
||||||
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
|
postChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
|
||||||
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
||||||
|
|
||||||
if r := <-openChan; r.Err != nil {
|
if r := <-openChan; r.Err != nil {
|
||||||
@@ -209,6 +210,47 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||||
}
|
}
|
||||||
|
} else if name == "extra_counts" {
|
||||||
|
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
|
||||||
|
rows[0] = &model.AnalyticsRow{"file_post_count", 0}
|
||||||
|
rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
|
||||||
|
rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
|
||||||
|
rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
|
||||||
|
|
||||||
|
fileChan := Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
|
||||||
|
hashtagChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
|
||||||
|
iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
|
||||||
|
oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
|
||||||
|
|
||||||
|
if r := <-fileChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[0].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-hashtagChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[1].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-iHookChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[2].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-oHookChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[3].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(rows.ToJson()))
|
||||||
} else {
|
} else {
|
||||||
c.SetInvalidParam("getAnalytics", "name")
|
c.SetInvalidParam("getAnalytics", "name")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,3 +362,113 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTeamAnalyticsExtra(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
|
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||||
|
|
||||||
|
user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||||
|
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||||
|
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||||
|
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||||
|
|
||||||
|
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||||
|
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||||
|
|
||||||
|
post2 := &model.Post{ChannelId: channel1.Id, Message: "#test a" + model.NewId() + "a"}
|
||||||
|
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||||
|
|
||||||
|
if _, err := Client.GetTeamAnalytics("", "extra_counts"); err == nil {
|
||||||
|
t.Fatal("Shouldn't have permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Context{}
|
||||||
|
c.RequestId = model.NewId()
|
||||||
|
c.IpAddress = "cmd_line"
|
||||||
|
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||||
|
|
||||||
|
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||||
|
|
||||||
|
if result, err := Client.GetTeamAnalytics(team.Id, "extra_counts"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Name != "file_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[0].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Name != "hashtag_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Name != "incoming_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Name != "outgoing_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Name != "file_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Name != "hashtag_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Value < 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Name != "incoming_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Name != "outgoing_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3079,6 +3079,14 @@
|
|||||||
"id": "store.sql_webhooks.update_outgoing.app_error",
|
"id": "store.sql_webhooks.update_outgoing.app_error",
|
||||||
"translation": "We couldn't update the webhook"
|
"translation": "We couldn't update the webhook"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_webhooks.analytics_incoming_count.app_error",
|
||||||
|
"translation": "We couldn't count the incoming webhooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_webhooks.analytics_outgoing_count.app_error",
|
||||||
|
"translation": "We couldn't count the outgoing webhooks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "utils.config.load_config.decoding.panic",
|
"id": "utils.config.load_config.decoding.panic",
|
||||||
"translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
|
"translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
|
||||||
|
|||||||
@@ -940,7 +940,7 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
|||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel {
|
||||||
storeChannel := make(StoreChannel)
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -959,8 +959,15 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
|||||||
query += " AND Channels.TeamId = :TeamId"
|
query += " AND Channels.TeamId = :TeamId"
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
|
if mustHaveFile {
|
||||||
if err != nil {
|
query += " AND Posts.Filenames != '[]'"
|
||||||
|
}
|
||||||
|
|
||||||
|
if mustHaveHashtag {
|
||||||
|
query += " AND Posts.Hashtags != ''"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
result.Err = model.NewLocAppError("SqlPostStore.AnalyticsPostCount", "store.sql_post.analytics_posts_count.app_error", nil, err.Error())
|
result.Err = model.NewLocAppError("SqlPostStore.AnalyticsPostCount", "store.sql_post.analytics_posts_count.app_error", nil, err.Error())
|
||||||
} else {
|
} else {
|
||||||
result.Data = v
|
result.Data = v
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ func TestPostCountsByDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
|
if r1 := <-store.Post().AnalyticsPostCount(t1.Id, false, false); r1.Err != nil {
|
||||||
t.Fatal(r1.Err)
|
t.Fatal(r1.Err)
|
||||||
} else {
|
} else {
|
||||||
if r1.Data.(int64) != 4 {
|
if r1.Data.(int64) != 4 {
|
||||||
|
|||||||
@@ -350,3 +350,65 @@ func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) StoreChanne
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
IncomingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " AND TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsIncomingCount", "store.sql_webhooks.analytics_incoming_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlWebhookStore) AnalyticsOutgoingCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
OutgoingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " AND TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsOutgoingCount", "store.sql_webhooks.analytics_outgoing_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -332,3 +332,42 @@ func TestWebhookStoreUpdateOutgoing(t *testing.T) {
|
|||||||
t.Fatal(r2.Err)
|
t.Fatal(r2.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebhookStoreCountIncoming(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := &model.IncomingWebhook{}
|
||||||
|
o1.ChannelId = model.NewId()
|
||||||
|
o1.UserId = model.NewId()
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
|
||||||
|
o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
|
||||||
|
|
||||||
|
if r := <-store.Webhook().AnalyticsIncomingCount(""); r.Err != nil {
|
||||||
|
t.Fatal(r.Err)
|
||||||
|
} else {
|
||||||
|
if r.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should have at least 1 incoming hook")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookStoreCountOutgoing(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := &model.OutgoingWebhook{}
|
||||||
|
o1.ChannelId = model.NewId()
|
||||||
|
o1.CreatorId = model.NewId()
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
o1.CallbackURLs = []string{"http://nowhere.com/"}
|
||||||
|
|
||||||
|
o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
|
||||||
|
|
||||||
|
if r := <-store.Webhook().AnalyticsOutgoingCount(""); r.Err != nil {
|
||||||
|
t.Fatal(r.Err)
|
||||||
|
} else {
|
||||||
|
if r.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should have at least 1 outgoing hook")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ type PostStore interface {
|
|||||||
GetForExport(channelId string) StoreChannel
|
GetForExport(channelId string) StoreChannel
|
||||||
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||||
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||||
AnalyticsPostCount(teamId string) StoreChannel
|
AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
@@ -182,6 +182,8 @@ type WebhookStore interface {
|
|||||||
DeleteOutgoing(webhookId string, time int64) StoreChannel
|
DeleteOutgoing(webhookId string, time int64) StoreChannel
|
||||||
PermanentDeleteOutgoingByUser(userId string) StoreChannel
|
PermanentDeleteOutgoingByUser(userId string) StoreChannel
|
||||||
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
|
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
|
||||||
|
AnalyticsIncomingCount(teamId string) StoreChannel
|
||||||
|
AnalyticsOutgoingCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferenceStore interface {
|
type PreferenceStore interface {
|
||||||
|
|||||||
@@ -4,11 +4,60 @@
|
|||||||
import * as Utils from '../../utils/utils.jsx';
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
import Constants from '../../utils/constants.jsx';
|
import Constants from '../../utils/constants.jsx';
|
||||||
import LineChart from './line_chart.jsx';
|
import LineChart from './line_chart.jsx';
|
||||||
|
import DoughnutChart from './doughnut_chart.jsx';
|
||||||
|
import StatisticCount from './statistic_count.jsx';
|
||||||
|
|
||||||
var Tooltip = ReactBootstrap.Tooltip;
|
var Tooltip = ReactBootstrap.Tooltip;
|
||||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
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 {
|
export default class Analytics extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -18,6 +67,8 @@ export default class Analytics extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() { // in the future, break down these into smaller components
|
render() { // in the future, break down these into smaller components
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
var serverError = '';
|
var serverError = '';
|
||||||
if (this.props.serverError) {
|
if (this.props.serverError) {
|
||||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
|
||||||
@@ -30,63 +81,116 @@ export default class Analytics extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
var totalCount = (
|
let firstRow;
|
||||||
<div className='col-sm-3'>
|
let extraGraphs;
|
||||||
<div className='total-count'>
|
if (this.props.showAdvanced) {
|
||||||
<div className='title'>
|
firstRow = (
|
||||||
<FormattedMessage
|
<div className='row'>
|
||||||
id='admin.analytics.totalUsers'
|
<StatisticCount
|
||||||
defaultMessage='Total Users'
|
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}
|
||||||
/>
|
/>
|
||||||
<i className='fa fa-users'/></div>
|
|
||||||
<div className='content'>{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
var openChannelCount = (
|
const channelTypeData = [
|
||||||
<div className='col-sm-3'>
|
{
|
||||||
<div className='total-count'>
|
value: this.props.channelOpenCount,
|
||||||
<div className='title'>
|
color: '#46BFBD',
|
||||||
<FormattedMessage
|
highlight: '#5AD3D1',
|
||||||
id='admin.analytics.publicChannels'
|
label: formatMessage(holders.analyticsPublicChannels)
|
||||||
defaultMessage='Public Channels'
|
},
|
||||||
|
{
|
||||||
|
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'
|
||||||
/>
|
/>
|
||||||
<i className='fa fa-globe'/></div>
|
|
||||||
<div className='content'>{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
var openPrivateCount = (
|
firstRow = (
|
||||||
<div className='col-sm-3'>
|
<div className='row'>
|
||||||
<div className='total-count'>
|
<StatisticCount
|
||||||
<div className='title'>
|
title={formatMessage(holders.analyticsTotalUsers)}
|
||||||
<FormattedMessage
|
icon='fa-users'
|
||||||
id='admin.analytics.privateGroups'
|
count={this.props.uniqueUserCount}
|
||||||
defaultMessage='Private Groups'
|
/>
|
||||||
|
<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}
|
||||||
/>
|
/>
|
||||||
<i className='fa fa-lock'/></div>
|
|
||||||
<div className='content'>{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var postCount = (
|
let postCountsByDay;
|
||||||
<div className='col-sm-3'>
|
if (this.props.postCountsDay == null) {
|
||||||
<div className='total-count'>
|
postCountsByDay = (
|
||||||
<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='col-sm-12'>
|
||||||
<div className='total-count by-day'>
|
<div className='total-count by-day'>
|
||||||
<div className='title'>
|
<div className='title'>
|
||||||
@@ -99,8 +203,7 @@ export default class Analytics extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
if (this.props.postCountsDay != null) {
|
|
||||||
let content;
|
let content;
|
||||||
if (this.props.postCountsDay.labels.length === 0) {
|
if (this.props.postCountsDay.labels.length === 0) {
|
||||||
content = (
|
content = (
|
||||||
@@ -137,7 +240,9 @@ export default class Analytics extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersWithPostsByDay = (
|
let usersWithPostsByDay;
|
||||||
|
if (this.props.userCountsWithPostsDay == null) {
|
||||||
|
usersWithPostsByDay = (
|
||||||
<div className='col-sm-12'>
|
<div className='col-sm-12'>
|
||||||
<div className='total-count by-day'>
|
<div className='total-count by-day'>
|
||||||
<div className='title'>
|
<div className='title'>
|
||||||
@@ -150,8 +255,7 @@ export default class Analytics extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
if (this.props.userCountsWithPostsDay != null) {
|
|
||||||
let content;
|
let content;
|
||||||
if (this.props.userCountsWithPostsDay.labels.length === 0) {
|
if (this.props.userCountsWithPostsDay.labels.length === 0) {
|
||||||
content = (
|
content = (
|
||||||
@@ -312,12 +416,8 @@ export default class Analytics extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
{serverError}
|
{serverError}
|
||||||
<div className='row'>
|
{firstRow}
|
||||||
{totalCount}
|
{extraGraphs}
|
||||||
{postCount}
|
|
||||||
{openChannelCount}
|
|
||||||
{openPrivateCount}
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
{postCountsByDay}
|
{postCountsByDay}
|
||||||
</div>
|
</div>
|
||||||
@@ -347,10 +447,16 @@ Analytics.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Analytics.propTypes = {
|
Analytics.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
channelOpenCount: React.PropTypes.number,
|
channelOpenCount: React.PropTypes.number,
|
||||||
channelPrivateCount: React.PropTypes.number,
|
channelPrivateCount: React.PropTypes.number,
|
||||||
postCount: 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,
|
postCountsDay: React.PropTypes.object,
|
||||||
userCountsWithPostsDay: React.PropTypes.object,
|
userCountsWithPostsDay: React.PropTypes.object,
|
||||||
recentActiveUsers: React.PropTypes.array,
|
recentActiveUsers: React.PropTypes.array,
|
||||||
@@ -358,3 +464,5 @@ Analytics.propTypes = {
|
|||||||
uniqueUserCount: React.PropTypes.number,
|
uniqueUserCount: React.PropTypes.number,
|
||||||
serverError: React.PropTypes.string
|
serverError: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(Analytics);
|
||||||
|
|||||||
77
web/react/components/admin_console/doughnut_chart.jsx
Обычный файл
77
web/react/components/admin_console/doughnut_chart.jsx
Обычный файл
@@ -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
|
||||||
|
};
|
||||||
37
web/react/components/admin_console/statistic_count.jsx
Обычный файл
37
web/react/components/admin_console/statistic_count.jsx
Обычный файл
@@ -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});
|
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() {
|
componentWillReceiveProps() {
|
||||||
@@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Analytics
|
<Analytics
|
||||||
|
intl={this.props.intl}
|
||||||
title={this.props.intl.formatMessage(labels.title)}
|
title={this.props.intl.formatMessage(labels.title)}
|
||||||
channelOpenCount={this.state.channel_open_count}
|
channelOpenCount={this.state.channel_open_count}
|
||||||
channelPrivateCount={this.state.channel_private_count}
|
channelPrivateCount={this.state.channel_private_count}
|
||||||
postCount={this.state.post_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}
|
postCountsDay={this.state.post_counts_day}
|
||||||
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
|
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
|
||||||
uniqueUserCount={this.state.unique_user_count}
|
uniqueUserCount={this.state.unique_user_count}
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class TeamAnalytics extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Analytics
|
<Analytics
|
||||||
|
intl={this.props.intl}
|
||||||
title={this.props.team.name}
|
title={this.props.team.name}
|
||||||
users={this.state.users}
|
users={this.state.users}
|
||||||
channelOpenCount={this.state.channel_open_count}
|
channelOpenCount={this.state.channel_open_count}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
float: right;
|
float: right;
|
||||||
margin: 3px 0 0;
|
margin: 0px 0 0;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,13 @@
|
|||||||
"admin.analytics.publicChannels": "Public Channels",
|
"admin.analytics.publicChannels": "Public Channels",
|
||||||
"admin.analytics.privateGroups": "Private Groups",
|
"admin.analytics.privateGroups": "Private Groups",
|
||||||
"admin.analytics.totalPosts": "Total Posts",
|
"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.meaningful": "Not enough data for a meaningful representation.",
|
||||||
"admin.analytics.activeUsers": "Active Users With Posts",
|
"admin.analytics.activeUsers": "Active Users With Posts",
|
||||||
"admin.analytics.recentActive": "Recent Active Users",
|
"admin.analytics.recentActive": "Recent Active Users",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user