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
Этот коммит содержится в:
коммит произвёл
Corey Hulen
родитель
9a9729f22f
Коммит
689cac535e
@@ -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'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
370
webapp/components/admin_console/system_users/system_users.jsx
Обычный файл
370
webapp/components/admin_console/system_users/system_users.jsx
Обычный файл
@@ -0,0 +1,370 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {
|
||||
loadProfiles,
|
||||
loadProfilesAndTeamMembers,
|
||||
loadProfilesWithoutTeam,
|
||||
searchUsers
|
||||
} from 'actions/user_actions.jsx';
|
||||
|
||||
import AdminStore from 'stores/admin_store.jsx';
|
||||
import AnalyticsStore from 'stores/analytics_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import {getAllTeams, getStandardAnalytics, getTeamStats, getUser} from 'utils/async_client.jsx';
|
||||
import {Constants, StatTypes, UserSearchOptions} from 'utils/constants.jsx';
|
||||
import {convertTeamMapToList} from 'utils/team_utils.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import SystemUsersList from './system_users_list.jsx';
|
||||
|
||||
const ALL_USERS = '';
|
||||
const NO_TEAM = 'no_team';
|
||||
|
||||
const USER_ID_LENGTH = 26;
|
||||
const USERS_PER_PAGE = 50;
|
||||
|
||||
export default class SystemUsers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.updateTeamsFromStore = this.updateTeamsFromStore.bind(this);
|
||||
this.updateTotalUsersFromStore = this.updateTotalUsersFromStore.bind(this);
|
||||
this.updateUsersFromStore = this.updateUsersFromStore.bind(this);
|
||||
|
||||
this.loadDataForTeam = this.loadDataForTeam.bind(this);
|
||||
this.loadComplete = this.loadComplete.bind(this);
|
||||
|
||||
this.handleTeamChange = this.handleTeamChange.bind(this);
|
||||
this.handleTermChange = this.handleTermChange.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
|
||||
this.doSearch = this.doSearch.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.getUserById = this.getUserById.bind(this);
|
||||
|
||||
this.renderFilterRow = this.renderFilterRow.bind(this);
|
||||
|
||||
this.state = {
|
||||
teams: convertTeamMapToList(AdminStore.getAllTeams()),
|
||||
totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS],
|
||||
users: UserStore.getProfileList(),
|
||||
|
||||
teamId: ALL_USERS,
|
||||
term: '',
|
||||
loading: true,
|
||||
searching: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AdminStore.addAllTeamsChangeListener(this.updateTeamsFromStore);
|
||||
|
||||
AnalyticsStore.addChangeListener(this.updateTotalUsersFromStore);
|
||||
TeamStore.addStatsChangeListener(this.updateTotalUsersFromStore);
|
||||
|
||||
UserStore.addChangeListener(this.updateUsersFromStore);
|
||||
UserStore.addInTeamChangeListener(this.updateUsersFromStore);
|
||||
UserStore.addWithoutTeamChangeListener(this.updateUsersFromStore);
|
||||
|
||||
this.loadDataForTeam(this.state.teamId);
|
||||
getAllTeams();
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
const nextTeamId = nextState.teamId;
|
||||
|
||||
if (this.state.teamId !== nextTeamId) {
|
||||
this.updateTotalUsersFromStore(nextTeamId);
|
||||
this.updateUsersFromStore(nextTeamId, nextState.term);
|
||||
|
||||
this.loadDataForTeam(nextTeamId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AdminStore.removeAllTeamsChangeListener(this.updateTeamsFromStore);
|
||||
|
||||
AnalyticsStore.removeChangeListener(this.updateTotalUsersFromStore);
|
||||
TeamStore.removeStatsChangeListener(this.updateTotalUsersFromStore);
|
||||
|
||||
UserStore.removeChangeListener(this.updateUsersFromStore);
|
||||
UserStore.removeInTeamChangeListener(this.updateUsersFromStore);
|
||||
UserStore.removeWithoutTeamChangeListener(this.updateUsersFromStore);
|
||||
}
|
||||
|
||||
updateTeamsFromStore() {
|
||||
this.setState({teams: convertTeamMapToList(AdminStore.getAllTeams())});
|
||||
}
|
||||
|
||||
updateTotalUsersFromStore(teamId = this.state.teamId) {
|
||||
if (teamId === ALL_USERS) {
|
||||
this.setState({
|
||||
totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS]
|
||||
});
|
||||
} else if (teamId === NO_TEAM) {
|
||||
this.setState({
|
||||
totalUsers: 0
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
totalUsers: TeamStore.getStats(teamId).total_member_count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) {
|
||||
if (term) {
|
||||
if (teamId === this.state.teamId) {
|
||||
// Search results aren't in the store, so manually update the users in them
|
||||
const users = [...this.state.users];
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
|
||||
if (UserStore.hasProfile(user.id)) {
|
||||
users[i] = UserStore.getProfile(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
users
|
||||
});
|
||||
} else {
|
||||
this.doSearch(teamId, term, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamId === ALL_USERS) {
|
||||
this.setState({users: UserStore.getProfileList(false, true)});
|
||||
} else if (teamId === NO_TEAM) {
|
||||
this.setState({users: UserStore.getProfileListWithoutTeam()});
|
||||
} else {
|
||||
this.setState({users: UserStore.getProfileListInTeam(this.state.teamId)});
|
||||
}
|
||||
}
|
||||
|
||||
loadDataForTeam(teamId) {
|
||||
if (teamId === ALL_USERS) {
|
||||
loadProfiles(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
|
||||
getStandardAnalytics();
|
||||
} else if (teamId === NO_TEAM) {
|
||||
loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
|
||||
} else {
|
||||
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, this.loadComplete);
|
||||
getTeamStats(teamId);
|
||||
}
|
||||
}
|
||||
|
||||
loadComplete() {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
handleTeamChange(e) {
|
||||
this.setState({teamId: e.target.value});
|
||||
}
|
||||
|
||||
handleTermChange(term) {
|
||||
this.setState({term});
|
||||
}
|
||||
|
||||
nextPage(page) {
|
||||
// Paging isn't supported while searching
|
||||
|
||||
if (this.state.teamId === ALL_USERS) {
|
||||
loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete);
|
||||
} else if (this.state.teamId === NO_TEAM) {
|
||||
loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete);
|
||||
} else {
|
||||
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete);
|
||||
}
|
||||
}
|
||||
|
||||
search(term) {
|
||||
if (term === '') {
|
||||
this.updateUsersFromStore(this.state.teamId, term);
|
||||
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
|
||||
this.searchTimeoutId = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.doSearch(this.state.teamId, term);
|
||||
}
|
||||
|
||||
doSearch(teamId, term, now = false) {
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
users: []
|
||||
});
|
||||
|
||||
const options = {
|
||||
[UserSearchOptions.ALLOW_INACTIVE]: true
|
||||
};
|
||||
if (teamId === NO_TEAM) {
|
||||
options[UserSearchOptions.WITHOUT_TEAM] = true;
|
||||
}
|
||||
|
||||
const searchTimeoutId = setTimeout(
|
||||
() => {
|
||||
searchUsers(
|
||||
term,
|
||||
teamId,
|
||||
options,
|
||||
(users) => {
|
||||
if (searchTimeoutId !== this.searchTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (users.length > 0) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
users
|
||||
});
|
||||
} else if (term.length === USER_ID_LENGTH) {
|
||||
// This term didn't match any users name, but it does look like it might be a user's ID
|
||||
this.getUserById(term, searchTimeoutId);
|
||||
} else {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS
|
||||
);
|
||||
|
||||
this.searchTimeoutId = searchTimeoutId;
|
||||
}
|
||||
|
||||
getUserById(id, searchTimeoutId) {
|
||||
if (UserStore.hasProfile(id)) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
users: [UserStore.getProfile(id)]
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
getUser(
|
||||
id,
|
||||
(user) => {
|
||||
if (searchTimeoutId !== this.searchTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
users: [user]
|
||||
});
|
||||
},
|
||||
() => {
|
||||
if (searchTimeoutId !== this.searchTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
users: []
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
renderFilterRow(doSearch) {
|
||||
const teams = this.state.teams.map((team) => {
|
||||
return (
|
||||
<option
|
||||
key={team.id}
|
||||
value={team.id}
|
||||
>
|
||||
{team.display_name}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='system-users__filter-row'>
|
||||
<div className='system-users__filter'>
|
||||
<input
|
||||
ref='filter'
|
||||
className='form-control filter-textbox'
|
||||
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
|
||||
onInput={doSearch}
|
||||
/>
|
||||
</div>
|
||||
<label>
|
||||
<span className='system-users__team-filter-label'>
|
||||
<FormattedMessage
|
||||
id='filtered_user_list.show'
|
||||
defaultMessage='Filter:'
|
||||
/>
|
||||
</span>
|
||||
<select
|
||||
className='form-control system-users__team-filter'
|
||||
onChange={this.handleTeamChange}
|
||||
value={this.state.teamId}
|
||||
>
|
||||
<option value={ALL_USERS}>{Utils.localizeMessage('admin.system_users.allUsers', 'All Users')}</option>
|
||||
<option value={NO_TEAM}>{Utils.localizeMessage('admin.system_users.noTeams', 'No Teams')}</option>
|
||||
{teams}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let users = null;
|
||||
if (!this.state.loading) {
|
||||
users = this.state.users;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3 className='admin-console-header'>
|
||||
<FormattedMessage
|
||||
id='admin.system_users.title'
|
||||
defaultMessage='{siteName} Users'
|
||||
values={{
|
||||
siteName: global.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
<div className='more-modal__list member-list-holder'>
|
||||
<SystemUsersList
|
||||
renderFilterRow={this.renderFilterRow}
|
||||
search={this.search}
|
||||
nextPage={this.nextPage}
|
||||
users={users}
|
||||
usersPerPage={USERS_PER_PAGE}
|
||||
total={this.state.totalUsers}
|
||||
teams={this.state.teams}
|
||||
teamId={this.state.teamId}
|
||||
term={this.state.term}
|
||||
onTermChange={this.handleTermChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
232
webapp/components/admin_console/system_users/system_users_list.jsx
Обычный файл
232
webapp/components/admin_console/system_users/system_users_list.jsx
Обычный файл
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx';
|
||||
import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx';
|
||||
|
||||
import {getUser} from 'utils/async_client.jsx';
|
||||
import {Constants} from 'utils/constants.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import SystemUsersDropdown from './system_users_dropdown.jsx';
|
||||
|
||||
export default class SystemUsersList extends React.Component {
|
||||
static propTypes = {
|
||||
users: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
usersPerPage: React.PropTypes.number,
|
||||
total: React.PropTypes.number,
|
||||
nextPage: React.PropTypes.func,
|
||||
search: React.PropTypes.func.isRequired,
|
||||
focusOnMount: React.PropTypes.bool,
|
||||
renderFilterRow: React.PropTypes.func,
|
||||
|
||||
teamId: React.PropTypes.string.isRequired,
|
||||
term: React.PropTypes.string.isRequired,
|
||||
onTermChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.previousPage = this.previousPage.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
|
||||
this.doPasswordReset = this.doPasswordReset.bind(this);
|
||||
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
|
||||
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.teamId !== this.props.teamId) {
|
||||
this.setState({page: 0});
|
||||
}
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
this.setState({page: this.state.page + 1});
|
||||
|
||||
this.props.nextPage(this.state.page + 1);
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
this.setState({page: this.state.page - 1});
|
||||
}
|
||||
|
||||
search(term) {
|
||||
this.props.search(term);
|
||||
|
||||
if (term !== '') {
|
||||
this.setState({page: 0});
|
||||
}
|
||||
}
|
||||
|
||||
doPasswordReset(user) {
|
||||
this.setState({
|
||||
showPasswordModal: true,
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
doPasswordResetDismiss() {
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
});
|
||||
}
|
||||
|
||||
doPasswordResetSubmit(user) {
|
||||
getUser(user.id);
|
||||
|
||||
this.setState({
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
});
|
||||
}
|
||||
|
||||
getInfoForUser(user) {
|
||||
const info = [];
|
||||
|
||||
if (user.auth_service) {
|
||||
let service;
|
||||
if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
|
||||
service = user.auth_service.toUpperCase();
|
||||
} else {
|
||||
service = Utils.toTitleCase(user.auth_service);
|
||||
}
|
||||
|
||||
info.push(
|
||||
<FormattedHTMLMessage
|
||||
key='admin.user_item.authServiceNotEmail'
|
||||
id='admin.user_item.authServiceNotEmail'
|
||||
defaultMessage='<strong>Sign-in Method:</strong> {service}'
|
||||
values={{
|
||||
service
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
info.push(
|
||||
<FormattedHTMLMessage
|
||||
key='admin.user_item.authServiceEmail'
|
||||
id='admin.user_item.authServiceEmail'
|
||||
defaultMessage='<strong>Sign-in Method:</strong> Email'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mfaEnabled = global.window.mm_license.IsLicensed === 'true' &&
|
||||
global.window.mm_license.MFA === 'true' &&
|
||||
global.window.mm_config.EnableMultifactorAuthentication === 'true';
|
||||
if (mfaEnabled) {
|
||||
info.push(', ');
|
||||
|
||||
if (user.mfa_active) {
|
||||
info.push(
|
||||
<FormattedHTMLMessage
|
||||
key='admin.user_item.mfaYes'
|
||||
id='admin.user_item.mfaYes'
|
||||
defaultMessage='<strong>MFA</strong>: Yes'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
info.push(
|
||||
<FormattedHTMLMessage
|
||||
key='admin.user_item.mfaNo'
|
||||
id='admin.user_item.mfaNo'
|
||||
defaultMessage='<strong>MFA</strong>: No'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
renderCount(count, total, startCount, endCount, isSearch) {
|
||||
if (total) {
|
||||
if (isSearch) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.countSearch'
|
||||
defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total} total'
|
||||
values={{
|
||||
count,
|
||||
total
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (startCount !== 0 || endCount !== total) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.countPage'
|
||||
defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total'
|
||||
values={{
|
||||
count,
|
||||
startCount: startCount + 1,
|
||||
endCount,
|
||||
total
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='system_users_list.count'
|
||||
defaultMessage='{count, number} {count, plural, one {user} other {users}}'
|
||||
values={{
|
||||
count
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const extraInfo = {};
|
||||
if (this.props.users) {
|
||||
for (const user of this.props.users) {
|
||||
extraInfo[user.id] = this.getInfoForUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchableUserList
|
||||
{...this.props}
|
||||
renderCount={this.renderCount}
|
||||
extraInfo={extraInfo}
|
||||
actions={[SystemUsersDropdown]}
|
||||
actionProps={{
|
||||
doPasswordReset: this.doPasswordReset
|
||||
}}
|
||||
nextPage={this.nextPage}
|
||||
previousPage={this.previousPage}
|
||||
search={this.search}
|
||||
page={this.state.page}
|
||||
term={this.props.term}
|
||||
onTermChange={this.props.onTermChange}
|
||||
/>
|
||||
<ResetPasswordModal
|
||||
user={this.state.user}
|
||||
show={this.state.showPasswordModal}
|
||||
onModalSubmit={this.doPasswordResetSubmit}
|
||||
onModalDismissed={this.doPasswordResetDismiss}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Ссылка в новой задаче
Block a user