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 удалений

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

@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
import {sortTeamsByDisplayName} from 'utils/utils.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';

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

@@ -112,7 +112,9 @@ export default class AdminSettings extends React.Component {
render() {
return (
<div className='wrapper--fixed'>
{this.renderTitle()}
<h3 className='admin-console-header'>
{this.renderTitle()}
</h3>
<form
className='form-horizontal'
role='form'

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

@@ -3,18 +3,12 @@
import $ from 'jquery';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import AdminStore from 'stores/admin_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import AdminSidebarTeam from './admin_sidebar_team.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import SelectTeamModal from './select_team_modal.jsx';
import AdminSidebarCategory from './admin_sidebar_category.jsx';
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebar extends React.Component {
@@ -27,31 +21,15 @@ export default class AdminSidebar extends React.Component {
constructor(props) {
super(props);
this.handleAllTeamsChange = this.handleAllTeamsChange.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.updateTitle = this.updateTitle.bind(this);
this.renderAddTeamButton = this.renderAddTeamButton.bind(this);
this.renderTeams = this.renderTeams.bind(this);
this.state = {
teams: AdminStore.getAllTeams(),
selectedTeams: AdminStore.getSelectedTeams(),
showSelectModal: false
};
}
componentDidMount() {
AdminStore.addAllTeamsChangeListener(this.handleAllTeamsChange);
AsyncClient.getAllTeams();
this.updateTitle();
if (!Utils.isMobile()) {
$('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
componentDidUpdate() {
@@ -60,51 +38,6 @@ export default class AdminSidebar extends React.Component {
}
}
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.handleAllTeamsChange);
}
handleAllTeamsChange() {
this.setState({
teams: AdminStore.getAllTeams(),
selectedTeams: AdminStore.getSelectedTeams()
});
}
removeTeam(team) {
const selectedTeams = Object.assign({}, this.state.selectedTeams);
Reflect.deleteProperty(selectedTeams, team.id);
AdminStore.saveSelectedTeams(selectedTeams);
this.handleAllTeamsChange();
if (this.context.router.isActive('/admin_console/team/' + team.id)) {
browserHistory.push('/admin_console');
}
}
showTeamSelect(e) {
e.preventDefault();
this.setState({showSelectModal: true});
}
teamSelectedModal(teamId) {
this.setState({
showSelectModal: false
});
const selectedTeams = Object.assign({}, this.state.selectedTeams);
selectedTeams[teamId] = true;
AdminStore.saveSelectedTeams(selectedTeams);
this.handleAllTeamsChange();
}
teamSelectedModalDismissed() {
this.setState({showSelectModal: false});
}
updateTitle() {
let currentSiteName = '';
if (global.window.mm_config.SiteName != null) {
@@ -114,79 +47,6 @@ export default class AdminSidebar extends React.Component {
document.title = Utils.localizeMessage('sidebar_right_menu.console', 'System Console') + ' - ' + currentSiteName;
}
renderAddTeamButton() {
const addTeamTooltip = (
<Tooltip id='add-team-tooltip'>
<FormattedMessage
id='admin.sidebar.addTeamSidebar'
defaultMessage='Add team from sidebar menu'
/>
</Tooltip>
);
return (
<span className='menu-icon--right'>
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={addTeamTooltip}
>
<a
href='#'
onClick={this.showTeamSelect}
>
<i
className='fa fa-plus'
/>
</a>
</OverlayTrigger>
</span>
);
}
renderTeams() {
const teams = [];
let teamsArray = [];
Reflect.ownKeys(this.state.selectedTeams).forEach((key) => {
if (this.state.teams[key]) {
teamsArray.push(this.state.teams[key]);
}
});
teamsArray = teamsArray.sort(Utils.sortTeamsByDisplayName);
for (let i = 0; i < teamsArray.length; i++) {
const team = teamsArray[i];
teams.push(
<AdminSidebarTeam
key={team.id}
team={team}
onRemoveTeam={this.removeTeam}
/>
);
}
return (
<AdminSidebarCategory
parentLink='/admin_console'
icon='fa-user'
title={
<FormattedMessage
id='admin.sidebar.teams'
defaultMessage='TEAMS ({count, number})'
values={{
count: Object.keys(this.state.teams).length
}}
/>
}
action={this.renderAddTeamButton()}
>
{teams}
</AdminSidebarCategory>
);
}
render() {
let oauthSettings = null;
let ldapSettings = null;
@@ -421,6 +281,24 @@ export default class AdminSidebar extends React.Component {
/>
}
/>
<AdminSidebarSection
name='team_analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='Team Statistics'
/>
}
/>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='Users'
/>
}
/>
<AdminSidebarSection
name='logs'
title={
@@ -760,16 +638,9 @@ export default class AdminSidebar extends React.Component {
{metricsSettings}
</AdminSidebarSection>
</AdminSidebarCategory>
{this.renderTeams()}
{otherCategory}
</ul>
</div>
<SelectTeamModal
teams={this.state.teams}
show={this.state.showSelectModal}
onModalSubmit={this.teamSelectedModal}
onModalDismissed={this.teamSelectedModalDismissed}
/>
</div>
);
}

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

@@ -1,87 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebarTeam extends React.Component {
static get propTypes() {
return {
team: React.PropTypes.object.isRequired,
onRemoveTeam: React.PropTypes.func.isRequired,
parentLink: React.PropTypes.string
};
}
constructor(props) {
super(props);
this.handleRemoveTeam = this.handleRemoveTeam.bind(this);
}
handleRemoveTeam(e) {
e.preventDefault();
this.props.onRemoveTeam(this.props.team);
}
render() {
const team = this.props.team;
const removeTeamTooltip = (
<Tooltip id='remove-team-tooltip'>
<FormattedMessage
id='admin.sidebar.rmTeamSidebar'
defaultMessage='Remove team from sidebar menu'
/>
</Tooltip>
);
const removeTeamButton = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={removeTeamTooltip}
>
<span
className='menu-icon--right menu__close'
onClick={this.handleRemoveTeam}
>
{'×'}
</span>
</OverlayTrigger>
);
return (
<AdminSidebarSection
key={team.id}
name={'team/' + team.id}
parentLink={this.props.parentLink}
title={team.display_name}
action={removeTeamButton}
>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='- Users'
/>
}
/>
<AdminSidebarSection
name='analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='- Team Statistics'
/>
}
/>
</AdminSidebarSection>
);
}
}

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

@@ -76,7 +76,7 @@ export default class Audits extends React.Component {
<ComplianceReports/>
<div className='panel audit-panel'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.audits.title'
defaultMessage='User Activity Logs'

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

@@ -51,12 +51,10 @@ export default class ClusterSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
</h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
);
}

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

@@ -38,12 +38,10 @@ export default class ComplianceSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
</h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
);
}

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

@@ -64,12 +64,10 @@ export default class ConfigurationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
</h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
);
}

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

