diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/admin_console/admin_controller.jsx | 84 | ||||
-rw-r--r-- | web/react/components/admin_console/admin_sidebar.jsx | 179 | ||||
-rw-r--r-- | web/react/components/admin_console/reset_password_modal.jsx | 132 | ||||
-rw-r--r-- | web/react/components/admin_console/select_team_modal.jsx | 193 | ||||
-rw-r--r-- | web/react/components/admin_console/team_users.jsx | 178 | ||||
-rw-r--r-- | web/react/components/admin_console/user_item.jsx | 266 | ||||
-rw-r--r-- | web/react/stores/admin_store.jsx | 40 | ||||
-rw-r--r-- | web/react/utils/async_client.jsx | 26 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 29 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 2 | ||||
-rw-r--r-- | web/react/utils/markdown.jsx | 16 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_admin-console.scss | 8 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_markdown.scss | 8 |
13 files changed, 1022 insertions, 139 deletions
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'>×</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 +} |