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;
useEffect(() => {
filter.offset = 0;
setFilter(filter);
}, [filter.limit]);
// useEffect(() => {
// filter.offset = 0;
// setFilter(filter);
// }, [filter.limit]);
return (
<>

@ -3,8 +3,22 @@ import { Box, Text, Select } from "grommet";
import { Filter } from "src/types";
import { FormPrevious, FormNext } from "grommet-icons";
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 {
filter: Filter;
@ -32,6 +46,15 @@ export function PaginationNavigator(props: PaginationNavigator) {
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 newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
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 (
<Box style={{ flex: "0 0 auto" }}>
<Pagination
//@ts-ignore
currentPage={+(+offset / limit).toFixed(0) + 1}
totalPages={+Math.ceil(Number(totalElements) / limit).toFixed(0)}
currentPage={currentPage}
totalPages={totalPages}
onFirstPageClick={onFirstPageClick}
onPrevPageClick={onPrevClick}
onNextPageClick={onNextClick}
onLastPageClick={onLastPageClick}
showPages={showPages}
disableNextBtn={elements.length < limit}
disablePrevBtn={filter.offset === 0}
disableNextBtn={currentPage >= totalPages}
/>
</Box>
);
@ -68,8 +105,10 @@ interface PaginationProps {
currentPage: number;
totalPages: number;
showPages?: boolean;
onFirstPageClick: () => void;
onPrevPageClick: () => void;
onNextPageClick: () => void;
onLastPageClick: () => void;
disableNextBtn: boolean;
disablePrevBtn: boolean;
}
@ -78,38 +117,40 @@ function Pagination(props: PaginationProps) {
const {
currentPage,
totalPages,
onFirstPageClick,
onPrevPageClick,
onNextPageClick,
onLastPageClick,
showPages,
disableNextBtn,
disablePrevBtn,
} = props;
return (
<Box direction="row" gap="small">
<FormPrevious
onClick={disablePrevBtn ? undefined : onPrevPageClick}
style={{
cursor: "pointer",
userSelect: "none",
opacity: disablePrevBtn ? 0.5 : 1,
}}
/>
{showPages && (
<Text style={{ fontWeight: "bold" }}>{formatNumber(+currentPage)}</Text>
)}
{showPages && <Text style={{ fontWeight: 300 }}>/</Text>}
{showPages && (
<Text style={{ fontWeight: 300 }}>{formatNumber(+totalPages)}</Text>
)}
<FormNext
onClick={disableNextBtn ? undefined : onNextPageClick}
style={{
cursor: "pointer",
userSelect: "none",
opacity: disableNextBtn ? 0.5 : 1,
}}
/>
<Box direction="row" gap="xsmall" align={'center'}>
{showPages &&
<NavigationItem disabled={disablePrevBtn} onClick={disablePrevBtn ? undefined : onFirstPageClick}>
<Text size={'xsmall'}>First</Text>
</NavigationItem>
}
<NavigationItem disabled={disablePrevBtn} onClick={disablePrevBtn ? undefined : onPrevPageClick}>
<FormPrevious size={'20px'} style={{ userSelect: "none"}} />
</NavigationItem>
{showPages &&
<NavigationItem disabled={true}>
<Text size={'xsmall'} style={{ cursor: 'default' }}>
Page {formatNumber(+currentPage)} of {formatNumber(+totalPages)}
</Text>
</NavigationItem>
}
<NavigationItem disabled={disableNextBtn} onClick={disableNextBtn ? undefined : onNextPageClick}>
<FormNext size={'20px'} style={{ userSelect: "none" }} />
</NavigationItem>
{showPages &&
<NavigationItem disabled={disableNextBtn} onClick={disableNextBtn ? undefined : onLastPageClick}>
<Text size={'xsmall'}>Last</Text>
</NavigationItem>
}
</Box>
);
}
@ -134,7 +175,7 @@ export function PaginationRecordsPerPage(props: ElementsPerPage) {
return (
<Box direction="row" gap="small" align="center">
<Box style={{ width: "95px" }}>
<Box style={{ width: "105px" }}>
<Select
options={options}
value={limit.toString()}

@ -34,61 +34,12 @@ import { parseHexToText } from "../../web3/parseHex";
import { EventsTab } from "./tabs/events/Events";
import { ToolsTab } from "./tabs/tools";
import { config } from "../../config";
import useQuery from "../../hooks/useQuery";
export function AddressPage() {
const history = useHistory();
const tabParamName = "activeTab=";
const oldTabParamName = "txType=";
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 queryParams = useQuery();
const activeTab = +(queryParams.get('activeTab') || 0);
const [contracts, setContracts] = useState<AddressDetails | null>(null);
const [contractShardId, setContractShardId] = useState<ShardID | null>(null);
@ -102,7 +53,7 @@ export function AddressPage() {
const [inventoryHolders, setInventoryForHolders] = useState<
IUserERC721Assets[]
>([]);
const [activeIndex, setActiveIndex] = useState(+activeTab);
const [activeIndex, setActiveIndex] = useState(activeTab);
const erc20Map = useERC20Pool();
const erc721Map = useERC721Pool();
const erc1155Map = useERC1155Pool();

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

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

@ -1,6 +1,6 @@
import * as blockchain from './blockchain'
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 StakingTransactionQueryField = 'block_number' | 'block_hash' | 'hash'

Loading…
Cancel
Save