Skip to content
This repository has been archived by the owner on Apr 29, 2022. It is now read-only.

Commit

Permalink
admin panel skeleton #11
Browse files Browse the repository at this point in the history
  • Loading branch information
sballesteros committed Nov 22, 2019
1 parent e196005 commit 3f58d6e
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 11 deletions.
21 changes: 12 additions & 9 deletions scripts/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/components/admin-panel.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
92 changes: 92 additions & 0 deletions src/components/admin-panel.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="admin-panel">
<Helmet>
<title>{ORG} • Admin panel</title>
</Helmet>
<HeaderBar />

<section>
<header className="admin-panel__header">
<span>Manage moderators</span>
</header>

{results.total_rows === 0 && !progress.isActive ? (
<div>No moderators.</div>
) : (
<div>
<Button primary={true}>Add moderator</Button>

<ul className="admin-panel__card-list">
{results.rows.map(({ doc: role }) => (
<li key={getId(role)}>
<RoleBadgeUI role={role} />
<span>{role.name || unprefix(getId(role))}</span>
<span>
{role['@type'] === 'AnonymousReviewerRole'
? 'Anonymous'
: 'Public'}
</span>

<IconButton>
<MdClose />
</IconButton>
</li>
))}
</ul>
</div>
)}

<div>
{/* Cloudant returns the same bookmark when it hits the end of the list */}
{!!bookmark && (
<Button
onClick={() => {
setBookmark(null);
}}
>
<MdFirstPage /> First page
</Button>
)}

{!!(
results.rows.length < results.total_rows &&
results.bookmark !== bookmark
) && (
<Button
onClick={() => {
setBookmark(results.bookmark);
}}
>
Next Page <MdChevronRight />
</Button>
)}
</div>
</section>
</div>
);
}
28 changes: 28 additions & 0 deletions src/components/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ export default function API() {
</td>
</tr>

<tr>
<td>
<code>isModerated</code>
</td>
<td>
A boolean indicating that the role has been moderated following
repeated violations of the{' '}
<XLink href="/code-of-conduct" to="/code-of-conduct">
Code of Conduct
</XLink>
</td>
<td>
Boolean (one of <code>true</code>, <code>false</code>)
</td>
</tr>

<tr>
<td>
<code>isModerator</code>
</td>
<td>
A boolean indicating that the role has moderator permissions
</td>
<td>
Boolean (one of <code>true</code>, <code>false</code>)
</td>
</tr>

<tr>
<td>
<code>isRoleOfId</code>
Expand Down
6 changes: 5 additions & 1 deletion src/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ 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';
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();
Expand Down Expand Up @@ -64,6 +65,9 @@ export default function App({ user }) {
<PrivateRoute exact={true} path="/settings">
<Settings />
</PrivateRoute>
<AdminRoute exact={true} path="/admin">
<AdminPanel />
</AdminRoute>
<ModeratorRoute exact={true} path="/moderate">
<Moderate />
</ModeratorRoute>
Expand Down
18 changes: 17 additions & 1 deletion src/components/header-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,24 @@ export default function HeaderBar({ onClickMenuButton }) {
}
target={process.env.IS_EXTENSION ? '_blank' : undefined}
>
Settings
Profile Settings
</MenuLink>

{user.isAdmin && (
<MenuLink
as={process.env.IS_EXTENSION ? undefined : Link}
to={process.env.IS_EXTENSION ? undefined : '/admin'}
href={
process.env.IS_EXTENSION
? `${process.env.API_URL}/admin`
: undefined
}
target={process.env.IS_EXTENSION ? '_blank' : undefined}
>
Admin Settings
</MenuLink>
)}

{!!(role && role.isModerator && !role.isModerated) && (
<MenuLink
as={process.env.IS_EXTENSION ? undefined : Link}
Expand Down
21 changes: 21 additions & 0 deletions src/components/private-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Route, Redirect } from 'react-router-dom';
import { useUser } from '../contexts/user-context';
import NotFound from './not-found';

// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
Expand All @@ -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 (
<Route {...rest}>
{user && user.isAdmin ? (
children
) : user && !user.isAdmin ? (
<NotFound />
) : (
<Redirect to="/login" />
)}
</Route>
);
}

AdminRoute.propTypes = {
children: PropTypes.any.isRequired
};
3 changes: 3 additions & 0 deletions src/ddocs/ddoc-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
70 changes: 70 additions & 0 deletions src/hooks/api-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
20 changes: 20 additions & 0 deletions src/utils/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>']));
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');
}

0 comments on commit 3f58d6e

Please sign in to comment.