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

feat: peers table can be filtered #2181

Merged
merged 4 commits into from
Dec 1, 2023
Merged
Changes from all 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
274 changes: 150 additions & 124 deletions src/peers/PeersTable/PeersTable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import ms from 'milliseconds'
import { connect } from 'redux-bundler-react'
Expand All @@ -11,166 +11,192 @@ import { sortByProperty } from '../../lib/sort.js'

import './PeersTable.css'

export class PeersTable extends React.Component {
/**
*
* @param {object} props
* @param {Promise<any[]>} props.peerLocationsForSwarm
* @param {string} props.className
* @param {import('i18next').TFunction} props.t
*/
constructor (props) {
super(props)

this.state = {
sortBy: 'latency',
sortDirection: SortDirection.ASC,
peerLocationsForSwarm: []
}

this.sort = this.sort.bind(this)
}

flagRenderer = (flagCode, isPrivate) => {
// Check if the OS is Windows to render the flags as SVGs
// Windows doesn't render the flags as emojis ¯\_(ツ)_/¯
const isWindows = window.navigator.appVersion.indexOf('Win') !== -1
return (
const flagRenderer = (flagCode, isPrivate) => {
// Check if the OS is Windows to render the flags as SVGs
// Windows doesn't render the flags as emojis ¯\_(ツ)_/¯
const isWindows = window.navigator.appVersion.indexOf('Win') !== -1
return (
<span className='f4 pr2'>
{isPrivate ? '🤝' : flagCode ? <CountryFlag code={flagCode} svg={isWindows} /> : '🌐'}
</span>
)
}
)
}

locationCellRenderer = ({ rowData }) => {
const ref = React.createRef()
const location = rowData.isPrivate
? this.props.t('localNetwork')
: rowData.location
? rowData.isNearby
? <span>{rowData.location} <span className='charcoal-muted'>({this.props.t('nearby')})</span></span>
: rowData.location
: <span className='charcoal-muted fw4'>{this.props.t('app:terms.unknown')}</span>
const value = rowData.location || this.props.t('app:terms.unknown')
return (
<CopyToClipboard text={value} onCopy={() => copyFeedback(ref, this.props.t)}>
const locationCellRenderer = (t) => ({ rowData }) => {
const ref = React.createRef()
const location = rowData.isPrivate
? t('localNetwork')
: rowData.location
? rowData.isNearby
? <span>{rowData.location} <span className='charcoal-muted'>({t('nearby')})</span></span>
: rowData.location
: <span className='charcoal-muted fw4'>{t('app:terms.unknown')}</span>
const value = rowData.location || t('app:terms.unknown')
return (
<CopyToClipboard text={value} onCopy={() => copyFeedback(ref, t)}>
<span title={value} className='copyable' ref={ref}>
{ this.flagRenderer(rowData.flagCode, rowData.isPrivate) }
{ flagRenderer(rowData.flagCode, rowData.isPrivate) }
{ location }
</span>
</CopyToClipboard>
)
}
)
}

latencyCellRenderer = ({ cellData, rowData }) => {
const style = { width: '60px' }
const latency = `${cellData}ms`
if (cellData == null) return (<span className='dib o-40 no-select' style={style}>-</span>)
return (<span className='dib no-select'>{latency}</span>)
}
const latencyCellRenderer = ({ cellData }) => {
const style = { width: '60px' }
const latency = `${cellData}ms`
if (cellData == null) return (<span className='dib o-40 no-select' style={style}>-</span>)
return (<span className='dib no-select'>{latency}</span>)
}

peerIdCellRenderer = ({ cellData: peerId }) => {
const ref = React.createRef()
const p2pMultiaddr = `/p2p/${peerId}`
return (
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, this.props.t)}>
const peerIdCellRenderer = (t) => ({ cellData: peerId }) => {
const ref = React.createRef()
const p2pMultiaddr = `/p2p/${peerId}`
return (
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, t)}>
<Cid value={peerId} identicon ref={ref} className='copyable' />
</CopyToClipboard>
)
}
)
}

