diff options
Diffstat (limited to 'webapp/components')
60 files changed, 1146 insertions, 1150 deletions
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx index a1ec2885b..b4fd889bc 100644 --- a/webapp/components/admin_console/admin_navbar_dropdown.jsx +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; -import {sortTeamsByDisplayName} from 'utils/utils.jsx'; +import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {FormattedMessage} from 'react-intl'; diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx index b9883d7d8..30b9cbd11 100644 --- a/webapp/components/admin_console/admin_settings.jsx +++ b/webapp/components/admin_console/admin_settings.jsx @@ -112,7 +112,9 @@ export default class AdminSettings extends React.Component { render() { return ( <div className='wrapper--fixed'> - {this.renderTitle()} + <h3 className='admin-console-header'> + {this.renderTitle()} + </h3> <form className='form-horizontal' role='form' diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 2f299bdeb..73ec436f4 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -3,18 +3,12 @@ import $ from 'jquery'; import React from 'react'; +import {FormattedMessage} from 'react-intl'; -import AdminStore from 'stores/admin_store.jsx'; -import * as AsyncClient from 'utils/async_client.jsx'; import * as Utils from 'utils/utils.jsx'; -import AdminSidebarHeader from './admin_sidebar_header.jsx'; -import AdminSidebarTeam from './admin_sidebar_team.jsx'; -import {FormattedMessage} from 'react-intl'; -import {browserHistory} from 'react-router/es6'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import SelectTeamModal from './select_team_modal.jsx'; import AdminSidebarCategory from './admin_sidebar_category.jsx'; +import AdminSidebarHeader from './admin_sidebar_header.jsx'; import AdminSidebarSection from './admin_sidebar_section.jsx'; export default class AdminSidebar extends React.Component { @@ -27,84 +21,23 @@ export default class AdminSidebar extends React.Component { constructor(props) { super(props); - this.handleAllTeamsChange = this.handleAllTeamsChange.bind(this); - - this.removeTeam = this.removeTeam.bind(this); - - this.showTeamSelect = this.showTeamSelect.bind(this); - this.teamSelectedModal = this.teamSelectedModal.bind(this); - this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this); - this.updateTitle = this.updateTitle.bind(this); - - this.renderAddTeamButton = this.renderAddTeamButton.bind(this); - this.renderTeams = this.renderTeams.bind(this); - - this.state = { - teams: AdminStore.getAllTeams(), - selectedTeams: AdminStore.getSelectedTeams(), - showSelectModal: false - }; } componentDidMount() { - AdminStore.addAllTeamsChangeListener(this.handleAllTeamsChange); - AsyncClient.getAllTeams(); - this.updateTitle(); - } - componentDidUpdate() { if (!Utils.isMobile()) { $('.admin-sidebar .nav-pills__container').perfectScrollbar(); } } - componentWillUnmount() { - AdminStore.removeAllTeamsChangeListener(this.handleAllTeamsChange); - } - - handleAllTeamsChange() { - this.setState({ - teams: AdminStore.getAllTeams(), - selectedTeams: AdminStore.getSelectedTeams() - }); - } - - removeTeam(team) { - const selectedTeams = Object.assign({}, this.state.selectedTeams); - Reflect.deleteProperty(selectedTeams, team.id); - AdminStore.saveSelectedTeams(selectedTeams); - - this.handleAllTeamsChange(); - - if (this.context.router.isActive('/admin_console/team/' + team.id)) { - browserHistory.push('/admin_console'); + componentDidUpdate() { + if (!Utils.isMobile()) { + $('.admin-sidebar .nav-pills__container').perfectScrollbar(); } } - showTeamSelect(e) { - e.preventDefault(); - this.setState({showSelectModal: true}); - } - - teamSelectedModal(teamId) { - this.setState({ - showSelectModal: false - }); - - const selectedTeams = Object.assign({}, this.state.selectedTeams); - selectedTeams[teamId] = true; - - AdminStore.saveSelectedTeams(selectedTeams); - - this.handleAllTeamsChange(); - } - - teamSelectedModalDismissed() { - this.setState({showSelectModal: false}); - } - updateTitle() { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { @@ -114,79 +47,6 @@ export default class AdminSidebar extends React.Component { document.title = Utils.localizeMessage('sidebar_right_menu.console', 'System Console') + ' - ' + currentSiteName; } - renderAddTeamButton() { - const addTeamTooltip = ( - <Tooltip id='add-team-tooltip'> - <FormattedMessage - id='admin.sidebar.addTeamSidebar' - defaultMessage='Add team from sidebar menu' - /> - </Tooltip> - ); - - return ( - <span className='menu-icon--right'> - <OverlayTrigger - delayShow={1000} - placement='top' - overlay={addTeamTooltip} - > - <a - href='#' - onClick={this.showTeamSelect} - > - <i - className='fa fa-plus' - /> - </a> - </OverlayTrigger> - </span> - ); - } - - renderTeams() { - const teams = []; - let teamsArray = []; - - Reflect.ownKeys(this.state.selectedTeams).forEach((key) => { - if (this.state.teams[key]) { - teamsArray.push(this.state.teams[key]); - } - }); - - teamsArray = teamsArray.sort(Utils.sortTeamsByDisplayName); - - for (let i = 0; i < teamsArray.length; i++) { - const team = teamsArray[i]; - teams.push( - <AdminSidebarTeam - key={team.id} - team={team} - onRemoveTeam={this.removeTeam} - /> - ); - } - - return ( - <AdminSidebarCategory - parentLink='/admin_console' - icon='fa-user' - title={ - <FormattedMessage - id='admin.sidebar.teams' - defaultMessage='TEAMS ({count, number})' - values={{ - count: Object.keys(this.state.teams).length - }} - /> - } - action={this.renderAddTeamButton()} - > - {teams} - </AdminSidebarCategory> - ); - } - render() { let oauthSettings = null; let ldapSettings = null; @@ -422,6 +282,24 @@ export default class AdminSidebar extends React.Component { } /> <AdminSidebarSection + name='team_analytics' + title={ + <FormattedMessage + id='admin.sidebar.statistics' + defaultMessage='Team Statistics' + /> + } + /> + <AdminSidebarSection + name='users' + title={ + <FormattedMessage + id='admin.sidebar.users' + defaultMessage='Users' + /> + } + /> + <AdminSidebarSection name='logs' title={ <FormattedMessage @@ -760,16 +638,9 @@ export default class AdminSidebar extends React.Component { {metricsSettings} </AdminSidebarSection> </AdminSidebarCategory> - {this.renderTeams()} {otherCategory} </ul> </div> - <SelectTeamModal - teams={this.state.teams} - show={this.state.showSelectModal} - onModalSubmit={this.teamSelectedModal} - onModalDismissed={this.teamSelectedModalDismissed} - /> </div> ); } diff --git a/webapp/components/admin_console/admin_sidebar_team.jsx b/webapp/components/admin_console/admin_sidebar_team.jsx deleted file mode 100644 index b1df92491..000000000 --- a/webapp/components/admin_console/admin_sidebar_team.jsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import {FormattedMessage} from 'react-intl'; -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; -import AdminSidebarSection from './admin_sidebar_section.jsx'; - -export default class AdminSidebarTeam extends React.Component { - static get propTypes() { - return { - team: React.PropTypes.object.isRequired, - onRemoveTeam: React.PropTypes.func.isRequired, - parentLink: React.PropTypes.string - }; - } - - constructor(props) { - super(props); - - this.handleRemoveTeam = this.handleRemoveTeam.bind(this); - } - - handleRemoveTeam(e) { - e.preventDefault(); - - this.props.onRemoveTeam(this.props.team); - } - - render() { - const team = this.props.team; - - const removeTeamTooltip = ( - <Tooltip id='remove-team-tooltip'> - <FormattedMessage - id='admin.sidebar.rmTeamSidebar' - defaultMessage='Remove team from sidebar menu' - /> - </Tooltip> - ); - - const removeTeamButton = ( - <OverlayTrigger - delayShow={1000} - placement='top' - overlay={removeTeamTooltip} - > - <span - className='menu-icon--right menu__close' - onClick={this.handleRemoveTeam} - > - {'×'} - </span> - </OverlayTrigger> - ); - - return ( - <AdminSidebarSection - key={team.id} - name={'team/' + team.id} - parentLink={this.props.parentLink} - title={team.display_name} - action={removeTeamButton} - > - <AdminSidebarSection - name='users' - title={ - <FormattedMessage - id='admin.sidebar.users' - defaultMessage='- Users' - /> - } - /> - <AdminSidebarSection - name='analytics' - title={ - <FormattedMessage - id='admin.sidebar.statistics' - defaultMessage='- Team Statistics' - /> - } - /> - </AdminSidebarSection> - ); - } -} diff --git a/webapp/components/admin_console/audits.jsx b/webapp/components/admin_console/audits.jsx index 5e0e03607..47a7e8d89 100644 --- a/webapp/components/admin_console/audits.jsx +++ b/webapp/components/admin_console/audits.jsx @@ -76,7 +76,7 @@ export default class Audits extends React.Component { <ComplianceReports/> <div className='panel audit-panel'> - <h3> + <h3 className='admin-console-header'> <FormattedMessage id='admin.audits.title' defaultMessage='User Activity Logs' diff --git a/webapp/components/admin_console/cluster_settings.jsx b/webapp/components/admin_console/cluster_settings.jsx index bbd135e50..31634d0bd 100644 --- a/webapp/components/admin_console/cluster_settings.jsx +++ b/webapp/components/admin_console/cluster_settings.jsx @@ -51,12 +51,10 @@ export default class ClusterSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.advance.cluster' - defaultMessage='High Availability (Beta)' - /> - </h3> + <FormattedMessage + id='admin.advance.cluster' + defaultMessage='High Availability (Beta)' + /> ); } diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx index f9dc61c1d..e2df967d5 100644 --- a/webapp/components/admin_console/compliance_settings.jsx +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -38,12 +38,10 @@ export default class ComplianceSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.compliance.title' - defaultMessage='Compliance Settings' - /> - </h3> + <FormattedMessage + id='admin.compliance.title' + defaultMessage='Compliance Settings' + /> ); } diff --git a/webapp/components/admin_console/configuration_settings.jsx b/webapp/components/admin_console/configuration_settings.jsx index a5e5abe87..ec5606fa1 100644 --- a/webapp/components/admin_console/configuration_settings.jsx +++ b/webapp/components/admin_console/configuration_settings.jsx @@ -64,12 +64,10 @@ export default class ConfigurationSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.configuration' - defaultMessage='Configuration' - /> - </h3> + <FormattedMessage + id='admin.general.configuration' + defaultMessage='Configuration' + /> ); } diff --git a/webapp/components/admin_console/connection_settings.jsx b/webapp/components/admin_console/connection_settings.jsx index 8e030b207..b35f3acf7 100644 --- a/webapp/components/admin_console/connection_settings.jsx +++ b/webapp/components/admin_console/connection_settings.jsx @@ -36,12 +36,10 @@ export default class ConnectionSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.security.connection' - defaultMessage='Connections' - /> - </h3> + <FormattedMessage + id='admin.security.connection' + defaultMessage='Connections' + /> ); } diff --git a/webapp/components/admin_console/custom_brand_settings.jsx b/webapp/components/admin_console/custom_brand_settings.jsx index ee8e464da..48954ef78 100644 --- a/webapp/components/admin_console/custom_brand_settings.jsx +++ b/webapp/components/admin_console/custom_brand_settings.jsx @@ -44,12 +44,10 @@ export default class CustomBrandSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.customization.customBrand' - defaultMessage='Custom Branding' - /> - </h3> + <FormattedMessage + id='admin.customization.customBrand' + defaultMessage='Custom Branding' + /> ); } @@ -155,4 +153,4 @@ export default class CustomBrandSettings extends AdminSettings { </SettingsGroup> ); } -}
\ No newline at end of file +} diff --git a/webapp/components/admin_console/custom_emoji_settings.jsx b/webapp/components/admin_console/custom_emoji_settings.jsx index 90b70241d..c1457d7e9 100644 --- a/webapp/components/admin_console/custom_emoji_settings.jsx +++ b/webapp/components/admin_console/custom_emoji_settings.jsx @@ -39,12 +39,10 @@ export default class CustomEmojiSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.customization.customEmoji' - defaultMessage='Custom Emoji' - /> - </h3> + <FormattedMessage + id='admin.customization.customEmoji' + defaultMessage='Custom Emoji' + /> ); } diff --git a/webapp/components/admin_console/custom_integrations_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx index 6a4202d00..63015a061 100644 --- a/webapp/components/admin_console/custom_integrations_settings.jsx +++ b/webapp/components/admin_console/custom_integrations_settings.jsx @@ -43,12 +43,10 @@ export default class WebhookSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.integrations.custom' - defaultMessage='Custom Integrations' - /> - </h3> + <FormattedMessage + id='admin.integrations.custom' + defaultMessage='Custom Integrations' + /> ); } diff --git a/webapp/components/admin_console/database_settings.jsx b/webapp/components/admin_console/database_settings.jsx index 2cd4929ec..84adae29c 100644 --- a/webapp/components/admin_console/database_settings.jsx +++ b/webapp/components/admin_console/database_settings.jsx @@ -46,12 +46,10 @@ export default class DatabaseSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.database.title' - defaultMessage='Database Settings' - /> - </h3> + <FormattedMessage + id='admin.database.title' + defaultMessage='Database Settings' + /> ); } diff --git a/webapp/components/admin_console/developer_settings.jsx b/webapp/components/admin_console/developer_settings.jsx index 119b92a5a..3bcc2a19b 100644 --- a/webapp/components/admin_console/developer_settings.jsx +++ b/webapp/components/admin_console/developer_settings.jsx @@ -33,12 +33,10 @@ export default class DeveloperSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.developer.title' - defaultMessage='Developer Settings' - /> - </h3> + <FormattedMessage + id='admin.developer.title' + defaultMessage='Developer Settings' + /> ); } diff --git a/webapp/components/admin_console/email_authentication_settings.jsx b/webapp/components/admin_console/email_authentication_settings.jsx index cb7ef3419..177f36d64 100644 --- a/webapp/components/admin_console/email_authentication_settings.jsx +++ b/webapp/components/admin_console/email_authentication_settings.jsx @@ -35,12 +35,10 @@ export default class EmailAuthenticationSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.authentication.email' - defaultMessage='Email' - /> - </h3> + <FormattedMessage + id='admin.authentication.email' + defaultMessage='Email' + /> ); } diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx index 9dc02857b..6cf09f653 100644 --- a/webapp/components/admin_console/email_settings.jsx +++ b/webapp/components/admin_console/email_settings.jsx @@ -56,12 +56,10 @@ export default class EmailSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.notifications.email' - defaultMessage='Email' - /> - </h3> + <FormattedMessage + id='admin.notifications.email' + defaultMessage='Email' + /> ); } diff --git a/webapp/components/admin_console/external_service_settings.jsx b/webapp/components/admin_console/external_service_settings.jsx index 53fdbfb53..21fc6c106 100644 --- a/webapp/components/admin_console/external_service_settings.jsx +++ b/webapp/components/admin_console/external_service_settings.jsx @@ -32,12 +32,10 @@ export default class ExternalServiceSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.integrations.external' - defaultMessage='External Services' - /> - </h3> + <FormattedMessage + id='admin.integrations.external' + defaultMessage='External Services' + /> ); } diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx index ec3849b26..6ba2245b8 100644 --- a/webapp/components/admin_console/gitlab_settings.jsx +++ b/webapp/components/admin_console/gitlab_settings.jsx @@ -44,12 +44,10 @@ export default class GitLabSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.authentication.gitlab' - defaultMessage='GitLab' - /> - </h3> + <FormattedMessage + id='admin.authentication.gitlab' + defaultMessage='GitLab' + /> ); } diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx index 8e8e2868e..0249e3979 100644 --- a/webapp/components/admin_console/image_settings.jsx +++ b/webapp/components/admin_console/image_settings.jsx @@ -43,12 +43,10 @@ export default class ImageSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.files.images' - defaultMessage='Images' - /> - </h3> + <FormattedMessage + id='admin.files.images' + defaultMessage='Images' + /> ); } diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx index b774d34f3..50883ac22 100644 --- a/webapp/components/admin_console/ldap_settings.jsx +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -76,12 +76,10 @@ export default class LdapSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.authentication.ldap' - defaultMessage='AD/LDAP' - /> - </h3> + <FormattedMessage + id='admin.authentication.ldap' + defaultMessage='AD/LDAP' + /> ); } diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx index 3108dd60b..b0f85f43d 100644 --- a/webapp/components/admin_console/legal_and_support_settings.jsx +++ b/webapp/components/admin_console/legal_and_support_settings.jsx @@ -41,12 +41,10 @@ export default class LegalAndSupportSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.customization.support' - defaultMessage='Legal and Support' - /> - </h3> + <FormattedMessage + id='admin.customization.support' + defaultMessage='Legal and Support' + /> ); } diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx index 6c14394b7..7e77f44b6 100644 --- a/webapp/components/admin_console/license_settings.jsx +++ b/webapp/components/admin_console/license_settings.jsx @@ -221,7 +221,7 @@ class LicenseSettings extends React.Component { return ( <div className='wrapper--fixed'> - <h3> + <h3 className='admin-console-header'> <FormattedMessage id='admin.license.title' defaultMessage='Edition and License' diff --git a/webapp/components/admin_console/link_previews_settings.jsx b/webapp/components/admin_console/link_previews_settings.jsx index aea8a56f1..f223ccc3e 100644 --- a/webapp/components/admin_console/link_previews_settings.jsx +++ b/webapp/components/admin_console/link_previews_settings.jsx @@ -31,12 +31,10 @@ export default class LinkPreviewsSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.customization.linkPreviews' - defaultMessage='Link Previews' - /> - </h3> + <FormattedMessage + id='admin.customization.linkPreviews' + defaultMessage='Link Previews' + /> ); } diff --git a/webapp/components/admin_console/localization_settings.jsx b/webapp/components/admin_console/localization_settings.jsx index 7868ca8eb..b3e8a7b65 100644 --- a/webapp/components/admin_console/localization_settings.jsx +++ b/webapp/components/admin_console/localization_settings.jsx @@ -52,12 +52,10 @@ export default class LocalizationSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.localization' - defaultMessage='Localization' - /> - </h3> + <FormattedMessage + id='admin.general.localization' + defaultMessage='Localization' + /> ); } diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx index 135369942..69dd4eda7 100644 --- a/webapp/components/admin_console/log_settings.jsx +++ b/webapp/components/admin_console/log_settings.jsx @@ -49,12 +49,10 @@ export default class LogSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.log' - defaultMessage='Logging' - /> - </h3> + <FormattedMessage + id='admin.general.log' + defaultMessage='Logging' + /> ); } diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx index 5846c91db..d2464b37f 100644 --- a/webapp/components/admin_console/logs.jsx +++ b/webapp/components/admin_console/logs.jsx @@ -85,7 +85,7 @@ export default class Logs extends React.Component { return ( <div className='panel'> - <h3> + <h3 className='admin-console-header'> <FormattedMessage id='admin.logs.title' defaultMessage='Server Logs' diff --git a/webapp/components/admin_console/metrics_settings.jsx b/webapp/components/admin_console/metrics_settings.jsx index 29fa028ec..607a21fb9 100644 --- a/webapp/components/admin_console/metrics_settings.jsx +++ b/webapp/components/admin_console/metrics_settings.jsx @@ -38,12 +38,10 @@ export default class MetricsSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.advance.metrics' - defaultMessage='Performance Monitoring' - /> - </h3> + <FormattedMessage + id='admin.advance.metrics' + defaultMessage='Performance Monitoring' + /> ); } diff --git a/webapp/components/admin_console/mfa_settings.jsx b/webapp/components/admin_console/mfa_settings.jsx index 5a7e0076f..7ae1f2e18 100644 --- a/webapp/components/admin_console/mfa_settings.jsx +++ b/webapp/components/admin_console/mfa_settings.jsx @@ -38,12 +38,10 @@ export default class MfaSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.mfa.title' - defaultMessage='Multi-factor Authentication' - /> - </h3> + <FormattedMessage + id='admin.mfa.title' + defaultMessage='Multi-factor Authentication' + /> ); } diff --git a/webapp/components/admin_console/native_app_link_settings.jsx b/webapp/components/admin_console/native_app_link_settings.jsx index 05d61a284..d932af645 100644 --- a/webapp/components/admin_console/native_app_link_settings.jsx +++ b/webapp/components/admin_console/native_app_link_settings.jsx @@ -35,12 +35,10 @@ export default class NativeAppLinkSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.customization.nativeAppLinks' - defaultMessage='Mattermost App Links' - /> - </h3> + <FormattedMessage + id='admin.customization.nativeAppLinks' + defaultMessage='Mattermost App Links' + /> ); } diff --git a/webapp/components/admin_console/oauth_settings.jsx b/webapp/components/admin_console/oauth_settings.jsx index 9a86abfa0..f5eac13eb 100644 --- a/webapp/components/admin_console/oauth_settings.jsx +++ b/webapp/components/admin_console/oauth_settings.jsx @@ -111,12 +111,10 @@ export default class OAuthSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.authentication.oauth' - defaultMessage='OAuth 2.0' - /> - </h3> + <FormattedMessage + id='admin.authentication.oauth' + defaultMessage='OAuth 2.0' + /> ); } diff --git a/webapp/components/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx index 43ec40904..edb9669e1 100644 --- a/webapp/components/admin_console/password_settings.jsx +++ b/webapp/components/admin_console/password_settings.jsx @@ -138,12 +138,10 @@ export default class PasswordSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.security.password' - defaultMessage='Password' - /> - </h3> + <FormattedMessage + id='admin.security.password' + defaultMessage='Password' + /> ); } diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx index 5d82fc69c..c8c145b8d 100644 --- a/webapp/components/admin_console/policy_settings.jsx +++ b/webapp/components/admin_console/policy_settings.jsx @@ -55,12 +55,10 @@ export default class PolicySettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.policy' - defaultMessage='Policy' - /> - </h3> + <FormattedMessage + id='admin.general.policy' + defaultMessage='Policy' + /> ); } diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx index 6da9e6c4f..518ec807e 100644 --- a/webapp/components/admin_console/privacy_settings.jsx +++ b/webapp/components/admin_console/privacy_settings.jsx @@ -33,12 +33,10 @@ export default class PrivacySettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.privacy' - defaultMessage='Privacy' - /> - </h3> + <FormattedMessage + id='admin.general.privacy' + defaultMessage='Privacy' + /> ); } diff --git a/webapp/components/admin_console/public_link_settings.jsx b/webapp/components/admin_console/public_link_settings.jsx index 9b93a6adc..592d607d1 100644 --- a/webapp/components/admin_console/public_link_settings.jsx +++ b/webapp/components/admin_console/public_link_settings.jsx @@ -34,12 +34,10 @@ export default class PublicLinkSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.security.public_links' - defaultMessage='Public Links' - /> - </h3> + <FormattedMessage + id='admin.security.public_links' + defaultMessage='Public Links' + /> ); } diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx index 73189cd8f..2fc63afe0 100644 --- a/webapp/components/admin_console/push_settings.jsx +++ b/webapp/components/admin_console/push_settings.jsx @@ -100,12 +100,10 @@ export default class PushSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.notifications.title' - defaultMessage='Notification Settings' - /> - </h3> + <FormattedMessage + id='admin.notifications.title' + defaultMessage='Notification Settings' + /> ); } diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx index 73e9a4131..9b0a8076f 100644 --- a/webapp/components/admin_console/rate_settings.jsx +++ b/webapp/components/admin_console/rate_settings.jsx @@ -44,12 +44,10 @@ export default class RateSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.rate.title' - defaultMessage='Rate Limit Settings' - /> - </h3> + <FormattedMessage + id='admin.rate.title' + defaultMessage='Rate Limit Settings' + /> ); } diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx index 1b9e5b37a..d01fc15f3 100644 --- a/webapp/components/admin_console/reset_password_modal.jsx +++ b/webapp/components/admin_console/reset_password_modal.jsx @@ -4,13 +4,24 @@ import * as Utils from 'utils/utils.jsx'; import {Modal} from 'react-bootstrap'; -import {injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import {adminResetPassword} from 'actions/admin_actions.jsx'; import React from 'react'; -class ResetPasswordModal extends React.Component { +export default class ResetPasswordModal extends React.Component { + static propTypes = { + user: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func + }; + + static defaultProps = { + show: false + }; + constructor(props) { super(props); @@ -150,18 +161,3 @@ class ResetPasswordModal extends React.Component { ); } } - -ResetPasswordModal.defaultProps = { - show: false -}; - -ResetPasswordModal.propTypes = { - intl: intlShape.isRequired, - user: React.PropTypes.object, - team: React.PropTypes.object, - show: React.PropTypes.bool.isRequired, - onModalSubmit: React.PropTypes.func, - onModalDismissed: React.PropTypes.func -}; - -export default injectIntl(ResetPasswordModal); diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx index 7b9ed38b8..6025abe28 100644 --- a/webapp/components/admin_console/saml_settings.jsx +++ b/webapp/components/admin_console/saml_settings.jsx @@ -130,12 +130,10 @@ export default class SamlSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.authentication.saml' - defaultMessage='SAML' - /> - </h3> + <FormattedMessage + id='admin.authentication.saml' + defaultMessage='SAML' + /> ); } diff --git a/webapp/components/admin_console/select_team_modal.jsx b/webapp/components/admin_console/select_team_modal.jsx deleted file mode 100644 index 68e20f852..000000000 --- a/webapp/components/admin_console/select_team_modal.jsx +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ReactDOM from 'react-dom'; -import {FormattedMessage} from 'react-intl'; -import {Modal} from 'react-bootstrap'; -import React from 'react'; - -import {sortTeamsByDisplayName} from 'utils/utils.jsx'; - -export default class SelectTeamModal extends React.Component { - constructor(props) { - super(props); - - this.doSubmit = this.doSubmit.bind(this); - this.doCancel = this.doCancel.bind(this); - } - - doSubmit(e) { - e.preventDefault(); - this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value); - } - doCancel() { - this.props.onModalDismissed(); - } - - render() { - if (this.props.teams == null) { - return <div/>; - } - - const options = []; - let teamsArray = []; - - Reflect.ownKeys(this.props.teams).forEach((key) => { - teamsArray.push(this.props.teams[key]); - }); - - teamsArray = teamsArray.sort(sortTeamsByDisplayName); - for (let i = 0; i < teamsArray.length; i++) { - const team = teamsArray[i]; - options.push( - <option - key={'opt_' + team.id} - value={team.id} - > - {team.display_name} - </option> - ); - } - - return ( - <Modal - show={this.props.show} - onHide={this.doCancel} - > - <Modal.Header closeButton={true}> - <Modal.Title> - <FormattedMessage - id='admin.select_team.selectTeam' - defaultMessage='Select Team' - /> - </Modal.Title> - </Modal.Header> - <form - role='form' - className='form-horizontal' - > - <Modal.Body> - <div className='form-group'> - <div className='col-sm-12'> - <select - ref='team' - size='10' - className='form-control' - > - {options} - </select> - </div> - </div> - </Modal.Body> - <Modal.Footer> - <button - type='button' - className='btn btn-default' - onClick={this.doCancel} - > - <FormattedMessage - id='admin.select_team.close' - defaultMessage='Close' - /> - </button> - <button - onClick={this.doSubmit} - type='submit' - className='btn btn-primary' - tabIndex='2' - > - <FormattedMessage - id='admin.select_team.select' - defaultMessage='Select' - /> - </button> - </Modal.Footer> - </form> - </Modal> - ); - } -} - -SelectTeamModal.defaultProps = { - show: false -}; - -SelectTeamModal.propTypes = { - teams: React.PropTypes.object, - show: React.PropTypes.bool.isRequired, - onModalSubmit: React.PropTypes.func, - onModalDismissed: React.PropTypes.func -}; diff --git a/webapp/components/admin_console/session_settings.jsx b/webapp/components/admin_console/session_settings.jsx index 9624dea18..b238da90f 100644 --- a/webapp/components/admin_console/session_settings.jsx +++ b/webapp/components/admin_console/session_settings.jsx @@ -39,12 +39,10 @@ export default class SessionSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.security.session' - defaultMessage='Sessions' - /> - </h3> + <FormattedMessage + id='admin.security.session' + defaultMessage='Sessions' + /> ); } diff --git a/webapp/components/admin_console/signup_settings.jsx b/webapp/components/admin_console/signup_settings.jsx index 0c884f486..b75b7591a 100644 --- a/webapp/components/admin_console/signup_settings.jsx +++ b/webapp/components/admin_console/signup_settings.jsx @@ -36,12 +36,10 @@ export default class SignupSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.security.signup' - defaultMessage='Signup' - /> - </h3> + <FormattedMessage + id='admin.security.signup' + defaultMessage='Signup' + /> ); } diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx index 381206bf0..41d38d1ca 100644 --- a/webapp/components/admin_console/storage_settings.jsx +++ b/webapp/components/admin_console/storage_settings.jsx @@ -52,12 +52,10 @@ export default class StorageSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.files.storage' - defaultMessage='Storage' - /> - </h3> + <FormattedMessage + id='admin.files.storage' + defaultMessage='Storage' + /> ); } diff --git a/webapp/components/admin_console/system_users/system_users.jsx b/webapp/components/admin_console/system_users/system_users.jsx new file mode 100644 index 000000000..a311aebb7 --- /dev/null +++ b/webapp/components/admin_console/system_users/system_users.jsx @@ -0,0 +1,370 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import { + loadProfiles, + loadProfilesAndTeamMembers, + loadProfilesWithoutTeam, + searchUsers +} from 'actions/user_actions.jsx'; + +import AdminStore from 'stores/admin_store.jsx'; +import AnalyticsStore from 'stores/analytics_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {getAllTeams, getStandardAnalytics, getTeamStats, getUser} from 'utils/async_client.jsx'; +import {Constants, StatTypes, UserSearchOptions} from 'utils/constants.jsx'; +import {convertTeamMapToList} from 'utils/team_utils.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import SystemUsersList from './system_users_list.jsx'; + +const ALL_USERS = ''; +const NO_TEAM = 'no_team'; + +const USER_ID_LENGTH = 26; +const USERS_PER_PAGE = 50; + +export default class SystemUsers extends React.Component { + constructor(props) { + super(props); + + this.updateTeamsFromStore = this.updateTeamsFromStore.bind(this); + this.updateTotalUsersFromStore = this.updateTotalUsersFromStore.bind(this); + this.updateUsersFromStore = this.updateUsersFromStore.bind(this); + + this.loadDataForTeam = this.loadDataForTeam.bind(this); + this.loadComplete = this.loadComplete.bind(this); + + this.handleTeamChange = this.handleTeamChange.bind(this); + this.handleTermChange = this.handleTermChange.bind(this); + this.nextPage = this.nextPage.bind(this); + + this.doSearch = this.doSearch.bind(this); + this.search = this.search.bind(this); + this.getUserById = this.getUserById.bind(this); + + this.renderFilterRow = this.renderFilterRow.bind(this); + + this.state = { + teams: convertTeamMapToList(AdminStore.getAllTeams()), + totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS], + users: UserStore.getProfileList(), + + teamId: ALL_USERS, + term: '', + loading: true, + searching: false + }; + } + + componentDidMount() { + AdminStore.addAllTeamsChangeListener(this.updateTeamsFromStore); + + AnalyticsStore.addChangeListener(this.updateTotalUsersFromStore); + TeamStore.addStatsChangeListener(this.updateTotalUsersFromStore); + + UserStore.addChangeListener(this.updateUsersFromStore); + UserStore.addInTeamChangeListener(this.updateUsersFromStore); + UserStore.addWithoutTeamChangeListener(this.updateUsersFromStore); + + this.loadDataForTeam(this.state.teamId); + getAllTeams(); + } + + componentWillUpdate(nextProps, nextState) { + const nextTeamId = nextState.teamId; + + if (this.state.teamId !== nextTeamId) { + this.updateTotalUsersFromStore(nextTeamId); + this.updateUsersFromStore(nextTeamId, nextState.term); + + this.loadDataForTeam(nextTeamId); + } + } + + componentWillUnmount() { + AdminStore.removeAllTeamsChangeListener(this.updateTeamsFromStore); + + AnalyticsStore.removeChangeListener(this.updateTotalUsersFromStore); + TeamStore.removeStatsChangeListener(this.updateTotalUsersFromStore); + + UserStore.removeChangeListener(this.updateUsersFromStore); + UserStore.removeInTeamChangeListener(this.updateUsersFromStore); + UserStore.removeWithoutTeamChangeListener(this.updateUsersFromStore); + } + + updateTeamsFromStore() { + this.setState({teams: convertTeamMapToList(AdminStore.getAllTeams())}); + } + + updateTotalUsersFromStore(teamId = this.state.teamId) { + if (teamId === ALL_USERS) { + this.setState({ + totalUsers: AnalyticsStore.getAllSystem()[StatTypes.TOTAL_USERS] + }); + } else if (teamId === NO_TEAM) { + this.setState({ + totalUsers: 0 + }); + } else { + this.setState({ + totalUsers: TeamStore.getStats(teamId).total_member_count + }); + } + } + + updateUsersFromStore(teamId = this.state.teamId, term = this.state.term) { + if (term) { + if (teamId === this.state.teamId) { + // Search results aren't in the store, so manually update the users in them + const users = [...this.state.users]; + + for (let i = 0; i < users.length; i++) { + const user = users[i]; + + if (UserStore.hasProfile(user.id)) { + users[i] = UserStore.getProfile(user.id); + } + } + + this.setState({ + users + }); + } else { + this.doSearch(teamId, term, true); + } + + return; + } + + if (teamId === ALL_USERS) { + this.setState({users: UserStore.getProfileList(false, true)}); + } else if (teamId === NO_TEAM) { + this.setState({users: UserStore.getProfileListWithoutTeam()}); + } else { + this.setState({users: UserStore.getProfileListInTeam(this.state.teamId)}); + } + } + + loadDataForTeam(teamId) { + if (teamId === ALL_USERS) { + loadProfiles(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete); + getStandardAnalytics(); + } else if (teamId === NO_TEAM) { + loadProfilesWithoutTeam(0, Constants.PROFILE_CHUNK_SIZE, this.loadComplete); + } else { + loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, teamId, this.loadComplete); + getTeamStats(teamId); + } + } + + loadComplete() { + this.setState({loading: false}); + } + + handleTeamChange(e) { + this.setState({teamId: e.target.value}); + } + + handleTermChange(term) { + this.setState({term}); + } + + nextPage(page) { + // Paging isn't supported while searching + + if (this.state.teamId === ALL_USERS) { + loadProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.loadComplete); + } else if (this.state.teamId === NO_TEAM) { + loadProfilesWithoutTeam(page + 1, USERS_PER_PAGE, this.loadComplete); + } else { + loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.state.teamId, this.loadComplete); + } + } + + search(term) { + if (term === '') { + this.updateUsersFromStore(this.state.teamId, term); + + this.setState({ + loading: false + }); + + this.searchTimeoutId = ''; + return; + } + + this.doSearch(this.state.teamId, term); + } + + doSearch(teamId, term, now = false) { + clearTimeout(this.searchTimeoutId); + + this.setState({ + loading: true, + users: [] + }); + + const options = { + [UserSearchOptions.ALLOW_INACTIVE]: true + }; + if (teamId === NO_TEAM) { + options[UserSearchOptions.WITHOUT_TEAM] = true; + } + + const searchTimeoutId = setTimeout( + () => { + searchUsers( + term, + teamId, + options, + (users) => { + if (searchTimeoutId !== this.searchTimeoutId) { + return; + } + + if (users.length > 0) { + this.setState({ + loading: false, + users + }); + } else if (term.length === USER_ID_LENGTH) { + // This term didn't match any users name, but it does look like it might be a user's ID + this.getUserById(term, searchTimeoutId); + } else { + this.setState({ + loading: false + }); + } + }, + () => { + this.setState({ + loading: false + }); + } + ); + }, + now ? 0 : Constants.SEARCH_TIMEOUT_MILLISECONDS + ); + + this.searchTimeoutId = searchTimeoutId; + } + + getUserById(id, searchTimeoutId) { + if (UserStore.hasProfile(id)) { + this.setState({ + loading: false, + users: [UserStore.getProfile(id)] + }); + + return; + } + + getUser( + id, + (user) => { + if (searchTimeoutId !== this.searchTimeoutId) { + return; + } + + this.setState({ + loading: false, + users: [user] + }); + }, + () => { + if (searchTimeoutId !== this.searchTimeoutId) { + return; + } + + this.setState({ + loading: false, + users: [] + }); + } + ); + } + + renderFilterRow(doSearch) { + const teams = this.state.teams.map((team) => { + return ( + <option + key={team.id} + value={team.id} + > + {team.display_name} + </option> + ); + }); + + return ( + <div className='system-users__filter-row'> + <div className='system-users__filter'> + <input + ref='filter' + className='form-control filter-textbox' + placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')} + onInput={doSearch} + /> + </div> + <label> + <span className='system-users__team-filter-label'> + <FormattedMessage + id='filtered_user_list.show' + defaultMessage='Filter:' + /> + </span> + <select + className='form-control system-users__team-filter' + onChange={this.handleTeamChange} + value={this.state.teamId} + > + <option value={ALL_USERS}>{Utils.localizeMessage('admin.system_users.allUsers', 'All Users')}</option> + <option value={NO_TEAM}>{Utils.localizeMessage('admin.system_users.noTeams', 'No Teams')}</option> + {teams} + </select> + </label> + </div> + ); + } + + render() { + let users = null; + if (!this.state.loading) { + users = this.state.users; + } + + return ( + <div className='wrapper--fixed'> + <h3 className='admin-console-header'> + <FormattedMessage + id='admin.system_users.title' + defaultMessage='{siteName} Users' + values={{ + siteName: global.mm_config.SiteName + }} + /> + </h3> + <div className='more-modal__list member-list-holder'> + <SystemUsersList + renderFilterRow={this.renderFilterRow} + search={this.search} + nextPage={this.nextPage} + users={users} + usersPerPage={USERS_PER_PAGE} + total={this.state.totalUsers} + teams={this.state.teams} + teamId={this.state.teamId} + term={this.state.term} + onTermChange={this.handleTermChange} + /> + </div> + </div> + ); + } +} diff --git a/webapp/components/admin_console/admin_team_members_dropdown.jsx b/webapp/components/admin_console/system_users/system_users_dropdown.jsx index 037d8c73f..6f18754a1 100644 --- a/webapp/components/admin_console/admin_team_members_dropdown.jsx +++ b/webapp/components/admin_console/system_users/system_users_dropdown.jsx @@ -1,38 +1,38 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ConfirmModal from '../confirm_modal.jsx'; +import ConfirmModal from 'components/confirm_modal.jsx'; -import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; import {updateUserRoles, updateActive} from 'actions/user_actions.jsx'; -import {updateTeamMemberRoles, removeUserFromTeam} from 'actions/team_actions.jsx'; import {adminResetMfa} from 'actions/admin_actions.jsx'; import {FormattedMessage} from 'react-intl'; import React from 'react'; -export default class AdminTeamMembersDropdown extends React.Component { +export default class SystemUsersDropdown extends React.Component { + static propTypes = { + user: React.PropTypes.object.isRequired, + doPasswordReset: React.PropTypes.func.isRequired + }; + constructor(props) { super(props); this.handleMakeMember = this.handleMakeMember.bind(this); - this.handleRemoveFromTeam = this.handleRemoveFromTeam.bind(this); this.handleMakeActive = this.handleMakeActive.bind(this); this.handleMakeNotActive = this.handleMakeNotActive.bind(this); - this.handleMakeTeamAdmin = this.handleMakeTeamAdmin.bind(this); this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this); this.handleResetPassword = this.handleResetPassword.bind(this); this.handleResetMfa = this.handleResetMfa.bind(this); this.handleDemoteSystemAdmin = this.handleDemoteSystemAdmin.bind(this); this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); this.handleDemoteCancel = this.handleDemoteCancel.bind(this); - this.doMakeMember = this.doMakeMember.bind(this); - this.doMakeTeamAdmin = this.doMakeTeamAdmin.bind(this); this.state = { serverError: null, @@ -51,16 +51,6 @@ export default class AdminTeamMembersDropdown extends React.Component { this.setState({serverError: err.message}); } ); - - updateTeamMemberRoles( - this.props.teamMember.team_id, - this.props.user.id, - 'team_user', - null, - (err) => { - this.setState({serverError: err.message}); - } - ); } handleMakeMember(e) { @@ -73,17 +63,6 @@ export default class AdminTeamMembersDropdown extends React.Component { } } - handleRemoveFromTeam() { - removeUserFromTeam( - this.props.teamMember.team_id, - this.props.user.id, - null, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - handleMakeActive(e) { e.preventDefault(); updateActive(this.props.user.id, true, null, @@ -102,28 +81,6 @@ export default class AdminTeamMembersDropdown extends React.Component { ); } - doMakeTeamAdmin() { - updateTeamMemberRoles( - this.props.teamMember.team_id, - this.props.user.id, - 'team_user team_admin', - null, - (err) => { - this.setState({serverError: err.message}); - } - ); - } - - handleMakeTeamAdmin(e) { - e.preventDefault(); - const me = UserStore.getCurrentUser(); - if (this.props.user.id === me.id && me.roles.includes('system_admin')) { - this.handleDemoteSystemAdmin(this.props.user, 'teamadmin'); - } else { - this.doMakeTeamAdmin(); - } - } - handleMakeSystemAdmin(e) { e.preventDefault(); @@ -174,8 +131,6 @@ export default class AdminTeamMembersDropdown extends React.Component { handleDemoteSubmit() { if (this.state.role === 'member') { this.doMakeMember(); - } else { - this.doMakeTeamAdmin(); } const teamUrl = TeamStore.getCurrentTeamUrl(); @@ -197,9 +152,8 @@ export default class AdminTeamMembersDropdown extends React.Component { ); } - const teamMember = this.props.teamMember; const user = this.props.user; - if (!user || !teamMember) { + if (!user) { return <div/>; } let currentRoles = ( @@ -209,15 +163,6 @@ export default class AdminTeamMembersDropdown extends React.Component { /> ); - if (teamMember.roles.length > 0 && Utils.isAdmin(teamMember.roles)) { - currentRoles = ( - <FormattedMessage - id='team_members_dropdown.teamAdmin' - defaultMessage='Team Admin' - /> - ); - } - if (user.roles.length > 0 && Utils.isSystemAdmin(user.roles)) { currentRoles = ( <FormattedMessage @@ -228,8 +173,7 @@ export default class AdminTeamMembersDropdown extends React.Component { } const me = UserStore.getCurrentUser(); - let showMakeMember = Utils.isAdmin(teamMember.roles) || Utils.isSystemAdmin(user.roles); - let showMakeAdmin = !Utils.isAdmin(teamMember.roles) && !Utils.isSystemAdmin(user.roles); + let showMakeMember = Utils.isSystemAdmin(user.roles); let showMakeSystemAdmin = !Utils.isSystemAdmin(user.roles); let showMakeActive = false; let showMakeNotActive = !Utils.isSystemAdmin(user.roles); @@ -244,7 +188,6 @@ export default class AdminTeamMembersDropdown extends React.Component { /> ); showMakeMember = false; - showMakeAdmin = false; showMakeSystemAdmin = false; showMakeActive = true; showMakeNotActive = false; @@ -273,24 +216,6 @@ export default class AdminTeamMembersDropdown extends React.Component { ); } - let makeAdmin = null; - if (showMakeAdmin) { - makeAdmin = ( - <li role='presentation'> - <a - role='menuitem' - href='#' - onClick={this.handleMakeTeamAdmin} - > - <FormattedMessage - id='admin.user_item.makeTeamAdmin' - defaultMessage='Make Team Admin' - /> - </a> - </li> - ); - } - let makeMember = null; if (showMakeMember) { makeMember = ( @@ -309,24 +234,6 @@ export default class AdminTeamMembersDropdown extends React.Component { ); } - let removeFromTeam = null; - if (this.props.user.id !== me.id) { - removeFromTeam = ( - <li role='presentation'> - <a - role='menuitem' - href='#' - onClick={this.handleRemoveFromTeam} - > - <FormattedMessage - id='team_members_dropdown.leave_team' - defaultMessage='Remove From Team' - /> - </a> - </li> - ); - } - let menuClass = ''; if (disableActivationToggle) { menuClass = 'disabled'; @@ -493,8 +400,6 @@ export default class AdminTeamMembersDropdown extends React.Component { className='dropdown-menu member-menu' role='menu' > - {removeFromTeam} - {makeAdmin} {makeMember} {makeActive} {makeNotActive} @@ -508,9 +413,3 @@ export default class AdminTeamMembersDropdown extends React.Component { ); } } - -AdminTeamMembersDropdown.propTypes = { - user: React.PropTypes.object.isRequired, - teamMember: React.PropTypes.object.isRequired, - doPasswordReset: React.PropTypes.func.isRequired -}; diff --git a/webapp/components/admin_console/system_users/system_users_list.jsx b/webapp/components/admin_console/system_users/system_users_list.jsx new file mode 100644 index 000000000..5d8837164 --- /dev/null +++ b/webapp/components/admin_console/system_users/system_users_list.jsx @@ -0,0 +1,232 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +import ResetPasswordModal from 'components/admin_console/reset_password_modal.jsx'; +import SearchableUserList from 'components/searchable_user_list/searchable_user_list.jsx'; + +import {getUser} from 'utils/async_client.jsx'; +import {Constants} from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import SystemUsersDropdown from './system_users_dropdown.jsx'; + +export default class SystemUsersList extends React.Component { + static propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.object), + usersPerPage: React.PropTypes.number, + total: React.PropTypes.number, + nextPage: React.PropTypes.func, + search: React.PropTypes.func.isRequired, + focusOnMount: React.PropTypes.bool, + renderFilterRow: React.PropTypes.func, + + teamId: React.PropTypes.string.isRequired, + term: React.PropTypes.string.isRequired, + onTermChange: React.PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + + this.nextPage = this.nextPage.bind(this); + this.previousPage = this.previousPage.bind(this); + this.search = this.search.bind(this); + + this.doPasswordReset = this.doPasswordReset.bind(this); + this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); + this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); + + this.state = { + page: 0, + + showPasswordModal: false, + user: null + }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.teamId !== this.props.teamId) { + this.setState({page: 0}); + } + } + + nextPage() { + this.setState({page: this.state.page + 1}); + + this.props.nextPage(this.state.page + 1); + } + + previousPage() { + this.setState({page: this.state.page - 1}); + } + + search(term) { + this.props.search(term); + + if (term !== '') { + this.setState({page: 0}); + } + } + + doPasswordReset(user) { + this.setState({ + showPasswordModal: true, + user + }); + } + + doPasswordResetDismiss() { + this.setState({ + showPasswordModal: false, + user: null + }); + } + + doPasswordResetSubmit(user) { + getUser(user.id); + + this.setState({ + showPasswordModal: false, + user: null + }); + } + + getInfoForUser(user) { + const info = []; + + if (user.auth_service) { + let service; + if (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) { + service = user.auth_service.toUpperCase(); + } else { + service = Utils.toTitleCase(user.auth_service); + } + + info.push( + <FormattedHTMLMessage + key='admin.user_item.authServiceNotEmail' + id='admin.user_item.authServiceNotEmail' + defaultMessage='<strong>Sign-in Method:</strong> {service}' + values={{ + service + }} + /> + ); + } else { + info.push( + <FormattedHTMLMessage + key='admin.user_item.authServiceEmail' + id='admin.user_item.authServiceEmail' + defaultMessage='<strong>Sign-in Method:</strong> Email' + /> + ); + } + + const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && + global.window.mm_license.MFA === 'true' && + global.window.mm_config.EnableMultifactorAuthentication === 'true'; + if (mfaEnabled) { + info.push(', '); + + if (user.mfa_active) { + info.push( + <FormattedHTMLMessage + key='admin.user_item.mfaYes' + id='admin.user_item.mfaYes' + defaultMessage='<strong>MFA</strong>: Yes' + /> + ); + } else { + info.push( + <FormattedHTMLMessage + key='admin.user_item.mfaNo' + id='admin.user_item.mfaNo' + defaultMessage='<strong>MFA</strong>: No' + /> + ); + } + } + + return info; + } + + renderCount(count, total, startCount, endCount, isSearch) { + if (total) { + if (isSearch) { + return ( + <FormattedMessage + id='system_users_list.countSearch' + defaultMessage='{count, number} {count, plural, one {user} other {users}} of {total} total' + values={{ + count, + total + }} + /> + ); + } else if (startCount !== 0 || endCount !== total) { + return ( + <FormattedMessage + id='system_users_list.countPage' + defaultMessage='{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total} total' + values={{ + count, + startCount: startCount + 1, + endCount, + total + }} + /> + ); + } + + return ( + <FormattedMessage + id='system_users_list.count' + defaultMessage='{count, number} {count, plural, one {user} other {users}}' + values={{ + count + }} + /> + ); + } + + return null; + } + + render() { + const extraInfo = {}; + if (this.props.users) { + for (const user of this.props.users) { + extraInfo[user.id] = this.getInfoForUser(user); + } + } + + return ( + <div> + <SearchableUserList + {...this.props} + renderCount={this.renderCount} + extraInfo={extraInfo} + actions={[SystemUsersDropdown]} + actionProps={{ + doPasswordReset: this.doPasswordReset + }} + nextPage={this.nextPage} + previousPage={this.previousPage} + search={this.search} + page={this.state.page} + term={this.props.term} + onTermChange={this.props.onTermChange} + /> + <ResetPasswordModal + user={this.state.user} + show={this.state.showPasswordModal} + onModalSubmit={this.doPasswordResetSubmit} + onModalDismissed={this.doPasswordResetDismiss} + /> + </div> + ); + } +} diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx deleted file mode 100644 index 5bdaedf6e..000000000 --- a/webapp/components/admin_console/team_users.jsx +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SearchableUserList from 'components/searchable_user_list.jsx'; -import AdminTeamMembersDropdown from './admin_team_members_dropdown.jsx'; -import ResetPasswordModal from './reset_password_modal.jsx'; -import FormError from 'components/form_error.jsx'; - -import AdminStore from 'stores/admin_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import UserStore from 'stores/user_store.jsx'; - -import {searchUsers, loadProfilesAndTeamMembers, loadTeamMembersForProfilesList} from 'actions/user_actions.jsx'; -import {getTeamStats, getUser} from 'utils/async_client.jsx'; - -import {Constants, UserSearchOptions} from 'utils/constants.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import React from 'react'; -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -const USERS_PER_PAGE = 50; - -export default class UserList extends React.Component { - static get propTypes() { - return { - params: React.PropTypes.object.isRequired - }; - } - - constructor(props) { - super(props); - - this.onAllTeamsChange = this.onAllTeamsChange.bind(this); - this.onStatsChange = this.onStatsChange.bind(this); - this.onUsersChange = this.onUsersChange.bind(this); - this.onTeamChange = this.onTeamChange.bind(this); - - this.doPasswordReset = this.doPasswordReset.bind(this); - this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); - this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); - this.nextPage = this.nextPage.bind(this); - this.search = this.search.bind(this); - this.loadComplete = this.loadComplete.bind(this); - - this.searchTimeoutId = 0; - - const stats = TeamStore.getStats(this.props.params.team); - - this.state = { - team: AdminStore.getTeam(this.props.params.team), - users: [], - teamMembers: TeamStore.getMembersInTeam(this.props.params.team), - total: stats.total_member_count, - serverError: null, - showPasswordModal: false, - loading: true, - user: null - }; - } - - componentDidMount() { - AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange); - UserStore.addChangeListener(this.onUsersChange); - UserStore.addInTeamChangeListener(this.onUsersChange); - TeamStore.addChangeListener(this.onTeamChange); - TeamStore.addStatsChangeListener(this.onStatsChange); - - loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, this.props.params.team, this.loadComplete); - getTeamStats(this.props.params.team); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.team !== this.props.params.team) { - const stats = TeamStore.getStats(nextProps.params.team); - - this.setState({ - team: AdminStore.getTeam(nextProps.params.team), - users: [], - teamMembers: TeamStore.getMembersInTeam(nextProps.params.team), - total: stats.total_member_count, - serverError: null, - showPasswordModal: false, - loading: true, - user: null - }); - - loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, nextProps.params.team, this.loadComplete); - getTeamStats(nextProps.params.team); - } - } - - componentWillUnmount() { - AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange); - UserStore.removeChangeListener(this.onUsersChange); - UserStore.removeInTeamChangeListener(this.onUsersChange); - TeamStore.removeChangeListener(this.onTeamChange); - TeamStore.removeStatsChangeListener(this.onStatsChange); - } - - loadComplete() { - this.setState({loading: false}); - } - - onAllTeamsChange() { - this.setState({ - team: AdminStore.getTeam(this.props.params.team) - }); - } - - onStatsChange() { - const stats = TeamStore.getStats(this.props.params.team); - this.setState({total: stats.total_member_count}); - } - - onUsersChange() { - this.setState({users: UserStore.getProfileListInTeam(this.props.params.team)}); - } - - onTeamChange() { - this.setState({teamMembers: TeamStore.getMembersInTeam(this.props.params.team)}); - } - - nextPage(page) { - loadProfilesAndTeamMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE, this.props.params.team); - } - - doPasswordReset(user) { - this.setState({ - showPasswordModal: true, - user - }); - } - - doPasswordResetDismiss() { - this.setState({ - showPasswordModal: false, - user: null - }); - } - - doPasswordResetSubmit(user) { - getUser(user.id); - this.setState({ - showPasswordModal: false, - user: null - }); - } - - search(term) { - clearTimeout(this.searchTimeoutId); - - if (term === '') { - this.setState({search: false, users: UserStore.getProfileListInTeam(this.props.params.team)}); - this.searchTimeoutId = ''; - return; - } - - const options = {}; - options[UserSearchOptions.ALLOW_INACTIVE] = true; - - const searchTimeoutId = setTimeout( - () => { - searchUsers( - term, - this.props.params.team, - options, - (users) => { - if (searchTimeoutId !== this.searchTimeoutId) { - return; - } - - this.setState({loading: true, search: true, users}); - loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete); - } - ); - }, - Constants.SEARCH_TIMEOUT_MILLISECONDS - ); - - this.searchTimeoutId = searchTimeoutId; - } - - render() { - if (!this.state.team) { - return null; - } - - const teamMembers = this.state.teamMembers; - const users = this.state.users; - const actionUserProps = {}; - const extraInfo = {}; - const mfaEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true' && global.window.mm_config.EnableMultifactorAuthentication === 'true'; - - let usersToDisplay; - if (this.state.loading) { - usersToDisplay = null; - } else { - usersToDisplay = []; - - for (let i = 0; i < users.length; i++) { - const user = users[i]; - - if (teamMembers[user.id]) { - usersToDisplay.push(user); - actionUserProps[user.id] = { - teamMember: teamMembers[user.id] - }; - - const info = []; - - if (user.auth_service) { - const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service); - info.push( - <FormattedHTMLMessage - key='admin.user_item.authServiceNotEmail' - id='admin.user_item.authServiceNotEmail' - defaultMessage='<strong>Sign-in Method:</strong> {service}' - values={{ - service - }} - /> - ); - } else { - info.push( - <FormattedHTMLMessage - key='admin.user_item.authServiceEmail' - id='admin.user_item.authServiceEmail' - defaultMessage='<strong>Sign-in Method:</strong> Email' - /> - ); - } - - if (mfaEnabled) { - info.push(', '); - if (user.mfa_active) { - info.push( - <FormattedHTMLMessage - key='admin.user_item.mfaYes' - id='admin.user_item.mfaYes' - defaultMessage='<strong>MFA</strong>: Yes' - /> - ); - } else { - info.push( - <FormattedHTMLMessage - key='admin.user_item.mfaNo' - id='admin.user_item.mfaNo' - defaultMessage='<strong>MFA</strong>: No' - /> - ); - } - } - - extraInfo[user.id] = info; - } - } - } - - return ( - <div className='wrapper--fixed'> - <h3> - <FormattedMessage - id='admin.userList.title2' - defaultMessage='Users for {team} ({count})' - values={{ - team: this.state.team.name, - count: this.state.total - }} - /> - </h3> - <FormError error={this.state.serverError}/> - <div className='more-modal__list member-list-holder'> - <SearchableUserList - users={usersToDisplay} - usersPerPage={USERS_PER_PAGE} - total={this.state.total} - extraInfo={extraInfo} - nextPage={this.nextPage} - search={this.search} - actions={[AdminTeamMembersDropdown]} - actionProps={{ - doPasswordReset: this.doPasswordReset - }} - actionUserProps={actionUserProps} - /> - </div> - <ResetPasswordModal - user={this.state.user} - show={this.state.showPasswordModal} - team={this.state.team} - onModalSubmit={this.doPasswordResetSubmit} - onModalDismissed={this.doPasswordResetDismiss} - /> - </div> - ); - } -} diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx index 2cb5b4e51..6e83c01e3 100644 --- a/webapp/components/admin_console/users_and_teams_settings.jsx +++ b/webapp/components/admin_console/users_and_teams_settings.jsx @@ -51,12 +51,10 @@ export default class UsersAndTeamsSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.general.usersAndTeams' - defaultMessage='Users and Teams' - /> - </h3> + <FormattedMessage + id='admin.general.usersAndTeams' + defaultMessage='Users and Teams' + /> ); } diff --git a/webapp/components/admin_console/webrtc_settings.jsx b/webapp/components/admin_console/webrtc_settings.jsx index 63c17f598..e0238e7f3 100644 --- a/webapp/components/admin_console/webrtc_settings.jsx +++ b/webapp/components/admin_console/webrtc_settings.jsx @@ -50,12 +50,10 @@ export default class WebrtcSettings extends AdminSettings { renderTitle() { return ( - <h3> - <FormattedMessage - id='admin.integrations.webrtc' - defaultMessage='Mattermost WebRTC (Beta)' - /> - </h3> + <FormattedMessage + id='admin.integrations.webrtc' + defaultMessage='Mattermost WebRTC (Beta)' + /> ); } diff --git a/webapp/components/analytics/line_chart.jsx b/webapp/components/analytics/line_chart.jsx index aa603d819..5ae80f9e9 100644 --- a/webapp/components/analytics/line_chart.jsx +++ b/webapp/components/analytics/line_chart.jsx @@ -21,14 +21,33 @@ export default class LineChart extends React.Component { this.initChart(); } + componentWillUpdate(nextProps) { + const willHaveData = nextProps.data && nextProps.data.labels.length > 0; + const hasChart = Boolean(this.chart); + + if (!willHaveData && hasChart) { + // Clean up the rendered chart before we render and destroy its context + this.chart.destroy(); + this.chart = null; + } + } + componentDidUpdate(prevProps) { - if (!Utils.areObjectsEqual(prevProps.data, this.props.data) || !Utils.areObjectsEqual(prevProps.options, this.props.options)) { - this.initChart(true); + if (Utils.areObjectsEqual(prevProps.data, this.props.data) && Utils.areObjectsEqual(prevProps.options, this.props.options)) { + return; + } + + const hasData = this.props.data && this.props.data.labels.length > 0; + const hasChart = Boolean(this.chart); + + if (hasData) { + // Update the rendered chart or initialize it as necessary + this.initChart(hasChart); } } componentWillUnmount() { - if (this.chart && this.refs.canvas) { + if (this.chart) { this.chart.destroy(); } } @@ -37,9 +56,11 @@ export default class LineChart extends React.Component { if (!this.refs.canvas) { return; } + var el = ReactDOM.findDOMNode(this.refs.canvas); var ctx = el.getContext('2d'); - this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); //eslint-disable-line new-cap + this.chart = new Chart(ctx, {type: 'line', data: this.props.data, options: this.props.options || {}}); // eslint-disable-line new-cap + if (update) { this.chart.update(); } diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx index 5af055924..bd09b8b0b 100644 --- a/webapp/components/analytics/system_analytics.jsx +++ b/webapp/components/analytics/system_analytics.jsx @@ -409,7 +409,7 @@ class SystemAnalytics extends React.Component { return ( <div className='wrapper--fixed team_statistics'> - <h3> + <h3 className='admin-console-header'> <FormattedMessage id='analytics.system.title' defaultMessage='System Statistics' diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx index 66eb7e2db..135bab4b4 100644 --- a/webapp/components/analytics/team_analytics.jsx +++ b/webapp/components/analytics/team_analytics.jsx @@ -1,40 +1,43 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import React from 'react'; +import {FormattedDate, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + import Banner from 'components/admin_console/banner.jsx'; -import LineChart from './line_chart.jsx'; -import StatisticCount from './statistic_count.jsx'; -import TableChart from './table_chart.jsx'; +import LoadingScreen from 'components/loading_screen.jsx'; import AdminStore from 'stores/admin_store.jsx'; import AnalyticsStore from 'stores/analytics_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; -import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import Constants from 'utils/constants.jsx'; -const StatTypes = Constants.StatTypes; +import {StatTypes} from 'utils/constants.jsx'; +import {convertTeamMapToList} from 'utils/team_utils.jsx'; +import LineChart from './line_chart.jsx'; +import StatisticCount from './statistic_count.jsx'; +import TableChart from './table_chart.jsx'; import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx'; -import {FormattedMessage, FormattedDate, FormattedHTMLMessage} from 'react-intl'; -import React from 'react'; +const LAST_ANALYTICS_TEAM = 'last_analytics_team'; export default class TeamAnalytics extends React.Component { - static get propTypes() { - return { - params: React.PropTypes.object.isRequired - }; - } - constructor(props) { super(props); this.onChange = this.onChange.bind(this); this.onAllTeamsChange = this.onAllTeamsChange.bind(this); + this.handleTeamChange = this.handleTeamChange.bind(this); + + const teams = convertTeamMapToList(AdminStore.getAllTeams()); + const teamId = BrowserStore.getGlobalItem(LAST_ANALYTICS_TEAM, teams.length > 0 ? teams[0].id : ''); this.state = { - team: AdminStore.getTeam(this.props.params.team), - stats: AnalyticsStore.getAllTeam(this.props.params.team) + teams, + teamId, + team: AdminStore.getTeam(teamId), + stats: AnalyticsStore.getAllTeam(teamId) }; } @@ -42,7 +45,19 @@ export default class TeamAnalytics extends React.Component { AnalyticsStore.addChangeListener(this.onChange); AdminStore.addAllTeamsChangeListener(this.onAllTeamsChange); - this.getData(this.props.params.team); + if (this.state.teamId !== '') { + this.getData(this.state.teamId); + } + + if (this.state.teams.length === 0) { + AsyncClient.getAllTeams(); + } + } + + componentWillUpdate(nextProps, nextState) { + if (nextState.teamId !== this.state.teamId) { + this.getData(nextState.teamId); + } } getData(id) { @@ -57,40 +72,60 @@ export default class TeamAnalytics extends React.Component { AdminStore.removeAllTeamsChangeListener(this.onAllTeamsChange); } - componentWillReceiveProps(nextProps) { - this.getData(nextProps.params.team); + onChange() { this.setState({ - stats: AnalyticsStore.getAllTeam(nextProps.params.team) + stats: AnalyticsStore.getAllTeam(this.state.teamId) }); } - shouldComponentUpdate(nextProps, nextState) { - if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) { - return true; - } - - if (!Utils.areObjectsEqual(nextProps.params.team, this.props.params.team)) { - return true; + onAllTeamsChange() { + const teams = convertTeamMapToList(AdminStore.getAllTeams()); + + if (teams.length > 0) { + if (this.state.teamId) { + this.setState({ + team: AdminStore.getTeam(this.state.teamId) + }); + } else { + this.setState({ + teamId: teams[0].id, + team: teams[0] + }); + } } - return false; - } - - onChange() { this.setState({ - stats: AnalyticsStore.getAllTeam(this.props.params.team) + teams }); } - onAllTeamsChange() { + handleTeamChange(e) { + const teamId = e.target.value; + this.setState({ - team: AdminStore.getTeam(this.props.params.team) + teamId, + team: AdminStore.getTeam(teamId) }); + + BrowserStore.setGlobalItem(LAST_ANALYTICS_TEAM, teamId); } render() { - if (!this.state.team || !this.state.stats) { - return null; + if (this.state.teams.length === 0 || !this.state.team || !this.state.stats) { + return <LoadingScreen/>; + } + + if (this.state.teamId === '') { + return ( + <Banner + description={ + <FormattedMessage + id='analytics.team.noTeams' + defaultMessage='There are no teams on this server for which to view statistics.' + /> + } + /> + ); } const stats = this.state.stats; @@ -129,6 +164,7 @@ export default class TeamAnalytics extends React.Component { postTotalGraph = ( <div className='row'> <LineChart + key={this.state.team.id} title={ <FormattedMessage id='analytics.team.totalPosts' @@ -150,6 +186,7 @@ export default class TeamAnalytics extends React.Component { userActiveGraph = ( <div className='row'> <LineChart + key={this.state.team.id} title={ <FormattedMessage id='analytics.team.activeUsers' @@ -172,17 +209,41 @@ export default class TeamAnalytics extends React.Component { const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]); const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]); + const teams = this.state.teams.map((team) => { + return ( + <option + key={team.id} + value={team.id} + > + {team.display_name} + </option> + ); + }); + return ( <div className='wrapper--fixed team_statistics'> - <h3> - <FormattedMessage - id='analytics.team.title' - defaultMessage='Team Statistics for {team}' - values={{ - team: this.state.team.name - }} - /> - </h3> + <div className='admin-console-header team-statistics__header-row'> + <div className='team-statistics__header'> + <h3> + <FormattedMessage + id='analytics.team.title' + defaultMessage='Team Statistics for {team}' + values={{ + team: this.state.team.display_name + }} + /> + </h3> + </div> + <div className='team-statistics__team-filter'> + <select + className='form-control team-statistics__team-filter__dropdown' + onChange={this.handleTeamChange} + value={this.state.teamId} + > + {teams} + </select> + </div> + </div> {banner} <div className='row'> <StatisticCount diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx index 2f1a10a75..dfc083f24 100644 --- a/webapp/components/channel_invite_modal.jsx +++ b/webapp/components/channel_invite_modal.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import ChannelInviteButton from './channel_invite_button.jsx'; -import SearchableUserList from './searchable_user_list.jsx'; +import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; import LoadingScreen from './loading_screen.jsx'; import ChannelStore from 'stores/channel_store.jsx'; diff --git a/webapp/components/member_list_channel.jsx b/webapp/components/member_list_channel.jsx index d9d28bcd0..c23be2836 100644 --- a/webapp/components/member_list_channel.jsx +++ b/webapp/components/member_list_channel.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx'; -import SearchableUserList from 'components/searchable_user_list.jsx'; +import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx index e06d61b0a..fce6e1927 100644 --- a/webapp/components/member_list_team.jsx +++ b/webapp/components/member_list_team.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import SearchableUserList from 'components/searchable_user_list.jsx'; +import SearchableUserList from 'components/searchable_user_list/searchable_user_list_container.jsx'; import TeamMembersDropdown from 'components/team_members_dropdown.jsx'; import UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 9ce4ed882..1f1f99aba 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -41,7 +41,6 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx'; import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx'; import InviteMemberModal from 'components/invite_member_modal.jsx'; import LeaveTeamModal from 'components/leave_team_modal.jsx'; -import SelectTeamModal from 'components/admin_console/select_team_modal.jsx'; import iNoBounce from 'inobounce'; import * as UserAgent from 'utils/user_agent.jsx'; @@ -212,7 +211,6 @@ export default class NeedsTeam extends React.Component { <EditPostModal/> <DeletePostModal/> <RemovedFromChannelModal/> - <SelectTeamModal/> </div> </div> ); diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list/searchable_user_list.jsx index ab3f9ee9b..91e0205b0 100644 --- a/webapp/components/searchable_user_list.jsx +++ b/webapp/components/searchable_user_list/searchable_user_list.jsx @@ -1,31 +1,61 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + import UserList from 'components/user_list.jsx'; import * as Utils from 'utils/utils.jsx'; -import $ from 'jquery'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import {FormattedMessage} from 'react-intl'; - const NEXT_BUTTON_TIMEOUT = 500; export default class SearchableUserList extends React.Component { + static propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.object), + usersPerPage: React.PropTypes.number, + total: React.PropTypes.number, + extraInfo: React.PropTypes.object, + nextPage: React.PropTypes.func.isRequired, + previousPage: React.PropTypes.func.isRequired, + search: React.PropTypes.func.isRequired, + actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object, + actionUserProps: React.PropTypes.object, + focusOnMount: React.PropTypes.bool, + renderCount: React.PropTypes.func, + renderFilterRow: React.PropTypes.func, + + page: React.PropTypes.number.isRequired, + term: React.PropTypes.string.isRequired, + onTermChange: React.PropTypes.func.isRequired + }; + + static defaultProps = { + users: [], + usersPerPage: 50, // eslint-disable-line no-magic-numbers + extraInfo: {}, + actions: [], + actionProps: {}, + actionUserProps: {}, + showTeamToggle: false, + focusOnMount: false + }; + constructor(props) { super(props); this.nextPage = this.nextPage.bind(this); this.previousPage = this.previousPage.bind(this); - this.doSearch = this.doSearch.bind(this); this.focusSearchBar = this.focusSearchBar.bind(this); + this.handleInput = this.handleInput.bind(this); + + this.renderCount = this.renderCount.bind(this); + this.nextTimeoutId = 0; this.state = { - page: 0, - search: false, nextDisabled: false }; } @@ -34,10 +64,11 @@ export default class SearchableUserList extends React.Component { this.focusSearchBar(); } - componentDidUpdate(prevProps, prevState) { - if (this.state.page !== prevState.page) { - $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0); + componentDidUpdate(prevProps) { + if (this.props.page !== prevProps.page || this.props.term !== prevProps.term) { + this.refs.userList.scrollToTop(); } + this.focusSearchBar(); } @@ -47,14 +78,17 @@ export default class SearchableUserList extends React.Component { nextPage(e) { e.preventDefault(); - this.setState({page: this.state.page + 1, nextDisabled: true}); + + this.setState({nextDisabled: true}); this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT); - this.props.nextPage(this.state.page + 1); + + this.props.nextPage(); } previousPage(e) { e.preventDefault(); - this.setState({page: this.state.page - 1}); + + this.props.previousPage(); } focusSearchBar() { @@ -63,41 +97,74 @@ export default class SearchableUserList extends React.Component { } } - doSearch() { - const term = this.refs.filter.value; - this.props.search(term); - if (term === '') { - this.setState({page: 0, search: false}); - } else { - this.setState({search: true}); - } + handleInput(e) { + this.props.onTermChange(e.target.value); + this.props.search(e.target.value); } - render() { - let nextButton; - let previousButton; - let usersToDisplay; - let count; + renderCount(users) { + if (!users) { + return null; + } - if (this.props.users == null) { - usersToDisplay = this.props.users; - } else if (this.state.search || this.props.users == null) { - usersToDisplay = this.props.users; + const count = users.length; + const total = this.props.total; + const isSearch = Boolean(this.props.term); + + let startCount; + let endCount; + if (isSearch) { + startCount = -1; + endCount = -1; + } else { + startCount = this.props.page * this.props.usersPerPage; + endCount = startCount + count; + } - if (this.props.total) { - count = ( + if (this.props.renderCount) { + return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch); + } + + if (this.props.total) { + if (isSearch) { + return ( <FormattedMessage id='filtered_user_list.countTotal' defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total' values={{ - count: usersToDisplay.length || 0, - total: this.props.total + count, + total }} /> ); } - } else { - const pageStart = this.state.page * this.props.usersPerPage; + + return ( + <FormattedMessage + id='filtered_user_list.countTotalPage' + defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total' + values={{ + count, + startCount: startCount + 1, + endCount, + total + }} + /> + ); + } + + return null; + } + + render() { + let nextButton; + let previousButton; + let usersToDisplay; + + if (this.props.term || !this.props.users) { + usersToDisplay = this.props.users; + } else if (!this.props.term) { + const pageStart = this.props.page * this.props.usersPerPage; const pageEnd = pageStart + this.props.usersPerPage; usersToDisplay = this.props.users.slice(pageStart, pageEnd); @@ -116,7 +183,7 @@ export default class SearchableUserList extends React.Component { ); } - if (this.state.page > 0) { + if (this.props.page > 0) { previousButton = ( <button className='btn btn-default filter-control filter-control__prev' @@ -129,46 +196,38 @@ export default class SearchableUserList extends React.Component { </button> ); } + } - if (this.props.total) { - const startCount = this.state.page * this.props.usersPerPage; - const endCount = startCount + usersToDisplay.length; - - count = ( - <FormattedMessage - id='filtered_user_list.countTotalPage' - defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total' - values={{ - count: usersToDisplay.length, - startCount: startCount + 1, - endCount, - total: this.props.total - }} + let filterRow; + if (this.props.renderFilterRow) { + filterRow = this.props.renderFilterRow(this.handleInput); + } else { + filterRow = ( + <div className='col-xs-12'> + <input + ref='filter' + className='form-control filter-textbox' + placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')} + value={this.props.term} + onInput={this.handleInput} /> - ); - } + </div> + ); } return ( <div className='filtered-user-list'> <div className='filter-row'> - <div className='col-xs-12'> - <input - ref='filter' - className='form-control filter-textbox' - placeholder={Utils.localizeMessage('filtered_user_list.search', 'Search users')} - onInput={this.doSearch} - /> - </div> + {filterRow} <div className='col-sm-12'> - <span className='member-count pull-left'>{count}</span> + <span className='member-count pull-left'>{this.renderCount(usersToDisplay)}</span> </div> </div> <div - ref='userList' className='more-modal__list' > <UserList + ref='userList' users={usersToDisplay} extraInfo={this.props.extraInfo} actions={this.props.actions} @@ -184,27 +243,3 @@ export default class SearchableUserList extends React.Component { ); } } - -SearchableUserList.defaultProps = { - users: [], - usersPerPage: 50, //eslint-disable-line no-magic-numbers - extraInfo: {}, - actions: [], - actionProps: {}, - actionUserProps: {}, - showTeamToggle: false, - focusOnMount: false -}; - -SearchableUserList.propTypes = { - users: React.PropTypes.arrayOf(React.PropTypes.object), - usersPerPage: React.PropTypes.number, - total: React.PropTypes.number, - extraInfo: React.PropTypes.object, - nextPage: React.PropTypes.func.isRequired, - search: React.PropTypes.func.isRequired, - actions: React.PropTypes.arrayOf(React.PropTypes.func), - actionProps: React.PropTypes.object, - actionUserProps: React.PropTypes.object, - focusOnMount: React.PropTypes.bool.isRequired -}; diff --git a/webapp/components/searchable_user_list/searchable_user_list_container.jsx b/webapp/components/searchable_user_list/searchable_user_list_container.jsx new file mode 100644 index 000000000..816dec062 --- /dev/null +++ b/webapp/components/searchable_user_list/searchable_user_list_container.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import SearchableUserList from './searchable_user_list.jsx'; + +export default class SearchableUserListContainer extends React.Component { + static propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.object), + usersPerPage: React.PropTypes.number, + total: React.PropTypes.number, + extraInfo: React.PropTypes.object, + nextPage: React.PropTypes.func.isRequired, + search: React.PropTypes.func.isRequired, + actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object, + actionUserProps: React.PropTypes.object, + focusOnMount: React.PropTypes.bool + }; + + constructor(props) { + super(props); + + this.handleTermChange = this.handleTermChange.bind(this); + + this.nextPage = this.nextPage.bind(this); + this.previousPage = this.previousPage.bind(this); + this.search = this.search.bind(this); + + this.state = { + term: '', + page: 0 + }; + } + + handleTermChange(term) { + this.setState({term}); + } + + nextPage() { + this.setState({page: this.state.page + 1}); + + this.props.nextPage(this.state.page + 1); + } + + previousPage() { + this.setState({page: this.state.page - 1}); + } + + search(term) { + this.props.search(term); + + if (term !== '') { + this.setState({page: 0}); + } + } + + render() { + return ( + <SearchableUserList + {...this.props} + nextPage={this.nextPage} + previousPage={this.previousPage} + search={this.search} + page={this.state.page} + term={this.state.term} + onTermChange={this.handleTermChange} + /> + ); + } +} diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index b6a5b0ba8..940f0b0a6 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -18,6 +18,7 @@ import PreferenceStore from 'stores/preference_store.jsx'; import ModalStore from 'stores/modal_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; +import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; @@ -637,7 +638,7 @@ export default class Sidebar extends React.Component { // create elements for all 4 types of channels const favoriteItems = this.state.favoriteChannels. - sort(Utils.sortTeamsByDisplayName). + sort(sortTeamsByDisplayName). map((channel, index, arr) => { if (channel.type === Constants.DM_CHANNEL) { return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); diff --git a/webapp/components/team_sidebar/team_sidebar_controller.jsx b/webapp/components/team_sidebar/team_sidebar_controller.jsx index 49635455f..9863b5e32 100644 --- a/webapp/components/team_sidebar/team_sidebar_controller.jsx +++ b/webapp/components/team_sidebar/team_sidebar_controller.jsx @@ -7,6 +7,7 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; +import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; import * as Utils from 'utils/utils.jsx'; import $ from 'jquery'; @@ -118,7 +119,7 @@ export default class TeamSidebar extends React.Component { } const teams = myTeams. - sort(Utils.sortTeamsByDisplayName). + sort(sortTeamsByDisplayName). map((team) => { return ( <TeamButton diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx index d34404c89..c521b95cc 100644 --- a/webapp/components/user_list.jsx +++ b/webapp/components/user_list.jsx @@ -8,6 +8,18 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; export default class UserList extends React.Component { + constructor(props) { + super(props); + + this.scrollToTop = this.scrollToTop.bind(this); + } + + scrollToTop() { + if (this.refs.container) { + this.refs.container.scrollTop = 0; + } + } + render() { const users = this.props.users; @@ -31,12 +43,12 @@ export default class UserList extends React.Component { content = ( <div key='no-users-found' - className='no-channel-message' + className='more-modal__placeholder-row' > - <p className='primary-message'> + <p> <FormattedMessage id='user_list.notFound' - defaultMessage='No users found :(' + defaultMessage='No users found' /> </p> </div> @@ -44,7 +56,7 @@ export default class UserList extends React.Component { } return ( - <div> + <div ref='container'> {content} </div> ); |