Merge pull request #765 from mattermost/PLT-349
PLT-349 adding team mgt to admin console
Этот коммит содержится в:
@@ -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)
|
||||
|
||||
48
api/team.go
48
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
99
api/user.go
99
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,32 +1182,38 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
hash := props["hash"]
|
||||
if len(hash) == 0 {
|
||||
c.SetInvalidParam("resetPassword", "hash")
|
||||
return
|
||||
}
|
||||
|
||||
data := model.MapFromJson(strings.NewReader(props["data"]))
|
||||
|
||||
userId := data["user_id"]
|
||||
if len(userId) != 26 {
|
||||
c.SetInvalidParam("resetPassword", "data:user_id")
|
||||
return
|
||||
}
|
||||
|
||||
timeStr := data["time"]
|
||||
if len(timeStr) == 0 {
|
||||
c.SetInvalidParam("resetPassword", "data:time")
|
||||
return
|
||||
}
|
||||
|
||||
name := props["name"]
|
||||
if len(name) == 0 {
|
||||
c.SetInvalidParam("resetPassword", "name")
|
||||
return
|
||||
}
|
||||
|
||||
userId := props["user_id"]
|
||||
hash := props["hash"]
|
||||
timeStr := ""
|
||||
|
||||
if !c.IsSystemAdmin() {
|
||||
if len(hash) == 0 {
|
||||
c.SetInvalidParam("resetPassword", "hash")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if len(userId) != 26 {
|
||||
c.SetInvalidParam("resetPassword", "user_id")
|
||||
return
|
||||
}
|
||||
|
||||
c.LogAuditWithUserId(userId, "attempt")
|
||||
|
||||
var team *model.Team
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
@@ -38,11 +132,17 @@ export default class AdminSidebar extends React.Component {
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
<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,25 +187,16 @@ 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>
|
||||
</li>
|
||||
<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
|
||||
};
|
||||
132
web/react/components/admin_console/reset_password_modal.jsx
Обычный файл
132
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
|
||||
};
|
||||
@@ -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'>×</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
id='teamsModalLabel'
|
||||
>
|
||||
{'Select a team'}
|
||||
</h4>
|
||||
<Modal.Body>
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
<select
|
||||
ref='team'
|
||||
size='10'
|
||||
style={{width: '100%'}}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectTeamModal.defaultProps = {
|
||||
show: false
|
||||
};
|
||||
|
||||
SelectTeamModal.propTypes = {
|
||||
teams: React.PropTypes.object,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalSubmit: React.PropTypes.func,
|
||||
onModalDismissed: React.PropTypes.func
|
||||
};
|
||||
|
||||
178
web/react/components/admin_console/team_users.jsx
Обычный файл
178
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
|
||||
};
|
||||
266
web/react/components/admin_console/user_item.jsx
Обычный файл
266
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
|
||||
};
|
||||
@@ -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:
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -35,8 +35,8 @@ module.exports = {
|
||||
RECIEVED_TEAM: null,
|
||||
|
||||
RECIEVED_CONFIG: null,
|
||||
|
||||
RECIEVED_LOGS: null,
|
||||
RECIEVED_ALL_TEAMS: null,
|
||||
|
||||
TOGGLE_IMPORT_THEME_MODAL: null
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Ссылка в новой задаче
Block a user