Skip to content

Commit 1474a04

Browse files
author
Martin Krulis
committed
Implementing effective roles and their switching for superadmins. Redesigning logged in user badge whilst at it.
1 parent 0da7e86 commit 1474a04

File tree

21 files changed

+269
-124
lines changed

21 files changed

+269
-124
lines changed

src/components/SystemMessages/MessagesList/MessagesList.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,11 @@ class MessagesList extends Component {
6464
className: 'valign-middle',
6565
}),
6666

67-
new SortableTableColumnDescriptor(
68-
'role',
69-
<FormattedMessage id="app.systemMessagesList.role" defaultMessage="Role" />,
70-
{
71-
comparator: ({ role: r1 }, { role: r2 }) => Number(rolePriorities[r2]) - Number(rolePriorities[r1]),
72-
cellRenderer: role => role && roleLabels[role],
73-
className: 'valign-middle',
74-
}
75-
),
67+
new SortableTableColumnDescriptor('role', <FormattedMessage id="generic.role" defaultMessage="Role" />, {
68+
comparator: ({ role: r1 }, { role: r2 }) => Number(rolePriorities[r2]) - Number(rolePriorities[r1]),
69+
cellRenderer: role => role && roleLabels[role],
70+
className: 'valign-middle',
71+
}),
7672

7773
new SortableTableColumnDescriptor(
7874
'type',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Table } from 'react-bootstrap';
4+
import classnames from 'classnames';
5+
6+
import { knownRoles, roleLabels, UserRoleIcon } from '../../helpers/usersRoles';
7+
8+
const EffectiveRoleSwitching = ({ effectiveRole, setEffectiveRole, updating = null }) => (
9+
<Table hover className="no-margin">
10+
<tbody>
11+
{knownRoles.map(role => (
12+
<tr
13+
key={role}
14+
className={classnames({
15+
'bg-info': role === (updating || effectiveRole),
16+
})}
17+
onClick={ev => {
18+
ev.preventDefault();
19+
setEffectiveRole(role);
20+
}}>
21+
<td className="shrink-col">
22+
<input
23+
type="radio"
24+
name="effectiveRole"
25+
value={role}
26+
checked={role === (updating || effectiveRole)}
27+
disabled={Boolean(updating)}
28+
readOnly
29+
/>
30+
</td>
31+
<td className="shrink-col">
32+
<UserRoleIcon role={role}></UserRoleIcon>
33+
</td>
34+
<td>{roleLabels[role]}</td>
35+
</tr>
36+
))}
37+
</tbody>
38+
</Table>
39+
);
40+
41+
EffectiveRoleSwitching.propTypes = {
42+
effectiveRole: PropTypes.string,
43+
updating: PropTypes.string,
44+
setEffectiveRole: PropTypes.func.isRequired,
45+
};
46+
47+
export default EffectiveRoleSwitching;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import EffectiveRoleSwitching from './EffectiveRoleSwitching';
2+
export default EffectiveRoleSwitching;

