summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin.go62
-rw-r--r--api/admin_test.go148
-rw-r--r--model/analytics_row.go55
-rw-r--r--model/analytics_row_test.go37
-rw-r--r--model/client.go9
-rw-r--r--store/sql_channel_store.go28
-rw-r--r--store/sql_channel_store_test.go18
-rw-r--r--store/sql_post_store.go102
-rw-r--r--store/sql_post_store_test.go128
-rw-r--r--store/store.go4
-rw-r--r--web/react/components/admin_console/admin_controller.jsx5
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx11
-rw-r--r--web/react/components/admin_console/team_analytics.jsx144
-rw-r--r--web/react/utils/client.jsx14
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss16
15 files changed, 779 insertions, 2 deletions
diff --git a/api/admin.go b/api/admin.go
index cd1e5d2de..89353d61d 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
-
+ sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -142,3 +142,63 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
m["SUCCESS"] = "true"
w.Write([]byte(model.MapToJson(m)))
}
+
+func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getAnalytics") {
+ return
+ }
+
+ params := mux.Vars(r)
+ teamId := params["id"]
+ name := params["name"]
+
+ if name == "standard" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
+ rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
+ rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
+ rows[2] = &model.AnalyticsRow{"post_count", 0}
+ openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
+ privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
+ postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
+
+ if r := <-openChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-privateChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-postChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+
+ w.Write([]byte(rows.ToJson()))
+ } else if name == "post_counts_day" {
+ if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
+ }
+ } else if name == "user_counts_with_posts_day" {
+ if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
+ }
+ } else {
+ c.SetInvalidParam("getAnalytics", "name")
+ }
+
+}
diff --git a/api/admin_test.go b/api/admin_test.go
index 0e51644d8..0db5caa4c 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -150,3 +150,151 @@ func TestEmailTest(t *testing.T) {
t.Fatal(err)
}
}
+
+func TestGetAnalyticsStandard(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Name != "channel_open_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[0].Value != 2 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Name != "channel_private_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[1].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Name != "post_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[2].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+}
+
+func TestGetPostCount(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+}
+
+func TestUserCountsWithPostsByDay(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
+ t.Fatal("Shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
+ t.Fatal(err)
+ } else {
+ rows := result.Data.(model.AnalyticsRows)
+
+ if rows[0].Value != 1 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+ }
+}
diff --git a/model/analytics_row.go b/model/analytics_row.go
new file mode 100644
index 000000000..ed1d69dd2
--- /dev/null
+++ b/model/analytics_row.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type AnalyticsRow struct {
+ Name string `json:"name"`
+ Value float64 `json:"value"`
+}
+
+type AnalyticsRows []*AnalyticsRow
+
+func (me *AnalyticsRow) ToJson() string {
+ b, err := json.Marshal(me)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow {
+ decoder := json.NewDecoder(data)
+ var me AnalyticsRow
+ err := decoder.Decode(&me)
+ if err == nil {
+ return &me
+ } else {
+ return nil
+ }
+}
+
+func (me AnalyticsRows) ToJson() string {
+ if b, err := json.Marshal(me); err != nil {
+ return "[]"
+ } else {
+ return string(b)
+ }
+}
+
+func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows {
+ decoder := json.NewDecoder(data)
+ var me AnalyticsRows
+ err := decoder.Decode(&me)
+ if err == nil {
+ return me
+ } else {
+ return nil
+ }
+}
diff --git a/model/analytics_row_test.go b/model/analytics_row_test.go
new file mode 100644
index 000000000..1202d5b52
--- /dev/null
+++ b/model/analytics_row_test.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestAnalyticsRowJson(t *testing.T) {
+ a1 := AnalyticsRow{}
+ a1.Name = "2015-10-12"
+ a1.Value = 12345.0
+ json := a1.ToJson()
+ ra1 := AnalyticsRowFromJson(strings.NewReader(json))
+
+ if a1.Name != ra1.Name {
+ t.Fatal("days didn't match")
+ }
+}
+
+func TestAnalyticsRowsJson(t *testing.T) {
+ a1 := AnalyticsRow{}
+ a1.Name = "2015-10-12"
+ a1.Value = 12345.0
+
+ var a1s AnalyticsRows = make([]*AnalyticsRow, 1)
+ a1s[0] = &a1
+
+ ljson := a1s.ToJson()
+ results := AnalyticsRowsFromJson(strings.NewReader(ljson))
+
+ if a1s[0].Name != results[0].Name {
+ t.Fatal("Ids do not match")
+ }
+}
diff --git a/model/client.go b/model/client.go
index 9183dcacb..6361c8e12 100644
--- a/model/client.go
+++ b/model/client.go
@@ -414,6 +414,15 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
}
}
+func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 56e190fee..4b30f646e 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -744,3 +744,31 @@ func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
return storeChannel
}
+
+func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ v, err := s.GetReplica().SelectInt(
+ `SELECT
+ COUNT(Id) AS Value
+ FROM
+ Channels
+ WHERE
+ TeamId = :TeamId
+ AND Type = :ChannelType`,
+ map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index b4e0f7593..3bfafff4b 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -460,6 +460,24 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
if list.Channels[0].Name != o3.Name {
t.Fatal("missing channel")
}
+
+ if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) != 2 {
+ t.Log(r1.Data)
+ t.Fatal("wrong value")
+ }
+ }
+
+ if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) != 2 {
+ t.Log(r1.Data)
+ t.Fatal("wrong value")
+ }
+ }
}
func TestChannelStoreGetChannelCounts(t *testing.T) {
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 6971de9d7..19a4e0adb 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -571,3 +571,105 @@ func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
return storeChannel
}
+
+func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var rows model.AnalyticsRows
+ _, err := s.GetReplica().Select(
+ &rows,
+ `SELECT
+ t1.Name, COUNT(t1.UserId) AS Value
+ FROM
+ (SELECT DISTINCT
+ DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
+ Posts.UserId
+ FROM
+ Posts, Channels
+ WHERE
+ Posts.ChannelId = Channels.Id
+ AND Channels.TeamId = :TeamId
+ ORDER BY Name DESC) AS t1
+ GROUP BY Name
+ ORDER BY Name DESC
+ LIMIT 30`,
+ map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.AnalyticsUserCountsWithPostsByDay", "We couldn't get user counts with posts", err.Error())
+ } else {
+ result.Data = rows
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var rows model.AnalyticsRows
+ _, err := s.GetReplica().Select(
+ &rows,
+ `SELECT
+ DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
+ COUNT(Posts.Id) AS Value
+ FROM
+ Posts,
+ Channels
+ WHERE
+ Posts.ChannelId = Channels.Id
+ AND Channels.TeamId = :TeamId
+ GROUP BY Name
+ ORDER BY Name DESC
+ LIMIT 30`,
+ map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCountsByDay", "We couldn't get post counts by day", err.Error())
+ } else {
+ result.Data = rows
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ v, err := s.GetReplica().SelectInt(
+ `SELECT
+ COUNT(Posts.Id) AS Value
+ FROM
+ Posts,
+ Channels
+ WHERE
+ Posts.ChannelId = Channels.Id
+ AND Channels.TeamId = :TeamId`,
+ map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index b2256417e..4e165d58a 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -580,3 +580,131 @@ func TestPostStoreSearch(t *testing.T) {
t.Fatal("returned wrong search result")
}
}
+
+func TestUserCountsWithPostsByDay(t *testing.T) {
+ Setup()
+
+ t1 := &model.Team{}
+ t1.DisplayName = "DisplayName"
+ t1.Name = "a" + model.NewId() + "b"
+ t1.Email = model.NewId() + "@nowhere.com"
+ t1.Type = model.TEAM_OPEN
+ t1 = Must(store.Team().Save(t1)).(*model.Team)
+
+ c1 := &model.Channel{}
+ c1.TeamId = t1.Id
+ c1.DisplayName = "Channel2"
+ c1.Name = "a" + model.NewId() + "b"
+ c1.Type = model.CHANNEL_OPEN
+ c1 = Must(store.Channel().Save(c1)).(*model.Channel)
+
+ o1 := &model.Post{}
+ o1.ChannelId = c1.Id
+ o1.UserId = model.NewId()
+ o1.CreateAt = model.GetMillis()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = Must(store.Post().Save(o1)).(*model.Post)
+
+ o1a := &model.Post{}
+ o1a.ChannelId = c1.Id
+ o1a.UserId = model.NewId()
+ o1a.CreateAt = o1.CreateAt
+ o1a.Message = "a" + model.NewId() + "b"
+ o1a = Must(store.Post().Save(o1a)).(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = c1.Id
+ o2.UserId = model.NewId()
+ o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
+ o2.Message = "a" + model.NewId() + "b"
+ o2 = Must(store.Post().Save(o2)).(*model.Post)
+
+ o2a := &model.Post{}
+ o2a.ChannelId = c1.Id
+ o2a.UserId = o2.UserId
+ o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
+ o2a.Message = "a" + model.NewId() + "b"
+ o2a = Must(store.Post().Save(o2a)).(*model.Post)
+
+ if r1 := <-store.Post().AnalyticsUserCountsWithPostsByDay(t1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ row1 := r1.Data.(model.AnalyticsRows)[0]
+ if row1.Value != 2 {
+ t.Fatal("wrong value")
+ }
+
+ row2 := r1.Data.(model.AnalyticsRows)[1]
+ if row2.Value != 1 {
+ t.Fatal("wrong value")
+ }
+ }
+}
+
+func TestPostCountsByDay(t *testing.T) {
+ Setup()
+
+ t1 := &model.Team{}
+ t1.DisplayName = "DisplayName"
+ t1.Name = "a" + model.NewId() + "b"
+ t1.Email = model.NewId() + "@nowhere.com"
+ t1.Type = model.TEAM_OPEN
+ t1 = Must(store.Team().Save(t1)).(*model.Team)
+
+ c1 := &model.Channel{}
+ c1.TeamId = t1.Id
+ c1.DisplayName = "Channel2"
+ c1.Name = "a" + model.NewId() + "b"
+ c1.Type = model.CHANNEL_OPEN
+ c1 = Must(store.Channel().Save(c1)).(*model.Channel)
+
+ o1 := &model.Post{}
+ o1.ChannelId = c1.Id
+ o1.UserId = model.NewId()
+ o1.CreateAt = model.GetMillis()
+ o1.Message = "a" + model.NewId() + "b"
+ o1 = Must(store.Post().Save(o1)).(*model.Post)
+
+ o1a := &model.Post{}
+ o1a.ChannelId = c1.Id
+ o1a.UserId = model.NewId()
+ o1a.CreateAt = o1.CreateAt
+ o1a.Message = "a" + model.NewId() + "b"
+ o1a = Must(store.Post().Save(o1a)).(*model.Post)
+
+ o2 := &model.Post{}
+ o2.ChannelId = c1.Id
+ o2.UserId = model.NewId()
+ o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
+ o2.Message = "a" + model.NewId() + "b"
+ o2 = Must(store.Post().Save(o2)).(*model.Post)
+
+ o2a := &model.Post{}
+ o2a.ChannelId = c1.Id
+ o2a.UserId = o2.UserId
+ o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
+ o2a.Message = "a" + model.NewId() + "b"
+ o2a = Must(store.Post().Save(o2a)).(*model.Post)
+
+ if r1 := <-store.Post().AnalyticsPostCountsByDay(t1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ row1 := r1.Data.(model.AnalyticsRows)[0]
+ if row1.Value != 2 {
+ t.Fatal("wrong value")
+ }
+
+ row2 := r1.Data.(model.AnalyticsRows)[1]
+ if row2.Value != 2 {
+ t.Fatal("wrong value")
+ }
+ }
+
+ if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) != 4 {
+ t.Fatal("wrong value")
+ }
+ }
+}
diff --git a/store/store.go b/store/store.go
index 27731cee1..e52368290 100644
--- a/store/store.go
+++ b/store/store.go
@@ -74,6 +74,7 @@ type ChannelStore interface {
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
UpdateLastViewedAt(channelId string, userId string) StoreChannel
IncrementMentionCount(channelId string, userId string) StoreChannel
+ AnalyticsTypeCount(teamId string, channelType string) StoreChannel
}
type PostStore interface {
@@ -86,6 +87,9 @@ type PostStore interface {
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
GetForExport(channelId string) StoreChannel
+ AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
+ AnalyticsPostCountsByDay(teamId string) StoreChannel
+ AnalyticsPostCount(teamId string) StoreChannel
}
type UserStore interface {
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f770d166c..d309ced2e 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
var TeamUsersTab = require('./team_users.jsx');
+var TeamAnalyticsTab = require('./team_analytics.jsx');
export default class AdminController extends React.Component {
constructor(props) {
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'team_analytics') {
+ if (this.state.teams) {
+ tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
+ }
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index b0e01ff17..c950b4629 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
+ history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
}
isSelected(name, teamId) {
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
{'- Users'}
</a>
</li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_analytics', team.id)}
+ onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
+ >
+ {'- Analytics'}
+ </a>
+ </li>
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
new file mode 100644
index 000000000..03123a3f0
--- /dev/null
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -0,0 +1,144 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = {
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null
+ };
+ }
+
+ componentDidMount() {
+ this.getData(this.props.team.id);
+ }
+
+ getData(teamId) {
+ Client.getAnalytics(
+ 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});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ this.setState({users});
+
+ // var memberList = [];
+ // for (var id in users) {
+ // if (users.hasOwnProperty(id)) {
+ // memberList.push(users[id]);
+ // }
+ // }
+
+ // memberList.sort((a, b) => {
+ // if (a.username < b.username) {
+ // return -1;
+ // }
+
+ // if (a.username > b.username) {
+ // return 1;
+ // }
+
+ // return 0;
+ // });
+ },
+ (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
+ });
+
+ this.getData(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='total-count text-center'>
+ <div>{'Total Users'}</div>
+ <div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='total-count text-center'>
+ <div>{'Public Groups'}</div>
+ <div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='total-count text-center'>
+ <div>{'Private Groups'}</div>
+ <div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='total-count text-center'>
+ <div>{'Total Posts'}</div>
+ <div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ </div>
+ );
+
+ return (
+ <div className='wrapper--fixed'>
+ <h2>{'Analytics for ' + this.props.team.name}</h2>
+ {serverError}
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index f92633439..eca4f4b3e 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -327,6 +327,20 @@ export function getConfig(success, error) {
});
}
+export function getAnalytics(teamId, name, success, error) {
+ $.ajax({
+ url: '/api/v1/admin/analytics/' + teamId + '/' + name,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAnalytics', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function saveConfig(config, success, error) {
$.ajax({
url: '/api/v1/admin/save_config',
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 14f1d9c2f..6997b0ba9 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -5,6 +5,22 @@
.table {
background: #fff;
}
+
+ .total-count {
+ width: 175px;
+ height: 100px;
+ border: 1px solid #ddd;
+ padding: 22px 10px 10px 10px;
+ margin: 10px 10px 10px 10px;
+ background: #fff;
+ float: left;
+
+ > div {
+ font-size: 18px;
+ font-weight: 300;
+ }
+ }
+
.sidebar--left {
&.sidebar--collapsable {
background: #333;