Skip to content

Commit 37b5880

Browse files
authored
feat: peers table can be filtered (#2181)
1 parent ff75d4e commit 37b5880

File tree

1 file changed

+150
-124
lines changed

1 file changed

+150
-124
lines changed

src/peers/PeersTable/PeersTable.js

+150-124
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
22
import classNames from 'classnames'
33
import ms from 'milliseconds'
44
import { connect } from 'redux-bundler-react'
@@ -11,166 +11,192 @@ import { sortByProperty } from '../../lib/sort.js'
1111

1212
import './PeersTable.css'
1313

14-
export class PeersTable extends React.Component {
15-
/**
16-
*
17-
* @param {object} props
18-
* @param {Promise<any[]>} props.peerLocationsForSwarm
19-
* @param {string} props.className
20-
* @param {import('i18next').TFunction} props.t
21-
*/
22-
constructor (props) {
23-
super(props)
24-
25-
this.state = {
26-
sortBy: 'latency',
27-
sortDirection: SortDirection.ASC,
28-
peerLocationsForSwarm: []
29-
}
30-
31-
this.sort = this.sort.bind(this)
32-
}
33-
34-
flagRenderer = (flagCode, isPrivate) => {
35-
// Check if the OS is Windows to render the flags as SVGs
36-
// Windows doesn't render the flags as emojis ¯\_(ツ)_/¯
37-
const isWindows = window.navigator.appVersion.indexOf('Win') !== -1
38-
return (
14+
const flagRenderer = (flagCode, isPrivate) => {
15+
// Check if the OS is Windows to render the flags as SVGs
16+
// Windows doesn't render the flags as emojis ¯\_(ツ)_/¯
17+
const isWindows = window.navigator.appVersion.indexOf('Win') !== -1
18+
return (
3919
<span className='f4 pr2'>
4020
{isPrivate ? '🤝' : flagCode ? <CountryFlag code={flagCode} svg={isWindows} /> : '🌐'}
4121
</span>
42-
)
43-
}
22+
)
23+
}
4424

45-
locationCellRenderer = ({ rowData }) => {
46-
const ref = React.createRef()
47-
const location = rowData.isPrivate
48-
? this.props.t('localNetwork')
49-
: rowData.location
50-
? rowData.isNearby
51-
? <span>{rowData.location} <span className='charcoal-muted'>({this.props.t('nearby')})</span></span>
52-
: rowData.location
53-
: <span className='charcoal-muted fw4'>{this.props.t('app:terms.unknown')}</span>
54-
const value = rowData.location || this.props.t('app:terms.unknown')
55-
return (
56-
<CopyToClipboard text={value} onCopy={() => copyFeedback(ref, this.props.t)}>
25+
const locationCellRenderer = (t) => ({ rowData }) => {
26+
const ref = React.createRef()
27+
const location = rowData.isPrivate
28+
? t('localNetwork')
29+
: rowData.location
30+
? rowData.isNearby
31+
? <span>{rowData.location} <span className='charcoal-muted'>({t('nearby')})</span></span>
32+
: rowData.location
33+
: <span className='charcoal-muted fw4'>{t('app:terms.unknown')}</span>
34+
const value = rowData.location || t('app:terms.unknown')
35+
return (
36+
<CopyToClipboard text={value} onCopy={() => copyFeedback(ref, t)}>
5737
<span title={value} className='copyable' ref={ref}>
58-
{ this.flagRenderer(rowData.flagCode, rowData.isPrivate) }
38+
{ flagRenderer(rowData.flagCode, rowData.isPrivate) }
5939
{ location }
6040
</span>
6141
</CopyToClipboard>
62-
)
63-
}
42+
)
43+
}
6444

65-
latencyCellRenderer = ({ cellData, rowData }) => {
66-
const style = { width: '60px' }
67-
const latency = `${cellData}ms`
68-
if (cellData == null) return (<span className='dib o-40 no-select' style={style}>-</span>)
69-
return (<span className='dib no-select'>{latency}</span>)
70-
}
45+
const latencyCellRenderer = ({ cellData }) => {
46+
const style = { width: '60px' }
47+
const latency = `${cellData}ms`
48+
if (cellData == null) return (<span className='dib o-40 no-select' style={style}>-</span>)
49+
return (<span className='dib no-select'>{latency}</span>)
50+
}
7151

72-
peerIdCellRenderer = ({ cellData: peerId }) => {
73-
const ref = React.createRef()
74-
const p2pMultiaddr = `/p2p/${peerId}`
75-
return (
76-
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, this.props.t)}>
52+
const peerIdCellRenderer = (t) => ({ cellData: peerId }) => {
53+
const ref = React.createRef()
54+
const p2pMultiaddr = `/p2p/${peerId}`
55+
return (
56+
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, t)}>
7757
<Cid value={peerId} identicon ref={ref} className='copyable' />
7858
</CopyToClipboard>
79-
)
80-
}
59+
)
60+
}
8161

