Skip to content

Commit 05b51be

Browse files
author
Matthew Grill
committed
Permissions
1 parent 5ad7c58 commit 05b51be

File tree

6 files changed

+270
-13
lines changed

6 files changed

+270
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"dependencies": {
66
"emotion": "^9.0.2",
7+
"makecancelable": "^1.0.0",
78
"prop-types": "^15.6.1",
89
"react": "^16.2.0",
910
"react-dom": "^16.2.0",
Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,121 @@
1-
import React from 'react';
2-
import { css } from 'emotion';
1+
import React, { Component, Fragment } from 'react';
2+
import makeCancelable from 'makecancelable';
33

4-
const styles = {
5-
title: css`
6-
text-decoration: underline;
7-
`,
8-
};
4+
import Loading from '../../Helpers/Loading';
5+
6+
import { Table, TableBody, TableHeaderSimple } from '../../UI';
97

10-
const Permissions = () => (
11-
<div>
12-
<h1 className={styles.title}>Permissions</h1>
13-
<p>This will be the permissions page.</p>
14-
</div>
15-
);
8+
const Permissions = class Permissions extends Component {
9+
state = {
10+
loaded: false,
11+
};
12+
componentDidMount() {
13+
this.cancelFetch = makeCancelable(
14+
Promise.all([
15+
fetch(
16+
`${
17+
process.env.REACT_APP_DRUPAL_BASE_URL
18+
}/admin-api/permissions?_format=json`,
19+
).then(res => res.json()),
20+
fetch(
21+
`${
22+
process.env.REACT_APP_DRUPAL_BASE_URL
23+
}/jsonapi/user_role/user_role`,
24+
).then(res => res.json()),
25+
])
26+
.then(([permissions, { data: roles }]) =>
27+
this.setState({
28+
rawPermissions: Object.keys(permissions).map(
29+
key => permissions[key],
30+
),
31+
renderablePermissions: Object.keys(permissions).map(
32+
key => permissions[key],
33+
),
34+
roles,
35+
loaded: true,
36+
}),
37+
)
38+
.catch(err => this.setState({ loaded: false, err })),
39+
);
40+
}
41+
componentWillUnmount() {
42+
this.cancelFetch();
43+
}
44+
groupPermissions = permissions =>
45+
Object.entries(
46+
permissions.reduce((acc, cur) => {
47+
acc[cur.provider] = acc[cur.provider] || [];
48+
acc[cur.provider].push(cur);
49+
return acc;
50+
}, {}),
51+
);
52+
createTableRows = (groupedPermissions, roles) =>
53+
[].concat(
54+
...groupedPermissions.map(([permissionGroupName, permissions]) => [
55+
{
56+
key: `permissionGroup-${permissionGroupName}`,
57+
colspan: roles.length + 1,
58+
tds: [<b>{permissionGroupName}</b>],
59+
},
60+
...permissions.map(permission => ({
61+
key: `permissionGroup-${permissionGroupName}-${permission.title}`,
62+
tds: [
63+
permission.title,
64+
...roles.map(
65+
({ attributes }) =>
66+
attributes.is_admin && attributes.id === 'administrator' ? (
67+
<input type="checkbox" checked />
68+
) : (
69+
<input
70+
type="checkbox"
71+
checked={attributes.permissions.includes(permission.id)}
72+
/>
73+
),
74+
),
75+
],
76+
})),
77+
]),
78+
);
79+
handleKeyPress = event => {
80+
const input = event.target.value.toLowerCase();
81+
this.setState(prevState => ({
82+
...prevState,
83+
renderablePermissions: prevState.rawPermissions.filter(
84+
({ title, description, provider, provider_label }) =>
85+
`${title}${description}${provider}${provider_label}`.includes(input),
86+
),
87+
}));
88+
};
89+
render() {
90+
return !this.state.loaded ? (
91+
<Loading />
92+
) : (
93+
<Fragment>
94+
<input
95+
type="text"
96+
placeholder="Filter by name, description or module"
97+
onChange={this.handleKeyPress}
98+
onKeyDown={this.handleKeyPress}
99+
/>
100+
<Table zebra>
101+
<TableHeaderSimple
102+
data={[
103+
'PERMISSION',
104+
...this.state.roles.map(({ attributes: { label } }) =>
105+
label.toUpperCase(),
106+
),
107+
]}
108+
/>
109+
<TableBody
110+
rows={this.createTableRows(
111+
this.groupPermissions(this.state.renderablePermissions),
112+
this.state.roles,
113+
)}
114+
/>
115+
</Table>
116+
</Fragment>
117+
);
118+
}
119+
};
16120

17121
export default Permissions;

src/components/Helpers/Loading.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { css } from 'emotion';
3+
4+
const spinner = css`
5+
margin: 100px auto 0;
6+
width: 70px;
7+
text-align: center;
8+
> div {
9+
width: 10px;
10+
height: 10px;
11+
background-color: #333;
12+
13+
border-radius: 100%;
14+
display: inline-block;
15+
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
16+
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
17+
}
18+
.bounce1 {
19+
-webkit-animation-delay: -0.32s;
20+
animation-delay: -0.32s;
21+
}
22+
.bounce2 {
23+
-webkit-animation-delay: -0.16s;
24+
animation-delay: -0.16s;
25+
}
26+
@-webkit-keyframes sk-bouncedelay {
27+
0%,
28+
80%,
29+
100% {
30+
-webkit-transform: scale(0);
31+
}
32+
40% {
33+
-webkit-transform: scale(1);
34+
}
35+
}
36+
@keyframes sk-bouncedelay {
37+
0%,
38+
80%,
39+
100% {
40+
-webkit-transform: scale(0);
41+
transform: scale(0);
42+
}
43+
40% {
44+
-webkit-transform: scale(1);
45+
transform: scale(1);
46+
}
47+
}
48+
`;
49+
50+
const Loading = () => (
51+
<div className={spinner}>
52+
<div className="bounce1" />
53+
<div className="bounce2" />
54+
<div className="bounce3" />
55+
</div>
56+
);
57+
58+
export default Loading;

src/components/UI/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TR, TD, TABLE, TBODY, THEAD } from './table';
2+
3+
export {
4+
TR as TableRow,
5+
TD as TableData,
6+
TABLE as Table,
7+
TBODY as TableBody,
8+
THEAD as TableHeaderSimple,
9+
};