@@ -36,12 +36,10 @@ export default class ConnectionSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
</h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
);
}

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

@@ -44,12 +44,10 @@ export default class CustomBrandSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
</h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
);
}
@@ -155,4 +153,4 @@ export default class CustomBrandSettings extends AdminSettings {
</SettingsGroup>
);
}
}
}

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

@@ -39,12 +39,10 @@ export default class CustomEmojiSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
</h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
);
}

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

@@ -43,12 +43,10 @@ export default class WebhookSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.custom'
defaultMessage='Custom Integrations'
/>
</h3>
<FormattedMessage
id='admin.integrations.custom'
defaultMessage='Custom Integrations'
/>
);
}

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

@@ -46,12 +46,10 @@ export default class DatabaseSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.database.title'
defaultMessage='Database Settings'
/>
</h3>
<FormattedMessage
id='admin.database.title'
defaultMessage='Database Settings'
/>
);
}

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

@@ -33,12 +33,10 @@ export default class DeveloperSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.developer.title'
defaultMessage='Developer Settings'
/>
</h3>
<FormattedMessage
id='admin.developer.title'
defaultMessage='Developer Settings'
/>
);
}

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

@@ -35,12 +35,10 @@ export default class EmailAuthenticationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.email'
defaultMessage='Email'
/>
</h3>
<FormattedMessage
id='admin.authentication.email'
defaultMessage='Email'
/>
);
}

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

@@ -56,12 +56,10 @@ export default class EmailSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.notifications.email'
defaultMessage='Email'
/>
</h3>
<FormattedMessage
id='admin.notifications.email'
defaultMessage='Email'
/>
);
}

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

@@ -32,12 +32,10 @@ export default class ExternalServiceSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.external'
defaultMessage='External Services'
/>
</h3>
<FormattedMessage
id='admin.integrations.external'
defaultMessage='External Services'
/>
);
}

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

@@ -44,12 +44,10 @@ export default class GitLabSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.gitlab'
defaultMessage='GitLab'
/>
</h3>
<FormattedMessage
id='admin.authentication.gitlab'
defaultMessage='GitLab'
/>
);
}

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

@@ -43,12 +43,10 @@ export default class ImageSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.files.images'
defaultMessage='Images'
/>
</h3>
<FormattedMessage
id='admin.files.images'
defaultMessage='Images'
/>
);
}

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

@@ -76,12 +76,10 @@ export default class LdapSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.ldap'
defaultMessage='AD/LDAP'
/>
</h3>
<FormattedMessage
id='admin.authentication.ldap'
defaultMessage='AD/LDAP'
/>
);
}

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

@@ -41,12 +41,10 @@ export default class LegalAndSupportSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.support'
defaultMessage='Legal and Support'
/>
</h3>
<FormattedMessage
id='admin.customization.support'
defaultMessage='Legal and Support'
/>
);
}

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

@@ -221,7 +221,7 @@ class LicenseSettings extends React.Component {
return (
<div className='wrapper--fixed'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.license.title'
defaultMessage='Edition and License'

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

@@ -31,12 +31,10 @@ export default class LinkPreviewsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.linkPreviews'
defaultMessage='Link Previews'
/>
</h3>
<FormattedMessage
id='admin.customization.linkPreviews'
defaultMessage='Link Previews'
/>
);
}

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

@@ -52,12 +52,10 @@ export default class LocalizationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.localization'
defaultMessage='Localization'
/>
</h3>
<FormattedMessage
id='admin.general.localization'
defaultMessage='Localization'
/>
);
}

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

@@ -49,12 +49,10 @@ export default class LogSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.log'
defaultMessage='Logging'
/>
</h3>
<FormattedMessage
id='admin.general.log'
defaultMessage='Logging'
/>
);
}

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

@@ -85,7 +85,7 @@ export default class Logs extends React.Component {
return (
<div className='panel'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.logs.title'
defaultMessage='Server Logs'

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

@@ -38,12 +38,10 @@ export default class MetricsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.advance.metrics'
defaultMessage='Performance Monitoring'
/>
</h3>
<FormattedMessage
id='admin.advance.metrics'
defaultMessage='Performance Monitoring'
/>
);
}

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

@@ -38,12 +38,10 @@ export default class MfaSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.mfa.title'
defaultMessage='Multi-factor Authentication'
/>
</h3>
<FormattedMessage
id='admin.mfa.title'
defaultMessage='Multi-factor Authentication'
/>
);
}

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

@@ -35,12 +35,10 @@ export default class NativeAppLinkSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.nativeAppLinks'
defaultMessage='Mattermost App Links'
/>
</h3>
<FormattedMessage
id='admin.customization.nativeAppLinks'
defaultMessage='Mattermost App Links'
/>
);
}

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

@@ -111,12 +111,10 @@ export default class OAuthSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.oauth'
defaultMessage='OAuth 2.0'
/>
</h3>
<FormattedMessage
id='admin.authentication.oauth'
defaultMessage='OAuth 2.0'
/>
);
}

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

@@ -138,12 +138,10 @@ export default class PasswordSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.password'
defaultMessage='Password'
/>
</h3>
<FormattedMessage
id='admin.security.password'
defaultMessage='Password'
/>
);
}

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

@@ -55,12 +55,10 @@ export default class PolicySettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.policy'
defaultMessage='Policy'
/>
</h3>
<FormattedMessage
id='admin.general.policy'
defaultMessage='Policy'
/>
);
}

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

@@ -33,12 +33,10 @@ export default class PrivacySettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.privacy'
defaultMessage='Privacy'
/>
</h3>
<FormattedMessage
id='admin.general.privacy'
defaultMessage='Privacy'
/>
);
}

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

@@ -34,12 +34,10 @@ export default class PublicLinkSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.public_links'
defaultMessage='Public Links'
/>
</h3>
<FormattedMessage
id='admin.security.public_links'
defaultMessage='Public Links'
/>
);
}

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

@@ -100,12 +100,10 @@ export default class PushSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.notifications.title'
defaultMessage='Notification Settings'
/>
</h3>
<FormattedMessage
id='admin.notifications.title'
defaultMessage='Notification Settings'
/>
);
}

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

@@ -44,12 +44,10 @@ export default class RateSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.rate.title'
defaultMessage='Rate Limit Settings'
/>
</h3>
<FormattedMessage
id='admin.rate.title'
defaultMessage='Rate Limit Settings'
/>
);
}

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

@@ -4,13 +4,24 @@
import * as Utils from 'utils/utils.jsx';
import {Modal} from 'react-bootstrap';
import {injectIntl, intlShape, FormattedMessage} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {adminResetPassword} from 'actions/admin_actions.jsx';
import React from 'react';
class ResetPasswordModal extends React.Component {
export default class ResetPasswordModal extends React.Component {
static propTypes = {
user: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};
static defaultProps = {
show: false
};
constructor(props) {
super(props);
@@ -150,18 +161,3 @@ class ResetPasswordModal extends React.Component {
);
}
}
ResetPasswordModal.defaultProps = {
show: false
};
ResetPasswordModal.propTypes = {
intl: intlShape.isRequired,
user: React.PropTypes.object,
team: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};
export default injectIntl(ResetPasswordModal);

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

@@ -130,12 +130,10 @@ export default class SamlSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.saml'
defaultMessage='SAML'
/>
</h3>
<FormattedMessage
id='admin.authentication.saml'
defaultMessage='SAML'
/>
);
}

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

@@ -1,120 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import React from 'react';
import {sortTeamsByDisplayName} from 'utils/utils.jsx';
export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
this.doSubmit = this.doSubmit.bind(this);
this.doCancel = this.doCancel.bind(this);
}
doSubmit(e) {
e.preventDefault();
this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value);
}
doCancel() {
this.props.onModalDismissed();
}
render() {
if (this.props.teams == null) {
return <div/>;
}
const options = [];
let teamsArray = [];
Reflect.ownKeys(this.props.teams).forEach((key) => {
teamsArray.push(this.props.teams[key]);
});
teamsArray = teamsArray.sort(sortTeamsByDisplayName);
for (let i = 0; i < teamsArray.length; i++) {
const team = teamsArray[i];
options.push(
<option
key={'opt_' + team.id}
value={team.id}
>
{team.display_name}
</option>
);
}
return (
<Modal
show={this.props.show}
onHide={this.doCancel}
>
<Modal.Header closeButton={true}>
<Modal.Title>
<FormattedMessage
id='admin.select_team.selectTeam'
defaultMessage='Select Team'
/>
</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='form-group'>
<div className='col-sm-12'>
<select
ref='team'
size='10'
className='form-control'
>
{options}
</select>
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.doCancel}
>
<FormattedMessage
id='admin.select_team.close'
defaultMessage='Close'
/>
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
tabIndex='2'
>
<FormattedMessage
id='admin.select_team.select'
defaultMessage='Select'
/>
</button>
</Modal.Footer>
</form>
</Modal>
);
}
}
SelectTeamModal.defaultProps = {
show: false
};
SelectTeamModal.propTypes = {
teams: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};

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

@@ -39,12 +39,10 @@ export default class SessionSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.session'
defaultMessage='Sessions'
/>
</h3>
<FormattedMessage
id='admin.security.session'
defaultMessage='Sessions'
/>
);
}

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

@@ -36,12 +36,10 @@ export default class SignupSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.signup'
defaultMessage='Signup'
/>
</h3>
<FormattedMessage
id='admin.security.signup'
defaultMessage='Signup'
/>
);
}

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

@@ -52,12 +52,10 @@ export default class StorageSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.files.storage'
defaultMessage='Storage'
/>
</h3>
<FormattedMessage
id='admin.files.storage'
defaultMessage='Storage'
/>
);
}

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

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

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

@@ -1,38 +1,38 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ConfirmModal from '../confirm_modal.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
import UserStore from 'stores/user_store.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 {updateTeamMemberRoles, removeUserFromTeam} from 'actions/team_actions.jsx';
import {adminResetMfa} from 'actions/admin_actions.jsx';
import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class AdminTeamMembersDropdown extends React.Component {
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.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this);
this.handleMakeActive = this.handleMakeActive.bind(this);
this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
this.handleMakeTeamAdmin = this.handleMakeTeamAdmin.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.doMakeMember = this.doMakeMember.bind(this);
this.doMakeTeamAdmin = this.doMakeTeamAdmin.bind(this);
this.state = {
serverError: null,
@@ -51,16 +51,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
this.setState({serverError: err.message});
}
);
updateTeamMemberRoles(
this.props.teamMember.team_id,
this.props.user.id,
'team_user',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeMember(e) {
@@ -73,17 +63,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
}
handleRemoveFromTeam() {
removeUserFromTeam(
this.props.teamMember.team_id,
this.props.user.id,
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeActive(e) {
e.preventDefault();
updateActive(this.props.user.id, true, null,
@@ -102,28 +81,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
doMakeTeamAdmin() {
updateTeamMemberRoles(
this.props.teamMember.team_id,
this.props.user.id,
'team_user team_admin',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeTeamAdmin(e) {
e.preventDefault();
const me = UserStore.getCurrentUser();
if (this.props.user.id === me.id && me.roles.includes('system_admin')) {
this.handleDemoteSystemAdmin(this.props.user, 'teamadmin');
} else {
this.doMakeTeamAdmin();
}
}
handleMakeSystemAdmin(e) {
e.preventDefault();
@@ -174,8 +131,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
handleDemoteSubmit() {
if (this.state.role === 'member') {
this.doMakeMember();
} else {
this.doMakeTeamAdmin();
}
const teamUrl = TeamStore.getCurrentTeamUrl();
@@ -197,9 +152,8 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
const teamMember = this.props.teamMember;
const user = this.props.user;
if (!user || !teamMember) {
if (!user) {
return <div/>;
}
let currentRoles = (
@@ -209,15 +163,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
/>
);
if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) {
currentRoles = (
<FormattedMessage
id='team_members_dropdown.teamAdmin'
defaultMessage='Team Admin'
/>
);
}
if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) {
currentRoles = (
<FormattedMessage
@@ -228,8 +173,7 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
const me = UserStore.getCurrentUser();
let showMakeMember = Utils.isAdmin(teamMember.roles) || Utils.isSystemAdmin(user.roles);
let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles);
let showMakeMember = Utils.isSystemAdmin(user.roles);
let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles);
let showMakeActive = false;
let showMakeNotActive = !Utils.isSystemAdmin(user.roles);
@@ -244,7 +188,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
/>
);
showMakeMember = false;
showMakeAdmin = false;
showMakeSystemAdmin = false;
showMakeActive = true;
showMakeNotActive = false;
@@ -273,24 +216,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
let makeAdmin = null;
if (showMakeAdmin) {
makeAdmin = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.handleMakeTeamAdmin}
>
<FormattedMessage
id='admin.user_item.makeTeamAdmin'
defaultMessage='Make Team Admin'
/>
</a>
</li>
);
}
let makeMember = null;
if (showMakeMember) {
makeMember = (
@@ -309,24 +234,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
let removeFromTeam = null;
if (this.props.user.id !== me.id) {
removeFromTeam = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.handleRemoveFromTeam}
>
<FormattedMessage
id='team_members_dropdown.leave_team'
defaultMessage='Remove From Team'
/>
</a>
</li>
);
}
let menuClass = '';
if (disableActivationToggle) {
menuClass = 'disabled';
@@ -493,8 +400,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
className='dropdown-menu member-menu'
role='menu'
>
{removeFromTeam}
{makeAdmin}
{makeMember}
{makeActive}
{makeNotActive}
@@ -508,9 +413,3 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
}
AdminTeamMembersDropdown.propTypes = {
user: React.PropTypes.object.isRequired,
teamMember: React.PropTypes.object.isRequired,
doPasswordReset: React.PropTypes.func.isRequired
};

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

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

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

@@ -1,298 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import SearchableUserList from 'components/searchable_user_list.jsx';
import AdminTeamMembersDropdown from './admin_team_members_dropdown.jsx';
import ResetPasswordModal from './reset_password_modal.jsx';
import FormError from 'components/form_error.jsx';
import AdminStore from 'stores/admin_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx';
import {getTeamStats, getUser} from 'utils/async_client.jsx';
import {Constants, UserSearchOptions} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const USERS_PER_PAGE = 50;
export default class UserList extends React.Component {
static get propTypes() {
return {
params: React.PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.onUsersChange = this.onUsersChange.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
this.searchTimeoutId = 0;
const stats = TeamStore.getStats(this.props.params.team);
this.state = {
team: AdminStore.getTeam(this.props.params.team),
users: [],
teamMembers: TeamStore.getMembersInTeam(this.props.params.team),
total: stats.total_member_count,
serverError: null,
showPasswordModal: false,
loading: true,
user: null
};
}
componentDidMount() {
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.addChangeListener(this.onUsersChange);
UserStore.addInTeamChangeListener(this.onUsersChange);
TeamStore.addChangeListener(this.onTeamChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, this.props.params.team, this.loadComplete);
getTeamStats(this.props.params.team);
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.team !== this.props.params.team) {
const stats = TeamStore.getStats(nextProps.params.team);
this.setState({
team: AdminStore.getTeam(nextProps.params.team),
users: [],
teamMembers: TeamStore.getMembersInTeam(nextProps.params.team),
total: stats.total_member_count,
serverError: null,
showPasswordModal: false,
loading: true,
user: null
});
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, nextProps.params.team, this.loadComplete);
getTeamStats(nextProps.params.team);
}
}
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.removeChangeListener(this.onUsersChange);
UserStore.removeInTeamChangeListener(this.onUsersChange);
TeamStore.removeChangeListener(this.onTeamChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
loadComplete() {
this.setState({loading: false});
}
onAllTeamsChange() {
this.setState({
team: AdminStore.getTeam(this.props.params.team)
});
}
onStatsChange() {
const stats = TeamStore.getStats(this.props.params.team);
this.setState({total: stats.total_member_count});
}
onUsersChange() {
this.setState({users: UserStore.getProfileListInTeam(this.props.params.team)});
}
onTeamChange() {
this.setState({teamMembers: TeamStore.getMembersInTeam(this.props.params.team)});
}
nextPage(page) {
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.props.params.team);
}
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
});
}
search(term) {
clearTimeout(this.searchTimeoutId);
if (term === '') {
this.setState({search: false, users: UserStore.getProfileListInTeam(this.props.params.team)});
this.searchTimeoutId = '';
return;
}
const options = {};
options[UserSearchOptions.ALLOW_INACTIVE] = true;
const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
this.props.params.team,
options,
(users) => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete);
}
);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
render() {
if (!this.state.team) {
return null;
}
const teamMembers = this.state.teamMembers;
const users = this.state.users;
const actionUserProps = {};
const extraInfo = {};
const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
let usersToDisplay;
if (this.state.loading) {
usersToDisplay = null;
} else {
usersToDisplay = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (teamMembers[user.id]) {
usersToDisplay.push(user);
actionUserProps[user.id] = {
teamMember: teamMembers[user.id]
};
const info = [];
if (user.auth_service) {
const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : 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'
/>
);
}
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'
/>
);
}
}
extraInfo[user.id] = info;
}
}
}
return (
<div className='wrapper--fixed'>
<h3>
<FormattedMessage
id='admin.userList.title2'
defaultMessage='Users for {team} ({count})'
values={{
team: this.state.team.name,
count: this.state.total
}}
/>
</h3>
<FormError error={this.state.serverError}/>
<div className='more-modal__list member-list-holder'>
<SearchableUserList
users={usersToDisplay}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
extraInfo={extraInfo}
nextPage={this.nextPage}
search={this.search}
actions={[AdminTeamMembersDropdown]}
actionProps={{
doPasswordReset: this.doPasswordReset
}}
actionUserProps={actionUserProps}
/>
</div>
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
team={this.state.team}
onModalSubmit={this.doPasswordResetSubmit}
onModalDismissed={this.doPasswordResetDismiss}
/>
</div>
);
}
}

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