82-
protocolsCellRenderer = ({ rowData, cellData }) => {
83-
const ref = React.createRef()
84-
const { protocols } = rowData
85-
const title = protocols.split(', ').join('\n')
86-
return (
87-
<CopyToClipboard text={protocols} onCopy={() => copyFeedback(ref, this.props.t)}>
62+
const protocolsCellRenderer = (t) => ({ rowData }) => {
63+
const ref = React.createRef()
64+
const { protocols } = rowData
65+
const title = protocols.split(', ').join('\n')
66+
return (
67+
<CopyToClipboard text={protocols} onCopy={() => copyFeedback(ref, t)}>
8868
<span
8969
ref={ref}
9070
className='copyable'
9171
title={title}>
9272
{ protocols.replaceAll('[unnamed]', '🤔') }
9373
</span>
9474
</CopyToClipboard>
95-
)
96-
}
75+
)
76+
}
9777

98-
connectionCellRenderer = ({ rowData }) => {
99-
const ref = React.createRef()
100-
const { address, direction, peerId } = rowData
101-
const p2pMultiaddr = `${address}/p2p/${peerId}`
102-
const title = direction != null
103-
? `${address}\n(${renderDirection(direction, this.props.t)})`
104-
: address
78+
const connectionCellRenderer = (t) => ({ rowData }) => {
79+
const ref = React.createRef()
80+
const { address, direction, peerId } = rowData
81+
const p2pMultiaddr = `${address}/p2p/${peerId}`
82+
const title = direction != null
83+
? `${address}\n(${renderDirection(direction, t)})`
84+
: address
10585

106-
return (
107-
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, this.props.t)}>
86+
return (
87+
<CopyToClipboard text={p2pMultiaddr} onCopy={() => copyFeedback(ref, t)}>
10888
<abbr
10989
ref={ref}
11090
className='copyable'
11191
title={title}>
11292
{rowData.connection}
11393
</abbr>
11494
</CopyToClipboard>
115-
)
116-
}
117-
118-
rowClassRenderer = ({ index }, peers = []) => {
119-
const { selectedPeers } = this.props
120-
const shouldAddHoverEffect = selectedPeers?.peerIds?.includes(peers[index]?.peerId)
121-
122-
return classNames('bb b--near-white peersTableItem', index === -1 && 'bg-near-white', shouldAddHoverEffect && 'bg-light-gray')
123-
}
124-
125-
sort ({ sortBy, sortDirection }) {
126-
this.setState({ sortBy, sortDirection })
127-
}
95+
)
96+
}
12897