src/components/Users/UsersName/UsersName.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ const UsersName = ({
2323
fullName,
2424
avatarUrl,
2525
name: { firstName },
26-
size = 20,
26+
size = null,
2727
large = false,
2828
isVerified,
29-
noLink,
29+
noLink = false,
3030
privateData = null,
3131
showEmail = null,
3232
showExternalIdentifiers = false,
3333
links: { USER_URI_FACTORY },
3434
currentUserId,
3535
}) => {
36+
if (size === null) {
37+
size = large ? 45 : 20;
38+
}
3639
const email = privateData && privateData.email && showEmail && encodeURIComponent(privateData.email);
3740
const externalIds = privateData && privateData.externalIds;
3841
return (

src/components/forms/EditSystemMessageForm/EditSystemMessageForm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const EditSystemMessageForm = ({
3939
isOpen,
4040
onClose,
4141
createNew = false,
42-
intl: { locale, formatMessage },
42+
intl: { formatMessage },
4343
}) => (
4444
<Modal show={isOpen} backdrop="static" size="lg" onHide={onClose}>
4545
<Modal.Header closeButton>

src/components/layout/Sidebar/Sidebar.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import { getLocalizedResourceName } from '../../../helpers/localizedData';
1616
import { isSupervisorRole, isEmpoweredSupervisorRole, isSuperadminRole } from '../../helpers/usersRoles';
1717
import withLinks from '../../../helpers/withLinks';
1818
import { getExternalIdForCAS } from '../../../helpers/cas';
19-
import { safeGet, EMPTY_OBJ } from '../../../helpers/common';
19+
import { EMPTY_OBJ } from '../../../helpers/common';
2020

2121
import styles from './sidebar.less';
2222

2323
const getUserData = defaultMemoize(user => getJsData(user));
2424

2525
const Sidebar = ({
2626
loggedInUser,
27+
effectiveRole = null,
2728
studentOf,
2829
supervisorOf,
2930
currentUrl,
@@ -45,7 +46,6 @@ const Sidebar = ({
4546
intl: { locale },
4647
}) => {
4748
const user = getUserData(loggedInUser);
48-
const role = safeGet(user, ['privateData', 'role']);
4949
const createLink = item => GROUP_DETAIL_URI_FACTORY(getId(item));
5050
const studentOfItems =
5151
studentOf &&
@@ -124,7 +124,7 @@ const Sidebar = ({
124124
/>
125125
)}
126126

127-
{isSupervisorRole(role) && supervisorOfItems && (
127+
{isSupervisorRole(effectiveRole) && supervisorOfItems && (
128128
<MenuGroup
129129
title={
130130
<FormattedMessage id="app.sidebar.menu.supervisorOfGroups" defaultMessage="Supervisor of Groups" />
@@ -139,7 +139,7 @@ const Sidebar = ({
139139
/>
140140
)}
141141

142-
{isSupervisorRole(role) && (
142+
{isSupervisorRole(effectiveRole) && (
143143
<MenuItem
144144
title={<FormattedMessage id="app.sidebar.menu.exercises" defaultMessage="Exercises" />}
145145
icon="puzzle-piece"
@@ -148,7 +148,7 @@ const Sidebar = ({
148148
/>
149149
)}
150150

151-
{isEmpoweredSupervisorRole(role) && (
151+
{isEmpoweredSupervisorRole(effectiveRole) && (
152152
<MenuItem
153153
title={<FormattedMessage id="app.sidebar.menu.pipelines" defaultMessage="Pipelines" />}
154154
icon="random"
@@ -183,7 +183,7 @@ const Sidebar = ({
183183
</React.Fragment>
184184
)}
185185

186-
{isSuperadminRole(role) && <Admin currentUrl={currentUrl} />}
186+
{isSuperadminRole(effectiveRole) && <Admin currentUrl={currentUrl} />}
187187
</section>
188188
</aside>
189189
);
@@ -195,6 +195,7 @@ Sidebar.propTypes = {
195195
search: PropTypes.string.isRequired,
196196
}).isRequired,
197197
loggedInUser: ImmutablePropTypes.map,
198+
effectiveRole: PropTypes.string,
198199
studentOf: ImmutablePropTypes.map,
199200
supervisorOf: ImmutablePropTypes.map,
200201
currentUrl: PropTypes.string,

src/components/widgets/Avatar/FakeAvatar.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33

4-
const getSize = (size, small) => (small ? size * (2 / 3) : size);
4+
const getSize = (size, borderWidth, small) => (small ? size * (2 / 3) : size) - 2 * borderWidth;
55

66
const FakeAvatar = ({ size = 45, borderWidth = 2, light = false, children, small = false, altClassName = '' }) => (
77
<span
@@ -10,14 +10,14 @@ const FakeAvatar = ({ size = 45, borderWidth = 2, light = false, children, small
1010
background: !light ? '#286090' : 'white',
1111
color: !light ? 'white' : 'gray',
1212
textAlign: 'center',
13-
width: getSize(size, small),
14-
height: getSize(size, small),
15-
lineHeight: `${getSize(size, small) - 2 * borderWidth}px`,
13+
width: getSize(size, borderWidth, small),
14+
height: getSize(size, borderWidth, small),
15+
lineHeight: `${getSize(size, borderWidth, small) - 2 * borderWidth}px`,
1616
borderStyle: 'solid',
1717
borderWidth,
1818
borderColor: !light ? 'transparent' : 'gray',
19-
borderRadius: Math.ceil(getSize(size, small) / 2),
20-
fontSize: Math.floor(Math.max(14, getSize(size, small) / 2)),
19+
borderRadius: Math.floor(getSize(size, borderWidth, small) / 2),
20+
fontSize: Math.floor(Math.max(10, getSize(size, borderWidth, small) / 2)),
2121
fontWeight: 'bolder',
2222
}}
2323
className={altClassName}>

src/components/widgets/Badge/Badge.js

Lines changed: 111 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,137 @@ import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage, FormattedRelative } from 'react-intl';
44
import { Link } from 'react-router-dom';
5-
import { Tooltip, OverlayTrigger } from 'react-bootstrap';
5+
import { Tooltip, OverlayTrigger, Modal } from 'react-bootstrap';
66

7+
import UserName from '../../Users/UsersName';
8+
import EffectiveRoleSwitching from '../../Users/EffectiveRoleSwitching';
79
import withLinks from '../../../helpers/withLinks';
810
import Icon from '../../icons';
911
import AvatarContainer from '../../../containers/AvatarContainer/AvatarContainer';
12+
import { isSuperadminRole } from '../../helpers/usersRoles';
1013

1114
class Badge extends Component {
15+
state = { effectiveRoleDialogOpened: false, effectiveRoleUpdating: null };
16+
17+
openEffectiveRoleDialog = () => {
18+
this.setState({ effectiveRoleDialogOpened: true, effectiveRoleUpdating: null });
19+
};
20+
21+
closeEffectiveRoleDialog = () => {
22+
this.setState({ effectiveRoleDialogOpened: false, effectiveRoleUpdating: null });
23+
};
24+
25+
setEffectiveRole = role => {
26+
const { setEffectiveRole } = this.props;
27+
this.setState({ effectiveRoleUpdating: role });
28+
setEffectiveRole(role).then(() => {
29+
this.closeEffectiveRoleDialog();
30+
window.location.reload();
31+
});
32+
};
33+
1234
render() {
1335
const {
14-
id,
15-
fullName,
16-
name: { firstName },
17-
avatarUrl,
36+
user,
37+
effectiveRole,
1838
expiration,
1939
logout,
20-
size = 45,
21-
links: { USER_URI_FACTORY, EDIT_USER_URI_FACTORY },
40+
small = false,
41+
links: { EDIT_USER_URI_FACTORY },
2242
} = this.props;
2343

2444
return (
25-
<div className="user-panel">
26-
<div className="pull-left image">
27-
<AvatarContainer avatarUrl={avatarUrl} fullName={fullName} firstName={firstName} size={size} />
28-
</div>
29-
<div className="info">
30-
<p>
31-
<Link to={USER_URI_FACTORY(id)}>{fullName}</Link>
32-
</p>
33-
<Link to={EDIT_USER_URI_FACTORY(id)}>
34-
<Icon icon="edit" gapRight />
35-
<FormattedMessage id="generic.settings" defaultMessage="Settings" />
36-
</Link>
37-
&nbsp;
38-
<OverlayTrigger
39-
placement="right"
40-
overlay={
41-
<Tooltip id="tokenExpiration">
42-
<FormattedMessage id="app.badge.sessionExpiration" defaultMessage="Session expiration:" />{' '}
43-
<FormattedRelative value={expiration} />
44-
</Tooltip>
45-
}>
46-
<a
47-
href="#"
48-
onClick={e => {
49-
e.preventDefault();
50-
logout();
51-
}}>
52-
<Icon icon="sign-out-alt" className="text-danger" />
53-
&nbsp;
54-
<FormattedMessage id="app.logout" defaultMessage="Logout" />
55-
</a>
56-
</OverlayTrigger>
45+
<React.Fragment>
46+
<div className="user-panel">
47+
<div className="text-center">
48+
{small ? (
49+
<AvatarContainer
50+
avatarUrl={user.avatarUrl}
51+
fullName={user.fullName}
52+
firstName={user.name.firstName}
53+
size={small ? 32 : 42}
54+
/>
55+
) : (
56+
<UserName currentUserId={''} {...user} isVerified />
57+
)}
58+
</div>
59+
60+
<div className="small text-center halfem-margin-top">
61+
<Link to={EDIT_USER_URI_FACTORY(user.id)}>
62+
<Icon icon="edit" className="text-warning" gapRight={!small} />
63+
{!small && <FormattedMessage id="generic.settings" defaultMessage="Settings" />}
64+
</Link>
65+
66+
{small && <br />}
67+
68+
<OverlayTrigger
69+
placement="right"
70+
overlay={
71+
<Tooltip id="tokenExpiration">
72+
<FormattedMessage id="app.badge.sessionExpiration" defaultMessage="Session expiration:" />{' '}
73+
<FormattedRelative value={expiration} />
74+
</Tooltip>
75+
}>
76+
<a
77+
href="#"
78+
onClick={e => {
79+
e.preventDefault();
80+
logout();
81+
}}>
82+
<Icon icon="sign-out-alt" className="text-danger" largeGapLeft={!small} gapRight={!small} />
83+
{!small && <FormattedMessage id="app.logout" defaultMessage="Logout" />}
84+
</a>
85+
</OverlayTrigger>
86+
87+
{small && <br />}
88+
89+
{isSuperadminRole(user.privateData.role) && (
90+
<a
91+
href="#"
92+
onClick={e => {
93+
e.preventDefault();
94+
this.openEffectiveRoleDialog();
95+
}}>
96+
<Icon icon="user" className="text-primary" largeGapLeft={!small} gapRight={!small} />
97+
{!small && <FormattedMessage id="generic.role" defaultMessage="Role" />}
98+
</a>
99+
)}
100+
</div>
57101
</div>
58-
</div>
102+
103+
{isSuperadminRole(user.privateData.role) && (
104+
<Modal
105+
show={this.state.effectiveRoleDialogOpened}
106+
backdrop="static"
107+
onHide={this.closeEffectiveRoleDialog}
108+
bsSize="large">
109+
<Modal.Header closeButton>
110+
<Modal.Title>
111+
<FormattedMessage id="app.badge.effectiveRoleDialog.title" defaultMessage="Change Effective Role" />
112+
</Modal.Title>
113+
</Modal.Header>
114+
<Modal.Body>
115+
<UserName currentUserId={user.id} {...user} large size={45} />
116+
<EffectiveRoleSwitching
117+
effectiveRole={effectiveRole}
118+
updating={this.state.effectiveRoleUpdating}
119+
setEffectiveRole={this.setEffectiveRole}
120+
/>
121+
</Modal.Body>
122+
</Modal>
123+
)}
124+
</React.Fragment>
59125
);
60126
}
61127
}
62128

63129
Badge.propTypes = {
64-
id: PropTypes.string.isRequired,
65-
fullName: PropTypes.string.isRequired,
66-
name: PropTypes.shape({ firstName: PropTypes.string.isRequired }).isRequired,
67-
avatarUrl: PropTypes.string,
68-
expiration: PropTypes.number.isRequired,
69-
privateData: PropTypes.shape({ settings: PropTypes.object.isRequired }).isRequired,
130+
user: PropTypes.object.isRequired,
131+
effectiveRole: PropTypes.string,
132+
setEffectiveRole: PropTypes.func.isRequired,
70133
logout: PropTypes.func,
71-
size: PropTypes.number,
134+
expiration: PropTypes.number.isRequired,
135+
small: PropTypes.bool,
72136
links: PropTypes.object,
73137
};
74138

0 commit comments

Comments
 (0)