summaryrefslogtreecommitdiffstats
path: root/webapp
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
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')
-rw-r--r--webapp/actions/global_actions.jsx3
-rw-r--r--webapp/actions/user_actions.jsx62
-rw-r--r--webapp/client/client.jsx23
-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
-rwxr-xr-xwebapp/i18n/en.json14
-rw-r--r--webapp/routes/route_admin_console.jsx24
-rw-r--r--webapp/sass/components/_modal.scss6
-rw-r--r--webapp/sass/routes/_admin-console.scss25
-rw-r--r--webapp/sass/routes/_statistics.scss26
-rw-r--r--webapp/stores/admin_store.jsx14
-rw-r--r--webapp/stores/user_store.jsx130
-rw-r--r--webapp/utils/async_client.jsx14
-rw-r--r--webapp/utils/constants.jsx50
-rw-r--r--webapp/utils/team_utils.jsx27
-rw-r--r--webapp/utils/utils.jsx11
74 files changed, 1436 insertions, 1289 deletions
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index 1f22cd773..95d4d5676 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -25,6 +25,7 @@ const ActionTypes = Constants.ActionTypes;
import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
+import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import en from 'i18n/en.json';
@@ -594,7 +595,7 @@ export function redirectUserToDefaultTeam() {
}
if (myTeams.length > 0) {
- myTeams = myTeams.sort(Utils.sortTeamsByDisplayName);
+ myTeams = myTeams.sort(sortTeamsByDisplayName);
teamId = myTeams[0].id;
}
}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 3b1fa96a8..b9d4ec376 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -133,6 +133,29 @@ export function loadTeamMembersForProfilesList(profiles, teamId = TeamStore.getC
loadTeamMembersForProfiles(list, teamId, success, error);
}
+export function loadProfilesWithoutTeam(page, perPage, success, error) {
+ Client.getProfilesWithoutTeam(
+ page,
+ perPage,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PROFILES_WITHOUT_TEAM,
+ profiles: data,
+ page
+ });
+
+ loadStatusesForProfilesMap(data);
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getProfilesWithoutTeam');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
function loadTeamMembersForProfiles(userIds, teamId, success, error) {
Client.getTeamMembersByIds(
teamId,
@@ -580,20 +603,16 @@ export function updateUserNotifyProps(data, success, error) {
export function updateUserRoles(userId, newRoles, success, error) {
Client.updateUserRoles(
- userId,
- newRoles,
- () => {
- AsyncClient.getUser(userId);
-
- if (success) {
- success();
- }
- },
- (err) => {
- if (error) {
- error(err);
- }
- }
+ userId,
+ newRoles,
+ () => {
+ AsyncClient.getUser(
+ userId,
+ success,
+ error
+ );
+ },
+ error
);
}
@@ -658,18 +677,17 @@ export function checkMfa(loginId, success, error) {
export function updateActive(userId, active, success, error) {
Client.updateActive(userId, active,
- () => {
- AsyncClient.getUser(userId);
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PROFILE,
+ profile: data
+ });
if (success) {
- success();
+ success(data);
}
},
- (err) => {
- if (error) {
- error(err);
- }
- }
+ error
);
}
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index ecb5b18f6..1f2e5517f 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1126,6 +1126,29 @@ export default class Client {
this.trackEvent('api', 'api_profiles_get_not_in_channel', {team_id: this.getTeamId(), channel_id: channelId});
}
+ getProfilesWithoutTeam(page, perPage, success, error) {
+ // Super hacky, but this option only exists in api v4
+ function wrappedSuccess(data, res) {
+ // Convert the profile list provided by api v4 to a map to match similar v3 calls
+ const profiles = {};
+
+ for (const profile of data) {
+ profiles[profile.id] = profile;
+ }
+
+ success(profiles, res);
+ }
+
+ request.
+ get(`${this.url}/api/v4/users?without_team=1&page=${page}&per_page=${perPage}`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getProfilesWithoutTeam', wrappedSuccess, error));
+
+ this.trackEvent('api', 'api_profiles_get_without_team');
+ }
+
getProfilesByIds(userIds, success, error) {
request.
post(`${this.getUsersRoute()}/ids`).
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>
);
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index bd6c2f979..fac930aae 100755
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -745,7 +745,6 @@
"admin.service.webhooksTitle": "Enable Incoming Webhooks: ",
"admin.service.writeTimeout": "Write Timeout:",
"admin.service.writeTimeoutDescription": "If using HTTP (insecure), this is the maximum time allowed from the end of reading the request headers until the response is written. If using HTTPS, it is the total time from when the connection is accepted until the response is written.",
- "admin.sidebar.addTeamSidebar": "Add team from sidebar menu",
"admin.sidebar.advanced": "Advanced",
"admin.sidebar.audits": "Compliance and Auditing",
"admin.sidebar.authentication": "Authentication",
@@ -787,7 +786,6 @@
"admin.sidebar.push": "Mobile Push",
"admin.sidebar.rateLimiting": "Rate Limiting",
"admin.sidebar.reports": "REPORTING",
- "admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
"admin.sidebar.saml": "SAML",
"admin.sidebar.security": "Security",
"admin.sidebar.sessions": "Sessions",
@@ -797,7 +795,6 @@
"admin.sidebar.statistics": "Team Statistics",
"admin.sidebar.storage": "Storage",
"admin.sidebar.support": "Legal and Support",
- "admin.sidebar.teams": "TEAMS ({count, number})",
"admin.sidebar.users": "Users",
"admin.sidebar.usersAndTeams": "Users and Teams",
"admin.sidebar.view_statistics": "Site Statistics",
@@ -837,6 +834,9 @@
"admin.system_analytics.activeUsers": "Active Users With Posts",
"admin.system_analytics.title": "the System",
"admin.system_analytics.totalPosts": "Total Posts",
+ "admin.system_users.allUsers": "All Users",
+ "admin.system_users.noTeams": "No Teams",
+ "admin.system_users.title": "{siteName} Users",
"admin.team.brandDesc": "Enable custom branding to show an image of your choice, uploaded below, and some help text, written below, on the login page.",
"admin.team.brandDescriptionExample": "All team communication in one place, searchable and accessible anywhere",
"admin.team.brandDescriptionHelp": "Description of service shown in login screens and UI. When not specified, \"All team communication in one place, searchable and accessible anywhere\" is displayed.",
@@ -883,8 +883,6 @@
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total Posts",
"admin.true": "true",
- "admin.userList.title": "Users for {team}",
- "admin.userList.title2": "Users for {team} ({count})",
"admin.user_item.authServiceEmail": "<strong>Sign-in Method:</strong> Email",
"admin.user_item.authServiceNotEmail": "<strong>Sign-in Method:</strong> {service}",
"admin.user_item.confirmDemoteDescription": "If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.",
@@ -904,7 +902,6 @@
"admin.user_item.resetMfa": "Remove MFA",
"admin.user_item.resetPwd": "Reset Password",
"admin.user_item.switchToEmail": "Switch to Email/Password",
- "admin.user_item.sysAdmin": "System Admin",
"admin.user_item.teamAdmin": "Team Admin",
"admin.webrtc.enableDescription": "When true, Mattermost allows making <strong>one-on-one</strong> video calls. WebRTC calls are available on Chrome, Firefox and Mattermost Desktop Apps.",
"admin.webrtc.enableTitle": "Enable Mattermost WebRTC: ",
@@ -964,6 +961,7 @@
"analytics.system.totalWebsockets": "WebSocket Conns",
"analytics.team.activeUsers": "Active Users With Posts",
"analytics.team.newlyCreated": "Newly Created Users",
+ "analytics.team.noTeams": "There are no teams on this server for which to view statistics.",
"analytics.team.privateGroups": "Private Groups",
"analytics.team.publicChannels": "Public Channels",
"analytics.team.recentActive": "Recent Active Users",
@@ -1323,6 +1321,7 @@
"filtered_channels_list.search": "Search channels",
"filtered_user_list.any_team": "All Users",
"filtered_user_list.count": "{count} {count, plural, =0 {0 members} one {member} other {members}}",
+ "filtered_user_list.countPage": "{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}}",
"filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total",
"filtered_user_list.countTotalPage": "{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total",
"filtered_user_list.member": "Member",
@@ -1916,6 +1915,9 @@
"suggestion.mention.special": "Special Mentions",
"suggestion.search.private": "Private Groups",
"suggestion.search.public": "Public Channels",
+ "system_users_list.count": "{count, number} {count, plural, one {user} other {users}}",
+ "system_users_list.countPage": "{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total",
+ "system_users_list.countSearch": "{count, number} {count, plural, one {user} other {users}} of {total} total",
"team_export_tab.download": "download",
"team_export_tab.export": "Export",
"team_export_tab.exportTeam": "Export your team",
diff --git a/webapp/routes/route_admin_console.jsx b/webapp/routes/route_admin_console.jsx
index f983af9f5..e892ca583 100644
--- a/webapp/routes/route_admin_console.jsx
+++ b/webapp/routes/route_admin_console.jsx
@@ -41,7 +41,7 @@ import NativeAppLinkSettings from 'components/admin_console/native_app_link_sett
import ComplianceSettings from 'components/admin_console/compliance_settings.jsx';
import RateSettings from 'components/admin_console/rate_settings.jsx';
import DeveloperSettings from 'components/admin_console/developer_settings.jsx';
-import TeamUsers from 'components/admin_console/team_users.jsx';
+import SystemUsers from 'components/admin_console/system_users/system_users.jsx';
import TeamAnalytics from 'components/analytics/team_analytics.jsx';
import LicenseSettings from 'components/admin_console/license_settings.jsx';
import Audits from 'components/admin_console/audits.jsx';
@@ -217,18 +217,26 @@ export default (
component={MetricsSettings}
/>
</Route>
+ <Route
+ path='users'
+ component={SystemUsers}
+ />
+ <Route
+ path='team_analytics'
+ component={TeamAnalytics}
+ />
<Route path='team'>
<Redirect
from=':team'
- to=':team/users'
+ to='../users'
/>
- <Route
- path=':team/users'
- component={TeamUsers}
+ <Redirect
+ from=':team/users'
+ to='../users'
/>
- <Route
- path=':team/analytics'
- component={TeamAnalytics}
+ <Redirect
+ from=':team/analytics'
+ to='../team_analytics'
/>
<Redirect
from='*'
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index bfc082ad3..7e5d0cd3d 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -602,6 +602,10 @@
}
}
+ .more-modal__placeholder-row {
+ padding: 10px 15px;
+ }
+
.more-modal__actions--round {
height: 32px;
line-height: 32px;
@@ -658,4 +662,4 @@
margin-left: 0;
padding-left: 0;
}
-} \ No newline at end of file
+}
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 56199a545..bf8aba6cc 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -28,7 +28,6 @@
}
h3 {
- border-bottom: 1px solid alpha-color($black, .1);
font-weight: 600;
margin: 1em 0;
padding-bottom: .5em;
@@ -276,6 +275,10 @@
@include opacity(.7);
margin-top: 8px;
}
+
+ .admin-console-header {
+ border-bottom: 1px solid alpha-color($black, .1);
+ }
}
.brand-img {
@@ -478,3 +481,23 @@
white-space: nowrap;
width: 130px;
}
+
+.system-users__filter-row {
+ display: flex;
+ margin-left: 15px;
+ margin-right: 15px;
+
+ .system-users__filter {
+ flex: 1;
+ margin-right: 15px;
+ }
+
+ .system-users__team-filter-label {
+ margin-right: 15px;
+ }
+
+ .system-users__team-filter {
+ display: inline-block;
+ width: 200px
+ }
+}
diff --git a/webapp/sass/routes/_statistics.scss b/webapp/sass/routes/_statistics.scss
index 797bc480b..5a35039e7 100644
--- a/webapp/sass/routes/_statistics.scss
+++ b/webapp/sass/routes/_statistics.scss
@@ -83,4 +83,30 @@
}
}
}
+
+ .team-statistics__header-row {
+ display: flex;
+ margin-bottom: 1.5em;
+ margin-top: 1.5em;
+ }
+
+ .team-statistics__header {
+ display: inline-block;
+ flex: 1;
+ margin-right: 15px;
+ overflow: hidden;
+ padding-bottom: .5em;
+
+ > h3 {
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .team-statistics__team-filter {
+ display: inline-block;
+ width: 200px;
+ }
}
diff --git a/webapp/stores/admin_store.jsx b/webapp/stores/admin_store.jsx
index 4a68ec14c..59c763575 100644
--- a/webapp/stores/admin_store.jsx
+++ b/webapp/stores/admin_store.jsx
@@ -4,8 +4,6 @@
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import EventEmitter from 'events';
-import BrowserStore from 'stores/browser_store.jsx';
-
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
@@ -138,18 +136,6 @@ class AdminStoreClass extends EventEmitter {
getTeam(id) {
return this.teams[id];
}
-
- getSelectedTeams() {
- const result = BrowserStore.getItem('selected_teams');
- if (!result) {
- return {};
- }
- return result;
- }
-
- saveSelectedTeams(teams) {
- BrowserStore.setItem('selected_teams', teams);
- }
}
var AdminStore = new AdminStoreClass();
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 007d8a5a7..6f4acae0a 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -16,6 +16,7 @@ const UserStatuses = Constants.UserStatuses;
const CHANGE_EVENT_NOT_IN_CHANNEL = 'change_not_in_channel';
const CHANGE_EVENT_IN_CHANNEL = 'change_in_channel';
const CHANGE_EVENT_IN_TEAM = 'change_in_team';
+const CHANGE_EVENT_WITHOUT_TEAM = 'change_without_team';
const CHANGE_EVENT = 'change';
const CHANGE_EVENT_SESSIONS = 'change_sessions';
const CHANGE_EVENT_AUDITS = 'change_audits';
@@ -50,6 +51,9 @@ class UserStoreClass extends EventEmitter {
this.not_in_channel_offset = {};
this.not_in_channel_count = {};
+ // Lists of sorted IDs for users without a team
+ this.profiles_without_team = {};
+
this.statuses = {};
this.sessions = {};
this.audits = [];
@@ -105,6 +109,18 @@ class UserStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT_NOT_IN_CHANNEL, callback);
}
+ emitWithoutTeamChange() {
+ this.emit(CHANGE_EVENT_WITHOUT_TEAM);
+ }
+
+ addWithoutTeamChangeListener(callback) {
+ this.on(CHANGE_EVENT_WITHOUT_TEAM, callback);
+ }
+
+ removeWithoutTeamChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT_WITHOUT_TEAM, callback);
+ }
+
emitSessionsChange() {
this.emit(CHANGE_EVENT_SESSIONS);
}
@@ -189,8 +205,33 @@ class UserStoreClass extends EventEmitter {
return null;
}
+ getProfileListForIds(userIds, skipCurrent = false, skipInactive = false) {
+ const profiles = [];
+ const currentId = this.getCurrentId();
+
+ for (let i = 0; i < userIds.length; i++) {
+ const profile = this.getProfile(userIds[i]);
+
+ if (!profile) {
+ continue;
+ }
+
+ if (skipCurrent && profile.id === currentId) {
+ continue;
+ }
+
+ if (skipInactive && profile.delete_at > 0) {
+ continue;
+ }
+
+ profiles.push(profile);
+ }
+
+ return profiles;
+ }
+
hasProfile(userId) {
- return this.getProfile(userId) != null;
+ return this.getProfiles().hasOwnProperty(userId);
}
getProfileByUsername(username) {
@@ -248,7 +289,7 @@ class UserStoreClass extends EventEmitter {
return profiles;
}
- getProfileList(skipCurrent) {
+ getProfileList(skipCurrent = false, allowInactive = false) {
const profiles = [];
const currentId = this.getCurrentId();
@@ -260,7 +301,7 @@ class UserStoreClass extends EventEmitter {
continue;
}
- if (profile.delete_at === 0) {
+ if (allowInactive || profile.delete_at === 0) {
profiles.push(profile);
}
}
@@ -314,28 +355,8 @@ class UserStoreClass extends EventEmitter {
getProfileListInTeam(teamId = TeamStore.getCurrentId(), skipCurrent = false, skipInactive = false) {
const userIds = this.profiles_in_team[teamId] || [];
- const profiles = [];
- const currentId = this.getCurrentId();
-
- for (let i = 0; i < userIds.length; i++) {
- const profile = this.getProfile(userIds[i]);
-
- if (!profile) {
- continue;
- }
-
- if (skipCurrent && profile.id === currentId) {
- continue;
- }
-
- if (skipInactive && profile.delete_at > 0) {
- continue;
- }
-
- profiles.push(profile);
- }
- return profiles;
+ return this.getProfileListForIds(userIds, skipCurrent, skipInactive);
}
removeProfileFromTeam(teamId, userId) {
@@ -416,21 +437,8 @@ class UserStoreClass extends EventEmitter {
getProfileListInChannel(channelId = ChannelStore.getCurrentId(), skipCurrent = false) {
const userIds = this.profiles_in_channel[channelId] || [];
- const currentId = this.getCurrentId();
- const profiles = [];
-
- for (let i = 0; i < userIds.length; i++) {
- const profile = this.getProfile(userIds[i]);
- if (profile) {
- if (skipCurrent && profile.id === currentId) {
- continue;
- }
-
- profiles.push(profile);
- }
- }
- return profiles;
+ return this.getProfileListForIds(userIds, skipCurrent, false);
}
saveProfilesNotInChannel(channelId = ChannelStore.getCurrentId(), profiles) {
@@ -482,23 +490,43 @@ class UserStoreClass extends EventEmitter {
getProfileListNotInChannel(channelId = ChannelStore.getCurrentId(), skipInactive = false) {
const userIds = this.profiles_not_in_channel[channelId] || [];
- const profiles = [];
- for (let i = 0; i < userIds.length; i++) {
- const profile = this.getProfile(userIds[i]);
+ return this.getProfileListForIds(userIds, false, skipInactive);
+ }
- if (!profile) {
- continue;
- }
+ // Profiles without any teams
- if (skipInactive && profile.delete_at > 0) {
- continue;
+ saveProfilesWithoutTeam(profiles) {
+ const oldProfileList = this.profiles_without_team;
+ const oldProfileMap = {};
+ for (let i = 0; i < oldProfileList.length; i++) {
+ oldProfileMap[oldProfileList[i]] = this.getProfile(oldProfileList[i]);
+ }
+
+ const newProfileMap = Object.assign({}, oldProfileMap, profiles);
+ const newProfileList = Object.keys(newProfileMap);
+
+ newProfileList.sort((a, b) => {
+ const aProfile = newProfileMap[a];
+ const bProfile = newProfileMap[b];
+
+ if (aProfile.username < bProfile.username) {
+ return -1;
}
+ if (aProfile.username > bProfile.username) {
+ return 1;
+ }
+ return 0;
+ });
- profiles.push(profile);
- }
+ this.profiles_without_team = newProfileList;
+ this.saveProfiles(profiles);
+ }
- return profiles;
+ getProfileListWithoutTeam(skipCurrent = false, skipInactive = false) {
+ const userIds = this.profiles_without_team || [];
+
+ return this.getProfileListForIds(userIds, skipCurrent, skipInactive);
}
// Other
@@ -680,6 +708,10 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
}
UserStore.emitNotInChannelChange();
break;
+ case ActionTypes.RECEIVED_PROFILES_WITHOUT_TEAM:
+ UserStore.saveProfilesWithoutTeam(action.profiles);
+ UserStore.emitWithoutTeamChange();
+ break;
case ActionTypes.RECEIVED_PROFILE:
UserStore.saveProfile(action.profile);
UserStore.emitChange();
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 4afd1cc20..b4b361cb4 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -314,7 +314,7 @@ export function getChannelMember(channelId, userId) {
});
}
-export function getUser(userId) {
+export function getUser(userId, success, error) {
const callName = `getUser${userId}`;
if (isCallInProgress(callName)) {
@@ -331,10 +331,18 @@ export function getUser(userId) {
type: ActionTypes.RECEIVED_PROFILE,
profile: data
});
+
+ if (success) {
+ success(data);
+ }
},
(err) => {
- callTracker[callName] = 0;
- dispatchError(err, 'getUser');
+ if (error) {
+ error(err);
+ } else {
+ callTracker[callName] = 0;
+ dispatchError(err, 'getUser');
+ }
}
);
}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 0abd69a62..61c418047 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -98,6 +98,7 @@ export const ActionTypes = keyMirror({
RECEIVED_PROFILE: null,
RECEIVED_PROFILES_IN_CHANNEL: null,
RECEIVED_PROFILES_NOT_IN_CHANNEL: null,
+ RECEIVED_PROFILES_WITHOUT_TEAM: null,
RECEIVED_ME: null,
RECEIVED_SESSIONS: null,
RECEIVED_AUDITS: null,
@@ -205,7 +206,8 @@ export const UserStatuses = {
};
export const UserSearchOptions = {
- ALLOW_INACTIVE: 'allow_inactive'
+ ALLOW_INACTIVE: 'allow_inactive',
+ WITHOUT_TEAM: 'without_team'
};
export const SocketEvents = {
@@ -253,6 +255,29 @@ export const PostTypes = {
EPHEMERAL: 'system_ephemeral'
};
+export const StatTypes = keyMirror({
+ TOTAL_USERS: null,
+ TOTAL_PUBLIC_CHANNELS: null,
+ TOTAL_PRIVATE_GROUPS: null,
+ TOTAL_POSTS: null,
+ TOTAL_TEAMS: null,
+ TOTAL_FILE_POSTS: null,
+ TOTAL_HASHTAG_POSTS: null,
+ TOTAL_IHOOKS: null,
+ TOTAL_OHOOKS: null,
+ TOTAL_COMMANDS: null,
+ TOTAL_SESSIONS: null,
+ POST_PER_DAY: null,
+ USERS_WITH_POSTS_PER_DAY: null,
+ RECENTLY_ACTIVE_USERS: null,
+ NEWLY_CREATED_USERS: null,
+ TOTAL_WEBSOCKET_CONNECTIONS: null,
+ TOTAL_MASTER_DB_CONNECTIONS: null,
+ TOTAL_READ_DB_CONNECTIONS: null,
+ DAILY_ACTIVE_USERS: null,
+ MONTHLY_ACTIVE_USERS: null
+});
+
export const Constants = {
Preferences,
SocketEvents,
@@ -269,28 +294,7 @@ export const Constants = {
VIEW_ACTION: null
}),
- StatTypes: keyMirror({
- TOTAL_USERS: null,
- TOTAL_PUBLIC_CHANNELS: null,
- TOTAL_PRIVATE_GROUPS: null,
- TOTAL_POSTS: null,
- TOTAL_TEAMS: null,
- TOTAL_FILE_POSTS: null,
- TOTAL_HASHTAG_POSTS: null,
- TOTAL_IHOOKS: null,
- TOTAL_OHOOKS: null,
- TOTAL_COMMANDS: null,
- TOTAL_SESSIONS: null,
- POST_PER_DAY: null,
- USERS_WITH_POSTS_PER_DAY: null,
- RECENTLY_ACTIVE_USERS: null,
- NEWLY_CREATED_USERS: null,
- TOTAL_WEBSOCKET_CONNECTIONS: null,
- TOTAL_MASTER_DB_CONNECTIONS: null,
- TOTAL_READ_DB_CONNECTIONS: null,
- DAILY_ACTIVE_USERS: null,
- MONTHLY_ACTIVE_USERS: null
- }),
+ StatTypes,
STAT_MAX_ACTIVE_USERS: 20,
STAT_MAX_NEW_USERS: 20,
diff --git a/webapp/utils/team_utils.jsx b/webapp/utils/team_utils.jsx
new file mode 100644
index 000000000..207245111
--- /dev/null
+++ b/webapp/utils/team_utils.jsx
@@ -0,0 +1,27 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LocalizationStore from 'stores/localization_store.jsx';
+
+export function convertTeamMapToList(teamMap) {
+ const teams = [];
+
+ for (const id in teamMap) {
+ if (teamMap.hasOwnProperty(id)) {
+ teams.push(teamMap[id]);
+ }
+ }
+
+ return teams.sort(sortTeamsByDisplayName);
+}
+
+// Use when sorting multiple teams by their `display_name` field
+export function sortTeamsByDisplayName(a, b) {
+ const locale = LocalizationStore.getLocale();
+
+ if (a.display_name !== b.display_name) {
+ return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
+ }
+
+ return a.name.localeCompare(b.name, locale, {numeric: true});
+}
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 7a16a3be8..9e69fd6d6 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -1117,17 +1117,6 @@ export function windowHeight() {
return $(window).height();
}
-// Use when sorting multiple teams by their `display_name` field
-export function sortTeamsByDisplayName(a, b) {
- const locale = LocalizationStore.getLocale();
-
- if (a.display_name !== b.display_name) {
- return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
- }
-
- return a.name.localeCompare(b.name, locale, {numeric: true});
-}
-
export function getChannelTerm(channelType) {
let channelTerm = 'Channel';
if (channelType === Constants.PRIVATE_CHANNEL) {