129-
componentWillReceiveProps (nextProps) {
130-
if (nextProps.peerLocationsForSwarm) {
131-
nextProps.peerLocationsForSwarm?.then?.((peerLocationsForSwarm) => {
132-
if (peerLocationsForSwarm !== this.state.peerLocationsForSwarm) {
133-
this.setState({ peerLocationsForSwarm })
134-
}
135-
})
136-
}
137-
}
98+
const rowClassRenderer = ({ index }, peers = [], selectedPeers) => {
99+
const shouldAddHoverEffect = selectedPeers?.peerIds?.includes(peers[index]?.peerId)
138100

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

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

146-
return (
147-
<div className={`bg-white-70 center ${className}`} style={{ height: `${tableHeight}px`, maxWidth: 1764 }}>
148-
{ peerLocationsForSwarm && <AutoSizer disableHeight>
119+
export const PeersTable = ({ className, t, peerLocationsForSwarm, selectedPeers }) => {
120+
const tableHeight = 400
121+
const [awaitedPeerLocationsForSwarm, setAwaitedPeerLocationsForSwarm] = useState([])
122+
const [sortBy, setSortBy] = useState('latency')
123+
const [sortDirection, setSortDirection] = useState(SortDirection.ASC)
124+
const [filter, setFilter] = useState('')
125+
126+
const sort = useCallback(({ sortBy, sortDirection }) => {
127+
setSortBy(sortBy)
128+
setSortDirection(sortDirection)
129+
}, [])
130+
const filterCb = useCallback((value) => {
131+
setFilter(value)
132+
}, [])
133+
134+
useEffect(() => {
135+
peerLocationsForSwarm?.then?.((peerLocationsForSwarm) => {
136+
setAwaitedPeerLocationsForSwarm(peerLocationsForSwarm)
137+
})
138+
}, [peerLocationsForSwarm])
139+
140+
const filteredPeerList = useMemo(() => {
141+
const filterLower = filter.toLowerCase()
142+
if (filterLower === '') return awaitedPeerLocationsForSwarm
143+
return awaitedPeerLocationsForSwarm.filter(({ location, latency, peerId, connection, protocols }) => {
144+
if (location != null && location.toLowerCase().includes(filterLower)) {
145+
return true
146+
}
147+
if (latency != null && [latency, `${latency}ms`].some((str) => str.toString().includes(filterLower))) {
148+
return true
149+
}
150+
if (peerId != null && peerId.toString().includes(filter)) {
151+
return true
152+
}
153+
console.log('connection: ', connection)
154+
if (connection != null && connection.toLowerCase().includes(filterLower)) {
155+
return true
156+
}
157+
if (protocols != null && protocols.toLowerCase().includes(filterLower)) {
158+
return true
159+
}
160+
161+
return false
162+
})
163+
}, [awaitedPeerLocationsForSwarm, filter])
164+
165+
const sortedList = useMemo(
166+
() => filteredPeerList.sort(sortByProperty(sortBy, sortDirection === SortDirection.ASC ? 1 : -1)),
167+
[filteredPeerList, sortBy, sortDirection]
168+
)
169+
170+
return (
171+
<div className={`bg-white-70 center ${className}`} style={{ height: `${tableHeight}px`, maxWidth: 1764 }}>
172+
<FilterInput setFilter={filterCb} t={t} filteredCount={sortedList.length} />
173+
{ awaitedPeerLocationsForSwarm && <AutoSizer disableHeight>
149174
{({ width }) => (
150-
<Table
151-
className='tl fw4 w-100 f6'
152-
headerClassName='teal fw2 ttu tracked ph2 no-select'
153-
rowClassName={(rowInfo) => this.rowClassRenderer(rowInfo, peerLocationsForSwarm)}
154-
width={width}
155-
height={tableHeight}
156-
headerHeight={32}
157-
rowHeight={36}
158-
rowCount={peerLocationsForSwarm.length}
159-
rowGetter={({ index }) => sortedList[index]}
160-
sort={this.sort}
161-
sortBy={sortBy}
162-
sortDirection={sortDirection}>
163-
<Column label={t('app:terms.location')} cellRenderer={this.locationCellRenderer} dataKey='location' width={450} className='f6 charcoal truncate pl2' />
164-
<Column label={t('app:terms.latency')} cellRenderer={this.latencyCellRenderer} dataKey='latency' width={200} className='f6 charcoal pl2' />
165-
<Column label={t('app:terms.peerId')} cellRenderer={this.peerIdCellRenderer} dataKey='peerId' width={250} className='charcoal monospace truncate f6 pl2' />
166-
<Column label={t('app:terms.connection')} cellRenderer={this.connectionCellRenderer} dataKey='connection' width={250} className='f6 charcoal truncate pl2' />
167-
<Column label={t('protocols')} cellRenderer={this.protocolsCellRenderer} dataKey='protocols' width={520} className='charcoal monospace truncate f7 pl2' />
168-
</Table>
175+
<>
176+
<Table
177+
className='tl fw4 w-100 f6'
178+
headerClassName='teal fw2 ttu tracked ph2 no-select'
179+
rowClassName={(rowInfo) => rowClassRenderer(rowInfo, awaitedPeerLocationsForSwarm, selectedPeers)}
180+
width={width}
181+
height={tableHeight}
182+
headerHeight={32}
183+
rowHeight={36}
184+
rowCount={sortedList.length}
185+
rowGetter={({ index }) => sortedList[index]}
186+
sort={sort}
187+
sortBy={sortBy}
188+
sortDirection={sortDirection}>
189+
<Column label={t('app:terms.location')} cellRenderer={locationCellRenderer(t)} dataKey='location' width={450} className='f6 charcoal truncate pl2' />
190+
<Column label={t('app:terms.latency')} cellRenderer={latencyCellRenderer} dataKey='latency' width={200} className='f6 charcoal pl2' />
191+
<Column label={t('app:terms.peerId')} cellRenderer={peerIdCellRenderer(t)} dataKey='peerId' width={250} className='charcoal monospace truncate f6 pl2' />
192+
<Column label={t('app:terms.connection')} cellRenderer={connectionCellRenderer(t)} dataKey='connection' width={250} className='f6 charcoal truncate pl2' />
193+
<Column label={t('protocols')} cellRenderer={protocolsCellRenderer(t)} dataKey='protocols' width={520} className='charcoal monospace truncate f7 pl2' />
194+
</Table>
195+
</>
169196
)}
170197
</AutoSizer> }
171198
</div>
172-
)
173-
}
199+
)
174200
}
175201

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

0 commit comments

Comments
 (0)