src/components/UI/table.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React from 'react';
2+
import { css } from 'emotion';
3+
import {
4+
node,
5+
bool,
6+
oneOfType,
7+
arrayOf,
8+
string,
9+
shape,
10+
number,
11+
} from 'prop-types';
12+
13+
const generateIDs = arr =>
14+
arr.map(value => ({
15+
value,
16+
id: Math.random()
17+
.toString(36)
18+
.substring(2),
19+
}));
20+
21+
const TABLE = ({ children, zebra, ...props }) => {
22+
const styles = css`
23+
${zebra ? 'tbody tr:nth-child(odd) {background-color: #e8e8e8;}' : ''};
24+
`;
25+
return (
26+
<table className={styles} {...props}>
27+
{children}
28+
</table>
29+
);
30+
};
31+
TABLE.propTypes = {
32+
children: oneOfType([arrayOf(node), node]).isRequired,
33+
zebra: bool,
34+
};
35+
TABLE.defaultProps = {
36+
zebra: false,
37+
};
38+
39+
const TR = ({ children, ...props }) => <tr {...props}>{children}</tr>;
40+
TR.propTypes = {
41+
children: oneOfType([arrayOf(node), node]).isRequired,
42+
};
43+
44+
const TD = ({ children, ...props }) => <td {...props}>{children}</td>;
45+
TD.propTypes = {
46+
children: oneOfType([arrayOf(node), node]).isRequired,
47+
};
48+
49+
const THEAD = ({ data }) => (
50+
<thead>
51+
<TR>{data.map(label => <TD key={`column-${label}`}>{label}</TD>)}</TR>
52+
</thead>
53+
);
54+
THEAD.propTypes = {
55+
data: arrayOf(string).isRequired,
56+
};
57+
58+
const TBODY = ({ rows }) => (
59+
<tbody>
60+
{rows.map(({ colspan, tds, key }) => (
61+
<TR key={key}>
62+
{generateIDs(tds).map(({ id, value }) => (
63+
<TD key={`td-${key}-${id}`} colSpan={colspan || undefined}>
64+
{value}
65+
</TD>
66+
))}
67+
</TR>
68+
))}
69+
</tbody>
70+
);
71+
TBODY.propTypes = {
72+
rows: arrayOf(
73+
shape({
74+
colspan: number,
75+
key: string,
76+
tds: arrayOf(node).isRequired,
77+
}),
78+
).isRequired,
79+
};
80+
81+
export { TR, TD, TABLE, TBODY, THEAD };

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4647,6 +4647,10 @@ make-dir@^1.0.0:
46474647
dependencies:
46484648
pify "^3.0.0"
46494649

4650+
makecancelable@^1.0.0:
4651+
version "1.0.0"
4652+
resolved "https://registry.yarnpkg.com/makecancelable/-/makecancelable-1.0.0.tgz#c7e2606e59db7a4bf8098ff5b52d7f13173499e5"
4653+
46504654
46514655
version "1.0.11"
46524656
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"

0 commit comments

Comments
 (0)