Skip to content
Open
Show file tree
Hide file tree
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
204 changes: 177 additions & 27 deletions src/containers/Network/NodesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from 'react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Loader } from '../shared/components/Loader'
import { durationToHuman } from '../shared/utils'
Expand All @@ -15,21 +15,28 @@
) : (
<i>unknown</i>
)

const renderLedgerHistory = (ledgers, range) => {
const getLedgerHistory = (ledgers, range, MAX_WIDTH = 160) => {
let count = 0
const MAX_WIDTH = 160
let boxes = ''
const min = Math.max(range[1] - 10000000, range[0])
const diff = range[1] - min

if (ledgers) {
const boxes = ledgers.map((l) => {
boxes = ledgers.map((l) => {
const [low, high] = l
const width = Math.min((high - low + 1) / diff, 1) * MAX_WIDTH
const left = Math.max((low - min) / diff, 0) * MAX_WIDTH
count += high - low
return <div key={low} style={{ left, width }} />
})
}
return { boxes, count }
}
const renderLedgerHistory = (ledgers, range) => {
const MAX_WIDTH = 160

if (ledgers) {
const { boxes, count } = getLedgerHistory(ledgers, range, MAX_WIDTH)

if (count < 0) {
return null
Expand Down Expand Up @@ -99,59 +106,202 @@
}) => {
const nodes = unformattedNodes ? formatLedgerHistory(unformattedNodes) : null
const ledgerRange = nodes && getLedgerRange(nodes)
const [sortedField, setSortedField] = useState(null)
const [sortOrder, setSortOrder] = useState(null)

const requestSort = (key) => {
let direction = 'desc'
if (sortedField === key && sortOrder === 'desc') {
direction = 'asc'
}
setSortOrder(direction)

Check failure on line 117 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Argument of type 'string' is not assignable to parameter of type 'SetStateAction<null>'.
setSortedField(key)
}

const { t } = useTranslation()
const renderNode = (node) => (
<tr key={node.node_public_key}>
<td className="pubkey text-truncate">{node.node_public_key}</td>
<td className="ip text-truncate">{node.ip}</td>
<td className="state center">
<td className={getClassNamesFor('pubkey text-truncate', 'pubkey')}>
{node.node_public_key}
</td>
<td className={getClassNamesFor('ip text-truncate', 'ip')}>{node.ip}</td>
<td className={getClassNamesFor('state center', 'server_state')}>
<span className={node.server_state}>{node.server_state}</span>
</td>
<td className="version">{getVersion(node.version)}</td>
<td className="last-ledger">{renderLastLedger(node.validated_ledger)}</td>
<td className="uptime">{durationToHuman(node.uptime)}</td>
<td className="peers right">
<td className={getClassNamesFor('version', 'rippled_version')}>
{getVersion(node.version)}
</td>
<td className={getClassNamesFor('last-ledger', 'last_ledger')}>
{renderLastLedger(node.validated_ledger)}
</td>
<td className={getClassNamesFor('uptime', 'uptime')}>
{durationToHuman(node.uptime)}
</td>
<td className={getClassNamesFor('peers right', 'peers')}>
{node.inbound_count + node.outbound_count}
</td>
<td className="in-out">
<small>
({node.inbound_count}:{node.outbound_count})
</small>
</td>
<td className="ledgers">
<td className={getClassNamesFor('ledgers', 'ledger_history')}>
{renderLedgerHistory(node.ledgers, ledgerRange)}
</td>
<td className="quorum right">{node.quorum}</td>
<td className="load-factor right">
<td className={getClassNamesFor('quorum right', 'quorum')}>
{node.quorum}
</td>
<td className={getClassNamesFor('load-factor right', 'load')}>
{node.load_factor && node.load_factor > 1
? node.load_factor.toFixed(2)
: ''}
</td>
<td className="latency right">
<td className={getClassNamesFor('latency right', 'latency')}>
{node.io_latency_ms && node.io_latency_ms > 1}
</td>
</tr>
)

const compareSemanticVersions = (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests for this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests have now been added that cover the compareSemanticVersions, via sorting the Version column.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see this function tested separately and cover all cases. similar to this:

it('isEarlierVersion compare versions correctly', () => {
expect(isEarlierVersion('1.6.2', 'N/A')).toEqual(false)
expect(isEarlierVersion('N/A', '0.9.4')).toEqual(true)
expect(isEarlierVersion('N/A', 'N/A')).toEqual(false)
expect(isEarlierVersion('1.9.4', '1.9.4')).toEqual(false)
expect(isEarlierVersion('0.9.2', '1.8.4')).toEqual(true)
expect(isEarlierVersion('1.8.2', '1.9.4')).toEqual(true)
expect(isEarlierVersion('1.9.2', '1.9.4')).toEqual(true)
expect(isEarlierVersion('1.9.2', '1.9.2-b1')).toEqual(false)
expect(isEarlierVersion('1.9.2', '1.9.2-rc2')).toEqual(false)
expect(isEarlierVersion('1.9.4-b2', '1.9.4-rc1')).toEqual(true)
expect(isEarlierVersion('1.9.4-b1', '1.9.4-b2')).toEqual(true)
expect(isEarlierVersion('1.9.4-rc1', '1.9.4-rc2')).toEqual(true)
expect(isEarlierVersion('1.6.2', '0.9.4')).toEqual(false)
expect(isEarlierVersion('1.9.4', '1.8.6')).toEqual(false)
expect(isEarlierVersion('1.9.4', '1.9.2-rc5')).toEqual(false)
expect(isEarlierVersion('1.8.0-rc1', '1.8.0')).toEqual(true)
expect(isEarlierVersion('1.9.4-rc1', '1.9.4-b3')).toEqual(false)
expect(isEarlierVersion('1.9.4-b2', '1.9.4-b1')).toEqual(false)
expect(isEarlierVersion('1.9.4-rc2', '1.9.4-rc1')).toEqual(false)
})

a: string,
b: string,
returnValue: number,

Check failure on line 168 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Duplicate identifier 'returnValue'.

Check failure on line 168 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Duplicate identifier 'returnValue'.
returnValue: string,

Check failure on line 169 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Duplicate identifier 'returnValue'.
returnValue: number,

Check failure on line 170 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Duplicate identifier 'returnValue'.
) => {
const a1 = a.split('.')
const b1 = b.split('.')

const len = Math.min(a1.length, b1.length)

for (let i = 0; i < len; i++) {
const a2 = +a1[i] || 0
const b2 = +b1[i] || 0

if (a2 !== b2) {
return a2 > b2 ? returnValue : returnValue * -1
}
}
return b1.length - a1.length
}

if (nodes !== null) {
const sort = (key: any, order: string) => {
const returnValue = order === 'desc' ? 1 : -1

if (key === 'peers') {
nodes.sort((a, b) =>
a.inbound_count + a.outbound_count >
b.inbound_count + b.outbound_count
? returnValue * -1
: returnValue,
)
} else if (key === 'ledger_history') {
nodes.sort((a, b) =>
getLedgerHistory(a.ledgers, ledgerRange).count >
getLedgerHistory(b.ledgers, ledgerRange).count
? returnValue * -1
: returnValue,
)
} else if (key === 'rippled_version') {
nodes.sort((a, b) =>
compareSemanticVersions(a.version, b.version, returnValue),

Check failure on line 208 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Expected 5 arguments, but got 3.
)
} else {
nodes.sort((a, b) => (a[key] > b[key] ? returnValue * -1 : returnValue))
}
}

sort(sortedField, sortOrder)

Check failure on line 215 in src/containers/Network/NodesTable.tsx

View workflow job for this annotation

GitHub Actions / typescript-check (18.12)

Argument of type 'null' is not assignable to parameter of type 'string'.
}

const getClassNamesFor = (name, order) =>
sortedField === order ? `${name} sorted` : name
const getClassArrowFor = (field) => {
if (sortedField === field && sortOrder === 'desc') {
return 'arrow down'
}
if (sortedField === field && sortOrder === 'asc') {
return 'arrow up'
}
return ''
}

const content = nodes ? (
<table className="basic">
<thead>
<tr>
<th className="pubkey">{t('node_pubkey')}</th>
<th className="ip">{t('ip')}</th>
<th className="server-state center">{t('state')}</th>
<th className="version">{t('rippled_version')}</th>
<th className="last-ledger">{t('last_ledger')}</th>
<th className="uptime">{t('uptime')}</th>
<th className="peers right">{t('peers')}</th>
<th className={getClassNamesFor('pubkey', 'pubkey')}>
<a href="#" onClick={() => requestSort('pubkey')}>
<i className={getClassArrowFor('pubkey')} />
{t('node_pubkey')}
</a>
</th>
<th className={getClassNamesFor('ip', 'ip')}>
Copy link
Collaborator

@pdp2121 pdp2121 Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe can create a reusable component like NodeTableHeader and just call <NodeTableHeader title='ip'> for instance

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im working on this even after the rebase, should have that function pushed up one level and then also use this sorting on the validators page.

<a href="#" onClick={() => requestSort('ip')}>
<i className={getClassArrowFor('ip')} />
{t('ip')}
</a>
</th>
<th
className={getClassNamesFor('server-state center', 'server_state')}
>
<a href="#" onClick={() => requestSort('server_state')}>
<i className={getClassArrowFor('server_state')} />
{t('state')}
</a>
</th>
<th className={getClassNamesFor('version', 'rippled_version')}>
<a href="#" onClick={() => requestSort('rippled_version')}>
<i className={getClassArrowFor('rippled_version')} />
{t('rippled_version')}{' '}
</a>
</th>
<th className={getClassNamesFor('last-ledger', 'last_ledger')}>
<a href="#" onClick={() => requestSort('last_ledger')}>
<i className={getClassArrowFor('last_ledger')} />
{t('last_ledger')}
</a>
</th>
<th className={getClassNamesFor('uptime', 'uptime')}>
<a href="#" onClick={() => requestSort('uptime')}>
<i className={getClassArrowFor('uptime')} />
{t('uptime')}
</a>
</th>
<th className={getClassNamesFor('peers right', 'peers')}>
<a href="#" onClick={() => requestSort('peers')}>
<i className={getClassArrowFor('peers')} />
{t('peers')}
</a>
</th>
<th className="in-out">
<small>{t('in_out')}</small>
</th>
<th className="ledgers">{t('ledger_history')}</th>
<th className="quorum right">{t('quorum')}</th>
<th className="load-factor right">{t('load')}</th>
<th className="latency right">{t('latency')}</th>
<th className={getClassNamesFor('ledgers', 'ledger_history')}>
<a href="#" onClick={() => requestSort('ledger_history')}>
<i className={getClassArrowFor('ledger_history')} />
{t('ledger_history')}
</a>
</th>
<th className={getClassNamesFor('quorum right', 'quorum')}>
<a href="#" onClick={() => requestSort('quorum')}>
<i className={getClassArrowFor('quorum')} />
{t('quorum')}
</a>
</th>
<th className={getClassNamesFor('load-factor right', 'load')}>
<a href="#" onClick={() => requestSort('load')}>
<i className={getClassArrowFor('load')} />
{t('load')}
</a>
</th>
<th className={getClassNamesFor('latency right', 'latency')}>
<a href="#" onClick={() => requestSort('latency')}>
<i className={getClassArrowFor('latency')} />
{t('latency')}
</a>
</th>
</tr>
</thead>
<tbody>{nodes.map(renderNode)}</tbody>
Expand Down
20 changes: 20 additions & 0 deletions src/containers/Network/css/nodesTable.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
min-height: 150px;

table {
.sorted {
background-color: $black-90;
}

.arrow {
display: inline-block;
padding: 3px;
border: solid $white;
border-width: 0 3px 3px 0;
margin-right: 5px;
}

.up {
transform: rotate(-135deg);
}

.down {
transform: rotate(45deg);
}

.pubkey {
max-width: 70px;

Expand Down
13 changes: 13 additions & 0 deletions src/containers/Network/test/nodes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ describe('Nodes Page container', () => {
expect(wrapper.find('.nodes-map .tooltip').length).toBe(0)
expect(wrapper.find('.nodes-map path.node').length).toBe(2)
expect(wrapper.find('.nodes-table table tr').length).toBe(4)
wrapper.find('.nodes-table table th.version a').simulate('click')
const rows = wrapper.find('.nodes-table table tbody tr td.version')
const expected = [
'<td class="version sorted">1.1.2</td>',
'<td class="version sorted">1.2.0-rc2</td>',
'<td class="version sorted">1.2.0-rc2</td>',
]
let index = 0
rows.forEach((element) => {
expect(element.html()).toBe(expected[index])
// eslint-disable-next-line no-plusplus
index++
})
wrapper.unmount()
done()
})
Expand Down
Loading