protocolsCellRenderer = ({ rowData, cellData }) => {
const ref = React.createRef()
const { protocols } = rowData
const title = protocols.split(', ').join('\n')
return (
<CopyToClipboard text={protocols} onCopy={() => copyFeedback(ref, this.props.t)}>
const protocolsCellRenderer = (t) => ({ rowData }) => {
const ref = React.createRef()
const { protocols } = rowData
const title = protocols.split(', ').join('\n')
return (
<CopyToClipboard text={protocols} onCopy={() => copyFeedback(ref, t)}>
<span
ref={ref}
className='copyable'
title={title}>
{ protocols.replaceAll('[unnamed]', '🤔') }
</span>
</CopyToClipboard>
)
}
)
}

connectionCellRenderer = ({ rowData }) => {
const ref = React.createRef()
const { address, direction, peerId } = rowData
const p2pMultiaddr = `${address}/p2p/${peerId}`
const title = direction != null
? `${address}\n(${renderDirection(direction, this.props.t)})`
: address
const connectionCellRenderer = (t) => ({ rowData }) => {
const ref = React.createRef()
const { address, direction, peerId } = rowData
const p2pMultiaddr = `${address}/p2p/${peerId}`
const title = direction != null
? `${address}\n(${renderDirection(direction, t)})`
: address

return (
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, this.props.t)}>
return (
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, t)}>
<abbr
ref={ref}
className='copyable'
title={title}>
{rowData.connection}
</abbr>
</CopyToClipboard>
)
}

rowClassRenderer = ({ index }, peers = []) => {
const { selectedPeers } = this.props
const shouldAddHoverEffect = selectedPeers?.peerIds?.includes(peers[index]?.peerId)

return classNames('bb b--near-white peersTableItem', index === -1 && 'bg-near-white', shouldAddHoverEffect && 'bg-light-gray')
}

sort ({ sortBy, sortDirection }) {
this.setState({ sortBy, sortDirection })
}
)
}

