diff --git a/package.json b/package.json index 1f73bc104..2fa9157fb 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "private": true, "dependencies": { "emotion": "^9.0.2", + "interweave": "^8.0.2", + "makecancelable": "^1.0.0", "prop-types": "^15.6.1", "react": "^16.2.0", "react-dom": "^16.2.0", diff --git a/src/components/05_pages/Permissions/Permissions.js b/src/components/05_pages/Permissions/Permissions.js index db8ca5e9a..fd8679850 100644 --- a/src/components/05_pages/Permissions/Permissions.js +++ b/src/components/05_pages/Permissions/Permissions.js @@ -1,17 +1,160 @@ -import React from 'react'; -import { css } from 'emotion'; +import React, { Component, Fragment } from 'react'; +import makeCancelable from 'makecancelable'; +import { Markup } from 'interweave'; -const styles = { - title: css` - text-decoration: underline; - `, -}; +import Loading from '../../Helpers/Loading'; +import { Table, TBody, THead } from '../../UI/table'; -const Permissions = () => ( -
-

Permissions

-

This will be the permissions page.

-
-); +const Permissions = class Permissions extends Component { + state = { + loaded: false, + rawPermissions: [], + renderablePermissions: [], + }; + componentDidMount() { + this.cancelFetch = makeCancelable( + Promise.all([ + fetch( + `${ + process.env.REACT_APP_DRUPAL_BASE_URL + }/admin-api/permissions?_format=json`, + ).then(res => res.json()), + fetch( + `${ + process.env.REACT_APP_DRUPAL_BASE_URL + }/jsonapi/user_role/user_role`, + { headers: { Accept: 'application/vnd.api+json' } }, + ).then(res => res.json()), + ]) + .then(([permissions, { data: roles }]) => + this.setState({ + rawPermissions: permissions, + renderablePermissions: permissions, + changedRoles: [], + // Move admin roles to the right. + roles: roles.sort((a, b) => { + if (a.attributes.is_admin && b.attributes.is_admin) { + return a.attributes.id - b.attributes.id; + } else if (a.attributes.is_admin) { + return 1; + } else if (b.attributes.is_admin) { + return -1; + } + return a.attributes.id - b.attributes.id; + }), + loaded: true, + }), + ) + .catch(err => this.setState({ loaded: false, err })), + ); + } + componentWillUnmount() { + this.cancelFetch(); + } + onPermissionCheck = (roleName, permission) => { + this.setState(prevState => ({ + changedRoles: [...new Set(prevState.changedRoles).add(roleName).values()], + roles: this.togglePermission(permission, roleName, prevState.roles), + })); + }; + togglePermission = (permission, roleName, roles) => { + const roleIndex = roles.map(role => role.attributes.id).indexOf(roleName); + const role = roles[roleIndex]; + const index = role.attributes.permissions.indexOf(permission); + if (index !== -1) { + role.attributes.permissions.splice(index, 1); + } else { + role.attributes.permissions.push(permission); + } + roles[roleIndex] = role; + return roles; + }; + groupPermissions = permissions => + Object.entries( + permissions.reduce((acc, cur) => { + acc[cur.provider] = acc[cur.provider] || { + providerLabel: cur.provider_label, + permissions: [], + }; + acc[cur.provider].permissions.push(cur); + return acc; + }, {}), + ); + createTableRows = (groupedPermissions, roles) => + [].concat( + ...groupedPermissions.map( + ([providerMachineName, { providerLabel, permissions }]) => [ + { + key: `permissionGroup-${providerMachineName}`, + colspan: roles.length + 1, + tds: [[`td-${providerMachineName}`, {providerLabel}]], + }, + ...permissions.map(permission => ({ + key: `permissionGroup-${providerMachineName}-${permission.title}`, + tds: [ + [ + `td-${providerMachineName}-${permission.title}`, + , + ], + ...roles.map(({ attributes }, index) => [ + `td-${providerMachineName}-${permission.title}-${index}-cb`, + attributes.is_admin && attributes.id === 'administrator' ? ( + + ) : ( + + this.onPermissionCheck(attributes.id, permission.id) + } + checked={attributes.permissions.includes(permission.id)} + /> + ), + ]), + ], + })), + ], + ), + ); + handleKeyPress = event => { + const input = event.target.value.toLowerCase(); + this.setState(prevState => ({ + ...prevState, + renderablePermissions: prevState.rawPermissions.filter( + ({ title, description, provider, provider_label: providerLabel }) => + `${title}${description}${provider}${providerLabel}`.includes(input), + ), + })); + }; + render() { + return !this.state.loaded ? ( + + ) : ( + + + + + label.toUpperCase(), + ), + ]} + /> + +
+
+ ); + } +}; export default Permissions; diff --git a/src/components/Helpers/Loading.js b/src/components/Helpers/Loading.js new file mode 100644 index 000000000..72562817a --- /dev/null +++ b/src/components/Helpers/Loading.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { css, keyframes } from 'emotion'; + +const rotate = keyframes` + from { + transform: rotate(-10deg); + } + to { + transform: rotate(10deg); + } +`; + +const styles = { + wrap: css` + margin: 100px auto 0; + `, + peace: css` + display: inline-block; + vertical-align: middle; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-duration: 0.5s; + animation-timing-function: cubic-bezier(0, 0, 1, 1); + transform-origin: bottom; + font-size: 50px; + animation-name: ${rotate}; + `, +}; + +const Loading = () => ( +
+ + ✌️ + +
+); + +export default Loading; diff --git a/src/components/UI/table.js b/src/components/UI/table.js new file mode 100644 index 000000000..e029ec07f --- /dev/null +++ b/src/components/UI/table.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { css } from 'emotion'; +import { + node, + bool, + oneOfType, + arrayOf, + string, + shape, + number, +} from 'prop-types'; + +const TABLE = ({ children, zebra, ...props }) => { + const styles = css` + ${zebra ? 'tbody tr:nth-child(odd) {background-color: #e8e8e8;}' : ''}; + `; + return ( + + {children} +
+ ); +}; +TABLE.propTypes = { + children: oneOfType([arrayOf(node), node]).isRequired, + zebra: bool, +}; +TABLE.defaultProps = { + zebra: false, +}; + +const TR = ({ children, ...props }) => {children}; +TR.propTypes = { + children: oneOfType([arrayOf(node), node]).isRequired, +}; + +const TD = ({ children, ...props }) => {children}; +TD.propTypes = { + children: oneOfType([arrayOf(node), node]).isRequired, +}; + +const THEAD = ({ data }) => ( + + {data.map(label => {label})} + +); +THEAD.propTypes = { + data: arrayOf(string).isRequired, +}; + +const TBODY = ({ rows }) => ( + + {rows.map(({ colspan, tds, key }) => ( + + {tds.map(([tdKey, tdValue]) => ( + + {tdValue} + + ))} + + ))} + +); +TBODY.propTypes = { + rows: arrayOf( + shape({ + colspan: number, + key: string, + tds: arrayOf(node).isRequired, + }), + ).isRequired, +}; + +export { TR, TD, TABLE as Table, TBODY as TBody, THEAD as THead }; diff --git a/yarn.lock b/yarn.lock index 681b12ffa..ed726b912 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3700,6 +3700,13 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" +interweave@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/interweave/-/interweave-8.0.2.tgz#51001199c58d311f8b1d0100c4f6b9d494e97480" + dependencies: + babel-runtime "^6.26.0" + prop-types "^15.6.0" + invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688" @@ -4647,6 +4654,10 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" +makecancelable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/makecancelable/-/makecancelable-1.0.0.tgz#c7e2606e59db7a4bf8098ff5b52d7f13173499e5" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"