15c3012199
* explorer-api: add nym node endpoints + UI to show nym-nodes and account balances * explorer-api: add endpoints to get operator rewards explorer-ui: show delegations on nym-nodes, show operator rewards, bug fixes * explorer-ui: change summary screen to only show nym-node stats * explorer-api: add unstable routes to get legacy mixnodes and gateways from the contract instead of the Nym API explorer-ui: adapt front-end to show less information in legacy nodes with plain bond types * explorer-ui: fix up source of legacy mixnode data * explorer-ui: add more account page null and undefined checks * explorer-ui: filter out null gateway versions * explorer-ui: sanitise gateway versions * explorer-ui: add more guards on the balance parts to check that greater than 0 * explorer-api: make /tmp/unstable/gateways endpoint compatible with the current Harbour Master API * explorer-ui: fix typo * cargo fmt * Add node-id, total stake and links to nodes list --------- Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com> Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo } from 'react'
|
|
import { Box, Card, Grid, Stack, Chip } from '@mui/material'
|
|
import { useTheme } from '@mui/material/styles'
|
|
import {
|
|
MRT_ColumnDef,
|
|
MaterialReactTable,
|
|
useMaterialReactTable,
|
|
} from 'material-react-table'
|
|
import { diff, gte, rcompare } from 'semver'
|
|
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
|
|
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
|
|
import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip'
|
|
import { useMainContext } from '@/app/context/main'
|
|
import { CustomColumnHeading } from '@/app/components/CustomColumnHeading'
|
|
import { Title } from '@/app/components/Title'
|
|
import { humanReadableCurrencyToString } from '@/app/utils/currency'
|
|
import { Tooltip } from '@/app/components/Tooltip'
|
|
import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants'
|
|
import { splice } from '@/app/utils'
|
|
|
|
import StyledLink from '@/app/components/StyledLink'
|
|
import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole";
|
|
|
|
function getFlagEmoji(countryCode: string) {
|
|
const codePoints = countryCode
|
|
.toUpperCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt(0));
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
|
|
const PageNodes = () => {
|
|
const [isLoading, setLoading] = React.useState(true);
|
|
const { nodes, fetchNodes } = useMainContext()
|
|
|
|
React.useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
await fetchNodes();
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const columns = useMemo<MRT_ColumnDef<any>[]>(() => {
|
|
return [
|
|
{
|
|
id: 'nym-node-data',
|
|
header: 'Nym Node Data',
|
|
columns: [
|
|
{
|
|
id: 'node_id',
|
|
header: 'Node Id',
|
|
accessorKey: 'node_id',
|
|
size: 75,
|
|
},
|
|
{
|
|
id: 'identity_key',
|
|
header: 'Identity Key',
|
|
accessorKey: 'identity_key',
|
|
size: 250,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<Stack direction="row" alignItems="center" gap={1}>
|
|
<CopyToClipboard
|
|
sx={{ mr: 0.5, color: 'grey.400' }}
|
|
smallIcons
|
|
value={row.original.bond_information.node.identity_key}
|
|
tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`}
|
|
/>
|
|
<StyledLink
|
|
to={`/network-components/nodes/${row.original.node_id}`}
|
|
dataTestId="identity-link"
|
|
color="text.primary"
|
|
>
|
|
{splice(7, 29, row.original.bond_information.node.identity_key)}
|
|
</StyledLink>
|
|
</Stack>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'version',
|
|
header: 'Version',
|
|
accessorKey: 'description.build_information.build_version',
|
|
size: 75,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/nodes/${row.original.node_id}`}
|
|
data-testid="version"
|
|
color="text.primary"
|
|
>
|
|
{row.original.description?.build_information?.build_version || "-"}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'contract_node_type',
|
|
header: 'Kind',
|
|
accessorKey: 'contract_node_type',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/nodes/${row.original.node_id}`}
|
|
data-testid="contract_node_type"
|
|
color="text.primary"
|
|
>
|
|
<code>{row.original.contract_node_type || "-"}</code>
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'declared_role',
|
|
header: 'Declare Role',
|
|
accessorKey: 'description.declared_role',
|
|
size: 250,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<Box
|
|
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
|
data-testid="declared_role-button"
|
|
>
|
|
<DeclaredRole declared_role={row.original.description?.declared_role}/>
|
|
</Box>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'total_stake',
|
|
header: 'Total Stake',
|
|
accessorKey: 'description.total_stake',
|
|
size: 250,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<Box
|
|
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
|
data-testid="total_stake-button"
|
|
>
|
|
{humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })}
|
|
</Box>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'location',
|
|
header: 'Location',
|
|
accessorKey: 'location.country_name',
|
|
size: 75,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<Box
|
|
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
|
data-testid="location-button"
|
|
>
|
|
<Tooltip
|
|
text={row.original.location?.country_name || "-"}
|
|
id="nym-node-location-text"
|
|
>
|
|
<Box
|
|
sx={{
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())} {row.original.location.two_letter_iso_country_code}</> : <>-</> }
|
|
</Box>
|
|
</Tooltip>
|
|
</Box>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'host',
|
|
header: 'IP',
|
|
accessorKey: 'bond_information.node.host',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/nodes/${row.original.node_id}`}
|
|
data-testid="host"
|
|
color="text.primary"
|
|
>
|
|
{row.original.bond_information?.node?.host || "-"}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'owner',
|
|
header: 'Owner',
|
|
accessorKey: 'bond_information.owner',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`}
|
|
target="_blank"
|
|
data-testid="bond_information.node.owner"
|
|
color="text.primary"
|
|
>
|
|
{splice(7, 29, row.original.bond_information?.owner)}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}, [])
|
|
|
|
const table = useMaterialReactTable({
|
|
columns,
|
|
data: nodes?.data || [],
|
|
state: {
|
|
isLoading,
|
|
showLoadingOverlay: isLoading,
|
|
},
|
|
initialState: {
|
|
isLoading: true,
|
|
showLoadingOverlay: true,
|
|
}
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<Box mb={2}>
|
|
<Title text="Nym Nodes" />
|
|
</Box>
|
|
<Grid container>
|
|
<Grid item xs={12}>
|
|
<Card
|
|
sx={{
|
|
padding: 2,
|
|
height: '100%',
|
|
}}
|
|
>
|
|
<MaterialReactTable table={table} />
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default PageNodes
|