Refactor and modularize analytics on the client
Этот коммит содержится в:
31
api/admin.go
31
api/admin.go
@@ -184,16 +184,18 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
name := params["name"]
|
name := params["name"]
|
||||||
|
|
||||||
if name == "standard" {
|
if name == "standard" {
|
||||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
|
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 5)
|
||||||
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
||||||
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}
|
||||||
|
rows[4] = &model.AnalyticsRow{"team_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, false, false)
|
postChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
|
||||||
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
||||||
|
teamChan := Srv.Store.Team().AnalyticsTeamCount()
|
||||||
|
|
||||||
if r := <-openChan; r.Err != nil {
|
if r := <-openChan; r.Err != nil {
|
||||||
c.Err = r.Err
|
c.Err = r.Err
|
||||||
@@ -223,6 +225,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
rows[3].Value = float64(r.Data.(int64))
|
rows[3].Value = float64(r.Data.(int64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r := <-teamChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[4].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
w.Write([]byte(rows.ToJson()))
|
w.Write([]byte(rows.ToJson()))
|
||||||
} else if name == "post_counts_day" {
|
} else if name == "post_counts_day" {
|
||||||
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
|
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
|
||||||
@@ -239,16 +248,20 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||||
}
|
}
|
||||||
} else if name == "extra_counts" {
|
} else if name == "extra_counts" {
|
||||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
|
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
|
||||||
rows[0] = &model.AnalyticsRow{"file_post_count", 0}
|
rows[0] = &model.AnalyticsRow{"file_post_count", 0}
|
||||||
rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
|
rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
|
||||||
rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
|
rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
|
||||||
rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
|
rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
|
||||||
|
rows[4] = &model.AnalyticsRow{"command_count", 0}
|
||||||
|
rows[5] = &model.AnalyticsRow{"session_count", 0}
|
||||||
|
|
||||||
fileChan := Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
|
fileChan := Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
|
||||||
hashtagChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
|
hashtagChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
|
||||||
iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
|
iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
|
||||||
oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
|
oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
|
||||||
|
commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId)
|
||||||
|
sessionChan := Srv.Store.Session().AnalyticsSessionCount(teamId)
|
||||||
|
|
||||||
if r := <-fileChan; r.Err != nil {
|
if r := <-fileChan; r.Err != nil {
|
||||||
c.Err = r.Err
|
c.Err = r.Err
|
||||||
@@ -278,6 +291,20 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
rows[3].Value = float64(r.Data.(int64))
|
rows[3].Value = float64(r.Data.(int64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r := <-commandChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[4].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-sessionChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[5].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
w.Write([]byte(rows.ToJson()))
|
w.Write([]byte(rows.ToJson()))
|
||||||
} else {
|
} else {
|
||||||
c.SetInvalidParam("getAnalytics", "name")
|
c.SetInvalidParam("getAnalytics", "name")
|
||||||
|
|||||||
@@ -254,6 +254,16 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
|||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rows[4].Name != "team_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[4].Value == 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if result, err := Client.GetSystemAnalytics("standard"); err != nil {
|
if result, err := Client.GetSystemAnalytics("standard"); err != nil {
|
||||||
@@ -300,6 +310,16 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
|
|||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rows[4].Name != "team_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[4].Value == 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +489,26 @@ func TestGetTeamAnalyticsExtra(t *testing.T) {
|
|||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rows[4].Name != "command_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[4].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[5].Name != "session_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[5].Value == 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
|
if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
|
||||||
@@ -500,5 +540,15 @@ func TestGetTeamAnalyticsExtra(t *testing.T) {
|
|||||||
t.Log(rows.ToJson())
|
t.Log(rows.ToJson())
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rows[4].Name != "command_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[5].Name != "session_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
i18n/en.json
12
i18n/en.json
@@ -2675,6 +2675,10 @@
|
|||||||
"id": "store.sql_command.save.update.app_error",
|
"id": "store.sql_command.save.update.app_error",
|
||||||
"translation": "We couldn't update the command"
|
"translation": "We couldn't update the command"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_command.analytics_command_count.app_error",
|
||||||
|
"translation": "We couldn't count the commands"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_license.get.app_error",
|
"id": "store.sql_license.get.app_error",
|
||||||
"translation": "We encountered an error getting the license"
|
"translation": "We encountered an error getting the license"
|
||||||
@@ -2947,6 +2951,10 @@
|
|||||||
"id": "store.sql_session.update_roles.app_error",
|
"id": "store.sql_session.update_roles.app_error",
|
||||||
"translation": "We couldn't update the roles"
|
"translation": "We couldn't update the roles"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_session.analytics_session_count.app_error",
|
||||||
|
"translation": "We couldn't count the sessions"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_system.get.app_error",
|
"id": "store.sql_system.get.app_error",
|
||||||
"translation": "We encountered an error finding the system properties"
|
"translation": "We encountered an error finding the system properties"
|
||||||
@@ -3027,6 +3035,10 @@
|
|||||||
"id": "store.sql_team.update_display_name.app_error",
|
"id": "store.sql_team.update_display_name.app_error",
|
||||||
"translation": "We couldn't update the team name"
|
"translation": "We couldn't update the team name"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_team.analytics_team_count.app_error",
|
||||||
|
"translation": "We couldn't count the teams"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.analytics_unique_user_count.app_error",
|
"id": "store.sql_user.analytics_unique_user_count.app_error",
|
||||||
"translation": "We couldn't get the unique user count"
|
"translation": "We couldn't get the unique user count"
|
||||||
|
|||||||
@@ -151,18 +151,49 @@ func (s SqlCommandStore) PermanentDeleteByUser(userId string) StoreChannel {
|
|||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlCommandStore) Update(hook *model.Command) StoreChannel {
|
func (s SqlCommandStore) Update(cmd *model.Command) StoreChannel {
|
||||||
storeChannel := make(StoreChannel)
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
result := StoreResult{}
|
result := StoreResult{}
|
||||||
|
|
||||||
hook.UpdateAt = model.GetMillis()
|
cmd.UpdateAt = model.GetMillis()
|
||||||
|
|
||||||
if _, err := s.GetMaster().Update(hook); err != nil {
|
if _, err := s.GetMaster().Update(cmd); err != nil {
|
||||||
result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+hook.Id+", "+err.Error())
|
result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+cmd.Id+", "+err.Error())
|
||||||
} else {
|
} else {
|
||||||
result.Data = hook
|
result.Data = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlCommandStore) AnalyticsCommandCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
Commands
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " AND TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlCommandStore.AnalyticsCommandCount", "store.sql_command.analytics_command_count.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = c
|
||||||
}
|
}
|
||||||
|
|
||||||
storeChannel <- result
|
storeChannel <- result
|
||||||
|
|||||||
@@ -153,3 +153,31 @@ func TestCommandStoreUpdate(t *testing.T) {
|
|||||||
t.Fatal(r2.Err)
|
t.Fatal(r2.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandCount(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := &model.Command{}
|
||||||
|
o1.CreatorId = model.NewId()
|
||||||
|
o1.Method = model.COMMAND_METHOD_POST
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
o1.URL = "http://nowhere.com/"
|
||||||
|
|
||||||
|
o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
|
||||||
|
|
||||||
|
if r1 := <-store.Command().AnalyticsCommandCount(""); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should be at least 1 command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r2 := <-store.Command().AnalyticsCommandCount(o1.TeamId); r2.Err != nil {
|
||||||
|
t.Fatal(r2.Err)
|
||||||
|
} else {
|
||||||
|
if r2.Data.(int64) != 1 {
|
||||||
|
t.Fatal("should be 1 command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -255,3 +255,32 @@ func (me SqlSessionStore) UpdateDeviceId(id, deviceId string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (me SqlSessionStore) AnalyticsSessionCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
Sessions`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " WHERE TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := me.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlSessionStore.AnalyticsSessionCount", "store.sql_session.analytics_session_count.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = c
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,3 +200,28 @@ func TestSessionStoreUpdateLastActivityAt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionCount(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
s1 := model.Session{}
|
||||||
|
s1.UserId = model.NewId()
|
||||||
|
s1.TeamId = model.NewId()
|
||||||
|
Must(store.Session().Save(&s1))
|
||||||
|
|
||||||
|
if r1 := <-store.Session().AnalyticsSessionCount(""); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should have at least 1 session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r2 := <-store.Session().AnalyticsSessionCount(s1.TeamId); r2.Err != nil {
|
||||||
|
t.Fatal(r2.Err)
|
||||||
|
} else {
|
||||||
|
if r2.Data.(int64) != 1 {
|
||||||
|
t.Fatal("should have 1 session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,3 +317,22 @@ func (s SqlTeamStore) PermanentDelete(teamId string) StoreChannel {
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlTeamStore) AnalyticsTeamCount() StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0", map[string]interface{}{}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlTeamStore.AnalyticsTeamCount", "store.sql_team.analytics_team_count.app_error", nil, err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = c
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -261,3 +261,23 @@ func TestDelete(t *testing.T) {
|
|||||||
t.Fatal(r1.Err)
|
t.Fatal(r1.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTeamCount(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := model.Team{}
|
||||||
|
o1.DisplayName = "DisplayName"
|
||||||
|
o1.Name = "a" + model.NewId() + "b"
|
||||||
|
o1.Email = model.NewId() + "@nowhere.com"
|
||||||
|
o1.Type = model.TEAM_OPEN
|
||||||
|
o1.AllowTeamListing = true
|
||||||
|
Must(store.Team().Save(&o1))
|
||||||
|
|
||||||
|
if r1 := <-store.Team().AnalyticsTeamCount(); r1.Err != nil {
|
||||||
|
t.Fatal(r1.Err)
|
||||||
|
} else {
|
||||||
|
if r1.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should be at least 1 team")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type TeamStore interface {
|
|||||||
GetAllTeamListing() StoreChannel
|
GetAllTeamListing() StoreChannel
|
||||||
GetByInviteId(inviteId string) StoreChannel
|
GetByInviteId(inviteId string) StoreChannel
|
||||||
PermanentDelete(teamId string) StoreChannel
|
PermanentDelete(teamId string) StoreChannel
|
||||||
|
AnalyticsTeamCount() StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelStore interface {
|
type ChannelStore interface {
|
||||||
@@ -141,6 +142,7 @@ type SessionStore interface {
|
|||||||
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
|
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
|
||||||
UpdateRoles(userId string, roles string) StoreChannel
|
UpdateRoles(userId string, roles string) StoreChannel
|
||||||
UpdateDeviceId(id string, deviceId string) StoreChannel
|
UpdateDeviceId(id string, deviceId string) StoreChannel
|
||||||
|
AnalyticsSessionCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuditStore interface {
|
type AuditStore interface {
|
||||||
@@ -196,6 +198,7 @@ type CommandStore interface {
|
|||||||
Delete(commandId string, time int64) StoreChannel
|
Delete(commandId string, time int64) StoreChannel
|
||||||
PermanentDeleteByUser(userId string) StoreChannel
|
PermanentDeleteByUser(userId string) StoreChannel
|
||||||
Update(hook *model.Command) StoreChannel
|
Update(hook *model.Command) StoreChannel
|
||||||
|
AnalyticsCommandCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferenceStore interface {
|
type PreferenceStore interface {
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import TeamSettingsTab from './team_settings.jsx';
|
|||||||
import ServiceSettingsTab from './service_settings.jsx';
|
import ServiceSettingsTab from './service_settings.jsx';
|
||||||
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
|
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
|
||||||
import TeamUsersTab from './team_users.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 LdapSettingsTab from './ldap_settings.jsx';
|
||||||
import LicenseSettingsTab from './license_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 {
|
export default class AdminController extends React.Component {
|
||||||
constructor(props) {
|
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) {
|
if (this.props.data == null) {
|
||||||
content = (
|
content = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.analytics.loading'
|
id='analytics.chart.loading'
|
||||||
defaultMessage='Loading...'
|
defaultMessage='Loading...'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -69,7 +69,7 @@ export default class DoughnutChart extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DoughnutChart.propTypes = {
|
DoughnutChart.propTypes = {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.node,
|
||||||
width: React.PropTypes.string,
|
width: React.PropTypes.string,
|
||||||
height: React.PropTypes.string,
|
height: React.PropTypes.string,
|
||||||
data: React.PropTypes.array,
|
data: React.PropTypes.array,
|
||||||
90
web/react/components/analytics/line_chart.jsx
Обычный файл
90
web/react/components/analytics/line_chart.jsx
Обычный файл
@@ -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() {
|
render() {
|
||||||
let loading = (
|
let loading = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.analytics.loading'
|
id='analytics.chart.loading'
|
||||||
defaultMessage='Loading...'
|
defaultMessage='Loading...'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -27,7 +27,7 @@ export default class StatisticCount extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatisticCount.propTypes = {
|
StatisticCount.propTypes = {
|
||||||
title: React.PropTypes.string.isRequired,
|
title: React.PropTypes.node.isRequired,
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
count: React.PropTypes.number
|
count: React.PropTypes.number
|
||||||
};
|
};
|
||||||
346
web/react/components/analytics/system_analytics.jsx
Обычный файл
346
web/react/components/analytics/system_analytics.jsx
Обычный файл
@@ -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;
|
||||||
|
}
|
||||||
60
web/react/components/analytics/table_chart.jsx
Обычный файл
60
web/react/components/analytics/table_chart.jsx
Обычный файл
@@ -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
|
||||||
|
};
|
||||||
235
web/react/components/analytics/team_analytics.jsx
Обычный файл
235
web/react/components/analytics/team_analytics.jsx
Обычный файл
@@ -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;
|
||||||
|
}
|
||||||
85
web/react/stores/analytics_store.jsx
Обычный файл
85
web/react/stores/analytics_store.jsx
Обычный файл
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
import Constants from '../utils/constants.jsx';
|
||||||
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
|
const CHANGE_EVENT = 'change';
|
||||||
|
|
||||||
|
class AnalyticsStoreClass extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.systemStats = {};
|
||||||
|
this.teamStats = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
emitChange() {
|
||||||
|
this.emit(CHANGE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
addChangeListener(callback) {
|
||||||
|
this.on(CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChangeListener(callback) {
|
||||||
|
this.removeListener(CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSystem() {
|
||||||
|
return JSON.parse(JSON.stringify(this.systemStats));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllTeam(id) {
|
||||||
|
if (id in this.teamStats) {
|
||||||
|
return JSON.parse(JSON.stringify(this.teamStats[id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
storeSystemStats(newStats) {
|
||||||
|
for (const stat in newStats) {
|
||||||
|
if (!newStats.hasOwnProperty(stat)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.systemStats[stat] = newStats[stat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storeTeamStats(id, newStats) {
|
||||||
|
if (!(id in this.teamStats)) {
|
||||||
|
this.teamStats[id] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const stat in newStats) {
|
||||||
|
if (!newStats.hasOwnProperty(stat)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.teamStats[id][stat] = newStats[stat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var AnalyticsStore = new AnalyticsStoreClass();
|
||||||
|
|
||||||
|
AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||||
|
var action = payload.action;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.RECEIVED_ANALYTICS:
|
||||||
|
if (action.teamId == null) {
|
||||||
|
AnalyticsStore.storeSystemStats(action.stats);
|
||||||
|
} else {
|
||||||
|
AnalyticsStore.storeTeamStats(action.teamId, action.stats);
|
||||||
|
}
|
||||||
|
AnalyticsStore.emitChange();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AnalyticsStore;
|
||||||
@@ -11,16 +11,17 @@ import UserStore from '../stores/user_store.jsx';
|
|||||||
import * as utils from './utils.jsx';
|
import * as utils from './utils.jsx';
|
||||||
|
|
||||||
import Constants from './constants.jsx';
|
import Constants from './constants.jsx';
|
||||||
var ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
const StatTypes = Constants.StatTypes;
|
||||||
|
|
||||||
// Used to track in progress async calls
|
// Used to track in progress async calls
|
||||||
var callTracker = {};
|
const callTracker = {};
|
||||||
|
|
||||||
export function dispatchError(err, method) {
|
export function dispatchError(err, method) {
|
||||||
AppDispatcher.handleServerAction({
|
AppDispatcher.handleServerAction({
|
||||||
type: ActionTypes.RECEIVED_ERROR,
|
type: ActionTypes.RECEIVED_ERROR,
|
||||||
err: err,
|
err,
|
||||||
method: method
|
method
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,3 +849,264 @@ export function getFileInfo(filename) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStandardAnalytics(teamId) {
|
||||||
|
const callName = 'getStandardAnaytics' + teamId;
|
||||||
|
|
||||||
|
if (isCallInProgress(callName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker[callName] = utils.getTimestamp();
|
||||||
|
|
||||||
|
client.getAnalytics(
|
||||||
|
'standard',
|
||||||
|
teamId,
|
||||||
|
(data) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
const stats = {};
|
||||||
|
|
||||||
|
for (const index in data) {
|
||||||
|
if (data[index].name === 'channel_open_count') {
|
||||||
|
stats[StatTypes.TOTAL_PUBLIC_CHANNELS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'channel_private_count') {
|
||||||
|
stats[StatTypes.TOTAL_PRIVATE_GROUPS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'post_count') {
|
||||||
|
stats[StatTypes.TOTAL_POSTS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'unique_user_count') {
|
||||||
|
stats[StatTypes.TOTAL_USERS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'team_count' && teamId == null) {
|
||||||
|
stats[StatTypes.TOTAL_TEAMS] = data[index].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_ANALYTICS,
|
||||||
|
teamId,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
dispatchError(err, 'getStandardAnalytics');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdvancedAnalytics(teamId) {
|
||||||
|
const callName = 'getAdvancedAnalytics' + teamId;
|
||||||
|
|
||||||
|
if (isCallInProgress(callName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker[callName] = utils.getTimestamp();
|
||||||
|
|
||||||
|
client.getAnalytics(
|
||||||
|
'extra_counts',
|
||||||
|
teamId,
|
||||||
|
(data) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
const stats = {};
|
||||||
|
|
||||||
|
for (const index in data) {
|
||||||
|
if (data[index].name === 'file_post_count') {
|
||||||
|
stats[StatTypes.TOTAL_FILE_POSTS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'hashtag_post_count') {
|
||||||
|
stats[StatTypes.TOTAL_HASHTAG_POSTS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'incoming_webhook_count') {
|
||||||
|
stats[StatTypes.TOTAL_IHOOKS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'outgoing_webhook_count') {
|
||||||
|
stats[StatTypes.TOTAL_OHOOKS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'command_count') {
|
||||||
|
stats[StatTypes.TOTAL_COMMANDS] = data[index].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[index].name === 'session_count') {
|
||||||
|
stats[StatTypes.TOTAL_SESSIONS] = data[index].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_ANALYTICS,
|
||||||
|
teamId,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
dispatchError(err, 'getAdvancedAnalytics');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPostsPerDayAnalytics(teamId) {
|
||||||
|
const callName = 'getPostsPerDayAnalytics' + teamId;
|
||||||
|
|
||||||
|
if (isCallInProgress(callName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker[callName] = utils.getTimestamp();
|
||||||
|
|
||||||
|
client.getAnalytics(
|
||||||
|
'post_counts_day',
|
||||||
|
teamId,
|
||||||
|
(data) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
data.reverse();
|
||||||
|
|
||||||
|
const stats = {};
|
||||||
|
stats[StatTypes.POST_PER_DAY] = data;
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_ANALYTICS,
|
||||||
|
teamId,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
dispatchError(err, 'getPostsPerDayAnalytics');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsersPerDayAnalytics(teamId) {
|
||||||
|
const callName = 'getUsersPerDayAnalytics' + teamId;
|
||||||
|
|
||||||
|
if (isCallInProgress(callName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker[callName] = utils.getTimestamp();
|
||||||
|
|
||||||
|
client.getAnalytics(
|
||||||
|
'user_counts_with_posts_day',
|
||||||
|
teamId,
|
||||||
|
(data) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
data.reverse();
|
||||||
|
|
||||||
|
const stats = {};
|
||||||
|
stats[StatTypes.USERS_WITH_POSTS_PER_DAY] = data;
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_ANALYTICS,
|
||||||
|
teamId,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
dispatchError(err, 'getUsersPerDayAnalytics');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecentAndNewUsersAnalytics(teamId) {
|
||||||
|
const callName = 'getRecentAndNewUsersAnalytics' + teamId;
|
||||||
|
|
||||||
|
if (isCallInProgress(callName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker[callName] = utils.getTimestamp();
|
||||||
|
|
||||||
|
client.getProfilesForTeam(
|
||||||
|
teamId,
|
||||||
|
(users) => {
|
||||||
|
const stats = {};
|
||||||
|
|
||||||
|
const usersList = [];
|
||||||
|
for (const 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActive = [];
|
||||||
|
for (let i = 0; i < usersList.length; i++) {
|
||||||
|
if (usersList[i].last_activity_at == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
recentActive.push(usersList[i]);
|
||||||
|
if (i >= Constants.STAT_MAX_ACTIVE_USERS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats[StatTypes.RECENTLY_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 >= Constants.STAT_MAX_NEW_USERS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats[StatTypes.NEWLY_CREATED_USERS] = newlyCreated;
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_ANALYTICS,
|
||||||
|
teamId,
|
||||||
|
stats
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker[callName] = 0;
|
||||||
|
|
||||||
|
dispatchError(err, 'getRecentAndNewUsersAnalytics');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -435,23 +435,16 @@ export function getConfig(success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTeamAnalytics(teamId, name, success, error) {
|
export function getAnalytics(name, teamId, success, error) {
|
||||||
$.ajax({
|
let url = '/api/v1/admin/analytics/';
|
||||||
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
|
if (teamId == null) {
|
||||||
dataType: 'json',
|
url += name;
|
||||||
contentType: 'application/json',
|
} else {
|
||||||
type: 'GET',
|
url += teamId + '/' + name;
|
||||||
success,
|
|
||||||
error: (xhr, status, err) => {
|
|
||||||
var e = handleError('getTeamAnalytics', xhr, status, err);
|
|
||||||
error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSystemAnalytics(name, success, error) {
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/admin/analytics/' + name,
|
url,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
|
|||||||
@@ -71,6 +71,26 @@ export default {
|
|||||||
VIEW_ACTION: null
|
VIEW_ACTION: null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
StatTypes: keyMirror({
|
||||||
|
TOTAL_USERS: null,
|
||||||
|
TOTAL_PUBLIC_CHANNELS: null,
|
||||||
|
TOTAL_PRIVATE_GROUPS: null,
|
||||||
|
TOTAL_POSTS: null,
|
||||||
|
TOTAL_TEAMS: null,
|
||||||
|
TOTAL_FILE_POSTS: null,
|
||||||
|
TOTAL_HASHTAG_POSTS: null,
|
||||||
|
TOTAL_IHOOKS: null,
|
||||||
|
TOTAL_OHOOKS: null,
|
||||||
|
TOTAL_COMMANDS: null,
|
||||||
|
TOTAL_SESSIONS: null,
|
||||||
|
POST_PER_DAY: null,
|
||||||
|
USERS_WITH_POSTS_PER_DAY: null,
|
||||||
|
RECENTLY_ACTIVE_USERS: null,
|
||||||
|
NEWLY_CREATED_USERS: null
|
||||||
|
}),
|
||||||
|
STAT_MAX_ACTIVE_USERS: 20,
|
||||||
|
STAT_MAX_NEW_USERS: 20,
|
||||||
|
|
||||||
SocketEvents: {
|
SocketEvents: {
|
||||||
POSTED: 'posted',
|
POSTED: 'posted',
|
||||||
POST_EDITED: 'post_edited',
|
POST_EDITED: 'post_edited',
|
||||||
|
|||||||
@@ -21,23 +21,33 @@
|
|||||||
"activity_log_modal.android": "Android",
|
"activity_log_modal.android": "Android",
|
||||||
"activity_log_modal.androidNativeApp": "Android Native App",
|
"activity_log_modal.androidNativeApp": "Android Native App",
|
||||||
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
|
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
|
||||||
"admin.analytics.activeUsers": "Active Users With Posts",
|
"analytics.chart.loading": "Loading...",
|
||||||
"admin.analytics.channelTypes": "Channel Types",
|
"analytics.chart.meaningful": "Not enough data for a meaningful representation.",
|
||||||
"admin.analytics.loading": "Loading...",
|
"analytics.system.activeUsers": "Active Users With Posts",
|
||||||
"admin.analytics.meaningful": "Not enough data for a meaningful representation.",
|
"analytics.system.channelTypes": "Channel Types",
|
||||||
"admin.analytics.newlyCreated": "Newly Created Users",
|
"analytics.system.postTypes": "Posts, Files and Hashtags",
|
||||||
"admin.analytics.postTypes": "Posts, Files and Hashtags",
|
"analytics.system.privateGroups": "Private Groups",
|
||||||
"admin.analytics.privateGroups": "Private Groups",
|
"analytics.system.publicChannels": "Public Channels",
|
||||||
"admin.analytics.publicChannels": "Public Channels",
|
"analytics.system.textPosts": "Posts with Text-only",
|
||||||
"admin.analytics.recentActive": "Recent Active Users",
|
"analytics.system.title": "System Statistics",
|
||||||
"admin.analytics.textPosts": "Posts with Text-only",
|
"analytics.system.totalFilePosts": "Posts with Files",
|
||||||
"admin.analytics.title": "Statistics for {title}",
|
"analytics.system.totalHashtagPosts": "Posts with Hashtags",
|
||||||
"admin.analytics.totalFilePosts": "Posts with Files",
|
"analytics.system.totalIncomingWebhooks": "Incoming Webhooks",
|
||||||
"admin.analytics.totalHashtagPosts": "Posts with Hashtags",
|
"analytics.system.totalOutgoingWebhooks": "Outgoing Webhooks",
|
||||||
"admin.analytics.totalIncomingWebhooks": "Incoming Webhooks",
|
"analytics.system.totalCommands": "Total Commands",
|
||||||
"admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks",
|
"analytics.system.totalSessions": "Total Sessions",
|
||||||
"admin.analytics.totalPosts": "Total Posts",
|
"analytics.system.totalPosts": "Total Posts",
|
||||||
"admin.analytics.totalUsers": "Total Users",
|
"analytics.system.totalUsers": "Total Users",
|
||||||
|
"analytics.system.totalTeams": "Total Teams",
|
||||||
|
"analytics.system.totalChannels": "Total Channels",
|
||||||
|
"analytics.team.activeUsers": "Active Users With Posts",
|
||||||
|
"analytics.team.recentActive": "Recent Active Users",
|
||||||
|
"analytics.team.newlyCreated": "Newly Created Users",
|
||||||
|
"analytics.team.privateGroups": "Private Groups",
|
||||||
|
"analytics.team.publicChannels": "Public Channels",
|
||||||
|
"analytics.team.title": "Team Statistics for {team}",
|
||||||
|
"analytics.team.totalPosts": "Total Posts",
|
||||||
|
"analytics.team.totalUsers": "Total Users",
|
||||||
"admin.audits.reload": "Reload",
|
"admin.audits.reload": "Reload",
|
||||||
"admin.audits.title": "User Activity",
|
"admin.audits.title": "User Activity",
|
||||||
"admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
|
"admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
|
||||||
|
|||||||
@@ -21,23 +21,26 @@
|
|||||||
"activity_log_modal.android": "Android",
|
"activity_log_modal.android": "Android",
|
||||||
"activity_log_modal.androidNativeApp": "Android App Nativa",
|
"activity_log_modal.androidNativeApp": "Android App Nativa",
|
||||||
"activity_log_modal.iphoneNativeApp": "iPhone App Nativa",
|
"activity_log_modal.iphoneNativeApp": "iPhone App Nativa",
|
||||||
"admin.analytics.activeUsers": "Usuarios Activos con Mensajes",
|
"analytics.chart.loading": "Cargando...",
|
||||||
"admin.analytics.channelTypes": "Tipos de Canales",
|
"analytics.chart.meaningful": "No hay suficiente data para tener una representación significativa.",
|
||||||
"admin.analytics.loading": "Cargando...",
|
"analytics.system.channelTypes": "Tipos de Canales",
|
||||||
"admin.analytics.meaningful": "No hay suficiente data para tener una representación significativa.",
|
"analytics.system.postTypes": "Mesajes, Archivos y Hashtags",
|
||||||
"admin.analytics.newlyCreated": "Nuevos Usuarios Creados",
|
"analytics.system.privateGroups": "Grupos Privados",
|
||||||
"admin.analytics.postTypes": "Mesajes, Archivos y Hashtags",
|
"analytics.system.publicChannels": "Canales Públicos",
|
||||||
"admin.analytics.privateGroups": "Grupos Privados",
|
"analytics.system.textPosts": "Mensajes de sólo Texto",
|
||||||
"admin.analytics.publicChannels": "Canales Públicos",
|
"analytics.system.totalFilePosts": "Mensajes con Archivos",
|
||||||
"admin.analytics.recentActive": "Usuarios Recientemente Activos",
|
"analytics.system.totalHashtagPosts": "Mensajes con Hashtags",
|
||||||
"admin.analytics.textPosts": "Mensajes de sólo Texto",
|
"analytics.system.totalIncomingWebhooks": "Webhooks de Entrada",
|
||||||
"admin.analytics.title": "Estadísticas para {title}",
|
"analytics.system.totalOutgoingWebhooks": "Webhooks de Salida",
|
||||||
"admin.analytics.totalFilePosts": "Mensajes con Archivos",
|
"analytics.system.totalPosts": "Total de Mensajes",
|
||||||
"admin.analytics.totalHashtagPosts": "Mensajes con Hashtags",
|
"analytics.system.totalUsers": "Total de Usuarios",
|
||||||
"admin.analytics.totalIncomingWebhooks": "Webhooks de Entrada",
|
"analytics.team.activeUsers": "Usuarios Activos con Mensajes",
|
||||||
"admin.analytics.totalOutgoingWebhooks": "Webhooks de Salida",
|
"analytics.team.newlyCreated": "Nuevos Usuarios Creados",
|
||||||
"admin.analytics.totalPosts": "Total de Mensajes",
|
"analytics.team.privateGroups": "Grupos Privados",
|
||||||
"admin.analytics.totalUsers": "Total de Usuarios",
|
"analytics.team.publicChannels": "Canales Públicos",
|
||||||
|
"analytics.team.recentActive": "Usuarios Recientemente Activos",
|
||||||
|
"analytics.team.totalPosts": "Total de Mensajes",
|
||||||
|
"analytics.team.totalUsers": "Total de Usuarios",
|
||||||
"admin.audits.reload": "Recargar",
|
"admin.audits.reload": "Recargar",
|
||||||
"admin.audits.title": "Auditorías del Servidor",
|
"admin.audits.title": "Auditorías del Servidor",
|
||||||
"admin.email.allowEmailSignInDescription": "Cuando es verdadero, Mattermost permite a los usuarios iniciar sesión utilizando el correo electrónico y contraseña.",
|
"admin.email.allowEmailSignInDescription": "Cuando es verdadero, Mattermost permite a los usuarios iniciar sesión utilizando el correo electrónico y contraseña.",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user