Commit b740a53f authored by tom's avatar tom

refactor block list

parent a75ee466
......@@ -34,6 +34,7 @@ const RESTRICTED_MODULES = {
'DrawerRoot', 'DrawerBody', 'DrawerContent', 'DrawerOverlay', 'DrawerBackdrop', 'DrawerTrigger', 'Drawer',
'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription',
'Heading', 'Badge', 'Tabs',
'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell',
],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
},
......
......@@ -2,7 +2,7 @@ import React from 'react';
type Id = string | number;
interface Params<T> {
export interface Params<T> {
data: Array<T>;
idFn: (item: T) => Id;
enabled: boolean;
......@@ -22,10 +22,15 @@ export default function useInitialList<T>({ data, idFn, enabled }: Params<T>) {
return !list.includes(idFn(data));
}, [ list, idFn ]);
const getAnimationProp = React.useCallback((data: T) => {
return isNew(data) ? 'fade-in 500ms linear' : undefined;
}, [ isNew ]);
return React.useMemo(() => {
return {
list,
isNew,
getAnimationProp,
};
}, [ list, isNew ]);
}, [ list, isNew, getAnimationProp ]);
}
import { Table as ChakraTable } from '@chakra-ui/react';
import { throttle } from 'es-toolkit';
import * as React from 'react';
export const TableRoot = ChakraTable.Root;
export const TableBody = ChakraTable.Body;
export const TableHeader = ChakraTable.Header;
export const TableRow = ChakraTable.Row;
export interface TableCellProps extends ChakraTable.CellProps {
isNumeric?: boolean;
}
export const TableCell = (props: TableCellProps) => {
const { isNumeric, ...rest } = props;
return <ChakraTable.Cell textAlign={ isNumeric ? 'right' : undefined } { ...rest }/>;
};
export interface TableColumnHeaderProps extends ChakraTable.ColumnHeaderProps {
isNumeric?: boolean;
}
export const TableColumnHeader = (props: TableColumnHeaderProps) => {
const { isNumeric, ...rest } = props;
return <ChakraTable.ColumnHeader textAlign={ isNumeric ? 'right' : undefined } { ...rest }/>;
};
export interface TableHeaderProps extends ChakraTable.HeaderProps {
top?: number;
}
export const TableHeaderSticky = (props: TableHeaderProps) => {
const { top, children, ...rest } = props;
const ref = React.useRef<HTMLTableSectionElement>(null);
const [ isStuck, setIsStuck ] = React.useState(false);
const handleScroll = React.useCallback(() => {
if (Number(ref.current?.getBoundingClientRect().y) <= (top || 0)) {
setIsStuck(true);
} else {
setIsStuck(false);
}
}, [ top ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
}, [ handleScroll ]);
return (
<TableHeader
ref={ ref }
position="sticky"
top={ top ? `${ top }px` : 0 }
backgroundColor={{ _light: 'white', _dark: 'black' }}
boxShadow={ isStuck ? 'action_bar' : 'none' }
zIndex="1"
{ ...rest }
>
{ children }
</TableHeader>
);
};
......@@ -286,6 +286,12 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
fg: { value: { _light: '{colors.cyan.500}', _dark: '{colors.cyan.100}' } },
},
},
table: {
header: {
bg: { value: { _light: '{colors.blackAlpha.100}', _dark: '{colors.whiteAlpha.200}' } },
fg: { value: { _light: '{colors.blackAlpha.700}', _dark: '{colors.whiteAlpha.700}' } },
},
},
heading: {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
},
......
......@@ -10,7 +10,7 @@ const globalCss: Record<string, SystemStyleObject> = {
bg: 'global.body.bg',
color: 'global.body.fg',
'-webkit-tap-highlight-color': 'transparent',
'font-variant-ligatures': 'no-contextual',
fontVariantLigatures: 'no-contextual',
},
mark: {
bg: 'global.mark.bg',
......
......@@ -15,6 +15,7 @@ import { recipe as select } from './select.recipe';
import { recipe as skeleton } from './skeleton.recipe';
import { recipe as spinner } from './spinner.recipe';
import { recipe as switchRecipe } from './switch.recipe';
import { recipe as table } from './table.recipe';
import { recipe as tabs } from './tabs.recipe';
import { recipe as textarea } from './textarea.recipe';
import { recipe as toast } from './toast.recipe';
......@@ -42,6 +43,7 @@ export const slotRecipes = {
progressCircle,
select,
'switch': switchRecipe,
table,
tabs,
toast,
tooltip,
......
import { defineSlotRecipe } from '@chakra-ui/react';
export const recipe = defineSlotRecipe({
slots: [ 'root', 'row', 'cell', 'columnHeader', 'caption', 'footer', 'body', 'header' ],
base: {
root: {
tableLayout: 'fixed',
fontVariant: 'normal',
fontVariantLigatures: 'no-contextual',
borderCollapse: 'collapse',
width: 'full',
textAlign: 'start',
verticalAlign: 'top',
overflow: 'unset',
},
cell: {
textAlign: 'start',
alignItems: 'center',
verticalAlign: 'top',
},
columnHeader: {
fontWeight: '500',
textAlign: 'start',
},
},
variants: {
variant: {
line: {
columnHeader: {
color: 'table.header.fg',
backgroundColor: 'table.header.bg',
_first: {
borderTopLeftRadius: '8px',
},
_last: {
borderTopRightRadius: '8px',
},
},
cell: {
borderBottomWidth: '1px',
},
row: {
bg: 'bg',
},
},
},
size: {
md: {
root: {
fontSize: 'sm',
},
columnHeader: {
px: '6px',
py: '10px',
_first: {
pl: 3,
},
_last: {
pr: 3,
},
},
cell: {
px: '6px',
py: 4,
_first: {
pl: 3,
},
_last: {
pr: 3,
},
},
},
},
},
defaultVariants: {
variant: 'line',
size: 'md',
},
});
......@@ -126,11 +126,12 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
return (
<DataListDisplay
isError={ query.isError }
items={ query.data?.items }
itemsNum={ query.data?.items?.length }
emptyText="There are no blocks."
content={ content }
actionBar={ actionBar }
/>
>
{ content }
</DataListDisplay>
);
};
......
import { Box } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { Block } from 'types/api/block';
import useInitialList from 'lib/hooks/useInitialList';
import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props {
......@@ -13,18 +13,23 @@ interface Props {
}
const BlocksList = ({ data, isLoading, page }: Props) => {
const initialList = useInitialList({
data: data ?? [],
idFn: (item) => item.height,
enabled: !isLoading,
});
return (
<Box>
<AnimatePresence initial={ false }>
{ data.map((item, index) => (
<BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
/>
)) }
</AnimatePresence>
{ data.map((item, index) => (
<BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
animation={ initialList.getAnimationProp(item) }
/>
)) }
</Box>
);
};
......
import { Flex, Text, Box, Tooltip } from '@chakra-ui/react';
import { Flex, Text, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { capitalize } from 'es-toolkit';
import React from 'react';
......@@ -12,8 +12,9 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { currencyUnits } from 'lib/units';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import IconSvg from 'ui/shared/IconSvg';
......@@ -28,18 +29,19 @@ interface Props {
data: Block;
isLoading?: boolean;
enableTimeIncrement?: boolean;
animation?: string;
}
const isRollup = config.features.rollup.isEnabled;
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const BlocksListItem = ({ data, isLoading, enableTimeIncrement, animation }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.transaction_fees || 0);
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return (
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<ListItemMobile rowGap={ 3 } key={ String(data.height) } animation={ animation }>
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
<BlockEntity
......@@ -50,7 +52,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
fontWeight={ 600 }
/>
{ data.celo?.is_epoch_block && (
<Tooltip label={ `Finalized epoch #${ data.celo.epoch_number }` }>
<Tooltip content={ `Finalized epoch #${ data.celo.epoch_number }` } disabled={ isLoading }>
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
</Tooltip>
) }
......@@ -66,7 +68,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<Skeleton loading={ isLoading } display="inline-block" color="text_secondary">
<span>{ data.size.toLocaleString() } bytes</span>
</Skeleton>
</Flex>
......@@ -83,33 +85,33 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
{ data.transaction_count > 0 ? (
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Skeleton loading={ isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(data.height), tab: 'txs' } }) }>
{ data.transaction_count }
</LinkInternal>
</Skeleton>
) :
<Text variant="secondary">{ data.transaction_count }</Text>
<Text color="text.secondary">{ data.transaction_count }</Text>
}
</Flex>
<Box>
<Text fontWeight={ 500 }>Gas used</Text>
<Flex mt={ 2 }>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<Skeleton loading={ isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<span>{ BigNumber(data.gas_used || 0).toFormat() }</span>
</Skeleton>
<BlockGasUsed
gasUsed={ data.gas_used }
gasUsed={ data.gas_used || undefined }
gasLimit={ data.gas_limit }
isLoading={ isLoading }
gasTarget={ data.gas_target_percentage }
gasTarget={ data.gas_target_percentage || undefined }
/>
</Flex>
</Box>
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { currencyUnits.ether }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<Skeleton loading={ isLoading } display="inline-block" color="text_secondary">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Flex>
......@@ -120,7 +122,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<IconSvg name="flame" boxSize={ 5 } color="gray.500" isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" ml={ 2 }>
<Skeleton loading={ isLoading } display="inline-block" color="text_secondary" ml={ 2 }>
<span>{ burntFees.div(WEI).toFixed() }</span>
</Skeleton>
</Flex>
......@@ -131,7 +133,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee && baseFeeValue && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Base fee</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<Skeleton loading={ isLoading } display="inline-block" color="text_secondary">
<span>{ baseFeeValue }</span>
</Skeleton>
</Flex>
......
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import { capitalize } from 'es-toolkit';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { Block } from 'types/api/block';
import config from 'configs/app';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import useInitialList from 'lib/hooks/useInitialList';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { currencyUnits } from 'lib/units';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import BlocksTableItem from 'ui/blocks/BlocksTableItem';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
......@@ -31,6 +30,11 @@ const FEES_COL_WEIGHT = 22;
const isRollup = config.features.rollup.isEnabled;
const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => {
const initialList = useInitialList({
data: data ?? [],
idFn: (item) => item.height,
enabled: !isLoading,
});
const widthBase =
(!config.UI.views.block.hiddenFields?.miner ? VALIDATOR_COL_WEIGHT : 0) +
......@@ -40,24 +44,27 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
return (
<AddressHighlightProvider>
<Table minWidth="1040px" fontWeight={ 500 }>
<Thead top={ top }>
<Tr>
<Th width="150px">Block</Th>
<Th width="120px">Size, bytes</Th>
{ !config.UI.views.block.hiddenFields?.miner &&
<Th width={ `${ VALIDATOR_COL_WEIGHT / widthBase * 100 }%` } minW="160px">{ capitalize(getNetworkValidatorTitle()) }</Th> }
<Th width="64px" isNumeric>Txn</Th>
<Th width={ `${ GAS_COL_WEIGHT / widthBase * 100 }%` }>Gas used</Th>
<TableRoot minWidth="1040px" fontWeight={ 500 }>
<TableHeaderSticky top={ top }>
<TableRow>
<TableColumnHeader width="150px">Block</TableColumnHeader>
<TableColumnHeader width="120px">Size, bytes</TableColumnHeader>
{ !config.UI.views.block.hiddenFields?.miner && (
<TableColumnHeader width={ `${ VALIDATOR_COL_WEIGHT / widthBase * 100 }%` } minW="160px">
{ capitalize(getNetworkValidatorTitle()) }
</TableColumnHeader>
) }
<TableColumnHeader width="64px" isNumeric>Txn</TableColumnHeader>
<TableColumnHeader width={ `${ GAS_COL_WEIGHT / widthBase * 100 }%` }>Gas used</TableColumnHeader>
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward &&
<Th width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</Th> }
<TableColumnHeader width={ `${ REWARD_COL_WEIGHT / widthBase * 100 }%` }>Reward { currencyUnits.ether }</TableColumnHeader> }
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees &&
<Th width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { currencyUnits.ether }</Th> }
<TableColumnHeader width={ `${ FEES_COL_WEIGHT / widthBase * 100 }%` }>Burnt fees { currencyUnits.ether }</TableColumnHeader> }
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee &&
<Th width="150px" isNumeric>Base fee</Th> }
</Tr>
</Thead>
<Tbody>
<TableColumnHeader width="150px" isNumeric>Base fee</TableColumnHeader> }
</TableRow>
</TableHeaderSticky>
<TableBody>
{ showSocketInfo && (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
......@@ -67,18 +74,17 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
isLoading={ isLoading }
/>
) }
<AnimatePresence initial={ false }>
{ data.map((item, index) => (
<BlocksTableItem
key={ item.height + (isLoading ? `${ index }_${ page }` : '') }
data={ item }
enableTimeIncrement={ page === 1 && !isLoading }
isLoading={ isLoading }
/>
)) }
</AnimatePresence>
</Tbody>
</Table>
{ data.map((item, index) => (
<BlocksTableItem
key={ item.height + (isLoading ? `${ index }_${ page }` : '') }
data={ item }
enableTimeIncrement={ page === 1 && !isLoading }
isLoading={ isLoading }
animation={ initialList.getAnimationProp(item) }
/>
)) }
</TableBody>
</TableRoot>
</AddressHighlightProvider>
);
};
......
import { Tr, Td, Flex, Box, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { Flex, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion';
import React from 'react';
import type { Block } from 'types/api/block';
......@@ -10,8 +9,10 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import { Tooltip } from 'toolkit/chakra/tooltip';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import IconSvg from 'ui/shared/IconSvg';
......@@ -24,44 +25,34 @@ import { getBaseFeeValue } from './utils';
interface Props {
data: Block;
isLoading?: boolean;
animation?: string;
enableTimeIncrement?: boolean;
}
const isRollup = config.features.rollup.isEnabled;
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement, animation }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.transaction_fees || 0);
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
const baseFeeValue = getBaseFeeValue(data.base_fee_per_gas);
return (
<Tr
as={ motion.tr }
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
key={ data.height }
>
<Td fontSize="sm">
<TableRow animation={ animation }>
<TableCell >
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ data.celo?.is_epoch_block && (
<Tooltip label={ `Finalized epoch #${ data.celo.epoch_number }` }>
<Tooltip content={ `Finalized epoch #${ data.celo.epoch_number }` }>
<IconSvg name="checkered_flag" boxSize={ 5 } p="1px" isLoading={ isLoading } flexShrink={ 0 }/>
</Tooltip>
) }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
{ /* TODO @tom2drum fix tooltip */ }
<Tooltip disabled={ data.type !== 'reorg' } content="Chain reorganizations">
<BlockEntity
isLoading={ isLoading }
number={ data.height }
hash={ data.type !== 'block' ? data.hash : undefined }
noIcon
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</Tooltip>
......@@ -74,25 +65,25 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
fontWeight={ 400 }
display="inline-block"
/>
</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
</TableCell>
<TableCell >
<Skeleton loading={ isLoading } display="inline-block">
{ data.size.toLocaleString() }
</Skeleton>
</Td>
</TableCell>
{ !config.UI.views.block.hiddenFields?.miner && (
<Td fontSize="sm">
<TableCell >
<AddressEntity
address={ data.miner }
isLoading={ isLoading }
truncation="constant"
maxW="min-content"
/>
</Td>
</TableCell>
) }
<Td isNumeric fontSize="sm">
<TableCell isNumeric >
{ data.transaction_count > 0 ? (
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Skeleton loading={ isLoading } display="inline-block">
<LinkInternal href={ route({
pathname: '/block/[height_or_hash]',
query: { height_or_hash: String(data.height), tab: 'txs' },
......@@ -101,48 +92,48 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</LinkInternal>
</Skeleton>
) : data.transaction_count }
</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
</TableCell>
<TableCell >
<Skeleton loading={ isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }>
<BlockGasUsed
gasUsed={ data.gas_used }
gasUsed={ data.gas_used || undefined }
gasLimit={ data.gas_limit }
isLoading={ isLoading }
gasTarget={ data.gas_target_percentage }
gasTarget={ data.gas_target_percentage || undefined }
/>
</Flex>
</Td>
</TableCell>
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
<TableCell >
<Skeleton loading={ isLoading } display="inline-block">
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
</TableCell>
) }
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Td fontSize="sm">
<TableCell >
<Flex alignItems="center" columnGap={ 2 }>
<IconSvg name="flame" boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<IconSvg name="flame" boxSize={ 5 } color={{ _light: 'gray.500', _dark: 'inherit' }} isLoading={ isLoading }/>
<Skeleton loading={ isLoading } display="inline-block">
{ burntFees.dividedBy(WEI).toFixed(8) }
</Skeleton>
</Flex>
<Tooltip label={ isLoading ? undefined : 'Burnt fees / Txn fees * 100%' }>
<Tooltip content="Burnt fees / Txn fees * 100%" disabled={ isLoading }>
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Box>
</Tooltip>
</Td>
</TableCell>
) }
{ !isRollup && !config.UI.views.block.hiddenFields?.base_fee && Boolean(baseFeeValue) && (
<Td fontSize="sm" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre-wrap" wordBreak="break-word">
<TableCell isNumeric>
<Skeleton loading={ isLoading } display="inline-block" whiteSpace="pre-wrap" wordBreak="break-word">
{ baseFeeValue }
</Skeleton>
</Td>
</TableCell>
) }
</Tr>
</TableRow>
);
};
......
......@@ -90,7 +90,7 @@ const LatestBlocks = () => {
key={ block.height + (isPlaceholderData ? String(index) : '') }
block={ block }
isLoading={ isPlaceholderData }
isNew={ initialList.isNew(block) }
animation={ initialList.getAnimationProp(block) }
/>
))) }
</VStack>
......
......@@ -16,14 +16,14 @@ import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = {
block: Block;
isLoading?: boolean;
isNew?: boolean;
animation?: string;
};
const LatestBlocksItem = ({ block, isLoading, isNew }: Props) => {
const LatestBlocksItem = ({ block, isLoading, animation }: Props) => {
const totalReward = getBlockTotalReward(block);
return (
<Box
animation={ isNew ? 'fade-in 500ms linear' : undefined }
animation={ animation }
borderRadius="md"
border="1px solid"
borderColor="border.divider"
......
......@@ -77,7 +77,7 @@ const LatestArbitrumL2Batches = () => {
timestamp={ batch.commitment_transaction.timestamp }
txCount={ batch.transactions_count }
isLoading={ isPlaceholderData }
isNew={ initialList.isNew(batch) }
animation={ initialList.getAnimationProp(batch) }
/>
))) }
</VStack>
......
......@@ -14,13 +14,13 @@ type Props = {
txCount: number;
status?: React.ReactNode;
isLoading: boolean;
isNew?: boolean;
animation?: string;
};
const LatestBatchItem = ({ number, timestamp, txCount, status, isLoading, isNew }: Props) => {
const LatestBatchItem = ({ number, timestamp, txCount, status, isLoading, animation }: Props) => {
return (
<Box
animation={ isNew ? 'fade-in 500ms linear' : undefined }
animation={ animation }
borderRadius="md"
border="1px solid"
borderColor="border.divider"
......
......@@ -80,7 +80,7 @@ const LatestZkEvmL2Batches = () => {
timestamp={ batch.timestamp }
status={ status }
isLoading={ isPlaceholderData }
isNew={ initialList.isNew(batch) }
animation={ initialList.getAnimationProp(batch) }
/>
);
})) }
......
......@@ -70,15 +70,10 @@ const BlocksPageContent = () => {
})();
const tabs: Array<RoutedTab> = [
{ id: 'blocks', title: 'All', component: <div>All</div> },
{ id: 'reorgs', title: 'Forked', component: <div>Forked</div> },
{ id: 'uncles', title: 'Uncles', component: <div>Uncles</div> },
{ id: 'blocks', title: 'All', component: <BlocksContent type="block" query={ blocksQuery }/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg" query={ reorgsQuery }/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle" query={ unclesQuery }/> },
];
// const tabs: Array<RoutedTab> = [
// { id: 'blocks', title: 'All', component: <BlocksContent type="block" query={ blocksQuery }/> },
// { id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg" query={ reorgsQuery }/> },
// { id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle" query={ unclesQuery }/> },
// ];
return (
<>
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import { Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
......@@ -19,7 +19,6 @@ const ActionBar = ({ children, className, showShadow }: Props) => {
const ref = React.useRef<HTMLDivElement>(null);
const scrollDirection = useScrollDirection();
const isSticky = useIsSticky(ref, TOP_UP + 5);
const bgColor = useColorModeValue('white', 'black');
if (!React.Children.toArray(children).filter(Boolean).length) {
return null;
......@@ -28,7 +27,7 @@ const ActionBar = ({ children, className, showShadow }: Props) => {
return (
<Flex
className={ className }
backgroundColor={ bgColor }
backgroundColor={{ _light: 'white', _dark: 'black' }}
pt={ 6 }
pb={{ base: 6, lg: 3 }}
mx={{ base: -3, lg: 0 }}
......
......@@ -12,11 +12,11 @@ type FilterProps = {
type Props = {
isError: boolean;
items?: Array<unknown>;
itemsNum?: number;
emptyText: React.ReactNode;
actionBar?: React.ReactNode;
showActionBarIfEmpty?: boolean;
content: React.ReactNode;
children: React.ReactNode;
className?: string;
filterProps?: FilterProps;
};
......@@ -26,7 +26,7 @@ const DataListDisplay = (props: Props) => {
return <DataFetchAlert className={ props.className }/>;
}
if (props.filterProps?.hasActiveFilters && !props.items?.length) {
if (props.filterProps?.hasActiveFilters && !props.itemsNum) {
return (
<Box className={ props.className }>
{ props.actionBar }
......@@ -35,7 +35,7 @@ const DataListDisplay = (props: Props) => {
);
}
if (!props.items?.length) {
if (!props.itemsNum) {
return (
<>
{ props.showActionBarIfEmpty && props.actionBar }
......@@ -47,7 +47,7 @@ const DataListDisplay = (props: Props) => {
return (
<Box className={ props.className }>
{ props.actionBar }
{ props.content }
{ props.children }
</Box>
);
};
......
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
type Props = {
value: number;
......@@ -10,8 +10,8 @@ type Props = {
const GasUsedToTargetRatio = ({ value, isLoading }: Props) => {
return (
<Tooltip label="% of Gas Target">
<Skeleton color="text_secondary" isLoaded={ !isLoading }>
<Tooltip content="% of Gas Target">
<Skeleton color="text_secondary" loading={ isLoading }>
<span>{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%</span>
</Skeleton>
</Tooltip>
......
import { Flex, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
isAnimated?: boolean;
animation?: string;
}
const ListItemMobile = ({ children, className, isAnimated }: Props) => {
const ListItemMobile = ({ children, className, animation }: Props) => {
return (
<Flex
as={ motion.div }
initial={ isAnimated ? { opacity: 0, scale: 0.97 } : { opacity: 1, scale: 1 } }
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
animation={ animation }
rowGap={ 6 }
alignItems="flex-start"
flexDirection="column"
......
import { Thead, useColorModeValue } from '@chakra-ui/react';
import type { TableHeadProps, PositionProps } from '@chakra-ui/react';
import { throttle } from 'es-toolkit';
import React from 'react';
interface Props extends TableHeadProps {
top?: number;
children?: React.ReactNode;
}
const TheadSticky = ({ top, children, ...restProps }: Props) => {
const ref = React.useRef<HTMLTableSectionElement>(null);
const [ isSticky, setIsSticky ] = React.useState(false);
const handleScroll = React.useCallback(() => {
if (Number(ref.current?.getBoundingClientRect().y) <= (top || 0)) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ top ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
}, [ handleScroll ]);
const props = {
...restProps,
position: 'sticky' as PositionProps['position'],
top: top ? `${ top }px` : 0,
backgroundColor: useColorModeValue('white', 'black'),
boxShadow: isSticky ? 'md' : 'none',
zIndex: '1',
};
return (
<Thead { ...props } ref={ ref }>
{ children }
</Thead>
);
};
export default TheadSticky;
import { chakra, Tooltip, Box, useColorModeValue } from '@chakra-ui/react';
import { chakra, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import config from 'configs/app';
import { Tooltip } from 'toolkit/chakra/tooltip';
import GasUsedToTargetRatio from '../GasUsedToTargetRatio';
import TextSeparator from '../TextSeparator';
......@@ -23,15 +24,13 @@ const BlockGasUsed = ({ className, gasUsed, gasLimit, gasTarget, isLoading }: Pr
gasUsed && gasUsed !== '0' &&
(!rollupFeature.isEnabled || rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium');
const separatorColor = useColorModeValue('gray.200', 'gray.700');
if (!hasGasUtilization) {
return null;
}
return (
<>
<Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
<Tooltip content="Gas Used %" disabled={ isLoading }>
<Box>
<Utilization
colorScheme="gray"
......@@ -43,7 +42,7 @@ const BlockGasUsed = ({ className, gasUsed, gasLimit, gasTarget, isLoading }: Pr
</Tooltip>
{ gasTarget && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<TextSeparator color={{ _light: 'gray.200', _dark: 'gray.700' }} mx={ 1 }/>
<GasUsedToTargetRatio value={ gasTarget } isLoading={ isLoading }/>
</>
) }
......
......@@ -39,9 +39,10 @@ export interface ContainerBaseProps extends Pick<EntityBaseProps, 'className'> {
onMouseLeave?: (event: React.MouseEvent) => void;
}
const Container = chakra(({ className, children, ...props }: ContainerBaseProps) => {
const Container = chakra(React.forwardRef(({ className, children, ...props }: ContainerBaseProps, ref: React.Ref<HTMLDivElement>) => {
return (
<Flex
ref={ ref }
className={ className }
alignItems="center"
minWidth={ 0 } // for content truncation - https://css-tricks.com/flexbox-truncated-text/
......@@ -50,7 +51,7 @@ const Container = chakra(({ className, children, ...props }: ContainerBaseProps)
{ children }
</Flex>
);
});
}));
export interface LinkBaseProps extends Pick<EntityBaseProps, 'className' | 'onClick' | 'isLoading' | 'isExternal' | 'href' | 'noLink' | 'query'> {
children: React.ReactNode;
......
......@@ -51,20 +51,20 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
hash?: string;
}
const BlockEntity = (props: EntityProps) => {
const BlockEntity = (props: EntityProps, ref: React.Ref<HTMLDivElement>) => {
const partsProps = distributeEntityProps(props);
const content = <Content { ...partsProps.content }/>;
return (
<Container { ...partsProps.container }>
<Container { ...partsProps.container } ref={ ref }>
<Icon { ...partsProps.icon }/>
{ props.noLink ? content : <Link { ...partsProps.link }>{ content }</Link> }
</Container>
);
};
export default React.memo(chakra(BlockEntity));
export default React.memo(chakra(React.forwardRef(BlockEntity)));
export {
Container,
......
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