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
Этот коммит содержится в:
Harrison Healey
2017-03-30 12:46:47 -04:00
коммит произвёл Corey Hulen
родитель 9a9729f22f
Коммит 689cac535e
74 изменённых файлов: 1445 добавлений и 1298 удалений

Просмотреть файл

@@ -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,31 +21,15 @@ export default class AdminSidebar extends React.Component {
constructor(props) {
super(props);
this.handleAllTeamsChange = this.handleAllTeamsChange.bind(this);
this.removeTeam = this.removeTeam.bind(this);
this.showTeamSelect = this.showTeamSelect.bind(this);
this.teamSelectedModal = this.teamSelectedModal.bind(this);
this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.updateTitle = this.updateTitle.bind(this);
this.renderAddTeamButton = this.renderAddTeamButton.bind(this);
this.renderTeams = this.renderTeams.bind(this);
this.state = {
teams: AdminStore.getAllTeams(),
selectedTeams: AdminStore.getSelectedTeams(),
showSelectModal: false
};
}
componentDidMount() {
AdminStore.addAllTeamsChangeListener(this.handleAllTeamsChange);
AsyncClient.getAllTeams();
this.updateTitle();
if (!Utils.isMobile()) {
$('.admin-sidebar .nav-pills__container').perfectScrollbar();
}
}
componentDidUpdate() {
@@ -60,51 +38,6 @@ export default class AdminSidebar extends React.Component {
}
}
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.handleAllTeamsChange);
}
handleAllTeamsChange() {
this.setState({
teams: AdminStore.getAllTeams(),
selectedTeams: AdminStore.getSelectedTeams()
});
}
removeTeam(team) {
const selectedTeams = Object.assign({}, this.state.selectedTeams);
Reflect.deleteProperty(selectedTeams, team.id);
AdminStore.saveSelectedTeams(selectedTeams);
this.handleAllTeamsChange();
if (this.context.router.isActive('/admin_console/team/' + team.id)) {
browserHistory.push('/admin_console');
}
}
showTeamSelect(e) {
e.preventDefault();
this.setState({showSelectModal: true});
}
teamSelectedModal(teamId) {
this.setState({
showSelectModal: false
});
const selectedTeams = Object.assign({}, this.state.selectedTeams);
selectedTeams[teamId] = true;
AdminStore.saveSelectedTeams(selectedTeams);
this.handleAllTeamsChange();
}
teamSelectedModalDismissed() {
this.setState({showSelectModal: false});
}
updateTitle() {
let currentSiteName = '';
if (global.window.mm_config.SiteName != null) {
@@ -114,79 +47,6 @@ export default class AdminSidebar extends React.Component {
document.title = Utils.localizeMessage('sidebar_right_menu.console', 'System Console') + ' - ' + currentSiteName;
}
renderAddTeamButton() {
const addTeamTooltip = (
<Tooltip id='add-team-tooltip'>
<FormattedMessage
id='admin.sidebar.addTeamSidebar'
defaultMessage='Add team from sidebar menu'
/>
</Tooltip>
);
return (
<span className='menu-icon--right'>
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={addTeamTooltip}
>
<a
href='#'
onClick={this.showTeamSelect}
>
<i
className='fa fa-plus'
/>
</a>
</OverlayTrigger>
</span>
);
}
renderTeams() {
const teams = [];
let teamsArray = [];
Reflect.ownKeys(this.state.selectedTeams).forEach((key) => {
if (this.state.teams[key]) {
teamsArray.push(this.state.teams[key]);
}
});
teamsArray = teamsArray.sort(Utils.sortTeamsByDisplayName);
for (let i = 0; i < teamsArray.length; i++) {
const team = teamsArray[i];
teams.push(
<AdminSidebarTeam
key={team.id}
team={team}
onRemoveTeam={this.removeTeam}
/>
);
}
return (
<AdminSidebarCategory
parentLink='/admin_console'
icon='fa-user'
title={
<FormattedMessage
id='admin.sidebar.teams'
defaultMessage='TEAMS ({count, number})'
values={{
count: Object.keys(this.state.teams).length
}}
/>
}
action={this.renderAddTeamButton()}
>
{teams}
</AdminSidebarCategory>
);
}
render() {
let oauthSettings = null;
let ldapSettings = null;
@@ -421,6 +281,24 @@ export default class AdminSidebar extends React.Component {
/>
}
/>
<AdminSidebarSection
name='team_analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='Team Statistics'
/>
}
/>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='Users'
/>
}
/>
<AdminSidebarSection
name='logs'
title={
@@ -760,16 +638,9 @@ export default class AdminSidebar extends React.Component {
{metricsSettings}
</AdminSidebarSection>
</AdminSidebarCategory>
{this.renderTeams()}
{otherCategory}
</ul>
</div>
<SelectTeamModal
teams={this.state.teams}
show={this.state.showSelectModal}
onModalSubmit={this.teamSelectedModal}
onModalDismissed={this.teamSelectedModalDismissed}
/>
</div>
);
}

Просмотреть файл

@@ -1,87 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import AdminSidebarSection from './admin_sidebar_section.jsx';
export default class AdminSidebarTeam extends React.Component {
static get propTypes() {
return {
team: React.PropTypes.object.isRequired,
onRemoveTeam: React.PropTypes.func.isRequired,
parentLink: React.PropTypes.string
};
}
constructor(props) {
super(props);
this.handleRemoveTeam = this.handleRemoveTeam.bind(this);
}
handleRemoveTeam(e) {
e.preventDefault();
this.props.onRemoveTeam(this.props.team);
}
render() {
const team = this.props.team;
const removeTeamTooltip = (
<Tooltip id='remove-team-tooltip'>
<FormattedMessage
id='admin.sidebar.rmTeamSidebar'
defaultMessage='Remove team from sidebar menu'
/>
</Tooltip>
);
const removeTeamButton = (
<OverlayTrigger
delayShow={1000}
placement='top'
overlay={removeTeamTooltip}
>
<span
className='menu-icon--right menu__close'
onClick={this.handleRemoveTeam}
>
{'×'}
</span>
</OverlayTrigger>
);
return (
<AdminSidebarSection
key={team.id}
name={'team/' + team.id}
parentLink={this.props.parentLink}
title={team.display_name}
action={removeTeamButton}
>
<AdminSidebarSection
name='users'
title={
<FormattedMessage
id='admin.sidebar.users'
defaultMessage='- Users'
/>
}
/>
<AdminSidebarSection
name='analytics'
title={
<FormattedMessage
id='admin.sidebar.statistics'
defaultMessage='- Team Statistics'
/>
}
/>
</AdminSidebarSection>
);
}
}

Просмотреть файл

@@ -76,7 +76,7 @@ export default class Audits extends React.Component {
<ComplianceReports/>
<div className='panel audit-panel'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.audits.title'
defaultMessage='User Activity Logs'

Просмотреть файл

@@ -51,12 +51,10 @@ export default class ClusterSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
</h3>
<FormattedMessage
id='admin.advance.cluster'
defaultMessage='High Availability (Beta)'
/>
);
}

Просмотреть файл

@@ -38,12 +38,10 @@ export default class ComplianceSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
</h3>
<FormattedMessage
id='admin.compliance.title'
defaultMessage='Compliance Settings'
/>
);
}

Просмотреть файл

