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