Files
Fouad e5f41731ae Explorer NextJS Rebuild (#4534)
* bootstrap next app + add overview page

* fix AssetList type

* fix up nav stuff

* Refactor Nav component and add network components pages

* Refactor WorldMap component and update TelegramIcon, GitHubIcon, NymVpnIcon, DiscordIcon, and TwitterIcon components

* add service providers page

* mixnodes page

* delegations page + use material react table for all tables

* nodes map page

* Refactor StyledLink component and remove unnecessary console.log statements

* Refactor ESLint configuration, remove unused dependencies, and update component imports

* update deps

* Refactor imports and update dependencies

* fix dark mode

* build single mixnode page

* build single gateway page

* Refactor handleOnDelegate function to use useCallback in mixnodes page.tsx

* Add defaults for constants

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2024-05-16 16:12:06 +01:00

401 lines
13 KiB
TypeScript

'use client'
import React, { useCallback, useMemo } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import {
MaterialReactTable,
useMaterialReactTable,
type MRT_ColumnDef,
} from 'material-react-table'
import { Grid, Card, Button, Box, Stack } from '@mui/material'
import {
CustomColumnHeading,
DelegateIconButton,
DelegateModal,
DelegationModal,
DelegationModalProps,
MixNodeStatusDropdown,
MixnodeRowType,
StyledLink,
TableToolbar,
Title,
Tooltip,
mixnodeToGridRow,
} from '@/app/components'
import { DelegationsProvider } from '@/app/context/delegations'
import { useWalletContext } from '@/app/context/wallet'
import { useGetMixNodeStatusColor, useIsMobile } from '@/app/hooks'
import { useMainContext } from '@/app/context/main'
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'
import { splice } from '@/app/utils'
import { currencyToString } from '@/app/utils/currency'
import { NYM_BIG_DIPPER } from '@/app/api/constants'
import {
MixnodeStatusWithAll,
toMixnodeStatus,
} from '@/app/typeDefs/explorer-api'
export default function MixnodesPage() {
const isMobile = useIsMobile()
const { isWalletConnected } = useWalletContext()
const { mixnodes, fetchMixnodes } = useMainContext()
const router = useRouter()
const [itemSelectedForDelegation, setItemSelectedForDelegation] =
React.useState<{
mixId: number
identityKey: string
}>()
const [confirmationModalProps, setConfirmationModalProps] = React.useState<
DelegationModalProps | undefined
>()
const search = useSearchParams()
const status = search.get('status') as MixnodeStatusWithAll
React.useEffect(() => {
// when the status changes, get the mixnodes
fetchMixnodes(toMixnodeStatus(status))
}, [status])
const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => {
router.push(
newStatus && newStatus !== 'all'
? `/network-components/mixnodes?status=${newStatus}`
: '/network-components/mixnodes'
)
}
const handleOnDelegate = useCallback(
({ identityKey, mixId }: { identityKey: string; mixId: number }) => {
if (!isWalletConnected) {
setConfirmationModalProps({
status: 'info',
message: 'Please connect your wallet to delegate',
})
} else {
setItemSelectedForDelegation({ identityKey, mixId })
}
},
[isWalletConnected]
)
const handleNewDelegation = (delegationModalProps: DelegationModalProps) => {
setItemSelectedForDelegation(undefined)
setConfirmationModalProps(delegationModalProps)
}
const columns = useMemo<MRT_ColumnDef<MixnodeRowType>[]>(() => {
return [
{
id: 'mixnode-data',
header: 'Mixnode Data',
columns: [
{
id: 'delegate',
accessorKey: 'delegate',
size: isMobile ? 50 : 150,
header: '',
grow: false,
Cell: ({ row }) => (
<DelegateIconButton
size="small"
onDelegate={() =>
handleOnDelegate({
identityKey: row.original.identity_key,
mixId: row.original.mix_id,
})
}
/>
),
enableSorting: false,
enableColumnActions: false,
Filter: () => null,
},
{
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.identity_key}
tooltip={`Copy identity key ${row.original.identity_key} to clipboard`}
/>
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
dataTestId="identity-link"
>
{splice(7, 29, row.original.identity_key)}
</StyledLink>
</Stack>
)
},
},
{
id: 'bond',
header: 'Stake',
accessorKey: 'bond',
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>
{currencyToString({ amount: row.original.bond.toString() })}
</StyledLink>
),
},
{
id: 'stake_saturation',
header: 'Stake Saturation',
accessorKey: 'stake_saturation',
size: 225,
Header() {
return (
<CustomColumnHeading
headingTitle="Stake Saturation"
tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set."
/>
)
},
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>{`${row.original.stake_saturation} %`}</StyledLink>
),
},
{
id: 'pledge_amount',
header: 'Bond',
accessorKey: 'pledge_amount',
size: 185,
Header: () => (
<CustomColumnHeading
headingTitle="Bond"
tooltipInfo="Node operator's share of stake."
/>
),
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>
{currencyToString({
amount: row.original.pledge_amount.toString(),
})}
</StyledLink>
),
},
{
id: 'profit_percentage',
accessorKey: 'profit_percentage',
header: 'Profit Margin',
size: 145,
Header: () => (
<CustomColumnHeading
headingTitle="Profit Margin"
tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators."
/>
),
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>{`${row.original.profit_percentage}%`}</StyledLink>
),
},
{
id: 'operating_cost',
accessorKey: 'operating_cost',
size: 220,
header: 'Operating Cost',
disableColumnMenu: true,
Header: () => (
<CustomColumnHeading
headingTitle="Operating Cost"
tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators."
/>
),
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>{`${row.original.operating_cost} NYM`}</StyledLink>
),
},
{
id: 'node_performance',
accessorKey: 'node_performance',
size: 200,
header: 'Routing Score',
Header: () => (
<CustomColumnHeading
headingTitle="Routing Score"
tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test."
/>
),
Cell: ({ row }) => (
<StyledLink
to={`/network-components/mixnodes/${row.original.mix_id}`}
color={useGetMixNodeStatusColor(row.original.status)}
>{`${row.original.node_performance}%`}</StyledLink>
),
},
{
id: 'owner',
accessorKey: 'owner',
size: 150,
header: 'Owner',
Header: () => <CustomColumnHeading headingTitle="Owner" />,
Cell: ({ row }) => (
<StyledLink
to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`}
color={useGetMixNodeStatusColor(row.original.status)}
target="_blank"
data-testid="big-dipper-link"
>
{splice(7, 29, row.original.owner)}
</StyledLink>
),
},
{
id: 'location',
accessorKey: 'location',
header: 'Location',
maxSize: 150,
Header: () => <CustomColumnHeading headingTitle="Location" />,
Cell: ({ row }) => (
<Tooltip text={row.original.location} id="mixnode-location-text">
<Box
sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
color: useGetMixNodeStatusColor(row.original.status),
}}
>
{row.original.location}
</Box>
</Tooltip>
),
},
{
id: 'host',
accessorKey: 'host',
header: 'Host',
size: 130,
Header: () => <CustomColumnHeading headingTitle="Host" />,
Cell: ({ row }) => (
<StyledLink
color={useGetMixNodeStatusColor(row.original.status)}
to={`/network-components/mixnodes/${row.original.mix_id}`}
>
{row.original.host}
</StyledLink>
),
},
],
},
]
}, [handleOnDelegate, isMobile])
const data = useMemo(() => {
return mixnodeToGridRow(mixnodes?.data)
}, [mixnodes?.data])
const table = useMaterialReactTable({
columns,
data,
enableFullScreenToggle: false,
state: {
isLoading: mixnodes?.isLoading,
},
layoutMode: 'grid-no-grow',
initialState: {
columnPinning: { left: ['delegate'] },
},
})
return (
<DelegationsProvider>
<Box mb={2}>
<Title text="Mixnodes" />
</Box>
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
height: '100%',
}}
>
<TableToolbar
childrenBefore={
<MixNodeStatusDropdown
sx={{ mr: 2 }}
status={status}
onSelectionChanged={handleMixnodeStatusChanged}
/>
}
childrenAfter={
isWalletConnected && (
<Button
fullWidth
size="large"
variant="outlined"
onClick={() => router.push('/delegations')}
>
Delegations
</Button>
)
}
/>
<MaterialReactTable table={table} />
</Card>
</Grid>
</Grid>
{itemSelectedForDelegation && (
<DelegateModal
onClose={() => {
setItemSelectedForDelegation(undefined)
}}
header="Delegate"
buttonText="Delegate stake"
denom="nym"
onOk={(delegationModalProps: DelegationModalProps) =>
handleNewDelegation(delegationModalProps)
}
identityKey={itemSelectedForDelegation.identityKey}
mixId={itemSelectedForDelegation.mixId}
/>
)}
{confirmationModalProps && (
<DelegationModal
{...confirmationModalProps}
open={Boolean(confirmationModalProps)}
onClose={async () => {
setConfirmationModalProps(undefined)
if (confirmationModalProps.status === 'success') {
router.push('/delegations')
}
}}
sx={{
width: {
xs: '90%',
sm: 600,
},
}}
/>
)}
</DelegationsProvider>
)
}