Merge branch 'master' into PLT-1429
Этот коммит содержится в:
6
Makefile
6
Makefile
@@ -151,7 +151,7 @@ package:
|
|||||||
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||||
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
|
||||||
rm $(DIST_PATH)/web/templates/*.bak
|
rm $(DIST_PATH)/web/templates/*.bak
|
||||||
|
|
||||||
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
|
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
|
||||||
|
|
||||||
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
|
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
|
||||||
@@ -283,7 +283,7 @@ run: start-docker .prepare-go .prepare-jsx
|
|||||||
$(GO) run $(GOFLAGS) mattermost.go -config=config.json &
|
$(GO) run $(GOFLAGS) mattermost.go -config=config.json &
|
||||||
|
|
||||||
@echo Starting compass watch
|
@echo Starting compass watch
|
||||||
cd web/sass-files && compass watch &
|
cd web/sass-files && compass compile && compass watch &
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@for PID in $$(ps -ef | grep [c]ompass | awk '{ print $$2 }'); do \
|
@for PID in $$(ps -ef | grep [c]ompass | awk '{ print $$2 }'); do \
|
||||||
@@ -296,7 +296,7 @@ stop:
|
|||||||
kill $$PID; \
|
kill $$PID; \
|
||||||
done
|
done
|
||||||
|
|
||||||
@for PID in $$(ps -ef | grep [m]atterm | awk '{ print $$2 }'); do \
|
@for PID in $$(ps -ef | grep [m]atterm | grep -v VirtualBox | awk '{ print $$2 }'); do \
|
||||||
echo stopping go web $$PID; \
|
echo stopping go web $$PID; \
|
||||||
kill $$PID; \
|
kill $$PID; \
|
||||||
done
|
done
|
||||||
|
|||||||
73
api/admin.go
73
api/admin.go
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
package api
|
package api
|
||||||
@@ -21,6 +21,7 @@ func InitAdmin(r *mux.Router) {
|
|||||||
|
|
||||||
sr := r.PathPrefix("/admin").Subrouter()
|
sr := r.PathPrefix("/admin").Subrouter()
|
||||||
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
|
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
|
||||||
|
sr.Handle("/audits", ApiUserRequired(getAllAudits)).Methods("GET")
|
||||||
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
|
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
|
||||||
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
|
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
|
||||||
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
||||||
@@ -58,6 +59,32 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.ArrayToJson(lines)))
|
w.Write([]byte(model.ArrayToJson(lines)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAllAudits(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
if !c.HasSystemAdminPermissions("getAllAudits") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Audit().Get("", 200); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
audits := result.Data.(model.Audits)
|
||||||
|
etag := audits.Etag()
|
||||||
|
|
||||||
|
if HandleEtag(etag, w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(etag) > 0 {
|
||||||
|
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(audits.ToJson()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte(model.MapToJson(utils.ClientCfg)))
|
w.Write([]byte(model.MapToJson(utils.ClientCfg)))
|
||||||
}
|
}
|
||||||
@@ -161,9 +188,10 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||||
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||||
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
|
rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
|
||||||
|
|
||||||
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
||||||
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
||||||
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
|
postChan := Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
|
||||||
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId)
|
||||||
|
|
||||||
if r := <-openChan; r.Err != nil {
|
if r := <-openChan; r.Err != nil {
|
||||||
@@ -209,6 +237,47 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||||
}
|
}
|
||||||
|
} else if name == "extra_counts" {
|
||||||
|
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4)
|
||||||
|
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}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if r := <-fileChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[0].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-hashtagChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[1].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-iHookChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[2].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r := <-oHookChan; r.Err != nil {
|
||||||
|
c.Err = r.Err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
rows[3].Value = float64(r.Data.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(rows.ToJson()))
|
||||||
} else {
|
} else {
|
||||||
c.SetInvalidParam("getAnalytics", "name")
|
c.SetInvalidParam("getAnalytics", "name")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,36 @@ func TestGetLogs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAllAudits(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() + "success+test@simulator.amazonses.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")
|
||||||
|
|
||||||
|
if _, err := Client.GetAllAudits(); 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 audits, err := Client.GetAllAudits(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(audits.Data.(model.Audits)) <= 0 {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetClientProperties(t *testing.T) {
|
func TestGetClientProperties(t *testing.T) {
|
||||||
Setup()
|
Setup()
|
||||||
|
|
||||||
@@ -362,3 +392,113 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTeamAnalyticsExtra(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() + "success+test@simulator.amazonses.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)
|
||||||
|
|
||||||
|
post2 := &model.Post{ChannelId: channel1.Id, Message: "#test a" + model.NewId() + "a"}
|
||||||
|
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||||
|
|
||||||
|
if _, err := Client.GetTeamAnalytics("", "extra_counts"); 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.GetTeamAnalytics(team.Id, "extra_counts"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Name != "file_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[0].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Name != "hashtag_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Value != 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Name != "incoming_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Name != "outgoing_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Value != 0 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result, err := Client.GetSystemAnalytics("extra_counts"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
rows := result.Data.(model.AnalyticsRows)
|
||||||
|
|
||||||
|
if rows[0].Name != "file_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Name != "hashtag_post_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[1].Value < 1 {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[2].Name != "incoming_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows[3].Name != "outgoing_webhook_count" {
|
||||||
|
t.Log(rows.ToJson())
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
34
api/user.go
34
api/user.go
@@ -444,6 +444,38 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User {
|
||||||
|
var team *model.Team
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
team = result.Data.(*model.Team)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-Srv.Store.User().GetByUsername(team.Id, username); result.Err != nil {
|
||||||
|
c.Err = result.Err
|
||||||
|
c.Err.StatusCode = http.StatusForbidden
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
user := result.Data.(*model.User)
|
||||||
|
|
||||||
|
if len(user.AuthData) != 0 {
|
||||||
|
c.Err = model.NewLocAppError("LoginByUsername", "api.user.login_by_email.sign_in.app_error",
|
||||||
|
map[string]interface{}{"AuthService": user.AuthService}, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
|
||||||
|
Login(c, w, r, user, deviceId)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
|
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
|
||||||
authData := ""
|
authData := ""
|
||||||
provider := einterfaces.GetOauthProvider(service)
|
provider := einterfaces.GetOauthProvider(service)
|
||||||
@@ -629,6 +661,8 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
|
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
|
||||||
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
|
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
|
||||||
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
|
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
|
||||||
|
} else if len(props["username"]) != 0 && len(props["name"]) != 0 {
|
||||||
|
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"])
|
||||||
} else {
|
} else {
|
||||||
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
|
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
|
||||||
c.Err.StatusCode = http.StatusForbidden
|
c.Err.StatusCode = http.StatusForbidden
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func TestLogin(t *testing.T) {
|
|||||||
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||||
rteam, _ := Client.CreateTeam(&team)
|
rteam, _ := Client.CreateTeam(&team)
|
||||||
|
|
||||||
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
|
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey", Password: "pwd"}
|
||||||
ruser, _ := Client.CreateUser(&user, "")
|
ruser, _ := Client.CreateUser(&user, "")
|
||||||
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
|
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ func TestLogin(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
if result.Data.(*model.User).Email != user.Email {
|
if result.Data.(*model.User).Email != user.Email {
|
||||||
t.Fatal("email's didn't match")
|
t.Fatal("emails didn't match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,14 +119,30 @@ func TestLogin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result, err := Client.LoginByUsername(team.Name, user.Username, user.Password); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if result.Data.(*model.User).Email != user.Email {
|
||||||
|
t.Fatal("emails didn't match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := Client.LoginByEmail(team.Name, user.Email, user.Password+"invalid"); err == nil {
|
if _, err := Client.LoginByEmail(team.Name, user.Email, user.Password+"invalid"); err == nil {
|
||||||
t.Fatal("Invalid Password")
|
t.Fatal("Invalid Password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := Client.LoginByUsername(team.Name, user.Username, user.Password+"invalid"); err == nil {
|
||||||
|
t.Fatal("Invalid Password")
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := Client.LoginByEmail(team.Name, "", user.Password); err == nil {
|
if _, err := Client.LoginByEmail(team.Name, "", user.Password); err == nil {
|
||||||
t.Fatal("should have failed")
|
t.Fatal("should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := Client.LoginByUsername(team.Name, "", user.Password); err == nil {
|
||||||
|
t.Fatal("should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
authToken := Client.AuthToken
|
authToken := Client.AuthToken
|
||||||
Client.AuthToken = "invalid"
|
Client.AuthToken = "invalid"
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
},
|
},
|
||||||
"EmailSettings": {
|
"EmailSettings": {
|
||||||
"EnableSignUpWithEmail": true,
|
"EnableSignUpWithEmail": true,
|
||||||
|
"EnableSignInWithEmail": true,
|
||||||
|
"EnableSignInWithUsername": false,
|
||||||
"SendEmailNotifications": false,
|
"SendEmailNotifications": false,
|
||||||
"RequireEmailVerification": false,
|
"RequireEmailVerification": false,
|
||||||
"FeedbackName": "",
|
"FeedbackName": "",
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ This test contains instructions for the core team to manually test common attach
|
|||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
- All file types should upload and post.
|
- All file types should upload and post.
|
||||||
- Read the expected for details on the behavior of the thumbnail and preview window.
|
- Read the expected for details on the behavior of the thumbnail and preview window.
|
||||||
- The expected behavior of video and audio formats depends on the operating system, browser and plugins. View the permalinks to the Public Test Channel on Pre-Release Core to see the expected cases.
|
- The expected behavior of video and audio formats depends on the operating system, browser and plugins. View the permalinks to the Public Test Channel on Pre-Release Core to see the expected cases.
|
||||||
|
- If the browser can play the media file, media player controls should appear. If the browser cannot play the file, it should show appear as a regular attachment without the media controls.
|
||||||
|
|
||||||
|
|
||||||
### Images
|
### Images
|
||||||
@@ -72,7 +73,7 @@ Expected: Generic Word thumbnail & preview window.
|
|||||||
|
|
||||||
**MP4**
|
**MP4**
|
||||||
`Videos/MP4.mp4`
|
`Videos/MP4.mp4`
|
||||||
Expected: Generic video thumbnail & playable preview window. View Permalink.
|
Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
|
||||||
[Permalink](https://pre-release.mattermost.com/core/pl/5dx5qx9t9brqfnhohccxjynx7c)
|
[Permalink](https://pre-release.mattermost.com/core/pl/5dx5qx9t9brqfnhohccxjynx7c)
|
||||||
|
|
||||||
**AVI**
|
**AVI**
|
||||||
@@ -114,7 +115,7 @@ Expected: Generic audio thumbnail & playable preview window
|
|||||||
|
|
||||||
**M4A**
|
**M4A**
|
||||||
`Audio/M4a.m4a`
|
`Audio/M4a.m4a`
|
||||||
Expected: Generic audio thumbnail & playable preview window
|
Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
|
||||||
[Permalink](https://pre-release.mattermost.com/core/pl/6c7qsw48ybd88bktgeykodsrrc)
|
[Permalink](https://pre-release.mattermost.com/core/pl/6c7qsw48ybd88bktgeykodsrrc)
|
||||||
|
|
||||||
**AAC**
|
**AAC**
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
},
|
},
|
||||||
"EmailSettings": {
|
"EmailSettings": {
|
||||||
"EnableSignUpWithEmail": true,
|
"EnableSignUpWithEmail": true,
|
||||||
|
"EnableSignInWithEmail": true,
|
||||||
|
"EnableSignInWithUsername": false,
|
||||||
"SendEmailNotifications": false,
|
"SendEmailNotifications": false,
|
||||||
"RequireEmailVerification": false,
|
"RequireEmailVerification": false,
|
||||||
"FeedbackName": "",
|
"FeedbackName": "",
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
},
|
},
|
||||||
"EmailSettings": {
|
"EmailSettings": {
|
||||||
"EnableSignUpWithEmail": true,
|
"EnableSignUpWithEmail": true,
|
||||||
|
"EnableSignInWithEmail": true,
|
||||||
|
"EnableSignInWithUsername": false,
|
||||||
"SendEmailNotifications": false,
|
"SendEmailNotifications": false,
|
||||||
"RequireEmailVerification": false,
|
"RequireEmailVerification": false,
|
||||||
"FeedbackName": "",
|
"FeedbackName": "",
|
||||||
|
|||||||
42
i18n/en.json
42
i18n/en.json
@@ -1667,6 +1667,38 @@
|
|||||||
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
|
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
|
||||||
"translation": "Inappropriate permissions to regenerate outcoming webhook token"
|
"translation": "Inappropriate permissions to regenerate outcoming webhook token"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.bind_admin_user.app_error",
|
||||||
|
"translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.invalid_password.app_error",
|
||||||
|
"translation": "Invalid Password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.licence_disable.app_error",
|
||||||
|
"translation": "LDAP functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.matched_to_many_users.app_error",
|
||||||
|
"translation": "Username given matches multiple users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.search_ldap_server.app_error",
|
||||||
|
"translation": "Failed to search LDAP server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.unable_to_connect.app_error",
|
||||||
|
"translation": "Unable to connect to LDAP server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.unable_to_create_user.app_error",
|
||||||
|
"translation": "Credentials valid but unable to create user."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.user_not_registered.app_error",
|
||||||
|
"translation": "User not registered on LDAP server"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "manaultesting.get_channel_id.no_found.debug",
|
"id": "manaultesting.get_channel_id.no_found.debug",
|
||||||
"translation": "Could not find channel: %v, %v possibilites searched"
|
"translation": "Could not find channel: %v, %v possibilites searched"
|
||||||
@@ -3131,6 +3163,14 @@
|
|||||||
"id": "store.sql_webhooks.update_outgoing.app_error",
|
"id": "store.sql_webhooks.update_outgoing.app_error",
|
||||||
"translation": "We couldn't update the webhook"
|
"translation": "We couldn't update the webhook"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_webhooks.analytics_incoming_count.app_error",
|
||||||
|
"translation": "We couldn't count the incoming webhooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_webhooks.analytics_outgoing_count.app_error",
|
||||||
|
"translation": "We couldn't count the outgoing webhooks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "utils.config.load_config.decoding.panic",
|
"id": "utils.config.load_config.decoding.panic",
|
||||||
"translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
|
"translation": "Error decoding config file={{.Filename}}, err={{.Error}}"
|
||||||
@@ -3491,4 +3531,4 @@
|
|||||||
"id": "web.watcher_fail.error",
|
"id": "web.watcher_fail.error",
|
||||||
"translation": "Failed to add directory to watcher %v"
|
"translation": "Failed to add directory to watcher %v"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
34
i18n/es.json
34
i18n/es.json
@@ -1531,6 +1531,38 @@
|
|||||||
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
|
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
|
||||||
"translation": "Permisos inapropiados para regenerar un token para el Webhook saliente"
|
"translation": "Permisos inapropiados para regenerar un token para el Webhook saliente"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.bind_admin_user.app_error",
|
||||||
|
"translation": "No se pudo enlazar con el servidor LDAP. Revisa las opciones de BindUsername y BindPassword."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.invalid_password.app_error",
|
||||||
|
"translation": "Contraseña inválida"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.licence_disable.app_error",
|
||||||
|
"translation": "Las funcionalidades de LDAP están deshabilitadas con la licencia actual. Por favor contacta a un administrador del sistema acerca de mejorar la licencia enterprise."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.matched_to_many_users.app_error",
|
||||||
|
"translation": "Nombre de usuario dado coincide con varios usuarios"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.search_ldap_server.app_error",
|
||||||
|
"translation": "Falla al buscar en el servidor LDAP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.unable_to_connect.app_error",
|
||||||
|
"translation": "No se pudo conectar con el servidor LDAP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.unable_to_create_user.app_error",
|
||||||
|
"translation": "Credenciales válidas pero no se pudo crear el usuario."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.ldap.do_login.user_not_registered.app_error",
|
||||||
|
"translation": "Usuario no registrado en el servidor LDAP"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "manaultesting.get_channel_id.no_found.debug",
|
"id": "manaultesting.get_channel_id.no_found.debug",
|
||||||
"translation": "No pudimos encontrar el canal: %v, búsqueda realizada con estas posibilidades %v"
|
"translation": "No pudimos encontrar el canal: %v, búsqueda realizada con estas posibilidades %v"
|
||||||
@@ -3287,4 +3319,4 @@
|
|||||||
"id": "web.watcher_fail.error",
|
"id": "web.watcher_fail.error",
|
||||||
"translation": "Falla al agregar el directorio a ser vigilado %v"
|
"translation": "Falla al agregar el directorio a ser vigilado %v"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -31,6 +31,8 @@ import (
|
|||||||
_ "github.com/go-ldap/ldap"
|
_ "github.com/go-ldap/ldap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//ENTERPRISE_IMPORTS
|
||||||
|
|
||||||
var flagCmdCreateTeam bool
|
var flagCmdCreateTeam bool
|
||||||
var flagCmdCreateUser bool
|
var flagCmdCreateUser bool
|
||||||
var flagCmdAssignRole bool
|
var flagCmdAssignRole bool
|
||||||
|
|||||||
@@ -280,6 +280,14 @@ func (c *Client) LoginByEmail(name string, email string, password string) (*Resu
|
|||||||
return c.login(m)
|
return c.login(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) LoginByUsername(name string, username string, password string) (*Result, *AppError) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
m["name"] = name
|
||||||
|
m["username"] = username
|
||||||
|
m["password"] = password
|
||||||
|
return c.login(m)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) LoginByEmailWithDevice(name string, email string, password string, deviceId string) (*Result, *AppError) {
|
func (c *Client) LoginByEmailWithDevice(name string, email string, password string, deviceId string) (*Result, *AppError) {
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
m["name"] = name
|
m["name"] = name
|
||||||
@@ -443,6 +451,15 @@ func (c *Client) GetLogs() (*Result, *AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAllAudits() (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiGet("/admin/audits", "", ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) GetClientProperties() (*Result, *AppError) {
|
func (c *Client) GetClientProperties() (*Result, *AppError) {
|
||||||
if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil {
|
if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ type FileSettings struct {
|
|||||||
|
|
||||||
type EmailSettings struct {
|
type EmailSettings struct {
|
||||||
EnableSignUpWithEmail bool
|
EnableSignUpWithEmail bool
|
||||||
|
EnableSignInWithEmail *bool
|
||||||
|
EnableSignInWithUsername *bool
|
||||||
SendEmailNotifications bool
|
SendEmailNotifications bool
|
||||||
RequireEmailVerification bool
|
RequireEmailVerification bool
|
||||||
FeedbackName string
|
FeedbackName string
|
||||||
@@ -260,6 +262,21 @@ func (o *Config) SetDefaults() {
|
|||||||
*o.TeamSettings.EnableTeamListing = false
|
*o.TeamSettings.EnableTeamListing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.EmailSettings.EnableSignInWithEmail == nil {
|
||||||
|
o.EmailSettings.EnableSignInWithEmail = new(bool)
|
||||||
|
|
||||||
|
if o.EmailSettings.EnableSignUpWithEmail == true {
|
||||||
|
*o.EmailSettings.EnableSignInWithEmail = true
|
||||||
|
} else {
|
||||||
|
*o.EmailSettings.EnableSignInWithEmail = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.EmailSettings.EnableSignInWithUsername == nil {
|
||||||
|
o.EmailSettings.EnableSignInWithUsername = new(bool)
|
||||||
|
*o.EmailSettings.EnableSignInWithUsername = false
|
||||||
|
}
|
||||||
|
|
||||||
if o.EmailSettings.SendPushNotifications == nil {
|
if o.EmailSettings.SendPushNotifications == nil {
|
||||||
o.EmailSettings.SendPushNotifications = new(bool)
|
o.EmailSettings.SendPushNotifications = new(bool)
|
||||||
*o.EmailSettings.SendPushNotifications = false
|
*o.EmailSettings.SendPushNotifications = false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
package store
|
package store
|
||||||
@@ -72,9 +72,16 @@ func (s SqlAuditStore) Get(user_id string, limit int) StoreChannel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query := "SELECT * FROM Audits"
|
||||||
|
|
||||||
|
if len(user_id) != 0 {
|
||||||
|
query += " WHERE UserId = :user_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY CreateAt DESC LIMIT :limit"
|
||||||
|
|
||||||
var audits model.Audits
|
var audits model.Audits
|
||||||
if _, err := s.GetReplica().Select(&audits, "SELECT * FROM Audits WHERE UserId = :user_id ORDER BY CreateAt DESC LIMIT :limit",
|
if _, err := s.GetReplica().Select(&audits, query, map[string]interface{}{"user_id": user_id, "limit": limit}); err != nil {
|
||||||
map[string]interface{}{"user_id": user_id, "limit": limit}); err != nil {
|
|
||||||
result.Err = model.NewLocAppError("SqlAuditStore.Get", "store.sql_audit.get.finding.app_error", nil, "user_id="+user_id)
|
result.Err = model.NewLocAppError("SqlAuditStore.Get", "store.sql_audit.get.finding.app_error", nil, "user_id="+user_id)
|
||||||
} else {
|
} else {
|
||||||
result.Data = audits
|
result.Data = audits
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ func TestSqlAuditStore(t *testing.T) {
|
|||||||
t.Fatal("Should have returned empty because user_id is missing")
|
t.Fatal("Should have returned empty because user_id is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c = store.Audit().Get("", 100)
|
||||||
|
result = <-c
|
||||||
|
audits = result.Data.(model.Audits)
|
||||||
|
|
||||||
|
if len(audits) <= 4 {
|
||||||
|
t.Fatal("Failed to save and retrieve 4 audit logs")
|
||||||
|
}
|
||||||
|
|
||||||
if r2 := <-store.Audit().PermanentDeleteByUser(audit.UserId); r2.Err != nil {
|
if r2 := <-store.Audit().PermanentDeleteByUser(audit.UserId); r2.Err != nil {
|
||||||
t.Fatal(r2.Err)
|
t.Fatal(r2.Err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,7 +629,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
|||||||
if params.IsHashtag {
|
if params.IsHashtag {
|
||||||
searchType = "Hashtags"
|
searchType = "Hashtags"
|
||||||
for _, term := range strings.Split(terms, " ") {
|
for _, term := range strings.Split(terms, " ") {
|
||||||
termMap[term] = true
|
termMap[strings.ToUpper(term)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,7 +748,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
|||||||
if searchType == "Hashtags" {
|
if searchType == "Hashtags" {
|
||||||
exactMatch := false
|
exactMatch := false
|
||||||
for _, tag := range strings.Split(p.Hashtags, " ") {
|
for _, tag := range strings.Split(p.Hashtags, " ") {
|
||||||
if termMap[tag] {
|
if termMap[strings.ToUpper(tag)] {
|
||||||
exactMatch = true
|
exactMatch = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -940,7 +940,7 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
|||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel {
|
||||||
storeChannel := make(StoreChannel)
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -959,8 +959,15 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
|||||||
query += " AND Channels.TeamId = :TeamId"
|
query += " AND Channels.TeamId = :TeamId"
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId})
|
if mustHaveFile {
|
||||||
if err != nil {
|
query += " AND Posts.Filenames != '[]'"
|
||||||
|
}
|
||||||
|
|
||||||
|
if mustHaveHashtag {
|
||||||
|
query += " AND Posts.Hashtags != ''"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
result.Err = model.NewLocAppError("SqlPostStore.AnalyticsPostCount", "store.sql_post.analytics_posts_count.app_error", nil, err.Error())
|
result.Err = model.NewLocAppError("SqlPostStore.AnalyticsPostCount", "store.sql_post.analytics_posts_count.app_error", nil, err.Error())
|
||||||
} else {
|
} else {
|
||||||
result.Data = v
|
result.Data = v
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ func TestPostCountsByDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
|
if r1 := <-store.Post().AnalyticsPostCount(t1.Id, false, false); r1.Err != nil {
|
||||||
t.Fatal(r1.Err)
|
t.Fatal(r1.Err)
|
||||||
} else {
|
} else {
|
||||||
if r1.Data.(int64) != 4 {
|
if r1.Data.(int64) != 4 {
|
||||||
|
|||||||
@@ -329,3 +329,65 @@ func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) StoreChanne
|
|||||||
|
|
||||||
return storeChannel
|
return storeChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlWebhookStore) AnalyticsIncomingCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
IncomingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " AND TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsIncomingCount", "store.sql_webhooks.analytics_incoming_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlWebhookStore) AnalyticsOutgoingCount(teamId string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
query :=
|
||||||
|
`SELECT
|
||||||
|
COUNT(*)
|
||||||
|
FROM
|
||||||
|
OutgoingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0`
|
||||||
|
|
||||||
|
if len(teamId) > 0 {
|
||||||
|
query += " AND TeamId = :TeamId"
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}); err != nil {
|
||||||
|
result.Err = model.NewLocAppError("SqlWebhookStore.AnalyticsOutgoingCount", "store.sql_webhooks.analytics_outgoing_count.app_error", nil, "team_id="+teamId+", err="+err.Error())
|
||||||
|
} else {
|
||||||
|
result.Data = v
|
||||||
|
}
|
||||||
|
|
||||||
|
storeChannel <- result
|
||||||
|
close(storeChannel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return storeChannel
|
||||||
|
}
|
||||||
|
|||||||
@@ -304,3 +304,42 @@ func TestWebhookStoreUpdateOutgoing(t *testing.T) {
|
|||||||
t.Fatal(r2.Err)
|
t.Fatal(r2.Err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebhookStoreCountIncoming(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := &model.IncomingWebhook{}
|
||||||
|
o1.ChannelId = model.NewId()
|
||||||
|
o1.UserId = model.NewId()
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
|
||||||
|
o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
|
||||||
|
|
||||||
|
if r := <-store.Webhook().AnalyticsIncomingCount(""); r.Err != nil {
|
||||||
|
t.Fatal(r.Err)
|
||||||
|
} else {
|
||||||
|
if r.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should have at least 1 incoming hook")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookStoreCountOutgoing(t *testing.T) {
|
||||||
|
Setup()
|
||||||
|
|
||||||
|
o1 := &model.OutgoingWebhook{}
|
||||||
|
o1.ChannelId = model.NewId()
|
||||||
|
o1.CreatorId = model.NewId()
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
o1.CallbackURLs = []string{"http://nowhere.com/"}
|
||||||
|
|
||||||
|
o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
|
||||||
|
|
||||||
|
if r := <-store.Webhook().AnalyticsOutgoingCount(""); r.Err != nil {
|
||||||
|
t.Fatal(r.Err)
|
||||||
|
} else {
|
||||||
|
if r.Data.(int64) == 0 {
|
||||||
|
t.Fatal("should have at least 1 outgoing hook")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ type PostStore interface {
|
|||||||
GetForExport(channelId string) StoreChannel
|
GetForExport(channelId string) StoreChannel
|
||||||
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||||
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||||
AnalyticsPostCount(teamId string) StoreChannel
|
AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
@@ -182,6 +182,8 @@ type WebhookStore interface {
|
|||||||
DeleteOutgoing(webhookId string, time int64) StoreChannel
|
DeleteOutgoing(webhookId string, time int64) StoreChannel
|
||||||
PermanentDeleteOutgoingByUser(userId string) StoreChannel
|
PermanentDeleteOutgoingByUser(userId string) StoreChannel
|
||||||
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
|
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
|
||||||
|
AnalyticsIncomingCount(teamId string) StoreChannel
|
||||||
|
AnalyticsOutgoingCount(teamId string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandStore interface {
|
type CommandStore interface {
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ func getClientConfig(c *model.Config) map[string]string {
|
|||||||
|
|
||||||
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
|
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
|
||||||
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
|
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
|
||||||
|
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
|
||||||
|
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
|
||||||
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
|
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
|
||||||
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
|
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
|
||||||
|
|
||||||
|
|||||||
@@ -1,204 +1,24 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
var Modal = ReactBootstrap.Modal;
|
var Modal = ReactBootstrap.Modal;
|
||||||
import UserStore from '../stores/user_store.jsx';
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
|
||||||
import LoadingScreen from './loading_screen.jsx';
|
import LoadingScreen from './loading_screen.jsx';
|
||||||
|
import AuditTable from './audit_table.jsx';
|
||||||
|
|
||||||
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
|
||||||
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
import {intlShape, injectIntl, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const holders = defineMessages({
|
|
||||||
sessionRevoked: {
|
|
||||||
id: 'access_history.sessionRevoked',
|
|
||||||
defaultMessage: 'The session with id {sessionId} was revoked'
|
|
||||||
},
|
|
||||||
channelCreated: {
|
|
||||||
id: 'access_history.channelCreated',
|
|
||||||
defaultMessage: 'Created the {channelName} channel/group'
|
|
||||||
},
|
|
||||||
establishedDM: {
|
|
||||||
id: 'access_history.establishedDM',
|
|
||||||
defaultMessage: 'Established a direct message channel with {username}'
|
|
||||||
},
|
|
||||||
nameUpdated: {
|
|
||||||
id: 'access_history.nameUpdated',
|
|
||||||
defaultMessage: 'Updated the {channelName} channel/group name'
|
|
||||||
},
|
|
||||||
headerUpdated: {
|
|
||||||
id: 'access_history.headerUpdated',
|
|
||||||
defaultMessage: 'Updated the {channelName} channel/group header'
|
|
||||||
},
|
|
||||||
channelDeleted: {
|
|
||||||
id: 'access_history.channelDeleted',
|
|
||||||
defaultMessage: 'Deleted the channel/group with the URL {url}'
|
|
||||||
},
|
|
||||||
userAdded: {
|
|
||||||
id: 'access_history.userAdded',
|
|
||||||
defaultMessage: 'Added {username} to the {channelName} channel/group'
|
|
||||||
},
|
|
||||||
userRemoved: {
|
|
||||||
id: 'access_history.userRemoved',
|
|
||||||
defaultMessage: 'Removed {username} to the {channelName} channel/group'
|
|
||||||
},
|
|
||||||
attemptedRegisterApp: {
|
|
||||||
id: 'access_history.attemptedRegisterApp',
|
|
||||||
defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
|
|
||||||
},
|
|
||||||
attemptedAllowOAuthAccess: {
|
|
||||||
id: 'access_history.attemptedAllowOAuthAccess',
|
|
||||||
defaultMessage: 'Attempted to allow a new OAuth service access'
|
|
||||||
},
|
|
||||||
successfullOAuthAccess: {
|
|
||||||
id: 'access_history.successfullOAuthAccess',
|
|
||||||
defaultMessage: 'Successfully gave a new OAuth service access'
|
|
||||||
},
|
|
||||||
failedOAuthAccess: {
|
|
||||||
id: 'access_history.failedOAuthAccess',
|
|
||||||
defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
|
|
||||||
},
|
|
||||||
attemptedOAuthToken: {
|
|
||||||
id: 'access_history.attemptedOAuthToken',
|
|
||||||
defaultMessage: 'Attempted to get an OAuth access token'
|
|
||||||
},
|
|
||||||
successfullOAuthToken: {
|
|
||||||
id: 'access_history.successfullOAuthToken',
|
|
||||||
defaultMessage: 'Successfully added a new OAuth service'
|
|
||||||
},
|
|
||||||
oauthTokenFailed: {
|
|
||||||
id: 'access_history.oauthTokenFailed',
|
|
||||||
defaultMessage: 'Failed to get an OAuth access token - {token}'
|
|
||||||
},
|
|
||||||
attemptedLogin: {
|
|
||||||
id: 'access_history.attemptedLogin',
|
|
||||||
defaultMessage: 'Attempted to login'
|
|
||||||
},
|
|
||||||
successfullLogin: {
|
|
||||||
id: 'access_history.successfullLogin',
|
|
||||||
defaultMessage: 'Successfully logged in'
|
|
||||||
},
|
|
||||||
failedLogin: {
|
|
||||||
id: 'access_history.failedLogin',
|
|
||||||
defaultMessage: 'FAILED login attempt'
|
|
||||||
},
|
|
||||||
updatePicture: {
|
|
||||||
id: 'access_history.updatePicture',
|
|
||||||
defaultMessage: 'Updated your profile picture'
|
|
||||||
},
|
|
||||||
updateGeneral: {
|
|
||||||
id: 'access_history.updateGeneral',
|
|
||||||
defaultMessage: 'Updated the general settings of your account'
|
|
||||||
},
|
|
||||||
attemptedPassword: {
|
|
||||||
id: 'access_history.attemptedPassword',
|
|
||||||
defaultMessage: 'Attempted to change password'
|
|
||||||
},
|
|
||||||
successfullPassword: {
|
|
||||||
id: 'access_history.successfullPassword',
|
|
||||||
defaultMessage: 'Successfully changed password'
|
|
||||||
},
|
|
||||||
failedPassword: {
|
|
||||||
id: 'access_history.failedPassword',
|
|
||||||
defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
|
|
||||||
},
|
|
||||||
updatedRol: {
|
|
||||||
id: 'access_history.updatedRol',
|
|
||||||
defaultMessage: 'Updated user role(s) to '
|
|
||||||
},
|
|
||||||
member: {
|
|
||||||
id: 'access_history.member',
|
|
||||||
defaultMessage: 'member'
|
|
||||||
},
|
|
||||||
accountActive: {
|
|
||||||
id: 'access_history.accountActive',
|
|
||||||
defaultMessage: 'Account made active'
|
|
||||||
},
|
|
||||||
accountInactive: {
|
|
||||||
id: 'access_history.accountInactive',
|
|
||||||
defaultMessage: 'Account made inactive'
|
|
||||||
},
|
|
||||||
by: {
|
|
||||||
id: 'access_history.by',
|
|
||||||
defaultMessage: ' by {username}'
|
|
||||||
},
|
|
||||||
byAdmin: {
|
|
||||||
id: 'access_history.byAdmin',
|
|
||||||
defaultMessage: ' by an admin'
|
|
||||||
},
|
|
||||||
sentEmail: {
|
|
||||||
id: 'access_history.sentEmail',
|
|
||||||
defaultMessage: 'Sent an email to {email} to reset your password'
|
|
||||||
},
|
|
||||||
attemptedReset: {
|
|
||||||
id: 'access_history.attemptedReset',
|
|
||||||
defaultMessage: 'Attempted to reset password'
|
|
||||||
},
|
|
||||||
successfullReset: {
|
|
||||||
id: 'access_history.successfullReset',
|
|
||||||
defaultMessage: 'Successfully reset password'
|
|
||||||
},
|
|
||||||
updateGlobalNotifications: {
|
|
||||||
id: 'access_history.updateGlobalNotifications',
|
|
||||||
defaultMessage: 'Updated your global notification settings'
|
|
||||||
},
|
|
||||||
attemptedWebhookCreate: {
|
|
||||||
id: 'access_history.attemptedWebhookCreate',
|
|
||||||
defaultMessage: 'Attempted to create a webhook'
|
|
||||||
},
|
|
||||||
succcessfullWebhookCreate: {
|
|
||||||
id: 'access_history.successfullWebhookCreate',
|
|
||||||
defaultMessage: 'Successfully created a webhook'
|
|
||||||
},
|
|
||||||
failedWebhookCreate: {
|
|
||||||
id: 'access_history.failedWebhookCreate',
|
|
||||||
defaultMessage: 'Failed to create a webhook - bad channel permissions'
|
|
||||||
},
|
|
||||||
attemptedWebhookDelete: {
|
|
||||||
id: 'access_history.attemptedWebhookDelete',
|
|
||||||
defaultMessage: 'Attempted to delete a webhook'
|
|
||||||
},
|
|
||||||
successfullWebhookDelete: {
|
|
||||||
id: 'access_history.successfullWebhookDelete',
|
|
||||||
defaultMessage: 'Successfully deleted a webhook'
|
|
||||||
},
|
|
||||||
failedWebhookDelete: {
|
|
||||||
id: 'access_history.failedWebhookDelete',
|
|
||||||
defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
|
|
||||||
},
|
|
||||||
logout: {
|
|
||||||
id: 'access_history.logout',
|
|
||||||
defaultMessage: 'Logged out of your account'
|
|
||||||
},
|
|
||||||
verified: {
|
|
||||||
id: 'access_history.verified',
|
|
||||||
defaultMessage: 'Sucessfully verified your email address'
|
|
||||||
},
|
|
||||||
revokedAll: {
|
|
||||||
id: 'access_history.revokedAll',
|
|
||||||
defaultMessage: 'Revoked all current sessions for the team'
|
|
||||||
},
|
|
||||||
loginAttempt: {
|
|
||||||
id: 'access_history.loginAttempt',
|
|
||||||
defaultMessage: ' (Login attempt)'
|
|
||||||
},
|
|
||||||
loginFailure: {
|
|
||||||
id: 'access_history.loginFailure',
|
|
||||||
defaultMessage: ' (Login failure)'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class AccessHistoryModal extends React.Component {
|
class AccessHistoryModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onAuditChange = this.onAuditChange.bind(this);
|
this.onAuditChange = this.onAuditChange.bind(this);
|
||||||
this.handleMoreInfo = this.handleMoreInfo.bind(this);
|
|
||||||
this.onShow = this.onShow.bind(this);
|
this.onShow = this.onShow.bind(this);
|
||||||
this.onHide = this.onHide.bind(this);
|
this.onHide = this.onHide.bind(this);
|
||||||
this.formatAuditInfo = this.formatAuditInfo.bind(this);
|
|
||||||
this.handleRevokedSession = this.handleRevokedSession.bind(this);
|
|
||||||
|
|
||||||
const state = this.getStateFromStoresForAudits();
|
const state = this.getStateFromStoresForAudits();
|
||||||
state.moreInfo = [];
|
state.moreInfo = [];
|
||||||
@@ -245,359 +65,17 @@ class AccessHistoryModal extends React.Component {
|
|||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleMoreInfo(index) {
|
|
||||||
var newMoreInfo = this.state.moreInfo;
|
|
||||||
newMoreInfo[index] = true;
|
|
||||||
this.setState({moreInfo: newMoreInfo});
|
|
||||||
}
|
|
||||||
handleRevokedSession(sessionId) {
|
|
||||||
return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
|
|
||||||
}
|
|
||||||
formatAuditInfo(currentAudit) {
|
|
||||||
const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
|
|
||||||
|
|
||||||
const {formatMessage} = this.props.intl;
|
|
||||||
let currentAuditDesc = '';
|
|
||||||
|
|
||||||
if (currentActionURL.indexOf('/channels') === 0) {
|
|
||||||
const channelInfo = currentAudit.extra_info.split(' ');
|
|
||||||
const channelNameField = channelInfo[0].split('=');
|
|
||||||
|
|
||||||
let channelURL = '';
|
|
||||||
let channelObj;
|
|
||||||
let channelName = '';
|
|
||||||
if (channelNameField.indexOf('name') >= 0) {
|
|
||||||
channelURL = channelNameField[channelNameField.indexOf('name') + 1];
|
|
||||||
channelObj = ChannelStore.getByName(channelURL);
|
|
||||||
if (channelObj) {
|
|
||||||
channelName = channelObj.display_name;
|
|
||||||
} else {
|
|
||||||
channelName = channelURL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (currentActionURL) {
|
|
||||||
case '/channels/create':
|
|
||||||
currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
|
|
||||||
break;
|
|
||||||
case '/channels/create_direct':
|
|
||||||
currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
|
|
||||||
break;
|
|
||||||
case '/channels/update':
|
|
||||||
currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
|
|
||||||
break;
|
|
||||||
case '/channels/update_desc': // support the old path
|
|
||||||
case '/channels/update_header':
|
|
||||||
currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
let userIdField = [];
|
|
||||||
let userId = '';
|
|
||||||
let username = '';
|
|
||||||
|
|
||||||
if (channelInfo[1]) {
|
|
||||||
userIdField = channelInfo[1].split('=');
|
|
||||||
|
|
||||||
if (userIdField.indexOf('user_id') >= 0) {
|
|
||||||
userId = userIdField[userIdField.indexOf('user_id') + 1];
|
|
||||||
username = UserStore.getProfile(userId).username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
|
|
||||||
currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
|
|
||||||
} else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
|
|
||||||
currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
|
|
||||||
} else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
|
|
||||||
currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentActionURL.indexOf('/oauth') === 0) {
|
|
||||||
const oauthInfo = currentAudit.extra_info.split(' ');
|
|
||||||
|
|
||||||
switch (currentActionURL) {
|
|
||||||
case '/oauth/register': {
|
|
||||||
const clientIdField = oauthInfo[0].split('=');
|
|
||||||
|
|
||||||
if (clientIdField[0] === 'client_id') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '/oauth/allow':
|
|
||||||
if (oauthInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
|
|
||||||
} else if (oauthInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
|
|
||||||
} else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
|
|
||||||
currentAuditDesc = formatMessage(holders.failedOAuthAccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case '/oauth/access_token':
|
|
||||||
if (oauthInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
|
|
||||||
} else if (oauthInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullOAuthToken);
|
|
||||||
} else {
|
|
||||||
const oauthTokenFailure = oauthInfo[0].split('-');
|
|
||||||
|
|
||||||
if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
|
|
||||||
currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (currentActionURL.indexOf('/users') === 0) {
|
|
||||||
const userInfo = currentAudit.extra_info.split(' ');
|
|
||||||
|
|
||||||
switch (currentActionURL) {
|
|
||||||
case '/users/login':
|
|
||||||
if (userInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedLogin);
|
|
||||||
} else if (userInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullLogin);
|
|
||||||
} else if (userInfo[0]) {
|
|
||||||
currentAuditDesc = formatMessage(holders.failedLogin);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case '/users/revoke_session':
|
|
||||||
currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
|
|
||||||
break;
|
|
||||||
case '/users/newimage':
|
|
||||||
currentAuditDesc = formatMessage(holders.updatePicture);
|
|
||||||
break;
|
|
||||||
case '/users/update':
|
|
||||||
currentAuditDesc = formatMessage(holders.updateGeneral);
|
|
||||||
break;
|
|
||||||
case '/users/newpassword':
|
|
||||||
if (userInfo[0] === 'attempted') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedPassword);
|
|
||||||
} else if (userInfo[0] === 'completed') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullPassword);
|
|
||||||
} else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
|
|
||||||
currentAuditDesc = formatMessage(holders.failedPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case '/users/update_roles': {
|
|
||||||
const userRoles = userInfo[0].split('=')[1];
|
|
||||||
|
|
||||||
currentAuditDesc = formatMessage(holders.updatedRol);
|
|
||||||
if (userRoles.trim()) {
|
|
||||||
currentAuditDesc += userRoles;
|
|
||||||
} else {
|
|
||||||
currentAuditDesc += formatMessage(holders.member);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '/users/update_active': {
|
|
||||||
const updateType = userInfo[0].split('=')[0];
|
|
||||||
const updateField = userInfo[0].split('=')[1];
|
|
||||||
|
|
||||||
/* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
|
|
||||||
if (updateType === 'active') {
|
|
||||||
if (updateField === 'true') {
|
|
||||||
currentAuditDesc = formatMessage(holders.accountActive);
|
|
||||||
} else if (updateField === 'false') {
|
|
||||||
currentAuditDesc = formatMessage(holders.accountInactive);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actingUserInfo = userInfo[1].split('=');
|
|
||||||
if (actingUserInfo[0] === 'session_user') {
|
|
||||||
const actingUser = UserStore.getProfile(actingUserInfo[1]);
|
|
||||||
const currentUser = UserStore.getCurrentUser();
|
|
||||||
if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
|
|
||||||
currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
|
|
||||||
} else if (currentUser && actingUser) {
|
|
||||||
currentAuditDesc += formatMessage(holders.byAdmin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (updateType === 'session_id') {
|
|
||||||
currentAuditDesc = this.handleRevokedSession(updateField);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '/users/send_password_reset':
|
|
||||||
currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
|
|
||||||
break;
|
|
||||||
case '/users/reset_password':
|
|
||||||
if (userInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedReset);
|
|
||||||
} else if (userInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullReset);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case '/users/update_notify':
|
|
||||||
currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (currentActionURL.indexOf('/hooks') === 0) {
|
|
||||||
const webhookInfo = currentAudit.extra_info.split(' ');
|
|
||||||
|
|
||||||
switch (currentActionURL) {
|
|
||||||
case '/hooks/incoming/create':
|
|
||||||
if (webhookInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
|
|
||||||
} else if (webhookInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
|
|
||||||
} else if (webhookInfo[0] === 'fail - bad channel permissions') {
|
|
||||||
currentAuditDesc = formatMessage(holders.failedWebhookCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case '/hooks/incoming/delete':
|
|
||||||
if (webhookInfo[0] === 'attempt') {
|
|
||||||
currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
|
|
||||||
} else if (webhookInfo[0] === 'success') {
|
|
||||||
currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
|
|
||||||
} else if (webhookInfo[0] === 'fail - inappropriate conditions') {
|
|
||||||
currentAuditDesc = formatMessage(holders.failedWebhookDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (currentActionURL) {
|
|
||||||
case '/logout':
|
|
||||||
currentAuditDesc = formatMessage(holders.logout);
|
|
||||||
break;
|
|
||||||
case '/verify_email':
|
|
||||||
currentAuditDesc = formatMessage(holders.verified);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If all else fails... */
|
|
||||||
if (!currentAuditDesc) {
|
|
||||||
/* Currently not called anywhere */
|
|
||||||
if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
|
|
||||||
currentAuditDesc = formatMessage(holders.revokedAll);
|
|
||||||
} else {
|
|
||||||
let currentActionDesc = '';
|
|
||||||
if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
|
|
||||||
currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
|
|
||||||
currentActionDesc = Utils.toTitleCase(currentActionDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentExtraInfoDesc = '';
|
|
||||||
if (currentAudit.extra_info) {
|
|
||||||
currentExtraInfoDesc = currentAudit.extra_info;
|
|
||||||
|
|
||||||
if (currentExtraInfoDesc.indexOf('=') !== -1) {
|
|
||||||
currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDate = new Date(currentAudit.create_at);
|
|
||||||
const currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' +
|
|
||||||
currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
|
|
||||||
return currentAuditInfo;
|
|
||||||
}
|
|
||||||
render() {
|
render() {
|
||||||
var accessList = [];
|
|
||||||
|
|
||||||
const {formatMessage} = this.props.intl;
|
|
||||||
for (var i = 0; i < this.state.audits.length; i++) {
|
|
||||||
const currentAudit = this.state.audits[i];
|
|
||||||
const currentAuditInfo = this.formatAuditInfo(currentAudit);
|
|
||||||
|
|
||||||
var moreInfo = (
|
|
||||||
<a
|
|
||||||
href='#'
|
|
||||||
className='theme'
|
|
||||||
onClick={this.handleMoreInfo.bind(this, i)}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='access_history.moreInfo'
|
|
||||||
defaultMessage='More info'
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.state.moreInfo[i]) {
|
|
||||||
if (!currentAudit.session_id) {
|
|
||||||
currentAudit.session_id = 'N/A';
|
|
||||||
|
|
||||||
if (currentAudit.action.search('/users/login') >= 0) {
|
|
||||||
if (currentAudit.extra_info === 'attempt') {
|
|
||||||
currentAudit.session_id += formatMessage(holders.loginAttempt);
|
|
||||||
} else {
|
|
||||||
currentAudit.session_id += formatMessage(holders.loginFailure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moreInfo = (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
id='access_history.ip'
|
|
||||||
defaultMessage='IP: {ip}'
|
|
||||||
values={{
|
|
||||||
ip: currentAudit.ip_address
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
id='access_history.session'
|
|
||||||
defaultMessage='Session ID: {id}'
|
|
||||||
values={{
|
|
||||||
id: currentAudit.session_id
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var divider = null;
|
|
||||||
if (i < this.state.audits.length - 1) {
|
|
||||||
divider = (<div className='divider-light'></div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
accessList[i] = (
|
|
||||||
<div
|
|
||||||
key={'accessHistoryEntryKey' + i}
|
|
||||||
className='access-history__table'
|
|
||||||
>
|
|
||||||
<div className='access__report'>
|
|
||||||
<div className='report__time'>{currentAuditInfo}</div>
|
|
||||||
<div className='report__info'>
|
|
||||||
{moreInfo}
|
|
||||||
</div>
|
|
||||||
{divider}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var content;
|
var content;
|
||||||
if (this.state.audits.loading) {
|
if (this.state.audits.loading) {
|
||||||
content = (<LoadingScreen />);
|
content = (<LoadingScreen />);
|
||||||
} else {
|
} else {
|
||||||
content = (<form role='form'>{accessList}</form>);
|
content = (
|
||||||
|
<AuditTable
|
||||||
|
audits={this.state.audits}
|
||||||
|
moreInfo={this.state.moreInfo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -628,4 +106,4 @@ AccessHistoryModal.propTypes = {
|
|||||||
onHide: React.PropTypes.func.isRequired
|
onHide: React.PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AccessHistoryModal);
|
export default injectIntl(AccessHistoryModal);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import * as Utils from '../../utils/utils.jsx';
|
|||||||
import EmailSettingsTab from './email_settings.jsx';
|
import EmailSettingsTab from './email_settings.jsx';
|
||||||
import LogSettingsTab from './log_settings.jsx';
|
import LogSettingsTab from './log_settings.jsx';
|
||||||
import LogsTab from './logs.jsx';
|
import LogsTab from './logs.jsx';
|
||||||
|
import AuditsTab from './audits.jsx';
|
||||||
import FileSettingsTab from './image_settings.jsx';
|
import FileSettingsTab from './image_settings.jsx';
|
||||||
import PrivacySettingsTab from './privacy_settings.jsx';
|
import PrivacySettingsTab from './privacy_settings.jsx';
|
||||||
import RateSettingsTab from './rate_settings.jsx';
|
import RateSettingsTab from './rate_settings.jsx';
|
||||||
@@ -138,6 +139,8 @@ export default class AdminController extends React.Component {
|
|||||||
tab = <LogSettingsTab config={this.state.config} />;
|
tab = <LogSettingsTab config={this.state.config} />;
|
||||||
} else if (this.state.selected === 'logs') {
|
} else if (this.state.selected === 'logs') {
|
||||||
tab = <LogsTab />;
|
tab = <LogsTab />;
|
||||||
|
} else if (this.state.selected === 'audits') {
|
||||||
|
tab = <AuditsTab />;
|
||||||
} else if (this.state.selected === 'image_settings') {
|
} else if (this.state.selected === 'image_settings') {
|
||||||
tab = <FileSettingsTab config={this.state.config} />;
|
tab = <FileSettingsTab config={this.state.config} />;
|
||||||
} else if (this.state.selected === 'privacy_settings') {
|
} else if (this.state.selected === 'privacy_settings') {
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ export default class AdminSidebar extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let audits;
|
||||||
|
if (global.window.mm_license.IsLicensed === 'true') {
|
||||||
|
audits = (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
className={this.isSelected('audits')}
|
||||||
|
onClick={this.handleClick.bind(this, 'audits', null)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.sidebar.audits'
|
||||||
|
defaultMessage='Audits'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='sidebar--left sidebar--collapsable'>
|
<div className='sidebar--left sidebar--collapsable'>
|
||||||
<div>
|
<div>
|
||||||
@@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{audits}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -4,11 +4,60 @@
|
|||||||
import * as Utils from '../../utils/utils.jsx';
|
import * as Utils from '../../utils/utils.jsx';
|
||||||
import Constants from '../../utils/constants.jsx';
|
import Constants from '../../utils/constants.jsx';
|
||||||
import LineChart from './line_chart.jsx';
|
import LineChart from './line_chart.jsx';
|
||||||
|
import DoughnutChart from './doughnut_chart.jsx';
|
||||||
|
import StatisticCount from './statistic_count.jsx';
|
||||||
|
|
||||||
var Tooltip = ReactBootstrap.Tooltip;
|
var Tooltip = ReactBootstrap.Tooltip;
|
||||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||||
|
|
||||||
import {FormattedMessage} from 'mm-intl';
|
import {injectIntl, intlShape, defineMessages, FormattedMessage} 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 {
|
export default class Analytics extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -18,6 +67,8 @@ export default class Analytics extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() { // in the future, break down these into smaller components
|
render() { // in the future, break down these into smaller components
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
var serverError = '';
|
var serverError = '';
|
||||||
if (this.props.serverError) {
|
if (this.props.serverError) {
|
||||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>;
|
||||||
@@ -30,77 +81,129 @@ export default class Analytics extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
var totalCount = (
|
let firstRow;
|
||||||
<div className='col-sm-3'>
|
let extraGraphs;
|
||||||
<div className='total-count'>
|
if (this.props.showAdvanced) {
|
||||||
<div className='title'>
|
firstRow = (
|
||||||
<FormattedMessage
|
<div className='row'>
|
||||||
id='admin.analytics.totalUsers'
|
<StatisticCount
|
||||||
defaultMessage='Total Users'
|
title={formatMessage(holders.analyticsTotalUsers)}
|
||||||
/>
|
icon='fa-users'
|
||||||
<i className='fa fa-users'/></div>
|
count={this.props.uniqueUserCount}
|
||||||
<div className='content'>{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}</div>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
var openChannelCount = (
|
const channelTypeData = [
|
||||||
<div className='col-sm-3'>
|
{
|
||||||
<div className='total-count'>
|
value: this.props.channelOpenCount,
|
||||||
<div className='title'>
|
color: '#46BFBD',
|
||||||
<FormattedMessage
|
highlight: '#5AD3D1',
|
||||||
id='admin.analytics.publicChannels'
|
label: formatMessage(holders.analyticsPublicChannels)
|
||||||
defaultMessage='Public Channels'
|
},
|
||||||
/>
|
{
|
||||||
<i className='fa fa-globe'/></div>
|
value: this.props.channelPrivateCount,
|
||||||
<div className='content'>{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}</div>
|
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>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
} else {
|
||||||
|
firstRow = (
|
||||||
var openPrivateCount = (
|
<div className='row'>
|
||||||
<div className='col-sm-3'>
|
<StatisticCount
|
||||||
<div className='total-count'>
|
title={formatMessage(holders.analyticsTotalUsers)}
|
||||||
<div className='title'>
|
icon='fa-users'
|
||||||
<FormattedMessage
|
count={this.props.uniqueUserCount}
|
||||||
id='admin.analytics.privateGroups'
|
/>
|
||||||
defaultMessage='Private Groups'
|
<StatisticCount
|
||||||
/>
|
title={formatMessage(holders.analyticsPublicChannels)}
|
||||||
<i className='fa fa-lock'/></div>
|
icon='fa-globe'
|
||||||
<div className='content'>{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}</div>
|
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>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
var postCount = (
|
let postCountsByDay;
|
||||||
<div className='col-sm-3'>
|
if (this.props.postCountsDay == null) {
|
||||||
<div className='total-count'>
|
postCountsByDay = (
|
||||||
<div className='title'>
|
<div className='col-sm-12'>
|
||||||
<FormattedMessage
|
<div className='total-count by-day'>
|
||||||
id='admin.analytics.totalPosts'
|
<div className='title'>
|
||||||
defaultMessage='Total Posts'
|
<FormattedMessage
|
||||||
/>
|
id='admin.analytics.totalPosts'
|
||||||
<i className='fa fa-comment'/></div>
|
defaultMessage='Total Posts'
|
||||||
<div className='content'>{this.props.postCount == null ? loading : this.props.postCount}</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='content'>{loading}</div>
|
||||||
);
|
|
||||||
|
|
||||||
var 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>
|
||||||
<div className='content'>{loading}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
} else {
|
||||||
|
|
||||||
if (this.props.postCountsDay != null) {
|
|
||||||
let content;
|
let content;
|
||||||
if (this.props.postCountsDay.labels.length === 0) {
|
if (this.props.postCountsDay.labels.length === 0) {
|
||||||
content = (
|
content = (
|
||||||
@@ -137,21 +240,22 @@ export default class Analytics extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersWithPostsByDay = (
|
let usersWithPostsByDay;
|
||||||
<div className='col-sm-12'>
|
if (this.props.userCountsWithPostsDay == null) {
|
||||||
<div className='total-count by-day'>
|
usersWithPostsByDay = (
|
||||||
<div className='title'>
|
<div className='col-sm-12'>
|
||||||
<FormattedMessage
|
<div className='total-count by-day'>
|
||||||
id='admin.analytics.activeUsers'
|
<div className='title'>
|
||||||
defaultMessage='Active Users With Posts'
|
<FormattedMessage
|
||||||
/>
|
id='admin.analytics.activeUsers'
|
||||||
|
defaultMessage='Active Users With Posts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='content'>{loading}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='content'>{loading}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
} else {
|
||||||
|
|
||||||
if (this.props.userCountsWithPostsDay != null) {
|
|
||||||
let content;
|
let content;
|
||||||
if (this.props.userCountsWithPostsDay.labels.length === 0) {
|
if (this.props.userCountsWithPostsDay.labels.length === 0) {
|
||||||
content = (
|
content = (
|
||||||
@@ -312,12 +416,8 @@ export default class Analytics extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
{serverError}
|
{serverError}
|
||||||
<div className='row'>
|
{firstRow}
|
||||||
{totalCount}
|
{extraGraphs}
|
||||||
{postCount}
|
|
||||||
{openChannelCount}
|
|
||||||
{openPrivateCount}
|
|
||||||
</div>
|
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
{postCountsByDay}
|
{postCountsByDay}
|
||||||
</div>
|
</div>
|
||||||
@@ -347,10 +447,16 @@ Analytics.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Analytics.propTypes = {
|
Analytics.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
channelOpenCount: React.PropTypes.number,
|
channelOpenCount: React.PropTypes.number,
|
||||||
channelPrivateCount: React.PropTypes.number,
|
channelPrivateCount: React.PropTypes.number,
|
||||||
postCount: 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,
|
postCountsDay: React.PropTypes.object,
|
||||||
userCountsWithPostsDay: React.PropTypes.object,
|
userCountsWithPostsDay: React.PropTypes.object,
|
||||||
recentActiveUsers: React.PropTypes.array,
|
recentActiveUsers: React.PropTypes.array,
|
||||||
@@ -358,3 +464,5 @@ Analytics.propTypes = {
|
|||||||
uniqueUserCount: React.PropTypes.number,
|
uniqueUserCount: React.PropTypes.number,
|
||||||
serverError: React.PropTypes.string
|
serverError: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(Analytics);
|
||||||
|
|||||||
94
web/react/components/admin_console/audits.jsx
Обычный файл
94
web/react/components/admin_console/audits.jsx
Обычный файл
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import LoadingScreen from '../loading_screen.jsx';
|
||||||
|
import AuditTable from '../audit_table.jsx';
|
||||||
|
|
||||||
|
import AdminStore from '../../stores/admin_store.jsx';
|
||||||
|
|
||||||
|
import * as AsyncClient from '../../utils/async_client.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
export default class Audits extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onAuditListenerChange = this.onAuditListenerChange.bind(this);
|
||||||
|
this.reload = this.reload.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
audits: AdminStore.getAudits()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
AdminStore.addAuditChangeListener(this.onAuditListenerChange);
|
||||||
|
AsyncClient.getServerAudits();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
AdminStore.removeAuditChangeListener(this.onAuditListenerChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuditListenerChange() {
|
||||||
|
this.setState({
|
||||||
|
audits: AdminStore.getAudits()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
AdminStore.saveAudits(null);
|
||||||
|
this.setState({
|
||||||
|
audits: null
|
||||||
|
});
|
||||||
|
|
||||||
|
AsyncClient.getServerAudits();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
var content = null;
|
||||||
|
|
||||||
|
if (global.window.mm_license.IsLicensed !== 'true') {
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.audits === null) {
|
||||||
|
content = <LoadingScreen />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<div style={{margin: '10px'}}>
|
||||||
|
<AuditTable
|
||||||
|
audits={this.state.audits}
|
||||||
|
oneLine={true}
|
||||||
|
showUserId={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='panel'>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.audits.title'
|
||||||
|
defaultMessage='Server Audits'
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
className='btn btn-primary'
|
||||||
|
onClick={this.reload}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.audits.reload'
|
||||||
|
defaultMessage='Reload'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className='log__panel'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
web/react/components/admin_console/doughnut_chart.jsx
Обычный файл
77
web/react/components/admin_console/doughnut_chart.jsx
Обычный файл
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
export default class DoughnutChart 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.refs.canvas);
|
||||||
|
var ctx = el.getContext('2d');
|
||||||
|
this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let content;
|
||||||
|
if (this.props.data == null) {
|
||||||
|
content = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.analytics.loading'
|
||||||
|
defaultMessage='Loading...'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<canvas
|
||||||
|
ref='canvas'
|
||||||
|
width={this.props.width}
|
||||||
|
height={this.props.height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='col-sm-6'>
|
||||||
|
<div className='total-count'>
|
||||||
|
<div className='title'>
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
<div className='content'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DoughnutChart.propTypes = {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
width: React.PropTypes.string,
|
||||||
|
height: React.PropTypes.string,
|
||||||
|
data: React.PropTypes.array,
|
||||||
|
options: React.PropTypes.object
|
||||||
|
};
|
||||||
@@ -112,6 +112,8 @@ class EmailSettings extends React.Component {
|
|||||||
buildConfig() {
|
buildConfig() {
|
||||||
var config = this.props.config;
|
var config = this.props.config;
|
||||||
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
|
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
|
||||||
|
config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
|
||||||
|
config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
|
||||||
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
|
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
|
||||||
config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
|
config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
|
||||||
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
|
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
|
||||||
@@ -317,6 +319,88 @@ class EmailSettings extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label
|
||||||
|
className='control-label col-sm-4'
|
||||||
|
htmlFor='allowSignInWithEmail'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.email.allowEmailSignInTitle'
|
||||||
|
defaultMessage='Allow Sign In With Email: '
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className='col-sm-8'>
|
||||||
|
<label className='radio-inline'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='allowSignInWithEmail'
|
||||||
|
value='true'
|
||||||
|
ref='allowSignInWithEmail'
|
||||||
|
defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail}
|
||||||
|
onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')}
|
||||||
|
/>
|
||||||
|
{'true'}
|
||||||
|
</label>
|
||||||
|
<label className='radio-inline'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='allowSignInWithEmail'
|
||||||
|
value='false'
|
||||||
|
defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail}
|
||||||
|
onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')}
|
||||||
|
/>
|
||||||
|
{'false'}
|
||||||
|
</label>
|
||||||
|
<p className='help-text'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.email.allowEmailSignInDescription'
|
||||||
|
defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label
|
||||||
|
className='control-label col-sm-4'
|
||||||
|
htmlFor='allowSignInWithUsername'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.email.allowUsernameSignInTitle'
|
||||||
|
defaultMessage='Allow Sign In With Username: '
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className='col-sm-8'>
|
||||||
|
<label className='radio-inline'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='allowSignInWithUsername'
|
||||||
|
value='true'
|
||||||
|
ref='allowSignInWithUsername'
|
||||||
|
defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername}
|
||||||
|
onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')}
|
||||||
|
/>
|
||||||
|
{'true'}
|
||||||
|
</label>
|
||||||
|
<label className='radio-inline'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='allowSignInWithUsername'
|
||||||
|
value='false'
|
||||||
|
defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername}
|
||||||
|
onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')}
|
||||||
|
/>
|
||||||
|
{'false'}
|
||||||
|
</label>
|
||||||
|
<p className='help-text'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.email.allowUsernameSignInDescription'
|
||||||
|
defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<label
|
<label
|
||||||
className='control-label col-sm-4'
|
className='control-label col-sm-4'
|
||||||
|
|||||||
37
web/react/components/admin_console/statistic_count.jsx
Обычный файл
37
web/react/components/admin_console/statistic_count.jsx
Обычный файл
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
export default class StatisticCount extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let loading = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.analytics.loading'
|
||||||
|
defaultMessage='Loading...'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='col-sm-3'>
|
||||||
|
<div className='total-count'>
|
||||||
|
<div className='title'>
|
||||||
|
{this.props.title}
|
||||||
|
<i className={'fa ' + this.props.icon}/>
|
||||||
|
</div>
|
||||||
|
<div className='content'>{this.props.count == null ? loading : this.props.count}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticCount.propTypes = {
|
||||||
|
title: React.PropTypes.string.isRequired,
|
||||||
|
icon: React.PropTypes.string.isRequired,
|
||||||
|
count: React.PropTypes.number
|
||||||
|
};
|
||||||
@@ -140,6 +140,34 @@ class SystemAnalytics extends React.Component {
|
|||||||
this.setState({serverError: err.message});
|
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() {
|
componentWillReceiveProps() {
|
||||||
@@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Analytics
|
<Analytics
|
||||||
|
intl={this.props.intl}
|
||||||
title={this.props.intl.formatMessage(labels.title)}
|
title={this.props.intl.formatMessage(labels.title)}
|
||||||
channelOpenCount={this.state.channel_open_count}
|
channelOpenCount={this.state.channel_open_count}
|
||||||
channelPrivateCount={this.state.channel_private_count}
|
channelPrivateCount={this.state.channel_private_count}
|
||||||
postCount={this.state.post_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}
|
postCountsDay={this.state.post_counts_day}
|
||||||
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
|
userCountsWithPostsDay={this.state.user_counts_with_posts_day}
|
||||||
uniqueUserCount={this.state.unique_user_count}
|
uniqueUserCount={this.state.unique_user_count}
|
||||||
@@ -179,4 +213,4 @@ SystemAnalytics.propTypes = {
|
|||||||
team: React.PropTypes.object
|
team: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SystemAnalytics);
|
export default injectIntl(SystemAnalytics);
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class TeamAnalytics extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Analytics
|
<Analytics
|
||||||
|
intl={this.props.intl}
|
||||||
title={this.props.team.name}
|
title={this.props.team.name}
|
||||||
users={this.state.users}
|
users={this.state.users}
|
||||||
channelOpenCount={this.state.channel_open_count}
|
channelOpenCount={this.state.channel_open_count}
|
||||||
@@ -249,4 +250,4 @@ TeamAnalytics.propTypes = {
|
|||||||
team: React.PropTypes.object
|
team: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(TeamAnalytics);
|
export default injectIntl(TeamAnalytics);
|
||||||
|
|||||||
571
web/react/components/audit_table.jsx
Обычный файл
571
web/react/components/audit_table.jsx
Обычный файл
@@ -0,0 +1,571 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
const holders = defineMessages({
|
||||||
|
sessionRevoked: {
|
||||||
|
id: 'audit_table.sessionRevoked',
|
||||||
|
defaultMessage: 'The session with id {sessionId} was revoked'
|
||||||
|
},
|
||||||
|
channelCreated: {
|
||||||
|
id: 'audit_table.channelCreated',
|
||||||
|
defaultMessage: 'Created the {channelName} channel/group'
|
||||||
|
},
|
||||||
|
establishedDM: {
|
||||||
|
id: 'audit_table.establishedDM',
|
||||||
|
defaultMessage: 'Established a direct message channel with {username}'
|
||||||
|
},
|
||||||
|
nameUpdated: {
|
||||||
|
id: 'audit_table.nameUpdated',
|
||||||
|
defaultMessage: 'Updated the {channelName} channel/group name'
|
||||||
|
},
|
||||||
|
headerUpdated: {
|
||||||
|
id: 'audit_table.headerUpdated',
|
||||||
|
defaultMessage: 'Updated the {channelName} channel/group header'
|
||||||
|
},
|
||||||
|
channelDeleted: {
|
||||||
|
id: 'audit_table.channelDeleted',
|
||||||
|
defaultMessage: 'Deleted the channel/group with the URL {url}'
|
||||||
|
},
|
||||||
|
userAdded: {
|
||||||
|
id: 'audit_table.userAdded',
|
||||||
|
defaultMessage: 'Added {username} to the {channelName} channel/group'
|
||||||
|
},
|
||||||
|
userRemoved: {
|
||||||
|
id: 'audit_table.userRemoved',
|
||||||
|
defaultMessage: 'Removed {username} to the {channelName} channel/group'
|
||||||
|
},
|
||||||
|
attemptedRegisterApp: {
|
||||||
|
id: 'audit_table.attemptedRegisterApp',
|
||||||
|
defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
|
||||||
|
},
|
||||||
|
attemptedAllowOAuthAccess: {
|
||||||
|
id: 'audit_table.attemptedAllowOAuthAccess',
|
||||||
|
defaultMessage: 'Attempted to allow a new OAuth service access'
|
||||||
|
},
|
||||||
|
successfullOAuthAccess: {
|
||||||
|
id: 'audit_table.successfullOAuthAccess',
|
||||||
|
defaultMessage: 'Successfully gave a new OAuth service access'
|
||||||
|
},
|
||||||
|
failedOAuthAccess: {
|
||||||
|
id: 'audit_table.failedOAuthAccess',
|
||||||
|
defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
|
||||||
|
},
|
||||||
|
attemptedOAuthToken: {
|
||||||
|
id: 'audit_table.attemptedOAuthToken',
|
||||||
|
defaultMessage: 'Attempted to get an OAuth access token'
|
||||||
|
},
|
||||||
|
successfullOAuthToken: {
|
||||||
|
id: 'audit_table.successfullOAuthToken',
|
||||||
|
defaultMessage: 'Successfully added a new OAuth service'
|
||||||
|
},
|
||||||
|
oauthTokenFailed: {
|
||||||
|
id: 'audit_table.oauthTokenFailed',
|
||||||
|
defaultMessage: 'Failed to get an OAuth access token - {token}'
|
||||||
|
},
|
||||||
|
attemptedLogin: {
|
||||||
|
id: 'audit_table.attemptedLogin',
|
||||||
|
defaultMessage: 'Attempted to login'
|
||||||
|
},
|
||||||
|
successfullLogin: {
|
||||||
|
id: 'audit_table.successfullLogin',
|
||||||
|
defaultMessage: 'Successfully logged in'
|
||||||
|
},
|
||||||
|
failedLogin: {
|
||||||
|
id: 'audit_table.failedLogin',
|
||||||
|
defaultMessage: 'FAILED login attempt'
|
||||||
|
},
|
||||||
|
updatePicture: {
|
||||||
|
id: 'audit_table.updatePicture',
|
||||||
|
defaultMessage: 'Updated your profile picture'
|
||||||
|
},
|
||||||
|
updateGeneral: {
|
||||||
|
id: 'audit_table.updateGeneral',
|
||||||
|
defaultMessage: 'Updated the general settings of your account'
|
||||||
|
},
|
||||||
|
attemptedPassword: {
|
||||||
|
id: 'audit_table.attemptedPassword',
|
||||||
|
defaultMessage: 'Attempted to change password'
|
||||||
|
},
|
||||||
|
successfullPassword: {
|
||||||
|
id: 'audit_table.successfullPassword',
|
||||||
|
defaultMessage: 'Successfully changed password'
|
||||||
|
},
|
||||||
|
failedPassword: {
|
||||||
|
id: 'audit_table.failedPassword',
|
||||||
|
defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
|
||||||
|
},
|
||||||
|
updatedRol: {
|
||||||
|
id: 'audit_table.updatedRol',
|
||||||
|
defaultMessage: 'Updated user role(s) to '
|
||||||
|
},
|
||||||
|
member: {
|
||||||
|
id: 'audit_table.member',
|
||||||
|
defaultMessage: 'member'
|
||||||
|
},
|
||||||
|
accountActive: {
|
||||||
|
id: 'audit_table.accountActive',
|
||||||
|
defaultMessage: 'Account made active'
|
||||||
|
},
|
||||||
|
accountInactive: {
|
||||||
|
id: 'audit_table.accountInactive',
|
||||||
|
defaultMessage: 'Account made inactive'
|
||||||
|
},
|
||||||
|
by: {
|
||||||
|
id: 'audit_table.by',
|
||||||
|
defaultMessage: ' by {username}'
|
||||||
|
},
|
||||||
|
byAdmin: {
|
||||||
|
id: 'audit_table.byAdmin',
|
||||||
|
defaultMessage: ' by an admin'
|
||||||
|
},
|
||||||
|
sentEmail: {
|
||||||
|
id: 'audit_table.sentEmail',
|
||||||
|
defaultMessage: 'Sent an email to {email} to reset your password'
|
||||||
|
},
|
||||||
|
attemptedReset: {
|
||||||
|
id: 'audit_table.attemptedReset',
|
||||||
|
defaultMessage: 'Attempted to reset password'
|
||||||
|
},
|
||||||
|
successfullReset: {
|
||||||
|
id: 'audit_table.successfullReset',
|
||||||
|
defaultMessage: 'Successfully reset password'
|
||||||
|
},
|
||||||
|
updateGlobalNotifications: {
|
||||||
|
id: 'audit_table.updateGlobalNotifications',
|
||||||
|
defaultMessage: 'Updated your global notification settings'
|
||||||
|
},
|
||||||
|
attemptedWebhookCreate: {
|
||||||
|
id: 'audit_table.attemptedWebhookCreate',
|
||||||
|
defaultMessage: 'Attempted to create a webhook'
|
||||||
|
},
|
||||||
|
succcessfullWebhookCreate: {
|
||||||
|
id: 'audit_table.successfullWebhookCreate',
|
||||||
|
defaultMessage: 'Successfully created a webhook'
|
||||||
|
},
|
||||||
|
failedWebhookCreate: {
|
||||||
|
id: 'audit_table.failedWebhookCreate',
|
||||||
|
defaultMessage: 'Failed to create a webhook - bad channel permissions'
|
||||||
|
},
|
||||||
|
attemptedWebhookDelete: {
|
||||||
|
id: 'audit_table.attemptedWebhookDelete',
|
||||||
|
defaultMessage: 'Attempted to delete a webhook'
|
||||||
|
},
|
||||||
|
successfullWebhookDelete: {
|
||||||
|
id: 'audit_table.successfullWebhookDelete',
|
||||||
|
defaultMessage: 'Successfully deleted a webhook'
|
||||||
|
},
|
||||||
|
failedWebhookDelete: {
|
||||||
|
id: 'audit_table.failedWebhookDelete',
|
||||||
|
defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
|
||||||
|
},
|
||||||
|
logout: {
|
||||||
|
id: 'audit_table.logout',
|
||||||
|
defaultMessage: 'Logged out of your account'
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
id: 'audit_table.verified',
|
||||||
|
defaultMessage: 'Sucessfully verified your email address'
|
||||||
|
},
|
||||||
|
revokedAll: {
|
||||||
|
id: 'audit_table.revokedAll',
|
||||||
|
defaultMessage: 'Revoked all current sessions for the team'
|
||||||
|
},
|
||||||
|
loginAttempt: {
|
||||||
|
id: 'audit_table.loginAttempt',
|
||||||
|
defaultMessage: ' (Login attempt)'
|
||||||
|
},
|
||||||
|
loginFailure: {
|
||||||
|
id: 'audit_table.loginFailure',
|
||||||
|
defaultMessage: ' (Login failure)'
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
id: 'audit_table.userId',
|
||||||
|
defaultMessage: 'User ID'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class AuditTable extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleMoreInfo = this.handleMoreInfo.bind(this);
|
||||||
|
this.formatAuditInfo = this.formatAuditInfo.bind(this);
|
||||||
|
this.handleRevokedSession = this.handleRevokedSession.bind(this);
|
||||||
|
|
||||||
|
this.state = {moreInfo: []};
|
||||||
|
}
|
||||||
|
handleMoreInfo(index) {
|
||||||
|
var newMoreInfo = this.state.moreInfo;
|
||||||
|
newMoreInfo[index] = true;
|
||||||
|
this.setState({moreInfo: newMoreInfo});
|
||||||
|
}
|
||||||
|
handleRevokedSession(sessionId) {
|
||||||
|
return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
|
||||||
|
}
|
||||||
|
formatAuditInfo(currentAudit) {
|
||||||
|
const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
|
||||||
|
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
let currentAuditDesc = '';
|
||||||
|
|
||||||
|
if (currentActionURL.indexOf('/channels') === 0) {
|
||||||
|
const channelInfo = currentAudit.extra_info.split(' ');
|
||||||
|
const channelNameField = channelInfo[0].split('=');
|
||||||
|
|
||||||
|
let channelURL = '';
|
||||||
|
let channelObj;
|
||||||
|
let channelName = '';
|
||||||
|
if (channelNameField.indexOf('name') >= 0) {
|
||||||
|
channelURL = channelNameField[channelNameField.indexOf('name') + 1];
|
||||||
|
channelObj = ChannelStore.getByName(channelURL);
|
||||||
|
if (channelObj) {
|
||||||
|
channelName = channelObj.display_name;
|
||||||
|
} else {
|
||||||
|
channelName = channelURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (currentActionURL) {
|
||||||
|
case '/channels/create':
|
||||||
|
currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
|
||||||
|
break;
|
||||||
|
case '/channels/create_direct':
|
||||||
|
currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
|
||||||
|
break;
|
||||||
|
case '/channels/update':
|
||||||
|
currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
|
||||||
|
break;
|
||||||
|
case '/channels/update_desc': // support the old path
|
||||||
|
case '/channels/update_header':
|
||||||
|
currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
let userIdField = [];
|
||||||
|
let userId = '';
|
||||||
|
let username = '';
|
||||||
|
|
||||||
|
if (channelInfo[1]) {
|
||||||
|
userIdField = channelInfo[1].split('=');
|
||||||
|
|
||||||
|
if (userIdField.indexOf('user_id') >= 0) {
|
||||||
|
userId = userIdField[userIdField.indexOf('user_id') + 1];
|
||||||
|
username = UserStore.getProfile(userId).username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
|
||||||
|
currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
|
||||||
|
} else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
|
||||||
|
currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
|
||||||
|
} else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
|
||||||
|
currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentActionURL.indexOf('/oauth') === 0) {
|
||||||
|
const oauthInfo = currentAudit.extra_info.split(' ');
|
||||||
|
|
||||||
|
switch (currentActionURL) {
|
||||||
|
case '/oauth/register': {
|
||||||
|
const clientIdField = oauthInfo[0].split('=');
|
||||||
|
|
||||||
|
if (clientIdField[0] === 'client_id') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '/oauth/allow':
|
||||||
|
if (oauthInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
|
||||||
|
} else if (oauthInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
|
||||||
|
} else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
|
||||||
|
currentAuditDesc = formatMessage(holders.failedOAuthAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case '/oauth/access_token':
|
||||||
|
if (oauthInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
|
||||||
|
} else if (oauthInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullOAuthToken);
|
||||||
|
} else {
|
||||||
|
const oauthTokenFailure = oauthInfo[0].split('-');
|
||||||
|
|
||||||
|
if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
|
||||||
|
currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentActionURL.indexOf('/users') === 0) {
|
||||||
|
const userInfo = currentAudit.extra_info.split(' ');
|
||||||
|
|
||||||
|
switch (currentActionURL) {
|
||||||
|
case '/users/login':
|
||||||
|
if (userInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedLogin);
|
||||||
|
} else if (userInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullLogin);
|
||||||
|
} else if (userInfo[0]) {
|
||||||
|
currentAuditDesc = formatMessage(holders.failedLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case '/users/revoke_session':
|
||||||
|
currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
|
||||||
|
break;
|
||||||
|
case '/users/newimage':
|
||||||
|
currentAuditDesc = formatMessage(holders.updatePicture);
|
||||||
|
break;
|
||||||
|
case '/users/update':
|
||||||
|
currentAuditDesc = formatMessage(holders.updateGeneral);
|
||||||
|
break;
|
||||||
|
case '/users/newpassword':
|
||||||
|
if (userInfo[0] === 'attempted') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedPassword);
|
||||||
|
} else if (userInfo[0] === 'completed') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullPassword);
|
||||||
|
} else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
|
||||||
|
currentAuditDesc = formatMessage(holders.failedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case '/users/update_roles': {
|
||||||
|
const userRoles = userInfo[0].split('=')[1];
|
||||||
|
|
||||||
|
currentAuditDesc = formatMessage(holders.updatedRol);
|
||||||
|
if (userRoles.trim()) {
|
||||||
|
currentAuditDesc += userRoles;
|
||||||
|
} else {
|
||||||
|
currentAuditDesc += formatMessage(holders.member);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '/users/update_active': {
|
||||||
|
const updateType = userInfo[0].split('=')[0];
|
||||||
|
const updateField = userInfo[0].split('=')[1];
|
||||||
|
|
||||||
|
/* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
|
||||||
|
if (updateType === 'active') {
|
||||||
|
if (updateField === 'true') {
|
||||||
|
currentAuditDesc = formatMessage(holders.accountActive);
|
||||||
|
} else if (updateField === 'false') {
|
||||||
|
currentAuditDesc = formatMessage(holders.accountInactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actingUserInfo = userInfo[1].split('=');
|
||||||
|
if (actingUserInfo[0] === 'session_user') {
|
||||||
|
const actingUser = UserStore.getProfile(actingUserInfo[1]);
|
||||||
|
const currentUser = UserStore.getCurrentUser();
|
||||||
|
if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
|
||||||
|
currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
|
||||||
|
} else if (currentUser && actingUser) {
|
||||||
|
currentAuditDesc += formatMessage(holders.byAdmin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (updateType === 'session_id') {
|
||||||
|
currentAuditDesc = this.handleRevokedSession(updateField);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '/users/send_password_reset':
|
||||||
|
currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
|
||||||
|
break;
|
||||||
|
case '/users/reset_password':
|
||||||
|
if (userInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedReset);
|
||||||
|
} else if (userInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case '/users/update_notify':
|
||||||
|
currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (currentActionURL.indexOf('/hooks') === 0) {
|
||||||
|
const webhookInfo = currentAudit.extra_info.split(' ');
|
||||||
|
|
||||||
|
switch (currentActionURL) {
|
||||||
|
case '/hooks/incoming/create':
|
||||||
|
if (webhookInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
|
||||||
|
} else if (webhookInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
|
||||||
|
} else if (webhookInfo[0] === 'fail - bad channel permissions') {
|
||||||
|
currentAuditDesc = formatMessage(holders.failedWebhookCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case '/hooks/incoming/delete':
|
||||||
|
if (webhookInfo[0] === 'attempt') {
|
||||||
|
currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
|
||||||
|
} else if (webhookInfo[0] === 'success') {
|
||||||
|
currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
|
||||||
|
} else if (webhookInfo[0] === 'fail - inappropriate conditions') {
|
||||||
|
currentAuditDesc = formatMessage(holders.failedWebhookDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (currentActionURL) {
|
||||||
|
case '/logout':
|
||||||
|
currentAuditDesc = formatMessage(holders.logout);
|
||||||
|
break;
|
||||||
|
case '/verify_email':
|
||||||
|
currentAuditDesc = formatMessage(holders.verified);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If all else fails... */
|
||||||
|
if (!currentAuditDesc) {
|
||||||
|
/* Currently not called anywhere */
|
||||||
|
if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
|
||||||
|
currentAuditDesc = formatMessage(holders.revokedAll);
|
||||||
|
} else {
|
||||||
|
let currentActionDesc = '';
|
||||||
|
if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
|
||||||
|
currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
|
||||||
|
currentActionDesc = Utils.toTitleCase(currentActionDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentExtraInfoDesc = '';
|
||||||
|
if (currentAudit.extra_info) {
|
||||||
|
currentExtraInfoDesc = currentAudit.extra_info;
|
||||||
|
|
||||||
|
if (currentExtraInfoDesc.indexOf('=') !== -1) {
|
||||||
|
currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = new Date(currentAudit.create_at);
|
||||||
|
let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
|
||||||
|
|
||||||
|
if (this.props.showUserId) {
|
||||||
|
currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAuditInfo += ' | ' + currentAuditDesc;
|
||||||
|
|
||||||
|
return currentAuditInfo;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
var accessList = [];
|
||||||
|
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
for (var i = 0; i < this.props.audits.length; i++) {
|
||||||
|
const currentAudit = this.props.audits[i];
|
||||||
|
const currentAuditInfo = this.formatAuditInfo(currentAudit);
|
||||||
|
|
||||||
|
let moreInfo;
|
||||||
|
if (!this.props.oneLine) {
|
||||||
|
moreInfo = (
|
||||||
|
<a
|
||||||
|
href='#'
|
||||||
|
className='theme'
|
||||||
|
onClick={this.handleMoreInfo.bind(this, i)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='audit_table.moreInfo'
|
||||||
|
defaultMessage='More info'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.moreInfo[i]) {
|
||||||
|
if (!currentAudit.session_id) {
|
||||||
|
currentAudit.session_id = 'N/A';
|
||||||
|
|
||||||
|
if (currentAudit.action.search('/users/login') >= 0) {
|
||||||
|
if (currentAudit.extra_info === 'attempt') {
|
||||||
|
currentAudit.session_id += formatMessage(holders.loginAttempt);
|
||||||
|
} else {
|
||||||
|
currentAudit.session_id += formatMessage(holders.loginFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moreInfo = (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<FormattedMessage
|
||||||
|
id='audit_table.ip'
|
||||||
|
defaultMessage='IP: {ip}'
|
||||||
|
values={{
|
||||||
|
ip: currentAudit.ip_address
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormattedMessage
|
||||||
|
id='audit_table.session'
|
||||||
|
defaultMessage='Session ID: {id}'
|
||||||
|
values={{
|
||||||
|
id: currentAudit.session_id
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var divider = null;
|
||||||
|
if (i < this.props.audits.length - 1) {
|
||||||
|
divider = (<div className='divider-light'></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
accessList[i] = (
|
||||||
|
<div
|
||||||
|
key={'accessHistoryEntryKey' + i}
|
||||||
|
className='access-history__table'
|
||||||
|
>
|
||||||
|
<div className='access__report'>
|
||||||
|
<div className='report__time'>{currentAuditInfo}</div>
|
||||||
|
<div className='report__info'>
|
||||||
|
{moreInfo}
|
||||||
|
</div>
|
||||||
|
{divider}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form role='form'>{accessList}</form>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditTable.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
audits: React.PropTypes.array.isRequired,
|
||||||
|
oneLine: React.PropTypes.bool,
|
||||||
|
showUserId: React.PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(AuditTable);
|
||||||
@@ -69,7 +69,7 @@ export default class CenterPanel extends React.Component {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<a href=''>
|
<a href=''>
|
||||||
{'You are viewing the Archives. Click here to jump to recent messages. '}
|
{'Click here to jump to recent messages. '}
|
||||||
{<i className='fa fa-arrow-down'></i>}
|
{<i className='fa fa-arrow-down'></i>}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import * as TextFormatting from '../utils/text_formatting.jsx';
|
|||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
const ActionTypes = Constants.ActionTypes;
|
||||||
const Popover = ReactBootstrap.Popover;
|
const Popover = ReactBootstrap.Popover;
|
||||||
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||||
const Tooltip = ReactBootstrap.Tooltip;
|
const Tooltip = ReactBootstrap.Tooltip;
|
||||||
@@ -124,7 +126,14 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const channel = this.state.channel;
|
const channel = this.state.channel;
|
||||||
const recentMentionsTooltip = <Tooltip id='recentMentionsTooltip'>{'Recent Mentions'}</Tooltip>;
|
const recentMentionsTooltip = (
|
||||||
|
<Tooltip id='recentMentionsTooltip'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.recentMentions'
|
||||||
|
defaultMessage='Recent Mentions'
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
const popoverContent = (
|
const popoverContent = (
|
||||||
<Popover
|
<Popover
|
||||||
id='hader-popover'
|
id='hader-popover'
|
||||||
@@ -157,9 +166,19 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let channelTerm = 'Channel';
|
let channelTerm = (
|
||||||
if (channel.type === 'P') {
|
<FormattedMessage
|
||||||
channelTerm = 'Group';
|
id='channel_header.channel'
|
||||||
|
defaultMessage='Channel'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||||
|
channelTerm = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.group'
|
||||||
|
defaultMessage='Group'
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdownContents = [];
|
const dropdownContents = [];
|
||||||
@@ -174,7 +193,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={EditChannelHeaderModal}
|
dialogType={EditChannelHeaderModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{'Set Channel Header...'}
|
<FormattedMessage
|
||||||
|
id='channel_header.channelHeader'
|
||||||
|
defaultMessage='Set Channel Header...'
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -189,7 +211,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={ChannelInfoModal}
|
dialogType={ChannelInfoModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{'View Info'}
|
<FormattedMessage
|
||||||
|
id='channel_header.viewInfo'
|
||||||
|
defaultMessage='View Info'
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -205,7 +230,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={ChannelInviteModal}
|
dialogType={ChannelInviteModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{'Add Members'}
|
<FormattedMessage
|
||||||
|
id='chanel_header.addMembers'
|
||||||
|
defaultMessage='Add Members'
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -221,7 +249,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
href='#'
|
href='#'
|
||||||
onClick={() => this.setState({showMembersModal: true})}
|
onClick={() => this.setState({showMembersModal: true})}
|
||||||
>
|
>
|
||||||
{'Manage Members'}
|
<FormattedMessage
|
||||||
|
id='channel_header.manageMembers'
|
||||||
|
defaultMessage='Manage Members'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -238,7 +269,13 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={EditChannelHeaderModal}
|
dialogType={EditChannelHeaderModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{`Set ${channelTerm} Header...`}
|
<FormattedMessage
|
||||||
|
id='channel_header.setHeader'
|
||||||
|
defaultMessage='Set {term} Header...'
|
||||||
|
values={{
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -252,7 +289,13 @@ export default class ChannelHeader extends React.Component {
|
|||||||
href='#'
|
href='#'
|
||||||
onClick={() => this.setState({showEditChannelPurposeModal: true})}
|
onClick={() => this.setState({showEditChannelPurposeModal: true})}
|
||||||
>
|
>
|
||||||
{'Set '}{channelTerm}{' Purpose...'}
|
<FormattedMessage
|
||||||
|
id='channel_header.setPurpose'
|
||||||
|
defaultMessage='Set {term} Purpose...'
|
||||||
|
values={{
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -266,7 +309,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={ChannelNotificationsModal}
|
dialogType={ChannelNotificationsModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{'Notification Preferences'}
|
<FormattedMessage
|
||||||
|
id='channel_header.notificationPreferences'
|
||||||
|
defaultMessage='Notification Preferences'
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -286,7 +332,13 @@ export default class ChannelHeader extends React.Component {
|
|||||||
data-name={channel.name}
|
data-name={channel.name}
|
||||||
data-channelid={channel.id}
|
data-channelid={channel.id}
|
||||||
>
|
>
|
||||||
{'Rename '}{channelTerm}{'...'}
|
<FormattedMessage
|
||||||
|
id='channel_header.rename'
|
||||||
|
defaultMessage='Rename {term}...'
|
||||||
|
values={{
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -302,7 +354,13 @@ export default class ChannelHeader extends React.Component {
|
|||||||
dialogType={DeleteChannelModal}
|
dialogType={DeleteChannelModal}
|
||||||
dialogProps={{channel}}
|
dialogProps={{channel}}
|
||||||
>
|
>
|
||||||
{'Delete '}{channelTerm}{'...'}
|
<FormattedMessage
|
||||||
|
id='channel_header.delete'
|
||||||
|
defaultMessage='Delete {term}...'
|
||||||
|
values={{
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -320,7 +378,13 @@ export default class ChannelHeader extends React.Component {
|
|||||||
href='#'
|
href='#'
|
||||||
onClick={this.handleLeave}
|
onClick={this.handleLeave}
|
||||||
>
|
>
|
||||||
{'Leave '}{channelTerm}
|
<FormattedMessage
|
||||||
|
id='channel_header.leave'
|
||||||
|
defaultMessage='Leave {term}'
|
||||||
|
values={{
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,17 +2,28 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class ChannelInfoModal extends React.Component {
|
const holders = defineMessages({
|
||||||
|
notFound: {
|
||||||
|
id: 'channel_info.notFound',
|
||||||
|
defaultMessage: 'No Channel Found'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class ChannelInfoModal extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
let channel = this.props.channel;
|
let channel = this.props.channel;
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
channel = {
|
channel = {
|
||||||
display_name: 'No Channel Found',
|
display_name: formatMessage(holders.notFound),
|
||||||
name: 'No Channel Found',
|
name: formatMessage(holders.notFound),
|
||||||
purpose: 'No Channel Found',
|
purpose: formatMessage(holders.notFound),
|
||||||
id: 'No Channel Found'
|
id: formatMessage(holders.notFound)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,19 +39,39 @@ export default class ChannelInfoModal extends React.Component {
|
|||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body ref='modalBody'>
|
<Modal.Body ref='modalBody'>
|
||||||
<div className='row form-group'>
|
<div className='row form-group'>
|
||||||
<div className='col-sm-3 info__label'>{'Channel Name:'}</div>
|
<div className='col-sm-3 info__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_info.name'
|
||||||
|
defaultMessage='Channel Name:'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className='col-sm-9'>{channel.display_name}</div>
|
<div className='col-sm-9'>{channel.display_name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row form-group'>
|
<div className='row form-group'>
|
||||||
<div className='col-sm-3 info__label'>{'Channel URL:'}</div>
|
<div className='col-sm-3 info__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_info.url'
|
||||||
|
defaultMessage='Channel URL:'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className='col-sm-9'>{channelURL}</div>
|
<div className='col-sm-9'>{channelURL}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='col-sm-3 info__label'>{'Channel ID:'}</div>
|
<div className='col-sm-3 info__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_info.id'
|
||||||
|
defaultMessage='Channel ID:'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className='col-sm-9'>{channel.id}</div>
|
<div className='col-sm-9'>{channel.id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='col-sm-3 info__label'>{'Channel Purpose:'}</div>
|
<div className='col-sm-3 info__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_info.purpose'
|
||||||
|
defaultMessage='Channel Purpose:'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className='col-sm-9'>{channel.purpose}</div>
|
<div className='col-sm-9'>{channel.purpose}</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
@@ -50,7 +81,10 @@ export default class ChannelInfoModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.props.onHide}
|
onClick={this.props.onHide}
|
||||||
>
|
>
|
||||||
{'Close'}
|
<FormattedMessage
|
||||||
|
id='channel_info.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -59,7 +93,10 @@ export default class ChannelInfoModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelInfoModal.propTypes = {
|
ChannelInfoModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
show: React.PropTypes.bool.isRequired,
|
show: React.PropTypes.bool.isRequired,
|
||||||
onHide: React.PropTypes.func.isRequired,
|
onHide: React.PropTypes.func.isRequired,
|
||||||
channel: React.PropTypes.object.isRequired
|
channel: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ChannelInfoModal);
|
||||||
@@ -11,6 +11,8 @@ import * as Utils from '../utils/utils.jsx';
|
|||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class ChannelInviteModal extends React.Component {
|
export default class ChannelInviteModal extends React.Component {
|
||||||
@@ -154,7 +156,13 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
onHide={this.props.onHide}
|
onHide={this.props.onHide}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title>{'Add New Members to '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_invite.addNewMembers'
|
||||||
|
defaultMessage='Add New Members to '
|
||||||
|
/>
|
||||||
|
<span className='name'>{this.props.channel.display_name}</span>
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body
|
<Modal.Body
|
||||||
ref='modalBody'
|
ref='modalBody'
|
||||||
@@ -168,7 +176,10 @@ export default class ChannelInviteModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.props.onHide}
|
onClick={this.props.onHide}
|
||||||
>
|
>
|
||||||
{'Close'}
|
<FormattedMessage
|
||||||
|
id='channel_invite.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import * as AsyncClient from '../utils/async_client.jsx';
|
|||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class ChannelMembersModal extends React.Component {
|
export default class ChannelMembersModal extends React.Component {
|
||||||
@@ -191,7 +193,13 @@ export default class ChannelMembersModal extends React.Component {
|
|||||||
onHide={this.props.onModalDismissed}
|
onHide={this.props.onModalDismissed}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title><span className='name'>{this.props.channel.display_name}</span>{' Members'}</Modal.Title>
|
<Modal.Title>
|
||||||
|
<span className='name'>{this.props.channel.display_name}</span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_memebers_modal.members'
|
||||||
|
defaultMessage=' Members'
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
<a
|
<a
|
||||||
className='btn btn-md btn-primary'
|
className='btn btn-md btn-primary'
|
||||||
href='#'
|
href='#'
|
||||||
@@ -200,7 +208,11 @@ export default class ChannelMembersModal extends React.Component {
|
|||||||
this.props.onModalDismissed();
|
this.props.onModalDismissed();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className='glyphicon glyphicon-envelope'/>{' Add New Members'}
|
<i className='glyphicon glyphicon-envelope'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_members_modal.addNew'
|
||||||
|
defaultMessage=' Add New Members'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body
|
<Modal.Body
|
||||||
@@ -215,7 +227,10 @@ export default class ChannelMembersModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.props.onModalDismissed}
|
onClick={this.props.onModalDismissed}
|
||||||
>
|
>
|
||||||
{'Close'}
|
<FormattedMessage
|
||||||
|
id='channel_members_modal.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import * as Client from '../utils/client.jsx';
|
|||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../stores/user_store.jsx';
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
export default class ChannelNotificationsModal extends React.Component {
|
export default class ChannelNotificationsModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -97,13 +99,35 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
|
|
||||||
let globalNotifyLevelName;
|
let globalNotifyLevelName;
|
||||||
if (globalNotifyLevel === 'all') {
|
if (globalNotifyLevel === 'all') {
|
||||||
globalNotifyLevelName = 'For all activity';
|
globalNotifyLevelName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.allActivity'
|
||||||
|
defaultMessage='For all activity'
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (globalNotifyLevel === 'mention') {
|
} else if (globalNotifyLevel === 'mention') {
|
||||||
globalNotifyLevelName = 'Only for mentions';
|
globalNotifyLevelName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.onlyMentions'
|
||||||
|
defaultMessage='Only for mentions'
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
globalNotifyLevelName = 'Never';
|
globalNotifyLevelName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.never'
|
||||||
|
defaultMessage='Never'
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendDesktop = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.sendDesktop'
|
||||||
|
defaultMessage='Send desktop notifications'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
if (this.state.activeSection === 'desktop') {
|
if (this.state.activeSection === 'desktop') {
|
||||||
var notifyActive = [false, false, false, false];
|
var notifyActive = [false, false, false, false];
|
||||||
if (this.state.notifyLevel === 'default') {
|
if (this.state.notifyLevel === 'default') {
|
||||||
@@ -127,7 +151,13 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={notifyActive[0]}
|
checked={notifyActive[0]}
|
||||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
|
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
|
||||||
/>
|
/>
|
||||||
{`Global default (${globalNotifyLevelName})`}
|
<FormattedMessage
|
||||||
|
id='channel_notifications.globalDefault'
|
||||||
|
defaultMessage='Global default ({notifyLevel}'
|
||||||
|
values={{
|
||||||
|
notifyLevel: (globalNotifyLevelName)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +168,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={notifyActive[1]}
|
checked={notifyActive[1]}
|
||||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
|
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
|
||||||
/>
|
/>
|
||||||
{'For all activity'}
|
<FormattedMessage id='channel_notifications.allActivity' />
|
||||||
</label>
|
</label>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +179,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={notifyActive[2]}
|
checked={notifyActive[2]}
|
||||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
|
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
|
||||||
/>
|
/>
|
||||||
{'Only for mentions'}
|
<FormattedMessage id='channel_notifications.onlyMentions' />
|
||||||
</label>
|
</label>
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +190,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={notifyActive[3]}
|
checked={notifyActive[3]}
|
||||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
|
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
|
||||||
/>
|
/>
|
||||||
{'Never'}
|
<FormattedMessage id='channel_notifications.never' />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,13 +204,16 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
|
|
||||||
const extraInfo = (
|
const extraInfo = (
|
||||||
<span>
|
<span>
|
||||||
{'Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'}
|
<FormattedMessage
|
||||||
|
id='channel_notifications.override'
|
||||||
|
defaultMessage='Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingItemMax
|
<SettingItemMax
|
||||||
title='Send desktop notifications'
|
title={sendDesktop}
|
||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
submit={this.handleSubmitNotifyLevel}
|
submit={this.handleSubmitNotifyLevel}
|
||||||
server_error={serverError}
|
server_error={serverError}
|
||||||
@@ -192,13 +225,20 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
|
|
||||||
var describe;
|
var describe;
|
||||||
if (this.state.notifyLevel === 'default') {
|
if (this.state.notifyLevel === 'default') {
|
||||||
describe = `Global default (${globalNotifyLevelName})`;
|
describe = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.globalDefault'
|
||||||
|
values={{
|
||||||
|
notifyLevel: (globalNotifyLevelName)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (this.state.notifyLevel === 'mention') {
|
} else if (this.state.notifyLevel === 'mention') {
|
||||||
describe = 'Only for mentions';
|
describe = (<FormattedMessage id='channel_notifications.onlyMentions' />);
|
||||||
} else if (this.state.notifyLevel === 'all') {
|
} else if (this.state.notifyLevel === 'all') {
|
||||||
describe = 'For all activity';
|
describe = (<FormattedMessage id='channel_notifications.allActivity' />);
|
||||||
} else {
|
} else {
|
||||||
describe = 'Never';
|
describe = (<FormattedMessage id='channel_notifications.never' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateSection = function updateSection(e) {
|
handleUpdateSection = function updateSection(e) {
|
||||||
@@ -208,7 +248,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingItemMin
|
<SettingItemMin
|
||||||
title='Send desktop notifications'
|
title={sendDesktop}
|
||||||
describe={describe}
|
describe={describe}
|
||||||
updateSection={handleUpdateSection}
|
updateSection={handleUpdateSection}
|
||||||
/>
|
/>
|
||||||
@@ -250,6 +290,12 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
createMarkUnreadLevelSection(serverError) {
|
createMarkUnreadLevelSection(serverError) {
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
|
const markUnread = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.markUnread'
|
||||||
|
defaultMessage='Mark Channel Unread'
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (this.state.activeSection === 'markUnreadLevel') {
|
if (this.state.activeSection === 'markUnreadLevel') {
|
||||||
const inputs = [(
|
const inputs = [(
|
||||||
<div key='channel-notification-unread-radio'>
|
<div key='channel-notification-unread-radio'>
|
||||||
@@ -260,7 +306,10 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={this.state.markUnreadLevel === 'all'}
|
checked={this.state.markUnreadLevel === 'all'}
|
||||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
||||||
/>
|
/>
|
||||||
{'For all unread messages'}
|
<FormattedMessage
|
||||||
|
id='channel_notifications.allUnread'
|
||||||
|
defaultMessage='For all unread messages'
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +320,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
checked={this.state.markUnreadLevel === 'mention'}
|
checked={this.state.markUnreadLevel === 'mention'}
|
||||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
||||||
/>
|
/>
|
||||||
{'Only for mentions'}
|
<FormattedMessage id='channel_notifications.onlyMentions' />
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -284,11 +333,18 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
const extraInfo = <span>{'The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'}</span>;
|
const extraInfo = (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.unreadInfo'
|
||||||
|
defaultMessage='The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
<SettingItemMax
|
<SettingItemMax
|
||||||
title='Mark Channel Unread'
|
title={markUnread}
|
||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
submit={this.handleSubmitMarkUnreadLevel}
|
submit={this.handleSubmitMarkUnreadLevel}
|
||||||
server_error={serverError}
|
server_error={serverError}
|
||||||
@@ -300,9 +356,14 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
let describe;
|
let describe;
|
||||||
|
|
||||||
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
|
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
|
||||||
describe = 'For all unread messages';
|
describe = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.allUnread'
|
||||||
|
defaultMessage='For all unread messages'
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
describe = 'Only for mentions';
|
describe = (<FormattedMessage id='channel_notifications.onlyMentions' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateSection = function handleUpdateSection(e) {
|
const handleUpdateSection = function handleUpdateSection(e) {
|
||||||
@@ -312,7 +373,7 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
|
|
||||||
content = (
|
content = (
|
||||||
<SettingItemMin
|
<SettingItemMin
|
||||||
title='Mark Channel Unread'
|
title={markUnread}
|
||||||
describe={describe}
|
describe={describe}
|
||||||
updateSection={handleUpdateSection}
|
updateSection={handleUpdateSection}
|
||||||
/>
|
/>
|
||||||
@@ -335,7 +396,13 @@ export default class ChannelNotificationsModal extends React.Component {
|
|||||||
onHide={this.props.onHide}
|
onHide={this.props.onHide}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title>{'Notification Preferences for '}<span className='name'>{this.props.channel.display_name}</span></Modal.Title>
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_notifications.preferences'
|
||||||
|
defaultMessage='Notification Preferences for '
|
||||||
|
/>
|
||||||
|
<span className='name'>{this.props.channel.display_name}</span>
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div className='settings-table'>
|
<div className='settings-table'>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import * as AsyncClient from '../utils/async_client.jsx';
|
|||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
import TeamStore from '../stores/team_store.jsx';
|
import TeamStore from '../stores/team_store.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
export default class DeleteChannelModal extends React.Component {
|
export default class DeleteChannelModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -32,7 +34,20 @@ export default class DeleteChannelModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const channelTerm = Utils.getChannelTerm(this.props.channel.type).toLowerCase();
|
let channelTerm = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_channel.channel'
|
||||||
|
defaultMessage='channel'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
|
||||||
|
channelTerm = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_channel.group'
|
||||||
|
defaultMessage='group'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -40,10 +55,22 @@ export default class DeleteChannelModal extends React.Component {
|
|||||||
onHide={this.props.onHide}
|
onHide={this.props.onHide}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<h4 className='modal-title'>{'Confirm DELETE Channel'}</h4>
|
<h4 className='modal-title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_channel.confirm'
|
||||||
|
defaultMessage='Confirm DELETE Channel'
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{`Are you sure you wish to delete the ${this.props.channel.display_name} ${channelTerm}?`}
|
<FormattedMessage
|
||||||
|
id='delete_channel.question'
|
||||||
|
defaultMessage='Are you sure you wish to delete the {display_name} {term}?'
|
||||||
|
values={{
|
||||||
|
display_name: this.props.channel.display_name,
|
||||||
|
term: (channelTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<button
|
<button
|
||||||
@@ -51,7 +78,10 @@ export default class DeleteChannelModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.props.onHide}
|
onClick={this.props.onHide}
|
||||||
>
|
>
|
||||||
{'Cancel'}
|
<FormattedMessage
|
||||||
|
id='delete_channel.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -59,7 +89,10 @@ export default class DeleteChannelModal extends React.Component {
|
|||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
onClick={this.handleDelete}
|
onClick={this.handleDelete}
|
||||||
>
|
>
|
||||||
{'Delete'}
|
<FormattedMessage
|
||||||
|
id='delete_channel.del'
|
||||||
|
defaultMessage='Delete'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import * as Utils from '../utils/utils.jsx';
|
|||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
export default class DeletePostModal extends React.Component {
|
export default class DeletePostModal extends React.Component {
|
||||||
@@ -128,10 +131,28 @@ export default class DeletePostModal extends React.Component {
|
|||||||
|
|
||||||
var commentWarning = '';
|
var commentWarning = '';
|
||||||
if (this.state.commentCount > 0) {
|
if (this.state.commentCount > 0) {
|
||||||
commentWarning = 'This post has ' + this.state.commentCount + ' comment(s) on it.';
|
commentWarning = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_post.warning'
|
||||||
|
defaultMessage='This post has {count} comment(s) on it.'
|
||||||
|
values={{
|
||||||
|
count: this.state.commentCount
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const postTerm = Utils.getPostTerm(this.state.post);
|
const postTerm = this.state.post.root_id ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_post.comment'
|
||||||
|
defaultMessage='Comment'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_post.post'
|
||||||
|
defaultMessage='Post'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -139,10 +160,24 @@ export default class DeletePostModal extends React.Component {
|
|||||||
onHide={this.handleHide}
|
onHide={this.handleHide}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title>{`Confirm ${postTerm} Delete`}</Modal.Title>
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id='delete_post.confirm'
|
||||||
|
defaultMessage='Confirm {term} Delete'
|
||||||
|
values={{
|
||||||
|
term: (postTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{`Are you sure you want to delete this ${postTerm.toLowerCase()}?`}
|
<FormattedMessage
|
||||||
|
id='delete_post.question'
|
||||||
|
defaultMessage='Are you sure you want to delete this ${term}?'
|
||||||
|
values={{
|
||||||
|
term: (postTerm)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
{commentWarning}
|
{commentWarning}
|
||||||
@@ -154,7 +189,10 @@ export default class DeletePostModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.handleHide}
|
onClick={this.handleHide}
|
||||||
>
|
>
|
||||||
{'Cancel'}
|
<FormattedMessage
|
||||||
|
id='delete_post.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
ref='deletePostBtn'
|
ref='deletePostBtn'
|
||||||
@@ -162,7 +200,10 @@ export default class DeletePostModal extends React.Component {
|
|||||||
className='btn btn-danger'
|
className='btn btn-danger'
|
||||||
onClick={this.handleDelete}
|
onClick={this.handleDelete}
|
||||||
>
|
>
|
||||||
{'Delete'}
|
<FormattedMessage
|
||||||
|
id='delete_post.del'
|
||||||
|
defaultMessage='Delete'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ import * as Client from '../utils/client.jsx';
|
|||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class EditChannelHeaderModal extends React.Component {
|
const holders = defineMessages({
|
||||||
|
error: {
|
||||||
|
id: 'edit_channel_header_modal.error',
|
||||||
|
defaultMessage: 'This channel header is too long, please enter a shorter one'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class EditChannelHeaderModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -64,8 +73,8 @@ export default class EditChannelHeaderModal extends React.Component {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.message === 'Invalid channel_header parameter') {
|
if (err.id === 'api.context.invalid_param.app_error') {
|
||||||
this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
|
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
|
||||||
} else {
|
} else {
|
||||||
this.setState({serverError: err.message});
|
this.setState({serverError: err.message});
|
||||||
}
|
}
|
||||||
@@ -99,10 +108,23 @@ export default class EditChannelHeaderModal extends React.Component {
|
|||||||
onHide={this.onHide}
|
onHide={this.onHide}
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title>{'Edit Header for ' + this.props.channel.display_name}</Modal.Title>
|
<Modal.Title>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_header_modal.title'
|
||||||
|
defaultMessage='Edit Header for {channel}'
|
||||||
|
values={{
|
||||||
|
channel: this.props.channel.display_name
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_header_modal.description'
|
||||||
|
defaultMessage='Edit the text appearing next to the channel name in the channel header.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
ref='textarea'
|
ref='textarea'
|
||||||
className='form-control no-resize'
|
className='form-control no-resize'
|
||||||
@@ -120,14 +142,20 @@ export default class EditChannelHeaderModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.onHide}
|
onClick={this.onHide}
|
||||||
>
|
>
|
||||||
{'Cancel'}
|
<FormattedMessage
|
||||||
|
id='edit_channel_header_modal.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
>
|
>
|
||||||
{'Save'}
|
<FormattedMessage
|
||||||
|
id='edit_channel_header_modal.save'
|
||||||
|
defaultMessage='Save'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -136,7 +164,10 @@ export default class EditChannelHeaderModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EditChannelHeaderModal.propTypes = {
|
EditChannelHeaderModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
show: React.PropTypes.bool.isRequired,
|
show: React.PropTypes.bool.isRequired,
|
||||||
onHide: React.PropTypes.func.isRequired,
|
onHide: React.PropTypes.func.isRequired,
|
||||||
channel: React.PropTypes.object.isRequired
|
channel: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(EditChannelHeaderModal);
|
||||||
|
|||||||
@@ -3,10 +3,19 @@
|
|||||||
|
|
||||||
import * as AsyncClient from '../utils/async_client.jsx';
|
import * as AsyncClient from '../utils/async_client.jsx';
|
||||||
import * as Client from '../utils/client.jsx';
|
import * as Client from '../utils/client.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
|
const holders = defineMessages({
|
||||||
|
error: {
|
||||||
|
id: 'edit_channel_purpose_modal.error',
|
||||||
|
defaultMessage: 'This channel purpose is too long, please enter a shorter one'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default class EditChannelPurposeModal extends React.Component {
|
export default class EditChannelPurposeModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -48,8 +57,8 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
this.handleHide();
|
this.handleHide();
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err.message === 'Invalid channel_purpose parameter') {
|
if (err.id === 'api.context.invalid_param.app_error') {
|
||||||
this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
|
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
|
||||||
} else {
|
} else {
|
||||||
this.setState({serverError: err.message});
|
this.setState({serverError: err.message});
|
||||||
}
|
}
|
||||||
@@ -72,9 +81,39 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = <span>{'Edit Purpose'}</span>;
|
let title = (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.title1'
|
||||||
|
defaultMessage='Edit Purpose'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
if (this.props.channel.display_name) {
|
if (this.props.channel.display_name) {
|
||||||
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
|
title = (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.title2'
|
||||||
|
defaultMessage='Edit Purpose for '
|
||||||
|
/>
|
||||||
|
<span className='name'>{this.props.channel.display_name}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channelType = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.channel'
|
||||||
|
defaultMessage='Channel'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
|
||||||
|
channelType = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.group'
|
||||||
|
defaultMessage='Group'
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +129,15 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.`}</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.body'
|
||||||
|
defaultMessage='Describe how this {type} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.'
|
||||||
|
values={{
|
||||||
|
type: (channelType)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
ref='purpose'
|
ref='purpose'
|
||||||
className='form-control no-resize'
|
className='form-control no-resize'
|
||||||
@@ -106,14 +153,20 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
onClick={this.handleHide}
|
onClick={this.handleHide}
|
||||||
>
|
>
|
||||||
{'Cancel'}
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
onClick={this.handleSave}
|
onClick={this.handleSave}
|
||||||
>
|
>
|
||||||
{'Save'}
|
<FormattedMessage
|
||||||
|
id='edit_channel_purpose_modal.save'
|
||||||
|
defaultMessage='Save'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -122,7 +175,10 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EditChannelPurposeModal.propTypes = {
|
EditChannelPurposeModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
show: React.PropTypes.bool.isRequired,
|
show: React.PropTypes.bool.isRequired,
|
||||||
channel: React.PropTypes.object,
|
channel: React.PropTypes.object,
|
||||||
onModalDismissed: React.PropTypes.func.isRequired
|
onModalDismissed: React.PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(EditChannelPurposeModal);
|
||||||
@@ -10,9 +10,19 @@ import PostStore from '../stores/post_store.jsx';
|
|||||||
import PreferenceStore from '../stores/preference_store.jsx';
|
import PreferenceStore from '../stores/preference_store.jsx';
|
||||||
|
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
var KeyCodes = Constants.KeyCodes;
|
var KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
export default class EditPostModal extends React.Component {
|
const holders = defineMessages({
|
||||||
|
editPost: {
|
||||||
|
id: 'edit_post.editPost',
|
||||||
|
defaultMessage: 'Edit the post...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class EditPostModal extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -151,7 +161,15 @@ export default class EditPostModal extends React.Component {
|
|||||||
>
|
>
|
||||||
<span aria-hidden='true'>×</span>
|
<span aria-hidden='true'>×</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 className='modal-title'>Edit {this.state.title}</h4>
|
<h4 className='modal-title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='edit_post.edit'
|
||||||
|
defaultMessage='Edit {title}'
|
||||||
|
values={{
|
||||||
|
title: this.state.title
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className='edit-modal-body modal-body'>
|
<div className='edit-modal-body modal-body'>
|
||||||
<Textbox
|
<Textbox
|
||||||
@@ -159,7 +177,7 @@ export default class EditPostModal extends React.Component {
|
|||||||
onKeyPress={this.handleEditKeyPress}
|
onKeyPress={this.handleEditKeyPress}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
messageText={this.state.editText}
|
messageText={this.state.editText}
|
||||||
createMessage='Edit the post...'
|
createMessage={this.props.intl.formatMessage(holders.editPost)}
|
||||||
supportsCommands={false}
|
supportsCommands={false}
|
||||||
id='edit_textbox'
|
id='edit_textbox'
|
||||||
ref='editbox'
|
ref='editbox'
|
||||||
@@ -172,14 +190,20 @@ export default class EditPostModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage
|
||||||
|
id='edit_post.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
onClick={this.handleEdit}
|
onClick={this.handleEdit}
|
||||||
>
|
>
|
||||||
Save
|
<FormattedMessage
|
||||||
|
id='edit_post.save'
|
||||||
|
defaultMessage='Save'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,3 +212,9 @@ export default class EditPostModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EditPostModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(EditPostModal);
|
||||||
@@ -41,6 +41,8 @@ export default class GetLinkModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const userCreationEnabled = global.window.mm_config.EnableUserCreation === 'true';
|
||||||
|
|
||||||
let helpText = null;
|
let helpText = null;
|
||||||
if (this.props.helpText) {
|
if (this.props.helpText) {
|
||||||
helpText = (
|
helpText = (
|
||||||
@@ -53,7 +55,7 @@ export default class GetLinkModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let copyLink = null;
|
let copyLink = null;
|
||||||
if (document.queryCommandSupported('copy')) {
|
if (userCreationEnabled && document.queryCommandSupported('copy')) {
|
||||||
copyLink = (
|
copyLink = (
|
||||||
<button
|
<button
|
||||||
data-copy-btn='true'
|
data-copy-btn='true'
|
||||||
@@ -69,6 +71,18 @@ export default class GetLinkModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let linkText = null;
|
||||||
|
if (userCreationEnabled) {
|
||||||
|
linkText = (
|
||||||
|
<textarea
|
||||||
|
className='form-control no-resize min-height'
|
||||||
|
readOnly='true'
|
||||||
|
ref='textarea'
|
||||||
|
value={this.props.link}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var copyLinkConfirm = null;
|
var copyLinkConfirm = null;
|
||||||
if (this.state.copiedLink) {
|
if (this.state.copiedLink) {
|
||||||
copyLinkConfirm = (
|
copyLinkConfirm = (
|
||||||
@@ -92,12 +106,7 @@ export default class GetLinkModal extends React.Component {
|
|||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{helpText}
|
{helpText}
|
||||||
<textarea
|
{linkText}
|
||||||
className='form-control no-resize min-height'
|
|
||||||
readOnly='true'
|
|
||||||
ref='textarea'
|
|
||||||
value={this.props.link}
|
|
||||||
/>
|
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ const holders = defineMessages({
|
|||||||
help: {
|
help: {
|
||||||
id: 'get_team_invite_link_modal.help',
|
id: 'get_team_invite_link_modal.help',
|
||||||
defaultMessage: 'Send teammates the link below for them to sign-up to this team site.'
|
defaultMessage: 'Send teammates the link below for them to sign-up to this team site.'
|
||||||
|
},
|
||||||
|
helpDisabled: {
|
||||||
|
id: 'get_team_invite_link_modal.helpDisabled',
|
||||||
|
defaultMessage: 'User creation has been disabled for your team. Please ask your team administrator for details.'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,12 +51,18 @@ class GetTeamInviteLinkModal extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
|
let helpText = formatMessage(holders.helpDisabled);
|
||||||
|
|
||||||
|
if (global.window.mm_config.EnableUserCreation === 'true') {
|
||||||
|
helpText = formatMessage(holders.help);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GetLinkModal
|
<GetLinkModal
|
||||||
show={this.state.show}
|
show={this.state.show}
|
||||||
onHide={() => this.setState({show: false})}
|
onHide={() => this.setState({show: false})}
|
||||||
title={formatMessage(holders.title)}
|
title={formatMessage(holders.title)}
|
||||||
helpText={formatMessage(holders.help)}
|
helpText={helpText}
|
||||||
link={TeamStore.getCurrentInviteLink()}
|
link={TeamStore.getCurrentInviteLink()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import LoginEmail from './login_email.jsx';
|
import LoginEmail from './login_email.jsx';
|
||||||
|
import LoginUsername from './login_username.jsx';
|
||||||
import LoginLdap from './login_ldap.jsx';
|
import LoginLdap from './login_ldap.jsx';
|
||||||
|
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
@@ -35,7 +36,7 @@ export default class Login extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
||||||
@@ -87,7 +88,7 @@ export default class Login extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let emailSignup;
|
let emailSignup;
|
||||||
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
|
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
|
||||||
emailSignup = (
|
emailSignup = (
|
||||||
<LoginEmail
|
<LoginEmail
|
||||||
teamName={this.props.teamName}
|
teamName={this.props.teamName}
|
||||||
@@ -189,6 +190,15 @@ export default class Login extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let usernameLogin = null;
|
||||||
|
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
|
||||||
|
usernameLogin = (
|
||||||
|
<LoginUsername
|
||||||
|
teamName={this.props.teamName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='signup-team__container'>
|
<div className='signup-team__container'>
|
||||||
<h5 className='margin--less'>
|
<h5 className='margin--less'>
|
||||||
@@ -210,6 +220,7 @@ export default class Login extends React.Component {
|
|||||||
{extraBox}
|
{extraBox}
|
||||||
{loginMessage}
|
{loginMessage}
|
||||||
{emailSignup}
|
{emailSignup}
|
||||||
|
{usernameLogin}
|
||||||
{ldapLogin}
|
{ldapLogin}
|
||||||
{userSignUp}
|
{userSignUp}
|
||||||
{findTeams}
|
{findTeams}
|
||||||
|
|||||||
181
web/react/components/login_username.jsx
Обычный файл
181
web/react/components/login_username.jsx
Обычный файл
@@ -0,0 +1,181 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
import * as Client from '../utils/client.jsx';
|
||||||
|
import UserStore from '../stores/user_store.jsx';
|
||||||
|
|
||||||
|
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
var holders = defineMessages({
|
||||||
|
badTeam: {
|
||||||
|
id: 'login_username.badTeam',
|
||||||
|
defaultMessage: 'Bad team name'
|
||||||
|
},
|
||||||
|
usernameReq: {
|
||||||
|
id: 'login_username.usernameReq',
|
||||||
|
defaultMessage: 'A username is required'
|
||||||
|
},
|
||||||
|
pwdReq: {
|
||||||
|
id: 'login_username.pwdReq',
|
||||||
|
defaultMessage: 'A password is required'
|
||||||
|
},
|
||||||
|
verifyEmailError: {
|
||||||
|
id: 'login_username.verifyEmailError',
|
||||||
|
defaultMessage: 'Please verify your email address. Check your inbox for an email.'
|
||||||
|
},
|
||||||
|
userNotFoundError: {
|
||||||
|
id: 'login_username.userNotFoundError',
|
||||||
|
defaultMessage: "We couldn't find an existing account matching your username for this team."
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
id: 'login_username.username',
|
||||||
|
defaultMessage: 'Username'
|
||||||
|
},
|
||||||
|
pwd: {
|
||||||
|
id: 'login_username.pwd',
|
||||||
|
defaultMessage: 'Password'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class LoginUsername extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
serverError: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
var state = {};
|
||||||
|
|
||||||
|
const name = this.props.teamName;
|
||||||
|
if (!name) {
|
||||||
|
state.serverError = formatMessage(holders.badTeam);
|
||||||
|
this.setState(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = this.refs.username.value.trim();
|
||||||
|
if (!username) {
|
||||||
|
state.serverError = formatMessage(holders.usernameReq);
|
||||||
|
this.setState(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = this.refs.password.value.trim();
|
||||||
|
if (!password) {
|
||||||
|
state.serverError = formatMessage(holders.pwdReq);
|
||||||
|
this.setState(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.serverError = '';
|
||||||
|
this.setState(state);
|
||||||
|
|
||||||
|
Client.loginByUsername(name, username, password,
|
||||||
|
() => {
|
||||||
|
UserStore.setLastUsername(username);
|
||||||
|
|
||||||
|
const redirect = Utils.getUrlParameter('redirect');
|
||||||
|
if (redirect) {
|
||||||
|
window.location.href = decodeURIComponent(redirect);
|
||||||
|
} else {
|
||||||
|
window.location.href = '/' + name + '/channels/town-square';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err.message === 'api.user.login.not_verified.app_error') {
|
||||||
|
state.serverError = formatMessage(holders.verifyEmailError);
|
||||||
|
} else if (err.message === 'store.sql_user.get_by_username.app_error') {
|
||||||
|
state.serverError = formatMessage(holders.userNotFoundError);
|
||||||
|
} else {
|
||||||
|
state.serverError = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.valid = false;
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let serverError;
|
||||||
|
let errorClass = '';
|
||||||
|
if (this.state.serverError) {
|
||||||
|
serverError = <label className='control-label'>{this.state.serverError}</label>;
|
||||||
|
errorClass = ' has-error';
|
||||||
|
}
|
||||||
|
|
||||||
|
let priorUsername = UserStore.getLastUsername();
|
||||||
|
let focusUsername = false;
|
||||||
|
let focusPassword = false;
|
||||||
|
if (priorUsername === '') {
|
||||||
|
focusUsername = true;
|
||||||
|
} else {
|
||||||
|
focusPassword = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailParam = Utils.getUrlParameter('email');
|
||||||
|
if (emailParam) {
|
||||||
|
priorUsername = decodeURIComponent(emailParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<div className='signup__email-container'>
|
||||||
|
<div className={'form-group' + errorClass}>
|
||||||
|
{serverError}
|
||||||
|
</div>
|
||||||
|
<div className={'form-group' + errorClass}>
|
||||||
|
<input
|
||||||
|
autoFocus={focusUsername}
|
||||||
|
type='username'
|
||||||
|
className='form-control'
|
||||||
|
name='username'
|
||||||
|
defaultValue={priorUsername}
|
||||||
|
ref='username'
|
||||||
|
placeholder={formatMessage(holders.username)}
|
||||||
|
spellCheck='false'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'form-group' + errorClass}>
|
||||||
|
<input
|
||||||
|
autoFocus={focusPassword}
|
||||||
|
type='password'
|
||||||
|
className='form-control'
|
||||||
|
name='password'
|
||||||
|
ref='password'
|
||||||
|
placeholder={formatMessage(holders.pwd)}
|
||||||
|
spellCheck='false'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group'>
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
className='btn btn-primary'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='login_username.signin'
|
||||||
|
defaultMessage='Sign in'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginUsername.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
LoginUsername.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
teamName: React.PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(LoginUsername);
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../stores/user_store.jsx';
|
||||||
import * as Utils from '../utils/utils.jsx';
|
import * as Utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
export default class MemberListItem extends React.Component {
|
export default class MemberListItem extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -38,7 +40,10 @@ export default class MemberListItem extends React.Component {
|
|||||||
className='btn btn-sm btn-primary'
|
className='btn btn-sm btn-primary'
|
||||||
>
|
>
|
||||||
<i className='glyphicon glyphicon-envelope'/>
|
<i className='glyphicon glyphicon-envelope'/>
|
||||||
{' Add'}
|
<FormattedMessage
|
||||||
|
id='member_item.add'
|
||||||
|
defaultMessage=' Add'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (isAdmin && !isMemberAdmin && (member.id !== UserStore.getCurrentId())) {
|
} else if (isAdmin && !isMemberAdmin && (member.id !== UserStore.getCurrentId())) {
|
||||||
@@ -53,7 +58,10 @@ export default class MemberListItem extends React.Component {
|
|||||||
role='menuitem'
|
role='menuitem'
|
||||||
onClick={self.handleMakeAdmin}
|
onClick={self.handleMakeAdmin}
|
||||||
>
|
>
|
||||||
Make Admin
|
<FormattedMessage
|
||||||
|
id='member_item.makeAdmin'
|
||||||
|
defaultMessage='Make Admin'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>);
|
</li>);
|
||||||
}
|
}
|
||||||
@@ -67,7 +75,10 @@ export default class MemberListItem extends React.Component {
|
|||||||
role='menuitem'
|
role='menuitem'
|
||||||
onClick={self.handleRemove}
|
onClick={self.handleRemove}
|
||||||
>
|
>
|
||||||
Remove Member
|
<FormattedMessage
|
||||||
|
id='member_item.removeMember'
|
||||||
|
defaultMessage='Remove Member'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>);
|
</li>);
|
||||||
}
|
}
|
||||||
@@ -82,7 +93,14 @@ export default class MemberListItem extends React.Component {
|
|||||||
aria-expanded='true'
|
aria-expanded='true'
|
||||||
>
|
>
|
||||||
<span className='fa fa-pencil'></span>
|
<span className='fa fa-pencil'></span>
|
||||||
<span className='text-capitalize'>{member.roles || 'Member'} </span>
|
<span className='text-capitalize'>
|
||||||
|
{member.roles ||
|
||||||
|
<FormattedMessage
|
||||||
|
id='member_item.member'
|
||||||
|
defaultMessage='Member'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul
|
<ul
|
||||||
className='dropdown-menu member-menu'
|
className='dropdown-menu member-menu'
|
||||||
@@ -94,7 +112,7 @@ export default class MemberListItem extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
invite = <div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || 'Member'}</div>;
|
invite = (<div className='member-role text-capitalize'><span className='fa fa-pencil hidden'></span>{member.roles || <FormattedMessage id='member_item.member' />}</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ class MsgTyping extends React.Component {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='msg_typing.areTyping'
|
id='msg_typing.areTyping'
|
||||||
defaultMessage='{users} and {last} are typing...'
|
defaultMessage='{users} and {last} are typing...'
|
||||||
vaues={{
|
values={{
|
||||||
users: users.join(', '),
|
users: (users.join(', ')),
|
||||||
last: last
|
last: (last)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
|
|||||||
|
|
||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
export default class PopoverListMembers extends React.Component {
|
export default class PopoverListMembers extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -92,7 +94,10 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
className='btn-message'
|
className='btn-message'
|
||||||
onClick={(e) => this.handleShowDirectChannel(m, e)}
|
onClick={(e) => this.handleShowDirectChannel(m, e)}
|
||||||
>
|
>
|
||||||
{'Message'}
|
<FormattedMessage
|
||||||
|
id='members_popover.msg'
|
||||||
|
defaultMessage='Message'
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,6 +152,12 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
countText = count.toString();
|
countText = count.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='members_popover.title'
|
||||||
|
defaultMessage='Members'
|
||||||
|
/>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -171,7 +182,7 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
ref='memebersPopover'
|
ref='memebersPopover'
|
||||||
title='Members'
|
title={title}
|
||||||
id='member-list-popover'
|
id='member-list-popover'
|
||||||
>
|
>
|
||||||
{popoverHtml}
|
{popoverHtml}
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export default class Post extends React.Component {
|
|||||||
commentCount={commentCount}
|
commentCount={commentCount}
|
||||||
handleCommentClick={this.handleCommentClick}
|
handleCommentClick={this.handleCommentClick}
|
||||||
isLastComment={this.props.isLastComment}
|
isLastComment={this.props.isLastComment}
|
||||||
|
sameUser={this.props.sameUser}
|
||||||
/>
|
/>
|
||||||
<PostBody
|
<PostBody
|
||||||
post={post}
|
post={post}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import UserStore from '../stores/user_store.jsx';
|
import UserStore from '../stores/user_store.jsx';
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
var ActionTypes = Constants.ActionTypes;
|
var ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
export default class PostDeletedModal extends React.Component {
|
export default class PostDeletedModal extends React.Component {
|
||||||
@@ -65,11 +68,19 @@ export default class PostDeletedModal extends React.Component {
|
|||||||
className='modal-title'
|
className='modal-title'
|
||||||
id='myModalLabel'
|
id='myModalLabel'
|
||||||
>
|
>
|
||||||
{'Comment could not be posted'}
|
<FormattedMessage
|
||||||
|
id='post_delete.notPosted'
|
||||||
|
defaultMessage='Comment could not be posted'
|
||||||
|
/>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-body'>
|
<div className='modal-body'>
|
||||||
<p>{'Someone deleted the message on which you tried to post a comment.'}</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='post_delete.someone'
|
||||||
|
defaultMessage='Someone deleted the message on which you tried to post a comment.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-footer'>
|
<div className='modal-footer'>
|
||||||
<button
|
<button
|
||||||
@@ -77,7 +88,10 @@ export default class PostDeletedModal extends React.Component {
|
|||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
>
|
>
|
||||||
{'Okay'}
|
<FormattedMessage
|
||||||
|
id='post_delete.okay'
|
||||||
|
defaultMessage='Okay'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default class PostHeader extends React.Component {
|
|||||||
handleCommentClick={this.props.handleCommentClick}
|
handleCommentClick={this.props.handleCommentClick}
|
||||||
allowReply='true'
|
allowReply='true'
|
||||||
isLastComment={this.props.isLastComment}
|
isLastComment={this.props.isLastComment}
|
||||||
|
sameUser={this.props.sameUser}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -62,11 +63,13 @@ export default class PostHeader extends React.Component {
|
|||||||
PostHeader.defaultProps = {
|
PostHeader.defaultProps = {
|
||||||
post: null,
|
post: null,
|
||||||
commentCount: 0,
|
commentCount: 0,
|
||||||
isLastComment: false
|
isLastComment: false,
|
||||||
|
sameUser: false
|
||||||
};
|
};
|
||||||
PostHeader.propTypes = {
|
PostHeader.propTypes = {
|
||||||
post: React.PropTypes.object,
|
post: React.PropTypes.object,
|
||||||
commentCount: React.PropTypes.number,
|
commentCount: React.PropTypes.number,
|
||||||
isLastComment: React.PropTypes.bool,
|
isLastComment: React.PropTypes.bool,
|
||||||
handleCommentClick: React.PropTypes.func
|
handleCommentClick: React.PropTypes.func,
|
||||||
|
sameUser: React.PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ export default class PostInfo extends React.Component {
|
|||||||
<li className='col'>
|
<li className='col'>
|
||||||
<TimeSince
|
<TimeSince
|
||||||
eventTime={post.create_at}
|
eventTime={post.create_at}
|
||||||
|
sameUser={this.props.sameUser}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li className='col col__reply'>
|
<li className='col col__reply'>
|
||||||
@@ -251,12 +252,14 @@ PostInfo.defaultProps = {
|
|||||||
post: null,
|
post: null,
|
||||||
commentCount: 0,
|
commentCount: 0,
|
||||||
isLastComment: false,
|
isLastComment: false,
|
||||||
allowReply: false
|
allowReply: false,
|
||||||
|
sameUser: false
|
||||||
};
|
};
|
||||||
PostInfo.propTypes = {
|
PostInfo.propTypes = {
|
||||||
post: React.PropTypes.object,
|
post: React.PropTypes.object,
|
||||||
commentCount: React.PropTypes.number,
|
commentCount: React.PropTypes.number,
|
||||||
isLastComment: React.PropTypes.bool,
|
isLastComment: React.PropTypes.bool,
|
||||||
allowReply: React.PropTypes.string,
|
allowReply: React.PropTypes.string,
|
||||||
handleCommentClick: React.PropTypes.func
|
handleCommentClick: React.PropTypes.func,
|
||||||
|
sameUser: React.PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import UserStore from '../stores/user_store.jsx';
|
|||||||
import BrowserStore from '../stores/browser_store.jsx';
|
import BrowserStore from '../stores/browser_store.jsx';
|
||||||
import * as utils from '../utils/utils.jsx';
|
import * as utils from '../utils/utils.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
export default class RemovedFromChannelModal extends React.Component {
|
export default class RemovedFromChannelModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -49,12 +51,22 @@ export default class RemovedFromChannelModal extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
var currentUser = UserStore.getCurrentUser();
|
var currentUser = UserStore.getCurrentUser();
|
||||||
|
|
||||||
var channelName = 'the channel';
|
var channelName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='removed_channel.channelName'
|
||||||
|
defaultMessage='the channel'
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (this.state.channelName) {
|
if (this.state.channelName) {
|
||||||
channelName = this.state.channelName;
|
channelName = this.state.channelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
var remover = 'Someone';
|
var remover = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='removed_channel.someone'
|
||||||
|
defaultMessage='Someone'
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (this.state.remover) {
|
if (this.state.remover) {
|
||||||
remover = this.state.remover;
|
remover = this.state.remover;
|
||||||
}
|
}
|
||||||
@@ -78,17 +90,36 @@ export default class RemovedFromChannelModal extends React.Component {
|
|||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
aria-label='Close'
|
aria-label='Close'
|
||||||
><span aria-hidden='true'>×</span></button>
|
><span aria-hidden='true'>×</span></button>
|
||||||
<h4 className='modal-title'>Removed from <span className='name'>{channelName}</span></h4>
|
<h4 className='modal-title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='removed_channel.from'
|
||||||
|
defaultMessage='Removed from '
|
||||||
|
/>
|
||||||
|
<span className='name'>{channelName}</span></h4>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-body'>
|
<div className='modal-body'>
|
||||||
<p>{remover} removed you from {channelName}</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='removed_channel.remover'
|
||||||
|
defaultMessage='{remover} removed you from {channel}'
|
||||||
|
values={{
|
||||||
|
remover: (remover),
|
||||||
|
channel: (channelName)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-footer'>
|
<div className='modal-footer'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
>Okay</button>
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='removed_channel.okay'
|
||||||
|
defaultMessage='Okay'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,39 @@ import * as AsyncClient from '../utils/async_client.jsx';
|
|||||||
import ChannelStore from '../stores/channel_store.jsx';
|
import ChannelStore from '../stores/channel_store.jsx';
|
||||||
import Constants from '../utils/constants.jsx';
|
import Constants from '../utils/constants.jsx';
|
||||||
|
|
||||||
|
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
|
||||||
|
|
||||||
|
const holders = defineMessages({
|
||||||
|
required: {
|
||||||
|
id: 'rename_channel.required',
|
||||||
|
defaultMessage: 'This field is required'
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
id: 'rename_channel.maxLength',
|
||||||
|
defaultMessage: 'This field must be less than 22 characters'
|
||||||
|
},
|
||||||
|
lowercase: {
|
||||||
|
id: 'rename_channel.lowercase',
|
||||||
|
defaultMessage: 'Must be lowercase alphanumeric characters'
|
||||||
|
},
|
||||||
|
handle: {
|
||||||
|
id: 'rename_channel.handle',
|
||||||
|
defaultMessage: 'Handle'
|
||||||
|
},
|
||||||
|
defaultError: {
|
||||||
|
id: 'rename_channel.defaultError',
|
||||||
|
defaultMessage: ' - Cannot be changed for the default channel'
|
||||||
|
},
|
||||||
|
displayNameHolder: {
|
||||||
|
id: 'rename_channel.displayNameHolder',
|
||||||
|
defaultMessage: 'Enter display name'
|
||||||
|
},
|
||||||
|
handleHolder: {
|
||||||
|
id: 'rename_channel.handleHolder',
|
||||||
|
defaultMessage: 'lowercase alphanumeric's only'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default class RenameChannelModal extends React.Component {
|
export default class RenameChannelModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -41,13 +74,14 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
const oldName = channel.name;
|
const oldName = channel.name;
|
||||||
const oldDisplayName = channel.displayName;
|
const oldDisplayName = channel.displayName;
|
||||||
const state = {serverError: ''};
|
const state = {serverError: ''};
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
channel.display_name = this.state.displayName.trim();
|
channel.display_name = this.state.displayName.trim();
|
||||||
if (!channel.display_name) {
|
if (!channel.display_name) {
|
||||||
state.displayNameError = 'This field is required';
|
state.displayNameError = formatMessage(holders.required);
|
||||||
state.invalid = true;
|
state.invalid = true;
|
||||||
} else if (channel.display_name.length > 22) {
|
} else if (channel.display_name.length > 22) {
|
||||||
state.displayNameError = 'This field must be less than 22 characters';
|
state.displayNameError = formatMessage(holders.maxLength);
|
||||||
state.invalid = true;
|
state.invalid = true;
|
||||||
} else {
|
} else {
|
||||||
state.displayNameError = '';
|
state.displayNameError = '';
|
||||||
@@ -55,17 +89,17 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
|
|
||||||
channel.name = this.state.channelName.trim();
|
channel.name = this.state.channelName.trim();
|
||||||
if (!channel.name) {
|
if (!channel.name) {
|
||||||
state.nameError = 'This field is required';
|
state.nameError = formatMessage(holders.required);
|
||||||
state.invalid = true;
|
state.invalid = true;
|
||||||
} else if (channel.name.length > 22) {
|
} else if (channel.name.length > 22) {
|
||||||
state.nameError = 'This field must be less than 22 characters';
|
state.nameError = formatMessage(holders.maxLength);
|
||||||
state.invalid = true;
|
state.invalid = true;
|
||||||
} else {
|
} else {
|
||||||
const cleanedName = Utils.cleanUpUrlable(channel.name);
|
const cleanedName = Utils.cleanUpUrlable(channel.name);
|
||||||
if (cleanedName === channel.name) {
|
if (cleanedName === channel.name) {
|
||||||
state.nameError = '';
|
state.nameError = '';
|
||||||
} else {
|
} else {
|
||||||
state.nameError = 'Must be lowercase alphanumeric characters';
|
state.nameError = formatMessage(holders.lowercase);
|
||||||
state.invalid = true;
|
state.invalid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +187,13 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let handleInputLabel = 'Handle';
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
|
let handleInputLabel = formatMessage(holders.handle);
|
||||||
let handleInputClass = 'form-control';
|
let handleInputClass = 'form-control';
|
||||||
let readOnlyHandleInput = false;
|
let readOnlyHandleInput = false;
|
||||||
if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
|
if (this.state.channelName === Constants.DEFAULT_CHANNEL) {
|
||||||
handleInputLabel += ' - Cannot be changed for the default channel';
|
handleInputLabel += formatMessage(holders.defaultError);
|
||||||
handleInputClass += ' disabled-input';
|
handleInputClass += ' disabled-input';
|
||||||
readOnlyHandleInput = true;
|
readOnlyHandleInput = true;
|
||||||
}
|
}
|
||||||
@@ -180,14 +216,29 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
>
|
>
|
||||||
<span aria-hidden='true'>{'×'}</span>
|
<span aria-hidden='true'>{'×'}</span>
|
||||||
<span className='sr-only'>{'Close'}</span>
|
<span className='sr-only'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rename_channel.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<h4 className='modal-title'>{'Rename Channel'}</h4>
|
<h4 className='modal-title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rename_channel.title'
|
||||||
|
defaultMessage='Rename Channel'
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<form role='form'>
|
<form role='form'>
|
||||||
<div className='modal-body'>
|
<div className='modal-body'>
|
||||||
<div className={displayNameClass}>
|
<div className={displayNameClass}>
|
||||||
<label className='control-label'>{'Display Name'}</label>
|
<label className='control-label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='rename_channel.displayName'
|
||||||
|
defaultMessage='Display Name'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
onKeyUp={this.displayNameKeyUp}
|
onKeyUp={this.displayNameKeyUp}
|
||||||
onChange={this.onDisplayNameChange}
|
onChange={this.onDisplayNameChange}
|
||||||
@@ -195,7 +246,7 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
ref='displayName'
|
ref='displayName'
|
||||||
id='display_name'
|
id='display_name'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
placeholder='Enter display name'
|
placeholder={formatMessage(holders.displayNameHolder)}
|
||||||
value={this.state.displayName}
|
value={this.state.displayName}
|
||||||
maxLength='64'
|
maxLength='64'
|
||||||
/>
|
/>
|
||||||
@@ -208,7 +259,7 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
type='text'
|
type='text'
|
||||||
className={handleInputClass}
|
className={handleInputClass}
|
||||||
ref='channelName'
|
ref='channelName'
|
||||||
placeholder='lowercase alphanumeric's only'
|
placeholder={formatMessage(holders.handleHolder)}
|
||||||
value={this.state.channelName}
|
value={this.state.channelName}
|
||||||
maxLength='64'
|
maxLength='64'
|
||||||
readOnly={readOnlyHandleInput}
|
readOnly={readOnlyHandleInput}
|
||||||
@@ -223,14 +274,20 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
className='btn btn-default'
|
className='btn btn-default'
|
||||||
data-dismiss='modal'
|
data-dismiss='modal'
|
||||||
>
|
>
|
||||||
{'Cancel'}
|
<FormattedMessage
|
||||||
|
id='rename_channel.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
type='submit'
|
type='submit'
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
>
|
>
|
||||||
{'Save'}
|
<FormattedMessage
|
||||||
|
id='rename_channel.save'
|
||||||
|
defaultMessage='Save'
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -240,3 +297,9 @@ export default class RenameChannelModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenameChannelModal.propTypes = {
|
||||||
|
intl: intlShape.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(RenameChannelModal);
|
||||||
@@ -85,6 +85,6 @@ SettingItemMax.propTypes = {
|
|||||||
extraInfo: React.PropTypes.element,
|
extraInfo: React.PropTypes.element,
|
||||||
updateSection: React.PropTypes.func,
|
updateSection: React.PropTypes.func,
|
||||||
submit: React.PropTypes.func,
|
submit: React.PropTypes.func,
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.node,
|
||||||
width: React.PropTypes.string
|
width: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export default class SettingItemMin extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingItemMin.propTypes = {
|
SettingItemMin.propTypes = {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.node,
|
||||||
disableOpen: React.PropTypes.bool,
|
disableOpen: React.PropTypes.bool,
|
||||||
updateSection: React.PropTypes.func,
|
updateSection: React.PropTypes.func,
|
||||||
describe: React.PropTypes.string
|
describe: React.PropTypes.node
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,9 +150,18 @@ class SignupUserComplete extends React.Component {
|
|||||||
|
|
||||||
// set up error labels
|
// set up error labels
|
||||||
var emailError = null;
|
var emailError = null;
|
||||||
|
var emailHelpText = (
|
||||||
|
<span className='help-block'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='signup_user_completed.emailHelp'
|
||||||
|
defaultMessage='Valid email required for sign-up'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
var emailDivStyle = 'form-group';
|
var emailDivStyle = 'form-group';
|
||||||
if (this.state.emailError) {
|
if (this.state.emailError) {
|
||||||
emailError = <label className='control-label'>{this.state.emailError}</label>;
|
emailError = <label className='control-label'>{this.state.emailError}</label>;
|
||||||
|
emailHelpText = '';
|
||||||
emailDivStyle += ' has-error';
|
emailDivStyle += ' has-error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +241,7 @@ class SignupUserComplete extends React.Component {
|
|||||||
spellCheck='false'
|
spellCheck='false'
|
||||||
/>
|
/>
|
||||||
{emailError}
|
{emailError}
|
||||||
|
{emailHelpText}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -575,6 +575,8 @@ class GeneralTab extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nameExtraInfo = <span>{formatMessage(holders.teamNameInfo)}</span>;
|
||||||
|
|
||||||
nameSection = (
|
nameSection = (
|
||||||
<SettingItemMax
|
<SettingItemMax
|
||||||
title={formatMessage({id: 'general_tab.teamName'})}
|
title={formatMessage({id: 'general_tab.teamName'})}
|
||||||
@@ -583,7 +585,7 @@ class GeneralTab extends React.Component {
|
|||||||
server_error={serverError}
|
server_error={serverError}
|
||||||
client_error={clientError}
|
client_error={clientError}
|
||||||
updateSection={this.onUpdateNameSection}
|
updateSection={this.onUpdateNameSection}
|
||||||
extraInfo={formatMessage(holders.teamNameInfo)}
|
extraInfo={nameExtraInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -129,13 +129,6 @@ export default class Textbox extends React.Component {
|
|||||||
this.resize();
|
this.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
showHelp(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.target.blur();
|
|
||||||
|
|
||||||
global.window.open('/docs/Messaging');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let previewLink = null;
|
let previewLink = null;
|
||||||
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
|
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
|
||||||
@@ -194,7 +187,8 @@ export default class Textbox extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
{previewLink}
|
{previewLink}
|
||||||
<a
|
<a
|
||||||
onClick={this.showHelp}
|
target='_blank'
|
||||||
|
href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
|
||||||
className='textbox-help-link'
|
className='textbox-help-link'
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default class TimeSince extends React.Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 30000);
|
}, Constants.TIME_SINCE_UPDATE_INTERVAL);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
@@ -23,6 +23,14 @@ export default class TimeSince extends React.Component {
|
|||||||
const displayDate = Utils.displayDate(this.props.eventTime);
|
const displayDate = Utils.displayDate(this.props.eventTime);
|
||||||
const displayTime = Utils.displayTime(this.props.eventTime);
|
const displayTime = Utils.displayTime(this.props.eventTime);
|
||||||
|
|
||||||
|
if (this.props.sameUser) {
|
||||||
|
return (
|
||||||
|
<time className='post__time'>
|
||||||
|
{Utils.displayTime(this.props.eventTime)}
|
||||||
|
</time>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
|
<Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
|
||||||
{displayDate + ' at ' + displayTime}
|
{displayDate + ' at ' + displayTime}
|
||||||
@@ -42,10 +50,13 @@ export default class TimeSince extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSince.defaultProps = {
|
TimeSince.defaultProps = {
|
||||||
eventTime: 0
|
eventTime: 0,
|
||||||
|
sameUser: false
|
||||||
};
|
};
|
||||||
|
|
||||||
TimeSince.propTypes = {
|
TimeSince.propTypes = {
|
||||||
eventTime: React.PropTypes.number.isRequired
|
eventTime: React.PropTypes.number.isRequired,
|
||||||
|
sameUser: React.PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ class SecurityTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
this.setState({passwordError: formatMessage(holders.passwordMatchError), serverError: ''});
|
var defaultState = Object.assign(this.getDefaultState(), {passwordError: formatMessage(holders.passwordMatchError), serverError: ''});
|
||||||
|
this.setState(defaultState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||||
@@ -10,6 +10,7 @@ import Constants from '../utils/constants.jsx';
|
|||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
const LOG_CHANGE_EVENT = 'log_change';
|
const LOG_CHANGE_EVENT = 'log_change';
|
||||||
|
const SERVER_AUDIT_CHANGE_EVENT = 'server_audit_change';
|
||||||
const CONFIG_CHANGE_EVENT = 'config_change';
|
const CONFIG_CHANGE_EVENT = 'config_change';
|
||||||
const ALL_TEAMS_EVENT = 'all_team_change';
|
const ALL_TEAMS_EVENT = 'all_team_change';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
this.logs = null;
|
this.logs = null;
|
||||||
|
this.audits = null;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
this.teams = null;
|
this.teams = null;
|
||||||
|
|
||||||
@@ -25,6 +27,10 @@ class AdminStoreClass extends EventEmitter {
|
|||||||
this.addLogChangeListener = this.addLogChangeListener.bind(this);
|
this.addLogChangeListener = this.addLogChangeListener.bind(this);
|
||||||
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
|
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
|
||||||
|
|
||||||
|
this.emitAuditChange = this.emitAuditChange.bind(this);
|
||||||
|
this.addAuditChangeListener = this.addAuditChangeListener.bind(this);
|
||||||
|
this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this);
|
||||||
|
|
||||||
this.emitConfigChange = this.emitConfigChange.bind(this);
|
this.emitConfigChange = this.emitConfigChange.bind(this);
|
||||||
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
|
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
|
||||||
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
|
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
|
||||||
@@ -46,6 +52,18 @@ class AdminStoreClass extends EventEmitter {
|
|||||||
this.removeListener(LOG_CHANGE_EVENT, callback);
|
this.removeListener(LOG_CHANGE_EVENT, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitAuditChange() {
|
||||||
|
this.emit(SERVER_AUDIT_CHANGE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAuditChangeListener(callback) {
|
||||||
|
this.on(SERVER_AUDIT_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAuditChangeListener(callback) {
|
||||||
|
this.removeListener(SERVER_AUDIT_CHANGE_EVENT, callback);
|
||||||
|
}
|
||||||
|
|
||||||
emitConfigChange() {
|
emitConfigChange() {
|
||||||
this.emit(CONFIG_CHANGE_EVENT);
|
this.emit(CONFIG_CHANGE_EVENT);
|
||||||
}
|
}
|
||||||
@@ -78,6 +96,14 @@ class AdminStoreClass extends EventEmitter {
|
|||||||
this.logs = logs;
|
this.logs = logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudits() {
|
||||||
|
return this.audits;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAudits(audits) {
|
||||||
|
this.audits = audits;
|
||||||
|
}
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
@@ -113,6 +139,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
|
|||||||
AdminStore.saveLogs(action.logs);
|
AdminStore.saveLogs(action.logs);
|
||||||
AdminStore.emitLogChange();
|
AdminStore.emitLogChange();
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.RECIEVED_SERVER_AUDITS:
|
||||||
|
AdminStore.saveAudits(action.audits);
|
||||||
|
AdminStore.emitAuditChange();
|
||||||
|
break;
|
||||||
case ActionTypes.RECIEVED_CONFIG:
|
case ActionTypes.RECIEVED_CONFIG:
|
||||||
AdminStore.saveConfig(action.config);
|
AdminStore.saveConfig(action.config);
|
||||||
AdminStore.emitConfigChange();
|
AdminStore.emitConfigChange();
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class UserStoreClass extends EventEmitter {
|
|||||||
this.setCurrentUser = this.setCurrentUser.bind(this);
|
this.setCurrentUser = this.setCurrentUser.bind(this);
|
||||||
this.getLastEmail = this.getLastEmail.bind(this);
|
this.getLastEmail = this.getLastEmail.bind(this);
|
||||||
this.setLastEmail = this.setLastEmail.bind(this);
|
this.setLastEmail = this.setLastEmail.bind(this);
|
||||||
|
this.getLastUsername = this.getLastUsername.bind(this);
|
||||||
|
this.setLastUsername = this.setLastUsername.bind(this);
|
||||||
this.hasProfile = this.hasProfile.bind(this);
|
this.hasProfile = this.hasProfile.bind(this);
|
||||||
this.getProfile = this.getProfile.bind(this);
|
this.getProfile = this.getProfile.bind(this);
|
||||||
this.getProfileByUsername = this.getProfileByUsername.bind(this);
|
this.getProfileByUsername = this.getProfileByUsername.bind(this);
|
||||||
@@ -159,6 +161,14 @@ class UserStoreClass extends EventEmitter {
|
|||||||
BrowserStore.setGlobalItem('last_email', email);
|
BrowserStore.setGlobalItem('last_email', email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastUsername() {
|
||||||
|
return BrowserStore.getGlobalItem('last_username', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastUsername(username) {
|
||||||
|
BrowserStore.setGlobalItem('last_username', username);
|
||||||
|
}
|
||||||
|
|
||||||
hasProfile(userId) {
|
hasProfile(userId) {
|
||||||
return this.getProfiles()[userId] != null;
|
return this.getProfiles()[userId] != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,32 @@ export function getLogs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getServerAudits() {
|
||||||
|
if (isCallInProgress('getServerAudits')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callTracker.getServerAudits = utils.getTimestamp();
|
||||||
|
client.getServerAudits(
|
||||||
|
(data, textStatus, xhr) => {
|
||||||
|
callTracker.getServerAudits = 0;
|
||||||
|
|
||||||
|
if (xhr.status === 304 || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECIEVED_SERVER_AUDITS,
|
||||||
|
audits: data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
callTracker.getServerAudits = 0;
|
||||||
|
dispatchError(err, 'getServerAudits');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
if (isCallInProgress('getConfig')) {
|
if (isCallInProgress('getConfig')) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -305,6 +305,28 @@ export function loginByEmail(name, email, password, success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loginByUsername(name, username, password, success, error) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/v1/users/login',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({name, username, password}),
|
||||||
|
success: function onSuccess(data, textStatus, xhr) {
|
||||||
|
track('api', 'api_users_login_success', data.team_id, 'username', data.username);
|
||||||
|
sessionStorage.removeItem(data.id + '_last_error');
|
||||||
|
BrowserStore.signalLogin();
|
||||||
|
success(data, textStatus, xhr);
|
||||||
|
},
|
||||||
|
error: function onError(xhr, status, err) {
|
||||||
|
track('api', 'api_users_login_fail', name, 'username', username);
|
||||||
|
|
||||||
|
var e = handleError('loginByUsername', xhr, status, err);
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function loginByLdap(teamName, id, password, success, error) {
|
export function loginByLdap(teamName, id, password, success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/users/login_ldap',
|
url: '/api/v1/users/login_ldap',
|
||||||
@@ -385,6 +407,20 @@ export function getLogs(success, error) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getServerAudits(success, error) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/v1/admin/audits',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
type: 'GET',
|
||||||
|
success,
|
||||||
|
error: function onError(xhr, status, err) {
|
||||||
|
var e = handleError('getServerAudits', xhr, status, err);
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getConfig(success, error) {
|
export function getConfig(success, error) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v1/admin/config',
|
url: '/api/v1/admin/config',
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
|
|
||||||
RECIEVED_CONFIG: null,
|
RECIEVED_CONFIG: null,
|
||||||
RECIEVED_LOGS: null,
|
RECIEVED_LOGS: null,
|
||||||
|
RECIEVED_SERVER_AUDITS: null,
|
||||||
RECIEVED_ALL_TEAMS: null,
|
RECIEVED_ALL_TEAMS: null,
|
||||||
|
|
||||||
SHOW_SEARCH: null,
|
SHOW_SEARCH: null,
|
||||||
@@ -84,7 +85,7 @@ export default {
|
|||||||
SPECIAL_MENTIONS: ['channel'],
|
SPECIAL_MENTIONS: ['channel'],
|
||||||
CHARACTER_LIMIT: 4000,
|
CHARACTER_LIMIT: 4000,
|
||||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
||||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac'],
|
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
|
||||||
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
|
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
|
||||||
PRESENTATION_TYPES: ['ppt', 'pptx'],
|
PRESENTATION_TYPES: ['ppt', 'pptx'],
|
||||||
SPREADSHEET_TYPES: ['xlsx', 'csv'],
|
SPREADSHEET_TYPES: ['xlsx', 'csv'],
|
||||||
@@ -462,5 +463,6 @@ export default {
|
|||||||
MIN_USERNAME_LENGTH: 3,
|
MIN_USERNAME_LENGTH: 3,
|
||||||
MAX_USERNAME_LENGTH: 15,
|
MAX_USERNAME_LENGTH: 15,
|
||||||
MIN_PASSWORD_LENGTH: 5,
|
MIN_PASSWORD_LENGTH: 5,
|
||||||
MAX_PASSWORD_LENGTH: 50
|
MAX_PASSWORD_LENGTH: 50,
|
||||||
|
TIME_SINCE_UPDATE_INTERVAL: 30000
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
);
|
);
|
||||||
} else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
|
} else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
|
||||||
try {
|
try {
|
||||||
const html = katex.renderToString(TextFormatting.sanitizeHtml(code), {throwOnError: false, displayMode: true});
|
const html = katex.renderToString(code, {throwOnError: false, displayMode: true});
|
||||||
|
|
||||||
return '<div class="post-body--code tex">' + html + '</div>';
|
return '<div class="post-body--code tex">' + html + '</div>';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -456,11 +456,7 @@ body.ios {
|
|||||||
&:hover {
|
&:hover {
|
||||||
|
|
||||||
.post__time {
|
.post__time {
|
||||||
|
@include opacity(0.5);
|
||||||
&:before {
|
|
||||||
@include opacity(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -484,27 +480,15 @@ body.ios {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post__time {
|
.post__time {
|
||||||
display: inline-block;
|
font: normal normal normal FontAwesome;
|
||||||
font: normal normal normal 14px/1 FontAwesome;
|
|
||||||
font-size: inherit;
|
|
||||||
text-rendering: auto;
|
text-rendering: auto;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
font-size: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
left: 17px;
|
left: -1.0em;
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 37px;
|
line-height: 37px;
|
||||||
|
@include opacity(0);
|
||||||
&:before {
|
|
||||||
@include opacity(0);
|
|
||||||
content: "\f017";
|
|
||||||
content: "\f017";
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
float: right;
|
float: right;
|
||||||
margin: 3px 0 0;
|
margin: 0px 0 0;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -83,4 +84,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,53 +8,54 @@
|
|||||||
"about.date": "Build Date:",
|
"about.date": "Build Date:",
|
||||||
"about.hash": "Build Hash:",
|
"about.hash": "Build Hash:",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"access_history.sessionRevoked": "The session with id {sessionId} was revoked",
|
"audit_table.sessionRevoked": "The session with id {sessionId} was revoked",
|
||||||
"access_history.channelCreated": "Created the {channelName} channel/group",
|
"audit_table.channelCreated": "Created the {channelName} channel/group",
|
||||||
"access_history.establishedDM": "Established a direct message channel with {username}",
|
"audit_table.establishedDM": "Established a direct message channel with {username}",
|
||||||
"access_history.nameUpdated": "Updated the {channelName} channel/group name",
|
"audit_table.nameUpdated": "Updated the {channelName} channel/group name",
|
||||||
"access_history.headerUpdated": "Updated the {channelName} channel/group header",
|
"audit_table.headerUpdated": "Updated the {channelName} channel/group header",
|
||||||
"access_history.channelDeleted": "Deleted the channel/group with the URL {url}",
|
"audit_table.channelDeleted": "Deleted the channel/group with the URL {url}",
|
||||||
"access_history.userAdded": "Added {username} to the {channelName} channel/group",
|
"audit_table.userAdded": "Added {username} to the {channelName} channel/group",
|
||||||
"access_history.userRemoved": "Removed {username} to the {channelName} channel/group",
|
"audit_table.userRemoved": "Removed {username} to the {channelName} channel/group",
|
||||||
"access_history.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
|
"audit_table.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
|
||||||
"access_history.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
|
"audit_table.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
|
||||||
"access_history.successfullOAuthAccess": "Successfully gave a new OAuth service access",
|
"audit_table.successfullOAuthAccess": "Successfully gave a new OAuth service access",
|
||||||
"access_history.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
|
"audit_table.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
|
||||||
"access_history.attemptedOAuthToken": "Attempted to get an OAuth access token",
|
"audit_table.attemptedOAuthToken": "Attempted to get an OAuth access token",
|
||||||
"access_history.successfullOAuthToken": "Successfully added a new OAuth service",
|
"audit_table.successfullOAuthToken": "Successfully added a new OAuth service",
|
||||||
"access_history.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
|
"audit_table.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
|
||||||
"access_history.attemptedLogin": "Attempted to login",
|
"audit_table.attemptedLogin": "Attempted to login",
|
||||||
"access_history.successfullLogin": "Successfully logged in",
|
"audit_table.successfullLogin": "Successfully logged in",
|
||||||
"access_history.failedLogin": "FAILED login attempt",
|
"audit_table.failedLogin": "FAILED login attempt",
|
||||||
"access_history.updatePicture": "Updated your profile picture",
|
"audit_table.updatePicture": "Updated your profile picture",
|
||||||
"access_history.updateGeneral": "Updated the general settings of your account",
|
"audit_table.updateGeneral": "Updated the general settings of your account",
|
||||||
"access_history.attemptedPassword": "Attempted to change password",
|
"audit_table.attemptedPassword": "Attempted to change password",
|
||||||
"access_history.successfullPassword": "Successfully changed password",
|
"audit_table.successfullPassword": "Successfully changed password",
|
||||||
"access_history.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
|
"audit_table.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
|
||||||
"access_history.updatedRol": "Updated user role(s) to ",
|
"audit_table.updatedRol": "Updated user role(s) to ",
|
||||||
"access_history.member": "member",
|
"audit_table.member": "member",
|
||||||
"access_history.accountActive": "Account made active",
|
"audit_table.accountActive": "Account made active",
|
||||||
"access_history.accountInactive": "Account made inactive",
|
"audit_table.accountInactive": "Account made inactive",
|
||||||
"access_history.by": " by {username}",
|
"audit_table.by": " by {username}",
|
||||||
"access_history.byAdmin": " by an admin",
|
"audit_table.byAdmin": " by an admin",
|
||||||
"access_history.sentEmail": "Sent an email to {email} to reset your password",
|
"audit_table.sentEmail": "Sent an email to {email} to reset your password",
|
||||||
"access_history.attemptedReset": "Attempted to reset password",
|
"audit_table.attemptedReset": "Attempted to reset password",
|
||||||
"access_history.successfullReset": "Successfully reset password",
|
"audit_table.successfullReset": "Successfully reset password",
|
||||||
"access_history.updateGlobalNotifications": "Updated your global notification settings",
|
"audit_table.updateGlobalNotifications": "Updated your global notification settings",
|
||||||
"access_history.attemptedWebhookCreate": "Attempted to create a webhook",
|
"audit_table.attemptedWebhookCreate": "Attempted to create a webhook",
|
||||||
"access_history.successfullWebhookCreate": "Successfully created a webhook",
|
"audit_table.successfullWebhookCreate": "Successfully created a webhook",
|
||||||
"access_history.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
|
"audit_table.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
|
||||||
"access_history.attemptedWebhookDelete": "Attempted to delete a webhook",
|
"audit_table.attemptedWebhookDelete": "Attempted to delete a webhook",
|
||||||
"access_history.successfullWebhookDelete": "Successfully deleted a webhook",
|
"audit_table.successfullWebhookDelete": "Successfully deleted a webhook",
|
||||||
"access_history.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
|
"audit_table.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
|
||||||
"access_history.logout": "Logged out of your account",
|
"audit_table.logout": "Logged out of your account",
|
||||||
"access_history.verified": "Sucessfully verified your email address",
|
"audit_table.verified": "Sucessfully verified your email address",
|
||||||
"access_history.revokedAll": "Revoked all current sessions for the team",
|
"audit_table.revokedAll": "Revoked all current sessions for the team",
|
||||||
"access_history.loginAttempt": " (Login attempt)",
|
"audit_table.loginAttempt": " (Login attempt)",
|
||||||
"access_history.loginFailure": " (Login failure)",
|
"audit_table.loginFailure": " (Login failure)",
|
||||||
"access_history.moreInfo": "More info",
|
"audit_table.moreInfo": "More info",
|
||||||
"access_history.ip": "IP: {ip}",
|
"audit_table.ip": "IP: {ip}",
|
||||||
"access_history.session": "Session ID: {id}",
|
"audit_table.session": "Session ID: {id}",
|
||||||
|
"audit_table.userId": "User ID",
|
||||||
"access_history.title": "Access History",
|
"access_history.title": "Access History",
|
||||||
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
|
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
|
||||||
"activity_log_modal.androidNativeApp": "Android Native App",
|
"activity_log_modal.androidNativeApp": "Android Native App",
|
||||||
@@ -96,11 +97,19 @@
|
|||||||
"admin.sidebar.teams": "TEAMS ({count})",
|
"admin.sidebar.teams": "TEAMS ({count})",
|
||||||
"admin.sidebar.other": "OTHER",
|
"admin.sidebar.other": "OTHER",
|
||||||
"admin.sidebar.logs": "Logs",
|
"admin.sidebar.logs": "Logs",
|
||||||
|
"admin.sidebar.audits": "Audits",
|
||||||
"admin.analytics.loading": "Loading...",
|
"admin.analytics.loading": "Loading...",
|
||||||
"admin.analytics.totalUsers": "Total Users",
|
"admin.analytics.totalUsers": "Total Users",
|
||||||
"admin.analytics.publicChannels": "Public Channels",
|
"admin.analytics.publicChannels": "Public Channels",
|
||||||
"admin.analytics.privateGroups": "Private Groups",
|
"admin.analytics.privateGroups": "Private Groups",
|
||||||
"admin.analytics.totalPosts": "Total Posts",
|
"admin.analytics.totalPosts": "Total Posts",
|
||||||
|
"admin.analytics.totalFilePosts": "Posts with Files",
|
||||||
|
"admin.analytics.totalHashtagPosts": "Posts with Hashtags",
|
||||||
|
"admin.analytics.totalIncomingWebhooks": "Incoming Webhooks",
|
||||||
|
"admin.analytics.totalOutgoingWebhooks": "Outgoing Webhooks",
|
||||||
|
"admin.analytics.channelTypes": "Channel Types",
|
||||||
|
"admin.analytics.textPosts": "Posts with Text-only",
|
||||||
|
"admin.analytics.postTypes": "Posts, Files and Hashtags",
|
||||||
"admin.analytics.meaningful": "Not enough data for a meaningful representation.",
|
"admin.analytics.meaningful": "Not enough data for a meaningful representation.",
|
||||||
"admin.analytics.activeUsers": "Active Users With Posts",
|
"admin.analytics.activeUsers": "Active Users With Posts",
|
||||||
"admin.analytics.recentActive": "Recent Active Users",
|
"admin.analytics.recentActive": "Recent Active Users",
|
||||||
@@ -127,6 +136,10 @@
|
|||||||
"admin.email.true": "true",
|
"admin.email.true": "true",
|
||||||
"admin.email.false": "false",
|
"admin.email.false": "false",
|
||||||
"admin.email.allowSignupDescription": "When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.",
|
"admin.email.allowSignupDescription": "When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.",
|
||||||
|
"admin.email.allowEmailSignInTitle": "Allow Sign In With Email: ",
|
||||||
|
"admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
|
||||||
|
"admin.email.allowUsernameSignInTitle": "Allow Sign In With Username: ",
|
||||||
|
"admin.email.allowUsernameSignInDescription": "When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.",
|
||||||
"admin.email.notificationsTitle": "Send Email Notifications: ",
|
"admin.email.notificationsTitle": "Send Email Notifications: ",
|
||||||
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
|
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
|
||||||
"admin.email.requireVerificationTitle": "Require Email Verification: ",
|
"admin.email.requireVerificationTitle": "Require Email Verification: ",
|
||||||
@@ -326,6 +339,8 @@
|
|||||||
"admin.log.save": "Save",
|
"admin.log.save": "Save",
|
||||||
"admin.logs.title": "Server Logs",
|
"admin.logs.title": "Server Logs",
|
||||||
"admin.logs.reload": "Reload",
|
"admin.logs.reload": "Reload",
|
||||||
|
"admin.audits.title": "Server Audits",
|
||||||
|
"admin.audits.reload": "Reload",
|
||||||
"admin.privacy.saving": "Saving Config...",
|
"admin.privacy.saving": "Saving Config...",
|
||||||
"admin.privacy.title": "Privacy Settings",
|
"admin.privacy.title": "Privacy Settings",
|
||||||
"admin.privacy.showEmailTitle": "Show Email Address: ",
|
"admin.privacy.showEmailTitle": "Show Email Address: ",
|
||||||
@@ -484,6 +499,40 @@
|
|||||||
"change_url.noUnderscore": "Can not contain two underscores in a row.",
|
"change_url.noUnderscore": "Can not contain two underscores in a row.",
|
||||||
"change_url.invalidUrl": "Invalid URL",
|
"change_url.invalidUrl": "Invalid URL",
|
||||||
"change_url.close": "Close",
|
"change_url.close": "Close",
|
||||||
|
"channel_header.recentMentions": "Recent Mentions",
|
||||||
|
"channel_header.channel": "Channel",
|
||||||
|
"channel_header.group": "Group",
|
||||||
|
"channel_header.channelHeader": "Set Channel Header...",
|
||||||
|
"channel_header.viewInfo": "View Info",
|
||||||
|
"chanel_header.addMembers": "Add Members",
|
||||||
|
"channel_header.manageMembers": "Manage Members",
|
||||||
|
"channel_header.setHeader": "Set {term} Header...",
|
||||||
|
"channel_header.setPurpose": "Set {term} Purpose...",
|
||||||
|
"channel_header.notificationPreferences": "Notification Preferences",
|
||||||
|
"channel_header.rename": "Rename {term}...",
|
||||||
|
"channel_header.delete": "Delete {term}...",
|
||||||
|
"channel_header.leave": "Leave {term}",
|
||||||
|
"channel_info.notFound": "No Channel Found",
|
||||||
|
"channel_info.name": "Channel Name:",
|
||||||
|
"channel_info.url": "Channel URL:",
|
||||||
|
"channel_info.id": "Channel ID:",
|
||||||
|
"channel_info.purpose": "Channel Purpose:",
|
||||||
|
"channel_info.close": "Close",
|
||||||
|
"channel_invite.addNewMembers": "Add New Members to ",
|
||||||
|
"channel_invite.close": "Close",
|
||||||
|
"channel_memebers_modal.members": " Members",
|
||||||
|
"channel_members_modal.addNew": " Add New Members",
|
||||||
|
"channel_members_modal.close": "Close",
|
||||||
|
"channel_notifications.allActivity": "For all activity",
|
||||||
|
"channel_notifications.onlyMentions": "Only for mentions",
|
||||||
|
"channel_notifications.never": "Never",
|
||||||
|
"channel_notifications.sendDesktop": "Send desktop notifications",
|
||||||
|
"channel_notifications.globalDefault": "Global default ({notifyLevel}",
|
||||||
|
"channel_notifications.override": "Selecting an option other than \"Default\" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.",
|
||||||
|
"channel_notifications.markUnread": "Mark Channel Unread",
|
||||||
|
"channel_notifications.allUnread": "For all unread messages",
|
||||||
|
"channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.",
|
||||||
|
"channel_notifications.preferences": "Notification Preferences for ",
|
||||||
"claim.account.noEmail": "No email specified",
|
"claim.account.noEmail": "No email specified",
|
||||||
"claim.email_to_sso.pwdError": "Please enter your password.",
|
"claim.email_to_sso.pwdError": "Please enter your password.",
|
||||||
"claim.email_to_sso.pwd": "Password",
|
"claim.email_to_sso.pwd": "Password",
|
||||||
@@ -506,6 +555,36 @@
|
|||||||
"create_comment.commentTitle": "Comment",
|
"create_comment.commentTitle": "Comment",
|
||||||
"create_comment.file": "File uploading",
|
"create_comment.file": "File uploading",
|
||||||
"create_comment.files": "Files uploading",
|
"create_comment.files": "Files uploading",
|
||||||
|
"delete_channel.channel": "channel",
|
||||||
|
"delete_channel.group": "group",
|
||||||
|
"delete_channel.confirm": "Confirm DELETE Channel",
|
||||||
|
"delete_channel.question": "Are you sure you wish to delete the {display_name} {term}?",
|
||||||
|
"delete_channel.cancel": "Cancel",
|
||||||
|
"delete_channel.del": "Delete",
|
||||||
|
"delete_post.warning": "This post has {count} comment(s) on it.",
|
||||||
|
"delete_post.comment": "Comment",
|
||||||
|
"delete_post.post": "Post",
|
||||||
|
"delete_post.confirm": "Confirm {term} Delete",
|
||||||
|
"delete_post.question": "Are you sure you want to delete this ${term}?",
|
||||||
|
"delete_post.cancel": "Cancel",
|
||||||
|
"delete_post.del": "Delete",
|
||||||
|
"edit_channel_header_modal.error": "This channel header is too long, please enter a shorter one",
|
||||||
|
"edit_channel_header_modal.title": "Edit Header for {channel}",
|
||||||
|
"edit_channel_header_modal.description": "Edit the text appearing next to the channel name in the channel header.",
|
||||||
|
"edit_channel_header_modal.cancel": "Cancel",
|
||||||
|
"edit_channel_header_modal.save": "Save",
|
||||||
|
"edit_channel_purpose_modal.error": "This channel purpose is too long, please enter a shorter one",
|
||||||
|
"edit_channel_purpose_modal.title1": "Edit Purpose",
|
||||||
|
"edit_channel_purpose_modal.title2": "Edit Purpose for ",
|
||||||
|
"edit_channel_purpose_modal.channel": "Channel",
|
||||||
|
"edit_channel_purpose_modal.group": "Group",
|
||||||
|
"edit_channel_purpose_modal.body": "Describe how this {type} should be used. This text appears in the channel list in the \"More...\" menu and helps others decide whether to join.",
|
||||||
|
"edit_channel_purpose_modal.cancel": "Cancel",
|
||||||
|
"edit_channel_purpose_modal.save": "Save",
|
||||||
|
"edit_post.editPost": "Edit the post...",
|
||||||
|
"edit_post.edit": "Edit {title}",
|
||||||
|
"edit_post.cancel": "Cancel",
|
||||||
|
"edit_post.save": "Save",
|
||||||
"email_verify.verified": "{siteName} Email Verified",
|
"email_verify.verified": "{siteName} Email Verified",
|
||||||
"email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>",
|
"email_verify.verifiedBody": "<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>",
|
||||||
"email_verify.almost": "{siteName}: You are almost done",
|
"email_verify.almost": "{siteName}: You are almost done",
|
||||||
@@ -530,6 +609,7 @@
|
|||||||
"get_link.close": "Close",
|
"get_link.close": "Close",
|
||||||
"get_team_invite_link_modal.title": "Team Invite Link",
|
"get_team_invite_link_modal.title": "Team Invite Link",
|
||||||
"get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site.",
|
"get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site.",
|
||||||
|
"get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.",
|
||||||
"invite_member.emailError": "Please enter a valid email address",
|
"invite_member.emailError": "Please enter a valid email address",
|
||||||
"invite_member.firstname": "First name",
|
"invite_member.firstname": "First name",
|
||||||
"invite_member.lastname": "Last name",
|
"invite_member.lastname": "Last name",
|
||||||
@@ -554,6 +634,14 @@
|
|||||||
"login_email.email": "Email",
|
"login_email.email": "Email",
|
||||||
"login_email.pwd": "Password",
|
"login_email.pwd": "Password",
|
||||||
"login_email.signin": "Sign in",
|
"login_email.signin": "Sign in",
|
||||||
|
"login_username.badTeam": "Bad team name",
|
||||||
|
"login_username.usernameReq": "A username is required",
|
||||||
|
"login_username.pwdReq": "A password is required",
|
||||||
|
"login_username.verifyEmailError": "Please verify your email address. Check your inbox for an email.",
|
||||||
|
"login_username.userNotFoundError": "We couldn't find an existing account matching your username for this team.",
|
||||||
|
"login_username.username": "Username",
|
||||||
|
"login_username.pwd": "Password",
|
||||||
|
"login_username.signin": "Sign in",
|
||||||
"login_ldap.badTeam": "Bad team name",
|
"login_ldap.badTeam": "Bad team name",
|
||||||
"login_ldap.idlReq": "An LDAP ID is required",
|
"login_ldap.idlReq": "An LDAP ID is required",
|
||||||
"login_ldap.pwdReq": "An LDAP password is required",
|
"login_ldap.pwdReq": "An LDAP password is required",
|
||||||
@@ -572,6 +660,10 @@
|
|||||||
"login.find": "Find your other teams",
|
"login.find": "Find your other teams",
|
||||||
"login.signTo": "Sign in to:",
|
"login.signTo": "Sign in to:",
|
||||||
"login.on": "on {siteName}",
|
"login.on": "on {siteName}",
|
||||||
|
"member_item.add": " Add",
|
||||||
|
"member_item.makeAdmin": "Make Admin",
|
||||||
|
"member_item.removeMember": "Remove Member",
|
||||||
|
"member_item.member": "Member",
|
||||||
"member_team_item.member": "Member",
|
"member_team_item.member": "Member",
|
||||||
"member_team_item.systemAdmin": "System Admin",
|
"member_team_item.systemAdmin": "System Admin",
|
||||||
"member_team_item.teamAdmin": "Team Admin",
|
"member_team_item.teamAdmin": "Team Admin",
|
||||||
@@ -647,6 +739,11 @@
|
|||||||
"password_send.title": "Password Reset",
|
"password_send.title": "Password Reset",
|
||||||
"password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.",
|
"password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.",
|
||||||
"password_send.reset": "Reset my password",
|
"password_send.reset": "Reset my password",
|
||||||
|
"members_popover.msg": "Message",
|
||||||
|
"members_popover.title": "Members",
|
||||||
|
"post_delete.notPosted": "Comment could not be posted",
|
||||||
|
"post_delete.someone": "Someone deleted the message on which you tried to post a comment.",
|
||||||
|
"post_delete.okay": "Okay",
|
||||||
"register_app.required": "Required",
|
"register_app.required": "Required",
|
||||||
"register_app.optional": "Optional",
|
"register_app.optional": "Optional",
|
||||||
"register_app.nameError": "Application name must be filled in.",
|
"register_app.nameError": "Application name must be filled in.",
|
||||||
@@ -666,6 +763,23 @@
|
|||||||
"register_app.credentialsSave": "I have saved both my Client Id and Client Secret somewhere safe",
|
"register_app.credentialsSave": "I have saved both my Client Id and Client Secret somewhere safe",
|
||||||
"register_app.close": "Close",
|
"register_app.close": "Close",
|
||||||
"register_app.dev": "Developer Applications",
|
"register_app.dev": "Developer Applications",
|
||||||
|
"removed_channel.channelName": "the channel",
|
||||||
|
"removed_channel.someone": "Someone",
|
||||||
|
"removed_channel.from": "Removed from ",
|
||||||
|
"removed_channel.remover": "{remover} removed you from {channel}",
|
||||||
|
"removed_channel.okay": "Okay",
|
||||||
|
"rename_channel.required": "This field is required",
|
||||||
|
"rename_channel.maxLength": "This field must be less than 22 characters",
|
||||||
|
"rename_channel.lowercase": "Must be lowercase alphanumeric characters",
|
||||||
|
"rename_channel.handle": "Handle",
|
||||||
|
"rename_channel.defaultError": " - Cannot be changed for the default channel",
|
||||||
|
"rename_channel.displayNameHolder": "Enter display name",
|
||||||
|
"rename_channel.handleHolder": "lowercase alphanumeric's only",
|
||||||
|
"rename_channel.close": "Close",
|
||||||
|
"rename_channel.title": "Rename Channel",
|
||||||
|
"rename_channel.displayName": "Display Name",
|
||||||
|
"rename_channel.cancel": "Cancel",
|
||||||
|
"rename_channel.save": "Save",
|
||||||
"rhs_comment.comment": "Comment",
|
"rhs_comment.comment": "Comment",
|
||||||
"rhs_comment.edit": "Edit",
|
"rhs_comment.edit": "Edit",
|
||||||
"rhs_comment.del": "Delete",
|
"rhs_comment.del": "Delete",
|
||||||
@@ -1081,4 +1195,4 @@
|
|||||||
"user.settings.security.title": "Security Settings",
|
"user.settings.security.title": "Security Settings",
|
||||||
"user.settings.security.viewHistory": "View Access History",
|
"user.settings.security.viewHistory": "View Access History",
|
||||||
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions"
|
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,6 +470,7 @@
|
|||||||
"authorize.app": "La app {appName} quiere tener la abilidad de accesar y modificar tu información básica.",
|
"authorize.app": "La app {appName} quiere tener la abilidad de accesar y modificar tu información básica.",
|
||||||
"authorize.deny": "Denegar",
|
"authorize.deny": "Denegar",
|
||||||
"authorize.title": "Una aplicación quiere conectarse con tu cuenta de {teamName}",
|
"authorize.title": "Una aplicación quiere conectarse con tu cuenta de {teamName}",
|
||||||
|
"chanel_header.addMembers": "Agregar Miembros",
|
||||||
"change_url.close": "Cerrar",
|
"change_url.close": "Cerrar",
|
||||||
"change_url.endWithLetter": "Debe terminar con una letra o número",
|
"change_url.endWithLetter": "Debe terminar con una letra o número",
|
||||||
"change_url.invalidUrl": "URL Inválida",
|
"change_url.invalidUrl": "URL Inválida",
|
||||||
@@ -484,6 +485,29 @@
|
|||||||
"channel_flow.group": "Grupo",
|
"channel_flow.group": "Grupo",
|
||||||
"channel_flow.invalidName": "Nombre de Canal Inválido",
|
"channel_flow.invalidName": "Nombre de Canal Inválido",
|
||||||
"channel_flow.set_url_title": "Asignar URL de {term}",
|
"channel_flow.set_url_title": "Asignar URL de {term}",
|
||||||
|
"channel_header.channel": "Canal",
|
||||||
|
"channel_header.channelHeader": "Encabezado del Canal...",
|
||||||
|
"channel_header.delete": "Eliminar {term}...",
|
||||||
|
"channel_header.group": "Grupo",
|
||||||
|
"channel_header.leave": "Abondanar {term}",
|
||||||
|
"channel_header.manageMembers": "Administrar Miembros",
|
||||||
|
"channel_header.notificationPreferences": "Preferencias de Notificación",
|
||||||
|
"channel_header.recentMentions": "Menciones recientes",
|
||||||
|
"channel_header.rename": "Renombrar {term}...",
|
||||||
|
"channel_header.setHeader": "Encabezado del {term}...",
|
||||||
|
"channel_header.setPurpose": "Propósito del {term}...",
|
||||||
|
"channel_header.viewInfo": "Ver Info",
|
||||||
|
"channel_info.close": "Cerrar",
|
||||||
|
"channel_info.id": "ID del Canal:",
|
||||||
|
"channel_info.name": "Nombre del Canal:",
|
||||||
|
"channel_info.notFound": "Canal no encontrado",
|
||||||
|
"channel_info.purpose": "Propósito del Canal:",
|
||||||
|
"channel_info.url": "URL del Canal:",
|
||||||
|
"channel_invite.addNewMembers": "Agregar nuevos Miembros a ",
|
||||||
|
"channel_invite.close": "Cerrar",
|
||||||
|
"channel_members_modal.addNew": " Agregar nuevos Miembros",
|
||||||
|
"channel_members_modal.close": "Cerrar",
|
||||||
|
"channel_memebers_modal.members": " Miembros",
|
||||||
"channel_modal.cancel": "Cancelar",
|
"channel_modal.cancel": "Cancelar",
|
||||||
"channel_modal.channel": "Canal",
|
"channel_modal.channel": "Canal",
|
||||||
"channel_modal.createNew": "Crear Nuevo ",
|
"channel_modal.createNew": "Crear Nuevo ",
|
||||||
@@ -500,6 +524,16 @@
|
|||||||
"channel_modal.publicChannel1": "Crear un canal público",
|
"channel_modal.publicChannel1": "Crear un canal público",
|
||||||
"channel_modal.publicChannel2": "Crear un canal público al que cualquiera puede unirse. ",
|
"channel_modal.publicChannel2": "Crear un canal público al que cualquiera puede unirse. ",
|
||||||
"channel_modal.purpose": "Propósito",
|
"channel_modal.purpose": "Propósito",
|
||||||
|
"channel_notifications.allActivity": "Para toda actividad",
|
||||||
|
"channel_notifications.allUnread": "Para todos los mensajes sin leer",
|
||||||
|
"channel_notifications.globalDefault": "Predeterminado global ({notifyLevel})",
|
||||||
|
"channel_notifications.markUnread": "Marcar Canal como No Leido",
|
||||||
|
"channel_notifications.never": "Nunca",
|
||||||
|
"channel_notifications.onlyMentions": "Sólo para menciones",
|
||||||
|
"channel_notifications.override": "Seleccionar una opción diferente a \"Predeterminada\" anulará las configuraciones globales de notificación. Las notificaciones de Escritorio están disponibles para Firefox, Safari, y Chrome.",
|
||||||
|
"channel_notifications.preferences": "Preferencias de Notificación de ",
|
||||||
|
"channel_notifications.sendDesktop": "Enviar notificaciones de escritorio",
|
||||||
|
"channel_notifications.unreadInfo": "El nombre del canal está en negritas en la barra lateral cuando hay mensajes sin leer. Al elegir \"Sólo para menciones\" sólo lo dejará en negritas cuando seas mencionado.",
|
||||||
"choose_auth_page.emailCreate": "Crea un nuevo equipo con tu cuenta de correo",
|
"choose_auth_page.emailCreate": "Crea un nuevo equipo con tu cuenta de correo",
|
||||||
"choose_auth_page.find": "Encontrar mi equipo",
|
"choose_auth_page.find": "Encontrar mi equipo",
|
||||||
"choose_auth_page.gitlabCreate": "Crear un nuevo equipo con una cuenta de GitLab",
|
"choose_auth_page.gitlabCreate": "Crear un nuevo equipo con una cuenta de GitLab",
|
||||||
@@ -527,6 +561,36 @@
|
|||||||
"create_comment.commentTitle": "Comentario",
|
"create_comment.commentTitle": "Comentario",
|
||||||
"create_comment.file": "Subiendo archivo",
|
"create_comment.file": "Subiendo archivo",
|
||||||
"create_comment.files": "Subiendo archivos",
|
"create_comment.files": "Subiendo archivos",
|
||||||
|
"delete_channel.cancel": "Cancelar",
|
||||||
|
"delete_channel.channel": "canal",
|
||||||
|
"delete_channel.confirm": "Confirmar la ELIMINACIÓN del Canal",
|
||||||
|
"delete_channel.del": "Eliminar",
|
||||||
|
"delete_channel.group": "grupo",
|
||||||
|
"delete_channel.question": "¿Estás seguro de querer eliminar el {term} {display_name}?",
|
||||||
|
"delete_post.cancel": "Cancelar",
|
||||||
|
"delete_post.comment": "Comentario",
|
||||||
|
"delete_post.confirm": "Confirmar Eliminación del {term}",
|
||||||
|
"delete_post.del": "Eliminar",
|
||||||
|
"delete_post.post": "Mensaje",
|
||||||
|
"delete_post.question": "¿Estás seguro(a) de querer eliminar este {term}?",
|
||||||
|
"delete_post.warning": "Este mensaje tiene {count} comentario(s).",
|
||||||
|
"edit_channel_header_modal.cancel": "Cancelar",
|
||||||
|
"edit_channel_header_modal.description": "Edita el texto que aparece al lado del nombre del canal en el encabezado del canal.",
|
||||||
|
"edit_channel_header_modal.error": "Este encabezado es demasiado largo, por favor ingresa uno más corto",
|
||||||
|
"edit_channel_header_modal.save": "Guardar",
|
||||||
|
"edit_channel_header_modal.title": "Edita el Encabezado de {channel}",
|
||||||
|
"edit_channel_purpose_modal.body": "Describe como este {type} debería ser usado. Este texto aparace en la lista de canales dentro del menú de \"Más...\" y ayuda a otros en decidir si unirse.",
|
||||||
|
"edit_channel_purpose_modal.cancel": "Cancelar",
|
||||||
|
"edit_channel_purpose_modal.channel": "Canal",
|
||||||
|
"edit_channel_purpose_modal.error": "El propósito de este canal es muy largo, por favor ingresa uno más corto",
|
||||||
|
"edit_channel_purpose_modal.group": "Grupo",
|
||||||
|
"edit_channel_purpose_modal.save": "Guardar",
|
||||||
|
"edit_channel_purpose_modal.title1": "Editar Propósito",
|
||||||
|
"edit_channel_purpose_modal.title2": "Editar el Propósito de ",
|
||||||
|
"edit_post.cancel": "Cancelar",
|
||||||
|
"edit_post.edit": "Editar {title}",
|
||||||
|
"edit_post.editPost": "Editar el mensaje...",
|
||||||
|
"edit_post.save": "Guardar",
|
||||||
"email_signup.address": "Correo electrónico",
|
"email_signup.address": "Correo electrónico",
|
||||||
"email_signup.createTeam": "Crear Equipo",
|
"email_signup.createTeam": "Crear Equipo",
|
||||||
"email_signup.emailError": "Por favor ingresa una dirección de correos válida",
|
"email_signup.emailError": "Por favor ingresa una dirección de correos válida",
|
||||||
@@ -615,6 +679,10 @@
|
|||||||
"login_ldap.pwdReq": "La contraseña LDAP es obligatoria",
|
"login_ldap.pwdReq": "La contraseña LDAP es obligatoria",
|
||||||
"login_ldap.signin": "Entrar",
|
"login_ldap.signin": "Entrar",
|
||||||
"login_ldap.username": "Usuario LDAP",
|
"login_ldap.username": "Usuario LDAP",
|
||||||
|
"member_item.add": " Agregar",
|
||||||
|
"member_item.makeAdmin": "Convertir en Admin de Equipo",
|
||||||
|
"member_item.member": "Miembro",
|
||||||
|
"member_item.removeMember": "Elminar Miembro",
|
||||||
"member_team_item.inactive": "Inactivo",
|
"member_team_item.inactive": "Inactivo",
|
||||||
"member_team_item.makeActive": "Activar",
|
"member_team_item.makeActive": "Activar",
|
||||||
"member_team_item.makeAdmin": "Convertir a Admin de Equipo",
|
"member_team_item.makeAdmin": "Convertir a Admin de Equipo",
|
||||||
@@ -623,6 +691,8 @@
|
|||||||
"member_team_item.member": "Miembro",
|
"member_team_item.member": "Miembro",
|
||||||
"member_team_item.systemAdmin": "Administrador de Sistema",
|
"member_team_item.systemAdmin": "Administrador de Sistema",
|
||||||
"member_team_item.teamAdmin": "Admin de Equipo",
|
"member_team_item.teamAdmin": "Admin de Equipo",
|
||||||
|
"members_popover.msg": "Mensaje",
|
||||||
|
"members_popover.title": "Miembros",
|
||||||
"more_channels.close": "Cerrar",
|
"more_channels.close": "Cerrar",
|
||||||
"more_channels.create": "Crear Nuevo Canal",
|
"more_channels.create": "Crear Nuevo Canal",
|
||||||
"more_channels.createClick": "Pincha 'Crear Nuevo Canal' para crear uno nuevo",
|
"more_channels.createClick": "Pincha 'Crear Nuevo Canal' para crear uno nuevo",
|
||||||
@@ -666,6 +736,9 @@
|
|||||||
"password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>",
|
"password_send.link": "<p>Se ha enviado un enlace para restablecer la contraseña a <b>{email}</b> para tu equipo <b>{teamDisplayName}</b> en {hostname}.</p>",
|
||||||
"password_send.reset": "Restablecer mi contraseña",
|
"password_send.reset": "Restablecer mi contraseña",
|
||||||
"password_send.title": "Restablecer Contraseña",
|
"password_send.title": "Restablecer Contraseña",
|
||||||
|
"post_delete.notPosted": "No se pudo enviar el comentario",
|
||||||
|
"post_delete.okay": "Ok",
|
||||||
|
"post_delete.someone": "Alguien borró el mensaje que querías comentar.",
|
||||||
"register_app.callback": "Callback URL",
|
"register_app.callback": "Callback URL",
|
||||||
"register_app.callbackError": "Al menos un callback URL debe ser ingresado.",
|
"register_app.callbackError": "Al menos un callback URL debe ser ingresado.",
|
||||||
"register_app.cancel": "Cancelar",
|
"register_app.cancel": "Cancelar",
|
||||||
@@ -685,6 +758,23 @@
|
|||||||
"register_app.register": "Registrar",
|
"register_app.register": "Registrar",
|
||||||
"register_app.required": "Requerido",
|
"register_app.required": "Requerido",
|
||||||
"register_app.title": "Registra una Nueva Aplicación",
|
"register_app.title": "Registra una Nueva Aplicación",
|
||||||
|
"removed_channel.channelName": "el canal",
|
||||||
|
"removed_channel.from": "Removido de ",
|
||||||
|
"removed_channel.okay": "OK",
|
||||||
|
"removed_channel.remover": "{remover} te removió de {channel}",
|
||||||
|
"removed_channel.someone": "Alguien",
|
||||||
|
"rename_channel.cancel": "Cancelar",
|
||||||
|
"rename_channel.close": "Cerrado",
|
||||||
|
"rename_channel.defaultError": " - No se puede cambiar el del canal predeterminado",
|
||||||
|
"rename_channel.displayName": "Nombre a Mostrar",
|
||||||
|
"rename_channel.displayNameHolder": "Ingresa el nombre a mostrar",
|
||||||
|
"rename_channel.handle": "Identificador",
|
||||||
|
"rename_channel.handleHolder": "Sólo caracteres alfanumericos y en minúscula",
|
||||||
|
"rename_channel.lowercase": "Debe tener caracteres alfanumericos y minúscula",
|
||||||
|
"rename_channel.maxLength": "Este campo debe tener menos de 22 caracteres",
|
||||||
|
"rename_channel.required": "Este campo es obligatorio",
|
||||||
|
"rename_channel.save": "Guardar",
|
||||||
|
"rename_channel.title": "Renombrar Canal",
|
||||||
"rhs_comment.comment": "Comentario",
|
"rhs_comment.comment": "Comentario",
|
||||||
"rhs_comment.del": "Borrar",
|
"rhs_comment.del": "Borrar",
|
||||||
"rhs_comment.edit": "Editar",
|
"rhs_comment.edit": "Editar",
|
||||||
@@ -1041,4 +1131,4 @@
|
|||||||
"user.settings.security.title": "Configuración de Seguridad",
|
"user.settings.security.title": "Configuración de Seguridad",
|
||||||
"user.settings.security.viewHistory": "Visualizar historial de acceso",
|
"user.settings.security.viewHistory": "Visualizar historial de acceso",
|
||||||
"user_profile.notShared": "Correo no compartido"
|
"user_profile.notShared": "Correo no compartido"
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user