Commit 3d60e745 authored by tom's avatar tom

blocks and block txs

parent 66197cb7
......@@ -7,7 +7,7 @@ import type { ApiResource } from './resources';
export default function buildUrl(
resource: ApiResource,
pathParams?: Record<string, string>,
queryParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
// FIXME
// 1. I was not able to figure out how to send CORS with credentials from localhost
......@@ -28,7 +28,7 @@ export default function buildUrl(
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, value);
value && url.searchParams.append(key, String(value));
});
return url.toString();
......
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
export interface ApiResource {
......@@ -7,7 +9,7 @@ export interface ApiResource {
}
export const RESOURCES = {
// account
// ACCOUNT
user_info: {
path: '/api/account/v1/user/info',
},
......@@ -42,12 +44,34 @@ export const RESOURCES = {
basePath: appConfig.statsApi.basePath,
},
// BLOCKS, TXS
blocks: {
path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ],
},
block_txs: {
path: '/api/v2/blocks/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
// DEPRECATED
old_api: {
path: '/api',
},
};
export type ResourceName = keyof typeof RESOURCES;
export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] extends {filterFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['filterFields']> :
never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> {
......
......@@ -10,7 +10,7 @@ import type { ResourceError, ApiResource } from './resources';
export interface Params {
pathParams?: Record<string, string>;
queryParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>;
}
......
......@@ -2,24 +2,33 @@ import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account';
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { Stats, Charts } from 'types/api/stats';
import type { RESOURCES, ResourceError } from './resources';
import type { RESOURCES, ResourceError, ResourceName } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
interface Params<R extends keyof typeof RESOURCES> extends ApiFetchParams {
export interface Params<R extends ResourceName> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
}
export default function useApiQuery<R extends keyof typeof RESOURCES>(
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
if (pathParams || queryParams) {
return [ resource, { ...pathParams, ...queryParams } ];
}
return [ resource ];
}
export default function useApiQuery<R extends ResourceName>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R> = {},
) {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError, ResourcePayload<R>>(
pathParams || queryParams ? [ resource, { ...pathParams, ...queryParams } ] : [ resource ],
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
}, queryOptions);
......@@ -35,4 +44,6 @@ export type ResourcePayload<Q extends keyof typeof RESOURCES> =
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'stats_counters' ? Stats :
Q extends 'stats_charts' ? Charts :
Q extends 'blocks' ? BlocksResponse :
Q extends 'block_txs' ? BlockTransactionsResponse :
never;
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import type { PaginatedResponseX, PaginatedResources, PaginationFiltersX } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch';
import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery';
interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
interface Params<Resource extends PaginatedResources> {
resourceName: Resource;
options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: Record<string, string>;
filters?: PaginationFiltersX<Resource>;
scroll?: { elem: string; offset: number };
}
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({
queryName,
export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName,
filters,
options,
apiPath,
queryIds,
pathParams,
scroll,
}: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName];
}: Params<Resource>) {
const resource = RESOURCES[resourceName];
const queryClient = useQueryClient();
const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponseX<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch();
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams,
});
const [ hasPagination, setHasPagination ] = React.useState(page > 1);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...filters, ...pageParams[page] };
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
queryKey,
async() => {
const params: Array<string> = [];
Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => {
if (Array.isArray(val)) {
val.length && params.push(`${ key }=${ val.join(',') }`);
} else if (val) {
params.push(`${ key }=${ val }`);
}
});
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
},
{ staleTime: page === 1 ? 0 : Infinity, ...options },
);
});
const { data } = queryResult;
const onNextPageClick = useCallback(() => {
......@@ -69,9 +66,13 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
return;
}
const nextPageParams = data.next_page_params;
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
setPageParams((prev) => ({
...prev,
[page + 1]: mapValues(nextPageParams, (value) => value?.toString()) as NextPageParams,
}));
setPage(prev => prev + 1);
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1);
......@@ -80,53 +81,52 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev + 1);
});
}, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]);
}, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) {
nextPageQuery = omit(router.query, paginationFields, 'page');
nextPageQuery = omit(router.query, resource.paginationFields, 'page');
canGoBackwards.current = true;
} else {
const nextPageParams = { ...pageParams[page - 2] };
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
const nextPageParams = pageParams[page - 1];
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page - 1);
}
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
});
setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
}, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
queryClient.removeQueries({ queryKey: [ resourceName ] });
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1);
setPageParams([ ]);
setPageParams({});
canGoBackwards.current = true;
window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :)
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
queryClient.removeQueries({ queryKey: [ resourceName ], type: 'inactive' });
}, 100);
});
setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
}, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]);
const onFilterChange = useCallback((newFilters: PaginationFiltersX<Resource> | undefined) => {
const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) {
......@@ -143,12 +143,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
{ shallow: true },
).then(() => {
setPage(1);
setPageParams([ ]);
setPageParams({});
scrollToTop();
});
}, [ queryName, router, scrollToTop, setPageParams, setPage ]);
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const hasPaginationParams = Object.keys(currPageParams || {}).length > 0;
const nextPageParams = data?.next_page_params;
const pagination = {
......@@ -165,12 +165,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
React.useEffect(() => {
if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey });
queryClient.cancelQueries({ queryKey: [ resourceName ] });
setPage(1);
}
// hook should run only when queryName has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ queryName ]);
}, [ resourceName ]);
React.useEffect(() => {
window.setTimeout(() => {
......
......@@ -32,6 +32,17 @@ export type PaginatedQueryKeys =
QueryKeys.addressCoinBalanceHistory |
QueryKeys.addressBlocksValidated;
export type PaginatedResources = 'blocks' | 'block_txs';
export type PaginatedResponseX<Q extends PaginatedResources> =
Q extends 'blocks' ? BlocksResponse :
Q extends 'block_txs' ? BlockTransactionsResponse :
never;
export type PaginationFiltersX<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters :
never;
export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.addressInternalTxs ? AddressInternalTxsResponse :
Q extends QueryKeys.addressTxs ? AddressTransactionsResponse :
......
......@@ -21,6 +21,7 @@ import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
interface Props {
......@@ -110,11 +111,9 @@ const BlocksContent = ({ type, query }: Props) => {
})();
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
return (
<>
{ isMobile && !isPaginatorHidden && (
{ isMobile && query.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
......
......@@ -2,7 +2,6 @@ import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
......@@ -30,8 +29,7 @@ const BlockPageContent = () => {
const appProps = useAppContext();
const blockTxsQuery = useQueryWithPages({
apiPath: `/node-api/blocks/${ router.query.id }/transactions`,
queryName: QueryKeys.blockTxs,
resourceName: 'block_txs',
options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'),
},
......
......@@ -2,7 +2,6 @@ import { useRouter } from 'next/router';
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 useIsMobile from 'lib/hooks/useIsMobile';
......@@ -19,12 +18,6 @@ const TAB_TO_TYPE: Record<string, BlockType> = {
uncles: 'uncle',
};
const TAB_TO_QUERY: Record<string, QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles> = {
blocks: QueryKeys.blocks,
reorgs: QueryKeys.blocksReorgs,
uncles: QueryKeys.blocksUncles,
};
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
......@@ -35,11 +28,9 @@ const BlocksPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const type = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_TYPE[router.query.tab] : 'block';
const queryName = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_QUERY[router.query.tab] : QueryKeys.blocks;
const blocksQuery = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName,
resourceName: 'blocks',
filters: { type },
});
......
......@@ -18,6 +18,7 @@ import useTxsSort from './useTxsSort';
type QueryResult = UseQueryResult<TxsResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
type Props = {
......@@ -31,7 +32,6 @@ type Props = {
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile();
const content = (() => {
......@@ -86,7 +86,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 }
top={ query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
......@@ -103,7 +103,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting }
setSorting={ setSortByValue }
paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden }
showPagination={ query.isPaginationVisible }
filterComponent={ filter }
/>
) }
......
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