From 3f58d6e52ceb1c3e8311e1917d2d6c87c14cd3a7 Mon Sep 17 00:00:00 2001 From: sballesteros Date: Thu, 21 Nov 2019 22:24:29 -0800 Subject: [PATCH] admin panel skeleton #11 --- scripts/seed.js | 21 ++++---- src/components/admin-panel.css | 19 +++++++ src/components/admin-panel.js | 92 +++++++++++++++++++++++++++++++++ src/components/api.js | 28 ++++++++++ src/components/app.js | 6 ++- src/components/header-bar.js | 18 ++++++- src/components/private-route.js | 21 ++++++++ src/ddocs/ddoc-docs.js | 3 ++ src/hooks/api-hooks.js | 70 +++++++++++++++++++++++++ src/index.css | 1 + src/utils/search.js | 20 +++++++ 11 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 src/components/admin-panel.css create mode 100644 src/components/admin-panel.js diff --git a/scripts/seed.js b/scripts/seed.js index 12875f1..374bc6c 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -44,15 +44,18 @@ const N_USERS = 50; const users = []; console.log('Registering users...'); for (let i = 0; i < N_USERS; i++) { - const action = await db.post({ - '@type': 'RegisterAction', - actionStatus: 'CompletedActionStatus', - agent: { - '@type': 'Person', - orcid: createRandomOrcid(), - name: faker.name.findName() - } - }); + const action = await db.post( + { + '@type': 'RegisterAction', + actionStatus: 'CompletedActionStatus', + agent: { + '@type': 'Person', + orcid: createRandomOrcid(), + name: faker.name.findName() + } + }, + { isModerator: true } + ); console.log(`\t- ${action.result.name} (${action.result.orcid})`); let user = action.result; diff --git a/src/components/admin-panel.css b/src/components/admin-panel.css new file mode 100644 index 0000000..5af5d56 --- /dev/null +++ b/src/components/admin-panel.css @@ -0,0 +1,19 @@ +.admin-panel { + padding: calc(var(--header-bar-height) + var(--lgrd)) var(--mgrd) var(--lgrd); + max-width: 900px; + min-height: 100vh; + margin: 0 auto; + + & .admin-panel__header { + font: var(--ui-header-font); + border-bottom: 1px solid var(--ruling-color); + padding-bottom: var(--sgrd); + margin-bottom: var(--lgrd); + display: flex; + justify-content: space-between; + } + & .admin-panel__card-list { + list-style: none; + padding: 0; + } +} diff --git a/src/components/admin-panel.js b/src/components/admin-panel.js new file mode 100644 index 0000000..7bcdb01 --- /dev/null +++ b/src/components/admin-panel.js @@ -0,0 +1,92 @@ +import React, { useState, useEffect } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { MdChevronRight, MdFirstPage, MdClose } from 'react-icons/md'; +import { useUser } from '../contexts/user-context'; +import { getId, unprefix } from '../utils/jsonld'; +import HeaderBar from './header-bar'; +import { ORG } from '../constants'; +import { createModeratorQs } from '../utils/search'; +import { useRolesSearchResults } from '../hooks/api-hooks'; +import Button from './button'; +import IconButton from './icon-button'; +import { RoleBadgeUI } from './role-badge'; + +export default function AdminPanel() { + const [user] = useUser(); + const [bookmark, setBookmark] = useState(null); + + const search = createModeratorQs({ bookmark }); + + const [results, progress] = useRolesSearchResults(search); + + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + return ( +
+ + {ORG} • Admin panel + + + +
+
+ Manage moderators +
+ + {results.total_rows === 0 && !progress.isActive ? ( +
No moderators.
+ ) : ( +
+ + +
    + {results.rows.map(({ doc: role }) => ( +
  • + + {role.name || unprefix(getId(role))} + + {role['@type'] === 'AnonymousReviewerRole' + ? 'Anonymous' + : 'Public'} + + + + + +
  • + ))} +
+
+ )} + +
+ {/* Cloudant returns the same bookmark when it hits the end of the list */} + {!!bookmark && ( + + )} + + {!!( + results.rows.length < results.total_rows && + results.bookmark !== bookmark + ) && ( + + )} +
+
+
+ ); +} diff --git a/src/components/api.js b/src/components/api.js index 0a76b66..5290b75 100644 --- a/src/components/api.js +++ b/src/components/api.js @@ -131,6 +131,34 @@ export default function API() { + + + isModerated + + + A boolean indicating that the role has been moderated following + repeated violations of the{' '} + + Code of Conduct + + + + Boolean (one of true, false) + + + + + + isModerator + + + A boolean indicating that the role has moderator permissions + + + Boolean (one of true, false) + + + isRoleOfId diff --git a/src/components/app.js b/src/components/app.js index 7ee0bb3..056d4dc 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -14,7 +14,7 @@ import { StoresProvider } from '../contexts/store-context'; import Login from './login'; import Settings from './settings'; import Profile from './profile'; -import PrivateRoute from './private-route'; +import PrivateRoute, { AdminRoute } from './private-route'; import ModeratorRoute from './moderator-route'; import ExtensionSplash from './extension-splash'; import ToCPage from './toc-page'; @@ -22,6 +22,7 @@ import CodeOfConduct from './code-of-conduct'; import NotFound from './not-found'; import API from './api'; import Moderate from './moderate'; +import AdminPanel from './admin-panel'; // kick off the polyfill! smoothscroll.polyfill(); @@ -64,6 +65,9 @@ export default function App({ user }) { + + + diff --git a/src/components/header-bar.js b/src/components/header-bar.js index 7b32d83..44da371 100644 --- a/src/components/header-bar.js +++ b/src/components/header-bar.js @@ -55,8 +55,24 @@ export default function HeaderBar({ onClickMenuButton }) { } target={process.env.IS_EXTENSION ? '_blank' : undefined} > - Settings + Profile Settings + + {user.isAdmin && ( + + Admin Settings + + )} + {!!(role && role.isModerator && !role.isModerated) && ( that redirects to the login // screen if you're not yet authenticated. @@ -14,3 +15,23 @@ export default function PrivateRoute({ children, ...rest }) { PrivateRoute.propTypes = { children: PropTypes.any.isRequired }; + +export function AdminRoute({ children, ...rest }) { + const [user] = useUser(); + + return ( + + {user && user.isAdmin ? ( + children + ) : user && !user.isAdmin ? ( + + ) : ( + + )} + + ); +} + +AdminRoute.propTypes = { + children: PropTypes.any.isRequired +}; diff --git a/src/ddocs/ddoc-docs.js b/src/ddocs/ddoc-docs.js index 72e98ab..0852d03 100644 --- a/src/ddocs/ddoc-docs.js +++ b/src/ddocs/ddoc-docs.js @@ -133,6 +133,9 @@ const ddoc = { index('isRoleOfId', doc.isRoleOf); } + index('isModerated', !!doc.isModerated, { facet: true }); + index('isModerator', !!doc.isModerator, { facet: true }); + var startDate = doc.startDate ? new Date(doc.startDate).getTime() : new Date('0000').getTime(); diff --git a/src/hooks/api-hooks.js b/src/hooks/api-hooks.js index 01a9ff5..026e4c9 100644 --- a/src/hooks/api-hooks.js +++ b/src/hooks/api-hooks.js @@ -685,3 +685,73 @@ export function useActionsSearchResults( return [results, progress, append]; } + +/** + * Search using the `roles` index + */ +export function useRolesSearchResults( + search, // the ?qs part of the url + append = false +) { + const [progress, setProgress] = useState({ + isActive: false, + error: null + }); + + const [results, setResults] = useState(DEFAULT_SEARCH_RESULTS); + + useEffect(() => { + setProgress({ + isActive: true, + error: null + }); + if (!append) { + setResults(DEFAULT_SEARCH_RESULTS); + } + + const controller = new AbortController(); + + fetch(`${process.env.API_URL}/api/role/${search}`, { + signal: controller.signal + }) + .then(resp => { + if (resp.ok) { + return resp.json(); + } else { + return resp.json().then( + body => { + throw createError(resp.status, body.description || body.name); + }, + err => { + throw createError(resp.status, 'something went wrong'); + } + ); + } + }) + .then(data => { + if (append) { + setResults(prevResults => { + return Object.assign(data, { + rows: prevResults.rows.concat(data.rows) + }); + }); + } else { + setResults(data); + } + + setProgress({ isActive: false, error: null }); + }) + .catch(err => { + if (err.name !== 'AbortError') { + setProgress({ isActive: false, error: err }); + setResults(DEFAULT_SEARCH_RESULTS); + } + }); + + return () => { + controller.abort(); + }; + }, [search, append]); + + return [results, progress, append]; +} diff --git a/src/index.css b/src/index.css index e423502..7b66ea5 100644 --- a/src/index.css +++ b/src/index.css @@ -9,6 +9,7 @@ @import './components/variables.css'; @import './components/app.css'; +@import './components/admin-panel.css'; @import './components/add-button.css'; @import './components/api.css'; @import './components/api-section.css'; diff --git a/src/utils/search.js b/src/utils/search.js index d1cec1b..93a91af 100644 --- a/src/utils/search.js +++ b/src/utils/search.js @@ -229,6 +229,26 @@ export function createModerationQs({ bookmark }) { return sapi ? `?${sapi}` : undefined; } +export function createModeratorQs({ bookmark }) { + const api = new URLSearchParams(); + + api.set('q', `isModerator:true`); + + if (bookmark) { + api.set('bookmark', bookmark); + } else { + api.delete('bookmark'); + } + + api.set('sort', JSON.stringify(['-startDate'])); + api.set('include_docs', true); + api.set('limit', 10); + + const sapi = api.toString(); + + return sapi ? `?${sapi}` : undefined; +} + function escapeLucene(term) { return term.replace(/([+&|!(){}[\]^"~*?:\\\/-])/g, '\\$1'); }