-
+
-
-
+
);
}
diff --git a/webapp/components/admin_console/mfa_settings.jsx b/webapp/components/admin_console/mfa_settings.jsx
index 5a7e0076fb..7ae1f2e18a 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/native_app_link_settings.jsx b/webapp/components/admin_console/native_app_link_settings.jsx
index 05d61a2842..d932af6457 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/oauth_settings.jsx b/webapp/components/admin_console/oauth_settings.jsx
index 9a86abfa0b..f5eac13eb0 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx
index 43ec409044..edb9669e15 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx
index 5d82fc69c4..c8c145b8d1 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx
index 6da9e6c4fb..518ec807e1 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/public_link_settings.jsx b/webapp/components/admin_console/public_link_settings.jsx
index 9b93a6adcb..592d607d16 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx
index 73189cd8f7..2fc63afe0b 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx
index 73e9a41311..9b0a8076f1 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
index 1b9e5b37ab..d01fc15f34 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 7b9ed38b8b..6025abe28e 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 (
-
-
-
+
);
}
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 68e20f8520..0000000000
--- 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
;
- }
-
- 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(
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
- );
- }
-}
-
-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 9624dea18a..b238da90f3 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/signup_settings.jsx b/webapp/components/admin_console/signup_settings.jsx
index 0c884f486d..b75b7591a2 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx
index 381206bf02..41d38d1ca4 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 (
-
-
-
+
);
}
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 0000000000..a311aebb71
--- /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 (
+
+ );
+ });
+
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ let users = null;
+ if (!this.state.loading) {
+ users = this.state.users;
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/webapp/components/admin_console/admin_team_members_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx
similarity index 77%
rename from webapp/components/admin_console/admin_team_members_dropdown.jsx
rename to webapp/components/admin_console/system_users/system_users_dropdown.jsx
index 037d8c73f2..6f18754a12 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 ;
}
let currentRoles = (
@@ -209,15 +163,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
/>
);
- if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) {
- currentRoles = (
-
- );
- }
-
if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) {
currentRoles = (
);
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 = (
-
-
-
-
-
- );
- }
-
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 = (
-
-
-
-
-
- );
- }
-
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 0000000000..5d88371643
--- /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(
+
+ );
+ } else {
+ info.push(
+
+ );
+ }
+
+ 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(
+
+ );
+ } else {
+ info.push(
+
+ );
+ }
+ }
+
+ return info;
+ }
+
+ renderCount(count, total, startCount, endCount, isSearch) {
+ if (total) {
+ if (isSearch) {
+ return (
+
+ );
+ } else if (startCount !== 0 || endCount !== total) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ const extraInfo = {};
+ if (this.props.users) {
+ for (const user of this.props.users) {
+ extraInfo[user.id] = this.getInfoForUser(user);
+ }
+ }
+
+ return (
+
+
+
+
+ );
+ }
+}
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
deleted file mode 100644
index 5bdaedf6eb..0000000000
--- 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(
-
- );
- } else {
- info.push(
-
- );
- }
-
- if (mfaEnabled) {
- info.push(', ');
- if (user.mfa_active) {
- info.push(
-
- );
- } else {
- info.push(
-
- );
- }
- }
-
- extraInfo[user.id] = info;
- }
- }
- }
-
- return (
-
- );
- }
-}
diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx
index 2cb5b4e516..6e83c01e31 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 (
-
-
-
+
);
}
diff --git a/webapp/components/admin_console/webrtc_settings.jsx b/webapp/components/admin_console/webrtc_settings.jsx
index 63c17f5982..e0238e7f35 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 (
-
-
-
+
);
}
diff --git a/webapp/components/analytics/line_chart.jsx b/webapp/components/analytics/line_chart.jsx
index aa603d819b..5ae80f9e96 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 5af0559249..bd09b8b0bb 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 (
-
+
0 ? teams[0].id : '');
this.state = {
- team: AdminStore.getTeam(this.props.params.team),
- stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ teams,
+ teamId,
+ team: AdminStore.getTeam(teamId),
+ stats: AnalyticsStore.getAllTeam(teamId)
};
}
@@ -42,7 +45,19 @@ export default class TeamAnalytics extends React.Component {
AnalyticsStore.addChangeListener(this.onChange);
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
- this.getData(this.props.params.team);
+ if (this.state.teamId !== '') {
+ this.getData(this.state.teamId);
+ }
+
+ if (this.state.teams.length === 0) {
+ AsyncClient.getAllTeams();
+ }
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ if (nextState.teamId !== this.state.teamId) {
+ this.getData(nextState.teamId);
+ }
}
getData(id) {
@@ -57,40 +72,60 @@ export default class TeamAnalytics extends React.Component {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
}
- componentWillReceiveProps(nextProps) {
- this.getData(nextProps.params.team);
- this.setState({
- stats: AnalyticsStore.getAllTeam(nextProps.params.team)
- });
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
- return true;
- }
-
- if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) {
- return true;
- }
-
- return false;
- }
-
onChange() {
this.setState({
- stats: AnalyticsStore.getAllTeam(this.props.params.team)
+ stats: AnalyticsStore.getAllTeam(this.state.teamId)
});
}
onAllTeamsChange() {
+ const teams = convertTeamMapToList(AdminStore.getAllTeams());
+
+ if (teams.length > 0) {
+ if (this.state.teamId) {
+ this.setState({
+ team: AdminStore.getTeam(this.state.teamId)
+ });
+ } else {
+ this.setState({
+ teamId: teams[0].id,
+ team: teams[0]
+ });
+ }
+ }
+
this.setState({
- team: AdminStore.getTeam(this.props.params.team)
+ teams
});
}
+ handleTeamChange(e) {
+ const teamId = e.target.value;
+
+ this.setState({
+ teamId,
+ team: AdminStore.getTeam(teamId)
+ });
+
+ BrowserStore.setGlobalItem(LAST_ANALYTICS_TEAM, teamId);
+ }
+
render() {
- if (!this.state.team || !this.state.stats) {
- return null;
+ if (this.state.teams.length === 0 || !this.state.team || !this.state.stats) {
+ return ;
+ }
+
+ if (this.state.teamId === '') {
+ return (
+
+ }
+ />
+ );
}
const stats = this.state.stats;
@@ -129,6 +164,7 @@ export default class TeamAnalytics extends React.Component {
postTotalGraph = (
{
+ return (
+
+ );
+ });
+
return (
-
-
-
+
+
+
+
+
+
+
+
+
+
{banner}
-
);
diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list/searchable_user_list.jsx
similarity index 50%
rename from webapp/components/searchable_user_list.jsx
rename to webapp/components/searchable_user_list/searchable_user_list.jsx
index ab3f9ee9bc..91e0205b05 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);
+ }
+
+ renderCount(users) {
+ if (!users) {
+ return null;
}
+
+ const count = users.length;
+ const total = this.props.total;
+ const isSearch = Boolean(this.props.term);
+
+ let startCount;
+ let endCount;
+ if (isSearch) {
+ startCount = -1;
+ endCount = -1;
+ } else {
+ startCount = this.props.page * this.props.usersPerPage;
+ endCount = startCount + count;
+ }
+
+ if (this.props.renderCount) {
+ return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch);
+ }
+
+ if (this.props.total) {
+ if (isSearch) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ return null;
}
render() {
let nextButton;
let previousButton;
let usersToDisplay;
- let count;
- if (this.props.users == null) {
+ if (this.props.term || !this.props.users) {
usersToDisplay = this.props.users;
- } else if (this.state.search || this.props.users == null) {
- usersToDisplay = this.props.users;
-
- if (this.props.total) {
- count = (
-
- );
- }
- } else {
- const pageStart = this.state.page * this.props.usersPerPage;
+ } else if (!this.props.term) {
+ const pageStart = this.props.page * this.props.usersPerPage;
const pageEnd = pageStart + this.props.usersPerPage;
usersToDisplay = this.props.users.slice(pageStart, pageEnd);
@@ -116,7 +183,7 @@ export default class SearchableUserList extends React.Component {
);
}
- if (this.state.page > 0) {
+ if (this.props.page > 0) {
previousButton = (
+ );
}
return (
-
-
-
+ {filterRow}
- {count}
+ {this.renderCount(usersToDisplay)}
+ );
+ }
+}
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index b6a5b0ba89..940f0b0a69 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 49635455f3..9863b5e322 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 (
-
+
@@ -44,7 +56,7 @@ export default class UserList extends React.Component {
}
return (
-
+
{content}
);
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index bd6c2f9790..fac930aae2 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": "
Sign-in Method: Email",
"admin.user_item.authServiceNotEmail": "
Sign-in Method: {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
one-on-one 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 f983af9f5c..e892ca5835 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}
/>
+
+
-
-
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 4a68ec14cd..59c7635755 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 007d8a5a72..6f4acae0ab 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;
- }
-
- profiles.push(profile);
+ 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]);
}
- return profiles;
+ 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;
+ });
+
+ this.profiles_without_team = newProfileList;
+ this.saveProfiles(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 4afd1cc201..b4b361cb4f 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 0abd69a62e..61c4180474 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 0000000000..2072451111
--- /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 7a16a3be89..9e69fd6d69 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) {