e5f41731ae
* 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>
406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo } from 'react'
|
|
import { Box, Card, Grid, Stack } from '@mui/material'
|
|
import { useTheme } from '@mui/material/styles'
|
|
import {
|
|
MRT_ColumnDef,
|
|
MaterialReactTable,
|
|
useMaterialReactTable,
|
|
} from 'material-react-table'
|
|
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 { diff, gte, rcompare } from 'semver'
|
|
import { useMainContext } from '@/app/context/main'
|
|
import { TableToolbar } from '@/app/components/TableToolbar'
|
|
import { CustomColumnHeading } from '@/app/components/CustomColumnHeading'
|
|
import { Title } from '@/app/components/Title'
|
|
import { unymToNym } from '@/app/utils/currency'
|
|
import { Tooltip } from '@/app/components/Tooltip'
|
|
import { NYM_BIG_DIPPER } from '@/app/api/constants'
|
|
import { splice } from '@/app/utils'
|
|
import {
|
|
VersionDisplaySelector,
|
|
VersionSelectOptions,
|
|
} from '@/app/components/Gateways/VersionDisplaySelector'
|
|
import StyledLink from '@/app/components/StyledLink'
|
|
import {
|
|
GatewayRowType,
|
|
gatewayToGridRow,
|
|
} from '@/app/components/Gateways/Gateways'
|
|
|
|
const PageGateways = () => {
|
|
const { gateways } = useMainContext()
|
|
const [versionFilter, setVersionFilter] =
|
|
React.useState<VersionSelectOptions>(VersionSelectOptions.all)
|
|
|
|
const theme = useTheme()
|
|
|
|
const highestVersion = React.useMemo(() => {
|
|
if (gateways?.data) {
|
|
const versions = gateways.data.reduce(
|
|
(a: string[], b) => [...a, b.gateway.version],
|
|
[]
|
|
)
|
|
const [lastestVersion] = versions.sort(rcompare)
|
|
return lastestVersion
|
|
}
|
|
// fallback value
|
|
return '2.0.0'
|
|
}, [gateways])
|
|
|
|
const filterByLatestVersions = React.useMemo(() => {
|
|
const filtered = gateways?.data?.filter((gw) => {
|
|
const versionDiff = diff(highestVersion, gw.gateway.version)
|
|
return versionDiff === 'patch' || versionDiff === null
|
|
})
|
|
if (filtered) return filtered
|
|
return []
|
|
}, [gateways])
|
|
|
|
const filterByOlderVersions = React.useMemo(() => {
|
|
const filtered = gateways?.data?.filter((gw) => {
|
|
const versionDiff = diff(highestVersion, gw.gateway.version)
|
|
return versionDiff === 'major' || versionDiff === 'minor'
|
|
})
|
|
if (filtered) return filtered
|
|
return []
|
|
}, [gateways])
|
|
|
|
const filteredByVersion = React.useMemo(() => {
|
|
switch (versionFilter) {
|
|
case VersionSelectOptions.latestVersion:
|
|
return filterByLatestVersions
|
|
case VersionSelectOptions.olderVersions:
|
|
return filterByOlderVersions
|
|
case VersionSelectOptions.all:
|
|
return gateways?.data || []
|
|
default:
|
|
return []
|
|
}
|
|
}, [versionFilter, gateways])
|
|
|
|
const data = useMemo(() => {
|
|
return gatewayToGridRow(filteredByVersion || [])
|
|
}, [filteredByVersion])
|
|
|
|
const columns = useMemo<MRT_ColumnDef<GatewayRowType>[]>(() => {
|
|
return [
|
|
{
|
|
id: 'gateway-data',
|
|
header: 'Gatewsay Data',
|
|
columns: [
|
|
{
|
|
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/gateways/${row.original.identity_key}`}
|
|
dataTestId="identity-link"
|
|
color="text.primary"
|
|
>
|
|
{splice(7, 29, row.original.identity_key)}
|
|
</StyledLink>
|
|
</Stack>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'node_performance',
|
|
header: 'Node Performance',
|
|
accessorKey: 'node_performance',
|
|
size: 200,
|
|
Header: () => {
|
|
return (
|
|
<Box display="flex">
|
|
<InfoTooltip
|
|
id="gateways-list-routing-score"
|
|
title="Gateway'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"
|
|
placement="top-start"
|
|
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
|
bgColor={
|
|
theme.palette.nym.networkExplorer.tooltip.background
|
|
}
|
|
maxWidth={230}
|
|
arrow
|
|
/>
|
|
<CustomColumnHeading headingTitle="Routing Score" />
|
|
</Box>
|
|
)
|
|
},
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${row.original.identity_key}`}
|
|
data-testid="node-performance"
|
|
color="text.primary"
|
|
>
|
|
{`${row.original.node_performance}%`}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'version',
|
|
header: 'Version',
|
|
accessorKey: 'version',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${row.original.identity_key}`}
|
|
data-testid="version"
|
|
color="text.primary"
|
|
>
|
|
{row.original.version}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'location',
|
|
header: 'Location',
|
|
accessorKey: 'location',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<Box
|
|
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
|
data-testid="location-button"
|
|
>
|
|
<Tooltip
|
|
text={row.original.location}
|
|
id="gateway-location-text"
|
|
>
|
|
<Box
|
|
sx={{
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{row.original.location}
|
|
</Box>
|
|
</Tooltip>
|
|
</Box>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'host',
|
|
header: 'IP:Port',
|
|
accessorKey: 'host',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${row.original.identity_key}`}
|
|
data-testid="host"
|
|
color="text.primary"
|
|
>
|
|
{row.original.host}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
id: 'owner',
|
|
header: 'Owner',
|
|
accessorKey: 'owner',
|
|
size: 150,
|
|
Cell: ({ row }) => {
|
|
return (
|
|
<StyledLink
|
|
to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`}
|
|
target="_blank"
|
|
data-testid="owner"
|
|
color="text.primary"
|
|
>
|
|
{splice(7, 29, row.original.owner)}
|
|
</StyledLink>
|
|
)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}, [])
|
|
|
|
const _columns: GridColDef[] = [
|
|
{
|
|
field: 'node_performance',
|
|
align: 'center',
|
|
renderHeader: () => (
|
|
<>
|
|
<InfoTooltip
|
|
id="gateways-list-routing-score"
|
|
title="Gateway'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"
|
|
placement="top-start"
|
|
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
|
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
|
maxWidth={230}
|
|
arrow
|
|
/>
|
|
<CustomColumnHeading headingTitle="Routing Score" />
|
|
</>
|
|
),
|
|
width: 120,
|
|
disableColumnMenu: true,
|
|
headerAlign: 'center',
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${params.row.identity_key}`}
|
|
data-testid="pledge-amount"
|
|
>
|
|
{`${params.value}%`}
|
|
</StyledLink>
|
|
),
|
|
},
|
|
{
|
|
field: 'version',
|
|
align: 'center',
|
|
renderHeader: () => <CustomColumnHeading headingTitle="Version" />,
|
|
width: 150,
|
|
disableColumnMenu: true,
|
|
headerAlign: 'center',
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${params.row.identity_key}`}
|
|
data-testid="version"
|
|
>
|
|
{params.value}
|
|
</StyledLink>
|
|
),
|
|
sortComparator: (a, b) => {
|
|
if (gte(a, b)) return 1
|
|
return -1
|
|
},
|
|
},
|
|
{
|
|
field: 'location',
|
|
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
|
width: 180,
|
|
disableColumnMenu: true,
|
|
headerAlign: 'left',
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<Box
|
|
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
|
data-testid="location-button"
|
|
>
|
|
<Tooltip text={params.value} id="gateway-location-text">
|
|
<Box
|
|
sx={{
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{params.value}
|
|
</Box>
|
|
</Tooltip>
|
|
</Box>
|
|
),
|
|
},
|
|
{
|
|
field: 'host',
|
|
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
|
width: 180,
|
|
disableColumnMenu: true,
|
|
headerAlign: 'left',
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${params.row.identity_key}`}
|
|
data-testid="host"
|
|
>
|
|
{params.value}
|
|
</StyledLink>
|
|
),
|
|
},
|
|
{
|
|
field: 'owner',
|
|
headerName: 'Owner',
|
|
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
|
width: 180,
|
|
disableColumnMenu: true,
|
|
headerAlign: 'left',
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<StyledLink
|
|
to={`${NYM_BIG_DIPPER}/account/${params.value}`}
|
|
target="_blank"
|
|
data-testid="owner"
|
|
>
|
|
{splice(7, 29, params.value)}
|
|
</StyledLink>
|
|
),
|
|
},
|
|
{
|
|
field: 'bond',
|
|
width: 150,
|
|
disableColumnMenu: true,
|
|
type: 'number',
|
|
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
|
|
headerClassName: 'MuiDataGrid-header-override',
|
|
headerAlign: 'left',
|
|
renderCell: (params: GridRenderCellParams) => (
|
|
<StyledLink
|
|
to={`/network-components/gateways/${params.row.identity_key}`}
|
|
data-testid="pledge-amount"
|
|
>
|
|
{`${unymToNym(params.value, 6)}`}
|
|
</StyledLink>
|
|
),
|
|
},
|
|
]
|
|
|
|
const table = useMaterialReactTable({
|
|
columns,
|
|
data,
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<Box mb={2}>
|
|
<Title text="Gateways" />
|
|
</Box>
|
|
<Grid container>
|
|
<Grid item xs={12}>
|
|
<Card
|
|
sx={{
|
|
padding: 2,
|
|
height: '100%',
|
|
}}
|
|
>
|
|
<TableToolbar
|
|
childrenBefore={
|
|
<VersionDisplaySelector
|
|
handleChange={(option) => setVersionFilter(option)}
|
|
selected={versionFilter}
|
|
/>
|
|
}
|
|
/>
|
|
<MaterialReactTable table={table} />
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default PageGateways
|