summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/export.go2
-rw-r--r--api/team.go48
-rw-r--r--api/team_test.go33
-rw-r--r--api/user.go91
-rw-r--r--api/user_test.go21
-rw-r--r--model/client.go12
-rw-r--r--model/team.go20
-rw-r--r--store/sql_session_store.go18
-rw-r--r--store/sql_session_store_test.go23
-rw-r--r--store/sql_team_store.go4
-rw-r--r--store/sql_user_store.go19
-rw-r--r--store/sql_user_store_test.go18
-rw-r--r--store/store.go4
-rw-r--r--web/react/components/admin_console/admin_controller.jsx84
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx179
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx193
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/stores/admin_store.jsx40
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx29
-rw-r--r--web/react/utils/constants.jsx2
-rw-r--r--web/react/utils/markdown.jsx16
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss8
-rw-r--r--web/sass-files/sass/partials/_markdown.scss8
26 files changed, 1302 insertions, 172 deletions
diff --git a/api/export.go b/api/export.go
index 73142a0e4..aff34073f 100644
--- a/api/export.go
+++ b/api/export.go
@@ -87,7 +87,7 @@ func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
// Get the teams
var teams []*model.Team
if len(options.TeamsToExport) == 0 {
- if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
return result.Err
} else {
teams = result.Data.([]*model.Team)
diff --git a/api/team.go b/api/team.go
index c9d2412d3..d59b2b484 100644
--- a/api/team.go
+++ b/api/team.go
@@ -25,6 +25,7 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
+ sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
@@ -302,6 +303,53 @@ func isTreamCreationAllowed(c *Context, email string) bool {
return true
}
+func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getLogs") {
+ return
+ }
+
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teams := result.Data.([]*model.Team)
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
+ }
+
+ w.Write([]byte(model.TeamMapToJson(m)))
+ }
+}
+
+func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ id := props["id"]
+
+ if result := <-Srv.Store.Session().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ session := result.Data.(*model.Session)
+
+ c.LogAudit("revoked_all=" + id)
+
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ sessionCache.Remove(session.Token)
+
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(model.MapToJson(props)))
+ return
+ }
+ }
+ }
+}
+
func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
diff --git a/api/team_test.go b/api/team_test.go
index cd39dacfe..e2a7cf430 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -132,6 +132,39 @@ func TestFindTeamByEmail(t *testing.T) {
}
}
+func TestGetAllTeams(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetAllTeams(); err == nil {
+ t.Fatal("you 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 r1, err := Client.GetAllTeams(); err != nil {
+ t.Fatal(err)
+ } else {
+ teams := r1.Data.(map[string]*model.Team)
+ if teams[team.Id].Name != team.Name {
+ t.Fatal()
+ }
+ }
+}
+
/*
XXXXXX investigate and fix failing test
diff --git a/api/user.go b/api/user.go
index 40410cc5b..3cce3cdd3 100644
--- a/api/user.go
+++ b/api/user.go
@@ -51,6 +51,7 @@ func InitUser(r *mux.Router) {
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
+ sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET")
@@ -166,6 +167,19 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
if team.Email == user.Email {
user.Roles = model.ROLE_TEAM_ADMIN
channelRole = model.CHANNEL_ROLE_ADMIN
+
+ // Below is a speical case where the first user in the entire
+ // system is granted the system_admin role instead of admin
+ if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ user.Roles = model.ROLE_SYSTEM_ADMIN
+ }
+ }
+
} else {
user.Roles = ""
}
@@ -550,13 +564,26 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id, ok := params["id"]
+ if ok {
+ // You must be system admin to access another team
+ if id != c.Session.TeamId {
+ if !c.HasSystemAdminPermissions("getProfiles") {
+ return
+ }
+ }
- etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string)
+ } else {
+ id = c.Session.TeamId
+ }
+
+ etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string)
if HandleEtag(etag, w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -924,8 +951,8 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) {
- c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set from the command line", "")
+ if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set by another system admin", "")
c.Err.StatusCode = http.StatusForbidden
return
}
@@ -1155,29 +1182,35 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := props["hash"]
- if len(hash) == 0 {
- c.SetInvalidParam("resetPassword", "hash")
+ name := props["name"]
+ if len(name) == 0 {
+ c.SetInvalidParam("resetPassword", "name")
return
}
- data := model.MapFromJson(strings.NewReader(props["data"]))
+ userId := props["user_id"]
+ hash := props["hash"]
+ timeStr := ""
- userId := data["user_id"]
- if len(userId) != 26 {
- c.SetInvalidParam("resetPassword", "data:user_id")
- return
- }
+ if !c.IsSystemAdmin() {
+ if len(hash) == 0 {
+ c.SetInvalidParam("resetPassword", "hash")
+ return
+ }
- timeStr := data["time"]
- if len(timeStr) == 0 {
- c.SetInvalidParam("resetPassword", "data:time")
- return
+ data := model.MapFromJson(strings.NewReader(props["data"]))
+
+ userId = data["user_id"]
+
+ timeStr = data["time"]
+ if len(timeStr) == 0 {
+ c.SetInvalidParam("resetPassword", "data:time")
+ return
+ }
}
- name := props["name"]
- if len(name) == 0 {
- c.SetInvalidParam("resetPassword", "name")
+ if len(userId) != 26 {
+ c.SetInvalidParam("resetPassword", "user_id")
return
}
@@ -1205,15 +1238,17 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
- c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
- return
- }
+ if !c.IsSystemAdmin() {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
+ c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
+ return
+ }
- t, err := strconv.ParseInt(timeStr, 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
- return
+ t, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
+ c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
+ return
+ }
}
if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
diff --git a/api/user_test.go b/api/user_test.go
index 669da4d20..baa567dec 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -228,6 +228,13 @@ func TestGetUser(t *testing.T) {
ruser2, _ := Client.CreateUser(&user2, "")
store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam2, _ := Client.CreateTeam(&team2)
+
+ user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser3, _ := Client.CreateUser(&user3, "")
+ store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
+
Client.LoginByEmail(team.Name, user.Email, user.Password)
rId := ruser.Data.(*model.User).Id
@@ -276,13 +283,27 @@ func TestGetUser(t *testing.T) {
t.Log(cache_result.Data)
t.Fatal("cache should be empty")
}
+ }
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil {
+ t.Fatal("shouldn't have access")
}
Client.AuthToken = ""
if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil {
t.Fatal("shouldn't have accss")
}
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil {
+ t.Fatal(err)
+ }
}
func TestGetAudits(t *testing.T) {
diff --git a/model/client.go b/model/client.go
index cc75ce370..ca17da6d2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -150,6 +150,16 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
}
}
+func (c *Client) GetAllTeams() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/teams/all", "", ""); err != nil {
+ return nil, err
+ } else {
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@@ -254,7 +264,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil {
+ if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/model/team.go b/model/team.go
index 0d740dde2..f80fa3b11 100644
--- a/model/team.go
+++ b/model/team.go
@@ -73,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team {
}
}
+func TeamMapToJson(u map[string]*Team) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamMapFromJson(data io.Reader) map[string]*Team {
+ decoder := json.NewDecoder(data)
+ var teams map[string]*Team
+ err := decoder.Decode(&teams)
+ if err == nil {
+ return teams
+ } else {
+ return nil
+ }
+}
+
func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index c1d2c852b..22411389d 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -140,6 +140,24 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel {
return storeChannel
}
+func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForTeam", "We couldn't remove all the sessions for the team", "id="+teamId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 4ae680556..3d8aafe25 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -80,6 +80,29 @@ func TestSessionRemove(t *testing.T) {
}
}
+func TestSessionRemoveAll(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId))
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
func TestSessionRemoveToken(t *testing.T) {
Setup()
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 3d644e577..109fe5401 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -196,7 +196,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
return storeChannel
}
-func (s SqlTeamStore) GetForExport() StoreChannel {
+func (s SqlTeamStore) GetAll() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -204,7 +204,7 @@ func (s SqlTeamStore) GetForExport() StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error())
}
result.Data = data
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 3fd1c82b5..0a723d965 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -481,3 +481,22 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users"); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetTotalUsersCount", "We could not count the users", err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index 466da2845..e2a454023 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -206,6 +206,24 @@ func TestUserStoreGet(t *testing.T) {
}
}
+func TestUserCountt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if result := <-store.User().GetTotalUsersCount(); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ t.Fatal()
+ }
+ }
+}
+
func TestUserStoreGetProfiles(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index c9d40cfa5..23580f452 100644
--- a/store/store.go
+++ b/store/store.go
@@ -47,7 +47,7 @@ type TeamStore interface {
Get(id string) StoreChannel
GetByName(name string) StoreChannel
GetTeamsForEmail(domain string) StoreChannel
- GetForExport() StoreChannel
+ GetAll() StoreChannel
}
type ChannelStore interface {
@@ -103,6 +103,7 @@ type UserStore interface {
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
+ GetTotalUsersCount() StoreChannel
}
type SessionStore interface {
@@ -110,6 +111,7 @@ type SessionStore interface {
Get(sessionIdOrToken string) StoreChannel
GetSessions(userId string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
+ RemoveAllSessionsForTeam(teamId string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 6fddfef07..92f0bbdce 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -3,6 +3,7 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
@@ -16,38 +17,104 @@ var GitLabSettingsTab = require('./gitlab_settings.jsx');
var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
this.state = {
- config: null,
- selected: 'service_settings'
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
- selected: this.state.selected
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
});
}
- selectTab(tab) {
- this.setState({selected: tab});
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
}
render() {
@@ -74,6 +141,8 @@ export default class AdminController extends React.Component {
tab = <TeamSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
}
@@ -85,7 +154,12 @@ export default class AdminController extends React.Component {
/>
<AdminSidebar
selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 17ce39c7c..cebb3ff20 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component {
this.isSelected = this.isSelected.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.state = {
+ showSelectModal: false
};
}
- handleClick(name, e) {
+ handleClick(name, teamId, e) {
e.preventDefault();
- this.props.selectTab(name);
+ this.props.selectTab(name, teamId);
}
- isSelected(name) {
+ isSelected(name, teamId) {
if (this.props.selected === name) {
- return 'active';
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
}
return '';
}
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
componentDidMount() {
}
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -39,10 +133,16 @@ export default class AdminSidebar extends React.Component {
<li>
<ul className='nav nav__sub-menu'>
<li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ <li>
<a
href='#'
className={this.isSelected('service_settings')}
- onClick={this.handleClick.bind(this, 'service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
>
{'Service Settings'}
</a>
@@ -51,7 +151,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('team_settings')}
- onClick={this.handleClick.bind(this, 'team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
>
{'Team Settings'}
</a>
@@ -60,7 +160,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('sql_settings')}
- onClick={this.handleClick.bind(this, 'sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
>
{'SQL Settings'}
</a>
@@ -69,7 +169,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
>
{'Email Settings'}
</a>
@@ -78,7 +178,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('image_settings')}
- onClick={this.handleClick.bind(this, 'image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
>
{'File Settings'}
</a>
@@ -87,7 +187,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
>
{'Log Settings'}
</a>
@@ -95,17 +195,8 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
- className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs')}
- >
- {'Logs'}
- </a>
- </li>
- <li>
- <a
- href='#'
className={this.isSelected('rate_settings')}
- onClick={this.handleClick.bind(this, 'rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
>
{'Rate Limit Settings'}
</a>
@@ -114,7 +205,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('privacy_settings')}
- onClick={this.handleClick.bind(this, 'privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
>
{'Privacy Settings'}
</a>
@@ -123,21 +214,65 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('gitlab_settings')}
- onClick={this.handleClick.bind(this, 'gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
>
{'GitLab Settings'}
</a>
</li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ <li>
+ {teams}
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
+ >
+ {'Logs'}
+ </a>
+ </li>
</ul>
</li>
</ul>
</div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
</div>
);
}
}
AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
}; \ No newline at end of file
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index fa30de7b2..343f65131 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -1,124 +1,99 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-export default class SelectTeam extends React.Component {
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
}
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
return (
- <div className='modal fade'
- id='select-team'
- tabIndex='-1'
- role='dialog'
- aria-labelledby='teamsModalLabel'
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
>
- <div className='modal-dialog'
- role='document'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='teamsModalLabel'
- >
- {'Select a team'}
- </h4>
- </div>
- <div className='modal-body'>
- <table className='more-channel-table table'>
- <tbody>
- <tr>
- <td>
- <p className='more-channel-name'>{'Descartes'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Grouping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Adventure'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Crossroads'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Sky scraping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Outdoors'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Microsoft'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Apple'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Close'}
- </button>
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
);
}
-} \ No newline at end of file
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ // this.setState({
+ // teamId: this.state.teamId,
+ // users: this.state.users,
+ // serverError: this.state.serverError,
+ // showPasswordModal: this.state.showPasswordModal,
+ // user: this.state.user
+ // });
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index dd5b60a24..7b2aeb631 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -4,11 +4,14 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
+var BrowserStore = require('../stores/browser_store.jsx');
+
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
+var ALL_TEAMS_EVENT = 'all_team_change';
class AdminStoreClass extends EventEmitter {
constructor() {
@@ -16,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.config = null;
+ this.teams = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
@@ -24,6 +28,10 @@ class AdminStoreClass extends EventEmitter {
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
+
+ this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
+ this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
+ this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
}
emitLogChange() {
@@ -50,6 +58,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
+ emitAllTeamsChange() {
+ this.emit(ALL_TEAMS_EVENT);
+ }
+
+ addAllTeamsChangeListener(callback) {
+ this.on(ALL_TEAMS_EVENT, callback);
+ }
+
+ removeAllTeamsChangeListener(callback) {
+ this.removeListener(ALL_TEAMS_EVENT, callback);
+ }
+
getLogs() {
return this.logs;
}
@@ -65,6 +85,22 @@ class AdminStoreClass extends EventEmitter {
saveConfig(config) {
this.config = config;
}
+
+ getAllTeams() {
+ return this.teams;
+ }
+
+ saveAllTeams(teams) {
+ this.teams = teams;
+ }
+
+ getSelectedTeams() {
+ return BrowserStore.getItem('seleted_teams');
+ }
+
+ saveSelectedTeams(teams) {
+ BrowserStore.setItem('seleted_teams', teams);
+ }
}
var AdminStore = new AdminStoreClass();
@@ -81,6 +117,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
+ case ActionTypes.RECIEVED_ALL_TEAMS:
+ AdminStore.saveAllTeams(action.teams);
+ AdminStore.emitAllTeamsChange();
+ break;
default:
}
});
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ed228f6c4..ab2965000 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -371,6 +371,32 @@ export function getConfig() {
);
}
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index a19f58e61..63924bff2 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -347,6 +347,20 @@ export function testEmail(config, success, error) {
});
}
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -890,6 +904,21 @@ export function getProfiles(success, error) {
});
}
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function uploadFile(formData, success, error) {
var request = $.ajax({
url: '/api/v1/files/upload',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 41e9e9ca6..f58816862 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -35,8 +35,8 @@ module.exports = {
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null,
-
RECIEVED_LOGS: null,
+ RECIEVED_ALL_TEAMS: null,
TOGGLE_IMPORT_THEME_MODAL: null
}),
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index c6ffb1871..7e88f8644 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -15,6 +15,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
heading(text, level, raw) {
const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
@@ -36,6 +44,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
return output;
}
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ return super.paragraph(text);
+ }
+
table(header, body) {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 9823d2611..e0019eb9b 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -19,6 +19,11 @@
background-color: $primary-color;
}
}
+ > h4 {
+ background: #333;
+ padding: 10px 10px;
+ margin-top: 1px;
+ }
}
.menu-icon--right {
vertical-align: top;
@@ -29,10 +34,10 @@
font-size: 13px;
right: -2px;
position: relative;
+ color: #fff;
}
}
&.nav__sub-menu {
- padding: 5px 0;
background: #111;
-webkit-font-smoothing: auto;
li {
@@ -88,6 +93,7 @@
overflow: auto;
background-color: #f1f1f1;
padding: 0 20px 20px;
+ min-height: 600px;
}
.wrapper--fixed {
max-width: 800px;
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index c09e9d7b4..bccea6e0e 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -1,6 +1,12 @@
.markdown__heading {
font-weight: bold;
}
+.markdown__paragraph-inline {
+ display: inline;
+ + .markdown__paragraph-inline {
+ margin-left: 4px;
+ }
+}
.markdown__table {
background: #fff;
margin: 5px 0 10px;
@@ -25,4 +31,4 @@ pre {
code {
color: #c7254e;
}
-} \ No newline at end of file
+}