summaryrefslogtreecommitdiffstats
path: root/webapp/components/admin_console/system_users
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2017-03-30 12:46:47 -0400
committerCorey Hulen <corey@hulen.com>2017-03-30 09:46:47 -0700
commit689cac535e45c47a4f603b236dc129dd456efcc9 (patch)
tree767ef80b310d6d073840bd5216da38c439f6e193 /webapp/components/admin_console/system_users
parent9a9729f22fea7275637eafb4046900c9f372ec56 (diff)
downloadchat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.gz
chat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.bz2
chat-689cac535e45c47a4f603b236dc129dd456efcc9.zip
PLT-2713/PLT-6028 Added System Users list to System Console (#5882)
* PLT-2713 Added ability for admins to list users not in any team * Updated style of unit test * Split SearchableUserList to give better control over its properties * Added users without any teams to the user store * Added ManageUsers page * Renamed ManageUsers to SystemUsers * Added ability to search by user id in SystemUsers page * Added SystemUsersDropdown * Removed unnecessary injectIntl * Created TeamUtils * Reduced scope of system console heading CSS * Added team filter to TeamAnalytics page * Updated admin console sidebar * Removed unnecessary TODO * Removed unused reference to deleted modal * Fixed system console sidebar not scrolling on first load * Fixed TeamAnalytics page not rendering on first load * Fixed chart.js throwing an error when switching between teams * Changed TeamAnalytics header to show the team's display name * Fixed appearance of TeamAnalytics and SystemUsers on small screen widths * Fixed placement of 'No users found' message * Fixed teams not appearing in SystemUsers on first load * Updated user count text for SystemUsers * Changed search by id fallback to trigger less often * Fixed SystemUsers list items not updating when searching * Fixed localization strings for SystemUsers page
Diffstat (limited to 'webapp/components/admin_console/system_users')
-rw-r--r--webapp/components/admin_console/system_users/system_users.jsx370
-rw-r--r--webapp/components/admin_console/system_users/system_users_dropdown.jsx415
-rw-r--r--webapp/components/admin_console/system_users/system_users_list.jsx232
3 files changed, 1017 insertions, 0 deletions
diff --git a/webapp/components/admin_console/system_users/system_users.jsx b/webapp/components/admin_console/system_users/system_users.jsx
new file mode 100644
index 000000000..a311aebb7
--- /dev/null
+++ b/webapp/components/admin_console/system_users/system_users.jsx
@@ -0,0 +1,370 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import {
+ loadProfiles,
+ loadProfilesAndTeamMembers,
+ loadProfilesWithoutTeam,
+ searchUsers
+} from 'actions/user_actions.jsx';
+
+import AdminStore from 'stores/admin_store.jsx';
+import AnalyticsStore from 'stores/analytics_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {getAllTeams, getStandardAnalytics, getTeamStats, getUser} from 'utils/async_client.jsx';
+import {Constants, StatTypes, UserSearchOptions} from 'utils/constants.jsx';
+import {convertTeamMapToList} from 'utils/team_utils.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import SystemUsersList from './system_users_list.jsx';
+
+const ALL_USERS = '';
+const NO_TEAM = 'no_team';
+
+const USER_ID_LENGTH = 26;
+const USERS_PER_PAGE = 50;
+
+export default class SystemUsers extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateTeamsFromStore = this.updateTeamsFromStore.bind(this);
+ this.updateTotalUsersFromStore = this.updateTotalUsersFromStore.bind(this);
+ this.updateUsersFromStore = this.updateUsersFromStore.bind(this);
+
+ this.loadDataForTeam = this.loadDataForTeam.bind(this);
+ this.loadComplete = this.loadComplete.bind(this);
+
+ this.handleTeamChange = this.handleTeamChange.bind(this);
+ this.handleTermChange = this.handleTermChange.bind(this);
+ this.nextPage = this.nextPage.bind(this);
+
+ this.doSearch = this.doSearch.bind(this);
+ this.search = this.search.bind(this);
+ this.getUserById = this.getUserById.bind(this);
+
+ this.renderFilterRow = this.renderFilterRow.bind(this);
+
+ this.state = {
+ teams: convertTeamMapToList(AdminStore.getAllTeams()),
+ totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS],
+ users: UserStore.getProfileList(),
+
+ teamId: ALL_USERS,
+ term: '',
+ loading: true,
+ searching: false
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addAllTeamsChangeListener(this.updateTeamsFromStore);
+
+ AnalyticsStore.addChangeListener(this.updateTotalUsersFromStore);
+ TeamStore.addStatsChangeListener(this.updateTotalUsersFromStore);
+
+ UserStore.addChangeListener(this.updateUsersFromStore);
+ UserStore.addInTeamChangeListener(this.updateUsersFromStore);
+ UserStore.addWithoutTeamChangeListener(this.updateUsersFromStore);
+
+ this.loadDataForTeam(this.state.teamId);
+ getAllTeams();
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ const nextTeamId = nextState.teamId;
+
+ if (this.state.teamId !== nextTeamId) {
+ this.updateTotalUsersFromStore(nextTeamId);
+ this.updateUsersFromStore(nextTeamId, nextState.term);
+
+ this.loadDataForTeam(nextTeamId);
+ }
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeAllTeamsChangeListener(this.updateTeamsFromStore);
+
+ AnalyticsStore.removeChangeListener(this.updateTotalUsersFromStore);
+ TeamStore.removeStatsChangeListener(this.updateTotalUsersFromStore);
+
+ UserStore.removeChangeListener(this.updateUsersFromStore);
+ UserStore.removeInTeamChangeListener(this.updateUsersFromStore);
+ UserStore.removeWithoutTeamChangeListener(this.updateUsersFromStore);
+ }
+
+ updateTeamsFromStore() {
+ this.setState({teams: convertTeamMapToList(AdminStore.getAllTeams())});
+ }
+
+ updateTotalUsersFromStore(teamId = this.state.teamId) {
+ if (teamId === ALL_USERS) {
+ this.setState({
+ totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS]
+ });
+ } else if (teamId === NO_TEAM) {
+ this.setState({
+ totalUsers: 0
+ });
+ } else {
+ this.setState({
+ totalUsers: TeamStore.getStats(teamId).total_member_count
+ });
+ }
+ }
+
+ updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) {
+ if (term) {
+ if (teamId === this.state.teamId) {
+ // Search results aren't in the store, so manually update the users in them
+ const users = [...this.state.users];
+
+ for (let i = 0; i < users.length; i++) {
+ const user = users[i];
+
+ if (UserStore.hasProfile(user.id)) {
+ users[i] = UserStore.getProfile(user.id);
+ }
+ }
+
+ this.setState({
+ users
+ });
+ } else {
+ this.doSearch(teamId, term, true);
+ }
+
+ return;
+ }
+
+ if (teamId === ALL_USERS) {
+ this.setState({users: UserStore.getProfileList(false, true)});
+ } else if (teamId === NO_TEAM) {
+ this.setState({users: UserStore.getProfileListWithoutTeam()});
+ } else {
+ this.setState({users: UserStore.getProfileListInTeam(this.state.teamId)});
+ }
+ }
+
+ loadDataForTeam(teamId) {
+ if (teamId === ALL_USERS) {
+ loadProfiles(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
+ getStandardAnalytics();
+ } else if (teamId === NO_TEAM) {
+ loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
+ } else {
+ loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, this.loadComplete);
+ getTeamStats(teamId);
+ }
+ }
+
+ loadComplete() {
+ this.setState({loading: false});
+ }
+
+ handleTeamChange(e) {
+ this.setState({teamId: e.target.value});
+ }
+
+ handleTermChange(term) {
+ this.setState({term});
+ }
+
+ nextPage(page) {
+ // Paging isn't supported while searching
+
+ if (this.state.teamId === ALL_USERS) {
+ loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete);
+ } else if (this.state.teamId === NO_TEAM) {
+ loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete);
+ } else {
+ loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete);
+ }
+ }
+
+ search(term) {
+ if (term === '') {
+ this.updateUsersFromStore(this.state.teamId, term);
+
+ this.setState({
+ loading: false
+ });
+
+ this.searchTimeoutId = '';
+ return;
+ }
+
+ this.doSearch(this.state.teamId, term);
+ }
+
+ doSearch(teamId, term, now = false) {
+ clearTimeout(this.searchTimeoutId);
+
+ this.setState({
+ loading: true,
+ users: []
+ });
+
+ const options = {
+ [UserSearchOptions.ALLOW_INACTIVE]: true
+ };
+ if (teamId === NO_TEAM) {
+ options[UserSearchOptions.WITHOUT_TEAM] = true;
+ }
+
+ const searchTimeoutId = setTimeout(
+ () => {
+ searchUsers(
+ term,
+ teamId,
+ options,
+ (users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ if (users.length > 0) {
+ this.setState({
+ loading: false,
+ users
+ });
+ } else if (term.length === USER_ID_LENGTH) {
+ // This term didn't match any users name, but it does look like it might be a user's ID
+ this.getUserById(term, searchTimeoutId);
+ } else {
+ this.setState({
+ loading: false
+ });
+ }
+ },
+ () => {
+ this.setState({
+ loading: false
+ });
+ }
+ );
+ },
+ now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS
+ );
+
+ this.searchTimeoutId = searchTimeoutId;
+ }
+
+ getUserById(id, searchTimeoutId) {
+ if (UserStore.hasProfile(id)) {
+ this.setState({
+ loading: false,
+ users: [UserStore.getProfile(id)]
+ });
+
+ return;
+ }
+
+ getUser(
+ id,
+ (user) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: false,
+ users: [user]
+ });
+ },
+ () => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: false,
+ users: []
+ });
+ }
+ );
+ }
+
+ renderFilterRow(doSearch) {
+ const teams = this.state.teams.map((team) => {
+ return (
+ <option
+ key={team.id}
+ value={team.id}
+ >
+ {team.display_name}
+ </option>
+ );
+ });
+
+ return (
+ <div className='system-users__filter-row'>
+ <div className='system-users__filter'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
+ onInput={doSearch}
+ />
+ </div>
+ <label>
+ <span className='system-users__team-filter-label'>
+ <FormattedMessage
+ id='filtered_user_list.show'
+ defaultMessage='Filter:'
+ />
+ </span>
+ <select
+ className='form-control system-users__team-filter'
+ onChange={this.handleTeamChange}
+ value={this.state.teamId}
+ >
+ <option value={ALL_USERS}>{Utils.localizeMessage('admin.system_users.allUsers', 'All Users')}</option>
+ <option value={NO_TEAM}>{Utils.localizeMessage('admin.system_users.noTeams', 'No Teams')}</option>
+ {teams}
+ </select>
+ </label>
+ </div>
+ );
+ }
+
+ render() {
+ let users = null;
+ if (!this.state.loading) {
+ users = this.state.users;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3 className='admin-console-header'>
+ <FormattedMessage
+ id='admin.system_users.title'
+ defaultMessage='{siteName} Users'
+ values={{
+ siteName: global.mm_config.SiteName
+ }}
+ />
+ </h3>
+ <div className='more-modal__list member-list-holder'>
+ <SystemUsersList
+ renderFilterRow={this.renderFilterRow}
+ search={this.search}
+ nextPage={this.nextPage}
+ users={users}
+ usersPerPage={USERS_PER_PAGE}
+ total={this.state.totalUsers}
+ teams={this.state.teams}
+ teamId={this.state.teamId}
+ term={this.state.term}
+ onTermChange={this.handleTermChange}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/system_users/system_users_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
new file mode 100644
index 000000000..6f18754a1
--- /dev/null
+++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
@@ -0,0 +1,415 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ConfirmModal from 'components/confirm_modal.jsx';
+
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+import {updateUserRoles, updateActive} from 'actions/user_actions.jsx';
+import {adminResetMfa} from 'actions/admin_actions.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class SystemUsersDropdown extends React.Component {
+ static propTypes = {
+ user: React.PropTypes.object.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+ this.handleResetMfa = this.handleResetMfa.bind(this);
+ this.handleDemoteSystemAdmin = this.handleDemoteSystemAdmin.bind(this);
+ this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this);
+ this.handleDemoteCancel = this.handleDemoteCancel.bind(this);
+
+ this.state = {
+ serverError: null,
+ showDemoteModal: false,
+ user: null,
+ role: null
+ };
+ }
+
+ doMakeMember() {
+ updateUserRoles(
+ this.props.user.id,
+ 'system_user',
+ null,
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const me = UserStore.getCurrentUser();
+ if (this.props.user.id === me.id && me.roles.includes('system_admin')) {
+ this.handleDemoteSystemAdmin(this.props.user, 'member');
+ } else {
+ this.doMakeMember();
+ }
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ updateActive(this.props.user.id, true, null,
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ updateActive(this.props.user.id, false, null,
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+
+ updateUserRoles(
+ this.props.user.id,
+ 'system_user system_admin',
+ null,
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ handleResetMfa(e) {
+ e.preventDefault();
+
+ adminResetMfa(this.props.user.id,
+ null,
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleDemoteSystemAdmin(user, role) {
+ this.setState({
+ serverError: this.state.serverError,
+ showDemoteModal: true,
+ user,
+ role
+ });
+ }
+
+ handleDemoteCancel() {
+ this.setState({
+ serverError: null,
+ showDemoteModal: false,
+ user: null,
+ role: null
+ });
+ }
+
+ handleDemoteSubmit() {
+ if (this.state.role === 'member') {
+ this.doMakeMember();
+ }
+
+ const teamUrl = TeamStore.getCurrentTeamUrl();
+ if (teamUrl) {
+ // the channel is added to the URL cause endless loading not being fully fixed
+ window.location.href = teamUrl + '/channels/town-square';
+ } else {
+ window.location.href = '/';
+ }
+ }
+
+ 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;
+ if (!user) {
+ return <div/>;
+ }
+ let currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.member'
+ defaultMessage='Member'
+ />
+ );
+
+ if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) {
+ currentRoles = (
+ <FormattedMessage
+ id='team_members_dropdown.systemAdmin'
+ defaultMessage='System Admin'
+ />
+ );
+ }
+
+ const me = UserStore.getCurrentUser();
+ let showMakeMember = Utils.isSystemAdmin(user.roles);
+ let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles);
+ let showMakeActive = false;
+ let showMakeNotActive = !Utils.isSystemAdmin(user.roles);
+ 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;
+
+ if (user.delete_at > 0) {
+ currentRoles = (
+ <FormattedMessage
+ id='admin.user_item.inactive'
+ defaultMessage='Inactive'
+ />
+ );
+ showMakeMember = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let disableActivationToggle = false;
+ if (user.auth_service === Constants.LDAP_SERVICE) {
+ disableActivationToggle = true;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeSysAdmin'
+ defaultMessage='Make System Admin'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeMember'
+ defaultMessage='Make Member'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let menuClass = '';
+ if (disableActivationToggle) {
+ menuClass = 'disabled';
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li
+ role='presentation'
+ className={menuClass}
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeActive'
+ defaultMessage='Make Active'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li
+ role='presentation'
+ className={menuClass}
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ <FormattedMessage
+ id='admin.user_item.makeInactive'
+ defaultMessage='Make Inactive'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let mfaReset = null;
+ if (showMfaReset) {
+ mfaReset = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetMfa}
+ >
+ <FormattedMessage
+ id='admin.user_item.resetMfa'
+ defaultMessage='Remove MFA'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let passwordReset;
+ if (user.auth_service) {
+ passwordReset = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ <FormattedMessage
+ id='admin.user_item.switchToEmail'
+ defaultMessage='Switch to Email/Password'
+ />
+ </a>
+ </li>
+ );
+ } else {
+ passwordReset = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ <FormattedMessage
+ id='admin.user_item.resetPwd'
+ defaultMessage='Reset Password'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeDemoteModal = null;
+ if (this.props.user.id === me.id) {
+ const title = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteRoleTitle'
+ defaultMessage='Confirm demotion from System Admin role'
+ />
+ );
+
+ const message = (
+ <div>
+ <FormattedMessage
+ id='admin.user_item.confirmDemoteDescription'
+ defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
+ />
+ <br/>
+ <br/>
+ <FormattedMessage
+ id='admin.user_item.confirmDemotionCmd'
+ defaultMessage='platform roles system_admin {username}'
+ values={{
+ username: me.username
+ }}
+ />
+ {serverError}
+ </div>
+ );
+
+ const confirmButton = (
+ <FormattedMessage
+ id='admin.user_item.confirmDemotion'
+ defaultMessage='Confirm Demotion'
+ />
+ );
+
+ makeDemoteModal = (
+ <ConfirmModal
+ show={this.state.showDemoteModal}
+ title={title}
+ message={message}
+ confirmButton={confirmButton}
+ onConfirm={this.handleDemoteSubmit}
+ onCancel={this.handleDemoteCancel}
+ />
+ );
+ }
+
+ let displayedName = Utils.getDisplayName(user);
+ if (displayedName !== user.username) {
+ displayedName += ' (@' + user.username + ')';
+ }
+
+ return (
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'/>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ >
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ {mfaReset}
+ {passwordReset}
+ </ul>
+ {makeDemoteModal}
+ {serverError}
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/system_users/system_users_list.jsx b/webapp/components/admin_console/system_users/system_users_list.jsx
new file mode 100644
index 000000000..5d8837164
--- /dev/null
+++ b/webapp/components/admin_console/system_users/system_users_list.jsx
@@ -0,0 +1,232 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx';
+import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx';
+
+import {getUser} from 'utils/async_client.jsx';
+import {Constants} from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import SystemUsersDropdown from './system_users_dropdown.jsx';
+
+export default class SystemUsersList extends React.Component {
+ static propTypes = {
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ usersPerPage: React.PropTypes.number,
+ total: React.PropTypes.number,
+ nextPage: React.PropTypes.func,
+ search: React.PropTypes.func.isRequired,
+ focusOnMount: React.PropTypes.bool,
+ renderFilterRow: React.PropTypes.func,
+
+ teamId: React.PropTypes.string.isRequired,
+ term: React.PropTypes.string.isRequired,
+ onTermChange: React.PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.nextPage = this.nextPage.bind(this);
+ this.previousPage = this.previousPage.bind(this);
+ this.search = this.search.bind(this);
+
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ page: 0,
+
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.teamId !== this.props.teamId) {
+ this.setState({page: 0});
+ }
+ }
+
+ nextPage() {
+ this.setState({page: this.state.page + 1});
+
+ this.props.nextPage(this.state.page + 1);
+ }
+
+ previousPage() {
+ this.setState({page: this.state.page - 1});
+ }
+
+ search(term) {
+ this.props.search(term);
+
+ if (term !== '') {
+ this.setState({page: 0});
+ }
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.setState({
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit(user) {
+ getUser(user.id);
+
+ this.setState({
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ getInfoForUser(user) {
+ const info = [];
+
+ if (user.auth_service) {
+ let service;
+ if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
+ service = user.auth_service.toUpperCase();
+ } else {
+ service = Utils.toTitleCase(user.auth_service);
+ }
+
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.authServiceNotEmail'
+ id='admin.user_item.authServiceNotEmail'
+ defaultMessage='<strong>Sign-in Method:</strong> {service}'
+ values={{
+ service
+ }}
+ />
+ );
+ } else {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.authServiceEmail'
+ id='admin.user_item.authServiceEmail'
+ defaultMessage='<strong>Sign-in Method:</strong> Email'
+ />
+ );
+ }
+
+ const mfaEnabled = global.window.mm_license.IsLicensed === 'true' &&
+ global.window.mm_license.MFA === 'true' &&
+ global.window.mm_config.EnableMultifactorAuthentication === 'true';
+ if (mfaEnabled) {
+ info.push(', ');
+
+ if (user.mfa_active) {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.mfaYes'
+ id='admin.user_item.mfaYes'
+ defaultMessage='<strong>MFA</strong>: Yes'
+ />
+ );
+ } else {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.mfaNo'
+ id='admin.user_item.mfaNo'
+ defaultMessage='<strong>MFA</strong>: No'
+ />
+ );
+ }
+ }
+
+ return info;
+ }
+
+ renderCount(count, total, startCount, endCount, isSearch) {
+ if (total) {
+ if (isSearch) {
+ return (
+ <FormattedMessage
+ id='system_users_list.countSearch'
+ defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total} total'
+ values={{
+ count,
+ total
+ }}
+ />
+ );
+ } else if (startCount !== 0 || endCount !== total) {
+ return (
+ <FormattedMessage
+ id='system_users_list.countPage'
+ defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total'
+ values={{
+ count,
+ startCount: startCount + 1,
+ endCount,
+ total
+ }}
+ />
+ );
+ }
+
+ return (
+ <FormattedMessage
+ id='system_users_list.count'
+ defaultMessage='{count, number} {count, plural, one {user} other {users}}'
+ values={{
+ count
+ }}
+ />
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ const extraInfo = {};
+ if (this.props.users) {
+ for (const user of this.props.users) {
+ extraInfo[user.id] = this.getInfoForUser(user);
+ }
+ }
+
+ return (
+ <div>
+ <SearchableUserList
+ {...this.props}
+ renderCount={this.renderCount}
+ extraInfo={extraInfo}
+ actions={[SystemUsersDropdown]}
+ actionProps={{
+ doPasswordReset: this.doPasswordReset
+ }}
+ nextPage={this.nextPage}
+ previousPage={this.previousPage}
+ search={this.search}
+ page={this.state.page}
+ term={this.props.term}
+ onTermChange={this.props.onTermChange}
+ />
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}