Commit 7bc58a3f authored by tom's avatar tom

pagination for blocks page (w/o sticky)

parent 2ad39b1d
import { Text, Show, Hide, Skeleton, Alert } from '@chakra-ui/react'; import { Text, Show, Hide, Alert } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -6,7 +7,7 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -6,7 +7,7 @@ import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
...@@ -15,25 +16,25 @@ import BlocksTable from 'ui/blocks/BlocksTable'; ...@@ -15,25 +16,25 @@ import BlocksTable from 'ui/blocks/BlocksTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps;
};
interface Props { interface Props {
type?: BlockType; type?: BlockType;
query: QueryResult;
} }
const BlocksContent = ({ type }: Props) => { const BlocksContent = ({ type, query }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName: QueryKeys.blocks,
filters: { type },
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => { queryClient.setQueryData([ QueryKeys.blocks, { page: query.pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
const shouldAddToList = !type || type === payload.block.type; const shouldAddToList = !type || type === payload.block.type;
if (!prevData) { if (!prevData) {
...@@ -44,7 +45,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -44,7 +45,7 @@ const BlocksContent = ({ type }: Props) => {
} }
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData; return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
}); });
}, [ pagination.page, queryClient, type ]); }, [ query.pagination.page, queryClient, type ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.'); setSocketAlert('Connection is lost. Please click here to load new blocks.');
...@@ -58,7 +59,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -58,7 +59,7 @@ const BlocksContent = ({ type }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1, isDisabled: query.isLoading || query.isError || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -67,7 +68,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -67,7 +68,7 @@ const BlocksContent = ({ type }: Props) => {
}); });
const content = (() => { const content = (() => {
if (isLoading) { if (query.isLoading) {
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile"> <Show below="lg" key="skeleton-mobile">
...@@ -80,37 +81,31 @@ const BlocksContent = ({ type }: Props) => { ...@@ -80,37 +81,31 @@ const BlocksContent = ({ type }: Props) => {
); );
} }
if (isError) { if (query.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (data.items.length === 0) { if (query.data.items.length === 0) {
return <Text as="span">There are no blocks.</Text>; return <Text as="span">There are no blocks.</Text>;
} }
return ( return (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ query.data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items } top={ isPaginatorHidden ? 0 : 80 } page={ pagination.page }/></Hide> <Hide below="lg" key="content-desktop"><BlocksTable data={ query.data.items } top={ 0 } page={ 1 }/></Hide>
</> </>
); );
})(); })();
const totalText = data?.items.length ? const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
null;
return ( return (
<> <>
{ !isLoading ? { isMobile && !isPaginatorHidden && (
totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
}
{ !isPaginatorHidden && (
<ActionBar> <ActionBar>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) } ) }
{ content } { content }
...@@ -118,4 +113,4 @@ const BlocksContent = ({ type }: Props) => { ...@@ -118,4 +113,4 @@ const BlocksContent = ({ type }: Props) => {
); );
}; };
export default BlocksContent; export default React.memo(BlocksContent);
import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
interface Props {
pagination: PaginationProps;
}
const BlocksTabSlot = ({ pagination }: Props) => {
const isMobile = useIsMobile();
const fetch = useFetch();
const statsQuery = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
if (isMobile) {
return null;
}
return (
<Flex alignItems="center" columnGap={ 8 }>
{ statsQuery.isLoading && <Skeleton w="175px" h="24px"/> }
{ statsQuery.data?.network_utilization_percentage !== undefined && (
<Box>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 600 }>
{ statsQuery.data.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<Pagination my={ 1 } { ...pagination }/>
</Flex>
);
};
export default BlocksTabSlot;
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { BlockType } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import BlocksContent from 'ui/blocks/BlocksContent'; import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [ const TAB_TO_TYPE: Record<string, BlockType> = {
{ id: 'blocks', title: 'All', component: <BlocksContent/> }, blocks: 'block',
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg"/> }, reorgs: 'reorg',
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle"/> }, uncles: 'uncle',
]; };
const BlocksPageContent = () => { const BlocksPageContent = () => {
const router = useRouter();
const type = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_TYPE[router.query.tab] : undefined;
const blocksQuery = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName: QueryKeys.blocks,
filters: { type },
});
const tabs: Array<RoutedTab> = [
{ id: 'blocks', title: 'All', component: <BlocksContent query={ blocksQuery }/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg" query={ blocksQuery }/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle" query={ blocksQuery }/> },
];
return ( return (
<Page> <Page>
<PageTitle text="Blocks"/> <PageTitle text="Blocks"/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs
tabs={ tabs }
tabListMarginBottom={{ base: 6, lg: 8 }}
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination }/> }
/>
</Page> </Page>
); );
}; };
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
TabList, TabList,
TabPanel, TabPanel,
TabPanels, TabPanels,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -27,11 +28,13 @@ const hiddenItemStyles: StyleProps = { ...@@ -27,11 +28,13 @@ const hiddenItemStyles: StyleProps = {
interface Props { interface Props {
tabs: Array<RoutedTab>; tabs: Array<RoutedTab>;
tabListMarginBottom?: ChakraProps['marginBottom']; tabListMarginBottom?: ChakraProps['marginBottom'];
rightSlot?: React.ReactNode;
} }
const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { const RoutedTabs = ({ tabs, tabListMarginBottom, rightSlot }: Props) => {
const router = useRouter(); const router = useRouter();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
let tabIndex = 0; let tabIndex = 0;
...@@ -46,7 +49,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -46,7 +49,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
}, [ tabs, router ]); }, [ tabs, router ]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile); const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
...@@ -59,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -59,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
}, [ tabs, router ]); }, [ tabs, router ]);
return ( return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }> <Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex } position="relative">
<TabList <TabList
marginBottom={ tabListMarginBottom } marginBottom={ tabListMarginBottom }
flexWrap="nowrap" flexWrap="nowrap"
...@@ -112,6 +115,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -112,6 +115,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
); );
}) } }) }
</TabList> </TabList>
{ rightSlot ? <Box position="absolute" top={ 0 } right={ 0 } ref={ rightSlotRef }>{ rightSlot }</Box> : null }
<TabPanels> <TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) } { tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
</TabPanels> </TabPanels>
......
...@@ -11,9 +11,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -11,9 +11,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0); const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0);
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]); const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null); const listRef = React.useRef<HTMLDivElement>(null);
const rightSlotRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => { const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width; const listWidth = listRef.current?.getBoundingClientRect().width;
const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width); const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths[tabWidths.length - 1]; const menuWidth = tabWidths[tabWidths.length - 1];
...@@ -26,11 +28,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -26,11 +28,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
return result; return result;
} }
if (result.accWidth + item <= listWidth - menuWidth) { if (result.accWidth + item <= listWidth - rightSlotWidth - menuWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) { if (result.accWidth + item <= listWidth - rightSlotWidth && index === tabWidths.length - 2) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
...@@ -82,6 +84,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -82,6 +84,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
tabsList, tabsList,
tabsRefs, tabsRefs,
listRef, listRef,
rightSlotRef,
}; };
}, [ tabsList, tabsCut, tabsRefs, listRef ]); }, [ tabsList, tabsCut, tabsRefs, listRef ]);
} }
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