Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Validators #80

Merged
merged 8 commits into from
Aug 1, 2024
Merged
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
1 change: 1 addition & 0 deletions src/components/Blocks/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const DetailsTab = ({ block }: { block: IChainBlockInfoResponse }) => {
fontWeight={'normal'}
h={0}
fontSize={'md'}
to={generatePath(RoutePath.Validator, { address: proposer })}
>
{proposer}
</ReducedTextAndCopy>
Expand Down
12 changes: 7 additions & 5 deletions src/components/Layout/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ const withCopyLogic = (Component: typeof IconButton | typeof Button) => {
export const CopyButton = withCopyLogic(Button)
export const CopyButtonIcon = withCopyLogic(IconButton)

export type ReducedTextAndCopyProps = {
breakPoint?: Parameters<typeof useBreakpointValue>[0]
to?: string
children?: string
} & ICopyButton

/**
* It shows a text with a copy button.
* if the length of the string is more than 13 it cut the string to something like 6be21a...0000.
Expand All @@ -56,11 +62,7 @@ export const ReducedTextAndCopy = ({
to,
children = '',
...rest
}: {
to?: string
breakPoint?: Parameters<typeof useBreakpointValue>[0]
children?: string
} & ICopyButton) => {
}: ReducedTextAndCopyProps) => {
let text = children
// If breakpoint is true and the length of the string is more than 13 it shorts the string
if (breakPoint && useBreakpointValue(breakPoint) && children.length > 13) {
Expand Down
34 changes: 34 additions & 0 deletions src/components/Layout/IconLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Flex, Icon, IconProps, Link } from '@chakra-ui/react'
import { generatePath, Link as RouterLink } from 'react-router-dom'
import { PropsWithChildren } from 'react'
import { RoutePath } from '~constants'
import { HiOutlineCube } from 'react-icons/hi2'
import { IconType } from 'react-icons'

export const BlockIconLink = ({ height }: { height: number }) => (
<IconLink
icon={HiOutlineCube}
to={generatePath(RoutePath.Block, {
height: height.toString(),
page: null,
})}
>
{height}
</IconLink>
)

type IconLinkProps = {
to: string
icon: IconType
} & IconProps

export const IconLink = ({ to, icon, children, ...iconProps }: IconLinkProps & PropsWithChildren) => {
return (
<Link as={RouterLink} to={to}>
<Flex align='center' gap={2}>
<Icon as={icon} {...iconProps} />
<span>{children}</span>
</Flex>
</Link>
)
}
118 changes: 118 additions & 0 deletions src/components/Validators/Detail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Flex, Heading, HStack, Tab, TabList, TabPanel, TabPanels, Text, VStack } from '@chakra-ui/react'
import { ensure0x, IChainValidator } from '@vocdoni/sdk'
import { Trans, useTranslation } from 'react-i18next'
import { DetailsGrid, GridItemProps } from '~components/Layout/DetailsGrid'
import { QueryParamsTabs } from '~components/Layout/QueryParamsTabs'
import { RawContentBox } from '~components/Layout/ShowRawButton'
import { ReducedTextAndCopy } from '~components/Layout/CopyButton'
import { BlockIconLink } from '~components/Layout/IconLink'
import { ValidatorName } from '~components/Validators/ValidatorCard'
import { generatePath } from 'react-router-dom'
import { RoutePath } from '~constants'

export type ValidatorFixedType = IChainValidator & {
// todo(kon): delete this type extension when https://github.com/vocdoni/vocdoni-sdk/pull/402 is merged
joinHeight: number
proposals: number
score: number
validatorAddress: string
votes: number
}

const DetailsTab = ({ validator }: { validator: ValidatorFixedType }) => {
const address = ensure0x(validator.address)
const pubKey = ensure0x(validator.pubKey)

const { t } = useTranslation()

const details: GridItemProps[] = [
{
label: t('validators.pubKey', { defaultValue: 'Public Key' }),
children: (
<ReducedTextAndCopy
breakPoint={{ base: true, lg: false }}
p={0}
color={'textAccent1'}
toCopy={pubKey}
fontWeight={'normal'}
h={0}
fontSize={'md'}
>
{pubKey}
</ReducedTextAndCopy>
),
},
{
label: t('validators.account', { defaultValue: 'Account' }),
children: (
<ReducedTextAndCopy
breakPoint={{ base: true, lg: false }}
p={0}
color={'textAccent1'}
toCopy={address}
fontWeight={'normal'}
h={0}
fontSize={'md'}
to={generatePath(RoutePath.Organization, { pid: address, page: null })}
>
{address}
</ReducedTextAndCopy>
),
},
{
label: t('validators.voting_power_cell', { defaultValue: 'Voting power' }),
children: validator.power,
},
{
label: t('validators.score', { defaultValue: 'Score' }),
children: validator.score,
},
{
label: t('validators.votes', { defaultValue: 'Votes' }),
children: validator.votes,
},
]

return (
<VStack align='start'>
<DetailsGrid details={details} />
</VStack>
)
}

export const ValidatorDetail = ({ validator }: { validator: ValidatorFixedType }) => {
return (
<Flex direction={'column'} mt={{ base: '20px', lg: '40px' }} gap={6} wordBreak='break-all'>
<VStack align='start' spacing={4}>
<Heading isTruncated wordBreak='break-word' mb={0}>
<Trans i18nKey={'validators.validator_details'}>Validator Details</Trans>
</Heading>
<ValidatorName name={validator.name} address={validator.validatorAddress} useCopy />
<HStack color={'lighterText'} fontWeight={'bold'}>
<Text>
<Trans i18nKey={'validators.validator_details'}>Joined on block</Trans>
</Text>
<BlockIconLink height={validator.joinHeight} />
</HStack>
</VStack>
<QueryParamsTabs>
<TabList display='flex' flexWrap='wrap'>
<Tab>
<Trans i18nKey={'process.tab_details'}>Details</Trans>
</Tab>
<Tab>
<Trans i18nKey={'raw'}>Raw</Trans>
</Tab>
</TabList>
<TabPanels>
<TabPanel>
<DetailsTab validator={validator} />
</TabPanel>
<TabPanel>
<RawContentBox obj={validator} />
</TabPanel>
</TabPanels>
</QueryParamsTabs>
</Flex>
)
}
94 changes: 65 additions & 29 deletions src/components/Validators/ValidatorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,76 @@
import { Card, CardBody, Flex, HStack, Text } from '@chakra-ui/react'
import { ensure0x, IChainValidator } from '@vocdoni/sdk'
import { Card, CardBody, Flex, HStack, Link, Text } from '@chakra-ui/react'
import { Trans } from 'react-i18next'
import { ReducedTextAndCopy } from '~components/Layout/CopyButton'
import { generatePath, Link as RouterLink } from 'react-router-dom'
import { RoutePath } from '~constants'
import { ValidatorFixedType } from '~components/Validators/Detail'

export const ValidatorCard = (validator: IChainValidator) => {
export const ValidatorName = ({ name, useCopy, address }: { name?: string; useCopy?: boolean; address: string }) => {
const showName = !!name
let addrsComponent = (
<Text fontWeight={'normal'} fontSize={showName ? 'sm' : 'xl'}>
{address}
</Text>
)
if (useCopy) {
addrsComponent = (
<ReducedTextAndCopy
color={'textAccent1'}
fontWeight={'normal'}
h={0}
px={0}
my={3}
toCopy={address}
fontSize={showName ? 'sm' : 'xl'}
>
{address}
</ReducedTextAndCopy>
)
}
return (
<Flex wrap={'wrap'} align={'baseline'} gap={2} wordBreak='break-all'>
{showName && (
<Text fontWeight={'bold'} fontSize={'xl'}>
{name}
</Text>
)}
{addrsComponent}
</Flex>
)
}

export const ValidatorCard = (validator: ValidatorFixedType) => {
return (
<Card>
<CardBody>
<Flex gap={2} direction={'column'}>
<Text fontWeight='bold' wordBreak='break-all'>
{ensure0x(validator.address)}
</Text>
<Flex fontSize={'sm'} direction={'column'} gap={2}>
<HStack gap={1}>
<Link as={RouterLink} to={generatePath(RoutePath.Validator, { address: validator.validatorAddress })}>
<CardBody>
<Flex gap={2} direction={'column'}>
<ValidatorName name={validator.name} address={validator.validatorAddress} />
<Flex fontSize={'sm'} direction={'column'} gap={2}>
<HStack gap={1}>
<Text fontWeight={'bold'}>
<Trans i18nKey='validators.pubkey'>PubKey:</Trans>
</Text>
<ReducedTextAndCopy
color={'textAccent1'}
toCopy={validator.pubKey}
fontWeight={'normal'}
h={0}
fontSize={'sm'}
p={0}
>
{validator.pubKey}
</ReducedTextAndCopy>
</HStack>
<Text fontWeight={'bold'}>
<Trans i18nKey='validators.pubkey'>PubKey:</Trans>
<Trans i18nKey='validators.voting_power' values={{ power: validator.power }}>
Voting power: {{ power: validator.power }}
</Trans>
</Text>
<ReducedTextAndCopy
color={'textAccent1'}
toCopy={validator.pubKey}
fontWeight={'normal'}
h={0}
fontSize={'sm'}
p={0}
>
{validator.pubKey}
</ReducedTextAndCopy>
</HStack>
<Text fontWeight={'bold'}>
<Trans i18nKey='validators.voting_power' values={{ power: validator.power }}>
Voting power: {{ power: validator.power }}
</Trans>
</Text>
</Flex>
</Flex>
</Flex>
</CardBody>
</CardBody>
</Link>
</Card>
)
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum RoutePath {
Transaction = '/transactions/:block/:index',
TransactionsList = '/transactions/:page?',
TransactionByHashOrHeight = '/transactions/:hashOrHeight',
Validator = '/validator/:address',
Validators = '/validators',
Verify = '/verify/:verifier?',
}
Expand Down
19 changes: 19 additions & 0 deletions src/pages/validator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useLoaderData, useParams } from 'react-router-dom'
import { ensure0x, IChainValidatorsListResponse } from '@vocdoni/sdk'
import { ValidatorDetail, ValidatorFixedType } from '~components/Validators/Detail'

const Validator = () => {
const validators = (useLoaderData() as IChainValidatorsListResponse).validators as Array<ValidatorFixedType>

const { address }: { address?: string } = useParams()

const validator = validators.find(
(v) => ensure0x(v.validatorAddress.toLowerCase()) === ensure0x(address?.toLowerCase() ?? '')
)

if (!address || !validator) throw new Error('Validator not found')

return <ValidatorDetail validator={validator} />
}

export default Validator
2 changes: 2 additions & 0 deletions src/pages/validators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const Validators = () => {
return (
<ListPageLayout title={t('validators.validators')} subtitle={subtitle}>
{validators.map((validator, i) => (
// todo(kon): remove this ignore when https://github.com/vocdoni/vocdoni-sdk/pull/402 is merged
// @ts-ignore
<ValidatorCard key={i} {...validator} />
))}
</ListPageLayout>
Expand Down
10 changes: 10 additions & 0 deletions src/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const ProcessList = lazy(() => import('~pages/processes'))
const Process = lazy(() => import('~pages/process'))
const Transaction = lazy(() => import('~pages/transaction'))
const TransactionsList = lazy(() => import('~pages/transactions'))
const Validator = lazy(() => import('~pages/validator'))
const Validators = lazy(() => import('~pages/validators'))
const Verify = lazy(() => import('~pages/verify'))

Expand Down Expand Up @@ -145,6 +146,15 @@ export const RoutesProvider = () => {
return await client.txInfoByBlock(Number(tx.blockHeight), Number(tx.transactionIndex))
},
},
{
path: RoutePath.Validator,
element: (
<SuspenseLoader>
<Validator />
</SuspenseLoader>
),
loader: async ({ params }) => await client.validatorsList(),
},
{
path: RoutePath.Validators,
element: (
Expand Down
Loading