Commit 3713f043 authored by Jesús Espino's avatar Jesús Espino Committed by Martin Kraft
Browse files

MM:8789: Adding admin panel for custom team schemes (#1251)

* MM-8789: Adding admin panel for schemes

* MM-8789: Adding tests
parent ce8b39b6
......@@ -247,6 +247,22 @@ export default class AdminConsole extends React.Component {
schema: AdminDefinition.settings.permissions.systemScheme.schema,
}}
/>
<SCRoute
path={`${props.match.url}/team-override-scheme/:scheme_id`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.permissions.teamScheme.schema,
}}
/>
<SCRoute
path={`${props.match.url}/team-override-scheme`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.permissions.teamScheme.schema,
}}
/>
</Switch>
)}
/>
......
......@@ -15,6 +15,7 @@ import Audits from './audits';
import LicenseSettings from './license_settings';
import PermissionSchemesSettings from './permission_schemes_settings';
import PermissionSystemSchemeSettings from './permission_schemes_settings/permission_system_scheme_settings';
import PermissionTeamSchemeSettings from './permission_schemes_settings/permission_team_scheme_settings';
import * as DefinitionConstants from './admin_definition_constants';
......@@ -546,6 +547,12 @@ export default {
component: PermissionSystemSchemeSettings,
},
},
teamScheme: {
schema: {
id: 'PermissionSystemScheme',
component: PermissionTeamSchemeSettings,
},
},
},
authentication: {
email: {
......
......@@ -15,7 +15,7 @@ const PHASE_2_MIGRATION_IMCOMPLETE_STATUS_CODE = 501;
export default class PermissionSchemesSettings extends React.PureComponent {
static propTypes = {
schemes: PropTypes.array.isRequired,
schemes: PropTypes.object.isRequired,
jobsAreEnabled: PropTypes.bool,
clusterIsEnabled: PropTypes.bool,
actions: PropTypes.shape({
......@@ -35,7 +35,7 @@ export default class PermissionSchemesSettings extends React.PureComponent {
}
static defaultProps = {
schemes: [],
schemes: {},
};
async componentWillMount() {
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {loadRolesIfNeeded, editRole, getRole as loadRole} from 'mattermost-redux/actions/roles';
import {getRoles, getRolesById} from 'mattermost-redux/selectors/entities/roles';
import {getScheme, makeGetSchemeTeams} from 'mattermost-redux/selectors/entities/schemes';
import {getScheme as loadScheme, patchScheme, createScheme, getSchemeTeams as loadSchemeTeams} from 'mattermost-redux/actions/schemes';
import {updateTeamScheme} from 'mattermost-redux/actions/teams';
import PermissionTeamSchemeSettings from './permission_team_scheme_settings.jsx';
function makeMapStateToProps() {
const getSchemeTeams = makeGetSchemeTeams();
return (state, ownProps) => {
const schemeId = ownProps.match.params.scheme_id;
return {
schemeId,
scheme: schemeId ? getScheme(state, schemeId) : null,
teams: schemeId ? getSchemeTeams(state, {schemeId}) : null,
roles: getRoles(state),
rolesById: getRolesById(state),
rolesRequest: state.requests.roles.getRolesByNames,
};
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadRolesIfNeeded,
loadScheme,
loadSchemeTeams,
editRole,
loadRole,
patchScheme,
updateTeamScheme,
createScheme,
}, dispatch),
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(PermissionTeamSchemeSettings);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getTeamStats as loadTeamStats} from 'mattermost-redux/actions/teams';
import {getTeamStats} from 'mattermost-redux/selectors/entities/teams';
import TeamInList from './team_in_list.jsx';
function mapStateToProps(state) {
return {
stats: getTeamStats(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadTeamStats,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TeamInList);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import TeamInfo from 'components/team_info';
export default class TeamInList extends React.Component {
static propTypes = {
team: PropTypes.object.isRequired,
onRemoveTeam: PropTypes.func,
}
render() {
const team = this.props.team;
return (
<div
className='team'
key={team.id}
>
<TeamInfo team={team}/>
<a
className='remove'
onClick={() => this.props.onRemoveTeam(team.id)}
>
<FormattedMessage
id='admin.permissions.teamScheme.removeTeam'
defaultMessage='Remove'
/>
</a>
</div>
);
}
}
......@@ -37,7 +37,7 @@ export default class PermissionsSchemeSummary extends React.Component {
<FormattedMessage
id='admin.permissions.permissionsSchemeSummary.deleteSchemeTitle'
defaultMessage='Delete {scheme} scheme?'
values={{scheme: this.props.scheme.name}}
values={{scheme: this.props.scheme.display_name}}
/>
);
......@@ -56,7 +56,7 @@ export default class PermissionsSchemeSummary extends React.Component {
<FormattedMessage
id='admin.permissions.permissionsSchemeSummary.deleteConfirmQuestion'
defaultMessage='The permissions in the teams using this scheme will reset to the defaults in the System Scheme. Are you sure you want to delete the {schemeName} scheme?'
values={{schemeName: this.props.scheme.name}}
values={{schemeName: this.props.scheme.display_name}}
/>
</p>
{serverError}
......@@ -131,7 +131,7 @@ export default class PermissionsSchemeSummary extends React.Component {
className='team'
key={team.id}
>
{team.name}
{team.display_name}
</span>
)) : [];
......@@ -177,7 +177,7 @@ export default class PermissionsSchemeSummary extends React.Component {
className='permissions-scheme-summary--header'
>
<div className='title'>
{scheme.name}
{scheme.display_name}
</div>
<div className='actions'>
<Link
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getTeamStats as loadTeamStats} from 'mattermost-redux/actions/teams';
import {getTeamStats} from 'mattermost-redux/selectors/entities/teams';
import TeamInfo from './team_info.jsx';
function mapStateToProps(state) {
return {
stats: getTeamStats(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadTeamStats,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TeamInfo);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import {imageURLForTeam} from 'utils/utils.jsx';
export default class TeamInList extends React.Component {
static propTypes = {
team: PropTypes.object.isRequired,
stats: PropTypes.object,
actions: PropTypes.shape({
loadTeamStats: PropTypes.func.isRequired,
}).isRequired,
}
componentDidMount() {
this.props.actions.loadTeamStats(this.props.team.id);
}
render() {
const {team, stats} = this.props;
let totalMembers = 0;
if (stats && stats[team.id]) {
totalMembers = stats[team.id].total_member_count;
}
const teamIconUrl = imageURLForTeam(team);
let icon = null;
if (teamIconUrl) {
icon = (
<div
className='team-btn__image'
style={{backgroundImage: `url('${teamIconUrl}')`}}
/>
);
} else {
icon = (
<div className='team-btn__initials'>
{team.display_name ? team.display_name.replace(/\s/g, '').substring(0, 2) : '??'}
<div className='team-btn__content'>
{team.display_name}
</div>
</div>
);
}
return (
<div className='team-info-block'>
<span className='icon'>{icon}</span>
<div className='team-data'>
<div className='title'>{team.display_name}</div>
<div className='members'>
<FormattedMessage
id='admin.team_info.numMembers'
defaultMessage='{count} {count, plural, one {Member} other {Members}}'
values={{count: totalMembers}}
/>
</div>
</div>
</div>
);
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getTeams as loadTeams, searchTeams} from 'mattermost-redux/actions/teams';
import {getTeams} from 'mattermost-redux/selectors/entities/teams';
import {setModalSearchTerm} from 'actions/views/search';
import TeamSelectorModal from './team_selector_modal.jsx';
function mapStateToProps(state) {
const searchTerm = state.views.search.modalSearch;
const teams = Object.values(getTeams(state) || {}).filter((team) => {
return team.display_name.toLowerCase().startsWith(searchTerm.toLowerCase()) ||
team.description.toLowerCase().startsWith(searchTerm.toLowerCase());
});
return {
searchTerm,
teams,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadTeams,
setModalSearchTerm,
searchTeams,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TeamSelectorModal);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import Constants from 'utils/constants.jsx';
import {localizeMessage} from 'utils/utils.jsx';
import MultiSelect from 'components/multiselect/multiselect.jsx';
import TeamInfo from 'components/team_info';
import ConfirmModal from 'components/confirm_modal.jsx';
const TEAMS_PER_PAGE = 50;
export default class TeamSelectorModal extends React.Component {
static propTypes = {
currentSchemeId: PropTypes.string.isRequired,
alreadySelected: PropTypes.array,
searchTerm: PropTypes.string.isRequired,
teams: PropTypes.array.isRequired,
onModalDismissed: PropTypes.func,
onTeamsSelected: PropTypes.func,
actions: PropTypes.shape({
loadTeams: PropTypes.func.isRequired,
setModalSearchTerm: PropTypes.func.isRequired,
searchTeams: PropTypes.func.isRequired,
}).isRequired,
}
constructor(props) {
super(props);
this.searchTimeoutId = 0;
this.state = {
values: [],
show: true,
search: false,
loadingTeams: true,
confirmAddModal: false,
confirmAddTeam: null,
};
}
componentDidMount() {
this.props.actions.loadTeams(0, TEAMS_PER_PAGE * 2).then(() => {
this.setTeamsLoadingState(false);
});
}
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (this.props.searchTerm !== nextProps.searchTerm) {
clearTimeout(this.searchTimeoutId);
const searchTerm = nextProps.searchTerm;
if (searchTerm === '') {
return;
}
this.searchTimeoutId = setTimeout(
async () => {
this.setTeamsLoadingState(true);
await this.props.actions.searchTeams(searchTerm);
this.setTeamsLoadingState(false);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
}
}
handleHide = () => {
this.props.actions.setModalSearchTerm('');
this.setState({show: false});
}
handleExit = () => {
if (this.props.onModalDismissed) {
this.props.onModalDismissed();
}
}
handleSubmit = (e) => {
if (e) {
e.preventDefault();
}
if (this.state.values.length === 0) {
return;
}
this.props.onTeamsSelected(this.state.values);
this.handleHide();
}
addValue = (value, confirmed = false) => {
if (value.scheme_id !== null && value.scheme_id !== '' && !confirmed) {
this.setState({confirmAddModal: true, confirmAddTeam: value});
return;
}
const values = Object.assign([], this.state.values);
const teamIds = values.map((v) => v.id);
if (value && value.id && teamIds.indexOf(value.id) === -1) {
values.push(value);
}
this.setState({values, confirmAddModal: false, confirmAddTeam: null});
}
setTeamsLoadingState = (loadingState) => {
this.setState({
loadingTeams: loadingState,
});
}
handlePageChange = (page, prevPage) => {
if (page > prevPage) {
this.setTeamsLoadingState(true);
this.props.actions.loadTeams(page + 1, TEAMS_PER_PAGE).then(() => {
this.setTeamsLoadingState(false);
});
}
}
handleDelete = (values) => {
this.setState({values});
}
search = (term) => {
this.props.actions.setModalSearchTerm(term);
}
renderOption(option, isSelected, onAdd) {
var rowSelected = '';
if (isSelected) {
rowSelected = 'more-modal__row--selected';
}
return (
<div
key={option.id}
ref={isSelected ? 'selected' : option.id}
className={'more-modal__row clickable ' + rowSelected}
onClick={() => onAdd(option)}
>
<div
className='more-modal__details'
>
<TeamInfo team={option}/>
</div>
<div className='more-modal__actions'>
<div className='more-modal__actions--round'>
<i className='fa fa-plus'/>
</div>
</div>
</div>
);
}
renderValue(team) {
return team.display_name;
}
renderConfirmModal(show, team) {
const title = (
<FormattedMessage
id='add_teams_to_scheme.confirmation.title'
defaultMessage='Team Override Scheme Change?'
/>
);
const message = (
<FormattedMessage
id='add_teams_to_scheme.confirmation.message'
defaultMessage='This team is already selected in another team scheme, are you sure you want to move it to this team scheme?'
/>
);
const confirmButtonText = (
<FormattedMessage
id='add_teams_to_scheme.confirmation.accept'
defaultMessage='Yes, Move Team'
/>
);
return (
<ConfirmModal
show={show}
title={title}
message={message}
confirmButtonText={confirmButtonText}
onCancel={() => this.setState({confirmAddModal: false, confirmAddTeam: null})}
onConfirm={() => this.addValue(team, true)}
/>
);
}
render() {
const confirmModal = this.renderConfirmModal(this.state.confirmAddModal, this.state.confirmAddTeam);
const numRemainingText = (
<FormattedMessage
id='multiselect.selectTeams'
defaultMessage='Use ↑↓ to browse, ↵ to select.'
/>
);
const buttonSubmitText = localizeMessage('multiselect.add', 'Add');
let teams = [];
if (this.props.teams) {
teams = this.props.teams.filter((team) => team.delete_at === 0);
teams = teams.filter((team) => team.scheme_id !== this.currentSchemeId);
teams = teams.filter((team) => this.props.alreadySelected.indexOf(team.id) === -1);
}
return (
<Modal
dialogClassName={'more-modal more-direct-channels team-selector-modal'}
show={this.state.show}
onHide={this.handleHide}
onExited={this.handleExit}
>
<Modal.Header closeButton={true}>
<Modal.Title>