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
Этот коммит содержится в:
Harrison Healey
2017-03-30 12:46:47 -04:00
коммит произвёл Corey Hulen
родитель 9a9729f22f
Коммит 689cac535e
74 изменённых файлов: 1445 добавлений и 1298 удалений

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -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>
);
}
}