@@ -64,12 +64,10 @@ export default class ConfigurationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
</h3>
<FormattedMessage
id='admin.general.configuration'
defaultMessage='Configuration'
/>
);
}

Просмотреть файл

@@ -36,12 +36,10 @@ export default class ConnectionSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
</h3>
<FormattedMessage
id='admin.security.connection'
defaultMessage='Connections'
/>
);
}

Просмотреть файл

@@ -44,12 +44,10 @@ export default class CustomBrandSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
</h3>
<FormattedMessage
id='admin.customization.customBrand'
defaultMessage='Custom Branding'
/>
);
}
@@ -155,4 +153,4 @@ export default class CustomBrandSettings extends AdminSettings {
</SettingsGroup>
);
}
}
}

Просмотреть файл

@@ -39,12 +39,10 @@ export default class CustomEmojiSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
</h3>
<FormattedMessage
id='admin.customization.customEmoji'
defaultMessage='Custom Emoji'
/>
);
}

Просмотреть файл

@@ -43,12 +43,10 @@ export default class WebhookSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.custom'
defaultMessage='Custom Integrations'
/>
</h3>
<FormattedMessage
id='admin.integrations.custom'
defaultMessage='Custom Integrations'
/>
);
}

Просмотреть файл

@@ -46,12 +46,10 @@ export default class DatabaseSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.database.title'
defaultMessage='Database Settings'
/>
</h3>
<FormattedMessage
id='admin.database.title'
defaultMessage='Database Settings'
/>
);
}

Просмотреть файл

@@ -33,12 +33,10 @@ export default class DeveloperSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.developer.title'
defaultMessage='Developer Settings'
/>
</h3>
<FormattedMessage
id='admin.developer.title'
defaultMessage='Developer Settings'
/>
);
}

Просмотреть файл

@@ -35,12 +35,10 @@ export default class EmailAuthenticationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.email'
defaultMessage='Email'
/>
</h3>
<FormattedMessage
id='admin.authentication.email'
defaultMessage='Email'
/>
);
}

Просмотреть файл

@@ -56,12 +56,10 @@ export default class EmailSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.notifications.email'
defaultMessage='Email'
/>
</h3>
<FormattedMessage
id='admin.notifications.email'
defaultMessage='Email'
/>
);
}

Просмотреть файл

@@ -32,12 +32,10 @@ export default class ExternalServiceSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.external'
defaultMessage='External Services'
/>
</h3>
<FormattedMessage
id='admin.integrations.external'
defaultMessage='External Services'
/>
);
}

Просмотреть файл

@@ -44,12 +44,10 @@ export default class GitLabSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.gitlab'
defaultMessage='GitLab'
/>
</h3>
<FormattedMessage
id='admin.authentication.gitlab'
defaultMessage='GitLab'
/>
);
}

Просмотреть файл

@@ -43,12 +43,10 @@ export default class ImageSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.files.images'
defaultMessage='Images'
/>
</h3>
<FormattedMessage
id='admin.files.images'
defaultMessage='Images'
/>
);
}

Просмотреть файл

@@ -76,12 +76,10 @@ export default class LdapSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.ldap'
defaultMessage='AD/LDAP'
/>
</h3>
<FormattedMessage
id='admin.authentication.ldap'
defaultMessage='AD/LDAP'
/>
);
}

Просмотреть файл

@@ -41,12 +41,10 @@ export default class LegalAndSupportSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.support'
defaultMessage='Legal and Support'
/>
</h3>
<FormattedMessage
id='admin.customization.support'
defaultMessage='Legal and Support'
/>
);
}

Просмотреть файл

@@ -221,7 +221,7 @@ class LicenseSettings extends React.Component {
return (
<div className='wrapper--fixed'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.license.title'
defaultMessage='Edition and License'

Просмотреть файл

@@ -31,12 +31,10 @@ export default class LinkPreviewsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.linkPreviews'
defaultMessage='Link Previews'
/>
</h3>
<FormattedMessage
id='admin.customization.linkPreviews'
defaultMessage='Link Previews'
/>
);
}

Просмотреть файл

@@ -52,12 +52,10 @@ export default class LocalizationSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.localization'
defaultMessage='Localization'
/>
</h3>
<FormattedMessage
id='admin.general.localization'
defaultMessage='Localization'
/>
);
}

Просмотреть файл

@@ -49,12 +49,10 @@ export default class LogSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.log'
defaultMessage='Logging'
/>
</h3>
<FormattedMessage
id='admin.general.log'
defaultMessage='Logging'
/>
);
}

Просмотреть файл

@@ -85,7 +85,7 @@ export default class Logs extends React.Component {
return (
<div className='panel'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.logs.title'
defaultMessage='Server Logs'

Просмотреть файл

@@ -38,12 +38,10 @@ export default class MetricsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.advance.metrics'
defaultMessage='Performance Monitoring'
/>
</h3>
<FormattedMessage
id='admin.advance.metrics'
defaultMessage='Performance Monitoring'
/>
);
}

Просмотреть файл

@@ -38,12 +38,10 @@ export default class MfaSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.mfa.title'
defaultMessage='Multi-factor Authentication'
/>
</h3>
<FormattedMessage
id='admin.mfa.title'
defaultMessage='Multi-factor Authentication'
/>
);
}

Просмотреть файл

@@ -35,12 +35,10 @@ export default class NativeAppLinkSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.customization.nativeAppLinks'
defaultMessage='Mattermost App Links'
/>
</h3>
<FormattedMessage
id='admin.customization.nativeAppLinks'
defaultMessage='Mattermost App Links'
/>
);
}

Просмотреть файл

@@ -111,12 +111,10 @@ export default class OAuthSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.oauth'
defaultMessage='OAuth 2.0'
/>
</h3>
<FormattedMessage
id='admin.authentication.oauth'
defaultMessage='OAuth 2.0'
/>
);
}

Просмотреть файл

@@ -138,12 +138,10 @@ export default class PasswordSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.password'
defaultMessage='Password'
/>
</h3>
<FormattedMessage
id='admin.security.password'
defaultMessage='Password'
/>
);
}

Просмотреть файл

@@ -55,12 +55,10 @@ export default class PolicySettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.policy'
defaultMessage='Policy'
/>
</h3>
<FormattedMessage
id='admin.general.policy'
defaultMessage='Policy'
/>
);
}

Просмотреть файл

@@ -33,12 +33,10 @@ export default class PrivacySettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.privacy'
defaultMessage='Privacy'
/>
</h3>
<FormattedMessage
id='admin.general.privacy'
defaultMessage='Privacy'
/>
);
}

Просмотреть файл

@@ -34,12 +34,10 @@ export default class PublicLinkSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.public_links'
defaultMessage='Public Links'
/>
</h3>
<FormattedMessage
id='admin.security.public_links'
defaultMessage='Public Links'
/>
);
}

Просмотреть файл

@@ -100,12 +100,10 @@ export default class PushSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.notifications.title'
defaultMessage='Notification Settings'
/>
</h3>
<FormattedMessage
id='admin.notifications.title'
defaultMessage='Notification Settings'
/>
);
}

Просмотреть файл

@@ -44,12 +44,10 @@ export default class RateSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.rate.title'
defaultMessage='Rate Limit Settings'
/>
</h3>
<FormattedMessage
id='admin.rate.title'
defaultMessage='Rate Limit Settings'
/>
);
}

