Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pagination to dashboard list views #23

Merged
merged 6 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.now
node_modules/
.next/
/npm-debug.log
.DS_Store
.env*
!.env.tmp
.vercel
*.log
77 changes: 43 additions & 34 deletions components/ErrorEventsDashboard.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import { ExternalLinkAlt } from '@styled-icons/fa-solid/ExternalLinkAlt'
import moment from 'moment'
import { Button } from 'react-bootstrap'
import useSWR, { mutate } from 'swr'
import { useState } from 'react'
import useSWR from 'swr'

import FetchMessage from './FetchMessage'
import PageControls from './PageControls'

const ERROR_EVENTS_URL = `${process.env.API_BASE_URL}/api/admin/bugsnag/eventsummary`

function ErrorEventsDashboard () {
const result = useSWR(ERROR_EVENTS_URL)
const { data, error } = result
const events = data && data.data
const hasEvents = events && events.length > 0
const MAX_EVENTS_TO_DISPLAY = 100
const [offset, setOffset] = useState(0)
const limit = 40
landonreed marked this conversation as resolved.
Show resolved Hide resolved
const url = `${ERROR_EVENTS_URL}?offset=${offset}&limit=${limit}`
const result = useSWR(url)
const { data: events } = result
console.log(result)
landonreed marked this conversation as resolved.
Show resolved Hide resolved
const hasEvents = events && events.data && events.data.length > 0
return (
<div>
<h2>Error Events Summary</h2>
<div className='controls'>
<Button className='mr-3' onClick={() => mutate(ERROR_EVENTS_URL)}>
Fetch errors
</Button>
<FetchMessage result={result} />
<h2>
Error Events Summary
</h2>
<h5>
<a
className='push'
target='_blank'
rel='noopener noreferrer'
href='https://app.bugsnag.com/'
>
<ExternalLinkAlt className='mr-1 mb-1' size={20} />
Open Bugsnag console
</a>
</div>
</h5>
landonreed marked this conversation as resolved.
Show resolved Hide resolved
<PageControls
limit={limit}
offset={offset}
setOffset={setOffset}
showSkipButtons
result={result} />
landonreed marked this conversation as resolved.
Show resolved Hide resolved
{hasEvents
? (
<div>
{error && <pre>Error loading events: {error}</pre>}
<p>
{events.length} error events recorded over the last two weeks
{events.length > MAX_EVENTS_TO_DISPLAY && ` (showing first ${MAX_EVENTS_TO_DISPLAY})`}
</p>
<table>
<thead>
<tr>
Expand All @@ -50,22 +53,20 @@ function ErrorEventsDashboard () {
should split up the errors according to the project they are
assigned to.
*/}
{events
{events.data
.sort((a, b) => moment(b.received) - moment(a.received))
// FIXME: Add pagination to server/UI.
.filter((event, index) => index <= MAX_EVENTS_TO_DISPLAY)
.map((event, eventIndex) => {
// TODO: these fields are subject to change pending backend
// changes.
return (
<tr key={eventIndex}>
<td>{event.projectName}</td>
<td>
<td className='component'>{event.projectName}</td>
<td className='details'>
{event.exceptions.map((e, i) =>
<span key={i}>{e.errorClass}: {e.message}</span>)
<span key={i}><strong>{e.errorClass}</strong>: {e.message}</span>)
}
</td>
<td>{moment(event.received).format('D MMM HH:mm')}</td>
<td className='date'>{moment(event.received).format('D MMM h:mm a')}</td>
</tr>
)
})}
Expand All @@ -76,19 +77,27 @@ function ErrorEventsDashboard () {
: 'No errors reported in the last two weeks.'
}
<style jsx>{`
landonreed marked this conversation as resolved.
Show resolved Hide resolved
.controls {
align-items: center;
display: flex;
}
.fetchMessage {
margin-left: 5px;
}
.push {
margin-left: auto;
}
td {
padding-right: 10px;
}
td.component {
vertical-align: top;
font-size: x-small;
white-space: nowrap;
}
td.details {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* number of lines to show */
-webkit-box-orient: vertical;
}
td.date {
white-space: nowrap;
}
`}
</style>
</div>
Expand Down
9 changes: 1 addition & 8 deletions components/FetchMessage.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import moment from 'moment'
import { Spinner } from 'react-bootstrap'

import { DEFAULT_REFRESH_MILLIS } from '../util/constants'

function FetchMessage ({ result }) {
const timestamp = result.data && result.data.timestamp
const spinner = (
Expand All @@ -16,12 +14,7 @@ function FetchMessage ({ result }) {
: result.isValidating
? spinner
: `Updated at ${timeString}`
return (
<small>
{fetchMessage}<br />
<small>auto-refreshes every {DEFAULT_REFRESH_MILLIS / 1000} seconds</small>
</small>
)
return <small>{fetchMessage}</small>
}

export default FetchMessage
59 changes: 59 additions & 0 deletions components/PageControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Sync } from '@styled-icons/fa-solid/Sync'
import { Button, Pagination } from 'react-bootstrap'

function PageControls ({ limit, offset, result, setOffset, showSkipButtons = false, url }) {
const { data: records, error, isValidating, mutate } = result
const hasRecords = records && records.data && records.data.length > 0
const lastOffset = hasRecords ? records.total - (records.total % limit) : 0
const pageIndex = (offset / limit) + 1
const onFirstPage = offset <= 0
const onLastPage = offset === lastOffset
const firstIndex = onFirstPage ? 1 : onLastPage ? pageIndex - 2 : pageIndex - 1
const indices = Array(3).fill(firstIndex)
return (
<>
<div className='controls'>
<span>
<Button disabled={isValidating} onClick={() => mutate()}>
<Sync size={20} />
</Button>
{hasRecords &&
<span>
Showing {records.offset + 1} - {records.offset + records.data.length} of {records.total}
</span>
}
</span>
<Pagination className='float-right'>
<Pagination.First disabled={onFirstPage} onClick={() => setOffset(0)} />
<Pagination.Prev disabled={onFirstPage} onClick={() => setOffset(offset - limit)} />
{indices.map((value, i) => {
const itemIndex = value + i
const newOffset = (itemIndex - 1) * limit
if (newOffset > lastOffset || newOffset < 0) return null
return (
<Pagination.Item
landonreed marked this conversation as resolved.
Show resolved Hide resolved
active={itemIndex === pageIndex}
onClick={() => setOffset(newOffset)}
>
{itemIndex}
landonreed marked this conversation as resolved.
Show resolved Hide resolved
</Pagination.Item>
)
})}
<Pagination.Next disabled={onLastPage} onClick={() => setOffset(offset + limit)} />
<Pagination.Last disabled={onLastPage} onClick={() => setOffset(lastOffset)} />
</Pagination>
</div>
<div className='mt-3'>
{error && <pre>Error loading items: {error}</pre>}
</div>
<style jsx>{`
.controls :global(.btn) {
margin-right: 1rem;
}
`}
</style>
</>
)
}

export default PageControls
36 changes: 18 additions & 18 deletions components/RequestLogsDashboard.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExternalLinkAlt } from '@styled-icons/fa-solid/ExternalLinkAlt'
import { Sync } from '@styled-icons/fa-solid/Sync'
import { Button } from 'react-bootstrap'
import useSWR, { mutate } from 'swr'
import { useAuth } from 'use-auth0-hooks'
Expand All @@ -14,29 +16,36 @@ function RequestLogsDashboard ({ isAdmin }) {
scope: AUTH0_SCOPE
})
const result = useSWR(REQUEST_LOGS_URL)
const { isValidating } = result
if (!auth.isAuthenticated) return null
return (
<div>
<h2>Request Log Summary</h2>
<div className='controls'>
<Button className='mr-3' onClick={() => mutate(REQUEST_LOGS_URL)}>
Fetch logs
</Button>
<FetchMessage result={result} />
{isAdmin &&
{isAdmin &&
<h5>
<a
className='push'
target='_blank'
rel='noopener noreferrer'
href='https://console.aws.amazon.com/apigateway/home?region=us-east-1#/usage-plans'
>
Open AWS console
<ExternalLinkAlt className='mr-1 mb-1' size={20} />Open AWS console
</a>
}
</h5>
landonreed marked this conversation as resolved.
Show resolved Hide resolved
}
<div className='controls'>
<Button
disabled={isValidating}
className='mr-3'
onClick={() => mutate(REQUEST_LOGS_URL)}
>
<Sync size={20} />
</Button>
<FetchMessage result={result} />
</div>
<ApiKeyUsage
isAdmin={isAdmin}
logs={result.data && result.data.data}
logs={result.data}
logsError={result.error} />
<style jsx>{`
.controls {
Expand All @@ -59,15 +68,6 @@ function RequestLogsDashboard ({ isAdmin }) {
list-style: none;
margin: 5px 0;
}

a {
text-decoration: none;
color: blue;
}

a:hover {
opacity: 0.6;
}
`}
</style>
</div>
Expand Down
18 changes: 12 additions & 6 deletions components/UserList.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, ListGroup } from 'react-bootstrap'
import useSWR, { mutate } from 'swr'
import { useAuth } from 'use-auth0-hooks'

import FetchMessage from './FetchMessage'
import PageControls from './PageControls'
import UserRow from './UserRow'
import { AUTH0_SCOPE, USER_TYPES } from '../util/constants'
import { secureFetch } from '../util/middleware'
Expand All @@ -23,11 +24,13 @@ function UserList ({ type }) {
audience: process.env.AUTH0_AUDIENCE,
scope: AUTH0_SCOPE
})
const [offset, setOffset] = useState(0)
const router = useRouter()
const onViewUser = (user) => {
if (!user) router.push(`/manage?type=${type}`)
else router.push(`/manage?type=${type}&userId=${user.id}`)
}
const url = `${_getUrl(type)}?offset=${offset}`
const onDeleteUser = async (user, type) => {
let message = `Are you sure you want to delete user ${user.email}?`
// TODO: Remove Data Tools user prop?
Expand Down Expand Up @@ -66,16 +69,20 @@ function UserList ({ type }) {
const selectedType = USER_TYPES.find(t => t.value === type)
if (!isAuthenticated) return null
if (!selectedType) return <div>Page does not exist!</div>
const result = useSWR(_getUrl(type))
const result = useSWR(url)
const { data, error } = result
const users = data && data.data
const limit = 10
return (
<div>
<h2 className='mb-4'>List of {selectedType.label}</h2>
<PageControls
limit={limit}
offset={offset}
setOffset={setOffset}
showSkipButtons
result={result} />
<div className='controls'>
<Button className='mr-3' onClick={() => mutate(_getUrl(type))}>
Fetch users
</Button>
{/*
Only permit user creation for admin users.
Other users must be created through standard flows.
Expand All @@ -85,7 +92,6 @@ function UserList ({ type }) {
Create user
</Button>
}
<FetchMessage result={result} />
</div>
{
users && (
Expand Down
6 changes: 2 additions & 4 deletions util/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ export async function secureFetch (url, accessToken, method = 'get', options = {
headers,
...options
})
return {
data: await res.json(),
timestamp: new Date()
}
const json = await res.json()
landonreed marked this conversation as resolved.
Show resolved Hide resolved
return json
}

export async function addUser (url, token, data) {
Expand Down