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

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

@@ -0,0 +1,245 @@
// 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';
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.focusSearchBar = this.focusSearchBar.bind(this);
this.handleInput = this.handleInput.bind(this);
this.renderCount = this.renderCount.bind(this);
this.nextTimeoutId = 0;
this.state = {
nextDisabled: false
};
}
componentDidMount() {
this.focusSearchBar();
}
componentDidUpdate(prevProps) {
if (this.props.page !== prevProps.page || this.props.term !== prevProps.term) {
this.refs.userList.scrollToTop();
}
this.focusSearchBar();
}
componentWillUnmount() {
clearTimeout(this.nextTimeoutId);
}
nextPage(e) {
e.preventDefault();
this.setState({nextDisabled: true});
this.nextTimeoutId = setTimeout(() => this.setState({nextDisabled: false}), NEXT_BUTTON_TIMEOUT);
this.props.nextPage();
}
previousPage(e) {
e.preventDefault();
this.props.previousPage();
}
focusSearchBar() {
if (this.props.focusOnMount) {
this.refs.filter.focus();
}
}
handleInput(e) {
this.props.onTermChange(e.target.value);
this.props.search(e.target.value);
}
renderCount(users) {
if (!users) {
return null;
}
const count = users.length;
const total = this.props.total;
const isSearch = Boolean(this.props.term);
let startCount;
let endCount;
if (isSearch) {
startCount = -1;
endCount = -1;
} else {
startCount = this.props.page * this.props.usersPerPage;
endCount = startCount + count;
}
if (this.props.renderCount) {
return this.props.renderCount(count, this.props.total, startCount, endCount, isSearch);
}
if (this.props.total) {
if (isSearch) {
return (
<FormattedMessage
id='filtered_user_list.countTotal'
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
total
}}
/>
);
}
return (
<FormattedMessage
id='filtered_user_list.countTotalPage'
defaultMessage='{startCount, number} - {endCount, number} {count, plural, =0 {0 members} one {member} other {members}} of {total} total'
values={{
count,
startCount: startCount + 1,
endCount,
total
}}
/>
);
}
return null;
}
render() {
let nextButton;
let previousButton;
let usersToDisplay;
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);
if (usersToDisplay.length >= this.props.usersPerPage) {
nextButton = (
<button
className='btn btn-default filter-control filter-control__next'
onClick={this.nextPage}
disabled={this.state.nextDisabled}
>
<FormattedMessage
id='filtered_user_list.next'
defaultMessage='Next'
/>
</button>
);
}
if (this.props.page > 0) {
previousButton = (
<button
className='btn btn-default filter-control filter-control__prev'
onClick={this.previousPage}
>
<FormattedMessage
id='filtered_user_list.prev'
defaultMessage='Previous'
/>
</button>
);
}
}
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'>
{filterRow}
<div className='col-sm-12'>
<span className='member-count pull-left'>{this.renderCount(usersToDisplay)}</span>
</div>
</div>
<div
className='more-modal__list'
>
<UserList
ref='userList'
users={usersToDisplay}
extraInfo={this.props.extraInfo}
actions={this.props.actions}
actionProps={this.props.actionProps}
actionUserProps={this.props.actionUserProps}
/>
</div>
<div className='filter-controls'>
{previousButton}
{nextButton}
</div>
</div>
);
}
}

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

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