@@ -51,12 +51,10 @@ export default class UsersAndTeamsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.usersAndTeams'
defaultMessage='Users and Teams'
/>
</h3>
<FormattedMessage
id='admin.general.usersAndTeams'
defaultMessage='Users and Teams'
/>
);
}

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

@@ -50,12 +50,10 @@ export default class WebrtcSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.webrtc'
defaultMessage='Mattermost WebRTC (Beta)'
/>
</h3>
<FormattedMessage
id='admin.integrations.webrtc'
defaultMessage='Mattermost WebRTC (Beta)'
/>
);
}

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

@@ -21,14 +21,33 @@ export default class LineChart extends React.Component {
this.initChart();
}
componentWillUpdate(nextProps) {
const willHaveData = nextProps.data && nextProps.data.labels.length > 0;
const hasChart = Boolean(this.chart);
if (!willHaveData && hasChart) {
// Clean up the rendered chart before we render and destroy its context
this.chart.destroy();
this.chart = null;
}
}
componentDidUpdate(prevProps) {
if (!Utils.areObjectsEqual(prevProps.data, this.props.data) || !Utils.areObjectsEqual(prevProps.options, this.props.options)) {
this.initChart(true);
if (Utils.areObjectsEqual(prevProps.data, this.props.data) && Utils.areObjectsEqual(prevProps.options, this.props.options)) {
return;
}
const hasData = this.props.data && this.props.data.labels.length > 0;
const hasChart = Boolean(this.chart);
if (hasData) {
// Update the rendered chart or initialize it as necessary
this.initChart(hasChart);
}
}
componentWillUnmount() {
if (this.chart && this.refs.canvas) {
if (this.chart) {
this.chart.destroy();
}
}
@@ -37,9 +56,11 @@ export default class LineChart extends React.Component {
if (!this.refs.canvas) {
return;
}
var el = ReactDOM.findDOMNode(this.refs.canvas);
var ctx = el.getContext('2d');
this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); //eslint-disable-line new-cap
this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); // eslint-disable-line new-cap
if (update) {
this.chart.update();
}

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

@@ -409,7 +409,7 @@ class SystemAnalytics extends React.Component {
return (
<div className='wrapper--fixed team_statistics'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='analytics.system.title'
defaultMessage='System Statistics'

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

@@ -1,40 +1,43 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedDate, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import Banner from 'components/admin_console/banner.jsx';
import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import AdminStore from 'stores/admin_store.jsx';
import AnalyticsStore from 'stores/analytics_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
const StatTypes = Constants.StatTypes;
import {StatTypes} from 'utils/constants.jsx';
import {convertTeamMapToList} from 'utils/team_utils.jsx';
import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
import {FormattedMessage, FormattedDate, FormattedHTMLMessage} from 'react-intl';
import React from 'react';
const LAST_ANALYTICS_TEAM = 'last_analytics_team';
export default class TeamAnalytics extends React.Component {
static get propTypes() {
return {
params: React.PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
this.handleTeamChange = this.handleTeamChange.bind(this);
const teams = convertTeamMapToList(AdminStore.getAllTeams());
const teamId = BrowserStore.getGlobalItem(LAST_ANALYTICS_TEAM, teams.length > 0 ? teams[0].id : '');
this.state = {
team: AdminStore.getTeam(this.props.params.team),
stats: AnalyticsStore.getAllTeam(this.props.params.team)
teams,
teamId,
team: AdminStore.getTeam(teamId),
stats: AnalyticsStore.getAllTeam(teamId)
};
}
@@ -42,7 +45,19 @@ export default class TeamAnalytics extends React.Component {
AnalyticsStore.addChangeListener(this.onChange);
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
this.getData(this.props.params.team);
if (this.state.teamId !== '') {
this.getData(this.state.teamId);
}
if (this.state.teams.length === 0) {
AsyncClient.getAllTeams();
}
}
componentWillUpdate(nextProps, nextState) {
if (nextState.teamId !== this.state.teamId) {
this.getData(nextState.teamId);
}
}
getData(id) {
@@ -57,40 +72,60 @@ export default class TeamAnalytics extends React.Component {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
}
componentWillReceiveProps(nextProps) {
this.getData(nextProps.params.team);
this.setState({
stats: AnalyticsStore.getAllTeam(nextProps.params.team)
});
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) {
return true;
}
return false;
}
onChange() {
this.setState({
stats: AnalyticsStore.getAllTeam(this.props.params.team)
stats: AnalyticsStore.getAllTeam(this.state.teamId)
});
}
onAllTeamsChange() {
const teams = convertTeamMapToList(AdminStore.getAllTeams());
if (teams.length > 0) {
if (this.state.teamId) {
this.setState({
team: AdminStore.getTeam(this.state.teamId)
});
} else {
this.setState({
teamId: teams[0].id,
team: teams[0]
});
}
}
this.setState({
team: AdminStore.getTeam(this.props.params.team)
teams
});
}
handleTeamChange(e) {
const teamId = e.target.value;
this.setState({
teamId,
team: AdminStore.getTeam(teamId)
});
BrowserStore.setGlobalItem(LAST_ANALYTICS_TEAM, teamId);
}
render() {
if (!this.state.team || !this.state.stats) {
return null;
if (this.state.teams.length === 0 || !this.state.team || !this.state.stats) {
return <LoadingScreen/>;
}
if (this.state.teamId === '') {
return (
<Banner
description={
<FormattedMessage
id='analytics.team.noTeams'
defaultMessage='There are no teams on this server for which to view statistics.'
/>
}
/>
);
}
const stats = this.state.stats;
@@ -129,6 +164,7 @@ export default class TeamAnalytics extends React.Component {
postTotalGraph = (
<div className='row'>
<LineChart
key={this.state.team.id}
title={
<FormattedMessage
id='analytics.team.totalPosts'
@@ -150,6 +186,7 @@ export default class TeamAnalytics extends React.Component {
userActiveGraph = (
<div className='row'>
<LineChart
key={this.state.team.id}
title={
<FormattedMessage
id='analytics.team.activeUsers'
@@ -172,17 +209,41 @@ export default class TeamAnalytics extends React.Component {
const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
const teams = this.state.teams.map((team) => {
return (
<option
key={team.id}
value={team.id}
>
{team.display_name}
</option>
);
});
return (
<div className='wrapper--fixed team_statistics'>
<h3>
<FormattedMessage
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
team: this.state.team.name
}}
/>
</h3>
<div className='admin-console-header team-statistics__header-row'>
<div className='team-statistics__header'>
<h3>
<FormattedMessage
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
team: this.state.team.display_name
}}
/>
</h3>
</div>
<div className='team-statistics__team-filter'>
<select
className='form-control team-statistics__team-filter__dropdown'
onChange={this.handleTeamChange}
value={this.state.teamId}
>
{teams}
</select>
</div>
</div>
{banner}
<div className='row'>
<StatisticCount

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

@@ -2,7 +2,7 @@
// See License.txt for license information.
import ChannelInviteButton from './channel_invite_button.jsx';
import SearchableUserList from './searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import LoadingScreen from './loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';

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

@@ -2,7 +2,7 @@
// See License.txt for license information.
import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx';
import SearchableUserList from 'components/searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import SearchableUserList from 'components/searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import TeamMembersDropdown from 'components/team_members_dropdown.jsx';
import UserStore from 'stores/user_store.jsx';

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

@@ -41,7 +41,6 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from 'components/invite_member_modal.jsx';
import LeaveTeamModal from 'components/leave_team_modal.jsx';
import SelectTeamModal from 'components/admin_console/select_team_modal.jsx';
import iNoBounce from 'inobounce';
import * as UserAgent from 'utils/user_agent.jsx';
@@ -212,7 +211,6 @@ export default class NeedsTeam extends React.Component {
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
<SelectTeamModal/>
</div>
</div>
);

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

@@ -1,31 +1,61 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import UserList from 'components/user_list.jsx';
import * as Utils from 'utils/utils.jsx';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
const NEXT_BUTTON_TIMEOUT = 500;
export default class SearchableUserList extends React.Component {
static propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
previousPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool,
renderCount: React.PropTypes.func,
renderFilterRow: React.PropTypes.func,
page: React.PropTypes.number.isRequired,
term: React.PropTypes.string.isRequired,
onTermChange: React.PropTypes.func.isRequired
};
static defaultProps = {
users: [],
usersPerPage: 50, // eslint-disable-line no-magic-numbers
extraInfo: {},
actions: [],
actionProps: {},
actionUserProps: {},
showTeamToggle: false,
focusOnMount: false
};
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.doSearch = this.doSearch.bind(this);
this.focusSearchBar = this.focusSearchBar.bind(this);
this.handleInput = this.handleInput.bind(this);
this.renderCount = this.renderCount.bind(this);
this.nextTimeoutId = 0;
this.state = {
page: 0,
search: false,
nextDisabled: false
};
}
@@ -34,10 +64,11 @@ export default class SearchableUserList extends React.Component {
this.focusSearchBar();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.page !== prevState.page) {
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
componentDidUpdate(prevProps) {
if (this.props.page !== prevProps.page || this.props.term !== prevProps.term) {
this.refs.userList.scrollToTop();
}
this.focusSearchBar();
}
@@ -47,14 +78,17 @@ export default class SearchableUserList extends React.Component {
nextPage(e) {
e.preventDefault();
this.setState({page: this.state.page + 1, nextDisabled: true});
this.setState({nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT);
this.props.nextPage(this.state.page + 1);
this.props.nextPage();
}
previousPage(e) {
e.preventDefault();
this.setState({page: this.state.page - 1});
this.props.previousPage();
}
focusSearchBar() {
@@ -63,41 +97,74 @@ export default class SearchableUserList extends React.Component {
}
}
doSearch() {
const term = this.refs.filter.value;
this.props.search(term);
if (term === '') {
this.setState({page: 0, search: false});
} else {
this.setState({search: true});
handleInput(e) {
this.props.onTermChange(e.target.value);
this.props.search(e.target.value);
}
renderCount(users) {
if (!users) {
return null;
}
const count = users.length;
const total = this.props.total;
const isSearch = Boolean(this.props.term);
let startCount;
let endCount;
if (isSearch) {
startCount = -1;
endCount = -1;
} else {
startCount = this.props.page * this.props.usersPerPage;
endCount = startCount + count;
}
if (this.props.renderCount) {
return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch);
}
if (this.props.total) {
if (isSearch) {
return (
<FormattedMessage
id='filtered_user_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
total
}}
/>
);
}
return (
<FormattedMessage
id='filtered_user_list.countTotalPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
startCount: startCount + 1,
endCount,
total
}}
/>
);
}
return null;
}
render() {
let nextButton;
let previousButton;
let usersToDisplay;
let count;
if (this.props.users == null) {
if (this.props.term || !this.props.users) {
usersToDisplay = this.props.users;
} else if (this.state.search || this.props.users == null) {
usersToDisplay = this.props.users;
if (this.props.total) {
count = (
<FormattedMessage
id='filtered_user_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count: usersToDisplay.length || 0,
total: this.props.total
}}
/>
);
}
} else {
const pageStart = this.state.page * this.props.usersPerPage;
} else if (!this.props.term) {
const pageStart = this.props.page * this.props.usersPerPage;
const pageEnd = pageStart + this.props.usersPerPage;
usersToDisplay = this.props.users.slice(pageStart, pageEnd);
@@ -116,7 +183,7 @@ export default class SearchableUserList extends React.Component {
);
}
if (this.state.page > 0) {
if (this.props.page > 0) {
previousButton = (
<button
className='btn btn-default filter-control filter-control__prev'
@@ -129,46 +196,38 @@ export default class SearchableUserList extends React.Component {
</button>
);
}
}
if (this.props.total) {
const startCount = this.state.page * this.props.usersPerPage;
const endCount = startCount + usersToDisplay.length;
count = (
<FormattedMessage
id='filtered_user_list.countTotalPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count: usersToDisplay.length,
startCount: startCount + 1,
endCount,
total: this.props.total
}}
let filterRow;
if (this.props.renderFilterRow) {
filterRow = this.props.renderFilterRow(this.handleInput);
} else {
filterRow = (
<div className='col-xs-12'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
value={this.props.term}
onInput={this.handleInput}
/>
);
}
</div>
);
}
return (
<div className='filtered-user-list'>
<div className='filter-row'>
<div className='col-xs-12'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
onInput={this.doSearch}
/>
</div>
{filterRow}
<div className='col-sm-12'>
<span className='member-count pull-left'>{count}</span>
<span className='member-count pull-left'>{this.renderCount(usersToDisplay)}</span>
</div>
</div>
<div
ref='userList'
className='more-modal__list'
>
<UserList
ref='userList'
users={usersToDisplay}
extraInfo={this.props.extraInfo}
actions={this.props.actions}
@@ -184,27 +243,3 @@ export default class SearchableUserList extends React.Component {
);
}
}
SearchableUserList.defaultProps = {
users: [],
usersPerPage: 50, //eslint-disable-line no-magic-numbers
extraInfo: {},
actions: [],
actionProps: {},
actionUserProps: {},
showTeamToggle: false,
focusOnMount: false
};
SearchableUserList.propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool.isRequired
};

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

@@ -0,0 +1,72 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import SearchableUserList from './searchable_user_list.jsx';
export default class SearchableUserListContainer extends React.Component {
static propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool
};
constructor(props) {
super(props);
this.handleTermChange = this.handleTermChange.bind(this);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.search = this.search.bind(this);
this.state = {
term: '',
page: 0
};
}
handleTermChange(term) {
this.setState({term});
}
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});
}
}
render() {
return (
<SearchableUserList
{...this.props}
nextPage={this.nextPage}
previousPage={this.previousPage}
search={this.search}
page={this.state.page}
term={this.state.term}
onTermChange={this.handleTermChange}
/>
);
}
}

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

@@ -18,6 +18,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import ModalStore from 'stores/modal_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
@@ -637,7 +638,7 @@ export default class Sidebar extends React.Component {
// create elements for all 4 types of channels
const favoriteItems = this.state.favoriteChannels.
sort(Utils.sortTeamsByDisplayName).
sort(sortTeamsByDisplayName).
map((channel, index, arr) => {
if (channel.type === Constants.DM_CHANNEL) {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);

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

@@ -7,6 +7,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import $ from 'jquery';
@@ -118,7 +119,7 @@ export default class TeamSidebar extends React.Component {
}
const teams = myTeams.
sort(Utils.sortTeamsByDisplayName).
sort(sortTeamsByDisplayName).
map((team) => {
return (
<TeamButton

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

@@ -8,6 +8,18 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.scrollToTop = this.scrollToTop.bind(this);
}
scrollToTop() {
if (this.refs.container) {
this.refs.container.scrollTop = 0;
}
}
render() {
const users = this.props.users;
@@ -31,12 +43,12 @@ export default class UserList extends React.Component {
content = (
<div
key='no-users-found'
className='no-channel-message'
className='more-modal__placeholder-row'
>
<p className='primary-message'>
<p>
<FormattedMessage
id='user_list.notFound'
defaultMessage='No users found :('
defaultMessage='No users found'
/>
</p>
</div>
@@ -44,7 +56,7 @@ export default class UserList extends React.Component {
}
return (
<div>
<div ref='container'>
{content}
</div>
);