Просмотреть файл

@@ -4,13 +4,24 @@
import * as Utils from 'utils/utils.jsx';
import {Modal} from 'react-bootstrap';
import {injectIntl, intlShape, FormattedMessage} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {adminResetPassword} from 'actions/admin_actions.jsx';
import React from 'react';
class ResetPasswordModal extends React.Component {
export default class ResetPasswordModal extends React.Component {
static propTypes = {
user: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};
static defaultProps = {
show: false
};
constructor(props) {
super(props);
@@ -150,18 +161,3 @@ class ResetPasswordModal extends React.Component {
);
}
}
ResetPasswordModal.defaultProps = {
show: false
};
ResetPasswordModal.propTypes = {
intl: intlShape.isRequired,
user: React.PropTypes.object,
team: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};
export default injectIntl(ResetPasswordModal);

Просмотреть файл

@@ -130,12 +130,10 @@ export default class SamlSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.authentication.saml'
defaultMessage='SAML'
/>
</h3>
<FormattedMessage
id='admin.authentication.saml'
defaultMessage='SAML'
/>
);
}

Просмотреть файл

@@ -1,120 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import React from 'react';
import {sortTeamsByDisplayName} from 'utils/utils.jsx';
export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
this.doSubmit = this.doSubmit.bind(this);
this.doCancel = this.doCancel.bind(this);
}
doSubmit(e) {
e.preventDefault();
this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value);
}
doCancel() {
this.props.onModalDismissed();
}
render() {
if (this.props.teams == null) {
return <div/>;
}
const options = [];
let teamsArray = [];
Reflect.ownKeys(this.props.teams).forEach((key) => {
teamsArray.push(this.props.teams[key]);
});
teamsArray = teamsArray.sort(sortTeamsByDisplayName);
for (let i = 0; i < teamsArray.length; i++) {
const team = teamsArray[i];
options.push(
<option
key={'opt_' + team.id}
value={team.id}
>
{team.display_name}
</option>
);
}
return (
<Modal
show={this.props.show}
onHide={this.doCancel}
>
<Modal.Header closeButton={true}>
<Modal.Title>
<FormattedMessage
id='admin.select_team.selectTeam'
defaultMessage='Select Team'
/>
</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='form-group'>
<div className='col-sm-12'>
<select
ref='team'
size='10'
className='form-control'
>
{options}
</select>
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.doCancel}
>
<FormattedMessage
id='admin.select_team.close'
defaultMessage='Close'
/>
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
tabIndex='2'
>
<FormattedMessage
id='admin.select_team.select'
defaultMessage='Select'
/>
</button>
</Modal.Footer>
</form>
</Modal>
);
}
}
SelectTeamModal.defaultProps = {
show: false
};
SelectTeamModal.propTypes = {
teams: React.PropTypes.object,
show: React.PropTypes.bool.isRequired,
onModalSubmit: React.PropTypes.func,
onModalDismissed: React.PropTypes.func
};

Просмотреть файл

@@ -39,12 +39,10 @@ export default class SessionSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.session'
defaultMessage='Sessions'
/>
</h3>
<FormattedMessage
id='admin.security.session'
defaultMessage='Sessions'
/>
);
}

Просмотреть файл

@@ -36,12 +36,10 @@ export default class SignupSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.security.signup'
defaultMessage='Signup'
/>
</h3>
<FormattedMessage
id='admin.security.signup'
defaultMessage='Signup'
/>
);
}

Просмотреть файл

@@ -52,12 +52,10 @@ export default class StorageSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.files.storage'
defaultMessage='Storage'
/>
</h3>
<FormattedMessage
id='admin.files.storage'
defaultMessage='Storage'
/>
);
}

Просмотреть файл

