Merge pull request #1956 from mattermost/plt-1779
PLT-1779 Add system-wide statistics page
Этот коммит содержится в:
12
api/admin.go
12
api/admin.go
@@ -27,6 +27,7 @@ func InitAdmin(r *mux.Router) {
|
||||
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).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")
|
||||
sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
|
||||
}
|
||||
|
||||
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -153,13 +154,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
name := params["name"]
|
||||
|
||||
if name == "standard" {
|
||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
|
||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
|
||||
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
||||
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||
rows[3] = &model.AnalyticsRow{"unique_user_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)
|
||||
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
||||
|
||||
if r := <-openChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
@@ -182,6 +185,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
rows[2].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
if r := <-userChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[3].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 {
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestEmailTest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAnalyticsStandard(t *testing.T) {
|
||||
func TestGetTeamAnalyticsStandard(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
@@ -169,7 +169,7 @@ func TestGetAnalyticsStandard(t *testing.T) {
|
||||
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 {
|
||||
if _, err := Client.GetTeamAnalytics(team.Id, "standard"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func TestGetAnalyticsStandard(t *testing.T) {
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
|
||||
if result, err := Client.GetTeamAnalytics(team.Id, "standard"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
@@ -214,6 +214,62 @@ func TestGetAnalyticsStandard(t *testing.T) {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[3].Name != "unique_user_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[3].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := Client.GetSystemAnalytics("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 == 0 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Name != "post_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Value == 0 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[3].Name != "unique_user_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[3].Value == 0 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +295,7 @@ func TestGetPostCount(t *testing.T) {
|
||||
Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
|
||||
map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
|
||||
if _, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
@@ -250,7 +306,7 @@ func TestGetPostCount(t *testing.T) {
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
|
||||
if result, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
@@ -284,7 +340,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
|
||||
map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
|
||||
if _, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
@@ -295,7 +351,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
|
||||
if result, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
@@ -5,6 +5,7 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/model"
|
||||
@@ -63,6 +64,18 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if success, licenseStr := utils.ValidateLicense(data); success {
|
||||
license = model.LicenseFromJson(strings.NewReader(licenseStr))
|
||||
|
||||
if result := <-Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil {
|
||||
c.Err = model.NewAppError("addLicense", "Unable to count total unique users.", fmt.Sprintf("err=%v", result.Err.Error()))
|
||||
return
|
||||
} else {
|
||||
uniqueUserCount := result.Data.(int64)
|
||||
|
||||
if uniqueUserCount > int64(*license.Features.Users) {
|
||||
c.Err = model.NewAppError("addLicense", fmt.Sprintf("This license only supports %d users, when your system has %d unique users. Unique users are counted distinctly by email address. You can see total user count under Site Reports -> View Statistics.", *license.Features.Users, uniqueUserCount), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ok := utils.SetLicense(license); !ok {
|
||||
c.LogAudit("failed - expired or non-started license")
|
||||
c.Err = model.NewLocAppError("addLicense", "api.license.add_license.expired.app_error", nil, "")
|
||||
|
||||
@@ -66,7 +66,9 @@ func main() {
|
||||
api.InitApi()
|
||||
web.InitWeb()
|
||||
|
||||
utils.LoadLicense()
|
||||
if model.BuildEnterpriseReady == "true" {
|
||||
utils.LoadLicense()
|
||||
}
|
||||
|
||||
if flagRunCmds {
|
||||
runCmds()
|
||||
|
||||
@@ -434,7 +434,7 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
|
||||
func (c *Client) GetTeamAnalytics(teamId, name string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
@@ -443,6 +443,15 @@ func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/admin/analytics/"+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
|
||||
|
||||
@@ -236,7 +236,6 @@ func (u *User) Sanitize(options map[string]bool) {
|
||||
}
|
||||
|
||||
func (u *User) ClearNonProfileFields() {
|
||||
u.CreateAt = 0
|
||||
u.UpdateAt = 0
|
||||
u.Password = ""
|
||||
u.AuthData = ""
|
||||
|
||||
@@ -869,15 +869,13 @@ func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) S
|
||||
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})
|
||||
query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType"
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND TeamId = :TeamId"
|
||||
}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(query, 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 {
|
||||
|
||||
@@ -805,9 +805,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt <= :EndTime
|
||||
Posts.ChannelId = Channels.Id`
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND Channels.TeamId = :TeamId"
|
||||
}
|
||||
|
||||
query += ` AND Posts.CreateAt <= :EndTime
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
@@ -824,9 +828,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt <= :EndTime
|
||||
Posts.ChannelId = Channels.Id`
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND Channels.TeamId = :TeamId"
|
||||
}
|
||||
|
||||
query += ` AND Posts.CreateAt <= :EndTime
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
@@ -869,9 +877,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt <= :EndTime
|
||||
Posts.ChannelId = Channels.Id`
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND Channels.TeamId = :TeamId"
|
||||
}
|
||||
|
||||
query += ` AND Posts.CreateAt <= :EndTime
|
||||
AND Posts.CreateAt >= :StartTime) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
@@ -888,9 +900,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt <= :EndTime
|
||||
Posts.ChannelId = Channels.Id`
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND Channels.TeamId = :TeamId"
|
||||
}
|
||||
|
||||
query += ` AND Posts.CreateAt <= :EndTime
|
||||
AND Posts.CreateAt >= :StartTime) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
@@ -924,16 +940,20 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(
|
||||
query :=
|
||||
`SELECT
|
||||
COUNT(Posts.Id) AS Value
|
||||
FROM
|
||||
Posts,
|
||||
Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId`,
|
||||
map[string]interface{}{"TeamId": teamId})
|
||||
Posts.ChannelId = Channels.Id`
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " AND Channels.TeamId = :TeamId"
|
||||
}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
|
||||
} else {
|
||||
|
||||
@@ -600,3 +600,30 @@ func (us SqlUserStore) PermanentDelete(userId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
query := "SELECT COUNT(DISTINCT Email) FROM Users"
|
||||
|
||||
if len(teamId) > 0 {
|
||||
query += " WHERE TeamId = :TeamId"
|
||||
}
|
||||
|
||||
v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserStore.AnalyticsUniqueUserCount", "We couldn't get the unique user count", err.Error())
|
||||
} else {
|
||||
result.Data = v
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ type UserStore interface {
|
||||
GetTotalActiveUsersCount() StoreChannel
|
||||
GetSystemAdminProfiles() StoreChannel
|
||||
PermanentDelete(userId string) StoreChannel
|
||||
AnalyticsUniqueUserCount(teamId string) StoreChannel
|
||||
}
|
||||
|
||||
type SessionStore interface {
|
||||
|
||||
@@ -55,6 +55,7 @@ func LoadLicense() {
|
||||
if success, licenseStr := ValidateLicense(buf.Bytes()); success {
|
||||
license := model.LicenseFromJson(strings.NewReader(licenseStr))
|
||||
SetLicense(license)
|
||||
return
|
||||
}
|
||||
|
||||
l4g.Warn("No valid enterprise license found")
|
||||
@@ -105,7 +106,7 @@ func ValidateLicense(signed []byte) (bool, string) {
|
||||
}
|
||||
|
||||
// remove null terminator
|
||||
if decoded[len(decoded)-1] == byte(0) {
|
||||
for decoded[len(decoded)-1] == byte(0) {
|
||||
decoded = decoded[:len(decoded)-1]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AdminSidebar from './admin_sidebar.jsx';
|
||||
@@ -23,6 +23,7 @@ import TeamUsersTab from './team_users.jsx';
|
||||
import TeamAnalyticsTab from './team_analytics.jsx';
|
||||
import LdapSettingsTab from './ldap_settings.jsx';
|
||||
import LicenseSettingsTab from './license_settings.jsx';
|
||||
import SystemAnalyticsTab from './system_analytics.jsx';
|
||||
|
||||
export default class AdminController extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -45,7 +46,7 @@ export default class AdminController extends React.Component {
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams,
|
||||
selected: props.tab || 'service_settings',
|
||||
selected: props.tab || 'system_analytics',
|
||||
selectedTeam: props.teamId || null
|
||||
};
|
||||
|
||||
@@ -165,6 +166,8 @@ export default class AdminController extends React.Component {
|
||||
if (this.state.teams) {
|
||||
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||
}
|
||||
} else if (this.state.selected === 'system_analytics') {
|
||||
tab = <SystemAnalyticsTab />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,25 @@ export default class AdminSidebar extends React.Component {
|
||||
<div className='nav-pills__container'>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
<li>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
<span className='icon fa fa-gear'></span>
|
||||
<span>{'SITE REPORTS'}</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu padded'>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('system_analytics')}
|
||||
onClick={this.handleClick.bind(this, 'system_analytics', null)}
|
||||
>
|
||||
{'View Statistics'}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
|
||||
279
web/react/components/admin_console/analytics.jsx
Обычный файл
279
web/react/components/admin_console/analytics.jsx
Обычный файл
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from '../../utils/utils.jsx';
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
import LineChart from './line_chart.jsx';
|
||||
|
||||
var Tooltip = ReactBootstrap.Tooltip;
|
||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||
|
||||
export default class Analytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() { // in the future, break down these into smaller components
|
||||
var serverError = '';
|
||||
if (this.props.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var totalCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
|
||||
<div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openChannelCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
|
||||
<div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openPrivateCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
|
||||
<div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
|
||||
<div className='content'>{this.props.postCount == null ? 'Loading...' : this.props.postCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCountsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Total Posts'}</div>
|
||||
<div className='content'>{'Loading...'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.postCountsDay != null) {
|
||||
let content;
|
||||
if (this.props.postCountsDay.labels.length === 0) {
|
||||
content = 'Not enough data for a meaningful representation.';
|
||||
} 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'>{'Total Posts'}</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var usersWithPostsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Active Users With Posts'}</div>
|
||||
<div className='content'>{'Loading...'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.userCountsWithPostsDay != null) {
|
||||
let content;
|
||||
if (this.props.userCountsWithPostsDay.labels.length === 0) {
|
||||
content = 'Not enough data for a meaningful representation.';
|
||||
} 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'>{'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>{Utils.displayDateTime(user.last_activity_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
recentActiveUser = (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>{'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>{Utils.displayDateTime(user.create_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
newUsers = (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>{'Newly Created Users'}</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>{'Statistics for ' + this.props.title}</h3>
|
||||
{serverError}
|
||||
<div className='row'>
|
||||
{totalCount}
|
||||
{postCount}
|
||||
{openChannelCount}
|
||||
{openPrivateCount}
|
||||
</div>
|
||||
<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 = {
|
||||
title: React.PropTypes.string,
|
||||
channelOpenCount: React.PropTypes.number,
|
||||
channelPrivateCount: React.PropTypes.number,
|
||||
postCount: 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
|
||||
};
|
||||
161
web/react/components/admin_console/system_analytics.jsx
Обычный файл
161
web/react/components/admin_console/system_analytics.jsx
Обычный файл
@@ -0,0 +1,161 @@
|
||||
// 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';
|
||||
|
||||
export default 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
|
||||
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: 'Total Posts',
|
||||
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: 'Active Users With Posts',
|
||||
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});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
title={'the System'}
|
||||
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}
|
||||
uniqueUserCount={this.state.unique_user_count}
|
||||
serverError={this.state.serverError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SystemAnalytics.propTypes = {
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
@@ -1,13 +1,8 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// 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 * as Utils from '../../utils/utils.jsx';
|
||||
import Constants from '../../utils/constants.jsx';
|
||||
import LineChart from './line_chart.jsx';
|
||||
|
||||
var Tooltip = ReactBootstrap.Tooltip;
|
||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||
|
||||
export default class TeamAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component {
|
||||
|
||||
this.getData = this.getData.bind(this);
|
||||
|
||||
this.state = {
|
||||
this.state = { // most of this state should be from a store in the future
|
||||
users: null,
|
||||
serverError: null,
|
||||
channel_open_count: null,
|
||||
@@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component {
|
||||
post_counts_day: null,
|
||||
user_counts_with_posts_day: null,
|
||||
recent_active_users: null,
|
||||
newly_created_users: null
|
||||
newly_created_users: null,
|
||||
unique_user_count: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component {
|
||||
this.getData(this.props.team.id);
|
||||
}
|
||||
|
||||
getData(teamId) {
|
||||
Client.getAnalytics(
|
||||
getData(teamId) { // should be moved to an action creator eventually
|
||||
Client.getTeamAnalytics(
|
||||
teamId,
|
||||
'standard',
|
||||
(data) => {
|
||||
@@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component {
|
||||
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) => {
|
||||
@@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component {
|
||||
}
|
||||
);
|
||||
|
||||
Client.getAnalytics(
|
||||
Client.getTeamAnalytics(
|
||||
teamId,
|
||||
'post_counts_day',
|
||||
(data) => {
|
||||
@@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component {
|
||||
}
|
||||
);
|
||||
|
||||
Client.getAnalytics(
|
||||
Client.getTeamAnalytics(
|
||||
teamId,
|
||||
'user_counts_with_posts_day',
|
||||
(data) => {
|
||||
@@ -152,6 +152,10 @@ export default class TeamAnalytics extends React.Component {
|
||||
|
||||
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;
|
||||
@@ -198,227 +202,29 @@ export default class TeamAnalytics extends React.Component {
|
||||
post_counts_day: null,
|
||||
user_counts_with_posts_day: null,
|
||||
recent_active_users: null,
|
||||
newly_created_users: null
|
||||
newly_created_users: null,
|
||||
unique_user_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='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
|
||||
<div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openChannelCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div>
|
||||
<div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openPrivateCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
|
||||
<div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCount = (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
|
||||
<div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCountsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Total Posts'}</div>
|
||||
<div className='content'>{'Loading...'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.post_counts_day != null) {
|
||||
postCountsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Total Posts'}</div>
|
||||
<div className='content'>
|
||||
<LineChart
|
||||
data={this.state.post_counts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var usersWithPostsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Total Posts'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.user_counts_with_posts_day != null) {
|
||||
usersWithPostsByDay = (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>{'Active Users With Posts'}</div>
|
||||
<div className='content'>
|
||||
<LineChart
|
||||
data={this.state.user_counts_with_posts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var recentActiveUser = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Recent Active Users'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.recent_active_users != null) {
|
||||
recentActiveUser = (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>{'Recent Active Users'}</div>
|
||||
<div className='content'>
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
this.state.recent_active_users.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>{Utils.displayDateTime(user.last_activity_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var newUsers = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Newly Created Users'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.newly_created_users != null) {
|
||||
newUsers = (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>{'Newly Created Users'}</div>
|
||||
<div className='content'>
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
this.state.newly_created_users.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>{Utils.displayDateTime(user.create_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>{'Statistics for ' + this.props.team.name}</h3>
|
||||
{serverError}
|
||||
<div className='row'>
|
||||
{totalCount}
|
||||
{postCount}
|
||||
{openChannelCount}
|
||||
{openPrivateCount}
|
||||
</div>
|
||||
<div className='row'>
|
||||
{postCountsByDay}
|
||||
</div>
|
||||
<div className='row'>
|
||||
{usersWithPostsByDay}
|
||||
</div>
|
||||
<div className='row'>
|
||||
{recentActiveUser}
|
||||
{newUsers}
|
||||
</div>
|
||||
<div>
|
||||
<Analytics
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ export function getConfig(success, error) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getAnalytics(teamId, name, success, error) {
|
||||
export function getTeamAnalytics(teamId, name, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
|
||||
dataType: 'json',
|
||||
@@ -407,7 +407,21 @@ export function getAnalytics(teamId, name, success, error) {
|
||||
type: 'GET',
|
||||
success,
|
||||
error: (xhr, status, err) => {
|
||||
var e = handleError('getAnalytics', xhr, status, err);
|
||||
var e = handleError('getTeamAnalytics', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getSystemAnalytics(name, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/analytics/' + name,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success,
|
||||
error: (xhr, status, err) => {
|
||||
var e = handleError('getSystemAnalytics', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
Ссылка в новой задаче
Block a user