mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' of https://github.com/mattermost/platform into ui-improvements
This commit is contained in:
@@ -1,5 +1,50 @@
|
||||
# Contributing
|
||||
# Code Contribution Guidelines
|
||||
|
||||
## Contributing Code
|
||||
Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know.
|
||||
|
||||
Please see [Mattermost Code Contribution Guidelines](https://github.com/mattermost/platform/blob/master/doc/developer/Code-Contribution-Guidelines.md)
|
||||
## Choose a Ticket
|
||||
|
||||
1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira
|
||||
- You are welcome to work on any ticket, even if it is assigned, so long as it is not yet marked "in progress"
|
||||
- (optional) Comment on the ticket that you're starting so no one else inadvertently duplicates your work
|
||||
|
||||
2. These projects are intended to be a straight forward first pull requests from new contributors
|
||||
- If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101)
|
||||
- Also, feel free to fix bugs you find, or items in GitHub issues that the core team has approved, but not yet added to Jira
|
||||
|
||||
3. If you have any questions at all about a ticket, there are several options to ask:
|
||||
1. Start a topic in the [Mattermost forum](http://forum.mattermost.org/)
|
||||
2. Join the [Mattermost core team discussion](https://pre-release.mattermost.com/signup_user_complete/?id=rcgiyftm7jyrxnma1osd8zswby) and post in the "Tickets" channel
|
||||
|
||||
## Install Mattermost and set up a Fork
|
||||
|
||||
1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/developer/Setup.md) to install Mattermost
|
||||
|
||||
2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command:
|
||||
|
||||
```
|
||||
git checkout -b <branch name>
|
||||
```
|
||||
|
||||
## Programming and Testing
|
||||
|
||||
1. Please review the [Mattermost Style Guide](doc/developer/Style-Guide.md) prior to making changes
|
||||
|
||||
To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted
|
||||
|
||||
2. Please make sure to thoroughly test your change before submitting a pull request
|
||||
|
||||
Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
|
||||
|
||||
2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon
|
||||
|
||||
- For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples
|
||||
|
||||
3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release
|
||||
|
||||
4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted
|
||||
|
||||
62
api/admin.go
62
api/admin.go
@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
|
||||
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
||||
sr.Handle("/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")
|
||||
}
|
||||
|
||||
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -142,3 +142,63 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
m["SUCCESS"] = "true"
|
||||
w.Write([]byte(model.MapToJson(m)))
|
||||
}
|
||||
|
||||
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.HasSystemAdminPermissions("getAnalytics") {
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
teamId := params["id"]
|
||||
name := params["name"]
|
||||
|
||||
if name == "standard" {
|
||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
|
||||
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
||||
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
||||
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
||||
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
|
||||
|
||||
if r := <-openChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[0].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
if r := <-privateChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[1].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
if r := <-postChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[2].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
w.Write([]byte(rows.ToJson()))
|
||||
} else if name == "post_counts_day" {
|
||||
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||
}
|
||||
} else if name == "user_counts_with_posts_day" {
|
||||
if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||
}
|
||||
} else {
|
||||
c.SetInvalidParam("getAnalytics", "name")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -150,3 +150,151 @@ func TestEmailTest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAnalyticsStandard(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
c := &Context{}
|
||||
c.RequestId = model.NewId()
|
||||
c.IpAddress = "cmd_line"
|
||||
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Name != "channel_open_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[0].Value != 2 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[1].Name != "channel_private_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[1].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Name != "post_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPostCount(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
c := &Context{}
|
||||
c.RequestId = model.NewId()
|
||||
c.IpAddress = "cmd_line"
|
||||
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
|
||||
t.Fatal("Shouldn't have permissions")
|
||||
}
|
||||
|
||||
c := &Context{}
|
||||
c.RequestId = model.NewId()
|
||||
c.IpAddress = "cmd_line"
|
||||
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,6 +652,12 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
for k, p := range profiles {
|
||||
options := utils.SanitizeOptions
|
||||
options["passwordupdate"] = false
|
||||
|
||||
if c.HasSystemAdminPermissions("getProfiles") {
|
||||
options["fullname"] = true
|
||||
options["email"] = true
|
||||
}
|
||||
|
||||
p.Sanitize(options)
|
||||
profiles[k] = p
|
||||
}
|
||||
|
||||
@@ -1,48 +1,5 @@
|
||||
# Code Contribution Guidelines
|
||||
|
||||
Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know.
|
||||
|
||||
## Choose a Ticket
|
||||
|
||||
1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira.
|
||||
2. These projects are intended to be a straight forward first pull requests from new contributors.
|
||||
If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101).
|
||||
|
||||
3. If you have any questions at all about a ticket, please post to the [Contributor Discussion section](http://forum.mattermost.org/) of the Mattermost forum, or email the [Mattermost Developer Mailing list](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join).
|
||||
|
||||
## Install Mattermost and set up a Fork
|
||||
|
||||
1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/developer/Setup.md) to install Mattermost.
|
||||
|
||||
2. Create a branch with <branch name> set to the ID of the ticket you're working on, for example ```PLT-394```, using command:
|
||||
|
||||
```
|
||||
git checkout -b <branch name>
|
||||
```
|
||||
|
||||
## Programming and Testing
|
||||
|
||||
1. Please review the [Mattermost Style Guide](Style-Guide.md) prior to making changes.
|
||||
|
||||
To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted.
|
||||
|
||||
2. Please make sure to thoroughly test your change before submitting a pull request.
|
||||
|
||||
Please review the ["Fast, Obvious, Forgiving" experience design principles](http://www.mattermost.org/design-principles/) for Mattermost and check that your feature meets the criteria. Also, for any changes to user interface or help text, please read the changes out loud, as a quick and easy way to catch any inconsitencies.
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
|
||||
|
||||
2. When you submit your pull request please make it against `master` and include the Ticket ID at the beginning of your pull request comment, followed by a colon.
|
||||
|
||||
For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples.
|
||||
|
||||
3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by at least two members of the Mattermost core team, who may either accept the PR or follow-up with feedback. It would then get merged into `master` for the next release.
|
||||
|
||||
4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted.
|
||||
|
||||
|
||||
Please see [CONTRIBUTING.md](https://github.com/mattermost/platform/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
|
||||
19
doc/install/Administration.md
Normal file
19
doc/install/Administration.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Administration
|
||||
|
||||
This document provides instructions for common administrator tasks
|
||||
|
||||
#### Important notes
|
||||
|
||||
##### **DO NOT manipulate the Mattermost database**
|
||||
- In particular, DO NOT delete data from the database, as Mattermost is designed to stop working if data integrity has been compromised. The system is designed to archive content continously and generally assumes data is never deleted.
|
||||
|
||||
### Common Tasks
|
||||
|
||||
##### Creating System Administrator account from commandline
|
||||
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||
|
||||
##### Deactivating a user
|
||||
|
||||
- Team Admin or System Admin can go to **Main Menu** > **Manage Members** > **Make Inactive** to deactivate a user, which removes them from the team.
|
||||
- To preserve audit history, users are never deleted from the system. It is highly recommended that System Administrators do not attempt to delete users manually from the database, as this may compromise system integrity and ability to upgrade in future.
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
#### Common Issues
|
||||
|
||||
##### Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
|
||||
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
||||
|
||||
##### Lost System Administrator account
|
||||
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||
@@ -19,7 +16,15 @@
|
||||
|
||||
The following is a list of common error messages and solutions:
|
||||
|
||||
##### "We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly"
|
||||
- Message appears in blue bar on team site. Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server).
|
||||
###### `Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.`
|
||||
- Message appears in blue bar on team site.
|
||||
- **Solution:** Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server).
|
||||
|
||||
|
||||
###### `x509: certificate signed by unknown authority` in server logs when attempting to sign-up
|
||||
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You
|
||||
- **Solution:** Set up a load balancer like Ngnix [per production install guide](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-with-ssl-recommended). A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
||||
|
||||
###### `panic: runtime error: invalid memory address or nil pointer dereference`
|
||||
- This error can occur if you have manually manipulated the Mattermost database, typically with deletions. Mattermost is designed to serve as a searchable archive, and manual manipulation of the database elements compromises integrity and may prevent upgrade.
|
||||
- **Solution:** Restore from database backup created prior to manual database updates, or reinstall the system.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Outgoing Webhooks
|
||||
|
||||
#### [To be released in Mattermost v1.2, available now on master]
|
||||
|
||||
Outgoing webhooks allow external applications, written in the programming language of your choice--to receive HTTP POST requests whenever a user posts to a certain channel, with a trigger word at the beginning of the message, or a combination of both. If the external application responds appropriately to the HTTP request, as response post can be made in the channel where the original post occurred.
|
||||
|
||||
A couple key points:
|
||||
@@ -112,7 +114,7 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl
|
||||
|
||||
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
|
||||
|
||||
#### Limitations
|
||||
#### Known Issues
|
||||
|
||||
- Overriding of usernames does not yet apply to notifications
|
||||
- Cannot supply `icon_emoji` to override the message icon
|
||||
|
||||
55
model/analytics_row.go
Normal file
55
model/analytics_row.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type AnalyticsRow struct {
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type AnalyticsRows []*AnalyticsRow
|
||||
|
||||
func (me *AnalyticsRow) ToJson() string {
|
||||
b, err := json.Marshal(me)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow {
|
||||
decoder := json.NewDecoder(data)
|
||||
var me AnalyticsRow
|
||||
err := decoder.Decode(&me)
|
||||
if err == nil {
|
||||
return &me
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (me AnalyticsRows) ToJson() string {
|
||||
if b, err := json.Marshal(me); err != nil {
|
||||
return "[]"
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows {
|
||||
decoder := json.NewDecoder(data)
|
||||
var me AnalyticsRows
|
||||
err := decoder.Decode(&me)
|
||||
if err == nil {
|
||||
return me
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
37
model/analytics_row_test.go
Normal file
37
model/analytics_row_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnalyticsRowJson(t *testing.T) {
|
||||
a1 := AnalyticsRow{}
|
||||
a1.Name = "2015-10-12"
|
||||
a1.Value = 12345.0
|
||||
json := a1.ToJson()
|
||||
ra1 := AnalyticsRowFromJson(strings.NewReader(json))
|
||||
|
||||
if a1.Name != ra1.Name {
|
||||
t.Fatal("days didn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsRowsJson(t *testing.T) {
|
||||
a1 := AnalyticsRow{}
|
||||
a1.Name = "2015-10-12"
|
||||
a1.Value = 12345.0
|
||||
|
||||
var a1s AnalyticsRows = make([]*AnalyticsRow, 1)
|
||||
a1s[0] = &a1
|
||||
|
||||
ljson := a1s.ToJson()
|
||||
results := AnalyticsRowsFromJson(strings.NewReader(ljson))
|
||||
|
||||
if a1s[0].Name != results[0].Name {
|
||||
t.Fatal("Ids do not match")
|
||||
}
|
||||
}
|
||||
@@ -416,6 +416,15 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -828,3 +828,31 @@ func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(
|
||||
`SELECT
|
||||
COUNT(Id) AS Value
|
||||
FROM
|
||||
Channels
|
||||
WHERE
|
||||
TeamId = :TeamId
|
||||
AND Type = :ChannelType`,
|
||||
map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
|
||||
} else {
|
||||
result.Data = v
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -561,6 +561,24 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
|
||||
if list.Channels[0].Name != o3.Name {
|
||||
t.Fatal("missing channel")
|
||||
}
|
||||
|
||||
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 2 {
|
||||
t.Log(r1.Data)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
|
||||
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 2 {
|
||||
t.Log(r1.Data)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreGetChannelCounts(t *testing.T) {
|
||||
|
||||
@@ -603,3 +603,152 @@ func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
query :=
|
||||
`SELECT
|
||||
t1.Name, COUNT(t1.UserId) AS Value
|
||||
FROM
|
||||
(SELECT DISTINCT
|
||||
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||
Posts.UserId
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query =
|
||||
`SELECT
|
||||
TO_CHAR(t1.Name, 'YYYY-MM-DD') AS Name, COUNT(t1.UserId) AS Value
|
||||
FROM
|
||||
(SELECT DISTINCT
|
||||
DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) AS Name,
|
||||
Posts.UserId
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
}
|
||||
|
||||
var rows model.AnalyticsRows
|
||||
_, err := s.GetReplica().Select(
|
||||
&rows,
|
||||
query,
|
||||
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsUserCountsWithPostsByDay", "We couldn't get user counts with posts", err.Error())
|
||||
} else {
|
||||
result.Data = rows
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
query :=
|
||||
`SELECT
|
||||
Name, COUNT(Value) AS Value
|
||||
FROM
|
||||
(SELECT
|
||||
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||
'1' AS Value
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt >:Time) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query =
|
||||
`SELECT
|
||||
Name, COUNT(Value) AS Value
|
||||
FROM
|
||||
(SELECT
|
||||
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name,
|
||||
'1' AS Value
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt > :Time) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
}
|
||||
|
||||
var rows model.AnalyticsRows
|
||||
_, err := s.GetReplica().Select(
|
||||
&rows,
|
||||
query,
|
||||
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCountsByDay", "We couldn't get post counts by day", err.Error())
|
||||
} else {
|
||||
result.Data = rows
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(
|
||||
`SELECT
|
||||
COUNT(Posts.Id) AS Value
|
||||
FROM
|
||||
Posts,
|
||||
Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId`,
|
||||
map[string]interface{}{"TeamId": teamId})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
|
||||
} else {
|
||||
result.Data = v
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -580,3 +580,135 @@ func TestPostStoreSearch(t *testing.T) {
|
||||
t.Fatal("returned wrong search result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
t1 := &model.Team{}
|
||||
t1.DisplayName = "DisplayName"
|
||||
t1.Name = "a" + model.NewId() + "b"
|
||||
t1.Email = model.NewId() + "@nowhere.com"
|
||||
t1.Type = model.TEAM_OPEN
|
||||
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||
|
||||
c1 := &model.Channel{}
|
||||
c1.TeamId = t1.Id
|
||||
c1.DisplayName = "Channel2"
|
||||
c1.Name = "a" + model.NewId() + "b"
|
||||
c1.Type = model.CHANNEL_OPEN
|
||||
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = c1.Id
|
||||
o1.UserId = model.NewId()
|
||||
o1.CreateAt = model.GetMillis()
|
||||
o1.Message = "a" + model.NewId() + "b"
|
||||
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||
|
||||
o1a := &model.Post{}
|
||||
o1a.ChannelId = c1.Id
|
||||
o1a.UserId = model.NewId()
|
||||
o1a.CreateAt = o1.CreateAt
|
||||
o1a.Message = "a" + model.NewId() + "b"
|
||||
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = c1.Id
|
||||
o2.UserId = model.NewId()
|
||||
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||
o2.Message = "a" + model.NewId() + "b"
|
||||
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||
|
||||
o2a := &model.Post{}
|
||||
o2a.ChannelId = c1.Id
|
||||
o2a.UserId = o2.UserId
|
||||
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||
o2a.Message = "a" + model.NewId() + "b"
|
||||
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||
|
||||
if r1 := <-store.Post().AnalyticsUserCountsWithPostsByDay(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||
if row1.Value != 2 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
|
||||
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||
if row2.Value != 1 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostCountsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
t1 := &model.Team{}
|
||||
t1.DisplayName = "DisplayName"
|
||||
t1.Name = "a" + model.NewId() + "b"
|
||||
t1.Email = model.NewId() + "@nowhere.com"
|
||||
t1.Type = model.TEAM_OPEN
|
||||
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||
|
||||
c1 := &model.Channel{}
|
||||
c1.TeamId = t1.Id
|
||||
c1.DisplayName = "Channel2"
|
||||
c1.Name = "a" + model.NewId() + "b"
|
||||
c1.Type = model.CHANNEL_OPEN
|
||||
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = c1.Id
|
||||
o1.UserId = model.NewId()
|
||||
o1.CreateAt = model.GetMillis()
|
||||
o1.Message = "a" + model.NewId() + "b"
|
||||
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||
|
||||
o1a := &model.Post{}
|
||||
o1a.ChannelId = c1.Id
|
||||
o1a.UserId = model.NewId()
|
||||
o1a.CreateAt = o1.CreateAt
|
||||
o1a.Message = "a" + model.NewId() + "b"
|
||||
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = c1.Id
|
||||
o2.UserId = model.NewId()
|
||||
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||
o2.Message = "a" + model.NewId() + "b"
|
||||
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||
|
||||
o2a := &model.Post{}
|
||||
o2a.ChannelId = c1.Id
|
||||
o2a.UserId = o2.UserId
|
||||
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||
o2a.Message = "a" + model.NewId() + "b"
|
||||
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
t.Log(t1.Id)
|
||||
|
||||
if r1 := <-store.Post().AnalyticsPostCountsByDay(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||
if row1.Value != 2 {
|
||||
t.Log(row1)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
|
||||
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||
if row2.Value != 2 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
|
||||
if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 4 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ type ChannelStore interface {
|
||||
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
||||
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
||||
IncrementMentionCount(channelId string, userId string) StoreChannel
|
||||
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
|
||||
}
|
||||
|
||||
type PostStore interface {
|
||||
@@ -87,6 +88,9 @@ type PostStore interface {
|
||||
GetEtag(channelId string) StoreChannel
|
||||
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
||||
GetForExport(channelId string) StoreChannel
|
||||
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||
AnalyticsPostCount(teamId string) StoreChannel
|
||||
}
|
||||
|
||||
type UserStore interface {
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"globals": {
|
||||
"React": false,
|
||||
"ReactDOM": false,
|
||||
"ReactBootstrap": false
|
||||
"ReactBootstrap": false,
|
||||
"Chart": false
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [2, "never"],
|
||||
|
||||
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
|
||||
var TeamSettingsTab = require('./team_settings.jsx');
|
||||
var ServiceSettingsTab = require('./service_settings.jsx');
|
||||
var TeamUsersTab = require('./team_users.jsx');
|
||||
var TeamAnalyticsTab = require('./team_analytics.jsx');
|
||||
|
||||
export default class AdminController extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
|
||||
if (this.state.teams) {
|
||||
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||
}
|
||||
} else if (this.state.selected === 'team_analytics') {
|
||||
if (this.state.teams) {
|
||||
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
|
||||
handleClick(name, teamId, e) {
|
||||
e.preventDefault();
|
||||
this.props.selectTab(name, teamId);
|
||||
history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
||||
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
||||
}
|
||||
|
||||
isSelected(name, teamId) {
|
||||
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
|
||||
{'- Users'}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('team_analytics', team.id)}
|
||||
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
|
||||
>
|
||||
{'- Statistics'}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
50
web/react/components/admin_console/line_chart.jsx
Normal file
50
web/react/components/admin_console/line_chart.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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
|
||||
};
|
||||
357
web/react/components/admin_console/team_analytics.jsx
Normal file
357
web/react/components/admin_console/team_analytics.jsx
Normal file
@@ -0,0 +1,357 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../../utils/client.jsx');
|
||||
var Utils = require('../../utils/utils.jsx');
|
||||
var LineChart = require('./line_chart.jsx');
|
||||
|
||||
export default class TeamAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getData = this.getData.bind(this);
|
||||
|
||||
this.state = {
|
||||
users: null,
|
||||
serverError: null,
|
||||
channel_open_count: null,
|
||||
channel_private_count: null,
|
||||
post_count: null,
|
||||
post_counts_day: null,
|
||||
user_counts_with_posts_day: null,
|
||||
recent_active_users: null,
|
||||
newly_created_users: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getData(this.props.team.id);
|
||||
}
|
||||
|
||||
getData(teamId) {
|
||||
Client.getAnalytics(
|
||||
teamId,
|
||||
'standard',
|
||||
(data) => {
|
||||
for (var index in data) {
|
||||
if (data[index].name === 'channel_open_count') {
|
||||
this.setState({channel_open_count: data[index].value});
|
||||
}
|
||||
|
||||
if (data[index].name === 'channel_private_count') {
|
||||
this.setState({channel_private_count: data[index].value});
|
||||
}
|
||||
|
||||
if (data[index].name === 'post_count') {
|
||||
this.setState({post_count: data[index].value});
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
|
||||
Client.getAnalytics(
|
||||
teamId,
|
||||
'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.getAnalytics(
|
||||
teamId,
|
||||
'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});
|
||||
}
|
||||
);
|
||||
|
||||
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++) {
|
||||
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
|
||||
});
|
||||
|
||||
this.getData(newProps.team.id);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var totalCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Total Users'}</div>
|
||||
<div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openChannelCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Public Groups'}</div>
|
||||
<div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openPrivateCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Private Groups'}</div>
|
||||
<div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCountsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.post_counts_day != null) {
|
||||
postCountsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<LineChart
|
||||
data={this.state.post_counts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var usersWithPostsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.user_counts_with_posts_day != null) {
|
||||
usersWithPostsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Active Users With Posts'}</div>
|
||||
<LineChart
|
||||
data={this.state.user_counts_with_posts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</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='recent-active-users'>
|
||||
<div>{'Recent Active Users'}</div>
|
||||
<table width='90%'>
|
||||
<tbody>
|
||||
{
|
||||
this.state.recent_active_users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td className='recent-active-users-td'>{user.email}</td>
|
||||
<td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</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='recent-active-users'>
|
||||
<div>{'Newly Created Users'}</div>
|
||||
<table width='90%'>
|
||||
<tbody>
|
||||
{
|
||||
this.state.newly_created_users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td className='recent-active-users-td'>{user.email}</td>
|
||||
<td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h2>{'Statistics for ' + this.props.team.name}</h2>
|
||||
{serverError}
|
||||
{totalCount}
|
||||
{postCount}
|
||||
{openChannelCount}
|
||||
{openPrivateCount}
|
||||
{postCountsByDay}
|
||||
{usersWithPostsByDay}
|
||||
{recentActiveUser}
|
||||
{newUsers}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamAnalytics.propTypes = {
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
@@ -33,14 +33,6 @@ export default class UserList extends React.Component {
|
||||
this.getTeamProfiles(this.props.team.id);
|
||||
}
|
||||
|
||||
// this.setState({
|
||||
// teamId: this.state.teamId,
|
||||
// users: this.state.users,
|
||||
// serverError: this.state.serverError,
|
||||
// showPasswordModal: this.state.showPasswordModal,
|
||||
// user: this.state.user
|
||||
// });
|
||||
|
||||
getTeamProfiles(teamId) {
|
||||
Client.getProfilesForTeam(
|
||||
teamId,
|
||||
@@ -95,8 +87,6 @@ export default class UserList extends React.Component {
|
||||
}
|
||||
|
||||
doPasswordResetDismiss() {
|
||||
this.state.showPasswordModal = false;
|
||||
this.state.user = null;
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: this.state.users,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
const UserStore = require('../stores/user_store.jsx');
|
||||
const PostStore = require('../stores/post_store.jsx');
|
||||
const SearchStore = require('../stores/search_store.jsx');
|
||||
const NavbarSearchBox = require('./search_bar.jsx');
|
||||
const AsyncClient = require('../utils/async_client.jsx');
|
||||
const Client = require('../utils/client.jsx');
|
||||
@@ -35,19 +35,19 @@ export default class ChannelHeader extends React.Component {
|
||||
memberChannel: ChannelStore.getCurrentMember(),
|
||||
memberTeam: UserStore.getCurrentUser(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members,
|
||||
searchVisible: PostStore.getSearchResults() !== null
|
||||
searchVisible: SearchStore.getSearchResults() !== null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||
PostStore.addSearchChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
PostStore.removeSearchChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Mention = require('./mention.jsx');
|
||||
|
||||
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addMentionDataChangeListener(this.onListenerChange);
|
||||
SearchStore.addMentionDataChangeListener(this.onListenerChange);
|
||||
|
||||
$('.post-right__scroll').scroll(this.onScroll);
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
|
||||
$(document).click(this.onClick);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeMentionDataChangeListener(this.onListenerChange);
|
||||
SearchStore.removeMentionDataChangeListener(this.onListenerChange);
|
||||
$('body').off('keydown.mentionlist', '#' + this.props.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
|
||||
this.state = state;
|
||||
}
|
||||
getSearchTermStateFromStores() {
|
||||
var term = PostStore.getSearchTerm() || '';
|
||||
var term = SearchStore.getSearchTerm() || '';
|
||||
return {
|
||||
searchTerm: term
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addSearchTermChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchTermChangeListener(this.onListenerChange);
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchTermChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
|
||||
this.mounted = false;
|
||||
}
|
||||
onListenerChange(doSearch, isMentionSearch) {
|
||||
@@ -84,8 +84,8 @@ export default class SearchBar extends React.Component {
|
||||
}
|
||||
handleUserInput(e) {
|
||||
var term = e.target.value;
|
||||
PostStore.storeSearchTerm(term);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
SearchStore.storeSearchTerm(term);
|
||||
SearchStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: term});
|
||||
|
||||
this.refs.autocomplete.handleInputChange(e.target, term);
|
||||
@@ -150,8 +150,8 @@ export default class SearchBar extends React.Component {
|
||||
textbox.value = text;
|
||||
utils.setCaretPosition(textbox, preText.length + word.length);
|
||||
|
||||
PostStore.storeSearchTerm(text);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
SearchStore.storeSearchTerm(text);
|
||||
SearchStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: text});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var SearchBox = require('./search_bar.jsx');
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
@@ -9,7 +9,7 @@ var SearchResultsHeader = require('./search_results_header.jsx');
|
||||
var SearchResultsItem = require('./search_results_item.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return {results: PostStore.getSearchResults()};
|
||||
return {results: SearchStore.getSearchResults()};
|
||||
}
|
||||
|
||||
export default class SearchResults extends React.Component {
|
||||
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
PostStore.addSearchChangeListener(this.onChange);
|
||||
SearchStore.addSearchChangeListener(this.onChange);
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchChangeListener(this.onChange);
|
||||
SearchStore.removeSearchChangeListener(this.onChange);
|
||||
this.mounted = false;
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export default class SearchResults extends React.Component {
|
||||
searchForm = <SearchBox />;
|
||||
}
|
||||
var noResults = (!results || !results.order || !results.order.length);
|
||||
var searchTerm = PostStore.getSearchTerm();
|
||||
var searchTerm = SearchStore.getSearchTerm();
|
||||
|
||||
var ctls = null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var UserProfile = require('./user_profile.jsx');
|
||||
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: data,
|
||||
from_search: PostStore.getSearchTerm()
|
||||
from_search: SearchStore.getSearchTerm()
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
var SearchResults = require('./search_results.jsx');
|
||||
var RhsThread = require('./rhs_thread.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()};
|
||||
return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
|
||||
}
|
||||
|
||||
export default class SidebarRight extends React.Component {
|
||||
@@ -22,11 +23,11 @@ export default class SidebarRight extends React.Component {
|
||||
this.state = getStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addSearchChangeListener(this.onSearchChange);
|
||||
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchChangeListener(this.onSearchChange);
|
||||
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
const PostStore = require('../stores/post_store.jsx');
|
||||
const SearchStore = require('../stores/search_store.jsx');
|
||||
const CommandList = require('./command_list.jsx');
|
||||
const ErrorStore = require('../stores/error_store.jsx');
|
||||
|
||||
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
PostStore.addAddMentionListener(this.onListenerChange);
|
||||
SearchStore.addAddMentionListener(this.onListenerChange);
|
||||
ErrorStore.addChangeListener(this.onRecievedError);
|
||||
|
||||
this.resize();
|
||||
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
PostStore.removeAddMentionListener(this.onListenerChange);
|
||||
SearchStore.removeAddMentionListener(this.onListenerChange);
|
||||
ErrorStore.removeChangeListener(this.onRecievedError);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,7 @@ var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
var EDIT_POST_EVENT = 'edit_post';
|
||||
|
||||
class PostStoreClass extends EventEmitter {
|
||||
@@ -26,21 +22,15 @@ class PostStoreClass extends EventEmitter {
|
||||
this.emitChange = this.emitChange.bind(this);
|
||||
this.addChangeListener = this.addChangeListener.bind(this);
|
||||
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||
this.emitSearchChange = this.emitSearchChange.bind(this);
|
||||
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
||||
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
||||
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||
|
||||
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
|
||||
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
|
||||
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
|
||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
||||
this.emitAddMention = this.emitAddMention.bind(this);
|
||||
this.addAddMentionListener = this.addAddMentionListener.bind(this);
|
||||
this.removeAddMentionListener = this.removeAddMentionListener.bind(this);
|
||||
|
||||
this.emitEditPost = this.emitEditPost.bind(this);
|
||||
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||
|
||||
this.getCurrentPosts = this.getCurrentPosts.bind(this);
|
||||
this.storePosts = this.storePosts.bind(this);
|
||||
this.pStorePosts = this.pStorePosts.bind(this);
|
||||
@@ -59,13 +49,8 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
|
||||
this.clearPendingPosts = this.clearPendingPosts.bind(this);
|
||||
this.updatePendingPost = this.updatePendingPost.bind(this);
|
||||
this.storeSearchResults = this.storeSearchResults.bind(this);
|
||||
this.getSearchResults = this.getSearchResults.bind(this);
|
||||
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
||||
this.storeSelectedPost = this.storeSelectedPost.bind(this);
|
||||
this.getSelectedPost = this.getSelectedPost.bind(this);
|
||||
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
||||
this.getSearchTerm = this.getSearchTerm.bind(this);
|
||||
this.getEmptyDraft = this.getEmptyDraft.bind(this);
|
||||
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
|
||||
this.getCurrentDraft = this.getCurrentDraft.bind(this);
|
||||
@@ -77,9 +62,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
||||
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
||||
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
||||
this.emitEditPost = this.emitEditPost.bind(this);
|
||||
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
|
||||
}
|
||||
emitChange() {
|
||||
@@ -94,30 +76,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchChange() {
|
||||
this.emit(SEARCH_CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addSearchChangeListener(callback) {
|
||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchChangeListener(callback) {
|
||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchTermChange(doSearch, isMentionSearch) {
|
||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||
}
|
||||
|
||||
addSearchTermChangeListener(callback) {
|
||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchTermChangeListener(callback) {
|
||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSelectedPostChange(fromSearch) {
|
||||
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
|
||||
}
|
||||
@@ -130,30 +88,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitMentionDataChange(id, mentionText) {
|
||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
||||
}
|
||||
|
||||
addMentionDataChangeListener(callback) {
|
||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeMentionDataChangeListener(callback) {
|
||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitAddMention(id, username) {
|
||||
this.emit(ADD_MENTION_EVENT, id, username);
|
||||
}
|
||||
|
||||
addAddMentionListener(callback) {
|
||||
this.on(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
removeAddMentionListener(callback) {
|
||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
emitEditPost(post) {
|
||||
this.emit(EDIT_POST_EVENT, post);
|
||||
}
|
||||
@@ -181,9 +115,9 @@ class PostStoreClass extends EventEmitter {
|
||||
|
||||
var postList = makePostListNonNull(this.getPosts(channelId));
|
||||
|
||||
for (let pid in newPostList.posts) {
|
||||
for (const pid in newPostList.posts) {
|
||||
if (newPostList.posts.hasOwnProperty(pid)) {
|
||||
var np = newPostList.posts[pid];
|
||||
const np = newPostList.posts[pid];
|
||||
if (np.delete_at === 0) {
|
||||
postList.posts[pid] = np;
|
||||
if (postList.order.indexOf(pid) === -1) {
|
||||
@@ -194,7 +128,7 @@ class PostStoreClass extends EventEmitter {
|
||||
delete postList.posts[pid];
|
||||
}
|
||||
|
||||
var index = postList.order.indexOf(pid);
|
||||
const index = postList.order.indexOf(pid);
|
||||
if (index !== -1) {
|
||||
postList.order.splice(index, 1);
|
||||
}
|
||||
@@ -202,7 +136,7 @@ class PostStoreClass extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
postList.order.sort(function postSort(a, b) {
|
||||
postList.order.sort((a, b) => {
|
||||
if (postList.posts[a].create_at > postList.posts[b].create_at) {
|
||||
return -1;
|
||||
}
|
||||
@@ -306,7 +240,7 @@ class PostStoreClass extends EventEmitter {
|
||||
var posts = postList.posts;
|
||||
|
||||
// sort failed posts to the bottom
|
||||
postList.order.sort(function postSort(a, b) {
|
||||
postList.order.sort((a, b) => {
|
||||
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
|
||||
return 1;
|
||||
}
|
||||
@@ -371,7 +305,7 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pStorePendingPosts(channelId, postList);
|
||||
}
|
||||
clearPendingPosts() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
|
||||
BrowserStore.removeItem(key);
|
||||
});
|
||||
}
|
||||
@@ -387,28 +321,12 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pStorePendingPosts(post.channel_id, postList);
|
||||
this.emitChange();
|
||||
}
|
||||
storeSearchResults(results, isMentionSearch) {
|
||||
BrowserStore.setItem('search_results', results);
|
||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||
}
|
||||
getSearchResults() {
|
||||
return BrowserStore.getItem('search_results');
|
||||
}
|
||||
getIsMentionSearch() {
|
||||
return BrowserStore.getItem('is_mention_search');
|
||||
}
|
||||
storeSelectedPost(postList) {
|
||||
BrowserStore.setItem('select_post', postList);
|
||||
}
|
||||
getSelectedPost() {
|
||||
return BrowserStore.getItem('select_post');
|
||||
}
|
||||
storeSearchTerm(term) {
|
||||
BrowserStore.setItem('search_term', term);
|
||||
}
|
||||
getSearchTerm() {
|
||||
return BrowserStore.getItem('search_term');
|
||||
}
|
||||
getEmptyDraft() {
|
||||
return {message: '', uploadsInProgress: [], previews: []};
|
||||
}
|
||||
@@ -433,7 +351,7 @@ class PostStoreClass extends EventEmitter {
|
||||
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
|
||||
}
|
||||
clearDraftUploads() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
|
||||
if (value) {
|
||||
value.uploadsInProgress = [];
|
||||
BrowserStore.setItem(key, value);
|
||||
@@ -441,7 +359,7 @@ class PostStoreClass extends EventEmitter {
|
||||
});
|
||||
}
|
||||
clearCommentDraftUploads() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
|
||||
if (value) {
|
||||
value.uploadsInProgress = [];
|
||||
BrowserStore.setItem(key, value);
|
||||
@@ -458,7 +376,7 @@ class PostStoreClass extends EventEmitter {
|
||||
|
||||
var PostStore = new PostStoreClass();
|
||||
|
||||
PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
||||
PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
var action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -469,24 +387,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
||||
PostStore.pStorePost(action.post);
|
||||
PostStore.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH:
|
||||
PostStore.storeSearchResults(action.results, action.is_mention_search);
|
||||
PostStore.emitSearchChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||
PostStore.storeSearchTerm(action.term);
|
||||
PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_POST_SELECTED:
|
||||
PostStore.storeSelectedPost(action.post_list);
|
||||
PostStore.emitSelectedPostChange(action.from_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||
PostStore.emitMentionDataChange(action.id, action.mention_text);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
PostStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_EDIT_POST:
|
||||
PostStore.emitEditPost(action);
|
||||
break;
|
||||
|
||||
153
web/react/stores/search_store.jsx
Normal file
153
web/react/stores/search_store.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var BrowserStore = require('../stores/browser_store.jsx');
|
||||
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
|
||||
class SearchStoreClass extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.emitChange = this.emitChange.bind(this);
|
||||
this.addChangeListener = this.addChangeListener.bind(this);
|
||||
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||
|
||||
this.emitSearchChange = this.emitSearchChange.bind(this);
|
||||
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
||||
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
||||
|
||||
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||
|
||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
||||
|
||||
this.getSearchResults = this.getSearchResults.bind(this);
|
||||
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
||||
|
||||
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
||||
this.getSearchTerm = this.getSearchTerm.bind(this);
|
||||
|
||||
this.storeSearchResults = this.storeSearchResults.bind(this);
|
||||
}
|
||||
|
||||
emitChange() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addChangeListener(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeChangeListener(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchChange() {
|
||||
this.emit(SEARCH_CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addSearchChangeListener(callback) {
|
||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchChangeListener(callback) {
|
||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchTermChange(doSearch, isMentionSearch) {
|
||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||
}
|
||||
|
||||
addSearchTermChangeListener(callback) {
|
||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchTermChangeListener(callback) {
|
||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
getSearchResults() {
|
||||
return BrowserStore.getItem('search_results');
|
||||
}
|
||||
|
||||
getIsMentionSearch() {
|
||||
return BrowserStore.getItem('is_mention_search');
|
||||
}
|
||||
|
||||
storeSearchTerm(term) {
|
||||
BrowserStore.setItem('search_term', term);
|
||||
}
|
||||
|
||||
getSearchTerm() {
|
||||
return BrowserStore.getItem('search_term');
|
||||
}
|
||||
|
||||
emitMentionDataChange(id, mentionText) {
|
||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
||||
}
|
||||
|
||||
addMentionDataChangeListener(callback) {
|
||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeMentionDataChangeListener(callback) {
|
||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitAddMention(id, username) {
|
||||
this.emit(ADD_MENTION_EVENT, id, username);
|
||||
}
|
||||
|
||||
addAddMentionListener(callback) {
|
||||
this.on(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
removeAddMentionListener(callback) {
|
||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
storeSearchResults(results, isMentionSearch) {
|
||||
BrowserStore.setItem('search_results', results);
|
||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||
}
|
||||
}
|
||||
|
||||
var SearchStore = new SearchStoreClass();
|
||||
|
||||
SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
var action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECIEVED_SEARCH:
|
||||
SearchStore.storeSearchResults(action.results, action.is_mention_search);
|
||||
SearchStore.emitSearchChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||
SearchStore.storeSearchTerm(action.term);
|
||||
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||
SearchStore.emitMentionDataChange(action.id, action.mention_text);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
SearchStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchStore;
|
||||
@@ -328,6 +328,20 @@ export function getConfig(success, error) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getAnalytics(teamId, name, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success,
|
||||
error: (xhr, status, err) => {
|
||||
var e = handleError('getAnalytics', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function saveConfig(config, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/save_config',
|
||||
|
||||
@@ -211,11 +211,15 @@ export function displayDateTime(ticks) {
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1) {
|
||||
if (interval >= 2) {
|
||||
return interval + ' minutes ago';
|
||||
}
|
||||
|
||||
return '1 minute ago';
|
||||
if (interval >= 1) {
|
||||
return '1 minute ago';
|
||||
}
|
||||
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
export function displayCommentDateTime(ticks) {
|
||||
|
||||
@@ -5,6 +5,62 @@
|
||||
.table {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.total-count {
|
||||
width: 175px;
|
||||
height: 100px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 22px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
float: left;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.total-count-by-day {
|
||||
width: 760px;
|
||||
height: 275px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
clear: both;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-active-users {
|
||||
width: 365px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
float: left;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
> table {
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.recent-active-users-td {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
border: 1px solid #ddd;
|
||||
padding: 3px 3px 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--left {
|
||||
&.sidebar--collapsable {
|
||||
background: #333;
|
||||
|
||||
11
web/static/js/Chart.min.js
vendored
Normal file
11
web/static/js/Chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
<script src="/static/js/Chart.min.js"></script>
|
||||
|
||||
<div id='error_bar'></div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user