@@ -0,0 +1,370 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {
loadProfiles,
loadProfilesAndTeamMembers,
loadProfilesWithoutTeam,
searchUsers
} from 'actions/user_actions.jsx';
import AdminStore from 'stores/admin_store.jsx';
import AnalyticsStore from 'stores/analytics_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {getAllTeams, getStandardAnalytics, getTeamStats, getUser} from 'utils/async_client.jsx';
import {Constants, StatTypes, UserSearchOptions} from 'utils/constants.jsx';
import {convertTeamMapToList} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import SystemUsersList from './system_users_list.jsx';
const ALL_USERS = '';
const NO_TEAM = 'no_team';
const USER_ID_LENGTH = 26;
const USERS_PER_PAGE = 50;
export default class SystemUsers extends React.Component {
constructor(props) {
super(props);
this.updateTeamsFromStore = this.updateTeamsFromStore.bind(this);
this.updateTotalUsersFromStore = this.updateTotalUsersFromStore.bind(this);
this.updateUsersFromStore = this.updateUsersFromStore.bind(this);
this.loadDataForTeam = this.loadDataForTeam.bind(this);
this.loadComplete = this.loadComplete.bind(this);
this.handleTeamChange = this.handleTeamChange.bind(this);
this.handleTermChange = this.handleTermChange.bind(this);
this.nextPage = this.nextPage.bind(this);
this.doSearch = this.doSearch.bind(this);
this.search = this.search.bind(this);
this.getUserById = this.getUserById.bind(this);
this.renderFilterRow = this.renderFilterRow.bind(this);
this.state = {
teams: convertTeamMapToList(AdminStore.getAllTeams()),
totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS],
users: UserStore.getProfileList(),
teamId: ALL_USERS,
term: '',
loading: true,
searching: false
};
}
componentDidMount() {
AdminStore.addAllTeamsChangeListener(this.updateTeamsFromStore);
AnalyticsStore.addChangeListener(this.updateTotalUsersFromStore);
TeamStore.addStatsChangeListener(this.updateTotalUsersFromStore);
UserStore.addChangeListener(this.updateUsersFromStore);
UserStore.addInTeamChangeListener(this.updateUsersFromStore);
UserStore.addWithoutTeamChangeListener(this.updateUsersFromStore);
this.loadDataForTeam(this.state.teamId);
getAllTeams();
}
componentWillUpdate(nextProps, nextState) {
const nextTeamId = nextState.teamId;
if (this.state.teamId !== nextTeamId) {
this.updateTotalUsersFromStore(nextTeamId);
this.updateUsersFromStore(nextTeamId, nextState.term);
this.loadDataForTeam(nextTeamId);
}
}
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.updateTeamsFromStore);
AnalyticsStore.removeChangeListener(this.updateTotalUsersFromStore);
TeamStore.removeStatsChangeListener(this.updateTotalUsersFromStore);
UserStore.removeChangeListener(this.updateUsersFromStore);
UserStore.removeInTeamChangeListener(this.updateUsersFromStore);
UserStore.removeWithoutTeamChangeListener(this.updateUsersFromStore);
}
updateTeamsFromStore() {
this.setState({teams: convertTeamMapToList(AdminStore.getAllTeams())});
}
updateTotalUsersFromStore(teamId = this.state.teamId) {
if (teamId === ALL_USERS) {
this.setState({
totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS]
});
} else if (teamId === NO_TEAM) {
this.setState({
totalUsers: 0
});
} else {
this.setState({
totalUsers: TeamStore.getStats(teamId).total_member_count
});
}
}
updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) {
if (term) {
if (teamId === this.state.teamId) {
// Search results aren't in the store, so manually update the users in them
const users = [...this.state.users];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (UserStore.hasProfile(user.id)) {
users[i] = UserStore.getProfile(user.id);
}
}
this.setState({
users
});
} else {
this.doSearch(teamId, term, true);
}
return;
}
if (teamId === ALL_USERS) {
this.setState({users: UserStore.getProfileList(false, true)});
} else if (teamId === NO_TEAM) {
this.setState({users: UserStore.getProfileListWithoutTeam()});
} else {
this.setState({users: UserStore.getProfileListInTeam(this.state.teamId)});
}
}
loadDataForTeam(teamId) {
if (teamId === ALL_USERS) {
loadProfiles(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
getStandardAnalytics();
} else if (teamId === NO_TEAM) {
loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete);
} else {
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, this.loadComplete);
getTeamStats(teamId);
}
}
loadComplete() {
this.setState({loading: false});
}
handleTeamChange(e) {
this.setState({teamId: e.target.value});
}
handleTermChange(term) {
this.setState({term});
}
nextPage(page) {
// Paging isn't supported while searching
if (this.state.teamId === ALL_USERS) {
loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete);
} else if (this.state.teamId === NO_TEAM) {
loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete);
} else {
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete);
}
}
search(term) {
if (term === '') {
this.updateUsersFromStore(this.state.teamId, term);
this.setState({
loading: false
});
this.searchTimeoutId = '';
return;
}
this.doSearch(this.state.teamId, term);
}
doSearch(teamId, term, now = false) {
clearTimeout(this.searchTimeoutId);
this.setState({
loading: true,
users: []
});
const options = {
[UserSearchOptions.ALLOW_INACTIVE]: true
};
if (teamId === NO_TEAM) {
options[UserSearchOptions.WITHOUT_TEAM] = true;
}
const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
teamId,
options,
(users) => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
if (users.length > 0) {
this.setState({
loading: false,
users
});
} else if (term.length === USER_ID_LENGTH) {
// This term didn't match any users name, but it does look like it might be a user's ID
this.getUserById(term, searchTimeoutId);
} else {
this.setState({
loading: false
});
}
},
() => {
this.setState({
loading: false
});
}
);
},
now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
getUserById(id, searchTimeoutId) {
if (UserStore.hasProfile(id)) {
this.setState({
loading: false,
users: [UserStore.getProfile(id)]
});
return;
}
getUser(
id,
(user) => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
this.setState({
loading: false,
users: [user]
});
},
() => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
this.setState({
loading: false,
users: []
});
}
);
}
renderFilterRow(doSearch) {
const teams = this.state.teams.map((team) => {
return (
<option
key={team.id}
value={team.id}
>
{team.display_name}
</option>
);
});
return (
<div className='system-users__filter-row'>
<div className='system-users__filter'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
onInput={doSearch}
/>
</div>
<label>
<span className='system-users__team-filter-label'>
<FormattedMessage
id='filtered_user_list.show'
defaultMessage='Filter:'
/>
</span>
<select
className='form-control system-users__team-filter'
onChange={this.handleTeamChange}
value={this.state.teamId}
>
<option value={ALL_USERS}>{Utils.localizeMessage('admin.system_users.allUsers', 'All Users')}</option>
<option value={NO_TEAM}>{Utils.localizeMessage('admin.system_users.noTeams', 'No Teams')}</option>
{teams}
</select>
</label>
</div>
);
}
render() {
let users = null;
if (!this.state.loading) {
users = this.state.users;
}
return (
<div className='wrapper--fixed'>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.system_users.title'
defaultMessage='{siteName} Users'
values={{
siteName: global.mm_config.SiteName
}}
/>
</h3>
<div className='more-modal__list member-list-holder'>
<SystemUsersList
renderFilterRow={this.renderFilterRow}
search={this.search}
nextPage={this.nextPage}
users={users}
usersPerPage={USERS_PER_PAGE}
total={this.state.totalUsers}
teams={this.state.teams}
teamId={this.state.teamId}
term={this.state.term}
onTermChange={this.handleTermChange}
/>
</div>
</div>
);
}
}

Просмотреть файл

