mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
skeleton for block details
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Head from 'next/head';
|
||||
import type { RoutedQuery } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import getSeo from 'lib/next/block/getSeo';
|
||||
import Block from 'ui/pages/Block';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
|
||||
|
||||
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
|
||||
const { title, description } = getSeo({ height });
|
||||
return (
|
||||
|
||||
+116
-61
@@ -1,4 +1,4 @@
|
||||
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -19,7 +19,6 @@ import dayjs from 'lib/date/dayjs';
|
||||
import { space } from 'lib/html-entities';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
|
||||
import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
@@ -42,7 +41,7 @@ const BlockDetails = ({ query }: Props) => {
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
const { data, isLoading, isError, error } = query;
|
||||
const { data, isPlaceholderData, isError, error } = query;
|
||||
|
||||
const handleCutClick = React.useCallback(() => {
|
||||
setIsExpanded((flag) => !flag);
|
||||
@@ -63,10 +62,6 @@ const BlockDetails = ({ query }: Props) => {
|
||||
router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined);
|
||||
}, [ data, router ]);
|
||||
|
||||
if (isLoading) {
|
||||
return <BlockDetailsSkeleton/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error?.status === 404) {
|
||||
throw Error('Block not found', { cause: error as unknown as Error });
|
||||
@@ -79,6 +74,10 @@ const BlockDetails = ({ query }: Props) => {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sectionGap = (
|
||||
<GridItem
|
||||
colSpan={{ base: undefined, lg: 2 }}
|
||||
@@ -92,13 +91,50 @@ const BlockDetails = ({ query }: Props) => {
|
||||
|
||||
const validatorTitle = getNetworkValidatorTitle();
|
||||
|
||||
const rewardBreakDown = (() => {
|
||||
if (appConfig.L2.isL2Network || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isPlaceholderData) {
|
||||
return <Skeleton w="525px" h="20px"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text variant="secondary" whiteSpace="break-spaces">
|
||||
<Tooltip label="Static block reward">
|
||||
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
{ !txFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
{ space }+{ space }
|
||||
<Tooltip label="Txn fees">
|
||||
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) }
|
||||
{ !burntFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
{ space }-{ space }
|
||||
<Tooltip label="Burnt fees">
|
||||
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) }
|
||||
</Text>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
|
||||
<DetailsInfoItem
|
||||
title={ `${ data.type === 'reorg' ? 'Reorg' : 'Block' } height` }
|
||||
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ data.height }
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ data.height }
|
||||
</Skeleton>
|
||||
{ data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
|
||||
<PrevNext
|
||||
ml={ 6 }
|
||||
@@ -106,37 +142,52 @@ const BlockDetails = ({ query }: Props) => {
|
||||
prevLabel="View previous block"
|
||||
nextLabel="View next block"
|
||||
isPrevDisabled={ data.height === 0 }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Size"
|
||||
hint="Size of the block in bytes"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ data.size.toLocaleString() }
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ data.size.toLocaleString() }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Timestamp"
|
||||
hint="Date & time at which block was produced."
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
|
||||
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } boxSize={ 5 } borderRadius="full">
|
||||
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
|
||||
</Skeleton>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
|
||||
{ dayjs(data.timestamp).fromNow() }
|
||||
</Skeleton>
|
||||
<TextSeparator/>
|
||||
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } whiteSpace="normal">
|
||||
{ dayjs(data.timestamp).format('LLLL') }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Transactions"
|
||||
hint="The number of transactions in the block"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'txs' } }) }>
|
||||
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
|
||||
</LinkInternal>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'txs' } }) }>
|
||||
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
|
||||
</LinkInternal>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
|
||||
hint="A block producer who successfully included the block onto the blockchain"
|
||||
columnGap={ 1 }
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<AddressLink type="address" hash={ data.miner.hash }/>
|
||||
<AddressLink type="address" hash={ data.miner.hash } isLoading={ isPlaceholderData }/>
|
||||
{ data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> }
|
||||
{ /* api doesn't return the block processing time yet */ }
|
||||
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
|
||||
@@ -149,31 +200,12 @@ const BlockDetails = ({ query }: Props) => {
|
||||
on top of the fees paid for all transactions in the block`
|
||||
}
|
||||
columnGap={ 1 }
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
|
||||
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && (
|
||||
<Text variant="secondary" whiteSpace="break-spaces">(
|
||||
<Tooltip label="Static block reward">
|
||||
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
{ !txFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
{ space }+{ space }
|
||||
<Tooltip label="Txn fees">
|
||||
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) }
|
||||
{ !burntFees.isEqualTo(ZERO) && (
|
||||
<>
|
||||
{ space }-{ space }
|
||||
<Tooltip label="Burnt fees">
|
||||
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) }
|
||||
)</Text>
|
||||
) }
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
|
||||
</Skeleton>
|
||||
{ rewardBreakDown }
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ data.rewards
|
||||
@@ -195,35 +227,49 @@ const BlockDetails = ({ query }: Props) => {
|
||||
<DetailsInfoItem
|
||||
title="Gas used"
|
||||
hint="The total gas amount used in the block and its percentage of gas filled in the block"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ BigNumber(data.gas_used || 0).toFormat() }
|
||||
</Skeleton>
|
||||
<Utilization
|
||||
ml={ 4 }
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isPlaceholderData }/>
|
||||
</>
|
||||
) }
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Gas limit"
|
||||
hint="Total gas limit provided by all transactions in the block"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ BigNumber(data.gas_limit).toFormat() }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ BigNumber(data.gas_limit).toFormat() }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
{ data.base_fee_per_gas && (
|
||||
<DetailsInfoItem
|
||||
title="Base fee per gas"
|
||||
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
|
||||
<Text variant="secondary" whiteSpace="pre">
|
||||
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
|
||||
</Text>
|
||||
{ isPlaceholderData ? (
|
||||
<Skeleton isLoaded={ !isPlaceholderData } h="20px" maxW="380px" w="100%"/>
|
||||
) : (
|
||||
<>
|
||||
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
|
||||
<Text variant="secondary" whiteSpace="pre">
|
||||
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
|
||||
</Text>
|
||||
</>
|
||||
) }
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
<DetailsInfoItem
|
||||
@@ -233,15 +279,21 @@ const BlockDetails = ({ query }: Props) => {
|
||||
|
||||
Equals Block Base Fee per Gas * Gas Used`
|
||||
}
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
|
||||
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } boxSize={ 5 }>
|
||||
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
|
||||
</Skeleton>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
|
||||
{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
|
||||
</Skeleton>
|
||||
{ !txFees.isEqualTo(ZERO) && (
|
||||
<Tooltip label="Burnt fees / Txn fees * 100%">
|
||||
<Box>
|
||||
<Utilization
|
||||
ml={ 4 }
|
||||
value={ burntFees.dividedBy(txFees).toNumber() }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -251,8 +303,11 @@ const BlockDetails = ({ query }: Props) => {
|
||||
<DetailsInfoItem
|
||||
title="Priority fee / Tip"
|
||||
hint="User-defined tips sent to validator for transaction priority/inclusion"
|
||||
isLoading={ isPlaceholderData }
|
||||
>
|
||||
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ /* api doesn't support extra data yet */ }
|
||||
@@ -267,21 +322,21 @@ const BlockDetails = ({ query }: Props) => {
|
||||
{ /* CUT */ }
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }}>
|
||||
<Element name="BlockDetails__cutLink">
|
||||
<Link
|
||||
mt={ 6 }
|
||||
display="inline-block"
|
||||
fontSize="sm"
|
||||
textDecorationLine="underline"
|
||||
textDecorationStyle="dashed"
|
||||
onClick={ handleCutClick }
|
||||
>
|
||||
{ isExpanded ? 'Hide details' : 'View details' }
|
||||
</Link>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } mt={ 6 } display="inline-block">
|
||||
<Link
|
||||
fontSize="sm"
|
||||
textDecorationLine="underline"
|
||||
textDecorationStyle="dashed"
|
||||
onClick={ handleCutClick }
|
||||
>
|
||||
{ isExpanded ? 'Hide details' : 'View details' }
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Element>
|
||||
</GridItem>
|
||||
|
||||
{ /* ADDITIONAL INFO */ }
|
||||
{ isExpanded && (
|
||||
{ isExpanded && !isPlaceholderData && (
|
||||
<>
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
|
||||
|
||||
const BlockDetailsSkeleton = () => {
|
||||
|
||||
const sectionGap = (
|
||||
<GridItem
|
||||
colSpan={{ base: undefined, lg: 2 }}
|
||||
mt={{ base: 2, lg: 3 }}
|
||||
mb={{ base: 0, lg: 3 }}
|
||||
borderBottom="1px solid"
|
||||
borderColor="divider"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
|
||||
<DetailsSkeletonRow w="25%"/>
|
||||
<DetailsSkeletonRow w="25%"/>
|
||||
<DetailsSkeletonRow w="65%"/>
|
||||
<DetailsSkeletonRow w="25%"/>
|
||||
<DetailsSkeletonRow/>
|
||||
<DetailsSkeletonRow/>
|
||||
{ sectionGap }
|
||||
<DetailsSkeletonRow w="50%"/>
|
||||
<DetailsSkeletonRow w="25%"/>
|
||||
<DetailsSkeletonRow w="50%"/>
|
||||
<DetailsSkeletonRow w="50%"/>
|
||||
<DetailsSkeletonRow w="50%"/>
|
||||
{ sectionGap }
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }}>
|
||||
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockDetailsSkeleton;
|
||||
+7
-3
@@ -10,6 +10,7 @@ import { useAppContext } from 'lib/appContext';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { BLOCK } from 'stubs/block';
|
||||
import BlockDetails from 'ui/block/BlockDetails';
|
||||
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
@@ -35,14 +36,17 @@ const BlockPageContent = () => {
|
||||
|
||||
const blockQuery = useApiQuery('block', {
|
||||
pathParams: { height },
|
||||
queryOptions: { enabled: Boolean(height) },
|
||||
queryOptions: {
|
||||
enabled: Boolean(height),
|
||||
placeholderData: BLOCK,
|
||||
},
|
||||
});
|
||||
|
||||
const blockTxsQuery = useQueryWithPages({
|
||||
resourceName: 'block_txs',
|
||||
pathParams: { height },
|
||||
options: {
|
||||
enabled: Boolean(blockQuery.data?.height && tab === 'txs'),
|
||||
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,7 +54,7 @@ const BlockPageContent = () => {
|
||||
resourceName: 'block_withdrawals',
|
||||
pathParams: { height },
|
||||
options: {
|
||||
enabled: Boolean(blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
|
||||
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
import { Box, Icon, IconButton, chakra, Tooltip } from '@chakra-ui/react';
|
||||
import { Box, Icon, IconButton, chakra, Tooltip, Flex, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import eastArrow from 'icons/arrows/east-mini.svg';
|
||||
@@ -10,9 +10,10 @@ interface Props {
|
||||
nextLabel?: string;
|
||||
isPrevDisabled?: boolean;
|
||||
isNextDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled }: Props) => {
|
||||
const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled, isLoading }: Props) => {
|
||||
const handelPrevClick = React.useCallback(() => {
|
||||
onClick('prev');
|
||||
}, [ onClick ]);
|
||||
@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
|
||||
onClick('next');
|
||||
}, [ onClick ]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex columnGap="10px" className={ className }>
|
||||
<Skeleton boxSize={ 6 } borderRadius="sm"/>
|
||||
<Skeleton boxSize={ 6 } borderRadius="sm"/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={ className }>
|
||||
<Tooltip label={ prevLabel }>
|
||||
|
||||
Reference in New Issue
Block a user