Commit 76193424 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #311 from blockscout/pagination-fixes

refactor pagination to re-use it in internal txs, logs etc.
parents db1e20c5 e62aa4ec
......@@ -6,35 +6,30 @@ import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import type { PaginationParams } from 'types/api/pagination';
import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS: Array<keyof PaginationParams> = [ 'block_number', 'index', 'items_count' ];
interface ResponseWithPagination {
next_page_params: PaginationParams | null;
}
interface Params<Response> {
interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryKeys;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
options?: Omit<UseQueryOptions<unknown, unknown, Response>, 'queryKey' | 'queryFn'>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
export default function useQueryWithPages<Response extends ResponseWithPagination>({ queryName, filters, options, apiPath, queryIds }: Params<Response>) {
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ queryName, filters, options, apiPath, queryIds }: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName];
const queryClient = useQueryClient();
const router = useRouter();
const [ page, setPage ] = React.useState(1);
const currPageParams = pick(router.query, PAGINATION_FIELDS);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<PaginationParams>>>([ {} ]);
const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch();
const queryResult = useQuery<unknown, unknown, Response>(
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
[ queryName, ...(queryIds || []), { page, filters } ],
async() => {
const params: Array<string> = [];
......@@ -58,9 +53,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
// we hide next page button if no next_page_params
return;
}
// api adds filters into next-page-params now
// later filters will be removed from response
const nextPageParams = pick(data.next_page_params, PAGINATION_FIELDS);
const nextPageParams = data.next_page_params;
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
......@@ -71,18 +64,18 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1);
});
}, [ data, page, pageParams, router ]);
}, [ data?.next_page_params, page, pageParams.length, router ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query;
if (page === 2) {
nextPageQuery = omit(router.query, PAGINATION_FIELDS);
nextPageQuery = omit(router.query, paginationFields);
} else {
const nextPageParams = { ...pageParams[page - 2] };
nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
}
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
......@@ -91,16 +84,16 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
setPage(prev => prev - 1);
page === 2 && queryClient.clear();
});
}, [ router, page, pageParams, queryClient ]);
}, [ router, page, paginationFields, pageParams, queryClient ]);
const resetPage = useCallback(() => {
queryClient.clear();
router.push({ pathname: router.pathname, query: omit(router.query, PAGINATION_FIELDS) }, undefined, { shallow: true }).then(() => {
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields) }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 });
setPage(1);
setPageParams([ {} ]);
setPageParams([ ]);
});
}, [ router, queryClient ]);
}, [ queryClient, router, paginationFields ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params;
......
import type { TransactionsResponse } from 'types/api/transaction';
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
export default function sortTxs(txs: TransactionsResponse['items'], sorting?: Sort) {
let sortedTxs;
const sortTxs = (sorting?: Sort) => (tx1: Transaction, tx2: Transaction) => {
switch (sorting) {
case 'val-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value));
break;
return compareBns(tx1.value, tx2.value);
case 'val-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value));
break;
return compareBns(tx2.value, tx1.value);
case 'fee-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value));
break;
return compareBns(tx1.fee.value, tx2.fee.value);
case 'fee-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value));
break;
return compareBns(tx2.fee.value, tx1.fee.value);
default:
sortedTxs = txs;
return 0;
}
};
return sortedTxs;
}
export default sortTxs;
import type { AddressParam } from 'types/api/addressParams';
import type { PaginationParams } from 'types/api/pagination';
import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
......@@ -34,12 +33,18 @@ export interface Block {
export interface BlocksResponse {
items: Array<Block>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
items_count: number;
} | null;
}
export interface BlockTransactionsResponse {
items: Array<Transaction>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
items_count: number;
} | null;
}
export interface NewBlockSocketResponse {
......
import type { AddressParam } from './addressParams';
import type { PaginationParams } from './pagination';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
......@@ -20,7 +19,10 @@ export interface InternalTransaction {
export interface InternalTransactionsResponse {
items: Array<InternalTransaction>;
next_page_params: PaginationParams & {
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
transaction_index: number;
};
......
export interface PaginationParams {
block_number: number;
index?: number;
items_count: number;
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys =
QueryKeys.blocks |
QueryKeys.blockTxs |
QueryKeys.txsValidate |
QueryKeys.txsPending |
QueryKeys.txInternals |
QueryKeys.txLogs |
QueryKeys.txTokenTransfers;
export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlocksResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txsPending ? TransactionsResponsePending :
Q extends QueryKeys.txInternals ? InternalTransactionsResponse :
Q extends QueryKeys.txLogs ? LogsResponse :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginatedResponse<K>['next_page_params']>>
}
export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blockTxs]: [ 'block_number', 'items_count' ],
[QueryKeys.txsValidate]: [ 'block_number', 'items_count', 'filter', 'index' ],
[QueryKeys.txsPending]: [ 'filter', 'hash', 'inserted_at' ],
[QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ],
[QueryKeys.txTokenTransfers]: [ 'block_number', 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.txLogs]: [ 'items_count', 'transaction_hash', 'index' ],
};
import type { AddressParam } from './addressParams';
import type { PaginationParams } from './pagination';
import type { TokenInfoGeneric } from './tokenInfo';
export type Erc20TotalPayload = {
......@@ -41,5 +40,10 @@ interface TokenTransferBase {
export interface TokenTransferResponse {
items: Array<TokenTransfer>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
} | null;
}
import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { PaginationParams } from './pagination';
import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
......@@ -44,9 +43,25 @@ export interface Transaction {
tx_tag: string | null;
}
export interface TransactionsResponse {
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
export interface TransactionsResponseValidated {
items: Array<Transaction>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
index: number;
items_count: number;
filter: 'validated';
} | null;
}
export interface TransactionsResponsePending {
items: Array<Transaction>;
next_page_params: {
inserted_at: string;
hash: string;
filter: 'pending';
} | null;
}
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
export enum QueryKeys {
csrf = 'csrf',
profile = 'profile',
transactions = 'transactions',
txsValidate = 'txs-validated',
txsPending = 'txs-pending',
tx = 'tx',
txInternals = 'tx-internals',
txLog = 'tx-log',
txLogs = 'tx-logs',
txRawTrace = 'tx-raw-trace',
txTokenTransfers = 'tx-token-transfers',
blockTxs = 'block-transactions',
......
export type KeysOfObjectOrNull<T> = T extends null ? never : keyof T;
......@@ -12,6 +12,7 @@ const BlockTxs = () => {
<TxsContent
queryName={ QueryKeys.blockTxs }
apiPath={ `/node-api/blocks/${ router.query.id }/transactions` }
showBlockInfo={ false }
/>
);
};
......
......@@ -25,11 +25,12 @@ const BlocksContent = ({ type }: Props) => {
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError, pagination } = useQueryWithPages<BlocksResponse>({
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) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
......@@ -91,21 +92,27 @@ const BlocksContent = ({ type }: Props) => {
<>
{ 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>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items } top={ isPaginatorHidden ? 0 : 80 } page={ pagination.page }/></Hide>
</>
);
})();
const totalText = data?.items.length ?
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
null;
return (
<>
{ data ?
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
}
{ !isPaginatorHidden && (
<ActionBar>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
......
......@@ -12,13 +12,15 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
top: number;
page: number;
}
const BlocksTable = ({ data }: Props) => {
const BlocksTable = ({ data, top, page }: Props) => {
return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead top={ 80 }>
<Thead top={ top }>
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size</Th>
......@@ -31,8 +33,7 @@ const BlocksTable = ({ data }: Props) => {
</Thead>
<Tbody>
<AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement/>) }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) }
</AnimatePresence>
</Tbody>
</Table>
......
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -19,7 +18,7 @@ interface Props {
isLoading?: boolean;
isDisabled?: boolean;
path: string;
queryName: QueryKeys;
queryName: QueryKeys.txTokenTransfers;
queryIds?: Array<string>;
baseAddress?: string;
showTxInfo?: boolean;
......@@ -27,7 +26,7 @@ interface Props {
}
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true, txHash }: Props) => {
const { isError, isLoading, data, pagination } = useQueryWithPages<TokenTransferResponse>({
const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
......
import { Box, Text, Show, Hide } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { InternalTransactionsResponse, InternalTransaction } from 'types/api/internalTransaction';
import type { InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
// import FilterInput from 'ui/shared/FilterInput';
// import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import Pagination from 'ui/shared/Pagination';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
......@@ -70,21 +70,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
// };
const TxInternals = () => {
const router = useRouter();
const fetch = useFetch();
// filters are not implemented yet in api
// const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
// const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ QueryKeys.txInternals, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`,
queryName: QueryKeys.txInternals,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
);
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const isMobile = useIsMobile();
......@@ -132,11 +131,16 @@ const TxInternals = () => {
return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>;
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginatorHidden ? 0 : 80 }/>;
})();
return (
<Box>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ /* <Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
......
import { Box, Text } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
......@@ -16,17 +15,16 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ QueryKeys.txLog, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`,
queryName: QueryKeys.txLogs,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
);
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
......@@ -51,6 +49,11 @@ const TxLogs = () => {
return (
<Box>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</Box>
);
......
......@@ -7,7 +7,7 @@ import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
return (
<Box mt={ 6 }>
<Box>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box>
);
......
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</>
);
};
......
......@@ -5,11 +5,6 @@ const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
......@@ -45,7 +40,6 @@ const TxInternalsSkeletonMobile = () => {
</Flex>
)) }
</Box>
</>
);
};
......
......@@ -13,14 +13,15 @@ interface Props {
data: Array<InternalTransaction>;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
top: number;
}
const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const TxInternalsTable = ({ data, sort, onSortToggle, top }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<Table variant="simple" size="sm" mt={ 6 }>
<Thead top={ 0 }>
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="28%">Type</Th>
<Th width="20%">From</Th>
......
import { Text, Box, Show, Hide } from '@chakra-ui/react';
import React, { useState, useCallback } from 'react';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsHeader from './TxsHeader';
import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort';
import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort';
type Props = {
queryName: QueryKeys;
queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs;
showDescription?: boolean;
stateFilter?: TTxsFilters['filter'];
apiPath: string;
showBlockInfo?: boolean;
}
const TxsContent = ({
......@@ -27,67 +28,53 @@ const TxsContent = ({
showDescription,
stateFilter,
apiPath,
showBlockInfo = true,
}: Props) => {
const [ sorting, setSorting ] = useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort || '');
// const [ filters, setFilters ] = useState<Partial<TTxsFilters>>({ type: [], method: [] });
const sort = useCallback((field: 'val' | 'fee') => () => {
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') {
newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
}
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = 'fee-asc';
} else {
newVal = 'fee-desc';
}
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ ]);
const {
data,
isLoading,
isError,
pagination,
} = useQueryWithPages<TransactionsResponse>({
...queryResult
} = useQueryWithPages({
apiPath,
queryName,
filters: stateFilter ? { filter: stateFilter } : undefined,
});
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(queryResult);
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
const txs = data?.items;
if (!isLoading && !txs?.length) {
return <Text as="span">There are no transactions.</Text>;
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }><TxsSkeletonMobile showBlockInfo={ showBlockInfo }/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop showBlockInfo={ showBlockInfo }/></Hide>
</>
);
}
if (!isLoading && txs) {
return <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
const txs = data.items;
if (!txs.length) {
return <Text as="span">There are no transactions.</Text>;
}
return (
<>
<Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop/></Hide>
<Show below="lg" ssr={ false }>
<Box>
<TxsNewItemNotice>
{ ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<TxsTable txs={ txs } sort={ setSortByField } sorting={ sorting } showBlockInfo={ showBlockInfo } top={ isPaginatorHidden ? 0 : 80 }/>
</Hide>
</>
);
})();
......@@ -95,7 +82,7 @@ const TxsContent = ({
return (
<>
{ showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> }
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSorting } paginationProps={ pagination }/>
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/>
{ content }
</>
);
......
......@@ -14,14 +14,19 @@ import TxsSorting from 'ui/txs/TxsSorting';
type Props = {
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
setSorting: (val: Sort) => void;
paginationProps: PaginationProps;
className?: string;
showPagination?: boolean;
}
const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) => {
const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
const isMobile = useIsMobile(false);
if (!showPagination && !isMobile) {
return null;
}
return (
<ActionBar className={ className }>
<HStack>
......@@ -47,7 +52,7 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) =
placeholder="Search by addresses, hash, method..."
/> */ }
</HStack>
<Pagination { ...paginationProps }/>
{ showPagination && <Pagination { ...paginationProps }/> }
</ActionBar>
);
};
......
......@@ -28,7 +28,7 @@ import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: Transaction}) => {
const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300');
......@@ -77,7 +77,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.method }
</Text>
</Flex>
{ tx.block !== null && (
{ showBlockInfo && tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
......
......@@ -3,10 +3,17 @@ import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = () => {
interface Props {
showBlockInfo: boolean;
}
const TxsInternalsSkeletonDesktop = ({ showBlockInfo }: Props) => {
return (
<Box mb={ 8 }>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
<SkeletonTable columns={ showBlockInfo ?
[ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] :
[ '32px', '20%', '18%', '15%', '292px', '18%', '18%' ]
}/>
</Box>
);
};
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
interface Props {
showBlockInfo: boolean;
}
const TxInternalsSkeletonMobile = ({ showBlockInfo }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
......@@ -25,7 +29,7 @@ const TxInternalsSkeletonMobile = () => {
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
{ showBlockInfo && <Skeleton w="100%" h={ 6 } mt={ 6 }/> }
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
......
......@@ -11,13 +11,12 @@ import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import SortButton from 'ui/shared/SortButton';
interface Props {
isActive: boolean;
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
setSorting: (val: Sort) => void;
}
const SORT_OPTIONS = [
......@@ -32,14 +31,8 @@ const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (val !== prevVal) {
newVal = val as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
const value = val as Sort | Array<Sort>;
setSorting(Array.isArray(value) ? value[0] : value);
}, [ setSorting ]);
return (
......
......@@ -9,14 +9,25 @@ type Props = {
}
const TxsTab = ({ tab }: Props) => {
if (tab === 'validated') {
return (
<TxsContent
queryName={ QueryKeys.transactions }
showDescription={ tab === 'validated' }
stateFilter={ tab }
queryName={ QueryKeys.txsValidate }
showDescription
stateFilter="validated"
apiPath="/node-api/transactions"
/>
);
}
return (
<TxsContent
queryName={ QueryKeys.txsPending }
stateFilter="pending"
apiPath="/node-api/transactions"
showBlockInfo={ false }
/>
);
};
export default TxsTab;
......@@ -15,18 +15,20 @@ type Props = {
txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void;
sorting?: Sort;
top: number;
showBlockInfo: boolean;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => {
return (
<Table variant="simple" minWidth="810px" size="xs">
<TheadSticky top={ 80 }>
<TheadSticky top={ top }>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
{ showBlockInfo && <Th width="11%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th>
......@@ -54,6 +56,7 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
<TxsTableItem
key={ item.hash }
tx={ item }
showBlockInfo={ showBlockInfo }
/>
)) }
</Tbody>
......
......@@ -34,7 +34,7 @@ import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: Transaction}) => {
const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean }) => {
const addressFrom = (
<Address>
<Tooltip label={ tx.from.implementation_name }>
......@@ -102,9 +102,11 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
</TruncatedTextTooltip>
) : '-' }
</Td>
{ showBlockInfo && (
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
) }
<Show above="xl" ssr={ false }>
<Td>
{ addressFrom }
......
import { Box, Show, Hide } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import sortTxs from 'lib/tx/sortTxs';
import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
sorting?: Sort;
sort: (field: 'val' | 'fee') => () => void;
}
const TxsWithSort = ({
txs,
sorting,
sort,
}: Props) => {
const [ sortedTxs, setSortedTxs ] = useState<TransactionsResponse['items']>(sortTxs(txs, sorting));
useEffect(() => {
setSortedTxs(sortTxs(txs, sorting));
}, [ sorting, txs ]);
return (
<>
<Show below="lg" ssr={ false }>
<Box>
<TxsNewItemNotice>
{ ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }
</Box>
</Show>
<Hide below="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Hide>
</>
);
};
export default TxsWithSort;
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { BlockTransactionsResponse } from 'types/api/block';
import type { TransactionsResponsePending, TransactionsResponseValidated } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import sortTxs from 'lib/tx/sortTxs';
type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
type HookResult = UseQueryResult<TxsResponse> & {
sorting: Sort;
setSortByField: (field: 'val' | 'fee') => () => void;
setSortByValue: (value: Sort) => void;
}
export default function useTxsSort(
queryResult: UseQueryResult<TxsResponse>,
): HookResult {
const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort);
const setSortByField = React.useCallback((field: 'val' | 'fee') => () => {
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') {
newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
}
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = 'fee-asc';
} else {
newVal = 'fee-desc';
}
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ ]);
const setSortByValue = React.useCallback((value: Sort) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (value !== prevVal) {
newVal = value as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, []);
return React.useMemo(() => {
if (queryResult.isError || queryResult.isLoading) {
return { ...queryResult, setSortByField, setSortByValue, sorting };
}
return {
...queryResult,
data: { ...queryResult.data, items: queryResult.data.items.slice().sort(sortTxs(sorting)) },
setSortByField,
setSortByValue,
sorting,
};
}, [ queryResult, setSortByField, setSortByValue, sorting ]);
}
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