diff options
-rw-r--r-- | api/admin.go | 62 | ||||
-rw-r--r-- | api/admin_test.go | 148 | ||||
-rw-r--r-- | model/analytics_row.go | 55 | ||||
-rw-r--r-- | model/analytics_row_test.go | 37 | ||||
-rw-r--r-- | model/client.go | 9 | ||||
-rw-r--r-- | store/sql_channel_store.go | 28 | ||||
-rw-r--r-- | store/sql_channel_store_test.go | 18 | ||||
-rw-r--r-- | store/sql_post_store.go | 102 | ||||
-rw-r--r-- | store/sql_post_store_test.go | 128 | ||||
-rw-r--r-- | store/store.go | 4 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_controller.jsx | 5 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_sidebar.jsx | 11 | ||||
-rw-r--r-- | web/react/components/admin_console/team_analytics.jsx | 144 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 14 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_admin-console.scss | 16 |
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; |