Commit 45188b97 authored by Jesús Espino's avatar Jesús Espino Committed by Martin Kraft

New admin console page for manage system schemes (#1116)

* New admin console page for manage system schemas

* Tunning header and footer styles

* WIP

* Improving styles

* Improved subgrous rendering and management

* Fixed SVG attributes

* New group toggle behavior changed

* Improved trnslations

* Some work in the Checkboxes behavior

* Fixing tests

* Updating editable permissions

* replacing code with id, and restructured the constants usage

* Fixing Schemes plural usage

* Fixing a syntax problem in translation

* Fix small problem in eslint

* Setting the permission name to fixed size

* Change link color inherited permissions description

* Change the behavior to the new proposed group change behavior

* Added auto expand/collapse on toggle group

* Fixed problem with empty groups

* Nicer animation on open/close permissions groups

* Added scroll to the permission

* Adding tests for auto expand/collapse feature

* Adding tests for state change behavior

* eslint fix

* Add rowhighlight animation for selected permission

* Open callapsed role on select permission

* Appliying changes from header-footer in master PR

* Some fixes

* Updating tests snapshots

* Changed Highlight color

* Changed how is selected the current selected row

* Removing/combining permissions

* Back to permalink color on parent permission

* Polishing a bit select permission

* Fixed combined permissions groups

* Fixing tests and styles

* Tooltips working

* Minor UI updates

* Add menu footer error tooltip

* Scroll to permissions-block beginning on expand

* Some tests fixes

* Some eslint fixes

* Fixed styles after merge

* Addining missed translation

* Re-applying transparency to description in permission rows

* Reverting scroll to permissions group on expand
parent a611e754
......@@ -227,6 +227,29 @@ export default class AdminConsole extends React.Component {
</Switch>
)}
/>
<Route
path={`${this.props.match.url}/permissions`}
render={(props) => (
<Switch>
<SCRoute
path={`${props.match.url}/schemes`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.permissions.schemes.schema,
}}
/>
<SCRoute
path={`${props.match.url}/system-scheme`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.permissions.systemScheme.schema,
}}
/>
</Switch>
)}
/>
<Route
path={`${this.props.match.url}/authentication`}
render={(props) => (
......
......@@ -13,6 +13,8 @@ import SystemUsers from './system_users';
import ServerLogs from './server_logs';
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 * as DefinitionConstants from './admin_definition_constants';
......@@ -531,6 +533,20 @@ export default {
},
},
},
permissions: {
schemes: {
schema: {
id: 'PermissionSchemes',
component: PermissionSchemesSettings,
},
},
systemScheme: {
schema: {
id: 'PermissionSystemScheme',
component: PermissionSystemSchemeSettings,
},
},
},
authentication: {
email: {
schema: {
......
......@@ -3,11 +3,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import {Overlay, Tooltip} from 'react-bootstrap';
import {saveConfig} from 'actions/admin_actions.jsx';
import {localizeMessage} from 'utils/utils.jsx';
import SaveButton from 'components/save_button.jsx';
import FormError from 'components/form_error.jsx';
import Constants from 'utils/constants.jsx';
export default class AdminSettings extends React.Component {
static propTypes = {
......@@ -35,9 +37,20 @@ export default class AdminSettings extends React.Component {
saveNeeded: false,
saving: false,
serverError: null,
errorTooltip: false,
});
}
closeTooltip = () => {
this.setState({errorTooltip: false});
}
openTooltip = (e) => {
const elm = e.currentTarget.querySelector('.control-label');
const isElipsis = elm.offsetWidth < elm.scrollWidth;
this.setState({errorTooltip: isElipsis});
}
handleChange = (id, value) => {
this.setState({
saveNeeded: true,
......@@ -173,18 +186,31 @@ export default class AdminSettings extends React.Component {
onSubmit={this.handleSubmit}
>
{this.renderSettings()}
<div className='form-group'>
<FormError error={this.state.serverError}/>
</div>
<div className='form-group'>
<div className='col-sm-12'>
<SaveButton
saving={this.state.saving}
disabled={!this.state.saveNeeded || (this.canSave && !this.canSave())}
onClick={this.handleSubmit}
savingMessage={localizeMessage('admin.saving', 'Saving Config...')}
/>
<div className='admin-console-save'>
<SaveButton
saving={this.state.saving}
disabled={!this.state.saveNeeded || (this.canSave && !this.canSave())}
onClick={this.handleSubmit}
savingMessage={localizeMessage('admin.saving', 'Saving Config...')}
/>
<div
className='error-message'
ref='errorMessage'
onMouseOver={this.openTooltip}
onMouseOut={this.closeTooltip}
>
<FormError error={this.state.serverError}/>
</div>
<Overlay
show={this.state.errorTooltip}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
target={this.refs.errorMessage}
>
<Tooltip id='error-tooltip' >
{this.state.serverError}
</Tooltip>
</Overlay>
</div>
</form>
</div>
......
......@@ -494,6 +494,26 @@ export default class AdminSidebar extends React.Component {
}
/>
</AdminSidebarSection>
<AdminSidebarSection
name='permissions'
type='text'
title={
<FormattedMessage
id='admin.sidebar.permissions'
defaultMessage='Permissions'
/>
}
>
<AdminSidebarSection
name='schemes'
title={
<FormattedMessage
id='admin.sidebar.schemes'
defaultMessage='Permission Schemes'
/>
}
/>
</AdminSidebarSection>
<AdminSidebarSection
name='authentication'
type='text'
......
......@@ -71,7 +71,7 @@ export default class Audits extends React.PureComponent {
}
return (
<div>
<div className='wrapper--admin'>
<ComplianceReports/>
<div className='panel audit-panel'>
......
// 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} from 'mattermost-redux/actions/roles';
import {getRoles} from 'mattermost-redux/selectors/entities/roles';
import PermissionSchemesSettings from './permission_schemes_settings.jsx';
function mapStateToProps(state) {
return {
roles: getRoles(state),
rolesRequest: state.requests.roles.getRolesByNames,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadRolesIfNeeded,
editRole,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PermissionSchemesSettings);
// 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 CheckboxCheckedIcon from 'components/svg/checkbox_checked_icon.jsx';
import CheckboxPartialIcon from 'components/svg/checkbox_partial_icon.jsx';
export default class PermissionCheckbox extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
};
static defaultProps = {
value: '',
}
render() {
const {value} = this.props;
let icon = null;
let extraClass = '';
if (value === 'checked') {
icon = (<CheckboxCheckedIcon/>);
extraClass = 'checked';
} else if (value === 'intermediate') {
icon = (<CheckboxPartialIcon/>);
extraClass = 'intermediate';
}
return (
<div className={'permission-check ' + extraClass}>
{icon}
</div>
);
}
}
// 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, FormattedHTMLMessage, injectIntl, intlShape} from 'react-intl';
import {Overlay, Tooltip} from 'react-bootstrap';
import {generateId} from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
export class PermissionDescription extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
id: PropTypes.string.isRequired,
rowType: PropTypes.string.isRequired,
inherited: PropTypes.object,
selectRow: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.id = generateId();
this.state = {
open: false,
};
}
closeTooltip = () => {
this.setState({open: false});
}
openTooltip = (e) => {
const elm = e.currentTarget.querySelector('span');
const isElipsis = elm.offsetWidth < elm.scrollWidth;
this.setState({open: isElipsis});
}
parentPermissionClicked = (e) => {
if (e.target.tagName === 'A') {
this.props.selectRow(this.props.id);
e.stopPropagation();
}
}
render() {
const {inherited, id, rowType} = this.props;
let content = '';
if (inherited) {
content = (
<FormattedHTMLMessage
id='admin.permissions.inherited_from'
values={{
name: this.props.intl.formatMessage({
id: 'admin.permissions.roles.' + inherited.name + '.name',
defaultMessage: inherited.display_name,
}),
}}
/>
);
} else {
content = <FormattedMessage id={'admin.permissions.' + rowType + '.' + id + '.description'}/>;
}
content = (
<span
className='permission-description'
onClick={this.parentPermissionClicked}
ref='content'
onMouseOver={this.openTooltip}
onMouseOut={this.closeTooltip}
>
{content}
<Overlay
show={this.state.open}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='top'
target={this.refs.content}
>
<Tooltip id={this.id}>
{content}
</Tooltip>
</Overlay>
</span>
);
return content;
}
}
export default injectIntl(PermissionDescription);
// 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 PermissionCheckbox from './permission_checkbox.jsx';
import PermissionDescription from './permission_description.jsx';
export default class PermissionRow extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
inherited: PropTypes.object,
readOnly: PropTypes.bool,
selected: PropTypes.string,
selectRow: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
toggleSelect = () => {
if (this.props.readOnly) {
return;
}
this.props.onChange(this.props.id);
}
render = () => {
const {id, inherited, value, readOnly, selected} = this.props;
let classes = 'permission-row';
if (readOnly) {
classes += ' read-only';
}
if (selected === id) {
classes += ' selected';
}
return (
<div
className={classes}
onClick={this.toggleSelect}
>
<PermissionCheckbox value={value}/>
<span className='permission-name'>
<FormattedMessage id={'admin.permissions.permission.' + id + '.name'}/>
</span>
<PermissionDescription
inherited={inherited}
id={id}
selectRow={this.props.selectRow}
rowType='permission'
/>
</div>
);
};
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
export default class PermissionSchemesSettings extends React.PureComponent {
renderTitle() {
return (
<FormattedMessage
id='admin.permissions.permissionSchemes'
defaultMessage='Permission Schemes'
/>
);
}
render = () => {
return (
<div className='wrapper--fixed'>
<h3 className='admin-console-header'>
{this.renderTitle()}
</h3>
<div className={'banner info'}>
<div className='banner__content'>
<span>
<FormattedMessage
id='admin.permissions.introBanner'
defaultMessage='Permission Schemes set the default permissions for Team Admins, Channel Admins and everyone else. Learn more about permission schemes in our documentation.'
/>
</span>
</div>
</div>
<div className='permissions-block'>
<div className='header'>
<div>
<h3>
<FormattedMessage
id='admin.permissions.systemSchemeBannerTitle'
defaultMessage='System Scheme'
/>
</h3>
<span>
<FormattedMessage
id='admin.permissions.systemSchemeBannerText'
defaultMessage='Set the default permissions inherited by all teams unless a Team Override Scheme is applied.'
/>
</span>
</div>
<div className='button'>
<Link
className='btn btn-primary'
to='/admin_console/permissions/system-scheme'
>
<FormattedMessage
id='admin.permissions.systemSchemeBannerButton'
defaultMessage='Edit Scheme'
/>
</Link>
</div>
</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 {loadRolesIfNeeded, editRole} from 'mattermost-redux/actions/roles';
import {getRoles} from 'mattermost-redux/selectors/entities/roles';
import PermissionSystemSchemeSettings from './permission_system_scheme_settings.jsx';
function mapStateToProps(state) {
return {
roles: getRoles(state),
rolesRequest: state.requests.roles.getRolesByNames,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadRolesIfNeeded,
editRole,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PermissionSystemSchemeSettings);
// 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 PermissionGroup from './permission_group.jsx';
const GROUPS = [
{
id: 'teams',
permissions: [
{
id: 'send_invites',
combined: true,
permissions: [
'invite_user',
'get_public_link',
'add_user_to_team',
],
},
'create_team',
],
},
{
id: 'public_channel',
permissions: [
'create_public_channel',
'manage_public_channel_properties',
'manage_public_channel_members',
'delete_public_channel',
],
},
{
id: 'private_channel',
permissions: [
'create_private_channel',
'manage_private_channel_properties',
'manage_private_channel_members',
'delete_private_channel',
],
},
{
id: 'posts',
permissions: [
'edit_post',
{
id: 'delete_posts',
permissions: [
'delete_post',
'delete_others_posts',
],
},
{
id: 'reactions',
combined: true,
permissions: [
'add_reaction',
'remove_reaction',
],
},
],
},
{
id: 'integrations',
permissions: [
'manage_webhooks',
'manage_oauth',
'manage_slash_commands',
],
},
];
export default class PermissionsTree extends React.Component {
static propTypes = {
scope: PropTypes.string.isRequired,
role: PropTypes.object.isRequired,
onToggle: PropTypes.func.isRequired,
parentRole: PropTypes.object,
selected: PropTypes.string,
selectRow: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
};
static defaultProps = {
role: {
permissions: [],
},
};
toggleGroup = (ids) => {
if (this.props.readOnly) {
return;
}
this.props.onToggle(this.props.role.name, ids);
}
render = () => {
return (
<div className='permissions-tree'>
<div className='permissions-tree--header'>
<div className='permission-name'>
<FormattedMessage
id='admin.permissions.permissionsTree.permission'
defaultMessage='Permission'
/>
</div>
<div className='permission-description'>
<FormattedMessage
id='admin.permissions.permissionsTree.description'
defaultMessage='Description'
/>
</div>
</div>
<div className='permissions-tree--body'>
<PermissionGroup
key='all'
id='all'
selected={this.props.selected}
selectRow={this.props.selectRow}
readOnly={this.props.readOnly}
permissions={GROUPS}
role={this.props.role}
parentRole={this.props.parentRole}
scope={this.props.scope}
combined={false}
onChange={this.toggleGroup}
root={true}
/>