Adding analytics tab
Этот коммит содержится в:
62
api/admin.go
62
api/admin.go
@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
|
|||||||
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
||||||
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
|
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
|
||||||
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
|
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) {
|
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"
|
m["SUCCESS"] = "true"
|
||||||
w.Write([]byte(model.MapToJson(m)))
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,3 +150,151 @@ func TestEmailTest(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
55
model/analytics_row.go
Обычный файл
55
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
|
||||||
|
}
|
||||||
|
}
|
||||||
37
model/analytics_row_test.go
Обычный файл
37
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
|
||||||
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -744,3 +744,31 @@ func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
|
|||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -460,6 +460,24 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
|
|||||||
if list.Channels[0].Name != o3.Name {
|
if list.Channels[0].Name != o3.Name {
|
||||||
t.Fatal("missing channel")
|
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) {
|
func TestChannelStoreGetChannelCounts(t *testing.T) {
|
||||||
|
|||||||
@@ -571,3 +571,105 @@ func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
|
|||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -580,3 +580,131 @@ func TestPostStoreSearch(t *testing.T) {
|
|||||||
t.Fatal("returned wrong search result")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type ChannelStore interface {
|
|||||||
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
||||||
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
||||||
IncrementMentionCount(channelId string, userId string) StoreChannel
|
IncrementMentionCount(channelId string, userId string) StoreChannel
|
||||||
|
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostStore interface {
|
type PostStore interface {
|
||||||
@@ -86,6 +87,9 @@ type PostStore interface {
|
|||||||
GetEtag(channelId string) StoreChannel
|
GetEtag(channelId string) StoreChannel
|
||||||
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
||||||
GetForExport(channelId string) StoreChannel
|
GetForExport(channelId string) StoreChannel
|
||||||
|
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||||
|
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||||
|
AnalyticsPostCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
|
|||||||
var TeamSettingsTab = require('./team_settings.jsx');
|
var TeamSettingsTab = require('./team_settings.jsx');
|
||||||
var ServiceSettingsTab = require('./service_settings.jsx');
|
var ServiceSettingsTab = require('./service_settings.jsx');
|
||||||
var TeamUsersTab = require('./team_users.jsx');
|
var TeamUsersTab = require('./team_users.jsx');
|
||||||
|
var TeamAnalyticsTab = require('./team_analytics.jsx');
|
||||||
|
|
||||||
export default class AdminController extends React.Component {
|
export default class AdminController extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
|
|||||||
if (this.state.teams) {
|
if (this.state.teams) {
|
||||||
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
|
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]} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
|
|||||||
handleClick(name, teamId, e) {
|
handleClick(name, teamId, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.selectTab(name, teamId);
|
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) {
|
isSelected(name, teamId) {
|
||||||
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
|
|||||||
{'- Users'}
|
{'- Users'}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
className={this.isSelected('team_analytics', team.id)}
|
||||||
|
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
|
||||||
|
>
|
||||||
|
{'- Analytics'}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
144
web/react/components/admin_console/team_analytics.jsx
Обычный файл
144
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
|
||||||
|
};
|
||||||
@@ -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) {
|
export function saveConfig(config, success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/admin/save_config',
|
url: '/api/v1/admin/save_config',
|
||||||
|
|||||||
@@ -5,6 +5,22 @@
|
|||||||
.table {
|
.table {
|
||||||
background: #fff;
|
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--left {
|
||||||
&.sidebar--collapsable {
|
&.sidebar--collapsable {
|
||||||
background: #333;
|
background: #333;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user