componentWillReceiveProps (nextProps) {
if (nextProps.peerLocationsForSwarm) {
nextProps.peerLocationsForSwarm?.then?.((peerLocationsForSwarm) => {
if (peerLocationsForSwarm !== this.state.peerLocationsForSwarm) {
this.setState({ peerLocationsForSwarm })
}
})
}
}
const rowClassRenderer = ({ index }, peers = [], selectedPeers) => {
const shouldAddHoverEffect = selectedPeers?.peerIds?.includes(peers[index]?.peerId)

render () {
const { className, t } = this.props
const { sortBy, sortDirection, peerLocationsForSwarm } = this.state
return classNames('bb b--near-white peersTableItem', index === -1 && 'bg-near-white', shouldAddHoverEffect && 'bg-light-gray')
}

const sortedList = peerLocationsForSwarm.sort(sortByProperty(sortBy, sortDirection === SortDirection.ASC ? 1 : -1))
const tableHeight = 400
const FilterInput = ({ setFilter, t, filteredCount }) => {
return (
<div className='flex items-center justify-between pa2'>
<input
className='input-reset ba b--black-20 pa2 mb2 db w-100'
type='text'
placeholder='Filter peers'
onChange={(e) => setFilter(e.target.value)}
/>
{/* Now to display the total number of peers filtered out on the right side of the inside of the input */}
<div className='f4 charcoal-muted absolute top-1 right-1'>{filteredCount}</div>
</div>
)
}

return (
<div className={`bg-white-70 center ${className}`} style={{ height: `${tableHeight}px`, maxWidth: 1764 }}>
{ peerLocationsForSwarm && <AutoSizer disableHeight>
export const PeersTable = ({ className, t, peerLocationsForSwarm, selectedPeers }) => {
const tableHeight = 400
const [awaitedPeerLocationsForSwarm, setAwaitedPeerLocationsForSwarm] = useState([])
const [sortBy, setSortBy] = useState('latency')
const [sortDirection, setSortDirection] = useState(SortDirection.ASC)
const [filter, setFilter] = useState('')

const sort = useCallback(({ sortBy, sortDirection }) => {
setSortBy(sortBy)
setSortDirection(sortDirection)
}, [])
const filterCb = useCallback((value) => {
setFilter(value)
}, [])

useEffect(() => {
peerLocationsForSwarm?.then?.((peerLocationsForSwarm) => {
setAwaitedPeerLocationsForSwarm(peerLocationsForSwarm)
})
}, [peerLocationsForSwarm])

const filteredPeerList = useMemo(() => {
const filterLower = filter.toLowerCase()
if (filterLower === '') return awaitedPeerLocationsForSwarm
return awaitedPeerLocationsForSwarm.filter(({ location, latency, peerId, connection, protocols }) => {
if (location != null && location.toLowerCase().includes(filterLower)) {
return true
}
if (latency != null && [latency, `${latency}ms`].some((str) => str.toString().includes(filterLower))) {
return true
}
if (peerId != null && peerId.toString().includes(filter)) {
return true
}
console.log('connection: ', connection)
if (connection != null && connection.toLowerCase().includes(filterLower)) {
return true
}
if (protocols != null && protocols.toLowerCase().includes(filterLower)) {
return true
}

return false
})
}, [awaitedPeerLocationsForSwarm, filter])

const sortedList = useMemo(
() => filteredPeerList.sort(sortByProperty(sortBy, sortDirection === SortDirection.ASC ? 1 : -1)),
[filteredPeerList, sortBy, sortDirection]
)

return (
<div className={`bg-white-70 center ${className}`} style={{ height: `${tableHeight}px`, maxWidth: 1764 }}>
<FilterInput setFilter={filterCb} t={t} filteredCount={sortedList.length} />
{ awaitedPeerLocationsForSwarm && <AutoSizer disableHeight>
{({ width }) => (
<Table
className='tl fw4 w-100 f6'
headerClassName='teal fw2 ttu tracked ph2 no-select'
rowClassName={(rowInfo) => this.rowClassRenderer(rowInfo, peerLocationsForSwarm)}
width={width}
height={tableHeight}
headerHeight={32}
rowHeight={36}
rowCount={peerLocationsForSwarm.length}
rowGetter={({ index }) => sortedList[index]}
sort={this.sort}
sortBy={sortBy}
sortDirection={sortDirection}>
<Column label={t('app:terms.location')} cellRenderer={this.locationCellRenderer} dataKey='location' width={450} className='f6 charcoal truncate pl2' />
<Column label={t('app:terms.latency')} cellRenderer={this.latencyCellRenderer} dataKey='latency' width={200} className='f6 charcoal pl2' />
<Column label={t('app:terms.peerId')} cellRenderer={this.peerIdCellRenderer} dataKey='peerId' width={250} className='charcoal monospace truncate f6 pl2' />
<Column label={t('app:terms.connection')} cellRenderer={this.connectionCellRenderer} dataKey='connection' width={250} className='f6 charcoal truncate pl2' />
<Column label={t('protocols')} cellRenderer={this.protocolsCellRenderer} dataKey='protocols' width={520} className='charcoal monospace truncate f7 pl2' />
</Table>
<>
<Table
className='tl fw4 w-100 f6'
headerClassName='teal fw2 ttu tracked ph2 no-select'
rowClassName={(rowInfo) => rowClassRenderer(rowInfo, awaitedPeerLocationsForSwarm, selectedPeers)}
width={width}
height={tableHeight}
headerHeight={32}
rowHeight={36}
rowCount={sortedList.length}
rowGetter={({ index }) => sortedList[index]}
sort={sort}
sortBy={sortBy}
sortDirection={sortDirection}>
<Column label={t('app:terms.location')} cellRenderer={locationCellRenderer(t)} dataKey='location' width={450} className='f6 charcoal truncate pl2' />
<Column label={t('app:terms.latency')} cellRenderer={latencyCellRenderer} dataKey='latency' width={200} className='f6 charcoal pl2' />
<Column label={t('app:terms.peerId')} cellRenderer={peerIdCellRenderer(t)} dataKey='peerId' width={250} className='charcoal monospace truncate f6 pl2' />
<Column label={t('app:terms.connection')} cellRenderer={connectionCellRenderer(t)} dataKey='connection' width={250} className='f6 charcoal truncate pl2' />
<Column label={t('protocols')} cellRenderer={protocolsCellRenderer(t)} dataKey='protocols' width={520} className='charcoal monospace truncate f7 pl2' />
</Table>
</>
)}
</AutoSizer> }
</div>
)
}
)
}

// API returns integer atm, but that may change in the future
Expand Down