@@ -1,38 +1,38 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ConfirmModal from '../confirm_modal.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import {updateUserRoles, updateActive} from 'actions/user_actions.jsx';
import {updateTeamMemberRoles, removeUserFromTeam} from 'actions/team_actions.jsx';
import {adminResetMfa} from 'actions/admin_actions.jsx';
import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class AdminTeamMembersDropdown extends React.Component {
export default class SystemUsersDropdown extends React.Component {
static propTypes = {
user: React.PropTypes.object.isRequired,
doPasswordReset: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.handleMakeMember = this.handleMakeMember.bind(this);
this.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this);
this.handleMakeActive = this.handleMakeActive.bind(this);
this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
this.handleMakeTeamAdmin = this.handleMakeTeamAdmin.bind(this);
this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
this.handleResetPassword = this.handleResetPassword.bind(this);
this.handleResetMfa = this.handleResetMfa.bind(this);
this.handleDemoteSystemAdmin = this.handleDemoteSystemAdmin.bind(this);
this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this);
this.handleDemoteCancel = this.handleDemoteCancel.bind(this);
this.doMakeMember = this.doMakeMember.bind(this);
this.doMakeTeamAdmin = this.doMakeTeamAdmin.bind(this);
this.state = {
serverError: null,
@@ -51,16 +51,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
this.setState({serverError: err.message});
}
);
updateTeamMemberRoles(
this.props.teamMember.team_id,
this.props.user.id,
'team_user',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeMember(e) {
@@ -73,17 +63,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
}
handleRemoveFromTeam() {
removeUserFromTeam(
this.props.teamMember.team_id,
this.props.user.id,
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeActive(e) {
e.preventDefault();
updateActive(this.props.user.id, true, null,
@@ -102,28 +81,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
doMakeTeamAdmin() {
updateTeamMemberRoles(
this.props.teamMember.team_id,
this.props.user.id,
'team_user team_admin',
null,
(err) => {
this.setState({serverError: err.message});
}
);
}
handleMakeTeamAdmin(e) {
e.preventDefault();
const me = UserStore.getCurrentUser();
if (this.props.user.id === me.id && me.roles.includes('system_admin')) {
this.handleDemoteSystemAdmin(this.props.user, 'teamadmin');
} else {
this.doMakeTeamAdmin();
}
}
handleMakeSystemAdmin(e) {
e.preventDefault();
@@ -174,8 +131,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
handleDemoteSubmit() {
if (this.state.role === 'member') {
this.doMakeMember();
} else {
this.doMakeTeamAdmin();
}
const teamUrl = TeamStore.getCurrentTeamUrl();
@@ -197,9 +152,8 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
const teamMember = this.props.teamMember;
const user = this.props.user;
if (!user || !teamMember) {
if (!user) {
return <div/>;
}
let currentRoles = (
@@ -209,15 +163,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
/>
);
if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) {
currentRoles = (
<FormattedMessage
id='team_members_dropdown.teamAdmin'
defaultMessage='Team Admin'
/>
);
}
if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) {
currentRoles = (
<FormattedMessage
@@ -228,8 +173,7 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
const me = UserStore.getCurrentUser();
let showMakeMember = Utils.isAdmin(teamMember.roles) || Utils.isSystemAdmin(user.roles);
let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles);
let showMakeMember = Utils.isSystemAdmin(user.roles);
let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles);
let showMakeActive = false;
let showMakeNotActive = !Utils.isSystemAdmin(user.roles);
@@ -244,7 +188,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
/>
);
showMakeMember = false;
showMakeAdmin = false;
showMakeSystemAdmin = false;
showMakeActive = true;
showMakeNotActive = false;
@@ -273,24 +216,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
let makeAdmin = null;
if (showMakeAdmin) {
makeAdmin = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.handleMakeTeamAdmin}
>
<FormattedMessage
id='admin.user_item.makeTeamAdmin'
defaultMessage='Make Team Admin'
/>
</a>
</li>
);
}
let makeMember = null;
if (showMakeMember) {
makeMember = (
@@ -309,24 +234,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
let removeFromTeam = null;
if (this.props.user.id !== me.id) {
removeFromTeam = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.handleRemoveFromTeam}
>
<FormattedMessage
id='team_members_dropdown.leave_team'
defaultMessage='Remove From Team'
/>
</a>
</li>
);
}
let menuClass = '';
if (disableActivationToggle) {
menuClass = 'disabled';
@@ -493,8 +400,6 @@ export default class AdminTeamMembersDropdown extends React.Component {
className='dropdown-menu member-menu'
role='menu'
>
{removeFromTeam}
{makeAdmin}
{makeMember}
{makeActive}
{makeNotActive}
@@ -508,9 +413,3 @@ export default class AdminTeamMembersDropdown extends React.Component {
);
}
}
AdminTeamMembersDropdown.propTypes = {
user: React.PropTypes.object.isRequired,
teamMember: React.PropTypes.object.isRequired,
doPasswordReset: React.PropTypes.func.isRequired
};

Просмотреть файл

@@ -0,0 +1,232 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx';
import {getUser} from 'utils/async_client.jsx';
import {Constants} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import SystemUsersDropdown from './system_users_dropdown.jsx';
export default class SystemUsersList extends React.Component {
static propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
nextPage: React.PropTypes.func,
search: React.PropTypes.func.isRequired,
focusOnMount: React.PropTypes.bool,
renderFilterRow: React.PropTypes.func,
teamId: React.PropTypes.string.isRequired,
term: React.PropTypes.string.isRequired,
onTermChange: React.PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.search = this.search.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
this.state = {
page: 0,
showPasswordModal: false,
user: null
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.teamId !== this.props.teamId) {
this.setState({page: 0});
}
}
nextPage() {
this.setState({page: this.state.page + 1});
this.props.nextPage(this.state.page + 1);
}
previousPage() {
this.setState({page: this.state.page - 1});
}
search(term) {
this.props.search(term);
if (term !== '') {
this.setState({page: 0});
}
}
doPasswordReset(user) {
this.setState({
showPasswordModal: true,
user
});
}
doPasswordResetDismiss() {
this.setState({
showPasswordModal: false,
user: null
});
}
doPasswordResetSubmit(user) {
getUser(user.id);
this.setState({
showPasswordModal: false,
user: null
});
}
getInfoForUser(user) {
const info = [];
if (user.auth_service) {
let service;
if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) {
service = user.auth_service.toUpperCase();
} else {
service = Utils.toTitleCase(user.auth_service);
}
info.push(
<FormattedHTMLMessage
key='admin.user_item.authServiceNotEmail'
id='admin.user_item.authServiceNotEmail'
defaultMessage='<strong>Sign-in Method:</strong> {service}'
values={{
service
}}
/>
);
} else {
info.push(
<FormattedHTMLMessage
key='admin.user_item.authServiceEmail'
id='admin.user_item.authServiceEmail'
defaultMessage='<strong>Sign-in Method:</strong> Email'
/>
);
}
const mfaEnabled = global.window.mm_license.IsLicensed === 'true' &&
global.window.mm_license.MFA === 'true' &&
global.window.mm_config.EnableMultifactorAuthentication === 'true';
if (mfaEnabled) {
info.push(', ');
if (user.mfa_active) {
info.push(
<FormattedHTMLMessage
key='admin.user_item.mfaYes'
id='admin.user_item.mfaYes'
defaultMessage='<strong>MFA</strong>: Yes'
/>
);
} else {
info.push(
<FormattedHTMLMessage
key='admin.user_item.mfaNo'
id='admin.user_item.mfaNo'
defaultMessage='<strong>MFA</strong>: No'
/>
);
}
}
return info;
}
renderCount(count, total, startCount, endCount, isSearch) {
if (total) {
if (isSearch) {
return (
<FormattedMessage
id='system_users_list.countSearch'
defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total} total'
values={{
count,
total
}}
/>
);
} else if (startCount !== 0 || endCount !== total) {
return (
<FormattedMessage
id='system_users_list.countPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total'
values={{
count,
startCount: startCount + 1,
endCount,
total
}}
/>
);
}
return (
<FormattedMessage
id='system_users_list.count'
defaultMessage='{count, number} {count, plural, one {user} other {users}}'
values={{
count
}}
/>
);
}
return null;
}
render() {
const extraInfo = {};
if (this.props.users) {
for (const user of this.props.users) {
extraInfo[user.id] = this.getInfoForUser(user);
}
}
return (
<div>
<SearchableUserList
{...this.props}
renderCount={this.renderCount}
extraInfo={extraInfo}
actions={[SystemUsersDropdown]}
actionProps={{
doPasswordReset: this.doPasswordReset
}}
nextPage={this.nextPage}
previousPage={this.previousPage}
search={this.search}
page={this.state.page}
term={this.props.term}
onTermChange={this.props.onTermChange}
/>
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
onModalSubmit={this.doPasswordResetSubmit}
onModalDismissed={this.doPasswordResetDismiss}
/>
</div>
);
}
}

Просмотреть файл

