Commit 689cac53 authored by Harrison Healey's avatar Harrison Healey Committed by Corey Hulen
Browse files

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
parent 9a9729f2
......@@ -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;
}
}
......
......@@ -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
);
}
......
......@@ -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`).
......
......@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
import {sortTeamsByDisplayName} from 'utils/utils.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
......
......@@ -112,7 +112,9 @@ export default class AdminSettings extends React.Component {
render() {
return (
<div className='wrapper--fixed'>
{this.renderTitle()}
<h3 className='admin-console-header'>
{this.renderTitle()}
</h3>
<form
className='form-horizontal'
role='form'
......
......@@ -3,18 +3,12 @@
import $ from 'jquery';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import AdminStore from 'stores/admin_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import AdminSidebarTeam from './admin_sidebar_team.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import SelectTeamModal from './select_team_modal.jsx';
import AdminSidebarCategory from './admin_sidebar_category.jsx';
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebar extends React.Component {
......@@ -27,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;
......@@ -421,6 +281,24 @@ export default class AdminSidebar extends React.Component {
/>
}
/>
<AdminSidebarSection
name='team_analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='Team Statistics'
/>
}
/>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='Users'
/>
}
/>
<AdminSidebarSection
name='logs'
title={
......@@ -760,16 +638,9 @@ export default class AdminSidebar extends React.Component {
{metricsSettings}
</AdminSidebarSection>
</AdminSidebarCategory>
{this.renderTeams()}
{otherCategory}
</ul>
</div>
<SelectTeamModal
teams={this.state.teams}
show={this.state.showSelectModal}
onModalSubmit={this.teamSelectedModal}
onModalDismissed={this.teamSelectedModalDismissed}
/>
</div>
);
}
......
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebarTeam extends React.Component {
static get propTypes() {
return {
team: React.PropTypes.object.isRequired,
onRemoveTeam: React.PropTypes.func.isRequired,
parentLink: React.PropTypes.string
};
}
constructor(props) {
super(props);
this.handleRemoveTeam = this.handleRemoveTeam.bind(this);
}
handleRemoveTeam(e) {
e.preventDefault();
this.props.onRemoveTeam(this.props.team);
}
render() {
const team = this.props.team;
const removeTeamTooltip = (
<Tooltip id='remove-team-tooltip'>
<FormattedMessage
id='admin.sidebar.rmTeamSidebar'
defaultMessage='Remove team from sidebar menu'
/>
</Tooltip>
);
const removeTeamButton = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={removeTeamTooltip}
>
<span
className='menu-icon--right menu__close'
onClick={this.handleRemoveTeam}
>
{'×'}
</span>
</OverlayTrigger>
);
return (
<AdminSidebarSection
key={team.id}
name={'team/' + team.id}
parentLink={this.props.parentLink}
title={team.display_name}
action={removeTeamButton}
>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='- Users'
/>
}
/>
<AdminSidebarSection
name='analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='- Team Statistics'
/>
}
/>
</AdminSidebarSection>
);
}
}
......@@ -76,7 +76,7 @@ export default class Audits extends React.Component {
<ComplianceReports/>
<div className='panel audit-panel'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.audits.title'
defaultMessage='User Activity Logs'
......
......@@ -51,12 +51,10 @@ export default class ClusterSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
</h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
);
}
......
......@@ -38,12 +38,10 @@ export default class ComplianceSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
</h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
);
}
......
......@@ -64,12 +64,10 @@ export default class ConfigurationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
</h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
);
}
......
......@@ -36,12 +36,10 @@ export default class ConnectionSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
</h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
);
}
......
......@@ -44,12 +44,10 @@ export default class CustomBrandSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
</h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
);
}
......@@ -155,4 +153,4 @@ export default class CustomBrandSettings extends AdminSettings {
</SettingsGroup>
);
}
}
\ No newline at end of file
}
......@@ -39,12 +39,10 @@ export default class CustomEmojiSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
</h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
);
}
......
......@@ -43,12 +43,10 @@ export default class WebhookSettings extends AdminSettings {