Commit c2226172 authored by tom's avatar tom

skeletons for L2 txn batches

parent 951ac583
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import L2TxnBatches from 'ui/pages/L2TxnBatches'; import Page from 'ui/shared/Page/Page';
const L2TxnBatches = dynamic(() => import('ui/pages/L2TxnBatches'), { ssr: false });
const TxnBatchesPage: NextPage = () => { const TxnBatchesPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const TxnBatchesPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const TxnBatchesPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<L2TxnBatches/> <Page>
<L2TxnBatches/>
</Page>
</> </>
); );
}; };
......
import type { L2DepositsItem } from 'types/api/l2Deposits'; import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals'; import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
...@@ -23,3 +24,13 @@ export const L2_WITHDRAWAL_ITEM: L2WithdrawalsItem = { ...@@ -23,3 +24,13 @@ export const L2_WITHDRAWAL_ITEM: L2WithdrawalsItem = {
msg_nonce_version: 1, msg_nonce_version: 1,
status: 'Ready to prove', status: 'Ready to prove',
}; };
export const L2_TXN_BATCHES_ITEM: L2TxnBatchesItem = {
epoch_number: 9103513,
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
TX_HASH,
],
l2_block_number: 5218590,
tx_count: 9,
};
...@@ -8,9 +8,8 @@ export type L2TxnBatchesItem = { ...@@ -8,9 +8,8 @@ export type L2TxnBatchesItem = {
export type L2TxnBatchesResponse = { export type L2TxnBatchesResponse = {
items: Array<L2TxnBatchesItem>; items: Array<L2TxnBatchesItem>;
total: number;
next_page_params: { next_page_params: {
index: number; block_number: number;
items_count: number; items_count: number;
}; };
} }
import { Box, Icon, VStack } from '@chakra-ui/react'; import { Skeleton, VStack } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,70 +8,87 @@ import appConfig from 'configs/app/config'; ...@@ -8,70 +8,87 @@ import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg'; import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: L2TxnBatchesItem }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesListItem = ({ item }: Props) => { const TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
display="flex" display="flex"
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>
{ item.l2_block_number }
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L2 block txn count</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }> <LinkInternal
{ item.tx_count } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Epoch number</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Epoch number</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkExternal <LinkExternal
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
isLoading={ isLoading }
> >
{ item.epoch_number } <Skeleton isLoaded={ !isLoading }>
{ item.epoch_number }
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<VStack spacing={ 3 } w="100%" overflow="hidden"> <VStack spacing={ 3 } w="100%" overflow="hidden" alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => ( { item.l1_tx_hashes.map(hash => (
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
key={ hash } key={ hash }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
)) } )) }
</VStack> </VStack>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container> </ListItemMobileGrid.Container>
); );
......
...@@ -10,9 +10,10 @@ import TxnBatchesTableItem from './TxnBatchesTableItem'; ...@@ -10,9 +10,10 @@ import TxnBatchesTableItem from './TxnBatchesTableItem';
type Props = { type Props = {
items: Array<L2TxnBatchesItem>; items: Array<L2TxnBatchesItem>;
top: number; top: number;
isLoading?: boolean;
} }
const TxnBatchesTable = ({ items, top }: Props) => { const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="850px"> <Table variant="simple" size="sm" minW="850px">
<Thead top={ top }> <Thead top={ top }>
...@@ -25,8 +26,12 @@ const TxnBatchesTable = ({ items, top }: Props) => { ...@@ -25,8 +26,12 @@ const TxnBatchesTable = ({ items, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item) => ( { items.map((item, index) => (
<TxnBatchesTableItem key={ item.l2_block_number } item={ item }/> <TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Td, Tr, Text, Icon, VStack } from '@chakra-ui/react'; import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,13 +8,14 @@ import appConfig from 'configs/app/config'; ...@@ -8,13 +8,14 @@ import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg'; import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: L2TxnBatchesItem }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item }: Props) => { const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
...@@ -26,46 +27,59 @@ const TxnBatchesTableItem = ({ item }: Props) => { ...@@ -26,46 +27,59 @@ const TxnBatchesTableItem = ({ item }: Props) => {
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>
{ item.l2_block_number }
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td> <Td>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
lineHeight="24px" isLoading={ isLoading }
> >
{ item.tx_count } <Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td> <Td>
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
lineHeight="24px"
display="inline-flex" display="inline-flex"
isLoading={ isLoading }
py="2px"
> >
{ item.epoch_number } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.epoch_number }
</Skeleton>
</LinkExternal> </LinkExternal>
</Td> </Td>
<Td pr={ 12 }> <Td pr={ 12 }>
<VStack spacing={ 3 }> <VStack spacing={ 3 } alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => ( { item.l1_tx_hashes.map(hash => (
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
key={ hash } key={ hash }
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
)) } )) }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<Text variant="secondary" lineHeight="24px">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Box, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import { L2_TXN_BATCHES_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem'; import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem';
import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable'; import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const L2TxnBatches = () => { const L2TxnBatches = () => {
const isMobile = useIsMobile(); const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'l2_txn_batches', resourceName: 'l2_txn_batches',
options: {
placeholderData: generateListStub<'l2_txn_batches'>(
L2_TXN_BATCHES_ITEM,
50,
{
next_page_params: {
items_count: 50,
block_number: 9045200,
},
},
),
},
}); });
const countersQuery = useApiQuery('l2_txn_batches_count'); const countersQuery = useApiQuery('l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
});
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <TxnBatchesListItem key={ item.l2_block_number } item={ item }/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/></Hide>
</> </>
) : null; ) : null;
const text = (() => { const text = (() => {
if (countersQuery.isLoading || isLoading) { if (countersQuery.isError || isError || !data?.items.length) {
return (
<Skeleton
w={{ base: '100%', lg: '400px' }}
h={{ base: '48px', lg: '24px' }}
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError || isError || data.items.length === 0) {
return null; return null;
} }
return ( return (
<Flex mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block) Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
(total of { countersQuery.data.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
</Flex> </Skeleton>
); );
})(); })();
const actionBar = ( const actionBar = (
<> <>
{ (isMobile || !isPaginationVisible) && text } <Box mb={ 6 } display={{ base: 'block', lg: 'none' }}>
{ isPaginationVisible && ( { text }
<ActionBar mt={ -6 }> </Box>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <ActionBar mt={ -6 } alignItems="center">
{ !isMobile && text } <Box display={{ base: 'none', lg: 'block' }}>
<Pagination ml="auto" { ...pagination }/> { text }
</Flex> </Box>
</ActionBar> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
) } </ActionBar>
</> </>
); );
return ( return (
<Page> <>
<PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/> <PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '170px', '170px', '160px', '100%', '150px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '170px', '170px', '160px', '100%', '150px' ] }}
emptyText="There are no tx batches." emptyText="There are no tx batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</Page> </>
); );
}; };
......
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