commit
5966a5b6fe
@ -0,0 +1,129 @@ |
|||||||
|
import {Box, Heading, Spinner, Text} from "grommet"; |
||||||
|
import {BaseContainer, BasePage} from "../../components/ui"; |
||||||
|
import styled from "styled-components"; |
||||||
|
import {Link} from "react-router-dom"; |
||||||
|
import {useMediaQuery} from "react-responsive"; |
||||||
|
import { Alert, Info } from 'grommet-icons'; |
||||||
|
|
||||||
|
const TextLink = styled(Text)` |
||||||
|
cursor: pointer; |
||||||
|
text-decoration: underline; |
||||||
|
` |
||||||
|
|
||||||
|
const ChartModalContainer = styled(Box)` |
||||||
|
` |
||||||
|
|
||||||
|
const LoadingErrorModal = () => { |
||||||
|
return <ChartModalContainer |
||||||
|
justify={'center'} |
||||||
|
gap={'16px'} |
||||||
|
pad={'16px'} |
||||||
|
align={'center'} |
||||||
|
background={'warningBackground'} |
||||||
|
round={'8px'} |
||||||
|
border={{ size: '1px' }} |
||||||
|
style={{ zIndex: 1 }} |
||||||
|
> |
||||||
|
<Alert size={'medium'} /> |
||||||
|
<Text>Error on loading data</Text> |
||||||
|
<Text size={'small'}>Please try again later</Text> |
||||||
|
</ChartModalContainer> |
||||||
|
} |
||||||
|
|
||||||
|
const parseKeyValue = (key : string) => { |
||||||
|
return key.replace(/([A-Z])/g, ' $1').trim().split(' ') |
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
||||||
|
.join(' '); |
||||||
|
} |
||||||
|
|
||||||
|
export interface StatProps { |
||||||
|
title: string |
||||||
|
description?: string |
||||||
|
items: any[] |
||||||
|
keys: string[] |
||||||
|
infoLeft: string |
||||||
|
infoRight?: string |
||||||
|
isLoading: boolean |
||||||
|
loadingError?: string |
||||||
|
} |
||||||
|
|
||||||
|
export const StatPage = (props: StatProps) => { |
||||||
|
const { isLoading, loadingError } = props |
||||||
|
|
||||||
|
const rows = [] |
||||||
|
for (let i = 0; i < props.items.length; i++) { |
||||||
|
rows.push( |
||||||
|
<Box
|
||||||
|
key={String(i)}
|
||||||
|
width={{width: '40%'}}
|
||||||
|
margin={{bottom: '8px'}}
|
||||||
|
border={{ size: '1px' }}
|
||||||
|
round={'8px'}
|
||||||
|
overflow={'hidden'} |
||||||
|
> |
||||||
|
<Box align='center' pad={'8px'} background={'backgroundDropdownItem'} > |
||||||
|
<Text color={'brand'}>Shard {i}</Text> |
||||||
|
</Box> |
||||||
|
<Box align='center' pad={'8px'}> |
||||||
|
{props.keys.map((key, _) => { |
||||||
|
return <Text>{parseKeyValue(key)}: {props.items[i][key]}</Text> |
||||||
|
})} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const isMobile = useMediaQuery({ query: '(max-width: 868px)' }) |
||||||
|
|
||||||
|
return <BaseContainer pad={{ horizontal: "0" }}> |
||||||
|
<Heading size="20px" margin={{ bottom: "medium", top: "0" }} style={{ maxWidth: 'unset' }}> |
||||||
|
<Box direction={'row'} justify={'between'} align={'center'}> |
||||||
|
<Box direction={"row"} gap={'8px'} align={'center'}> |
||||||
|
<Box> |
||||||
|
<Link to={'/charts'}><TextLink color={'brand'}>Charts</TextLink></Link> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<Text style={{ opacity: 0.4 }}>/</Text> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
{props.title} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
{!isMobile && props.description && |
||||||
|
<Box align={'end'}> |
||||||
|
<Text size={'xsmall'} weight={'normal'}>{props.description}</Text> |
||||||
|
</Box> |
||||||
|
} |
||||||
|
</Box> |
||||||
|
</Heading> |
||||||
|
<BasePage pad={"small"}> |
||||||
|
<Box direction={'row'} justify={'between'} flex={'grow'} wrap={true}> |
||||||
|
<Box direction={'row'} gap={'8px'} justify={'center'} align={'center'} style={{ flexGrow: 2 }}> |
||||||
|
<Info size={'small'} /> |
||||||
|
<Text size={'small'}> {props.infoLeft} </Text> |
||||||
|
</Box> |
||||||
|
{props.infoRight && <Box direction={'row'} gap={'8px'} justify={'center'} align={'center'} style={{ flexGrow: 2 }}> |
||||||
|
<Info size={'small'} /> |
||||||
|
<Text size={'small'}> {props.infoRight} </Text> |
||||||
|
</Box>} |
||||||
|
</Box> |
||||||
|
</BasePage> |
||||||
|
<BasePage pad={"small"} style={{overflow: 'inherit', marginTop: '16px'}}> |
||||||
|
<Box |
||||||
|
width={'100%'} |
||||||
|
height='280px'
|
||||||
|
direction={'row'}
|
||||||
|
justify={'evenly'}
|
||||||
|
align={'center'}
|
||||||
|
wrap={true} |
||||||
|
> |
||||||
|
{isLoading && <ChartModalContainer justify={'center'} gap={'16px'} align={'center'}> |
||||||
|
<Spinner size={'medium'} /> |
||||||
|
<Text>Loading Data</Text> |
||||||
|
</ChartModalContainer>} |
||||||
|
{!isLoading && loadingError && <LoadingErrorModal />} |
||||||
|
{!isLoading && !loadingError && rows} |
||||||
|
</Box> |
||||||
|
</BasePage> |
||||||
|
</BaseContainer> |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import {useEffect, useState} from 'react' |
||||||
|
import { StatPage } from './StatPage'; |
||||||
|
import { hmyv2_getNodeMetadata } from 'src/api/rpc'; |
||||||
|
import { config } from 'src/config'; |
||||||
|
|
||||||
|
const CONSENSUS = "consensus" |
||||||
|
const BLOCK_NUM = "blocknum" |
||||||
|
const VIEW_ID = "viewId" |
||||||
|
|
||||||
|
export const ViewChangeStats = () => { |
||||||
|
const { availableShards } = config |
||||||
|
const [items, setItems] = useState<any[]>([]) |
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false); |
||||||
|
const [loadingError, setLoadingError] = useState('') |
||||||
|
const [totalViewChange, setTotalViewChange] = useState(0) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const loadData = async () => { |
||||||
|
try { |
||||||
|
setIsLoading(true)
|
||||||
|
for (let i in availableShards) { |
||||||
|
const nodeMetadata = await hmyv2_getNodeMetadata(i) |
||||||
|
const blockHeight = nodeMetadata[CONSENSUS][BLOCK_NUM] |
||||||
|
const viewId = nodeMetadata[CONSENSUS][VIEW_ID] |
||||||
|
const viewChange = viewId - blockHeight |
||||||
|
setItems(items => [...items, { |
||||||
|
viewChange: viewChange, |
||||||
|
viewId: viewId, |
||||||
|
blockHeight: blockHeight, |
||||||
|
}]) |
||||||
|
setTotalViewChange(totalViewChange => totalViewChange + viewChange) |
||||||
|
}
|
||||||
|
} catch (e) { |
||||||
|
console.error('Error on loading metrics:', e) |
||||||
|
setLoadingError('Loading error') |
||||||
|
} finally { |
||||||
|
setIsLoading(false) |
||||||
|
} |
||||||
|
} |
||||||
|
loadData() |
||||||
|
}, []) |
||||||
|
|
||||||
|
const viewChangeProps = { |
||||||
|
title: 'Harmony View Change Statistics', |
||||||
|
description: '', |
||||||
|
items: items, |
||||||
|
keys: ['viewChange', 'viewId', 'blockHeight'], |
||||||
|
isLoading, |
||||||
|
loadingError, |
||||||
|
infoLeft: `Total of ${totalViewChange} view changes across ${availableShards.length} shards`, |
||||||
|
infoRight: 'Data is obtained from the explorer node, which may result in a slight delay' |
||||||
|
} |
||||||
|
|
||||||
|
return <StatPage {...viewChangeProps} /> |
||||||
|
} |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 153 KiB |
Loading…
Reference in new issue