commit
92ca62c65a
@ -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> |
||||||
|
} |
Loading…
Reference in new issue