@@ -1,298 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import SearchableUserList from 'components/searchable_user_list.jsx';
import AdminTeamMembersDropdown from './admin_team_members_dropdown.jsx';
import ResetPasswordModal from './reset_password_modal.jsx';
import FormError from 'components/form_error.jsx';
import AdminStore from 'stores/admin_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx';
import {getTeamStats, getUser} from 'utils/async_client.jsx';
import {Constants, UserSearchOptions} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
const USERS_PER_PAGE = 50;
export default class UserList extends React.Component {
static get propTypes() {
return {
params: React.PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.onUsersChange = this.onUsersChange.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.doPasswordReset = this.doPasswordReset.bind(this);
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
this.nextPage = this.nextPage.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
this.searchTimeoutId = 0;
const stats = TeamStore.getStats(this.props.params.team);
this.state = {
team: AdminStore.getTeam(this.props.params.team),
users: [],
teamMembers: TeamStore.getMembersInTeam(this.props.params.team),
total: stats.total_member_count,
serverError: null,
showPasswordModal: false,
loading: true,
user: null
};
}
componentDidMount() {
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.addChangeListener(this.onUsersChange);
UserStore.addInTeamChangeListener(this.onUsersChange);
TeamStore.addChangeListener(this.onTeamChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, this.props.params.team, this.loadComplete);
getTeamStats(this.props.params.team);
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.team !== this.props.params.team) {
const stats = TeamStore.getStats(nextProps.params.team);
this.setState({
team: AdminStore.getTeam(nextProps.params.team),
users: [],
teamMembers: TeamStore.getMembersInTeam(nextProps.params.team),
total: stats.total_member_count,
serverError: null,
showPasswordModal: false,
loading: true,
user: null
});
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, nextProps.params.team, this.loadComplete);
getTeamStats(nextProps.params.team);
}
}
componentWillUnmount() {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
UserStore.removeChangeListener(this.onUsersChange);
UserStore.removeInTeamChangeListener(this.onUsersChange);
TeamStore.removeChangeListener(this.onTeamChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
loadComplete() {
this.setState({loading: false});
}
onAllTeamsChange() {
this.setState({
team: AdminStore.getTeam(this.props.params.team)
});
}
onStatsChange() {
const stats = TeamStore.getStats(this.props.params.team);
this.setState({total: stats.total_member_count});
}
onUsersChange() {
this.setState({users: UserStore.getProfileListInTeam(this.props.params.team)});
}
onTeamChange() {
this.setState({teamMembers: TeamStore.getMembersInTeam(this.props.params.team)});
}
nextPage(page) {
loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.props.params.team);
}
doPasswordReset(user) {
this.setState({
showPasswordModal: true,
user
});
}
doPasswordResetDismiss() {
this.setState({
showPasswordModal: false,
user: null
});
}
doPasswordResetSubmit(user) {
getUser(user.id);
this.setState({
showPasswordModal: false,
user: null
});
}
search(term) {
clearTimeout(this.searchTimeoutId);
if (term === '') {
this.setState({search: false, users: UserStore.getProfileListInTeam(this.props.params.team)});
this.searchTimeoutId = '';
return;
}
const options = {};
options[UserSearchOptions.ALLOW_INACTIVE] = true;
const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
this.props.params.team,
options,
(users) => {
if (searchTimeoutId !== this.searchTimeoutId) {
return;
}
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete);
}
);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
this.searchTimeoutId = searchTimeoutId;
}
render() {
if (!this.state.team) {
return null;
}
const teamMembers = this.state.teamMembers;
const users = this.state.users;
const actionUserProps = {};
const extraInfo = {};
const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true';
let usersToDisplay;
if (this.state.loading) {
usersToDisplay = null;
} else {
usersToDisplay = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (teamMembers[user.id]) {
usersToDisplay.push(user);
actionUserProps[user.id] = {
teamMember: teamMembers[user.id]
};
const info = [];
if (user.auth_service) {
const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service);
info.push(
<FormattedHTMLMessage
key='admin.user_item.authServiceNotEmail'
id='admin.user_item.authServiceNotEmail'
defaultMessage='<strong>Sign-in Method:</strong> {service}'
values={{
service
}}
/>
);
} else {
info.push(
<FormattedHTMLMessage
key='admin.user_item.authServiceEmail'
id='admin.user_item.authServiceEmail'
defaultMessage='<strong>Sign-in Method:</strong> Email'
/>
);
}
if (mfaEnabled) {
info.push(', ');
if (user.mfa_active) {
info.push(
<FormattedHTMLMessage
key='admin.user_item.mfaYes'
id='admin.user_item.mfaYes'
defaultMessage='<strong>MFA</strong>: Yes'
/>
);
} else {
info.push(
<FormattedHTMLMessage
key='admin.user_item.mfaNo'
id='admin.user_item.mfaNo'
defaultMessage='<strong>MFA</strong>: No'
/>
);
}
}
extraInfo[user.id] = info;
}
}
}
return (
<div className='wrapper--fixed'>
<h3>
<FormattedMessage
id='admin.userList.title2'
defaultMessage='Users for {team} ({count})'
values={{
team: this.state.team.name,
count: this.state.total
}}
/>
</h3>
<FormError error={this.state.serverError}/>
<div className='more-modal__list member-list-holder'>
<SearchableUserList
users={usersToDisplay}
usersPerPage={USERS_PER_PAGE}
total={this.state.total}
extraInfo={extraInfo}
nextPage={this.nextPage}
search={this.search}
actions={[AdminTeamMembersDropdown]}
actionProps={{
doPasswordReset: this.doPasswordReset
}}
actionUserProps={actionUserProps}
/>
</div>
<ResetPasswordModal
user={this.state.user}
show={this.state.showPasswordModal}
team={this.state.team}
onModalSubmit={this.doPasswordResetSubmit}
onModalDismissed={this.doPasswordResetDismiss}
/>
</div>
);
}
}

Просмотреть файл

@@ -51,12 +51,10 @@ export default class UsersAndTeamsSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.general.usersAndTeams'
defaultMessage='Users and Teams'
/>
</h3>
<FormattedMessage
id='admin.general.usersAndTeams'
defaultMessage='Users and Teams'
/>
);
}

Просмотреть файл

@@ -50,12 +50,10 @@ export default class WebrtcSettings extends AdminSettings {
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.integrations.webrtc'
defaultMessage='Mattermost WebRTC (Beta)'
/>
</h3>
<FormattedMessage
id='admin.integrations.webrtc'
defaultMessage='Mattermost WebRTC (Beta)'
/>
);
}

Просмотреть файл

