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 {PermissionsScope} from 'utils/constants.jsx';
import PermissionCheckbox from './permission_checkbox.jsx';
import PermissionRow from './permission_row.jsx';
import PermissionDescription from './permission_description.jsx';
export default class PermissionGroup extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
permissions: PropTypes.array.isRequired,
readOnly: PropTypes.bool,
role: PropTypes.object,
parentRole: PropTypes.object,
scope: PropTypes.string.isRequired,
combined: PropTypes.bool,
selected: PropTypes.string,
selectRow: PropTypes.func.isRequired,
root: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
expanded: true,
prevPermissions: [],
};
}
componentWillUpdate(nextProps) {
if (this.props.selected !== nextProps.selected) {
if (this.getRecursivePermissions(this.props.permissions).indexOf(nextProps.selected) !== -1) {
this.setState({expanded: true});
}
}
}
toggleExpanded = (e) => {
e.stopPropagation();
this.setState({expanded: !this.state.expanded});
}
toggleSelectRow = (id) => {
if (this.props.readOnly) {
return;
}
this.props.onChange([id]);
}
getRecursivePermissions = (permissions) => {
let result = [];
for (const permission of permissions) {
if (typeof permission === 'string') {
result.push(permission);
} else {
result = result.concat(this.getRecursivePermissions(permission.permissions));
}
}
return result;
}
toggleSelectSubGroup = (ids) => {
if (this.props.readOnly) {
return;
}
this.props.onChange(ids);
}
toggleSelectGroup = () => {
const {readOnly, permissions, role, onChange} = this.props;
if (readOnly) {
return;
}
if (this.getStatus(permissions) === 'checked') {
const permissionsToToggle = [];
for (const permission of this.getRecursivePermissions(permissions)) {
if (!this.fromParent(permission)) {
permissionsToToggle.push(permission);
}
}
this.setState({expanded: true});
onChange(permissionsToToggle);
} else if (this.getStatus(permissions) === '') {
const permissionsToToggle = [];
let expanded = true;
if (this.state.prevPermissions.length === 0) {
for (const permission of this.getRecursivePermissions(permissions)) {
if (!this.fromParent(permission)) {
permissionsToToggle.push(permission);
expanded = false;
}
}
} else {
for (const permission of this.getRecursivePermissions(permissions)) {
if (this.state.prevPermissions.indexOf(permission) !== -1 && !this.fromParent(permission)) {
permissionsToToggle.push(permission);
}
}
}
onChange(permissionsToToggle);
this.setState({prevPermissions: [], expanded});
} else {
const permissionsToToggle = [];
for (const permission of this.getRecursivePermissions(permissions)) {
if (role.permissions.indexOf(permission) === -1 && !this.fromParent(permission)) {
permissionsToToggle.push(permission);
}
}
this.setState({prevPermissions: role.permissions, expanded: false});
onChange(permissionsToToggle);
}
}
isInScope = (permission) => {
if (this.props.scope === 'channel_scope' && PermissionsScope[permission] !== 'channel_scope') {
return false;
}
if (this.props.scope === 'team_scope' && PermissionsScope[permission] === 'system_scope') {
return false;
}
return true;
}
renderPermission = (permission) => {
if (!this.isInScope(permission)) {
return null;
}
const comesFromParent = this.fromParent(permission);
const active = comesFromParent || this.props.role.permissions.indexOf(permission) !== -1;
return (
<PermissionRow
key={permission}
id={permission}
selected={this.props.selected}
selectRow={this.props.selectRow}
readOnly={this.props.readOnly || comesFromParent}
inherited={comesFromParent ? this.props.parentRole : null}
value={active ? 'checked' : ''}
onChange={this.toggleSelectRow}
/>
);
}
renderGroup = (g) => {
return (
<PermissionGroup
key={g.id}
id={g.id}
selected={this.props.selected}
selectRow={this.props.selectRow}
readOnly={this.props.readOnly}
permissions={g.permissions}
role={this.props.role}
parentRole={this.props.parentRole}
scope={this.props.scope}
onChange={this.toggleSelectSubGroup}
combined={g.combined}
root={false}
/>
);
}
fromParent = (id) => {
return this.props.parentRole && this.props.parentRole.permissions.indexOf(id) !== -1;
}
getStatus = (permissions) => {
let anyChecked = false;
let anyUnchecked = false;
for (const permission of permissions) {
if (typeof permission === 'string') {
if (!this.isInScope(permission)) {
continue;
}
anyChecked = anyChecked || this.fromParent(permission) || this.props.role.permissions.indexOf(permission) !== -1;
anyUnchecked = anyUnchecked || (!this.fromParent(permission) && this.props.role.permissions.indexOf(permission) === -1);
} else {
const status = this.getStatus(permission.permissions);
if (status === 'intermediate') {
return 'intermediate';
}
if (status === 'checked') {
anyChecked = true;
}
if (status === '') {
anyUnchecked = true;
}
}
}
if (anyChecked && anyUnchecked) {
return 'intermediate';
}
if (anyChecked && !anyUnchecked) {
return 'checked';
}
return '';
}
hasPermissionsOnScope = () => {
for (const permission of this.getRecursivePermissions(this.props.permissions)) {
if (this.isInScope(permission)) {
return true;
}
}
return false;
}
allPermissionsFromParent = (permissions) => {
for (const permission of permissions) {
if (typeof permission !== 'string') {
if (!this.allPermissionsFromParent(permission.permissions)) {
return false;
}
continue;
}
if (this.isInScope(permission) && !this.fromParent(permission)) {
return false;
}
}
return true;
}
render = () => {
const {id, permissions, readOnly, combined, root, selected} = this.props;
if (!this.hasPermissionsOnScope()) {
return null;
}
const permissionsRows = permissions.map((group) => {
if (typeof group === 'string') {
return this.renderPermission(group);
}
return this.renderGroup(group);
});
if (root) {
return (
<div className={'permission-group-permissions ' + (this.state.expanded ? 'open' : '')}>
{permissionsRows}
</div>
);
}
let inherited = null;
if (this.allPermissionsFromParent(this.props.permissions) && this.props.combined) {
inherited = this.props.parentRole;
}
let classes = '';
if (selected === id) {
classes += ' selected';
}
if (readOnly || this.allPermissionsFromParent(this.props.permissions)) {
classes += ' read-only';
}
if (combined) {
classes += ' combined';
}
return (
<div className='permission-group'>
{!root &&
<div
className={'permission-group-row ' + classes}
onClick={this.toggleSelectGroup}
>
{!combined &&
<div
className={'fa fa-caret-right permission-arrow ' + (this.state.expanded ? 'open' : '')}
onClick={this.toggleExpanded}
/>}
<PermissionCheckbox value={this.getStatus(this.props.permissions)}/>
<span className='permission-name'>
<FormattedMessage id={'admin.permissions.group.' + id + '.name'}/>
</span>
<PermissionDescription
inherited={inherited}