Skip to content

Commit

Permalink
Use PrincipalList in RB settings (admins/acl)
Browse files Browse the repository at this point in the history
only viewing+removing users supported so far
  • Loading branch information
ThiefMaster committed Mar 15, 2019
1 parent 02e9ec1 commit fc51155
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 1 deletion.
32 changes: 32 additions & 0 deletions indico/modules/rb_new/client/js/modules/admin/SettingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {connect} from 'react-redux';
import {Field, Form as FinalForm} from 'react-final-form';
import {Form, Header, Message, Placeholder} from 'semantic-ui-react';
import {PluralTranslate, Translate} from 'indico/react/i18n';
import {PrincipalListField} from 'indico/react/components';
import {
formatters, getChangedValues, FieldCondition, ReduxFormField, ReduxCheckboxField,
validators as v
Expand Down Expand Up @@ -58,6 +59,7 @@ class SettingsPage extends React.PureComponent {

render() {
const {settingsLoaded, settings} = this.props;

if (!settingsLoaded) {
return (
<Placeholder>
Expand Down Expand Up @@ -88,6 +90,36 @@ class SettingsPage extends React.PureComponent {
{fprops => (
<Form onSubmit={fprops.handleSubmit}
success={fprops.submitSucceeded && !fprops.dirtySinceLastSubmit}>
<Message>
<Message.Header>
<Translate>
Specify who has access to the room booking system.
</Translate>
</Message.Header>
<Form.Group widths="equal">
<Field name="authorized_principals" component={ReduxFormField}
as={PrincipalListField}
isEqual={_.isEqual}
label={Translate.string('Authorized users')}>
<p className="field-description">
<Translate>
Restrict access to the room booking system to these users/groups.
If empty, all logged-in users have access.
</Translate>
</p>
</Field>
<Field name="admin_principals" component={ReduxFormField}
as={PrincipalListField}
isEqual={_.isEqual}
label={Translate.string('Administrators')}>
<p className="field-description">
<Translate>
Grant full room booking admin permissions to these users/groups.
</Translate>
</p>
</Field>
</Form.Group>
</Message>
<Field name="tileserver_url" component={ReduxFormField} as="input"
format={formatters.trim} formatOnBlur
label={Translate.string('Tileserver URL')}
Expand Down
4 changes: 3 additions & 1 deletion indico/modules/rb_new/controllers/backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
room_update_schema)
from indico.modules.users.models.users import User
from indico.util.i18n import _
from indico.util.marshmallow import ModelList
from indico.util.marshmallow import ModelList, PrincipalList


class RHRoomBookingAdminBase(RHRoomBookingBase):
Expand All @@ -53,6 +53,8 @@ def _check_access(self):


class SettingsSchema(mm.Schema):
admin_principals = PrincipalList(allow_groups=True)
authorized_principals = PrincipalList(allow_groups=True)
tileserver_url = fields.String(validate=[
validate.URL(schemes={'http', 'https'}),
lambda value: all(x in value for x in ('{x}', '{y}', '{z}'))
Expand Down
152 changes: 152 additions & 0 deletions indico/web/client/js/react/components/PrincipalListField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* This file is part of Indico.
* Copyright (C) 2002 - 2019 European Organization for Nuclear Research (CERN).
*
* Indico is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Indico is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Indico; if not, see <http://www.gnu.org/licenses/>.
*/

import principalsURL from 'indico-url:core.principals';

import _ from 'lodash';
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {Icon, List, Loader} from 'semantic-ui-react';
import {Translate} from 'indico/react/i18n';
import {indicoAxios, handleAxiosError} from 'indico/utils/axios';
import {useAsyncEffect} from '../hooks';

import './PrincipalListField.module.scss';


/**
* A field that lets the user select a list of users/groups
*/
const PrincipalListField = (props) => {
const {value, disabled, onChange, onFocus, onBlur} = props;

const isGroup = identifier => identifier.startsWith('Group:');
const handleDelete = identifier => {
onChange(value.filter(x => x !== identifier));
onFocus();
onBlur();
};

// keep track of details for each entry
const [identifierMap, setIdentifierMap] = useState({});

// fetch missing details
useAsyncEffect(async () => {
const missingData = _.difference(value, Object.keys(identifierMap));
if (!missingData.length) {
return;
}

let response;
try {
response = await indicoAxios.post(principalsURL(), {values: missingData});
} catch (error) {
handleAxiosError(error);
return;
}
setIdentifierMap(prev => ({...prev, ...response.data}));
}, [identifierMap, value]);

const entries = _.sortBy(
value.filter(x => x in identifierMap).map(x => identifierMap[x]),
x => `${x.group ? 0 : 1}-${x.name.toLowerCase()}`
);
const pendingEntries = _.sortBy(
value.filter(x => !(x in identifierMap)).map(x => ({identifier: x, group: isGroup(x)})),
x => `${x.group ? 0 : 1}-${x.identifier.toLowerCase()}`
);

return (
<>
<List divided relaxed styleName="list">
{entries.map(data => (
<PrincipalListItem key={data.identifier}
name={data.name}
detail={data.detail}
isGroup={data.group}
onDelete={() => !disabled && handleDelete(data.identifier)}
disabled={disabled} />
))}
{pendingEntries.map(data => (
<PendingPrincipalListItem key={data.identifier} isGroup={data.group} />
))}
{!value.length && (
<List.Item styleName="empty">
<Translate>
This list is currently empty
</Translate>
</List.Item>
)}
</List>
</>
);
};

PrincipalListField.propTypes = {
value: PropTypes.arrayOf(PropTypes.string).isRequired,
disabled: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired,
};

// eslint-disable-next-line react/prop-types
const PendingPrincipalListItem = ({isGroup}) => (
<List.Item>
<div styleName="item">
<div styleName="icon">
<Icon name={isGroup ? 'users' : 'user'} size="large" />
</div>
<div styleName="content">
<List.Header>
{isGroup
? <Translate>Unknown group</Translate>
: <Translate>Unknown user</Translate>}
</List.Header>
</div>
<div styleName="loader">
<Loader active inline size="small" />
</div>
</div>
</List.Item>
);

// eslint-disable-next-line react/prop-types
const PrincipalListItem = ({isGroup, name, detail, onDelete, disabled}) => (
<List.Item>
<div styleName="item">
<div styleName="icon">
<Icon name={isGroup ? 'users' : 'user'} size="large" />
</div>
<div styleName="content">
<List.Header>
{name}
</List.Header>
{detail && (
<List.Description>
{detail}
</List.Description>
)}
</div>
<div>
<Icon styleName="delete-button" name="remove" onClick={onDelete} size="large" disabled={disabled} />
</div>
</div>
</List.Item>
);

export default React.memo(PrincipalListField);
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* This file is part of Indico.
* Copyright (C) 2002 - 2019 European Organization for Nuclear Research (CERN).
*
* Indico is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Indico is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Indico; if not, see <http://www.gnu.org/licenses/>.
*/

@import 'base/palette';

.list {
margin-top: 0 !important;
height: 200px;
overflow-y: scroll;
}

.item {
display: flex;
align-items: center;
}

.icon {
padding-right: 5px;
color: $light-black;
}

.content {
flex-grow: 1;
}

.loader {
margin-right: 5px;
margin-bottom: 5px;
}

.delete-button {
cursor: pointer;
color: $red;
opacity: 0.5 !important;

&:hover {
opacity: 1 !important;
}
}

.empty {
padding-top: 20px !important;
text-align: center;
color: $light-black;
}
1 change: 1 addition & 0 deletions indico/web/client/js/react/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export {default as Carousel} from './Carousel';
export {default as PrincipalSearchField} from './PrincipalSearchField';
export {default as ScrollButton} from './ScrollButton';
export {default as StickyWithScrollBack} from './StickyWithScrollBack';
export {default as PrincipalListField} from './PrincipalListField';

0 comments on commit fc51155

Please sign in to comment.