Skip to content

Commit

Permalink
Add admin panel with search users (#960)
Browse files Browse the repository at this point in the history
  • Loading branch information
Guaka authored and simison committed May 11, 2019
1 parent cd3eb6d commit a746ad1
Show file tree
Hide file tree
Showing 24 changed files with 1,102 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Expand Up @@ -172,6 +172,8 @@ module.exports = {
// ES 2018 - specify migrated files and folders here
files: [
'bin/db-maintenance/ensure-indexes.js',
'modules/admin/server/**',
'modules/admin/tests/**',
'modules/references/server/**',
'modules/references/tests/server/**',
'modules/users/tests/server/user-change-locale.server.routes.tests.js',
Expand All @@ -187,6 +189,7 @@ module.exports = {
'config/client/**',
'config/env/**',
'config/webpack/**',
'modules/admin/client/**',
'modules/core/client/app/config.js',
'modules/**/client/components/**',
'modules/**/client/api/**',
Expand Down
1 change: 1 addition & 0 deletions config/lib/express.js
Expand Up @@ -483,6 +483,7 @@ module.exports.initHelmetHeaders = function (app) {
module.exports.initModulesClientRoutes = function (app) {
// Setting the app router and static folder
app.use('/', express.static(path.resolve('./public')));
app.use('/', express.static(path.resolve('./public/assets')));

// Globbing static routing
config.folders.client.forEach(function (staticPath) {
Expand Down
12 changes: 12 additions & 0 deletions config/webpack/webpack.config.js
Expand Up @@ -43,6 +43,18 @@ module.exports = merge(shims, {
]
}
}]
},
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]-[hash].[ext]',
outputPath: 'images/'
}
}
]
}
]
},
Expand Down
5 changes: 5 additions & 0 deletions modules/admin/client/admin.client.module.js
@@ -0,0 +1,5 @@
'use strict';

// Use application configuration module to register a new module
// The core module is required for special route handling; see /core/client/config/core.client.routes
AppConfig.registerModule('admin', ['core']);
11 changes: 11 additions & 0 deletions modules/admin/client/api/users.api.js
@@ -0,0 +1,11 @@
import axios from 'axios';

export async function searchUsers(search) {
const { data } = await axios.post('/api/admin/users', { search });
return data;
}

export async function getUser(id) {
const { data } = await axios.post('/api/admin/user', { id });
return data;
}
21 changes: 21 additions & 0 deletions modules/admin/client/components/Admin.component.js
@@ -0,0 +1,21 @@
import React from 'react';
import AdminHeader from './AdminHeader.component.js';
import bmoDancing from '../images/bmo-dancing.gif';

export default function Admin() {
return (
<>
<AdminHeader />
<div className="container container-spacer">
<p><img src={ bmoDancing } alt="" width="200" /></p>
<p>Welcome, friend! 👋</p>
<p>
See our <a href="https://team.trustroots.org/">Team Guide</a> & <a href="https://trustroots.zendesk.com/inbox/">Support queue</a>.
</p>
<p><strong><em>Remember to logout on public computers!</em></strong></p>
</div>
</>
);
}

Admin.propTypes = {};
35 changes: 35 additions & 0 deletions modules/admin/client/components/AdminHeader.component.js
@@ -0,0 +1,35 @@
// External dependencies
import classnames from 'classnames';
import React from 'react';

// Internal dependencies
import adminIcon from '../images/bmo.png';

export default function AdminHeader() {
const path = window.location.pathname;
return (
<>
<br/><br/>
<nav className="navbar navbar-white navbar-admin">
<div className="container">
<div className="navbar-header">
<a className="navbar-brand" href="/admin" aria-label="Admin dash index">
<img src={ adminIcon } height="24" alt="" aria-hidden="true" focusable="false" />
</a>
</div>
<ul className="nav navbar-nav">
<li className={ classnames({ 'active': path === '/admin/user' })}>
<a href="/admin/user">Show user</a>
</li>
<li className={ classnames({ 'active': path === '/admin/search-users' })}>
<a href="/admin/search-users">Search users</a>
</li>
</ul>
<p className="navbar-text pull-right text-muted hidden-xs"><em>Admin dash</em></p>
</div>
</nav>
</>
);
}

AdminHeader.propTypes = {};
150 changes: 150 additions & 0 deletions modules/admin/client/components/AdminSearchUsers.component.js
@@ -0,0 +1,150 @@
// External dependencies
import React, { Component } from 'react';

// Internal dependencies
import { searchUsers } from '../api/users.api';
import AdminHeader from './AdminHeader.component.js';
import UserState from './UserState.component.js';
import ZendeskInboxSearch from './ZendeskInboxSearch.component.js';

// Limitations set in the API
const SEARCH_USERS_LIMIT = 50;
const SEARCH_STRING_LIMIT = 3;

