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 удалений

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

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