summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/admin.go31
-rw-r--r--api/admin_test.go50
-rw-r--r--i18n/en.json14
-rw-r--r--store/sql_command_store.go41
-rw-r--r--store/sql_command_store_test.go28
-rw-r--r--store/sql_post_store.go2
-rw-r--r--store/sql_session_store.go29
-rw-r--r--store/sql_session_store_test.go25
-rw-r--r--store/sql_team_store.go19
-rw-r--r--store/sql_team_store_test.go20
-rw-r--r--store/store.go3
-rw-r--r--web/react/components/admin_console/admin_controller.jsx4
-rw-r--r--web/react/components/admin_console/analytics.jsx489
-rw-r--r--web/react/components/admin_console/line_chart.jsx50
-rw-r--r--web/react/components/admin_console/system_analytics.jsx216
-rw-r--r--web/react/components/admin_console/team_analytics.jsx253
-rw-r--r--web/react/components/analytics/doughnut_chart.jsx (renamed from web/react/components/admin_console/doughnut_chart.jsx)4
-rw-r--r--web/react/components/analytics/line_chart.jsx90
-rw-r--r--web/react/components/analytics/statistic_count.jsx (renamed from web/react/components/admin_console/statistic_count.jsx)4
-rw-r--r--web/react/components/analytics/system_analytics.jsx346
-rw-r--r--web/react/components/analytics/table_chart.jsx60
-rw-r--r--web/react/components/analytics/team_analytics.jsx235
-rw-r--r--web/react/stores/analytics_store.jsx85
-rw-r--r--web/react/utils/async_client.jsx270
-rw-r--r--web/react/utils/client.jsx23
-rw-r--r--web/react/utils/constants.jsx20
-rw-r--r--web/static/i18n/en.json44
-rw-r--r--web/static/i18n/es.json37
28 files changed, 1416 insertions, 1076 deletions
diff --git a/api/admin.go b/api/admin.go
index d04991353..feb70aae3 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -184,16 +184,18 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
name := params["name"]
if name == "standard" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 5)
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
rows[2] = &model.AnalyticsRow{"post_count", 0}
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
+ rows[4] = &model.AnalyticsRow{"team_count", 0}
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
postChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
+ teamChan := Srv.Store.Team().AnalyticsTeamCount()
if r := <-openChan; r.Err != nil {
c.Err = r.Err
@@ -223,6 +225,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[3].Value = float64(r.Data.(int64))
}
+ if r := <-teamChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[4].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 {
@@ -239,16 +248,20 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
}
} else if name == "extra_counts" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
rows[0] = &model.AnalyticsRow{"file_post_count", 0}
rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
+ rows[4] = &model.AnalyticsRow{"command_count", 0}
+ rows[5] = &model.AnalyticsRow{"session_count", 0}
fileChan := Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
hashtagChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
+ commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId)
+ sessionChan := Srv.Store.Session().AnalyticsSessionCount(teamId)
if r := <-fileChan; r.Err != nil {
c.Err = r.Err
@@ -278,6 +291,20 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
rows[3].Value = float64(r.Data.(int64))
}
+ if r := <-commandChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[4].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-sessionChan; r.Err != nil {
+ c.Err = r.Err
+ return
+ } else {
+ rows[5].Value = float64(r.Data.(int64))
+ }
+
w.Write([]byte(rows.ToJson()))
} else {
c.SetInvalidParam("getAnalytics", "name")
diff --git a/api/admin_test.go b/api/admin_test.go
index 8a9c82b44..bdea0bc5b 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -254,6 +254,16 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
t.Log(rows.ToJson())
t.Fatal()
}
+
+ if rows[4].Name != "team_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[4].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
}
if result, err := Client.GetSystemAnalytics("standard"); err != nil {
@@ -300,6 +310,16 @@ func TestGetTeamAnalyticsStandard(t *testing.T) {
t.Log(rows.ToJson())
t.Fatal()
}
+
+ if rows[4].Name != "team_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[4].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
}
}
@@ -469,6 +489,26 @@ func TestGetTeamAnalyticsExtra(t *testing.T) {
t.Log(rows.ToJson())
t.Fatal()
}
+
+ if rows[4].Name != "command_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[4].Value != 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[5].Name != "session_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[5].Value == 0 {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
}
if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
@@ -500,5 +540,15 @@ func TestGetTeamAnalyticsExtra(t *testing.T) {
t.Log(rows.ToJson())
t.Fatal()
}
+
+ if rows[4].Name != "command_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
+
+ if rows[5].Name != "session_count" {
+ t.Log(rows.ToJson())
+ t.Fatal()
+ }
}
}
diff --git a/i18n/en.json b/i18n/en.json
index 79b6921a4..0e912f95c 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -2676,6 +2676,10 @@
"translation": "We couldn't update the command"
},
{
+ "id": "store.sql_command.analytics_command_count.app_error",
+ "translation": "We couldn't count the commands"
+ },
+ {
"id": "store.sql_license.get.app_error",
"translation": "We encountered an error getting the license"
},
@@ -2948,6 +2952,10 @@
"translation": "We couldn't update the roles"
},
{
+ "id": "store.sql_session.analytics_session_count.app_error",
+ "translation": "We couldn't count the sessions"
+ },
+ {
"id": "store.sql_system.get.app_error",
"translation": "We encountered an error finding the system properties"
},
@@ -3028,6 +3036,10 @@
"translation": "We couldn't update the team name"
},
{
+ "id": "store.sql_team.analytics_team_count.app_error",
+ "translation": "We couldn't count the teams"
+ },
+ {
"id": "store.sql_user.analytics_unique_user_count.app_error",
"translation": "We couldn't get the unique user count"
},
@@ -3575,4 +3587,4 @@
"id": "web.watcher_fail.error",
"translation": "Failed to add directory to watcher %v"
}
-] \ No newline at end of file
+]
diff --git a/store/sql_command_store.go b/store/sql_command_store.go
index 760235e10..074a6e588 100644
--- a/store/sql_command_store.go
+++ b/store/sql_command_store.go
@@ -151,18 +151,49 @@ func (s SqlCommandStore) PermanentDeleteByUser(userId string) StoreChannel {
return storeChannel
}
-func (s SqlCommandStore) Update(hook *model.Command) StoreChannel {
+func (s SqlCommandStore) Update(cmd *model.Command) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
- hook.UpdateAt = model.GetMillis()
+ cmd.UpdateAt = model.GetMillis()
- if _, err := s.GetMaster().Update(hook); err != nil {
- result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+hook.Id+", "+err.Error())
+ if _, err := s.GetMaster().Update(cmd); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.Update", "store.sql_command.save.update.app_error", nil, "id="+cmd.Id+", "+err.Error())
} else {
- result.Data = hook
+ result.Data = cmd
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlCommandStore) AnalyticsCommandCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ query :=
+ `SELECT
+ COUNT(*)
+ FROM
+ Commands
+ WHERE
+ DeleteAt = 0`
+
+ if len(teamId) > 0 {
+ query += " AND TeamId = :TeamId"
+ }
+
+ if c, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlCommandStore.AnalyticsCommandCount", "store.sql_command.analytics_command_count.app_error", nil, err.Error())
+ } else {
+ result.Data = c
}
storeChannel <- result
diff --git a/store/sql_command_store_test.go b/store/sql_command_store_test.go
index b4610d4aa..644ebc9ae 100644
--- a/store/sql_command_store_test.go
+++ b/store/sql_command_store_test.go
@@ -153,3 +153,31 @@ func TestCommandStoreUpdate(t *testing.T) {
t.Fatal(r2.Err)
}
}
+
+func TestCommandCount(t *testing.T) {
+ Setup()
+
+ o1 := &model.Command{}
+ o1.CreatorId = model.NewId()
+ o1.Method = model.COMMAND_METHOD_POST
+ o1.TeamId = model.NewId()
+ o1.URL = "http://nowhere.com/"
+
+ o1 = (<-store.Command().Save(o1)).Data.(*model.Command)
+
+ if r1 := <-store.Command().AnalyticsCommandCount(""); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) == 0 {
+ t.Fatal("should be at least 1 command")
+ }
+ }
+
+ if r2 := <-store.Command().AnalyticsCommandCount(o1.TeamId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if r2.Data.(int64) != 1 {
+ t.Fatal("should be 1 command")
+ }
+ }
+}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 6a614b6a7..3346534ab 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -947,7 +947,7 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustH
result := StoreResult{}
query :=
- `SELECT
+ `SELECT
COUNT(Posts.Id) AS Value
FROM
Posts,
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index 8dccc0770..6fe71db17 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -255,3 +255,32 @@ func (me SqlSessionStore) UpdateDeviceId(id, deviceId string) StoreChannel {
return storeChannel
}
+
+func (me SqlSessionStore) AnalyticsSessionCount(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ query :=
+ `SELECT
+ COUNT(*)
+ FROM
+ Sessions`
+
+ if len(teamId) > 0 {
+ query += " WHERE TeamId = :TeamId"
+ }
+
+ if c, err := me.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewLocAppError("SqlSessionStore.AnalyticsSessionCount", "store.sql_session.analytics_session_count.app_error", nil, err.Error())
+ } else {
+ result.Data = c
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 34d3128a6..9b430eb30 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -200,3 +200,28 @@ func TestSessionStoreUpdateLastActivityAt(t *testing.T) {
}
}
+
+func TestSessionCount(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if r1 := <-store.Session().AnalyticsSessionCount(""); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) == 0 {
+ t.Fatal("should have at least 1 session")
+ }
+ }
+
+ if r2 := <-store.Session().AnalyticsSessionCount(s1.TeamId); r2.Err != nil {
+ t.Fatal(r2.Err)
+ } else {
+ if r2.Data.(int64) != 1 {
+ t.Fatal("should have 1 session")
+ }
+ }
+}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 86ab9ac04..1893268c8 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -317,3 +317,22 @@ func (s SqlTeamStore) PermanentDelete(teamId string) StoreChannel {
return storeChannel
}
+
+func (s SqlTeamStore) AnalyticsTeamCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0", map[string]interface{}{}); err != nil {
+ result.Err = model.NewLocAppError("SqlTeamStore.AnalyticsTeamCount", "store.sql_team.analytics_team_count.app_error", nil, err.Error())
+ } else {
+ result.Data = c
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go
index 7dc31cbe2..743ef053f 100644
--- a/store/sql_team_store_test.go
+++ b/store/sql_team_store_test.go
@@ -261,3 +261,23 @@ func TestDelete(t *testing.T) {
t.Fatal(r1.Err)
}
}
+
+func TestTeamCount(t *testing.T) {
+ Setup()
+
+ o1 := model.Team{}
+ o1.DisplayName = "DisplayName"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Email = model.NewId() + "@nowhere.com"
+ o1.Type = model.TEAM_OPEN
+ o1.AllowTeamListing = true
+ Must(store.Team().Save(&o1))
+
+ if r1 := <-store.Team().AnalyticsTeamCount(); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(int64) == 0 {
+ t.Fatal("should be at least 1 team")
+ }
+ }
+}
diff --git a/store/store.go b/store/store.go
index 397601543..b041cfa25 100644
--- a/store/store.go
+++ b/store/store.go
@@ -55,6 +55,7 @@ type TeamStore interface {
GetAllTeamListing() StoreChannel
GetByInviteId(inviteId string) StoreChannel
PermanentDelete(teamId string) StoreChannel
+ AnalyticsTeamCount() StoreChannel
}
type ChannelStore interface {
@@ -141,6 +142,7 @@ type SessionStore interface {
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
UpdateDeviceId(id string, deviceId string) StoreChannel
+ AnalyticsSessionCount(teamId string) StoreChannel
}
type AuditStore interface {
@@ -196,6 +198,7 @@ type CommandStore interface {
Delete(commandId string, time int64) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
Update(hook *model.Command) StoreChannel
+ AnalyticsCommandCount(teamId string) StoreChannel
}
type PreferenceStore interface {
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index de0b085bc..32ed70a99 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -21,10 +21,10 @@ import TeamSettingsTab from './team_settings.jsx';
import ServiceSettingsTab from './service_settings.jsx';
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx';
-import TeamAnalyticsTab from './team_analytics.jsx';
+import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
-import SystemAnalyticsTab from './system_analytics.jsx';
+import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
export default class AdminController extends React.Component {
constructor(props) {
diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx
deleted file mode 100644
index ec9ad4da0..000000000
--- a/web/react/components/admin_console/analytics.jsx
+++ /dev/null
@@ -1,489 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Constants from '../../utils/constants.jsx';
-import LineChart from './line_chart.jsx';
-import DoughnutChart from './doughnut_chart.jsx';
-import StatisticCount from './statistic_count.jsx';
-
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
-
-import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl';
-
-const holders = defineMessages({
- analyticsTotalUsers: {
- id: 'admin.analytics.totalUsers',
- defaultMessage: 'Total Users'
- },
- analyticsPublicChannels: {
- id: 'admin.analytics.publicChannels',
- defaultMessage: 'Public Channels'
- },
- analyticsPrivateGroups: {
- id: 'admin.analytics.privateGroups',
- defaultMessage: 'Private Groups'
- },
- analyticsTotalPosts: {
- id: 'admin.analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- analyticsFilePosts: {
- id: 'admin.analytics.totalFilePosts',
- defaultMessage: 'Posts with Files'
- },
- analyticsHashtagPosts: {
- id: 'admin.analytics.totalHashtagPosts',
- defaultMessage: 'Posts with Hashtags'
- },
- analyticsIncomingHooks: {
- id: 'admin.analytics.totalIncomingWebhooks',
- defaultMessage: 'Incoming Webhooks'
- },
- analyticsOutgoingHooks: {
- id: 'admin.analytics.totalOutgoingWebhooks',
- defaultMessage: 'Outgoing Webhooks'
- },
- analyticsChannelTypes: {
- id: 'admin.analytics.channelTypes',
- defaultMessage: 'Channel Types'
- },
- analyticsTextPosts: {
- id: 'admin.analytics.textPosts',
- defaultMessage: 'Posts with Text-only'
- },
- analyticsPostTypes: {
- id: 'admin.analytics.postTypes',
- defaultMessage: 'Posts, Files and Hashtags'
- }
-});
-
-export default class Analytics extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- }
-
- render() { // in the future, break down these into smaller components
- const {formatMessage} = this.props.intl;
-
- var serverError = '';
- if (this.props.serverError) {
- serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
- }
-
- let loading = (
- <h5>
- <FormattedMessage
- id='admin.analytics.loading'
- defaultMessage='Loading...'
- />
- </h5>
- );
-
- let firstRow;
- let extraGraphs;
- if (this.props.showAdvanced) {
- firstRow = (
- <div className='row'>
- <StatisticCount
- title={formatMessage(holders.analyticsTotalUsers)}
- icon='fa-users'
- count={this.props.uniqueUserCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsTotalPosts)}
- icon='fa-comment'
- count={this.props.postCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsIncomingHooks)}
- icon='fa-arrow-down'
- count={this.props.incomingWebhookCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsOutgoingHooks)}
- icon='fa-arrow-up'
- count={this.props.outgoingWebhookCount}
- />
- </div>
- );
-
- const channelTypeData = [
- {
- value: this.props.channelOpenCount,
- color: '#46BFBD',
- highlight: '#5AD3D1',
- label: formatMessage(holders.analyticsPublicChannels)
- },
- {
- value: this.props.channelPrivateCount,
- color: '#FDB45C',
- highlight: '#FFC870',
- label: formatMessage(holders.analyticsPrivateGroups)
- }
- ];
-
- const postTypeData = [
- {
- value: this.props.filePostCount,
- color: '#46BFBD',
- highlight: '#5AD3D1',
- label: formatMessage(holders.analyticsFilePosts)
- },
- {
- value: this.props.filePostCount,
- color: '#F7464A',
- highlight: '#FF5A5E',
- label: formatMessage(holders.analyticsHashtagPosts)
- },
- {
- value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount,
- color: '#FDB45C',
- highlight: '#FFC870',
- label: formatMessage(holders.analyticsTextPosts)
- }
- ];
-
- extraGraphs = (
- <div className='row'>
- <DoughnutChart
- title={formatMessage(holders.analyticsChannelTypes)}
- data={channelTypeData}
- width='300'
- height='225'
- />
- <DoughnutChart
- title={formatMessage(holders.analyticsPostTypes)}
- data={postTypeData}
- width='300'
- height='225'
- />
- </div>
- );
- } else {
- firstRow = (
- <div className='row'>
- <StatisticCount
- title={formatMessage(holders.analyticsTotalUsers)}
- icon='fa-users'
- count={this.props.uniqueUserCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsPublicChannels)}
- icon='fa-globe'
- count={this.props.channelOpenCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsPrivateGroups)}
- icon='fa-lock'
- count={this.props.channelPrivateCount}
- />
- <StatisticCount
- title={formatMessage(holders.analyticsTotalPosts)}
- icon='fa-comment'
- count={this.props.postCount}
- />
- </div>
- );
- }
-
- let postCountsByDay;
- if (this.props.postCountsDay == null) {
- postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.totalPosts'
- defaultMessage='Total Posts'
- />
- </div>
- <div className='content'>{loading}</div>
- </div>
- </div>
- );
- } else {
- let content;
- if (this.props.postCountsDay.labels.length === 0) {
- content = (
- <h5>
- <FormattedMessage
- id='admin.analytics.meaningful'
- defaultMessage='Not enough data for a meaningful representation.'
- />
- </h5>
- );
- } else {
- content = (
- <LineChart
- data={this.props.postCountsDay}
- width='740'
- height='225'
- />
- );
- }
- postCountsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.totalPosts'
- defaultMessage='Total Posts'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- let usersWithPostsByDay;
- if (this.props.userCountsWithPostsDay == null) {
- usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.activeUsers'
- defaultMessage='Active Users With Posts'
- />
- </div>
- <div className='content'>{loading}</div>
- </div>
- </div>
- );
- } else {
- let content;
- if (this.props.userCountsWithPostsDay.labels.length === 0) {
- content = (
- <h5>
- <FormattedMessage
- id='admin.analytics.meaningful'
- defaultMessage='Not enough data for a meaningful representation.'
- />
- </h5>
- );
- } else {
- content = (
- <LineChart
- data={this.props.userCountsWithPostsDay}
- width='740'
- height='225'
- />
- );
- }
- usersWithPostsByDay = (
- <div className='col-sm-12'>
- <div className='total-count by-day'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.activeUsers'
- defaultMessage='Active Users With Posts'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- let recentActiveUser;
- if (this.props.recentActiveUsers != null) {
- let content;
- if (this.props.recentActiveUsers.length === 0) {
- content = loading;
- } else {
- content = (
- <table>
- <tbody>
- {
- this.props.recentActiveUsers.map((user) => {
- const tooltip = (
- <Tooltip id={'recent-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'recent-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>
- <FormattedDate
- value={user.last_activity_at}
- day='numeric'
- month='long'
- year='numeric'
- hour12={true}
- hour='2-digit'
- minute='2-digit'
- />
- </td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- );
- }
- recentActiveUser = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.recentActive'
- defaultMessage='Recent Active Users'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- let newUsers;
- if (this.props.newlyCreatedUsers != null) {
- let content;
- if (this.props.newlyCreatedUsers.length === 0) {
- content = loading;
- } else {
- content = (
- <table>
- <tbody>
- {
- this.props.newlyCreatedUsers.map((user) => {
- const tooltip = (
- <Tooltip id={'new-user-email-tooltip-' + user.id}>
- {user.email}
- </Tooltip>
- );
-
- return (
- <tr key={'new-user-table-entry-' + user.id}>
- <td>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={tooltip}
- >
- <time>
- {user.username}
- </time>
- </OverlayTrigger>
- </td>
- <td>
- <FormattedDate
- value={user.create_at}
- day='numeric'
- month='long'
- year='numeric'
- hour12={true}
- hour='2-digit'
- minute='2-digit'
- />
- </td>
- </tr>
- );
- })
- }
- </tbody>
- </table>
- );
- }
- newUsers = (
- <div className='col-sm-6'>
- <div className='total-count recent-active-users'>
- <div className='title'>
- <FormattedMessage
- id='admin.analytics.newlyCreated'
- defaultMessage='Newly Created Users'
- />
- </div>
- <div className='content'>
- {content}
- </div>
- </div>
- </div>
- );
- }
-
- return (
- <div className='wrapper--fixed team_statistics'>
- <h3>
- <FormattedMessage
- id='admin.analytics.title'
- defaultMessage='Statistics for {title}'
- values={{
- title: this.props.title
- }}
- />
- </h3>
- {serverError}
- {firstRow}
- {extraGraphs}
- <div className='row'>
- {postCountsByDay}
- </div>
- <div className='row'>
- {usersWithPostsByDay}
- </div>
- <div className='row'>
- {recentActiveUser}
- {newUsers}
- </div>
- </div>
- );
- }
-}
-
-Analytics.defaultProps = {
- title: null,
- channelOpenCount: null,
- channelPrivateCount: null,
- postCount: null,
- postCountsDay: null,
- userCountsWithPostsDay: null,
- recentActiveUsers: null,
- newlyCreatedUsers: null,
- uniqueUserCount: null,
- serverError: null
-};
-
-Analytics.propTypes = {
- intl: intlShape.isRequired,
- title: React.PropTypes.string,
- channelOpenCount: React.PropTypes.number,
- channelPrivateCount: React.PropTypes.number,
- postCount: React.PropTypes.number,
- showAdvanced: React.PropTypes.bool,
- filePostCount: React.PropTypes.number,
- hashtagPostCount: React.PropTypes.number,
- incomingWebhookCount: React.PropTypes.number,
- outgoingWebhookCount: React.PropTypes.number,
- postCountsDay: React.PropTypes.object,
- userCountsWithPostsDay: React.PropTypes.object,
- recentActiveUsers: React.PropTypes.array,
- newlyCreatedUsers: React.PropTypes.array,
- uniqueUserCount: React.PropTypes.number,
- serverError: React.PropTypes.string
-};
-
-export default injectIntl(Analytics);
diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx
deleted file mode 100644
index 7e2f95c84..000000000
--- a/web/react/components/admin_console/line_chart.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-export default class LineChart extends React.Component {
- constructor(props) {
- super(props);
-
- this.initChart = this.initChart.bind(this);
- this.chart = null;
- }
-
- componentDidMount() {
- this.initChart(this.props);
- }
-
- componentWillReceiveProps(nextProps) {
- if (this.chart) {
- this.chart.destroy();
- this.initChart(nextProps);
- }
- }
-
- componentWillUnmount() {
- if (this.chart) {
- this.chart.destroy();
- }
- }
-
- initChart(props) {
- var el = ReactDOM.findDOMNode(this);
- var ctx = el.getContext('2d');
- this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
- }
-
- render() {
- return (
- <canvas
- width={this.props.width}
- height={this.props.height}
- />
- );
- }
-}
-
-LineChart.propTypes = {
- width: React.PropTypes.string,
- height: React.PropTypes.string,
- data: React.PropTypes.object,
- options: React.PropTypes.object
-};
diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx
deleted file mode 100644
index f983db177..000000000
--- a/web/react/components/admin_console/system_analytics.jsx
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Analytics from './analytics.jsx';
-import * as Client from '../../utils/client.jsx';
-
-import {injectIntl, intlShape, defineMessages} from 'mm-intl';
-
-const labels = defineMessages({
- totalPosts: {
- id: 'admin.system_analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- activeUsers: {
- id: 'admin.system_analytics.activeUsers',
- defaultMessage: 'Active Users With Posts'
- },
- title: {
- id: 'admin.system_analytics.title',
- defaultMessage: 'the System'
- }
-});
-
-class SystemAnalytics extends React.Component {
- constructor(props) {
- super(props);
-
- this.getData = this.getData.bind(this);
-
- this.state = { // most of this state should be from a store in the future
- users: null,
- serverError: null,
- channel_open_count: null,
- channel_private_count: null,
- post_count: null,
- post_counts_day: null,
- user_counts_with_posts_day: null,
- recent_active_users: null,
- newly_created_users: null,
- unique_user_count: null
- };
- }
-
- componentDidMount() {
- this.getData();
- }
-
- getData() { // should be moved to an action creator eventually
- const {formatMessage} = this.props.intl;
- Client.getSystemAnalytics(
- 'standard',
- (data) => {
- for (var index in data) {
- if (data[index].name === 'channel_open_count') {
- this.setState({channel_open_count: data[index].value});
- }
-
- if (data[index].name === 'channel_private_count') {
- this.setState({channel_private_count: data[index].value});
- }
-
- if (data[index].name === 'post_count') {
- this.setState({post_count: data[index].value});
- }
-
- if (data[index].name === 'unique_user_count') {
- this.setState({unique_user_count: data[index].value});
- }
- }
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- Client.getSystemAnalytics(
- 'post_counts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.totalPosts),
- fillColor: 'rgba(151,187,205,0.2)',
- strokeColor: 'rgba(151,187,205,1)',
- pointColor: 'rgba(151,187,205,1)',
- pointStrokeColor: '#fff',
- pointHighlightFill: '#fff',
- pointHighlightStroke: 'rgba(151,187,205,1)',
- data: []
- }]
- };
-
- for (var index in data) {
- if (data[index]) {
- var row = data[index];
- chartData.labels.push(row.name);
- chartData.datasets[0].data.push(row.value);
- }
- }
-
- this.setState({post_counts_day: chartData});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- Client.getSystemAnalytics(
- 'user_counts_with_posts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.activeUsers),
- fillColor: 'rgba(151,187,205,0.2)',
- strokeColor: 'rgba(151,187,205,1)',
- pointColor: 'rgba(151,187,205,1)',
- pointStrokeColor: '#fff',
- pointHighlightFill: '#fff',
- pointHighlightStroke: 'rgba(151,187,205,1)',
- data: []
- }]
- };
-
- for (var index in data) {
- if (data[index]) {
- var row = data[index];
- chartData.labels.push(row.name);
- chartData.datasets[0].data.push(row.value);
- }
- }
-
- this.setState({user_counts_with_posts_day: chartData});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- if (global.window.mm_license.IsLicensed === 'true') {
- Client.getSystemAnalytics(
- 'extra_counts',
- (data) => {
- for (var index in data) {
- if (data[index].name === 'file_post_count') {
- this.setState({file_post_count: data[index].value});
- }
-
- if (data[index].name === 'hashtag_post_count') {
- this.setState({hashtag_post_count: data[index].value});
- }
-
- if (data[index].name === 'incoming_webhook_count') {
- this.setState({incoming_webhook_count: data[index].value});
- }
-
- if (data[index].name === 'outgoing_webhook_count') {
- this.setState({outgoing_webhook_count: data[index].value});
- }
- }
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
- }
- }
-
- componentWillReceiveProps() {
- this.setState({
- serverError: null,
- channel_open_count: null,
- channel_private_count: null,
- post_count: null,
- post_counts_day: null,
- user_counts_with_posts_day: null,
- unique_user_count: null
- });
-
- this.getData();
- }
-
- render() {
- return (
- <div>
- <Analytics
- intl={this.props.intl}
- title={this.props.intl.formatMessage(labels.title)}
- channelOpenCount={this.state.channel_open_count}
- channelPrivateCount={this.state.channel_private_count}
- postCount={this.state.post_count}
- showAdvanced={global.window.mm_license.IsLicensed === 'true'}
- filePostCount={this.state.file_post_count}
- hashtagPostCount={this.state.hashtag_post_count}
- incomingWebhookCount={this.state.incoming_webhook_count}
- outgoingWebhookCount={this.state.outgoing_webhook_count}
- postCountsDay={this.state.post_counts_day}
- userCountsWithPostsDay={this.state.user_counts_with_posts_day}
- uniqueUserCount={this.state.unique_user_count}
- serverError={this.state.serverError}
- />
- </div>
- );
- }
-}
-
-SystemAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
-};
-
-export default injectIntl(SystemAnalytics);
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
deleted file mode 100644
index 808d8046d..000000000
--- a/web/react/components/admin_console/team_analytics.jsx
+++ /dev/null
@@ -1,253 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import Analytics from './analytics.jsx';
-import * as Client from '../../utils/client.jsx';
-
-import {injectIntl, intlShape, defineMessages} from 'mm-intl';
-
-const labels = defineMessages({
- totalPosts: {
- id: 'admin.team_analytics.totalPosts',
- defaultMessage: 'Total Posts'
- },
- activeUsers: {
- id: 'admin.team_analytics.activeUsers',
- defaultMessage: 'Active Users With Posts'
- }
-});
-
-class TeamAnalytics extends React.Component {
- constructor(props) {
- super(props);
-
- this.getData = this.getData.bind(this);
-
- this.state = { // most of this state should be from a store in the future
- users: null,
- serverError: null,
- channel_open_count: null,
- channel_private_count: null,
- post_count: null,
- post_counts_day: null,
- user_counts_with_posts_day: null,
- recent_active_users: null,
- newly_created_users: null,
- unique_user_count: null
- };
- }
-
- componentDidMount() {
- this.getData(this.props.team.id);
- }
-
- getData(teamId) { // should be moved to an action creator eventually
- const {formatMessage} = this.props.intl;
- Client.getTeamAnalytics(
- teamId,
- 'standard',
- (data) => {
- for (var index in data) {
- if (data[index].name === 'channel_open_count') {
- this.setState({channel_open_count: data[index].value});
- }
-
- if (data[index].name === 'channel_private_count') {
- this.setState({channel_private_count: data[index].value});
- }
-
- if (data[index].name === 'post_count') {
- this.setState({post_count: data[index].value});
- }
-
- if (data[index].name === 'unique_user_count') {
- this.setState({unique_user_count: data[index].value});
- }
- }
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- Client.getTeamAnalytics(
- teamId,
- 'post_counts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.totalPosts),
- fillColor: 'rgba(151,187,205,0.2)',
- strokeColor: 'rgba(151,187,205,1)',
- pointColor: 'rgba(151,187,205,1)',
- pointStrokeColor: '#fff',
- pointHighlightFill: '#fff',
- pointHighlightStroke: 'rgba(151,187,205,1)',
- data: []
- }]
- };
-
- for (var index in data) {
- if (data[index]) {
- var row = data[index];
- chartData.labels.push(row.name);
- chartData.datasets[0].data.push(row.value);
- }
- }
-
- this.setState({post_counts_day: chartData});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- Client.getTeamAnalytics(
- teamId,
- 'user_counts_with_posts_day',
- (data) => {
- data.reverse();
-
- var chartData = {
- labels: [],
- datasets: [{
- label: formatMessage(labels.activeUsers),
- fillColor: 'rgba(151,187,205,0.2)',
- strokeColor: 'rgba(151,187,205,1)',
- pointColor: 'rgba(151,187,205,1)',
- pointStrokeColor: '#fff',
- pointHighlightFill: '#fff',
- pointHighlightStroke: 'rgba(151,187,205,1)',
- data: []
- }]
- };
-
- for (var index in data) {
- if (data[index]) {
- var row = data[index];
- chartData.labels.push(row.name);
- chartData.datasets[0].data.push(row.value);
- }
- }
-
- this.setState({user_counts_with_posts_day: chartData});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
-
- Client.getProfilesForTeam(
- teamId,
- (users) => {
- this.setState({users});
-
- var usersList = [];
- for (var id in users) {
- if (users.hasOwnProperty(id)) {
- usersList.push(users[id]);
- }
- }
-
- usersList.sort((a, b) => {
- if (a.last_activity_at < b.last_activity_at) {
- return 1;
- }
-
- if (a.last_activity_at > b.last_activity_at) {
- return -1;
- }
-
- return 0;
- });
-
- var recentActive = [];
- for (let i = 0; i < usersList.length; i++) {
- if (usersList[i].last_activity_at == null) {
- continue;
- }
-
- recentActive.push(usersList[i]);
- if (i > 19) {
- break;
- }
- }
-
- this.setState({recent_active_users: recentActive});
-
- usersList.sort((a, b) => {
- if (a.create_at < b.create_at) {
- return 1;
- }
-
- if (a.create_at > b.create_at) {
- return -1;
- }
-
- return 0;
- });
-
- var newlyCreated = [];
- for (let i = 0; i < usersList.length; i++) {
- newlyCreated.push(usersList[i]);
- if (i > 19) {
- break;
- }
- }
-
- this.setState({newly_created_users: newlyCreated});
- },
- (err) => {
- this.setState({serverError: err.message});
- }
- );
- }
-
- componentWillReceiveProps(newProps) {
- this.setState({
- users: null,
- serverError: null,
- channel_open_count: null,
- channel_private_count: null,
- post_count: null,
- post_counts_day: null,
- user_counts_with_posts_day: null,
- recent_active_users: null,
- newly_created_users: null,
- unique_user_count: null
- });
-
- this.getData(newProps.team.id);
- }
-
- render() {
- return (
- <div>
- <Analytics
- intl={this.props.intl}
- title={this.props.team.name}
- users={this.state.users}
- channelOpenCount={this.state.channel_open_count}
- channelPrivateCount={this.state.channel_private_count}
- postCount={this.state.post_count}
- postCountsDay={this.state.post_counts_day}
- userCountsWithPostsDay={this.state.user_counts_with_posts_day}
- recentActiveUsers={this.state.recent_active_users}
- newlyCreatedUsers={this.state.newly_created_users}
- uniqueUserCount={this.state.unique_user_count}
- serverError={this.state.serverError}
- />
- </div>
- );
- }
-}
-
-TeamAnalytics.propTypes = {
- intl: intlShape.isRequired,
- team: React.PropTypes.object
-};
-
-export default injectIntl(TeamAnalytics);
diff --git a/web/react/components/admin_console/doughnut_chart.jsx b/web/react/components/analytics/doughnut_chart.jsx
index e2dc01528..00bb66f0a 100644
--- a/web/react/components/admin_console/doughnut_chart.jsx
+++ b/web/react/components/analytics/doughnut_chart.jsx
@@ -39,7 +39,7 @@ export default class DoughnutChart extends React.Component {
if (this.props.data == null) {
content = (
<FormattedMessage
- id='admin.analytics.loading'
+ id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -69,7 +69,7 @@ export default class DoughnutChart extends React.Component {
}
DoughnutChart.propTypes = {
- title: React.PropTypes.string,
+ title: React.PropTypes.node,
width: React.PropTypes.string,
height: React.PropTypes.string,
data: React.PropTypes.array,
diff --git a/web/react/components/analytics/line_chart.jsx b/web/react/components/analytics/line_chart.jsx
new file mode 100644
index 000000000..d1bb6b9cb
--- /dev/null
+++ b/web/react/components/analytics/line_chart.jsx
@@ -0,0 +1,90 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class LineChart extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.initChart = this.initChart.bind(this);
+ this.chart = null;
+ }
+
+ componentDidMount() {
+ this.initChart();
+ }
+
+ componentDidUpdate() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ this.initChart();
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ initChart() {
+ if (!this.refs.canvas) {
+ return;
+ }
+ var el = ReactDOM.findDOMNode(this.refs.canvas);
+ var ctx = el.getContext('2d');
+ this.chart = new Chart(ctx).Line(this.props.data, this.props.options || {}); //eslint-disable-line new-cap
+ }
+
+ render() {
+ let content;
+ if (this.props.data == null) {
+ content = (
+ <FormattedMessage
+ id='analytics.chart.loading'
+ defaultMessage='Loading...'
+ />
+ );
+ } else if (this.props.data.labels.length === 0) {
+ content = (
+ <h5>
+ <FormattedMessage
+ id='analytics.chart.meaningful'
+ defaultMessage='Not enough data for a meaningful representation.'
+ />
+ </h5>
+ );
+ } else {
+ content = (
+ <canvas
+ ref='canvas'
+ width={this.props.width}
+ height={this.props.height}
+ />
+ );
+ }
+
+ return (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>
+ {this.props.title}
+ </div>
+ <div className='content'>
+ {content}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+LineChart.propTypes = {
+ title: React.PropTypes.node.isRequired,
+ width: React.PropTypes.string.isRequired,
+ height: React.PropTypes.string.isRequired,
+ data: React.PropTypes.object,
+ options: React.PropTypes.object
+};
+
diff --git a/web/react/components/admin_console/statistic_count.jsx b/web/react/components/analytics/statistic_count.jsx
index 118a0ad31..cf457310f 100644
--- a/web/react/components/admin_console/statistic_count.jsx
+++ b/web/react/components/analytics/statistic_count.jsx
@@ -7,7 +7,7 @@ export default class StatisticCount extends React.Component {
render() {
let loading = (
<FormattedMessage
- id='admin.analytics.loading'
+ id='analytics.chart.loading'
defaultMessage='Loading...'
/>
);
@@ -27,7 +27,7 @@ export default class StatisticCount extends React.Component {
}
StatisticCount.propTypes = {
- title: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
icon: React.PropTypes.string.isRequired,
count: React.PropTypes.number
};
diff --git a/web/react/components/analytics/system_analytics.jsx b/web/react/components/analytics/system_analytics.jsx
new file mode 100644
index 000000000..a2b783a79
--- /dev/null
+++ b/web/react/components/analytics/system_analytics.jsx
@@ -0,0 +1,346 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LineChart from './line_chart.jsx';
+import DoughnutChart from './doughnut_chart.jsx';
+import StatisticCount from './statistic_count.jsx';
+
+import AnalyticsStore from '../../stores/analytics_store.jsx';
+
+import * as Utils from '../../utils/utils.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import Constants from '../../utils/constants.jsx';
+const StatTypes = Constants.StatTypes;
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ analyticsPublicChannels: {
+ id: 'analytics.system.publicChannels',
+ defaultMessage: 'Public Channels'
+ },
+ analyticsPrivateGroups: {
+ id: 'analytics.system.privateGroups',
+ defaultMessage: 'Private Groups'
+ },
+ analyticsFilePosts: {
+ id: 'analytics.system.totalFilePosts',
+ defaultMessage: 'Posts with Files'
+ },
+ analyticsHashtagPosts: {
+ id: 'analytics.system.totalHashtagPosts',
+ defaultMessage: 'Posts with Hashtags'
+ },
+ analyticsTextPosts: {
+ id: 'analytics.system.textPosts',
+ defaultMessage: 'Posts with Text-only'
+ }
+});
+
+class SystemAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+
+ this.state = {stats: AnalyticsStore.getAllSystem()};
+ }
+
+ componentDidMount() {
+ AnalyticsStore.addChangeListener(this.onChange);
+
+ AsyncClient.getStandardAnalytics();
+ AsyncClient.getPostsPerDayAnalytics();
+ AsyncClient.getUsersPerDayAnalytics();
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ AsyncClient.getAdvancedAnalytics();
+ }
+ }
+
+ componentWillUnmount() {
+ AnalyticsStore.removeChangeListener(this.onChange);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ onChange() {
+ this.setState({stats: AnalyticsStore.getAllSystem()});
+ }
+
+ render() {
+ const stats = this.state.stats;
+
+ let advancedCounts;
+ let advancedGraphs;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ advancedCounts = (
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalSessions'
+ defaultMessage='Total Sessions'
+ />
+ }
+ icon='fa-signal'
+ count={stats[StatTypes.TOTAL_SESSIONS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalCommands'
+ defaultMessage='Total Commands'
+ />
+ }
+ icon='fa-terminal'
+ count={stats[StatTypes.TOTAL_COMMANDS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalIncomingWebhooks'
+ defaultMessage='Incoming Webhooks'
+ />
+ }
+ icon='fa-arrow-down'
+ count={stats[StatTypes.TOTAL_IHOOKS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalOutgoingWebhooks'
+ defaultMessage='Outgoing Webhooks'
+ />
+ }
+ icon='fa-arrow-up'
+ count={stats[StatTypes.TOTAL_OHOOKS]}
+ />
+ </div>
+ );
+
+ const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl);
+ const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl);
+
+ advancedGraphs = (
+ <div className='row'>
+ <DoughnutChart
+ title={
+ <FormattedMessage
+ id='analytics.system.channelTypes'
+ defaultMessage='Channel Types'
+ />
+ }
+ data={channelTypeData}
+ width='300'
+ height='225'
+ />
+ <DoughnutChart
+ title={
+ <FormattedMessage
+ id='analytics.system.postTypes'
+ defaultMessage='Posts, Files and Hashtags'
+ />
+ }
+ data={postTypeData}
+ width='300'
+ height='225'
+ />
+ </div>
+ );
+ }
+
+ const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
+ const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>
+ <FormattedMessage
+ id='analytics.system.title'
+ defaultMessage='System Statistics'
+ />
+ </h3>
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalUsers'
+ defaultMessage='Total Users'
+ />
+ }
+ icon='fa-user'
+ count={stats[StatTypes.TOTAL_USERS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalTeams'
+ defaultMessage='Total Teams'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.TOTAL_TEAMS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ icon='fa-comment'
+ count={stats[StatTypes.TOTAL_POSTS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.totalChannels'
+ defaultMessage='Total Channels'
+ />
+ }
+ icon='fa-globe'
+ count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS] + stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
+ />
+ </div>
+ {advancedCounts}
+ {advancedGraphs}
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.system.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ data={postCountsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.system.activeUsers'
+ defaultMessage='Active Users With Posts'
+ />
+ }
+ data={userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+SystemAnalytics.propTypes = {
+ intl: intlShape.isRequired,
+ team: React.PropTypes.object
+};
+
+export default injectIntl(SystemAnalytics);
+
+export function formatChannelDoughtnutData(totalPublic, totalPrivate, intl) {
+ const {formatMessage} = intl;
+ const channelTypeData = [
+ {
+ value: totalPublic,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsPublicChannels)
+ },
+ {
+ value: totalPrivate,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsPrivateGroups)
+ }
+ ];
+
+ return channelTypeData;
+}
+
+export function formatPostDoughtnutData(filePosts, hashtagPosts, totalPosts, intl) {
+ const {formatMessage} = intl;
+ const postTypeData = [
+ {
+ value: filePosts,
+ color: '#46BFBD',
+ highlight: '#5AD3D1',
+ label: formatMessage(holders.analyticsFilePosts)
+ },
+ {
+ value: hashtagPosts,
+ color: '#F7464A',
+ highlight: '#FF5A5E',
+ label: formatMessage(holders.analyticsHashtagPosts)
+ },
+ {
+ value: totalPosts - filePosts - hashtagPosts,
+ color: '#FDB45C',
+ highlight: '#FFC870',
+ label: formatMessage(holders.analyticsTextPosts)
+ }
+ ];
+
+ return postTypeData;
+}
+
+export function formatPostsPerDayData(data) {
+ var chartData = {
+ labels: [],
+ datasets: [{
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ return chartData;
+}
+
+export function formatUsersWithPostsPerDayData(data) {
+ var chartData = {
+ labels: [],
+ datasets: [{
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ return chartData;
+}
diff --git a/web/react/components/analytics/table_chart.jsx b/web/react/components/analytics/table_chart.jsx
new file mode 100644
index 000000000..c94fa300b
--- /dev/null
+++ b/web/react/components/analytics/table_chart.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Constants from '../../utils/constants.jsx';
+
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class TableChart extends React.Component {
+ render() {
+ return (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>
+ {this.props.title}
+ </div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.props.data.map((item) => {
+ const tooltip = (
+ <Tooltip id={'tip-table-entry-' + item.name}>
+ {item.tip}
+ </Tooltip>
+ );
+
+ return (
+ <tr key={'table-entry-' + item.name}>
+ <td>
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time>
+ {item.name}
+ </time>
+ </OverlayTrigger>
+ </td>
+ <td>
+ {item.value}
+ </td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+TableChart.propTypes = {
+ title: React.PropTypes.node,
+ data: React.PropTypes.array
+};
diff --git a/web/react/components/analytics/team_analytics.jsx b/web/react/components/analytics/team_analytics.jsx
new file mode 100644
index 000000000..1236c070b
--- /dev/null
+++ b/web/react/components/analytics/team_analytics.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LineChart from './line_chart.jsx';
+import StatisticCount from './statistic_count.jsx';
+import TableChart from './table_chart.jsx';
+
+import AnalyticsStore from '../../stores/analytics_store.jsx';
+
+import * as Utils from '../../utils/utils.jsx';
+import * as AsyncClient from '../../utils/async_client.jsx';
+import Constants from '../../utils/constants.jsx';
+const StatTypes = Constants.StatTypes;
+
+import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
+import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'mm-intl';
+
+class TeamAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+
+ this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
+ }
+
+ componentDidMount() {
+ AnalyticsStore.addChangeListener(this.onChange);
+
+ this.getData(this.props.team.id);
+ }
+
+ getData(id) {
+ AsyncClient.getStandardAnalytics(id);
+ AsyncClient.getPostsPerDayAnalytics(id);
+ AsyncClient.getUsersPerDayAnalytics(id);
+ AsyncClient.getRecentAndNewUsersAnalytics(id);
+ }
+
+ componentWillUnmount() {
+ AnalyticsStore.removeChangeListener(this.onChange);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.getData(nextProps.team.id);
+ this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ onChange() {
+ this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
+ }
+
+ render() {
+ const stats = this.state.stats;
+ const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
+ const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
+ const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
+ const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>
+ <FormattedMessage
+ id='analytics.team.title'
+ defaultMessage='Team Statistics for {team}'
+ values={{
+ team: this.props.team.name
+ }}
+ />
+ </h3>
+ <div className='row'>
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.totalUsers'
+ defaultMessage='Total Users'
+ />
+ }
+ icon='fa-user'
+ count={stats[StatTypes.TOTAL_USERS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.publicChannels'
+ defaultMessage='Public Channels'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.privateGroups'
+ defaultMessage='Private Groups'
+ />
+ }
+ icon='fa-globe'
+ count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
+ />
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.team.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ icon='fa-comment'
+ count={stats[StatTypes.TOTAL_POSTS]}
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.team.totalPosts'
+ defaultMessage='Total Posts'
+ />
+ }
+ data={postCountsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <LineChart
+ title={
+ <FormattedMessage
+ id='analytics.team.activeUsers'
+ defaultMessage='Active Users With Posts'
+ />
+ }
+ data={userCountsWithPostsDay}
+ width='740'
+ height='225'
+ />
+ </div>
+ <div className='row'>
+ <TableChart
+ title={
+ <FormattedMessage
+ id='analytics.team.activeUsers'
+ defaultMessage='Recent Active Users'
+ />
+ }
+ data={recentActiveUsers}
+ />
+ <TableChart
+ title={
+ <FormattedMessage
+ id='analytics.team.newlyCreated'
+ defaultMessage='Newly Created Users'
+ />
+ }
+ data={newlyCreatedUsers}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+TeamAnalytics.propTypes = {
+ intl: intlShape.isRequired,
+ team: React.PropTypes.object.isRequired
+};
+
+export default injectIntl(TeamAnalytics);
+
+export function formatRecentUsersData(data) {
+ if (data == null) {
+ return [];
+ }
+
+ const formattedData = data.map((user) => {
+ const item = {};
+ item.name = user.username;
+ item.value = (
+ <FormattedDate
+ value={user.last_activity_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ );
+ item.tip = user.email;
+
+ return item;
+ });
+
+ return formattedData;
+}
+
+export function formatNewUsersData(data) {
+ if (data == null) {
+ return [];
+ }
+
+ const formattedData = data.map((user) => {
+ const item = {};
+ item.name = user.username;
+ item.value = (
+ <FormattedDate
+ value={user.create_at}
+ day='numeric'
+ month='long'
+ year='numeric'
+ hour12={true}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ );
+ item.tip = user.email;
+
+ return item;
+ });
+
+ return formattedData;
+}
diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx
new file mode 100644
index 000000000..ec827f6d7
--- /dev/null
+++ b/web/react/stores/analytics_store.jsx
@@ -0,0 +1,85 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+
+import Constants from '../utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class AnalyticsStoreClass extends EventEmitter {
+ constructor() {
+ super();
+ this.systemStats = {};
+ this.teamStats = {};
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ getAllSystem() {
+ return JSON.parse(JSON.stringify(this.systemStats));
+ }
+
+ getAllTeam(id) {
+ if (id in this.teamStats) {
+ return JSON.parse(JSON.stringify(this.teamStats[id]));
+ }
+
+ return {};
+ }
+
+ storeSystemStats(newStats) {
+ for (const stat in newStats) {
+ if (!newStats.hasOwnProperty(stat)) {
+ continue;
+ }
+ this.systemStats[stat] = newStats[stat];
+ }
+ }
+
+ storeTeamStats(id, newStats) {
+ if (!(id in this.teamStats)) {
+ this.teamStats[id] = {};
+ }
+
+ for (const stat in newStats) {
+ if (!newStats.hasOwnProperty(stat)) {
+ continue;
+ }
+ this.teamStats[id][stat] = newStats[stat];
+ }
+ }
+
+}
+
+var AnalyticsStore = new AnalyticsStoreClass();
+
+AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_ANALYTICS:
+ if (action.teamId == null) {
+ AnalyticsStore.storeSystemStats(action.stats);
+ } else {
+ AnalyticsStore.storeTeamStats(action.teamId, action.stats);
+ }
+ AnalyticsStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default AnalyticsStore;
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ca9d81865..7d5e1bd0f 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -11,16 +11,17 @@ import UserStore from '../stores/user_store.jsx';
import * as utils from './utils.jsx';
import Constants from './constants.jsx';
-var ActionTypes = Constants.ActionTypes;
+const ActionTypes = Constants.ActionTypes;
+const StatTypes = Constants.StatTypes;
// Used to track in progress async calls
-var callTracker = {};
+const callTracker = {};
export function dispatchError(err, method) {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_ERROR,
- err: err,
- method: method
+ err,
+ method
});
}
@@ -848,3 +849,264 @@ export function getFileInfo(filename) {
}
);
}
+
+export function getStandardAnalytics(teamId) {
+ const callName = 'getStandardAnaytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'standard',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'channel_open_count') {
+ stats[StatTypes.TOTAL_PUBLIC_CHANNELS] = data[index].value;
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ stats[StatTypes.TOTAL_PRIVATE_GROUPS] = data[index].value;
+ }
+
+ if (data[index].name === 'post_count') {
+ stats[StatTypes.TOTAL_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'unique_user_count') {
+ stats[StatTypes.TOTAL_USERS] = data[index].value;
+ }
+
+ if (data[index].name === 'team_count' && teamId == null) {
+ stats[StatTypes.TOTAL_TEAMS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getStandardAnalytics');
+ }
+ );
+}
+
+export function getAdvancedAnalytics(teamId) {
+ const callName = 'getAdvancedAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'extra_counts',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ const stats = {};
+
+ for (const index in data) {
+ if (data[index].name === 'file_post_count') {
+ stats[StatTypes.TOTAL_FILE_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'hashtag_post_count') {
+ stats[StatTypes.TOTAL_HASHTAG_POSTS] = data[index].value;
+ }
+
+ if (data[index].name === 'incoming_webhook_count') {
+ stats[StatTypes.TOTAL_IHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'outgoing_webhook_count') {
+ stats[StatTypes.TOTAL_OHOOKS] = data[index].value;
+ }
+
+ if (data[index].name === 'command_count') {
+ stats[StatTypes.TOTAL_COMMANDS] = data[index].value;
+ }
+
+ if (data[index].name === 'session_count') {
+ stats[StatTypes.TOTAL_SESSIONS] = data[index].value;
+ }
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getAdvancedAnalytics');
+ }
+ );
+}
+
+export function getPostsPerDayAnalytics(teamId) {
+ const callName = 'getPostsPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'post_counts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.POST_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getPostsPerDayAnalytics');
+ }
+ );
+}
+
+export function getUsersPerDayAnalytics(teamId) {
+ const callName = 'getUsersPerDayAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getAnalytics(
+ 'user_counts_with_posts_day',
+ teamId,
+ (data) => {
+ callTracker[callName] = 0;
+
+ data.reverse();
+
+ const stats = {};
+ stats[StatTypes.USERS_WITH_POSTS_PER_DAY] = data;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getUsersPerDayAnalytics');
+ }
+ );
+}
+
+export function getRecentAndNewUsersAnalytics(teamId) {
+ const callName = 'getRecentAndNewUsersAnalytics' + teamId;
+
+ if (isCallInProgress(callName)) {
+ return;
+ }
+
+ callTracker[callName] = utils.getTimestamp();
+
+ client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ const stats = {};
+
+ const usersList = [];
+ for (const id in users) {
+ if (users.hasOwnProperty(id)) {
+ usersList.push(users[id]);
+ }
+ }
+
+ usersList.sort((a, b) => {
+ if (a.last_activity_at < b.last_activity_at) {
+ return 1;
+ }
+
+ if (a.last_activity_at > b.last_activity_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ const recentActive = [];
+ for (let i = 0; i < usersList.length; i++) {
+ if (usersList[i].last_activity_at == null) {
+ continue;
+ }
+
+ recentActive.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_ACTIVE_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.RECENTLY_ACTIVE_USERS] = recentActive;
+
+ usersList.sort((a, b) => {
+ if (a.create_at < b.create_at) {
+ return 1;
+ }
+
+ if (a.create_at > b.create_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var newlyCreated = [];
+ for (let i = 0; i < usersList.length; i++) {
+ newlyCreated.push(usersList[i]);
+ if (i >= Constants.STAT_MAX_NEW_USERS) {
+ break;
+ }
+ }
+
+ stats[StatTypes.NEWLY_CREATED_USERS] = newlyCreated;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_ANALYTICS,
+ teamId,
+ stats
+ });
+ },
+ (err) => {
+ callTracker[callName] = 0;
+
+ dispatchError(err, 'getRecentAndNewUsersAnalytics');
+ }
+ );
+}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index f647e2296..1a002bc8c 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -435,23 +435,16 @@ export function getConfig(success, error) {
});
}
-export function getTeamAnalytics(teamId, name, success, error) {
- $.ajax({
- url: '/api/v1/admin/analytics/' + teamId + '/' + name,
- dataType: 'json',
- contentType: 'application/json',
- type: 'GET',
- success,
- error: (xhr, status, err) => {
- var e = handleError('getTeamAnalytics', xhr, status, err);
- error(e);
- }
- });
-}
+export function getAnalytics(name, teamId, success, error) {
+ let url = '/api/v1/admin/analytics/';
+ if (teamId == null) {
+ url += name;
+ } else {
+ url += teamId + '/' + name;
+ }
-export function getSystemAnalytics(name, success, error) {
$.ajax({
- url: '/api/v1/admin/analytics/' + name,
+ url,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0a4944708..88f01a475 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -71,6 +71,26 @@ export default {
VIEW_ACTION: null
}),
+ StatTypes: keyMirror({
+ TOTAL_USERS: null,
+ TOTAL_PUBLIC_CHANNELS: null,
+ TOTAL_PRIVATE_GROUPS: null,
+ TOTAL_POSTS: null,
+ TOTAL_TEAMS: null,
+ TOTAL_FILE_POSTS: null,
+ TOTAL_HASHTAG_POSTS: null,
+ TOTAL_IHOOKS: null,
+ TOTAL_OHOOKS: null,
+ TOTAL_COMMANDS: null,
+ TOTAL_SESSIONS: null,
+ POST_PER_DAY: null,
+ USERS_WITH_POSTS_PER_DAY: null,
+ RECENTLY_ACTIVE_USERS: null,
+ NEWLY_CREATED_USERS: null
+ }),
+ STAT_MAX_ACTIVE_USERS: 20,
+ STAT_MAX_NEW_USERS: 20,
+
SocketEvents: {
POSTED: 'posted',
POST_EDITED: 'post_edited',
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index 0d7be4b08..68942c61a 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -21,23 +21,33 @@
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android Native App",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
- "admin.analytics.activeUsers": "Active Users With Posts",
- "admin.analytics.channelTypes": "Channel Types",
- "admin.analytics.loading": "Loading...",
- "admin.analytics.meaningful": "Not enough data for a meaningful representation.",
- "admin.analytics.newlyCreated": "Newly Created Users",
- "admin.analytics.postTypes": "Posts, Files and Hashtags",
- "admin.analytics.privateGroups": "Private Groups",
- "admin.analytics.publicChannels": "Public Channels",
- "admin.analytics.recentActive": "Recent Active Users",
- "admin.analytics.textPosts": "Posts with Text-only",
- "admin.analytics.title": "Statistics for {title}",
- "admin.analytics.totalFilePosts": "Posts with Files",
- "admin.analytics.totalHashtagPosts": "Posts with Hashtags",
- "admin.analytics.totalIncomingWebhooks": "Incoming Webhooks",
- "admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks",
- "admin.analytics.totalPosts": "Total Posts",
- "admin.analytics.totalUsers": "Total Users",
+ "analytics.chart.loading": "Loading...",
+ "analytics.chart.meaningful": "Not enough data for a meaningful representation.",
+ "analytics.system.activeUsers": "Active Users With Posts",
+ "analytics.system.channelTypes": "Channel Types",
+ "analytics.system.postTypes": "Posts, Files and Hashtags",
+ "analytics.system.privateGroups": "Private Groups",
+ "analytics.system.publicChannels": "Public Channels",
+ "analytics.system.textPosts": "Posts with Text-only",
+ "analytics.system.title": "System Statistics",
+ "analytics.system.totalFilePosts": "Posts with Files",
+ "analytics.system.totalHashtagPosts": "Posts with Hashtags",
+ "analytics.system.totalIncomingWebhooks": "Incoming Webhooks",
+ "analytics.system.totalOutgoingWebhooks": "Outgoing Webhooks",
+ "analytics.system.totalCommands": "Total Commands",
+ "analytics.system.totalSessions": "Total Sessions",
+ "analytics.system.totalPosts": "Total Posts",
+ "analytics.system.totalUsers": "Total Users",
+ "analytics.system.totalTeams": "Total Teams",
+ "analytics.system.totalChannels": "Total Channels",
+ "analytics.team.activeUsers": "Active Users With Posts",
+ "analytics.team.recentActive": "Recent Active Users",
+ "analytics.team.newlyCreated": "Newly Created Users",
+ "analytics.team.privateGroups": "Private Groups",
+ "analytics.team.publicChannels": "Public Channels",
+ "analytics.team.title": "Team Statistics for {team}",
+ "analytics.team.totalPosts": "Total Posts",
+ "analytics.team.totalUsers": "Total Users",
"admin.audits.reload": "Reload",
"admin.audits.title": "User Activity",
"admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index ea1b4663a..ac9fd173f 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -21,23 +21,26 @@
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android App Nativa",
"activity_log_modal.iphoneNativeApp": "iPhone App Nativa",
- "admin.analytics.activeUsers": "Usuarios Activos con Mensajes",
- "admin.analytics.channelTypes": "Tipos de Canales",
- "admin.analytics.loading": "Cargando...",
- "admin.analytics.meaningful": "No hay suficiente data para tener una representación significativa.",
- "admin.analytics.newlyCreated": "Nuevos Usuarios Creados",
- "admin.analytics.postTypes": "Mesajes, Archivos y Hashtags",
- "admin.analytics.privateGroups": "Grupos Privados",
- "admin.analytics.publicChannels": "Canales Públicos",
- "admin.analytics.recentActive": "Usuarios Recientemente Activos",
- "admin.analytics.textPosts": "Mensajes de sólo Texto",
- "admin.analytics.title": "Estadísticas para {title}",
- "admin.analytics.totalFilePosts": "Mensajes con Archivos",
- "admin.analytics.totalHashtagPosts": "Mensajes con Hashtags",
- "admin.analytics.totalIncomingWebhooks": "Webhooks de Entrada",
- "admin.analytics.totalOutgoingWebhooks": "Webhooks de Salida",
- "admin.analytics.totalPosts": "Total de Mensajes",
- "admin.analytics.totalUsers": "Total de Usuarios",
+ "analytics.chart.loading": "Cargando...",
+ "analytics.chart.meaningful": "No hay suficiente data para tener una representación significativa.",
+ "analytics.system.channelTypes": "Tipos de Canales",
+ "analytics.system.postTypes": "Mesajes, Archivos y Hashtags",
+ "analytics.system.privateGroups": "Grupos Privados",
+ "analytics.system.publicChannels": "Canales Públicos",
+ "analytics.system.textPosts": "Mensajes de sólo Texto",
+ "analytics.system.totalFilePosts": "Mensajes con Archivos",
+ "analytics.system.totalHashtagPosts": "Mensajes con Hashtags",
+ "analytics.system.totalIncomingWebhooks": "Webhooks de Entrada",
+ "analytics.system.totalOutgoingWebhooks": "Webhooks de Salida",
+ "analytics.system.totalPosts": "Total de Mensajes",
+ "analytics.system.totalUsers": "Total de Usuarios",
+ "analytics.team.activeUsers": "Usuarios Activos con Mensajes",
+ "analytics.team.newlyCreated": "Nuevos Usuarios Creados",
+ "analytics.team.privateGroups": "Grupos Privados",
+ "analytics.team.publicChannels": "Canales Públicos",
+ "analytics.team.recentActive": "Usuarios Recientemente Activos",
+ "analytics.team.totalPosts": "Total de Mensajes",
+ "analytics.team.totalUsers": "Total de Usuarios",
"admin.audits.reload": "Recargar",
"admin.audits.title": "Auditorías del Servidor",
"admin.email.allowEmailSignInDescription": "Cuando es verdadero, Mattermost permite a los usuarios iniciar sesión utilizando el correo electrónico y contraseña.",