Commit 327cee07 authored by tom's avatar tom

skeletons for blocks page

parent e938c242
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import getSeo from 'lib/next/blocks/getSeo';
import Blocks from 'ui/pages/Blocks';
import Page from 'ui/shared/Page/Page';
const Blocks = dynamic(() => import('ui/pages/Blocks'), { ssr: false });
const BlockPage: NextPage = () => {
const { title } = getSeo();
......@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => {
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
<Page>
<Blocks/>
</Page>
</>
);
};
......
import type { Block, BlocksResponse } from 'types/api/block';
import { ADDRESS_PARAMS } from './addressParams';
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
export const BLOCK: Block = {
base_fee_per_gas: '14',
burnt_fees: '92834504000000000',
burnt_fees_percentage: 42.2,
difficulty: '340282366920938463463374607431768211451',
extra_data: 'TODO',
gas_limit: '30000000',
gas_target_percentage: 55.79,
gas_used: '6631036',
gas_used_percentage: 22.10,
has_beacon_chain_withdrawals: null,
hash: BLOCK_HASH,
height: 8988736,
miner: ADDRESS_PARAMS,
nonce: '0x0000000000000000',
parent_hash: BLOCK_HASH,
priority_fee: '19241635454943109',
rewards: [
{
reward: '19241635454943109',
type: 'Validator Reward',
},
],
size: 46406,
state_root: 'TODO',
timestamp: '2023-05-12T19:29:12.000000Z',
total_difficulty: '10837812015930321201107455268036056402048391639',
tx_count: 142,
tx_fees: '19241635547777613',
type: 'block',
uncles_hashes: [],
};
export const BLOCKS: BlocksResponse = {
items: Array(50).fill(BLOCK),
next_page_params: {
block_number: 8988686,
items_count: 50,
},
};
......@@ -10,7 +10,7 @@ export interface Block {
tx_count: number;
miner: AddressParam;
size: number;
has_beacon_chain_withdrawals?: boolean;
has_beacon_chain_withdrawals: boolean | null;
hash: string;
parent_hash: string;
difficulty: string;
......
import { Text } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/react';
import type { TypographyProps } from '@chakra-ui/react';
import React from 'react';
......@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props {
ts: string;
isEnabled?: boolean;
isLoading?: boolean;
fontSize?: TypographyProps['fontSize'];
}
const BlockTimestamp = ({ ts, isEnabled, fontSize }: Props) => {
const BlockTimestamp = ({ ts, isEnabled, isLoading, fontSize }: Props) => {
const timeAgo = useTimeAgoIncrement(ts, isEnabled);
return <Text variant="secondary" fontWeight={ 400 } fontSize={ fontSize }>{ timeAgo }</Text>;
return (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight={ 400 } fontSize={ fontSize } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
);
};
export default React.memo(BlockTimestamp);
......@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => {
topic: 'blocks:new_block',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: query.isLoading || query.isError || query.pagination.page !== 1,
isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1,
});
useSocketMessage({
channel,
......@@ -78,10 +78,10 @@ const BlocksContent = ({ type, query }: Props) => {
<>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }>
<BlocksList data={ query.data.items }/>
<BlocksList data={ query.data.items } isLoading={ query.isPlaceholderData } page={ query.pagination.page }/>
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page } isLoading={ query.isPlaceholderData }/>
</Hide>
</>
) : null;
......@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => {
return (
<DataListDisplay
isError={ query.isError }
isLoading={ query.isLoading }
isLoading={ false }
items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks."
......
......@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props {
data: Array<Block>;
isLoading: boolean;
page: number;
}
const BlocksList = ({ data }: Props) => {
const BlocksList = ({ data, isLoading, page }: Props) => {
return (
<Box>
<AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) }
{ data.map((item, index) => (
<BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
/>
)) }
</AnimatePresence>
</Box>
);
......
import { Flex, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import { Flex, Skeleton, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes';
......@@ -21,11 +21,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
data: Block;
isPending?: boolean;
isLoading?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
......@@ -36,30 +36,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> }
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
</Skeleton>
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString() } bytes</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ data.size.toLocaleString() } bytes</span>
</Skeleton>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" isLoading={ isLoading }/>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
</Skeleton>
) :
<Text variant="secondary">{ data.tx_count }</Text>
}
......@@ -67,12 +72,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Box>
<Text fontWeight={ 500 }>Gas used</Text>
<Flex mt={ 2 }>
<Text variant="secondary" mr={ 4 }>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<span>{ BigNumber(data.gas_used || 0).toFormat() }</span>
</Skeleton>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() } isLoading={ isLoading }/>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</>
) }
</Flex>
......@@ -80,7 +87,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Flex>
) }
{ !appConfig.L2.isL2Network && (
......@@ -88,10 +97,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
<Skeleton isLoaded={ !isLoading } boxSize={ 5 } display="inline-block">
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" ml={ 1 }>
<span>{ burntFees.div(WEI).toFixed() }</span>
</Skeleton>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Flex>
</Box>
) }
......
......@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
isLoading?: boolean;
top: number;
page: number;
}
const BlocksTable = ({ data, top, page }: Props) => {
const BlocksTable = ({ data, isLoading, top, page }: Props) => {
return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
......@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => {
</Thead>
<Tbody>
<AnimatePresence initial={ false }>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) }
{ data.map((item, index) => (
<BlocksTableItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
enableTimeIncrement={ page === 1 && !isLoading }
isLoading={ isLoading }
/>
)) }
</AnimatePresence>
</Tbody>
</Table>
......
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import { Tr, Td, Flex, Box, Icon, Tooltip, Skeleton, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
......@@ -19,17 +19,18 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
data: Block;
isPending?: boolean;
isLoading?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return (
<Tr
as={ motion.tr }
......@@ -41,57 +42,84 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
>
<Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
</Skeleton>
</Tooltip>
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.size.toLocaleString() }
</Skeleton>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
<AddressLink
type="address"
alias={ data.miner.name }
hash={ data.miner.hash }
truncation="constant"
display="inline-flex"
maxW="100%"
isLoading={ isLoading }
/>
</Td>
<Td isNumeric fontSize="sm">
{ data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
</Skeleton>
) : data.tx_count }
</Td>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }>
<Tooltip label="Gas Used %">
<Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
<Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
<Utilization
colorScheme="gray"
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
isLoading={ isLoading }
/>
</Box>
</Tooltip>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</>
) }
</Flex>
</Td>
) }
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
<Skeleton isLoaded={ !isLoading } boxSize={ 5 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ burntFees.dividedBy(WEI).toFixed(8) }
</Skeleton>
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Tooltip label={ isLoading ? undefined : 'Burnt fees / Txn fees * 100%' }>
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Box>
</Tooltip>
</Td>
......
......@@ -6,9 +6,9 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { BLOCKS } from 'stubs/block';
import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
......@@ -32,6 +32,9 @@ const BlocksPageContent = () => {
const blocksQuery = useQueryWithPages({
resourceName: 'blocks',
filters: { type },
options: {
placeholderData: BLOCKS,
},
});
const tabs: Array<RoutedTab> = [
......@@ -41,7 +44,7 @@ const BlocksPageContent = () => {
];
return (
<Page>
<>
<PageTitle text="Blocks" withTextAd/>
<RoutedTabs
tabs={ tabs }
......@@ -49,7 +52,7 @@ const BlocksPageContent = () => {
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> }
stickyEnabled={ !isMobile }
/>
</Page>
</>
);
};
......
import { Text, Tooltip } from '@chakra-ui/react';
import { Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
type Props = {
value: number;
isLoading?: boolean;
}
const GasUsedToTargetRatio = ({ value }: Props) => {
const GasUsedToTargetRatio = ({ value, isLoading }: Props) => {
return (
<Tooltip label="% of Gas Target">
<Text variant="secondary">
{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%
</Text>
<Skeleton color="text_secondary" isLoaded={ !isLoading }>
<span>{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%</span>
</Skeleton>
</Tooltip>
);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment