Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add admin panel with search users (#960)
- Loading branch information
Showing
24 changed files
with
1,102 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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']); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
150
modules/admin/client/components/AdminSearchUsers.component.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = {}; |
Oops, something went wrong.