diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2017-04-04 00:18:04 -0400 |
---|---|---|
committer | Corey Hulen <corey@hulen.com> | 2017-04-03 21:18:04 -0700 |
commit | 348374fba5db8415d37d5cd8b897048b1300f415 (patch) | |
tree | 8277c9be4b8032b488e2fe87ceee58a283569a3e | |
parent | 4c9019b9eb789152439ad6a56e93c7f0fb7832c5 (diff) | |
download | chat-348374fba5db8415d37d5cd8b897048b1300f415.tar.gz chat-348374fba5db8415d37d5cd8b897048b1300f415.tar.bz2 chat-348374fba5db8415d37d5cd8b897048b1300f415.zip |
PLT-6098 Added Manage Teams modal to System Console users list (#5914)
* Added Manage Teams modal to System Console users list
* Localized ManageTeamsModal
* Fixed borders between Manage Teams list items
* Updated appearance of ManageTeamsModal
* Fixed admin being redirected from system console when removing self from a team
* Sorted teams in ManageTeamsModal
* Updated Manage Teams styling
-rw-r--r-- | webapp/actions/team_actions.jsx | 8 | ||||
-rw-r--r-- | webapp/actions/websocket_actions.jsx | 5 | ||||
-rw-r--r-- | webapp/client/client.jsx | 30 | ||||
-rw-r--r-- | webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx | 137 | ||||
-rw-r--r-- | webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx | 225 | ||||
-rw-r--r-- | webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx | 52 | ||||
-rw-r--r-- | webapp/components/admin_console/system_users/system_users_dropdown.jsx | 31 | ||||
-rw-r--r-- | webapp/components/admin_console/system_users/system_users_list.jsx | 27 | ||||
-rwxr-xr-x | webapp/i18n/en.json | 2 | ||||
-rw-r--r-- | webapp/sass/routes/_admin-console.scss | 70 |
10 files changed, 584 insertions, 3 deletions
diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx index 478d3dffc..4cb57961b 100644 --- a/webapp/actions/team_actions.jsx +++ b/webapp/actions/team_actions.jsx @@ -150,3 +150,11 @@ export function switchTeams(url) { AsyncClient.viewChannel(); browserHistory.push(url); } + +export function getTeamsForUser(userId, success, error) { + Client.getTeamsForUser(userId, success, error); +} + +export function getTeamMembersForUser(userId, success, error) { + Client.getTeamMembersForUser(userId, success, error); +} diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index e07e3e217..ab798df28 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -251,7 +251,10 @@ function handleLeaveTeamEvent(msg) { Client.setTeamId(''); BrowserStore.removeGlobalItem('team'); BrowserStore.removeGlobalItem(msg.data.team_id); - GlobalActions.redirectUserToDefaultTeam(); + + if (!global.location.pathname.startsWith('/admin_console')) { + GlobalActions.redirectUserToDefaultTeam(); + } } } else { UserStore.removeProfileFromTeam(msg.data.team_id, msg.data.user_id); diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 1f2e5517f..1f70300e8 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -550,6 +550,16 @@ export default class Client { end(this.handleResponse.bind(this, 'getAllTeamListings', success, error)); } + getTeamsForUser(userId, success, error) { + // Call out to API v4 since this call doesn't exist in v3 + request. + get(`${this.url}/api/v4/users/${userId}/teams`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamsForUser', success, error)); + } + getMyTeam(success, error) { request. get(`${this.getTeamNeededRoute()}/me`). @@ -586,6 +596,16 @@ export default class Client { end(this.handleResponse.bind(this, 'getMyTeamMembers', success, error)); } + getTeamMembersForUser(userId, success, error) { + // Call out to API v4 since this call doesn't exist in v3 + request. + get(`${this.url}/api/v4/users/${userId}/teams/members`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + end(this.handleResponse.bind(this, 'getTeamsForUser', success, error)); + } + getMyTeamsUnread(teamId, success, error) { let url = `${this.getTeamsRoute()}/unread`; @@ -1275,6 +1295,16 @@ export default class Client { this.trackEvent('api', 'api_users_update_profile_picture'); } + getProfilePictureUrl(id, lastPictureUpdate) { + let url = `${this.getUsersRoute()}/${id}/image`; + + if (lastPictureUpdate) { + url += `?time=${lastPictureUpdate}`; + } + + return url; + } + // Channel Routes Section createChannel(channel, success, error) { diff --git a/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx b/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx new file mode 100644 index 000000000..81e6460af --- /dev/null +++ b/webapp/components/admin_console/manage_teams_modal/manage_teams_dropdown.jsx @@ -0,0 +1,137 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {Dropdown, MenuItem} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import {updateTeamMemberRoles, removeUserFromTeam} from 'actions/team_actions.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +export default class ManageTeamsDropdown extends React.Component { + static propTypes = { + user: React.PropTypes.object.isRequired, + teamMember: React.PropTypes.object.isRequired, + onError: React.PropTypes.func.isRequired, + onMemberChange: React.PropTypes.func.isRequired, + onMemberRemove: React.PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + + this.toggleDropdown = this.toggleDropdown.bind(this); + + this.makeTeamAdmin = this.makeTeamAdmin.bind(this); + this.makeMember = this.makeMember.bind(this); + this.removeFromTeam = this.removeFromTeam.bind(this); + + this.handleMemberChange = this.handleMemberChange.bind(this); + this.handleMemberRemove = this.handleMemberRemove.bind(this); + + this.state = { + show: false + }; + } + + toggleDropdown() { + this.setState({ + show: !this.state.show + }); + } + + makeTeamAdmin() { + updateTeamMemberRoles( + this.props.teamMember.team_id, + this.props.user.id, + 'team_user team_admin', + this.handleMemberChange, + this.props.onError + ); + } + + makeMember() { + updateTeamMemberRoles( + this.props.teamMember.team_id, + this.props.user.id, + 'team_user', + this.handleMemberChange, + this.props.onError + ); + } + + removeFromTeam() { + removeUserFromTeam( + this.props.teamMember.team_id, + this.props.user.id, + this.handleMemberRemove, + this.props.onError + ); + } + + handleMemberChange() { + this.props.onMemberChange(this.props.teamMember.team_id); + } + + handleMemberRemove() { + this.props.onMemberRemove(this.props.teamMember.team_id); + } + + render() { + const isTeamAdmin = Utils.isAdmin(this.props.teamMember.roles); + + let title; + if (isTeamAdmin) { + title = Utils.localizeMessage('admin.user_item.teamAdmin', 'Team Admin'); + } else { + title = Utils.localizeMessage('admin.user_item.teamMember', 'Team Member'); + } + + let makeTeamAdmin = null; + if (!isTeamAdmin) { + makeTeamAdmin = ( + <MenuItem onSelect={this.makeTeamAdmin}> + <FormattedMessage + id='admin.user_item.makeTeamAdmin' + defaultMessage='Make Team Admin' + /> + </MenuItem> + ); + } + + let makeMember = null; + if (isTeamAdmin) { + makeMember = ( + <MenuItem onSelect={this.makeMember}> + <FormattedMessage + id='admin.user_item.makeMember' + defaultMessage='Make Member' + /> + </MenuItem> + ); + } + + return ( + <Dropdown + id={`manage-teams-${this.props.user.id}-${this.props.teamMember.team_id}`} + open={this.state.show} + onToggle={this.toggleDropdown} + > + <Dropdown.Toggle useAnchor={true}> + {title} + </Dropdown.Toggle> + <Dropdown.Menu> + {makeTeamAdmin} + {makeMember} + <MenuItem onSelect={this.removeFromTeam}> + <FormattedMessage + id='team_members_dropdown.leave_team' + defaultMessage='Remove from Team' + /> + </MenuItem> + </Dropdown.Menu> + </Dropdown> + ); + } +} diff --git a/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx b/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx new file mode 100644 index 000000000..e3eae6310 --- /dev/null +++ b/webapp/components/admin_console/manage_teams_modal/manage_teams_modal.jsx @@ -0,0 +1,225 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {Modal} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import * as TeamActions from 'actions/team_actions.jsx'; + +import Client from 'client/web_client.jsx'; + +import LoadingScreen from 'components/loading_screen.jsx'; + +import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import ManageTeamsDropdown from './manage_teams_dropdown.jsx'; +import RemoveFromTeamButton from './remove_from_team_button.jsx'; + +export default class ManageTeamsModal extends React.Component { + static propTypes = { + onModalDismissed: React.PropTypes.func.isRequired, + show: React.PropTypes.bool.isRequired, + user: React.PropTypes.object + }; + + constructor(props) { + super(props); + + this.loadTeamsAndTeamMembers = this.loadTeamsAndTeamMembers.bind(this); + + this.handleError = this.handleError.bind(this); + this.handleMemberChange = this.handleMemberChange.bind(this); + this.handleMemberRemove = this.handleMemberRemove.bind(this); + + this.renderContents = this.renderContents.bind(this); + + this.state = { + error: null, + teams: null, + teamMembers: null + }; + } + + componentDidMount() { + if (this.props.user) { + this.loadTeamsAndTeamMembers(); + } + } + + componentWillReceiveProps(nextProps) { + const userId = this.props.user ? this.props.user.id : ''; + const nextUserId = nextProps.user ? nextProps.user.id : ''; + + if (userId !== nextUserId) { + this.setState({ + teams: null, + teamMembers: null + }); + + if (nextProps.user) { + this.loadTeamsAndTeamMembers(nextProps.user); + } + } + } + + loadTeamsAndTeamMembers(user = this.props.user) { + TeamActions.getTeamsForUser(user.id, (teams) => { + this.setState({ + teams: teams.sort(sortTeamsByDisplayName) + }); + }); + + TeamActions.getTeamMembersForUser(user.id, (teamMembers) => { + this.setState({ + teamMembers + }); + }); + } + + handleError(error) { + this.setState({ + error + }); + } + + handleMemberChange() { + TeamActions.getTeamMembersForUser(this.props.user.id, (teamMembers) => { + this.setState({ + teamMembers + }); + }); + } + + handleMemberRemove(teamId) { + this.setState({ + teams: this.state.teams.filter((team) => team.id !== teamId), + teamMembers: this.state.teamMembers.filter((teamMember) => teamMember.team_id !== teamId) + }); + } + + renderContents() { + const {user} = this.props; + const {teams, teamMembers} = this.state; + + if (!user) { + return <LoadingScreen/>; + } + + const isSystemAdmin = Utils.isAdmin(user.roles); + + let name = Utils.getFullName(user); + if (name) { + name += ` (@${user.username})`; + } else { + name = `@${user.username}`; + } + + let teamList; + if (teams && teamMembers) { + teamList = teams.map((team) => { + const teamMember = teamMembers.find((member) => member.team_id === team.id); + if (!teamMember) { + return null; + } + + let action; + if (isSystemAdmin) { + action = ( + <RemoveFromTeamButton + user={user} + team={team} + onError={this.handleError} + onMemberRemove={this.handleMemberRemove} + /> + ); + } else { + action = ( + <ManageTeamsDropdown + user={user} + team={team} + teamMember={teamMember} + onError={this.handleError} + onMemberChange={this.handleMemberChange} + onMemberRemove={this.handleMemberRemove} + /> + ); + } + + return ( + <div + key={team.id} + className='manage-teams__team' + > + <div className='manage-teams__team-name'> + {team.display_name} + </div> + <div className='manage-teams__team-actions'> + {action} + </div> + </div> + ); + }); + } else { + teamList = <LoadingScreen/>; + } + + let systemAdminIndicator = null; + if (isSystemAdmin) { + systemAdminIndicator = ( + <div className='manage-teams__system-admin'> + <FormattedMessage + id='admin.user_item.sysAdmin' + defaultMessage='System Admin' + /> + </div> + ); + } + + return ( + <div> + <div className='manage-teams__user'> + <img + className='manage-teams__profile-picture' + src={Client.getProfilePictureUrl(user.id, user.last_picture_update)} + /> + <div className='manage-teams__info'> + <div className='manage-teams__name'> + {name} + </div> + <div className='manage-teams__email'> + {user.email} + </div> + </div> + {systemAdminIndicator} + </div> + <div className='manage-teams__teams'> + {teamList} + </div> + </div> + ); + } + + render() { + return ( + <Modal + show={this.props.show} + onHide={this.props.onModalDismissed} + dialogClassName='manage-teams' + > + <Modal.Header closeButton={true}> + <Modal.Title> + <FormattedMessage + id='admin.user_item.manageTeams' + defaultMessage='Manage Teams' + /> + </Modal.Title> + </Modal.Header> + <Modal.Body> + {this.renderContents()} + </Modal.Body> + </Modal> + ); + } +} diff --git a/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx b/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx new file mode 100644 index 000000000..d733135f4 --- /dev/null +++ b/webapp/components/admin_console/manage_teams_modal/remove_from_team_button.jsx @@ -0,0 +1,52 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {removeUserFromTeam} from 'actions/team_actions.jsx'; + +export default class RemoveFromTeamButton extends React.PureComponent { + static propTypes = { + onError: React.PropTypes.func.isRequired, + onMemberRemove: React.PropTypes.func.isRequired, + team: React.PropTypes.object.isRequired, + user: React.PropTypes.object.isRequired + }; + + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.handleMemberRemove = this.handleMemberRemove.bind(this); + } + + handleClick(e) { + e.preventDefault(); + + removeUserFromTeam( + this.props.team.id, + this.props.user.id, + this.handleMemberRemove, + this.props.onError + ); + } + + handleMemberRemove() { + this.props.onMemberRemove(this.props.team.id); + } + + render() { + return ( + <button + className='btn btn-default' + onClick={this.handleClick} + > + <FormattedMessage + id='team_members_dropdown.leave_team' + defaultMessage='Remove from Team' + /> + </button> + ); + } +} diff --git a/webapp/components/admin_console/system_users/system_users_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx index 6f18754a1..534017cf8 100644 --- a/webapp/components/admin_console/system_users/system_users_dropdown.jsx +++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx @@ -18,7 +18,8 @@ import React from 'react'; export default class SystemUsersDropdown extends React.Component { static propTypes = { user: React.PropTypes.object.isRequired, - doPasswordReset: React.PropTypes.func.isRequired + doPasswordReset: React.PropTypes.func.isRequired, + doManageTeams: React.PropTypes.func.isRequired }; constructor(props) { @@ -28,6 +29,7 @@ export default class SystemUsersDropdown extends React.Component { this.handleMakeActive = this.handleMakeActive.bind(this); this.handleMakeNotActive = this.handleMakeNotActive.bind(this); this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this); + this.handleManageTeams = this.handleManageTeams.bind(this); this.handleResetPassword = this.handleResetPassword.bind(this); this.handleResetMfa = this.handleResetMfa.bind(this); this.handleDemoteSystemAdmin = this.handleDemoteSystemAdmin.bind(this); @@ -94,6 +96,12 @@ export default class SystemUsersDropdown extends React.Component { ); } + handleManageTeams(e) { + e.preventDefault(); + + this.props.doManageTeams(this.props.user); + } + handleResetPassword(e) { e.preventDefault(); this.props.doPasswordReset(this.props.user); @@ -177,6 +185,7 @@ export default class SystemUsersDropdown extends React.Component { let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles); let showMakeActive = false; let showMakeNotActive = !Utils.isSystemAdmin(user.roles); + let showManageTeams = true; const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true'; const showMfaReset = mfaEnabled && user.mfa_active; @@ -191,6 +200,7 @@ export default class SystemUsersDropdown extends React.Component { showMakeSystemAdmin = false; showMakeActive = true; showMakeNotActive = false; + showManageTeams = false; } let disableActivationToggle = false; @@ -281,6 +291,24 @@ export default class SystemUsersDropdown extends React.Component { ); } + let manageTeams = null; + if (showManageTeams) { + manageTeams = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleManageTeams} + > + <FormattedMessage + id='admin.user_item.manageTeams' + defaultMessage='Manage Teams' + /> + </a> + </li> + ); + } + let mfaReset = null; if (showMfaReset) { mfaReset = ( @@ -404,6 +432,7 @@ export default class SystemUsersDropdown extends React.Component { {makeActive} {makeNotActive} {makeSystemAdmin} + {manageTeams} {mfaReset} {passwordReset} </ul> diff --git a/webapp/components/admin_console/system_users/system_users_list.jsx b/webapp/components/admin_console/system_users/system_users_list.jsx index 5d8837164..ccb1a39d4 100644 --- a/webapp/components/admin_console/system_users/system_users_list.jsx +++ b/webapp/components/admin_console/system_users/system_users_list.jsx @@ -4,6 +4,7 @@ import React from 'react'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal.jsx'; import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx'; import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx'; @@ -35,6 +36,9 @@ export default class SystemUsersList extends React.Component { this.previousPage = this.previousPage.bind(this); this.search = this.search.bind(this); + this.doManageTeams = this.doManageTeams.bind(this); + this.doManageTeamsDismiss = this.doManageTeamsDismiss.bind(this); + this.doPasswordReset = this.doPasswordReset.bind(this); this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); @@ -42,6 +46,7 @@ export default class SystemUsersList extends React.Component { this.state = { page: 0, + showManageTeamsModal: false, showPasswordModal: false, user: null }; @@ -71,6 +76,20 @@ export default class SystemUsersList extends React.Component { } } + doManageTeams(user) { + this.setState({ + showManageTeamsModal: true, + user + }); + } + + doManageTeamsDismiss() { + this.setState({ + showManageTeamsModal: false, + user: null + }); + } + doPasswordReset(user) { this.setState({ showPasswordModal: true, @@ -211,7 +230,8 @@ export default class SystemUsersList extends React.Component { extraInfo={extraInfo} actions={[SystemUsersDropdown]} actionProps={{ - doPasswordReset: this.doPasswordReset + doPasswordReset: this.doPasswordReset, + doManageTeams: this.doManageTeams }} nextPage={this.nextPage} previousPage={this.previousPage} @@ -220,6 +240,11 @@ export default class SystemUsersList extends React.Component { term={this.props.term} onTermChange={this.props.onTermChange} /> + <ManageTeamsModal + user={this.state.user} + show={this.state.showManageTeamsModal} + onModalDismissed={this.doManageTeamsDismiss} + /> <ResetPasswordModal user={this.state.user} show={this.state.showPasswordModal} diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 13660e12a..4c28126e5 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -896,6 +896,7 @@ "admin.user_item.makeMember": "Make Member", "admin.user_item.makeSysAdmin": "Make System Admin", "admin.user_item.makeTeamAdmin": "Make Team Admin", + "admin.user_item.manageTeams": "Manage Teams", "admin.user_item.member": "Member", "admin.user_item.mfaNo": "<strong>MFA</strong>: No", "admin.user_item.mfaYes": "<strong>MFA</strong>: Yes", @@ -903,6 +904,7 @@ "admin.user_item.resetPwd": "Reset Password", "admin.user_item.switchToEmail": "Switch to Email/Password", "admin.user_item.teamAdmin": "Team Admin", + "admin.user_item.sysAdmin": "System Admin", "admin.webrtc.enableDescription": "When true, Mattermost allows making <strong>one-on-one</strong> video calls. WebRTC calls are available on Chrome, Firefox and Mattermost Desktop Apps.", "admin.webrtc.enableTitle": "Enable Mattermost WebRTC: ", "admin.webrtc.gatewayAdminSecretDescription": "Enter your admin secret password to access the Gateway Admin URL.", diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index bf8aba6cc..a39e1c20a 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -501,3 +501,73 @@ width: 200px } } + +.manage-teams { + .manage-teams__user { + align-items: center; + border-bottom-color: lightgray; + border-bottom-style: solid; + border-bottom-width: 1px; + display: flex; + padding-bottom: 15px; + } + + .manage-teams__profile-picture { + border-radius: 20px; + height: 40px; + width: 40px; + } + + .manage-teams__info { + flex: 1; + margin-left: 10px; + overflow: hidden; + white-space: nowrap; + + .manage-teams__name { + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + } + + .manage-teams__email { + opacity: 0.5; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .manage-teams__system-admin { + margin-left: 10px; + opacity: 0.5; + padding-right: 10px; + } + + .manage-teams__team { + align-items: center; + display: flex; + padding: 10px; + } + + .manage-teams__team + .manage-teams__team { + border-top-color: lightgray; + border-top-style: solid; + border-top-width: 1px; + } + + .manage-teams__team-name { + flex: 1; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + } + + .manage-teams__team-actions { + margin-left: 10px; + + // Override default react-bootstrap style + .dropdown-toggle { + @include box-shadow(none); + } + } +} |