Merge pull request #225 from ArtemKolodko/metrics_top

Metrics top
pull/231/head
Artem 2 years ago committed by GitHub
commit 92ca62c65a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Routes.tsx
  2. 6
      src/api/client.ts
  3. 12
      src/components/appHeader/ToolsButton.tsx
  4. 124
      src/pages/TopStatsPage/CommonTopTable.tsx
  5. 52
      src/pages/TopStatsPage/OptionsSelect.tsx
  6. 133
      src/pages/TopStatsPage/Transaction.tsx
  7. 26
      src/pages/TopStatsPage/index.tsx
  8. 23
      src/types/api.ts

@ -17,6 +17,7 @@ import { ApprovalPage } from "./pages/ApprovalPage";
import { CheckHRC } from "./pages/tools/CheckHRC";
import {VerifyProxyContract} from "./pages/VerifyProxyContract/VerifyProxyContract";
import { ChartsPage } from "./pages/ChartsPage";
import { TopStatsPage } from "./pages/TopStatsPage";
export function Routes() {
return (
@ -108,6 +109,10 @@ export function Routes() {
<Route path={'/'}><ChartsPage /></Route>
</Route>
<Route path="/topstat">
<Route path={'/'}><TopStatsPage /></Route>
</Route>
<Route path="*">
<Redirect to="/" />
</Route>

@ -6,7 +6,7 @@ import {
RPCTransactionHarmony,
RelatedTransaction,
Log, LogDetailed, AddressDetails,
ShardID, MetricsType, MetricsDailyItem
ShardID, MetricsType, MetricsDailyItem, MetricsTopType, MetricsTopItem, MetricsTopPeriod
} from "src/types";
import {
IHoldersInfo,
@ -115,6 +115,10 @@ export function getMetricsByType(type: MetricsType, offset = 0, limit = 14) {
return transport("getMetricsByType", [type, offset, limit]) as Promise<MetricsDailyItem[]>;
}
export function getTopMetricsByType(type: MetricsTopType, period: MetricsTopPeriod, limit = 10) {
return transport("getTopMetricsByType", [type, period, limit]) as Promise<MetricsTopItem[]>;
}
export function getContractsByField(params: any[]) {
return transport("getContractsByField", params) as Promise<AddressDetails>;
}

@ -73,6 +73,18 @@ export function ToolsButton() {
<Text size={'xsmall'} color={'successText'}>(new)</Text>
</Box>
</Anchor>
<Anchor
style={{ textDecoration: "underline" }}
onClick={(e) => {
setIsOpen(false);
history.push("/topstat");
}}
>
<Box direction={'row'} align={'center'} gap={'4px'}>
Top Statistics
<Text size={'xsmall'} color={'successText'}>(new)</Text>
</Box>
</Anchor>
</Box>
}
style={{

@ -0,0 +1,124 @@
import React from 'react'
import {Box, Text} from "grommet";
import {MetricsTopItem, MetricsTopType} from "../../types";
import styled from "styled-components";
import {Address} from "../../components/ui";
import {ReactComponent as HarmonyLogo} from '../../assets/Logo.svg';
export interface TopTableProps {
items: MetricsTopItem[]
title: string
columns: string[]
isLoading?: boolean
}
interface TopTableRowProps {
item: MetricsTopItem
}
const TableContainer = styled(Box)`
flex: 0 0 calc(50% - 16px);
margin-left: 8px;
margin-right: 8px;
@media (max-width: 1024px) {
flex: 0 0 calc(100% - 16px);
margin-bottom: 0.75rem;
}
`
const LogoWrapper = styled(Box)`
svg path {
fill: #00AEE9;
}
`
const columnsWidth = ['5%', '65%', '15%', '10%']
const TopTableHeader = (props: { columns: string[] }) => {
const { columns } = props
return <Box
direction={'row'}
gap={'8px'}
pad={'8px'}
border={{ size: '2px', side: 'bottom' }}
style={{ fontWeight: 'bold' }}
>
<Box width={columnsWidth[0]}>
<Text size={'xsmall'}>{columns[0]}</Text>
</Box>
<Box width={columnsWidth[1]}>
<Text size={'xsmall'}>{columns[1]}</Text>
</Box>
<Box width={columnsWidth[2]}>
<Text size={'xsmall'}>{columns[2]}</Text>
</Box>
<Box width={columnsWidth[3]}>
<Text size={'xsmall'}>{columns[3]}</Text>
</Box>
</Box>
}
const TopTableRow = (props: TopTableRowProps) => {
const { item: { type, rank, address, value, share } } = props
const isOneTransfer = [MetricsTopType.topOneSender, MetricsTopType.topOneReceiver].includes(type)
const valueFormat = isOneTransfer
? Math.round(+value / Math.pow(10, 18))
: value
const valueFormatEn = Intl.NumberFormat('en-US', {
notation: "compact",
maximumFractionDigits: 2
}).format(+valueFormat)
const shareFormatEn = Intl.NumberFormat('en-US', {
notation: "compact",
maximumFractionDigits: 2
}).format(+share)
return <Box
direction={'row'}
gap={'8px'}
pad={'8px'}
border={{ size: '1px', side: 'bottom' }}
>
<Box width={columnsWidth[0]}>
<Text size={'small'}>{rank}</Text>
</Box>
<Box width={columnsWidth[1]}>
<Address address={address} hideCopyBtn={true} isShortEllipsis={true} style={{ fontSize: 'small' }} />
</Box>
<Box width={columnsWidth[2]} direction={'row'} align={'center'} gap={'6px'}>
{isOneTransfer && <LogoWrapper>
<HarmonyLogo width={'12px'} height={'12px'} />
</LogoWrapper> }
<Text size={'xsmall'}>{valueFormatEn}</Text>
</Box>
<Box width={columnsWidth[3]}>
<Text size={'xsmall'}>{shareFormatEn}%</Text>
</Box>
</Box>
}
export const TopTable = (props: TopTableProps) => {
return <TableContainer
border={{ size: '1px' }}
round={'8px'}
// overflow={'hidden'}
margin={{ bottom: '16px' }}
style={{ opacity: props.isLoading ? 0.5 : 1 }}
background={'background'}
>
<Box style={{ overflowX: 'auto' }}>
<Box style={{ minWidth: '550px' }}>
<Box pad={'8px'} border={{ size: '1px', side: 'bottom' }} background={'backgroundBackEmpty'}>
<Text size={'small'}>{props.title}</Text>
</Box>
<TopTableHeader columns={props.columns} />
{props.items.map(item => <TopTableRow key={item.address} item={item} />)}
</Box>
</Box>
</TableContainer>
}

@ -0,0 +1,52 @@
import React from 'react'
import {Box, Text} from "grommet";
import {MetricsTopPeriod} from "../../types";
const OptionAlias = {
[MetricsTopPeriod.d1]: '1 Day',
[MetricsTopPeriod.d3]: '3 Days',
[MetricsTopPeriod.d7]: '7 Days',
}
export const ChartOptions = Object.values(MetricsTopPeriod).filter(v => typeof v === 'number') as MetricsTopPeriod[]
const Option = (props: { value: MetricsTopPeriod, isActive: boolean, onSelect: (value: MetricsTopPeriod) => void }) => {
const {value, isActive, onSelect} = props
return <Box
align={'center'}
pad={'8px'}
round={'8px'}
background={isActive ? 'background' : 'unset'}
onClick={() => onSelect(value)}
style={{ cursor: 'pointer', fontWeight: isActive ? 'bold': 'normal' }}
>
<Text size='small'>{OptionAlias[value]}</Text>
</Box>
}
export interface OptionsSelectProps {
activeOption: MetricsTopPeriod
onSelect: (option: MetricsTopPeriod) => void
disabled?: boolean
}
export const OptionsSelect = (props: OptionsSelectProps) => {
const { activeOption, disabled, onSelect } = props
return <Box
direction={'row'}
justify={'between'}
pad={'4px'}
gap={'8px'}
round={'8px'}
background={'backgroundBack'}
border={{ size: '1px' }}
>
{ChartOptions.map(option => <Option
key={option}
value={option}
isActive={activeOption === option}
onSelect={(option) => !disabled ? onSelect(option) : undefined}
/>)}
</Box>
}

@ -0,0 +1,133 @@
import React, {useEffect, useState} from 'react'
import {Box, Spinner, Tip, Text} from "grommet";
import {TopTable} from "./CommonTopTable";
import {getTopMetricsByType} from "../../api/client";
import {MetricsTopItem, MetricsTopPeriod, MetricsTopType} from "../../types";
import {OptionsSelect} from "./OptionsSelect";
import dayjs from "dayjs";
import {TipContent} from "../../components/ui";
const defaultMetricsItem = {
[MetricsTopType.topOneSender]: [] as MetricsTopItem[],
[MetricsTopType.topOneReceiver]: [] as MetricsTopItem[],
[MetricsTopType.topTxsCountSent]: [] as MetricsTopItem[],
[MetricsTopType.topTxsCountReceived]: [] as MetricsTopItem[],
}
const defaultCache = {
[MetricsTopPeriod.d1]: {...defaultMetricsItem},
[MetricsTopPeriod.d3]: {...defaultMetricsItem},
[MetricsTopPeriod.d7]: {...defaultMetricsItem},
}
export const TransactionTopStats = () => {
const [isLoading, setLoading] = useState(false)
const [period, setPeriod] = useState(MetricsTopPeriod.d1)
const [cache, setCache] = useState(defaultCache)
const [oneSenders, setOneSenders] = useState<MetricsTopItem[]>([])
const [oneReceivers, setOneReceivers] = useState<MetricsTopItem[]>([])
const [txsSenders, setTxsSenders] = useState<MetricsTopItem[]>([])
const [txsReceivers, setTxsReceivers] = useState<MetricsTopItem[]>([])
useEffect(() => {
const retrieveMetrics = async (type: MetricsTopType, p: MetricsTopPeriod) => {
const cachedRows = cache[p][type]
if(cachedRows.length > 0) {
return [...cachedRows]
}
// await new Promise(resolve => setTimeout(resolve, 500))
return await getTopMetricsByType(type, p)
}
const loadData = async () => {
try {
setLoading(true)
const rowsOneSent = await retrieveMetrics(MetricsTopType.topOneSender, period)
const rowsOneReceive = await retrieveMetrics(MetricsTopType.topOneReceiver, period)
const rowsTxsSent = await retrieveMetrics(MetricsTopType.topTxsCountSent, period)
const rowsTxsReceived = await retrieveMetrics(MetricsTopType.topTxsCountReceived, period)
setOneSenders(rowsOneSent)
setOneReceivers(rowsOneReceive)
setTxsSenders(rowsTxsSent)
setTxsReceivers(rowsTxsReceived)
const cacheUpdated = {
...cache,
[period]: {
[MetricsTopType.topOneSender]: rowsOneSent,
[MetricsTopType.topOneReceiver]: rowsOneReceive,
[MetricsTopType.topTxsCountSent]: rowsTxsSent,
[MetricsTopType.topTxsCountReceived]: rowsTxsReceived
}
}
setCache(cacheUpdated)
} catch (e) {
console.error('Error on loading top metrics:', (e as Error).message)
} finally {
setLoading(false)
}
}
loadData()
}, [period])
const dateFrom = oneSenders.length > 0 ? dayjs(oneSenders[0].updatedAt).subtract(period, 'day') : ''
const dateTo = oneSenders.length > 0 ? dayjs(oneSenders[0].updatedAt): ''
return <Box gap={'16px'}>
<Box direction={'row'} align={'center'} pad={'8px'} justify={'between'}>
<Box direction={'row'} gap={'24px'} justify={'center'}>
<OptionsSelect
disabled={isLoading}
activeOption={period}
onSelect={(option) => setPeriod(option)}
/>
<Box justify={'center'}>
{isLoading && <Spinner size={'small'} />}
</Box>
</Box>
{!isLoading && dateFrom && dateTo &&
<Box pad={{ right: '4px' }}>
<Tip
dropProps={{ align: { bottom: "top" }}}
content={<TipContent showArrow={true} message={`Last update: ${dateTo.format('DD MMM HH:mm:ss')}`} />}
>
<Text size={'small'}>{dateFrom.format('DD MMM')} - {dateTo.format('DD MMM')}</Text>
</Tip>
</Box>
}
</Box>
<Box
wrap
direction={'row'}
justify={'start'}
align={'center'}
>
<TopTable
items={oneSenders}
title={'Top ONE Senders'}
columns={['Rank', 'Address', 'Total ONE', 'Percentage']}
isLoading={isLoading}
/>
<TopTable
items={oneReceivers}
title={'Top ONE Receivers'}
columns={['Rank', 'Address', 'Total ONE', 'Percentage']}
isLoading={isLoading}
/>
<TopTable
items={txsSenders}
title={'Top Txs Count Sent'}
columns={['Rank', 'Address', 'Total Txs', 'Percentage']}
isLoading={isLoading}
/>
<TopTable
items={txsReceivers}
title={'Top Txs Count Received'}
columns={['Rank', 'Address', 'Total Txs', 'Percentage']}
isLoading={isLoading}
/>
</Box>
</Box>
}

@ -0,0 +1,26 @@
import React from 'react'
import {Box, Heading, Text} from "grommet";
import {BaseContainer, BasePage} from "../../components/ui";
import {TransactionTopStats} from "./Transaction";
export const TopStatsPage = () => {
return <BaseContainer pad={{ horizontal: "0" }}>
<Heading size="xsmall" margin={{ bottom: "medium", top: "0" }}>
<Box direction={"row"}>Top Statistics</Box>
</Heading>
<BasePage pad={'0'} style={{overflow: 'inherit'}}>
<Box border={{ side: 'bottom' }} pad={"small"}>
<Text weight={'bold'}>Transactions</Text>
</Box>
<Box
wrap
direction={'row'}
pad={"small"}
justify={'start'}
align={'center'}
>
<TransactionTopStats />
</Box>
</BasePage>
</BaseContainer>
}

@ -57,3 +57,26 @@ export enum MetricsType {
averageFee = 'average_fee',
blockSize = 'block_size'
}
export enum MetricsTopType {
topOneSender = 'top_one_sender',
topOneReceiver = 'top_one_receiver',
topTxsCountSent = 'top_txs_count_sent',
topTxsCountReceived = 'top_txs_count_received',
}
export interface MetricsTopItem {
type: MetricsTopType
address: string
period: number
rank: number
share: number
value: string
updatedAt: string
}
export enum MetricsTopPeriod {
d1 = 1,
d3 = 3,
d7 = 7
}

Loading…
Cancel
Save