@@ -21,14 +21,33 @@ export default class LineChart extends React.Component {
this.initChart();
}
componentWillUpdate(nextProps) {
const willHaveData = nextProps.data && nextProps.data.labels.length > 0;
const hasChart = Boolean(this.chart);
if (!willHaveData && hasChart) {
// Clean up the rendered chart before we render and destroy its context
this.chart.destroy();
this.chart = null;
}
}
componentDidUpdate(prevProps) {
if (!Utils.areObjectsEqual(prevProps.data, this.props.data) || !Utils.areObjectsEqual(prevProps.options, this.props.options)) {
this.initChart(true);
if (Utils.areObjectsEqual(prevProps.data, this.props.data) && Utils.areObjectsEqual(prevProps.options, this.props.options)) {
return;
}
const hasData = this.props.data && this.props.data.labels.length > 0;
const hasChart = Boolean(this.chart);
if (hasData) {
// Update the rendered chart or initialize it as necessary
this.initChart(hasChart);
}
}
componentWillUnmount() {
if (this.chart && this.refs.canvas) {
if (this.chart) {
this.chart.destroy();
}
}
@@ -37,9 +56,11 @@ export default class LineChart extends React.Component {
if (!this.refs.canvas) {
return;
}
var el = ReactDOM.findDOMNode(this.refs.canvas);
var ctx = el.getContext('2d');
this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); //eslint-disable-line new-cap
this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); // eslint-disable-line new-cap
if (update) {
this.chart.update();
}

Просмотреть файл

@@ -409,7 +409,7 @@ class SystemAnalytics extends React.Component {
return (
<div className='wrapper--fixed team_statistics'>
<h3>
<h3 className='admin-console-header'>
<FormattedMessage
id='analytics.system.title'
defaultMessage='System Statistics'

Просмотреть файл

@@ -1,40 +1,43 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedDate, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import Banner from 'components/admin_console/banner.jsx';
import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import AdminStore from 'stores/admin_store.jsx';
import AnalyticsStore from 'stores/analytics_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
const StatTypes = Constants.StatTypes;
import {StatTypes} from 'utils/constants.jsx';
import {convertTeamMapToList} from 'utils/team_utils.jsx';
import LineChart from './line_chart.jsx';
import StatisticCount from './statistic_count.jsx';
import TableChart from './table_chart.jsx';
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
import {FormattedMessage, FormattedDate, FormattedHTMLMessage} from 'react-intl';
import React from 'react';
const LAST_ANALYTICS_TEAM = 'last_analytics_team';
export default class TeamAnalytics extends React.Component {
static get propTypes() {
return {
params: React.PropTypes.object.isRequired
};
}
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onAllTeamsChange = this.onAllTeamsChange.bind(this);
this.handleTeamChange = this.handleTeamChange.bind(this);
const teams = convertTeamMapToList(AdminStore.getAllTeams());
const teamId = BrowserStore.getGlobalItem(LAST_ANALYTICS_TEAM, teams.length > 0 ? teams[0].id : '');
this.state = {
team: AdminStore.getTeam(this.props.params.team),
stats: AnalyticsStore.getAllTeam(this.props.params.team)
teams,
teamId,
team: AdminStore.getTeam(teamId),
stats: AnalyticsStore.getAllTeam(teamId)
};
}
@@ -42,7 +45,19 @@ export default class TeamAnalytics extends React.Component {
AnalyticsStore.addChangeListener(this.onChange);
AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange);
this.getData(this.props.params.team);
if (this.state.teamId !== '') {
this.getData(this.state.teamId);
}
if (this.state.teams.length === 0) {
AsyncClient.getAllTeams();
}
}
componentWillUpdate(nextProps, nextState) {
if (nextState.teamId !== this.state.teamId) {
this.getData(nextState.teamId);
}
}
getData(id) {
@@ -57,40 +72,60 @@ export default class TeamAnalytics extends React.Component {
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange);
}
componentWillReceiveProps(nextProps) {
this.getData(nextProps.params.team);
this.setState({
stats: AnalyticsStore.getAllTeam(nextProps.params.team)
});
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) {
return true;
}
return false;
}
onChange() {
this.setState({
stats: AnalyticsStore.getAllTeam(this.props.params.team)
stats: AnalyticsStore.getAllTeam(this.state.teamId)
});
}
onAllTeamsChange() {
const teams = convertTeamMapToList(AdminStore.getAllTeams());
if (teams.length > 0) {
if (this.state.teamId) {
this.setState({
team: AdminStore.getTeam(this.state.teamId)
});
} else {
this.setState({
teamId: teams[0].id,
team: teams[0]
});
}
}
this.setState({
team: AdminStore.getTeam(this.props.params.team)
teams
});
}
handleTeamChange(e) {
const teamId = e.target.value;
this.setState({
teamId,
team: AdminStore.getTeam(teamId)
});
BrowserStore.setGlobalItem(LAST_ANALYTICS_TEAM, teamId);
}
render() {
if (!this.state.team || !this.state.stats) {
return null;
if (this.state.teams.length === 0 || !this.state.team || !this.state.stats) {
return <LoadingScreen/>;
}
if (this.state.teamId === '') {
return (
<Banner
description={
<FormattedMessage
id='analytics.team.noTeams'
defaultMessage='There are no teams on this server for which to view statistics.'
/>
}
/>
);
}
const stats = this.state.stats;
@@ -129,6 +164,7 @@ export default class TeamAnalytics extends React.Component {
postTotalGraph = (
<div className='row'>
<LineChart
key={this.state.team.id}
title={
<FormattedMessage
id='analytics.team.totalPosts'
@@ -150,6 +186,7 @@ export default class TeamAnalytics extends React.Component {
userActiveGraph = (
<div className='row'>
<LineChart
key={this.state.team.id}
title={
<FormattedMessage
id='analytics.team.activeUsers'
@@ -172,17 +209,41 @@ export default class TeamAnalytics extends React.Component {
const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
const teams = this.state.teams.map((team) => {
return (
<option
key={team.id}
value={team.id}
>
{team.display_name}
</option>
);
});
return (
<div className='wrapper--fixed team_statistics'>
<h3>
<FormattedMessage
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
team: this.state.team.name
}}
/>
</h3>
<div className='admin-console-header team-statistics__header-row'>
<div className='team-statistics__header'>
<h3>
<FormattedMessage
id='analytics.team.title'
defaultMessage='Team Statistics for {team}'
values={{
team: this.state.team.display_name
}}
/>
</h3>
</div>
<div className='team-statistics__team-filter'>
<select
className='form-control team-statistics__team-filter__dropdown'
onChange={this.handleTeamChange}
value={this.state.teamId}
>
{teams}
</select>
</div>
</div>
{banner}
<div className='row'>
<StatisticCount

Просмотреть файл

@@ -2,7 +2,7 @@
// See License.txt for license information.
import ChannelInviteButton from './channel_invite_button.jsx';
import SearchableUserList from './searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import LoadingScreen from './loading_screen.jsx';
import ChannelStore from 'stores/channel_store.jsx';

Просмотреть файл

@@ -2,7 +2,7 @@
// See License.txt for license information.
import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx';
import SearchableUserList from 'components/searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import SearchableUserList from 'components/searchable_user_list.jsx';
import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx';
import TeamMembersDropdown from 'components/team_members_dropdown.jsx';
import UserStore from 'stores/user_store.jsx';

Просмотреть файл

@@ -41,7 +41,6 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
import InviteMemberModal from 'components/invite_member_modal.jsx';
import LeaveTeamModal from 'components/leave_team_modal.jsx';
import SelectTeamModal from 'components/admin_console/select_team_modal.jsx';
import iNoBounce from 'inobounce';
import * as UserAgent from 'utils/user_agent.jsx';
@@ -212,7 +211,6 @@ export default class NeedsTeam extends React.Component {
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
<SelectTeamModal/>
</div>
</div>
);

Просмотреть файл

@@ -1,31 +1,61 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import UserList from 'components/user_list.jsx';
import * as Utils from 'utils/utils.jsx';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
const NEXT_BUTTON_TIMEOUT = 500;
export default class SearchableUserList extends React.Component {
static propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
previousPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool,
renderCount: React.PropTypes.func,
renderFilterRow: React.PropTypes.func,
page: React.PropTypes.number.isRequired,
term: React.PropTypes.string.isRequired,
onTermChange: React.PropTypes.func.isRequired
};
static defaultProps = {
users: [],
usersPerPage: 50, // eslint-disable-line no-magic-numbers
extraInfo: {},
actions: [],
actionProps: {},
actionUserProps: {},
showTeamToggle: false,
focusOnMount: false
};
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.doSearch = this.doSearch.bind(this);
this.focusSearchBar = this.focusSearchBar.bind(this);
this.handleInput = this.handleInput.bind(this);
this.renderCount = this.renderCount.bind(this);
this.nextTimeoutId = 0;
this.state = {
page: 0,
search: false,
nextDisabled: false
};
}
@@ -34,10 +64,11 @@ export default class SearchableUserList extends React.Component {
this.focusSearchBar();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.page !== prevState.page) {
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
componentDidUpdate(prevProps) {
if (this.props.page !== prevProps.page || this.props.term !== prevProps.term) {
this.refs.userList.scrollToTop();
}
this.focusSearchBar();
}
@@ -47,14 +78,17 @@ export default class SearchableUserList extends React.Component {
nextPage(e) {
e.preventDefault();
this.setState({page: this.state.page + 1, nextDisabled: true});
this.setState({nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT);
this.props.nextPage(this.state.page + 1);
this.props.nextPage();
}
previousPage(e) {
e.preventDefault();
this.setState({page: this.state.page - 1});
this.props.previousPage();
}
focusSearchBar() {
@@ -63,41 +97,74 @@ export default class SearchableUserList extends React.Component {
}
}
doSearch() {
const term = this.refs.filter.value;
this.props.search(term);
if (term === '') {
this.setState({page: 0, search: false});
} else {
this.setState({search: true});
handleInput(e) {
this.props.onTermChange(e.target.value);
this.props.search(e.target.value);
}
renderCount(users) {
if (!users) {
return null;
}
const count = users.length;
const total = this.props.total;
const isSearch = Boolean(this.props.term);
let startCount;
let endCount;
if (isSearch) {
startCount = -1;
endCount = -1;
} else {
startCount = this.props.page * this.props.usersPerPage;
endCount = startCount + count;
}
if (this.props.renderCount) {
return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch);
}
if (this.props.total) {
if (isSearch) {
return (
<FormattedMessage
id='filtered_user_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
total
}}
/>
);
}
return (
<FormattedMessage
id='filtered_user_list.countTotalPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
startCount: startCount + 1,
endCount,
total
}}
/>
);
}
return null;
}
render() {
let nextButton;
let previousButton;
let usersToDisplay;
let count;
if (this.props.users == null) {
if (this.props.term || !this.props.users) {
usersToDisplay = this.props.users;
} else if (this.state.search || this.props.users == null) {
usersToDisplay = this.props.users;
if (this.props.total) {
count = (
<FormattedMessage
id='filtered_user_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count: usersToDisplay.length || 0,
total: this.props.total
}}
/>
);
}
} else {
const pageStart = this.state.page * this.props.usersPerPage;
} else if (!this.props.term) {
const pageStart = this.props.page * this.props.usersPerPage;
const pageEnd = pageStart + this.props.usersPerPage;
usersToDisplay = this.props.users.slice(pageStart, pageEnd);
@@ -116,7 +183,7 @@ export default class SearchableUserList extends React.Component {
);
}
if (this.state.page > 0) {
if (this.props.page > 0) {
previousButton = (
<button
className='btn btn-default filter-control filter-control__prev'
@@ -129,46 +196,38 @@ export default class SearchableUserList extends React.Component {
</button>
);
}
}
if (this.props.total) {
const startCount = this.state.page * this.props.usersPerPage;
const endCount = startCount + usersToDisplay.length;
count = (
<FormattedMessage
id='filtered_user_list.countTotalPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count: usersToDisplay.length,
startCount: startCount + 1,
endCount,
total: this.props.total
}}
let filterRow;
if (this.props.renderFilterRow) {
filterRow = this.props.renderFilterRow(this.handleInput);
} else {
filterRow = (
<div className='col-xs-12'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
value={this.props.term}
onInput={this.handleInput}
/>
);
}
</div>
);
}
return (
<div className='filtered-user-list'>
<div className='filter-row'>
<div className='col-xs-12'>
<input
ref='filter'
className='form-control filter-textbox'
placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')}
onInput={this.doSearch}
/>
</div>
{filterRow}
<div className='col-sm-12'>
<span className='member-count pull-left'>{count}</span>
<span className='member-count pull-left'>{this.renderCount(usersToDisplay)}</span>
</div>
</div>
<div
ref='userList'
className='more-modal__list'
>
<UserList
ref='userList'
users={usersToDisplay}
extraInfo={this.props.extraInfo}
actions={this.props.actions}
@@ -184,27 +243,3 @@ export default class SearchableUserList extends React.Component {
);
}
}
SearchableUserList.defaultProps = {
users: [],
usersPerPage: 50, //eslint-disable-line no-magic-numbers
extraInfo: {},
actions: [],
actionProps: {},
actionUserProps: {},
showTeamToggle: false,
focusOnMount: false
};
SearchableUserList.propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool.isRequired
};

Просмотреть файл

@@ -0,0 +1,72 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import SearchableUserList from './searchable_user_list.jsx';
export default class SearchableUserListContainer extends React.Component {
static propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
usersPerPage: React.PropTypes.number,
total: React.PropTypes.number,
extraInfo: React.PropTypes.object,
nextPage: React.PropTypes.func.isRequired,
search: React.PropTypes.func.isRequired,
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
actionUserProps: React.PropTypes.object,
focusOnMount: React.PropTypes.bool
};
constructor(props) {
super(props);
this.handleTermChange = this.handleTermChange.bind(this);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.search = this.search.bind(this);
this.state = {
term: '',
page: 0
};
}
handleTermChange(term) {
this.setState({term});
}
nextPage() {
this.setState({page: this.state.page + 1});
this.props.nextPage(this.state.page + 1);
}
previousPage() {
this.setState({page: this.state.page - 1});
}
search(term) {
this.props.search(term);
if (term !== '') {
this.setState({page: 0});
}
}
render() {
return (
<SearchableUserList
{...this.props}
nextPage={this.nextPage}
previousPage={this.previousPage}
search={this.search}
page={this.state.page}
term={this.state.term}
onTermChange={this.handleTermChange}
/>
);
}
}

Просмотреть файл

@@ -18,6 +18,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import ModalStore from 'stores/modal_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
@@ -637,7 +638,7 @@ export default class Sidebar extends React.Component {
// create elements for all 4 types of channels
const favoriteItems = this.state.favoriteChannels.
sort(Utils.sortTeamsByDisplayName).
sort(sortTeamsByDisplayName).
map((channel, index, arr) => {
if (channel.type === Constants.DM_CHANNEL) {
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);

Просмотреть файл

@@ -7,6 +7,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
import $ from 'jquery';
@@ -118,7 +119,7 @@ export default class TeamSidebar extends React.Component {
}
const teams = myTeams.
sort(Utils.sortTeamsByDisplayName).
sort(sortTeamsByDisplayName).
map((team) => {
return (
<TeamButton

Просмотреть файл

@@ -8,6 +8,18 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.scrollToTop = this.scrollToTop.bind(this);
}
scrollToTop() {
if (this.refs.container) {
this.refs.container.scrollTop = 0;
}
}
render() {
const users = this.props.users;
@@ -31,12 +43,12 @@ export default class UserList extends React.Component {
content = (
<div
key='no-users-found'
className='no-channel-message'
className='more-modal__placeholder-row'
>
<p className='primary-message'>
<p>
<FormattedMessage
id='user_list.notFound'
defaultMessage='No users found :('
defaultMessage='No users found'
/>
</p>
</div>
@@ -44,7 +56,7 @@ export default class UserList extends React.Component {
}
return (
<div>
<div ref='container'>
{content}
</div>
);

Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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='*'

Просмотреть файл

@@ -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;
}
}
}

Просмотреть файл

@@ -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
}
}

Просмотреть файл

@@ -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;
}
}

Просмотреть файл

@@ -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();

Просмотреть файл

@@ -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();

Просмотреть файл

@@ -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');
}
}
);
}

Просмотреть файл

@@ -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,

27
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});
}

Просмотреть файл

@@ -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) {