export default class AdminSearchUsers extends Component {
constructor(props) {
super(props);
this.onSearchChange = this.onSearchChange.bind(this);
this.doSearch = this.doSearch.bind(this);
this.state = {
search: '',
userResults: []
};
}

componentDidMount() {
const urlParams = new URLSearchParams(window.location.search);
const search = urlParams.get('search');
if (search) {
this.setState({ search }, this.doSearch);
}
}

onSearchChange(event) {
const search = event.target.value;
this.setState({ search });

// Update URL
const url = new URL(document.location);
url.searchParams.set('search', search);
window.history.pushState(
{ search },
window.document.title,
url.toString()
);
}

async doSearch(event) {
if (event) {
event.preventDefault();
}
const { search } = this.state;
if (search.length >= SEARCH_STRING_LIMIT) {
const userResults = await searchUsers(search);
this.setState({ userResults });
}
}

render() {
const { userResults } = this.state;

return (
<>
<AdminHeader />
<div className="container">
<h2>Search users</h2>

<form onSubmit={ this.doSearch } className="form-inline">
<label>
Name, username or email<br/>
<input
className="form-control input-lg"
type="search"
value={ this.state.search }
onChange={ this.onSearchChange }
/>
</label>
<button
className="btn btn-lg btn-default"
disabled={ this.state.search.length < SEARCH_STRING_LIMIT }
type="submit"
>
Search
</button>
</form>

{ userResults.length ? (
<div className="panel panel-default">
<div className="panel-body">
<table className="table table-striped table-responsive">
<thead>
<tr>
<th>Username</th>
<th>Name</th>
<th>Email</th>
<th>ID</th>
</tr>
</thead>
<tbody>
{
userResults.map((user) => {
const { _id, displayName, email, emailTemporary, username } = user;
return (
<tr key={_id}>
<td className="admin-search-users__actions">
<a href={'/profile/' + username} title="Profile on Trustroots">{ username }</a>
<UserState user={ user } />
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ username } />
<a
className="admin-action admin-hidden-until-hover"
href={ `/admin/user?id=${ _id }` }
>
Show more
</a>
</td>
<td>
{ displayName }
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ displayName } />
</td>
<td>
{ email }
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ email } />
{ (emailTemporary && emailTemporary !== email) && (
<>
<br/>
{ emailTemporary } (temporary email)
<ZendeskInboxSearch className="admin-action admin-hidden-until-hover" q={ emailTemporary } />
</>
) }
</td>
<td><small><code style={ { 'userSelect': 'all' } }>{ _id }</code></small></td>
</tr>
);
})
}
</tbody>
</table>
</div>
<div className="panel-footer">
{ userResults.length } user(s).
{ userResults.length === SEARCH_USERS_LIMIT && <p className="text-warning">There might be more results but { SEARCH_USERS_LIMIT } is maximum.</p>}
</div>
</div>
) : <p><br/><em className="text-muted">Search something...</em></p> }
</div>
</>
);
};
}

AdminSearchUsers.propTypes = {};
103 changes: 103 additions & 0 deletions modules/admin/client/components/AdminUser.component.js
@@ -0,0 +1,103 @@
// External dependencies
import React, { Component } from 'react';

// Internal dependencies
import { getUser } from '../api/users.api';
import AdminHeader from './AdminHeader.component.js';
import UserState from './UserState.component.js';

// Mongo ObjectId is always 24 chars long
const MONGO_OBJECT_ID_LENGTH = 24;

export default class AdminUser extends Component {
constructor(props) {
super(props);
this.onIdChange = this.onIdChange.bind(this);
this.queryUser = this.queryUser.bind(this);
this.state = { id: '', user: false };
}

componentDidMount() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');

if (id && id.length === MONGO_OBJECT_ID_LENGTH) {
this.setState({ id }, this.queryUser);
}
}

onIdChange(event) {
const id = event.target.value;
this.setState({ id });

// Update URL
const url = new URL(document.location);
url.searchParams.set('id', id);
window.history.pushState(
{ id },
window.document.title,
url.toString()
);
}

queryUser(event) {
if (event) {
event.preventDefault();
}

const { id } = this.state;

this.setState({ user: false }, async () => {
if (id.length === MONGO_OBJECT_ID_LENGTH) {
const user = await getUser(id);
this.setState({ user });
}
});
}

render() {
const { user } = this.state;

return (
<>
<AdminHeader />
<div className="container">

<h2>Show user</h2>

<form onSubmit={ this.queryUser } className="form-inline">
<label>
User ID<br/>
<input
className="form-control input-lg"
onChange={ this.onIdChange }
size={ MONGO_OBJECT_ID_LENGTH + 2 }
maxLength={ MONGO_OBJECT_ID_LENGTH }
type="text"
value={ this.state.id }
/>
</label>
<button
className="btn btn-lg btn-default"
disabled={ this.state.id.length !== MONGO_OBJECT_ID_LENGTH }
type="submit"
>
Show
</button>
</form>

{ user && (
<div className="panel panel-default">
<div className="panel-body">
<UserState user={ user } />
<pre>{ JSON.stringify(user, null, 2) }</pre>
</div>
</div>
) }
</div>
</>
);
};
}

AdminUser.propTypes = {};

0 comments on commit a746ad1

Please sign in to comment.