Filter by address (#212)

* Transactions table: add filter by destination address

* Allow filter by one1 address

* Update styles

* Fixed txs count update
pull/213/head
Artem 2 years ago committed by GitHub
parent f431fe63c7
commit 488ad3d4c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/components/tables/TransactionsTable.tsx
  2. 93
      src/pages/AddressPage/tabs/transactions/ColumnFilter.tsx
  3. 178
      src/pages/AddressPage/tabs/transactions/Transactions.tsx
  4. 35
      src/pages/AddressPage/tabs/transactions/columns/transactions.tsx
  5. 14
      src/theme.ts

@ -122,7 +122,7 @@ function getColumns(props: any) {
),
},
{
property: "timestamp",
property: "timestamp",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ width: '180px' }}>
@ -223,20 +223,16 @@ export function TransactionsTable(props: TransactionTableProps) {
overflow: "auto",
opacity: _IsLoading ? "0.4" : "1",
transition: "0.1s all",
minHeight: "600px",
minHeight: _IsLoading || data.length > 0 ? "600px" : 'unset',
}}
>
{_IsLoading ? (
<Box align={"center"} justify={"center"} flex>
<Spinner size={"large"} />
</Box>
) : !data.length && !_IsLoading ? (
<Box style={{ height: "120px" }} justify="center" align="center">
<Text size="small">{emptyText}</Text>
</Box>
) : (
<TableComponent
alwaysOpenedRowDetails={props.rowDetails ? true : false}
alwaysOpenedRowDetails={!!props.rowDetails}
tableProps={{
className: "g-table-transactions",
style: { width: "100%", minWidth, tableLayout: 'auto' },
@ -272,6 +268,11 @@ export function TransactionsTable(props: TransactionTableProps) {
/>
)}
</Box>
{!_IsLoading && data.length === 0 &&
<Box style={{ height: "120px" }} justify="center" align="center">
<Text size="small">{emptyText}</Text>
</Box>
}
{!hidePagination && (
<Box
direction="row"

@ -0,0 +1,93 @@
import styled from "styled-components";
import {Box, Button, DropButton, Text, TextInput} from "grommet";
import React, {useState} from "react";
import {Filter as FilterIcon} from "grommet-icons/icons";
import {Close} from "grommet-icons";
interface ColumnFilterProps {
initialValue?: string
onApply: (value: string) => void
}
const FilterDropButton = styled(DropButton)`
padding: 4px;
border-radius: 4px;
border: 1px solid #e7ecf7;
font-weight: normal;
&:hover, &:active {
box-shadow: 0 2px 6px rgb(119 131 143 / 35%);
}
`
export const ColumnFilter = (props: ColumnFilterProps) => {
const { initialValue = '', onApply } = props
const [value, setValue] = useState(initialValue)
const [isValueApplied, setValueApplied] = useState(!!initialValue)
const [errorMsg, setErrorMsg] = useState('')
const validateValue = (v: string) => {
if(v.length > 0) {
if(!v.startsWith('0x') && !v.startsWith('one1')) {
return 'Address should start with 0x or one1'
}
if(v.length != 42) {
return 'Address must be 42 characters long'
}
}
return ''
}
const onApplyClicked = () => {
const err = validateValue(value)
setErrorMsg(err)
if(!err) {
onApply(value)
setValueApplied(true)
}
}
const onClearClicked = () => {
if (isValueApplied) {
onApply('')
}
setValue('')
setValueApplied(false)
}
return <FilterDropButton
label={<Box direction={'row'} gap={'8px'}>
<FilterIcon size={'16px'} color={'text'} />
{value && isValueApplied &&
<Box direction={'row'} gap={'8px'} justify={'between'} align={'center'}>
<Text size={'12px'} color={'text'}>{value.slice(0, 5)}...{value.slice(-3)}</Text>
<Close size={'12px'} onClick={onClearClicked} />
</Box>
}
</Box>}
dropContent={
<Box pad="small">
<TextInput
placeholder={'Search by address e.g. 0x..'}
value={value}
size={'xsmall'}
onChange={(e) => setValue(e.target.value)}
onKeyPress={(e) => { if(e.charCode === 13) onApplyClicked() }}
style={{ fontWeight: 'normal' }}
/>
{errorMsg && <Box margin={{ top: 'xsmall' }}>
<Text size={'xsmall'}>{errorMsg}</Text>
</Box>}
<Box direction={'row'} margin={{ top: 'small' }} gap={'8px'}>
<Button primary label={<Box direction={'row'} justify={'between'} align={'center'}>
<FilterIcon size={'12px'} color={'text'} />
<Text color={'text'} size={'12px'}>Filter</Text>
</Box>} onClick={onApplyClicked} />
<Button label={'Clear'} onClick={onClearClicked} style={{ fontSize: '12px' }} />
</Box>
</Box>
}
dropProps={{ margin: { top: '32px' }, round: '4px', background: 'background' }}
/>
}

@ -8,29 +8,45 @@ import {
} from "src/api/client";
import { TransactionsTable } from "src/components/tables/TransactionsTable";
import {
Filter,
Filter, FilterProperty, FilterType,
RelatedTransaction,
RelatedTransactionType, RPCTransactionHarmony
} from "src/types";
import { TRelatedTransaction } from "src/api/client.interface";
import { getAddress, mapBlockchainTxToRelated } from "src/utils";
import {getAddress} from "src/utils";
import { ExportToCsvButton } from "../../../../components/ui/ExportToCsvButton";
import {
hmyv2_getStakingTransactionsCount, hmyv2_getStakingTransactionsHistory,
hmyv2_getTransactionsCount,
hmyv2_getTransactionsHistory
} from "../../../../api/rpc";
import { getColumns, getERC20Columns, getNFTColumns, getStakingColumns } from "./columns";
import useQuery from "../../../../hooks/useQuery";
const internalTxsBlocksFrom = 23000000
const allowedLimits = [10, 25, 50, 100]
const relatedTxMap: Record<RelatedTransactionType, string> = {
transaction: "Transaction",
internal_transaction: "Internal Transaction",
stacking_transaction: "Staking Transaction",
};
const prepareFilter = (type: TRelatedTransaction, filter: Filter) => {
if (type === 'internal_transaction') {
filter.filters = [...filter.filters, {
type: "gte", property: "block_number", value: internalTxsBlocksFrom
}]
}
return {
...filter,
filters: filter.filters.map(item => {
if(item.property === 'to') {
const value = item.value as string
let address = value
if(value.startsWith('one1')) { // convert one1 to 0x before send request to backend
try {
address = getAddress(value as string).basicHex
} catch (e) {}
}
return {
...item,
value: `'${address}'`
}
}
return item
})
}
}
const usePrevious = (value: TRelatedTransaction) => {
const ref = useRef();
@ -50,6 +66,7 @@ export function Transactions(props: {
const queryParams = useQuery();
let limitParam = +(queryParams.get('limit') || localStorage.getItem("tableLimitValue") || allowedLimits[0]);
const offsetParam = +(queryParams.get('offset') || 0)
const toParam = queryParams.get('to') || ''
if (!allowedLimits.includes(limitParam)) {
limitParam = allowedLimits[0]
@ -60,8 +77,9 @@ export function Transactions(props: {
limit: limitParam,
orderBy: "block_number",
orderDirection: "desc",
filters: [{ type: "gte", property: "block_number", value: 0 }],
filters: [],
};
const initFilterState = {
transaction: { ...initFilter },
staking_transaction: { ...initFilter },
@ -70,12 +88,23 @@ export function Transactions(props: {
erc721: { ...initFilter },
erc1155: { ...initFilter },
}
const filterOnLoad = {...initFilterState}
if(toParam) {
filterOnLoad[props.type].filters.push({
type: 'eq' as FilterType,
property: 'to' as FilterProperty,
value: toParam.toLowerCase()
})
}
const initTotalElements = 100
const [cachedTxs, setCachedTxs] = useState<{ [name: string]: RelatedTransaction[]}>({})
const [relatedTrxs, setRelatedTrxs] = useState<RelatedTransaction[]>([]);
const [totalElements, setTotalElements] = useState<number>(initTotalElements)
const [cachedTotalElements, setCachedTotalElements] = useState<{ [name: string]: number}>({})
const [filter, setFilter] = useState<{ [name: string]: Filter }>(initFilterState);
const [filter, setFilter] = useState<{ [name: string]: Filter }>(filterOnLoad);
const [isLoading, setIsLoading] = useState<boolean>(false);
const prevType = usePrevious(props.type);
@ -91,24 +120,13 @@ export function Transactions(props: {
setIsLoading(true)
try {
let txs = []
const txsFilter = {...filter[props.type]}
if (props.type === 'internal_transaction') {
txsFilter.filters = [{ type: "gte", property: "block_number", value: internalTxsBlocksFrom }]
}
if (props.type === 'transaction') {
const pageSize = limit
const pageIndex = Math.floor(offset / limit)
const params = [{ address: id, pageIndex, pageSize }]
txs = await hmyv2_getTransactionsHistory(params)
txs = txs.map(tx => mapBlockchainTxToRelated(tx))
} else {
txs = await getRelatedTransactionsByType([
0,
id,
props.type,
txsFilter,
]);
}
const txsFilter = prepareFilter(props.type, filter[props.type])
txs = await getRelatedTransactionsByType([
0,
id,
props.type,
txsFilter,
]);
// for transactions we display call method if any
if (props.type === "transaction") {
@ -142,6 +160,26 @@ export function Transactions(props: {
}
}
const loadTransactionsCount = async () => {
try {
const countFilter = prepareFilter(props.type, filter[props.type])
const txsCount = await getRelatedTransactionsCountByType([
0,
id,
props.type,
countFilter,
])
setTotalElements(txsCount)
setCachedTotalElements({
...cachedTotalElements,
[props.type]: txsCount
})
} catch (e) {
console.error('Cannot get txs count', (e as Error).message)
setTotalElements(initTotalElements)
}
}
useEffect(() => {
setCachedTxs({})
setCachedTotalElements({})
@ -149,43 +187,13 @@ export function Transactions(props: {
}, [id])
useEffect(() => {
const getTxsCount = async () => {
try {
const countFilter = {...filter[props.type]}
// Note: internal_transactions index from & to supported only for block_number >= internalTxsBlocksFrom
if (props.type === 'internal_transaction') {
countFilter.filters = [{ type: "gte", property: "block_number", value: internalTxsBlocksFrom }]
}
let txsCount
if (props.type ==='transaction') {
txsCount = await hmyv2_getTransactionsCount(id)
} else {
txsCount = await getRelatedTransactionsCountByType([
0,
id,
props.type,
countFilter,
])
}
setTotalElements(txsCount)
setCachedTotalElements({
...cachedTotalElements,
[props.type]: txsCount
})
} catch (e) {
console.error('Cannot get txs count', (e as Error).message)
setTotalElements(initTotalElements)
}
}
const cachedValue = cachedTotalElements[props.type]
if (cachedValue && id === prevId) {
setTotalElements(cachedValue)
} else {
getTxsCount()
loadTransactionsCount()
}
}, [props.type, id])
}, [props.type, id, filter[props.type]])
// Change active tab
useEffect(() => {
@ -213,7 +221,15 @@ export function Transactions(props: {
}
}, [filter[props.type], id]);
// change filter by "to" field
useEffect(() => {
if (prevType === props.type) {
loadTransactionsCount()
}
}, [filter[props.type].filters.length]);
let columns = [];
const filterTo = filter[props.type].filters.find(item => item.property === 'to')
switch (props.type) {
case "staking_transaction": {
@ -231,19 +247,45 @@ export function Transactions(props: {
}
default: {
columns = getColumns(id);
columns = getColumns(id, {
'to': {
value: filterTo ? filterTo.value.toString() : '',
onApply: (value = '') => {
const filters = filter[props.type].filters.filter(item => item.property !== 'to')
if (value) {
filters.push({
type: 'eq' as FilterType,
property: 'to' as FilterProperty,
value: value.toLowerCase()
})
}
onFilterChanged({
...filter[props.type],
filters
})
}
}
});
break;
}
}
const onFilterChanged = (value: Filter) => {
const { offset, limit } = value
const { offset, limit, filters } = 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}`)
let historyUrl = `?activeTab=${activeTab}&offset=${offset}&limit=${limit}`
const filterTo = filters.find(item => item.property === 'to')
if(filterTo) {
historyUrl += `&to=${filterTo.value}`
}
history.push(historyUrl)
}
return (

@ -1,10 +1,21 @@
import { Box, ColumnConfig, Text, Tip } from "grommet";
import {Box, ColumnConfig, Text, Tip} from "grommet";
import React from "react";
import { RelatedTransaction } from "../../../../../types";
import { Address, DateTime, ONEValue, TipContent } from "../../../../../components/ui";
import { TransactionAddress, TransferDirectionMarker, TxMethod } from "./common";
import {ColumnFilter} from "../ColumnFilter";
export function getColumns(id: string): ColumnConfig<any>[] {
interface ColumnFilters {
[property: string]: {
value: string
onApply: (value: string) => void
}
}
export function getColumns(
id: string,
columnFilters?: ColumnFilters
): ColumnConfig<any>[] {
return [
// {
// property: "type",
@ -128,13 +139,19 @@ export function getColumns(id: string): ColumnConfig<any>[] {
{
property: "to",
header: (
<Text
color="minorText"
size="small"
style={{ width: "180px" }}
>
To
</Text>
<Box direction={'row'} justify={'start'} gap={'8px'} align={'center'} style={{ width: "180px" }}>
<Text
color="minorText"
size="small"
>
To
</Text>
{columnFilters && columnFilters['to'] &&
<ColumnFilter
initialValue={columnFilters['to'].value}
onApply={columnFilters['to'].onApply}
/>}
</Box>
),
render: (data: RelatedTransaction) => <TransactionAddress id={id} address={data.to} width={'180px'} />,
},

@ -45,7 +45,7 @@ export const theme = {
backgroundError: "rgba(230, 0, 0, 0.4)",
backgroundSuccess: "rgb(106 250 188 / 44%)",
backgroundToaster: "rgba(0, 174, 233, 0.7)",
backgroundTip: palette.MidnightBlue,
backgroundTip: '#005ca7',
backgroundMark: palette.WhiteBlue,
warning: palette.GoldenBrown,
warningBackground: palette.WhiteBrown,
@ -73,8 +73,16 @@ export const theme = {
},
button: {
// backgroundColor: "transparent",
color: "brand",
borderColor: "brand",
primary: {
color: 'backgroundDropdownItem', // Bug in grommet library, it should be background-color
},
color: "text",
borderColor: "border",
border: {
radius: '4px',
width: '1px',
color: 'border'
}
},
dataTable: {
border: {

Loading…
Cancel
Save