Improved pagination (#194)

* Improve pagination in address history

* Use explorer API to search transactions; improve UI

* Improve txs pagination

* Improve limit validation

* Refactor transactions tab params
pull/195/head
Artem 2 years ago committed by GitHub
parent 621aa6ae9d
commit 4fb0294f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/components/tables/TransactionsTable.tsx
  2. 99
      src/components/ui/Pagination/index.tsx
  3. 57
      src/pages/AddressPage/index.tsx
  4. 88
      src/pages/AddressPage/tabs/Transactions.tsx
  5. 61
      src/pages/AddressPage/tabs/txsColumns.tsx
  6. 2
      src/types/api.ts

@ -184,10 +184,10 @@ export function TransactionsTable(props: TransactionTableProps) {
const _IsLoading = isLoading; const _IsLoading = isLoading;
useEffect(() => { // useEffect(() => {
filter.offset = 0; // filter.offset = 0;
setFilter(filter); // setFilter(filter);
}, [filter.limit]); // }, [filter.limit]);
return ( return (
<> <>

@ -3,8 +3,22 @@ import { Box, Text, Select } from "grommet";
import { Filter } from "src/types"; import { Filter } from "src/types";
import { FormPrevious, FormNext } from "grommet-icons"; import { FormPrevious, FormNext } from "grommet-icons";
import { formatNumber } from "src/components/ui/utils"; import { formatNumber } from "src/components/ui/utils";
import styled from "styled-components";
export type TPaginationAction = "nextPage" | "prevPage"; const NavigationItem = styled(Box)<{ disabled?: boolean }>`
border-radius: 4px;
height: 28px;
align-items: center;
justify-content: center;
padding: 4px 8px;
text-align: center;
background: ${(props) => props.theme.global.colors.backgroundBack};
cursor: ${(props) => props.disabled ? 'default': 'pointer'};
opacity: ${(props) => props.disabled ? 0.6: 1};
border: 1px solid ${(props) => props.theme.global.colors.border};
`
export type TPaginationAction = "nextPage" | "prevPage" | "firstPage" | "lastPage";
interface PaginationNavigator { interface PaginationNavigator {
filter: Filter; filter: Filter;
@ -32,6 +46,15 @@ export function PaginationNavigator(props: PaginationNavigator) {
const { offset = 0, limit = 10 } = filter; const { offset = 0, limit = 10 } = filter;
const onFirstPageClick = () => {
const newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
newFilter.offset = 0;
if (!isLoading) {
onChange(newFilter, "firstPage");
}
};
const onPrevClick = () => { const onPrevClick = () => {
const newFilter = JSON.parse(JSON.stringify(filter)) as Filter; const newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
newFilter.offset = newFilter.offset - (filter.limit || 10); newFilter.offset = newFilter.offset - (filter.limit || 10);
@ -49,17 +72,31 @@ export function PaginationNavigator(props: PaginationNavigator) {
} }
}; };
const onLastPageClick = () => {
const newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
const limit = filter.limit || 10
newFilter.offset = limit * +Math.ceil(Number(totalElements) / limit).toFixed(0) - limit
if (!isLoading) {
onChange(newFilter, "lastPage");
}
};
const currentPage = +(+offset / limit).toFixed(0) + 1
const totalPages = +Math.ceil(Number(totalElements) / limit).toFixed(0)
return ( return (
<Box style={{ flex: "0 0 auto" }}> <Box style={{ flex: "0 0 auto" }}>
<Pagination <Pagination
//@ts-ignore //@ts-ignore
currentPage={+(+offset / limit).toFixed(0) + 1} currentPage={currentPage}
totalPages={+Math.ceil(Number(totalElements) / limit).toFixed(0)} totalPages={totalPages}
onFirstPageClick={onFirstPageClick}
onPrevPageClick={onPrevClick} onPrevPageClick={onPrevClick}
onNextPageClick={onNextClick} onNextPageClick={onNextClick}
onLastPageClick={onLastPageClick}
showPages={showPages} showPages={showPages}
disableNextBtn={elements.length < limit}
disablePrevBtn={filter.offset === 0} disablePrevBtn={filter.offset === 0}
disableNextBtn={currentPage >= totalPages}
/> />
</Box> </Box>
); );
@ -68,8 +105,10 @@ interface PaginationProps {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
showPages?: boolean; showPages?: boolean;
onFirstPageClick: () => void;
onPrevPageClick: () => void; onPrevPageClick: () => void;
onNextPageClick: () => void; onNextPageClick: () => void;
onLastPageClick: () => void;
disableNextBtn: boolean; disableNextBtn: boolean;
disablePrevBtn: boolean; disablePrevBtn: boolean;
} }
@ -78,38 +117,40 @@ function Pagination(props: PaginationProps) {
const { const {
currentPage, currentPage,
totalPages, totalPages,
onFirstPageClick,
onPrevPageClick, onPrevPageClick,
onNextPageClick, onNextPageClick,
onLastPageClick,
showPages, showPages,
disableNextBtn, disableNextBtn,
disablePrevBtn, disablePrevBtn,
} = props; } = props;
return ( return (
<Box direction="row" gap="small"> <Box direction="row" gap="xsmall" align={'center'}>
<FormPrevious {showPages &&
onClick={disablePrevBtn ? undefined : onPrevPageClick} <NavigationItem disabled={disablePrevBtn} onClick={disablePrevBtn ? undefined : onFirstPageClick}>
style={{ <Text size={'xsmall'}>First</Text>
cursor: "pointer", </NavigationItem>
userSelect: "none", }
opacity: disablePrevBtn ? 0.5 : 1, <NavigationItem disabled={disablePrevBtn} onClick={disablePrevBtn ? undefined : onPrevPageClick}>
}} <FormPrevious size={'20px'} style={{ userSelect: "none"}} />
/> </NavigationItem>
{showPages && ( {showPages &&
<Text style={{ fontWeight: "bold" }}>{formatNumber(+currentPage)}</Text> <NavigationItem disabled={true}>
)} <Text size={'xsmall'} style={{ cursor: 'default' }}>
{showPages && <Text style={{ fontWeight: 300 }}>/</Text>} Page {formatNumber(+currentPage)} of {formatNumber(+totalPages)}
{showPages && ( </Text>
<Text style={{ fontWeight: 300 }}>{formatNumber(+totalPages)}</Text> </NavigationItem>
)} }
<FormNext <NavigationItem disabled={disableNextBtn} onClick={disableNextBtn ? undefined : onNextPageClick}>
onClick={disableNextBtn ? undefined : onNextPageClick} <FormNext size={'20px'} style={{ userSelect: "none" }} />
style={{ </NavigationItem>
cursor: "pointer", {showPages &&
userSelect: "none", <NavigationItem disabled={disableNextBtn} onClick={disableNextBtn ? undefined : onLastPageClick}>
opacity: disableNextBtn ? 0.5 : 1, <Text size={'xsmall'}>Last</Text>
}} </NavigationItem>
/> }
</Box> </Box>
); );
} }
@ -134,7 +175,7 @@ export function PaginationRecordsPerPage(props: ElementsPerPage) {
return ( return (
<Box direction="row" gap="small" align="center"> <Box direction="row" gap="small" align="center">
<Box style={{ width: "95px" }}> <Box style={{ width: "105px" }}>
<Select <Select
options={options} options={options}
value={limit.toString()} value={limit.toString()}

@ -34,61 +34,12 @@ import { parseHexToText } from "../../web3/parseHex";
import { EventsTab } from "./tabs/events/Events"; import { EventsTab } from "./tabs/events/Events";
import { ToolsTab } from "./tabs/tools"; import { ToolsTab } from "./tabs/tools";
import { config } from "../../config"; import { config } from "../../config";
import useQuery from "../../hooks/useQuery";
export function AddressPage() { export function AddressPage() {
const history = useHistory(); const history = useHistory();
const tabParamName = "activeTab="; const queryParams = useQuery();
const oldTabParamName = "txType="; const activeTab = +(queryParams.get('activeTab') || 0);
let activeTab = 0;
try {
const newValue = +history.location.search.slice(
history.location.search.indexOf("activeTab=") + tabParamName.length
);
const oldTxType = history.location.search.slice(
history.location.search.indexOf(oldTabParamName) + oldTabParamName.length
);
activeTab = isNaN(newValue) ? 0 : newValue;
switch (oldTxType) {
case "regular": {
activeTab = 0;
break;
}
case "staking": {
activeTab = 1;
break;
}
case "hrc20": {
activeTab = 3;
break;
}
case "hrc721": {
activeTab = 4;
break;
}
case "hrc721Assets": {
activeTab = 5;
break;
}
default: {
}
}
if (activeTab === 0 && history.location.hash === "#code") {
activeTab = 7; // note how do i derive this active tab? its a bit hard coded atm
}
} catch {
activeTab = 0;
}
const [contracts, setContracts] = useState<AddressDetails | null>(null); const [contracts, setContracts] = useState<AddressDetails | null>(null);
const [contractShardId, setContractShardId] = useState<ShardID | null>(null); const [contractShardId, setContractShardId] = useState<ShardID | null>(null);
@ -102,7 +53,7 @@ export function AddressPage() {
const [inventoryHolders, setInventoryForHolders] = useState< const [inventoryHolders, setInventoryForHolders] = useState<
IUserERC721Assets[] IUserERC721Assets[]
>([]); >([]);
const [activeIndex, setActiveIndex] = useState(+activeTab); const [activeIndex, setActiveIndex] = useState(activeTab);
const erc20Map = useERC20Pool(); const erc20Map = useERC20Pool();
const erc721Map = useERC721Pool(); const erc721Map = useERC721Pool();
const erc1155Map = useERC1155Pool(); const erc1155Map = useERC1155Pool();

@ -1,17 +1,12 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Box } from "grommet"; import { Box } from "grommet";
import { useParams } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { import {
getByteCodeSignatureByHash, getByteCodeSignatureByHash,
getRelatedTransactionsByType, getRelatedTransactionsByType,
getRelatedTransactionsCountByType getRelatedTransactionsCountByType
} from "src/api/client"; } from "src/api/client";
import { TransactionsTable } from "src/components/tables/TransactionsTable"; import { TransactionsTable } from "src/components/tables/TransactionsTable";
import {
Address,
ONEValue,
DateTime, ONEValueWithInternal, TipContent
} from "src/components/ui";
import { import {
Filter, Filter,
RelatedTransaction, RelatedTransaction,
@ -26,8 +21,10 @@ import {
hmyv2_getTransactionsHistory hmyv2_getTransactionsHistory
} from "../../../api/rpc"; } from "../../../api/rpc";
import { getColumns, getERC20Columns, getNFTColumns, getStackingColumns } from "./txsColumns"; import { getColumns, getERC20Columns, getNFTColumns, getStackingColumns } from "./txsColumns";
import useQuery from "../../../hooks/useQuery";
const internalTxsBlocksFrom = 23000000 const internalTxsBlocksFrom = 23000000
const allowedLimits = [10, 25, 50, 100]
const relatedTxMap: Record<RelatedTransactionType, string> = { const relatedTxMap: Record<RelatedTransactionType, string> = {
transaction: "Transaction", transaction: "Transaction",
@ -49,11 +46,18 @@ export function Transactions(props: {
rowDetails?: (row: any) => JSX.Element; rowDetails?: (row: any) => JSX.Element;
onTxsLoaded?: (txs: RelatedTransaction[]) => void; onTxsLoaded?: (txs: RelatedTransaction[]) => void;
}) { }) {
const limitValue = localStorage.getItem("tableLimitValue"); const history = useHistory()
const queryParams = useQuery();
let limitParam = +(queryParams.get('limit') || localStorage.getItem("tableLimitValue") || allowedLimits[0]);
const offsetParam = +(queryParams.get('offset') || 0)
if (!allowedLimits.includes(limitParam)) {
limitParam = allowedLimits[0]
}
const initFilter: Filter = { const initFilter: Filter = {
offset: 0, offset: offsetParam,
limit: limitValue ? +limitValue : 10, limit: limitParam,
orderBy: "block_number", orderBy: "block_number",
orderDirection: "desc", orderDirection: "desc",
filters: [{ type: "gte", property: "block_number", value: 0 }], filters: [{ type: "gte", property: "block_number", value: 0 }],
@ -111,18 +115,18 @@ export function Transactions(props: {
const loadTransactions = async () => { const loadTransactions = async () => {
setIsLoading(true) setIsLoading(true)
try { try {
let txs = await getTransactionsFromRPC() // let txs = await getTransactionsFromRPC()
// let txs = [] let txs = []
// const txsFilter = {...filter[props.type]} const txsFilter = {...filter[props.type]}
// if (props.type === 'internal_transaction') { if (props.type === 'internal_transaction') {
// txsFilter.filters = [{ type: "gte", property: "block_number", value: internalTxsBlocksFrom }] txsFilter.filters = [{ type: "gte", property: "block_number", value: internalTxsBlocksFrom }]
// } }
// txs = await getRelatedTransactionsByType([ txs = await getRelatedTransactionsByType([
// 0, 0,
// id, id,
// props.type, props.type,
// txsFilter, txsFilter,
// ]); ]);
// for transactions we display call method if any // for transactions we display call method if any
if (props.type === "transaction") { if (props.type === "transaction") {
@ -209,17 +213,23 @@ export function Transactions(props: {
if (cachedValue && id === prevId) { if (cachedValue && id === prevId) {
setTotalElements(cachedValue) setTotalElements(cachedValue)
} else { } else {
getTxsCountFromRPC() // getTxsCountFromRPC()
getTxsCount()
} }
}, [props.type, id]) }, [props.type, id])
// Change active tab
useEffect(() => { useEffect(() => {
if (prevType === props.type) { // If tab changed (and not initially loaded), drop offset to zero
loadTransactions() if (prevType) {
setFilter({
...initFilterState,
[props.type]: {
...initFilter,
offset: 0
}
})
} }
}, [filter[props.type], id]);
useEffect(() => {
if (cachedTxs[props.type]) { if (cachedTxs[props.type]) {
setRelatedTrxs(cachedTxs[props.type]); setRelatedTrxs(cachedTxs[props.type]);
} else { } else {
@ -227,6 +237,13 @@ export function Transactions(props: {
} }
}, [props.type]) }, [props.type])
// Change params: offset, limit
useEffect(() => {
if (prevType === props.type) {
loadTransactions()
}
}, [filter[props.type], id]);
let columns = []; let columns = [];
switch (props.type) { switch (props.type) {
@ -250,6 +267,16 @@ export function Transactions(props: {
} }
} }
const onFilterChanged = (value: Filter) => {
const { offset, limit } = value
if (limit !== filter[props.type].limit) {
localStorage.setItem("tableLimitValue", `${limit}`);
}
setFilter({ ...filter, [props.type]: value });
const activeTab = queryParams.get('activeTab') || 0
history.push(`?activeTab=${activeTab}&offset=${offset}&limit=${limit}`)
}
return ( return (
<Box style={{ padding: "10px" }}> <Box style={{ padding: "10px" }}>
<TransactionsTable <TransactionsTable
@ -259,12 +286,7 @@ export function Transactions(props: {
limit={+limit} limit={+limit}
filter={filter[props.type]} filter={filter[props.type]}
isLoading={isLoading} isLoading={isLoading}
setFilter={(value) => { setFilter={onFilterChanged}
if (value.limit !== filter[props.type].limit) {
localStorage.setItem("tableLimitValue", `${value.limit}`);
}
setFilter({ ...filter, [props.type]: value });
}}
noScrollTop noScrollTop
minWidth="1266px" minWidth="1266px"
hideCounter hideCounter

@ -17,7 +17,9 @@ const transferSingleSignature = erc1155ABIManager.getEntryByName('TransferSingle
const erc20TransferTopic = const erc20TransferTopic =
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
const Marker = styled.div<{ out: boolean }>` type TxDirection = 'in' | 'out' | 'self'
const Marker = styled.div<{ direction: TxDirection }>`
border-radius: 2px; border-radius: 2px;
padding: 5px; padding: 5px;
@ -25,12 +27,16 @@ const Marker = styled.div<{ out: boolean }>`
font-weight: bold; font-weight: bold;
${(props) => ${(props) =>
props.out props.direction === 'self'
? css` ? css`
background: ${(props) => props.theme.global.colors.backgroundBack};
`
: props.direction === 'out'
? css`
background: rgb(239 145 62); background: rgb(239 145 62);
color: #fff; color: #fff;
` `
: css` : css`
background: rgba(105, 250, 189, 0.8); background: rgba(105, 250, 189, 0.8);
color: #1b295e; color: #1b295e;
`}; `};
@ -41,7 +47,6 @@ const NeutralMarker = styled(Box)`
padding: 5px; padding: 5px;
text-align: center; text-align: center;
font-weight: bold;
` `
const TxMethod = styled(Text)` const TxMethod = styled(Text)`
@ -135,6 +140,19 @@ const extractTokenId = memo((data: any) => {
return '' return ''
}) })
const TransferDirectionMarker = (props: { id: string, data: RelatedTransaction }) => {
const { id, data: { from, to } } = props
let direction: TxDirection = from === id ? 'out' : 'in'
if (from === to) {
direction = 'self'
}
return <Text size="12px">
<Marker direction={direction}>{direction.toUpperCase()}</Marker>
</Text>
}
export function getERC20Columns(id: string): ColumnConfig<any>[] { export function getERC20Columns(id: string): ColumnConfig<any>[] {
return [ return [
{ {
@ -193,16 +211,7 @@ export function getERC20Columns(id: string): ColumnConfig<any>[] {
{ {
property: 'marker', property: 'marker',
header: <></>, header: <></>,
render: (data: RelatedTransaction) => { render: (data: RelatedTransaction) => <TransferDirectionMarker id={id} data={data} />
const { from } = data
return (
<Text size="10px">
<Marker out={from === id}>
{from === id ? 'OUT' : 'IN'}
</Marker>
</Text>
)
}
}, },
{ {
property: 'to', property: 'to',
@ -416,13 +425,7 @@ export function getColumns(id: string): ColumnConfig<any>[] {
{ {
property: "marker", property: "marker",
header: <></>, header: <></>,
render: (data: RelatedTransaction) => ( render: (data: RelatedTransaction) => <TransferDirectionMarker id={id} data={data} />,
<Text size="12px">
<Marker out={data.from === id}>
{data.from === id ? "OUT" : "IN"}
</Marker>
</Text>
),
}, },
{ {
property: "to", property: "to",
@ -542,13 +545,7 @@ export function getNFTColumns(id: string): ColumnConfig<any>[] {
{ {
property: "marker", property: "marker",
header: <></>, header: <></>,
render: (data: RelatedTransaction) => ( render: (data: RelatedTransaction) => <TransferDirectionMarker id={id} data={data} />,
<Text size="10px">
<Marker out={data.from === id}>
{data.from === id ? "OUT" : "IN"}
</Marker>
</Text>
),
}, },
{ {
property: "to", property: "to",
@ -714,13 +711,7 @@ export const getStackingColumns = (id: string): ColumnConfig<any>[] => {
{ {
property: "marker", property: "marker",
header: <></>, header: <></>,
render: (data: RelatedTransaction) => ( render: (data: RelatedTransaction) => <TransferDirectionMarker id={id} data={data} />,
<Text size="12px">
<Marker out={data.from === id}>
{data.from === id ? "OUT" : "IN"}
</Marker>
</Text>
),
}, },
{ {
property: "delegator", property: "delegator",

@ -1,6 +1,6 @@
import * as blockchain from './blockchain' import * as blockchain from './blockchain'
export type FilterType = 'gt' | 'gte' | 'lt' | 'lte' | 'eq' export type FilterType = 'gt' | 'gte' | 'lt' | 'lte' | 'eq'
export type FilterProperty = 'number' | 'block_number' | 'address' export type FilterProperty = 'number' | 'block_number' | 'address' | 'to'
export type TransactionQueryField = 'block_number' | 'block_hash' | 'hash' | 'hash_harmony' export type TransactionQueryField = 'block_number' | 'block_hash' | 'hash' | 'hash_harmony'
export type StakingTransactionQueryField = 'block_number' | 'block_hash' | 'hash' export type StakingTransactionQueryField = 'block_number' | 'block_hash' | 'hash'

Loading…
Cancel
Save