summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2017-03-30 12:46:47 -0400
committerCorey Hulen <corey@hulen.com>2017-03-30 09:46:47 -0700
commit689cac535e45c47a4f603b236dc129dd456efcc9 (patch)
tree767ef80b310d6d073840bd5216da38c439f6e193 /webapp/components
parent9a9729f22fea7275637eafb4046900c9f372ec56 (diff)
downloadchat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.gz
chat-689cac535e45c47a4f603b236dc129dd456efcc9.tar.bz2
chat-689cac535e45c47a4f603b236dc129dd456efcc9.zip
PLT-2713/PLT-6028 Added System Users list to System Console (#5882)
* PLT-2713 Added ability for admins to list users not in any team * Updated style of unit test * Split SearchableUserList to give better control over its properties * Added users without any teams to the user store * Added ManageUsers page * Renamed ManageUsers to SystemUsers * Added ability to search by user id in SystemUsers page * Added SystemUsersDropdown * Removed unnecessary injectIntl * Created TeamUtils * Reduced scope of system console heading CSS * Added team filter to TeamAnalytics page * Updated admin console sidebar * Removed unnecessary TODO * Removed unused reference to deleted modal * Fixed system console sidebar not scrolling on first load * Fixed TeamAnalytics page not rendering on first load * Fixed chart.js throwing an error when switching between teams * Changed TeamAnalytics header to show the team's display name * Fixed appearance of TeamAnalytics and SystemUsers on small screen widths * Fixed placement of 'No users found' message * Fixed teams not appearing in SystemUsers on first load * Updated user count text for SystemUsers * Changed search by id fallback to trigger less often * Fixed SystemUsers list items not updating when searching * Fixed localization strings for SystemUsers page
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx2
-rw-r--r--webapp/components/admin_console/admin_settings.jsx4
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx175
-rw-r--r--webapp/components/admin_console/admin_sidebar_team.jsx87
-rw-r--r--webapp/components/admin_console/audits.jsx2
-rw-r--r--webapp/components/admin_console/cluster_settings.jsx10
-rw-r--r--webapp/components/admin_console/compliance_settings.jsx10
-rw-r--r--webapp/components/admin_console/configuration_settings.jsx10
-rw-r--r--webapp/components/admin_console/connection_settings.jsx10
-rw-r--r--webapp/components/admin_console/custom_brand_settings.jsx12
-rw-r--r--webapp/components/admin_console/custom_emoji_settings.jsx10
-rw-r--r--webapp/components/admin_console/custom_integrations_settings.jsx10
-rw-r--r--webapp/components/admin_console/database_settings.jsx10
-rw-r--r--webapp/components/admin_console/developer_settings.jsx10
-rw-r--r--webapp/components/admin_console/email_authentication_settings.jsx10
-rw-r--r--webapp/components/admin_console/email_settings.jsx10
-rw-r--r--webapp/components/admin_console/external_service_settings.jsx10
-rw-r--r--webapp/components/admin_console/gitlab_settings.jsx10
-rw-r--r--webapp/components/admin_console/image_settings.jsx10
-rw-r--r--webapp/components/admin_console/ldap_settings.jsx10
-rw-r--r--webapp/components/admin_console/legal_and_support_settings.jsx10
-rw-r--r--webapp/components/admin_console/license_settings.jsx2
-rw-r--r--webapp/components/admin_console/link_previews_settings.jsx10
-rw-r--r--webapp/components/admin_console/localization_settings.jsx10
-rw-r--r--webapp/components/admin_console/log_settings.jsx10
-rw-r--r--webapp/components/admin_console/logs.jsx2
-rw-r--r--webapp/components/admin_console/metrics_settings.jsx10
-rw-r--r--webapp/components/admin_console/mfa_settings.jsx10
-rw-r--r--webapp/components/admin_console/native_app_link_settings.jsx10
-rw-r--r--webapp/components/admin_console/oauth_settings.jsx10
-rw-r--r--webapp/components/admin_console/password_settings.jsx10
-rw-r--r--webapp/components/admin_console/policy_settings.jsx10
-rw-r--r--webapp/components/admin_console/privacy_settings.jsx10
-rw-r--r--webapp/components/admin_console/public_link_settings.jsx10
-rw-r--r--webapp/components/admin_console/push_settings.jsx10
-rw-r--r--webapp/components/admin_console/rate_settings.jsx10
-rw-r--r--webapp/components/admin_console/reset_password_modal.jsx30
-rw-r--r--webapp/components/admin_console/saml_settings.jsx10
-rw-r--r--webapp/components/admin_console/select_team_modal.jsx120
-rw-r--r--webapp/components/admin_console/session_settings.jsx10
-rw-r--r--webapp/components/admin_console/signup_settings.jsx10
-rw-r--r--webapp/components/admin_console/storage_settings.jsx10
-rw-r--r--webapp/components/admin_console/system_users/system_users.jsx370
-rw-r--r--webapp/components/admin_console/system_users/system_users_dropdown.jsx (renamed from webapp/components/admin_console/admin_team_members_dropdown.jsx)121
-rw-r--r--webapp/components/admin_console/system_users/system_users_list.jsx232
-rw-r--r--webapp/components/admin_console/team_users.jsx298
-rw-r--r--webapp/components/admin_console/users_and_teams_settings.jsx10
-rw-r--r--webapp/components/admin_console/webrtc_settings.jsx10
-rw-r--r--webapp/components/analytics/line_chart.jsx29
-rw-r--r--webapp/components/analytics/system_analytics.jsx2
-rw-r--r--webapp/components/analytics/team_analytics.jsx151
-rw-r--r--webapp/components/channel_invite_modal.jsx2
-rw-r--r--webapp/components/member_list_channel.jsx2
-rw-r--r--webapp/components/member_list_team.jsx2
-rw-r--r--webapp/components/needs_team.jsx2
-rw-r--r--webapp/components/searchable_user_list/searchable_user_list.jsx (renamed from webapp/components/searchable_user_list.jsx)211
-rw-r--r--webapp/components/searchable_user_list/searchable_user_list_container.jsx72
-rw-r--r--webapp/components/sidebar.jsx3
-rw-r--r--webapp/components/team_sidebar/team_sidebar_controller.jsx3
-rw-r--r--webapp/components/user_list.jsx20
60 files changed, 1146 insertions, 1150 deletions
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
index a1ec2885b..b4fd889bc 100644
--- a/webapp/components/admin_console/admin_navbar_dropdown.jsx
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -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';
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
index b9883d7d8..30b9cbd11 100644
--- a/webapp/components/admin_console/admin_settings.jsx
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -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'
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index 2f299bdeb..73ec436f4 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -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,84 +21,23 @@ 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();
- }
- componentDidUpdate() {
if (!Utils.isMobile()) {
$('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
- 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');
+ componentDidUpdate() {
+ if (!Utils.isMobile()) {
+ $('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
- 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;
@@ -422,6 +282,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={
<FormattedMessage
@@ -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>
);
}
diff --git a/webapp/components/admin_console/admin_sidebar_team.jsx b/webapp/components/admin_console/admin_sidebar_team.jsx
deleted file mode 100644
index b1df92491..000000000
--- a/webapp/components/admin_console/admin_sidebar_team.jsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/webapp/components/admin_console/audits.jsx b/webapp/components/admin_console/audits.jsx
index 5e0e03607..47a7e8d89 100644
--- a/webapp/components/admin_console/audits.jsx
+++ b/webapp/components/admin_console/audits.jsx
@@ -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'
diff --git a/webapp/components/admin_console/cluster_settings.jsx b/webapp/components/admin_console/cluster_settings.jsx
index bbd135e50..31634d0bd 100644
--- a/webapp/components/admin_console/cluster_settings.jsx
+++ b/webapp/components/admin_console/cluster_settings.jsx
@@ -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)'
+ />
);
}
diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx
index f9dc61c1d..e2df967d5 100644
--- a/webapp/components/admin_console/compliance_settings.jsx
+++ b/webapp/components/admin_console/compliance_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/configuration_settings.jsx b/webapp/components/admin_console/configuration_settings.jsx
index a5e5abe87..ec5606fa1 100644
--- a/webapp/components/admin_console/configuration_settings.jsx
+++ b/webapp/components/admin_console/configuration_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/connection_settings.jsx b/webapp/components/admin_console/connection_settings.jsx
index 8e030b207..b35f3acf7 100644
--- a/webapp/components/admin_console/connection_settings.jsx
+++ b/webapp/components/admin_console/connection_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx
index ee8e464da..48954ef78 100644
--- a/webapp/components/admin_console/custom_brand_settings.jsx
+++ b/webapp/components/admin_console/custom_brand_settings.jsx
@@ -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>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/custom_emoji_settings.jsx b/webapp/components/admin_console/custom_emoji_settings.jsx
index 90b70241d..c1457d7e9 100644
--- a/webapp/components/admin_console/custom_emoji_settings.jsx
+++ b/webapp/components/admin_console/custom_emoji_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/custom_integrations_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx
index 6a4202d00..63015a061 100644
--- a/webapp/components/admin_console/custom_integrations_settings.jsx
+++ b/webapp/components/admin_console/custom_integrations_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/database_settings.jsx b/webapp/components/admin_console/database_settings.jsx
index 2cd4929ec..84adae29c 100644
--- a/webapp/components/admin_console/database_settings.jsx
+++ b/webapp/components/admin_console/database_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/developer_settings.jsx b/webapp/components/admin_console/developer_settings.jsx
index 119b92a5a..3bcc2a19b 100644
--- a/webapp/components/admin_console/developer_settings.jsx
+++ b/webapp/components/admin_console/developer_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/email_authentication_settings.jsx b/webapp/components/admin_console/email_authentication_settings.jsx
index cb7ef3419..177f36d64 100644
--- a/webapp/components/admin_console/email_authentication_settings.jsx
+++ b/webapp/components/admin_console/email_authentication_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx
index 9dc02857b..6cf09f653 100644
--- a/webapp/components/admin_console/email_settings.jsx
+++ b/webapp/components/admin_console/email_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx
index 53fdbfb53..21fc6c106 100644
--- a/webapp/components/admin_console/external_service_settings.jsx
+++ b/webapp/components/admin_console/external_service_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx
index ec3849b26..6ba2245b8 100644
--- a/webapp/components/admin_console/gitlab_settings.jsx
+++ b/webapp/components/admin_console/gitlab_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx
index 8e8e2868e..0249e3979 100644
--- a/webapp/components/admin_console/image_settings.jsx
+++ b/webapp/components/admin_console/image_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx
index b774d34f3..50883ac22 100644
--- a/webapp/components/admin_console/ldap_settings.jsx
+++ b/webapp/components/admin_console/ldap_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx
index 3108dd60b..b0f85f43d 100644
--- a/webapp/components/admin_console/legal_and_support_settings.jsx
+++ b/webapp/components/admin_console/legal_and_support_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index 6c14394b7..7e77f44b6 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -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'
diff --git a/webapp/components/admin_console/link_previews_settings.jsx b/webapp/components/admin_console/link_previews_settings.jsx
index aea8a56f1..f223ccc3e 100644
--- a/webapp/components/admin_console/link_previews_settings.jsx
+++ b/webapp/components/admin_console/link_previews_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/localization_settings.jsx b/webapp/components/admin_console/localization_settings.jsx
index 7868ca8eb..b3e8a7b65 100644
--- a/webapp/components/admin_console/localization_settings.jsx
+++ b/webapp/components/admin_console/localization_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx
index 135369942..69dd4eda7 100644
--- a/webapp/components/admin_console/log_settings.jsx
+++ b/webapp/components/admin_console/log_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index 5846c91db..d2464b37f 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -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'
diff --git a/webapp/components/admin_console/metrics_settings.jsx b/webapp/components/admin_console/metrics_settings.jsx
index 29fa028ec..607a21fb9 100644
--- a/webapp/components/admin_console/metrics_settings.jsx
+++ b/webapp/components/admin_console/metrics_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/mfa_settings.jsx b/webapp/components/admin_console/mfa_settings.jsx
index 5a7e0076f..7ae1f2e18 100644
--- a/webapp/components/admin_console/mfa_settings.jsx
+++ b/webapp/components/admin_console/mfa_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/native_app_link_settings.jsx b/webapp/components/admin_console/native_app_link_settings.jsx
index 05d61a284..d932af645 100644
--- a/webapp/components/admin_console/native_app_link_settings.jsx
+++ b/webapp/components/admin_console/native_app_link_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/oauth_settings.jsx b/webapp/components/admin_console/oauth_settings.jsx
index 9a86abfa0..f5eac13eb 100644
--- a/webapp/components/admin_console/oauth_settings.jsx
+++ b/webapp/components/admin_console/oauth_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx
index 43ec40904..edb9669e1 100644
--- a/webapp/components/admin_console/password_settings.jsx
+++ b/webapp/components/admin_console/password_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx
index 5d82fc69c..c8c145b8d 100644
--- a/webapp/components/admin_console/policy_settings.jsx
+++ b/webapp/components/admin_console/policy_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx
index 6da9e6c4f..518ec807e 100644
--- a/webapp/components/admin_console/privacy_settings.jsx
+++ b/webapp/components/admin_console/privacy_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/public_link_settings.jsx b/webapp/components/admin_console/public_link_settings.jsx
index 9b93a6adc..592d607d1 100644
--- a/webapp/components/admin_console/public_link_settings.jsx
+++ b/webapp/components/admin_console/public_link_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx
index 73189cd8f..2fc63afe0 100644
--- a/webapp/components/admin_console/push_settings.jsx
+++ b/webapp/components/admin_console/push_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx
index 73e9a4131..9b0a8076f 100644
--- a/webapp/components/admin_console/rate_settings.jsx
+++ b/webapp/components/admin_console/rate_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
index 1b9e5b37a..d01fc15f3 100644
--- a/webapp/components/admin_console/reset_password_modal.jsx
+++ b/webapp/components/admin_console/reset_password_modal.jsx
@@ -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);
diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx
index 7b9ed38b8..6025abe28 100644
--- a/webapp/components/admin_console/saml_settings.jsx
+++ b/webapp/components/admin_console/saml_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/select_team_modal.jsx b/webapp/components/admin_console/select_team_modal.jsx
deleted file mode 100644
index 68e20f852..000000000
--- a/webapp/components/admin_console/select_team_modal.jsx
+++ /dev/null
@@ -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
-};
diff --git a/webapp/components/admin_console/session_settings.jsx b/webapp/components/admin_console/session_settings.jsx
index 9624dea18..b238da90f 100644
--- a/webapp/components/admin_console/session_settings.jsx
+++ b/webapp/components/admin_console/session_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/signup_settings.jsx b/webapp/components/admin_console/signup_settings.jsx
index 0c884f486..b75b7591a 100644
--- a/webapp/components/admin_console/signup_settings.jsx
+++ b/webapp/components/admin_console/signup_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
index 381206bf0..41d38d1ca 100644
--- a/webapp/components/admin_console/storage_settings.jsx
+++ b/webapp/components/admin_console/storage_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/system_users/system_users.jsx b/webapp/components/admin_console/system_users/system_users.jsx
new file mode 100644
index 000000000..a311aebb7
--- /dev/null
+++ b/webapp/components/admin_console/system_users/system_users.jsx
@@ -0,0 +1,370 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import {
+ loadProfiles,
+ loadProfilesAndTeamMembers,
+ loadProfilesWithoutTeam,
+ searchUsers
+} from 'actions/user_actions.jsx';
+
+import AdminStore from 'stores/admin_store.jsx';
+import AnalyticsStore from 'stores/analytics_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {getAllTeams, getStandardAnalytics, getTeamStats, getUser} from 'utils/async_client.jsx';
+import {Constants, StatTypes, UserSearchOptions} from 'utils/constants.jsx';
+import {convertTeamMapToList} from 'utils/team_utils.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import SystemUsersList from './system_users_list.jsx';
+
+const ALL_USERS = '';
+const NO_TEAM = 'no_team';
+
+const USER_ID_LENGTH = 26;
+const USERS_PER_PAGE = 50;
+
+export default class SystemUsers extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateTeamsFromStore = this.updateTeamsFromStore.bind(this);
+ this.updateTotalUsersFromStore = this.updateTotalUsersFromStore.bind(this);
+ this.updateUsersFromStore = this.updateUsersFromStore.bind(this);
+
+ this.loadDataForTeam = this.loadDataForTeam.bind(this);
+ this.loadComplete = this.loadComplete.bind(this);
+
+ this.handleTeamChange = this.handleTeamChange.bind(this);
+ this.handleTermChange = this.handleTermChange.bind(this);
+ this.nextPage = this.nextPage.bind(this);
+
+ this.doSearch = this.doSearch.bind(this);
+ this.search = this.search.bind(this);
+ this.getUserById = this.getUserById.bind(this);
+
+ this.renderFilterRow = this.renderFilterRow.bind(this);
+
+ this.state = {
+ teams: convertTeamMapToList(AdminStore.getAllTeams()),
+ totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS],
+ users: UserStore.getProfileList(),
+
+ teamId: ALL_USERS,
+ term: '',
+ loading: true,
+ searching: false
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addAllTeamsChangeListener(this.updateTeamsFromStore);
+
+ AnalyticsStore.addChangeListener(this.updateTotalUsersFromStore);
+ TeamStore.addStatsChangeListener(this.updateTotalUsersFromStore);
+
+ UserStore.addChangeListener(this.updateUsersFromStore);
+ UserStore.addInTeamChangeListener(this.updateUsersFromStore);
+ UserStore.addWithoutTeamChangeListener(this.updateUsersFromStore);
+
+ this.loadDataForTeam(this.state.teamId);
+ getAllTeams();
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ const nextTeamId = nextState.teamId;
+
+ if (this.state.teamId !== nextTeamId) {
+ this.updateTotalUsersFromStore(nextTeamId);
+ this.updateUsersFromStore(nextTeamId, nextState.term);
+
+ this.loadDataForTeam(nextTeamId);
+ }
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeAllTeamsChangeListener(this.updateTeamsFromStore);
+
+ AnalyticsStore.removeChangeListener(this.updateTotalUsersFromStore);
+ TeamStore.removeStatsChangeListener(this.updateTotalUsersFromStore);
+
+ UserStore.removeChangeListener(this.updateUsersFromStore);
+ UserStore.removeInTeamChangeListener(this.updateUsersFromStore);
+ UserStore.removeWithoutTeamChangeListener(this.updateUsersFromStore);
+ }
+
+ updateTeamsFromStore() {
+ this.setState({teams: convertTeamMapToList(AdminStore.getAllTeams())});
+ }
+
+ updateTotalUsersFromStore(teamId = this.state.teamId) {
+ if (teamId === ALL_USERS) {
+ this.setState({
+ totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS]
+ });
+ } else if (teamId === NO_TEAM) {
+ this.setState({
+ totalUsers: 0
+ });
+ } else {
+ this.setState({
+ totalUsers: TeamStore.getStats(teamId).total_member_count
+ });
+ }
+ }
+
+ updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) {
+ if (term) {
+ if (teamId === this.state.teamId) {
+ // Search results aren't in the store, so manually update the users in them
+ const users = [...this.state.users];
+
+ for (let i = 0; i < users.length; i++) {
+ const user = users[i];
+
+ if (UserStore.hasProfile(user.id)) {
+ users[i] = UserStore.getProfile(user.id);
+ }
+ }
+
+ this.setState({
+ users
+ });
+ } else {
+ this.doSearch(teamId, term, true);
+ }
+
+ return;
+ }
+
+ if (teamId === ALL_USERS) {
+ this.setState({users: UserStore.getProfileList(false, true)});
+ } else if (teamId === NO_TEAM) {
+ this.setState({users: UserStore.getProfileListWithoutTeam()});
+ } else {
+ this.setState({users: UserStore.getProfileListInTeam(this.state.teamId)});
+ }
+ }
+
+ loadDataForTeam(teamId) {
+ if (teamId === ALL_USERS) {
+ loadProfiles(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
+ getStandardAnalytics();
+ } else if (teamId === NO_TEAM) {
+ loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
+ } else {
+ loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, this.loadComplete);
+ getTeamStats(teamId);
+ }
+ }
+
+ loadComplete() {
+ this.setState({loading: false});
+ }
+
+ handleTeamChange(e) {
+ this.setState({teamId: e.target.value});
+ }
+
+ handleTermChange(term) {
+ this.setState({term});
+ }
+
+ nextPage(page) {
+ // Paging isn't supported while searching
+
+ if (this.state.teamId === ALL_USERS) {
+ loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete);
+ } else if (this.state.teamId === NO_TEAM) {
+ loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete);
+ } else {
+ loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete);
+ }
+ }
+
+ search(term) {
+ if (term === '') {
+ this.updateUsersFromStore(this.state.teamId, term);
+
+ this.setState({
+ loading: false
+ });
+
+ this.searchTimeoutId = '';
+ return;
+ }
+
+ this.doSearch(this.state.teamId, term);
+ }
+
+ doSearch(teamId, term, now = false) {
+ clearTimeout(this.searchTimeoutId);
+
+ this.setState({
+ loading: true,
+ users: []
+ });
+
+ const options = {
+ [UserSearchOptions.ALLOW_INACTIVE]: true
+ };
+ if (teamId === NO_TEAM) {
+ options[UserSearchOptions.WITHOUT_TEAM] = true;
+ }
+
+ const searchTimeoutId = setTimeout(
+ () => {
+ searchUsers(
+ term,
+ teamId,
+ options,
+ (users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ if (users.length > 0) {
+ this.setState({
+ loading: false,
+ users
+ });
+ } else if (term.length === USER_ID_LENGTH) {
+ // This term didn't match any users name, but it does look like it might be a user's ID
+ this.getUserById(term, searchTimeoutId);
+ } else {
+ this.setState({
+ loading: false
+ });
+ }
+ },
+ () => {
+ this.setState({
+ loading: false
+ });
+ }
+ );
+ },
+ now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS
+ );
+
+ this.searchTimeoutId = searchTimeoutId;
+ }
+
+ getUserById(id, searchTimeoutId) {
+ if (UserStore.hasProfile(id)) {
+ this.setState({
+ loading: false,
+ users: [UserStore.getProfile(id)]
+ });
+
+ return;
+ }
+
+ getUser(
+ id,
+ (user) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: false,
+ users: [user]
+ });
+ },
+ () => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: false,
+ users: []
+ });
+ }
+ );
+ }
+
+ renderFilterRow(doSearch) {
+ const teams = this.state.teams.map((team) => {
+ return (
+ <option
+ key={team.id}
+ value={team.id}
+ >
+ {team.display_name}
+ </option>
+ );
+ });
+
+ return (
+ <div className='system-users__filter-row'>
+ <div className='system-users__filter'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
+ onInput={doSearch}
+ />
+ </div>
+ <label>
+ <span className='system-users__team-filter-label'>
+ <FormattedMessage
+ id='filtered_user_list.show'
+ defaultMessage='Filter:'
+ />
+ </span>
+ <select
+ className='form-control system-users__team-filter'
+ onChange={this.handleTeamChange}
+ value={this.state.teamId}
+ >
+ <option value={ALL_USERS}>{Utils.localizeMessage('admin.system_users.allUsers', 'All Users')}</option>
+ <option value={NO_TEAM}>{Utils.localizeMessage('admin.system_users.noTeams', 'No Teams')}</option>
+ {teams}
+ </select>
+ </label>
+ </div>
+ );
+ }
+
+ render() {
+ let users = null;
+ if (!this.state.loading) {
+ users = this.state.users;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3 className='admin-console-header'>
+ <FormattedMessage
+ id='admin.system_users.title'
+ defaultMessage='{siteName} Users'
+ values={{
+ siteName: global.mm_config.SiteName
+ }}
+ />
+ </h3>
+ <div className='more-modal__list member-list-holder'>
+ <SystemUsersList
+ renderFilterRow={this.renderFilterRow}
+ search={this.search}
+ nextPage={this.nextPage}
+ users={users}
+ usersPerPage={USERS_PER_PAGE}
+ total={this.state.totalUsers}
+ teams={this.state.teams}
+ teamId={this.state.teamId}
+ term={this.state.term}
+ onTermChange={this.handleTermChange}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_team_members_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
index 037d8c73f..6f18754a1 100644
--- a/webapp/components/admin_console/admin_team_members_dropdown.jsx
+++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
@@ -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
-};
diff --git a/webapp/components/admin_console/system_users/system_users_list.jsx b/webapp/components/admin_console/system_users/system_users_list.jsx
new file mode 100644
index 000000000..5d8837164
--- /dev/null
+++ b/webapp/components/admin_console/system_users/system_users_list.jsx
@@ -0,0 +1,232 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx';
+import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx';
+
+import {getUser} from 'utils/async_client.jsx';
+import {Constants} from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import SystemUsersDropdown from './system_users_dropdown.jsx';
+
+export default class SystemUsersList extends React.Component {
+ static propTypes = {
+ users: React.PropTypes.arrayOf(React.PropTypes.object),
+ usersPerPage: React.PropTypes.number,
+ total: React.PropTypes.number,
+ nextPage: React.PropTypes.func,
+ search: React.PropTypes.func.isRequired,
+ focusOnMount: React.PropTypes.bool,
+ renderFilterRow: React.PropTypes.func,
+
+ teamId: React.PropTypes.string.isRequired,
+ term: React.PropTypes.string.isRequired,
+ onTermChange: React.PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.nextPage = this.nextPage.bind(this);
+ this.previousPage = this.previousPage.bind(this);
+ this.search = this.search.bind(this);
+
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ page: 0,
+
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.teamId !== this.props.teamId) {
+ this.setState({page: 0});
+ }
+ }
+
+ nextPage() {
+ this.setState({page: this.state.page + 1});
+
+ this.props.nextPage(this.state.page + 1);
+ }
+
+ previousPage() {
+ this.setState({page: this.state.page - 1});
+ }
+
+ search(term) {
+ this.props.search(term);
+
+ if (term !== '') {
+ this.setState({page: 0});
+ }
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.setState({
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit(user) {
+ getUser(user.id);
+
+ this.setState({
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ getInfoForUser(user) {
+ const info = [];
+
+ if (user.auth_service) {
+ let service;
+ if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
+ service = user.auth_service.toUpperCase();
+ } else {
+ service = Utils.toTitleCase(user.auth_service);
+ }
+
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.authServiceNotEmail'
+ id='admin.user_item.authServiceNotEmail'
+ defaultMessage='<strong>Sign-in Method:</strong> {service}'
+ values={{
+ service
+ }}
+ />
+ );
+ } else {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.authServiceEmail'
+ id='admin.user_item.authServiceEmail'
+ defaultMessage='<strong>Sign-in Method:</strong> Email'
+ />
+ );
+ }
+
+ const mfaEnabled = global.window.mm_license.IsLicensed === 'true' &&
+ global.window.mm_license.MFA === 'true' &&
+ global.window.mm_config.EnableMultifactorAuthentication === 'true';
+ if (mfaEnabled) {
+ info.push(', ');
+
+ if (user.mfa_active) {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.mfaYes'
+ id='admin.user_item.mfaYes'
+ defaultMessage='<strong>MFA</strong>: Yes'
+ />
+ );
+ } else {
+ info.push(
+ <FormattedHTMLMessage
+ key='admin.user_item.mfaNo'
+ id='admin.user_item.mfaNo'
+ defaultMessage='<strong>MFA</strong>: No'
+ />
+ );
+ }
+ }
+
+ return info;
+ }
+
+ renderCount(count, total, startCount, endCount, isSearch) {
+ if (total) {
+ if (isSearch) {
+ return (
+ <FormattedMessage
+ id='system_users_list.countSearch'
+ defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total} total'
+ values={{
+ count,
+ total
+ }}
+ />
+ );
+ } else if (startCount !== 0 || endCount !== total) {
+ return (
+ <FormattedMessage
+ id='system_users_list.countPage'
+ defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total'
+ values={{
+ count,
+ startCount: startCount + 1,
+ endCount,
+ total
+ }}
+ />
+ );
+ }
+
+ return (
+ <FormattedMessage
+ id='system_users_list.count'
+ defaultMessage='{count, number} {count, plural, one {user} other {users}}'
+ values={{
+ count
+ }}
+ />
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ const extraInfo = {};
+ if (this.props.users) {
+ for (const user of this.props.users) {
+ extraInfo[user.id] = this.getInfoForUser(user);
+ }
+ }
+
+ return (
+ <div>
+ <SearchableUserList
+ {...this.props}
+ renderCount={this.renderCount}
+ extraInfo={extraInfo}
+ actions={[SystemUsersDropdown]}
+ actionProps={{
+ doPasswordReset: this.doPasswordReset
+ }}
+ nextPage={this.nextPage}
+ previousPage={this.previousPage}
+ search={this.search}
+ page={this.state.page}
+ term={this.props.term}
+ onTermChange={this.props.onTermChange}
+ />
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
deleted file mode 100644
index 5bdaedf6e..000000000
--- a/webapp/components/admin_console/team_users.jsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx
index 2cb5b4e51..6e83c01e3 100644
--- a/webapp/components/admin_console/users_and_teams_settings.jsx
+++ b/webapp/components/admin_console/users_and_teams_settings.jsx
@@ -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'
+ />
);
}
diff --git a/webapp/components/admin_console/webrtc_settings.jsx b/webapp/components/admin_console/webrtc_settings.jsx
index 63c17f598..e0238e7f3 100644
--- a/webapp/components/admin_console/webrtc_settings.jsx
+++ b/webapp/components/admin_console/webrtc_settings.jsx
@@ -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)'
+ />
);
}
diff --git a/webapp/components/analytics/line_chart.jsx b/webapp/components/analytics/line_chart.jsx
index aa603d819..5ae80f9e9 100644
--- a/webapp/components/analytics/line_chart.jsx
+++ b/webapp/components/analytics/line_chart.jsx
@@ -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();
}
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
index 5af055924..bd09b8b0b 100644
--- a/webapp/components/analytics/system_analytics.jsx
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -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'
diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx
index 66eb7e2db..135bab4b4 100644
--- a/webapp/components/analytics/team_analytics.jsx
+++ b/webapp/components/analytics/team_analytics.jsx
@@ -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);
+ onChange() {
this.setState({
- stats: AnalyticsStore.getAllTeam(nextProps.params.team)
+ stats: AnalyticsStore.getAllTeam(this.state.teamId)
});
}
- 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;
+ 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]
+ });
+ }
}
- return false;
- }
-
- onChange() {
this.setState({
- stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ teams
});
}
- onAllTeamsChange() {
+ handleTeamChange(e) {
+ const teamId = e.target.value;
+
this.setState({
- team: AdminStore.getTeam(this.props.params.team)
+ 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
diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx
index 2f1a10a75..dfc083f24 100644
--- a/webapp/components/channel_invite_modal.jsx
+++ b/webapp/components/channel_invite_modal.jsx
@@ -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';
diff --git a/webapp/components/member_list_channel.jsx b/webapp/components/member_list_channel.jsx
index d9d28bcd0..c23be2836 100644
--- a/webapp/components/member_list_channel.jsx
+++ b/webapp/components/member_list_channel.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';
diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx
index e06d61b0a..fce6e1927 100644
--- a/webapp/components/member_list_team.jsx
+++ b/webapp/components/member_list_team.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';
diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx
index 9ce4ed882..1f1f99aba 100644
--- a/webapp/components/needs_team.jsx
+++ b/webapp/components/needs_team.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>
);
diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list/searchable_user_list.jsx
index ab3f9ee9b..91e0205b0 100644
--- a/webapp/components/searchable_user_list.jsx
+++ b/webapp/components/searchable_user_list/searchable_user_list.jsx
@@ -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);
}
- render() {
- let nextButton;
- let previousButton;
- let usersToDisplay;
- let count;
+ renderCount(users) {
+ if (!users) {
+ return null;
+ }
- if (this.props.users == null) {
- usersToDisplay = this.props.users;
- } else if (this.state.search || this.props.users == null) {
- usersToDisplay = this.props.users;
+ 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.total) {
- 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: usersToDisplay.length || 0,
- total: this.props.total
+ count,
+ total
}}
/>
);
}
- } else {
- const pageStart = this.state.page * this.props.usersPerPage;
+
+ 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;
+
+ if (this.props.term || !this.props.users) {
+ usersToDisplay = this.props.users;
+ } 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
-};
diff --git a/webapp/components/searchable_user_list/searchable_user_list_container.jsx b/webapp/components/searchable_user_list/searchable_user_list_container.jsx
new file mode 100644
index 000000000..816dec062
--- /dev/null
+++ b/webapp/components/searchable_user_list/searchable_user_list_container.jsx
@@ -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}
+ />
+ );
+ }
+}
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index b6a5b0ba8..940f0b0a6 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -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);
diff --git a/webapp/components/team_sidebar/team_sidebar_controller.jsx b/webapp/components/team_sidebar/team_sidebar_controller.jsx
index 49635455f..9863b5e32 100644
--- a/webapp/components/team_sidebar/team_sidebar_controller.jsx
+++ b/webapp/components/team_sidebar/team_sidebar_controller.jsx
@@ -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
diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx
index d34404c89..c521b95cc 100644
--- a/webapp/components/user_list.jsx
+++ b/webapp/components/user_list.jsx
@@ -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>
);