From 7699dd3b4c0754d8e33b7d1831cb89ff495d9fa9 Mon Sep 17 00:00:00 2001 From: Teto Gomez Date: Mon, 3 May 2021 00:25:40 -0600 Subject: [PATCH] fix(FE): use highchart instead of rechart, add card new UI (#442) --- .../src/components/InformationCard/index.js | 205 ++++++++++++++++++ .../src/components/InformationCard/styles.js | 149 +++++++++++++ .../components/ProducerHealthIndicators.js | 20 +- .../src/components/TransactionsLineChart.js | 163 +++----------- webapp/src/routes/BlockProducers.js | 15 +- webapp/src/routes/Home/TransactionInfo.js | 77 ++++--- webapp/src/routes/NetworkInfo.js | 77 +++++-- 7 files changed, 502 insertions(+), 204 deletions(-) create mode 100644 webapp/src/components/InformationCard/index.js create mode 100644 webapp/src/components/InformationCard/styles.js diff --git a/webapp/src/components/InformationCard/index.js b/webapp/src/components/InformationCard/index.js new file mode 100644 index 00000000..fc7af5d0 --- /dev/null +++ b/webapp/src/components/InformationCard/index.js @@ -0,0 +1,205 @@ +import React, { memo, useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { makeStyles, useTheme } from '@material-ui/core/styles' +import Card from '@material-ui/core/Card' +import CardHeader from '@material-ui/core/CardHeader' +import CardActions from '@material-ui/core/CardActions' +import Collapse from '@material-ui/core/Collapse' +import { useTranslation } from 'react-i18next' +import Box from '@material-ui/core/Box' +import Typography from '@material-ui/core/Typography' +import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' +import Link from '@material-ui/core/Link' +import Button from '@material-ui/core/Button' +import useMediaQuery from '@material-ui/core/useMediaQuery' + +import moment from 'moment' +import 'flag-icon-css/css/flag-icon.min.css' + +import { onImgError } from '../../utils' +import { generalConfig } from '../../config' +import CountryFlag from '../CountryFlag' +import ProducerSocialLinks from '../ProducerSocialLinks' +import ProducerHealthIndicators from '../ProducerHealthIndicators' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const InformationCard = ({ producer, rank, onNodeClick }) => { + const classes = useStyles() + const theme = useTheme() + const { t } = useTranslation('producerCardComponent') + + const matches = useMediaQuery(theme.breakpoints.up('lg')) + const [expanded, setExpanded] = useState(false) + const [producerOrg, setProducerOrg] = useState({}) + const [producerNodes, setProducerNodes] = useState([]) + + const handleExpandClick = () => { + setExpanded(!expanded) + } + + useEffect(() => { + setProducerOrg(producer.bp_json?.org || {}) + setProducerNodes(producer.bp_json?.nodes || []) + }, [producer]) + + return ( + + + + + avatar + + {producerOrg.candidate_name || + producerOrg.organization_name || + producer.owner} + + 12letteracco + + + + + Info + + Location:{` ${producerOrg.location?.name || 'N/A'} `} + + + + Website:{' '} + + {producerOrg.website} + + + + Email:{' '} + {producerOrg.email ? ( + + {producerOrg.email} + + ) : ( + 'N/A' + )} + + + Onwership Disclosure:{' '} + {producerOrg.chain_resources ? ( + + {producerOrg.ownership_disclosure} + + ) : ( + 'N/A' + )} + + + Chain Resources:{' '} + {producerOrg.chain_resources ? ( + + {producerOrg.chain_resources} + + ) : ( + 'N/A' + )} + + + + + Stats + Votes: N/A + Rewards: 0 eos + + Last Checked: + {` ${moment(new Date()).diff( + moment(producer.updated_at), + 'seconds' + )} ${t('secondsAgo')}`} + + + Missed Blocks:{' '} + {(producer.missed_blocks || []).reduce( + (result, current) => result + current.value, + 0 + )} + + + + {t('nodes')} + + {producerNodes.length > 0 && ( + <> + {producerNodes.map((node, i) => ( + + {node.node_name || node.node_type}{' '} + + + ))} + + )} + + + + + + Health Social + + + + Social + + + + + + + + + + + + + + + ) +} + +InformationCard.propTypes = { + producer: PropTypes.any, + rank: PropTypes.number, + onNodeClick: PropTypes.func +} + +InformationCard.defaultProps = { + producer: {}, + rank: 0, + onNodeClick: () => {} +} + +export default memo(InformationCard) diff --git a/webapp/src/components/InformationCard/styles.js b/webapp/src/components/InformationCard/styles.js new file mode 100644 index 00000000..b154a4aa --- /dev/null +++ b/webapp/src/components/InformationCard/styles.js @@ -0,0 +1,149 @@ +export default (theme) => ({ + root: { + width: '100%', + marginBottom: theme.spacing(2), + paddingBottom: 0, + [theme.breakpoints.up('sm')]: { + width: 300 + }, + [theme.breakpoints.up('lg')]: { + width: '100%', + paddingBottom: theme.spacing(2) + } + }, + wrapper: { + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(4), + '& .bodyWrapper': { + display: 'flex', + flexDirection: 'column' + }, + [theme.breakpoints.up('lg')]: { + flexDirection: 'row', + '& .bodyWrapper': { + flexDirection: 'row' + } + } + }, + media: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 0, + '& img': { + width: 82, + height: 82, + borderRadius: 40 + }, + '& .bpName': { + fontSize: 28, + lineHeight: '34px', + letterSpacing: '-0.233333px', + marginBottom: theme.spacing(1), + textAlign: 'center' + }, + [theme.breakpoints.up('lg')]: { + padding: theme.spacing(0, 6) + } + }, + expand: { + transform: 'rotate(0deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest + }) + }, + expandOpen: { + transform: 'rotate(180deg)' + }, + expandMore: { + width: '100%', + display: 'flex', + justifyContent: 'center', + '& .MuiButtonBase-root': { + textTransform: 'capitalize' + } + }, + info: { + borderLeft: 'none', + marginBottom: theme.spacing(3), + '& .MuiTypography-body1': { + margin: theme.spacing(1, 0), + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + maxWidth: 350 + }, + [theme.breakpoints.up('lg')]: { + borderLeft: '1px solid rgba(0, 0, 0, 0.2)', + + padding: theme.spacing(0, 2), + marginBottom: 0 + } + }, + twoBoxes: { + marginBottom: theme.spacing(3), + borderLeft: 'none', + display: 'flex', + justifyContent: 'space-between', + '& .MuiTypography-body1': { + margin: theme.spacing(1, 0), + display: 'flex', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden' + }, + '& .nodes': { + borderLeft: 'none', + width: 100, + '& .MuiSvgIcon-root': { + marginLeft: theme.spacing(1), + fontSize: 20 + } + }, + '& .healthStatus': { + '& .MuiSvgIcon-root': { + marginLeft: theme.spacing(1), + fontSize: 20 + } + }, + '& .social': { + borderLeft: 'none', + width: 100, + '& a': { + display: 'flex' + }, + '& svg': { + marginRight: theme.spacing(1) + } + }, + '& .success': { + color: theme.palette.success.main + }, + '& .error': { + color: theme.palette.error.main + }, + '& .warning': { + color: theme.palette.warning.main + }, + [theme.breakpoints.up('lg')]: { + padding: theme.spacing(0, 2), + marginBottom: 0, + borderLeft: '1px solid rgba(0, 0, 0, 0.2)', + + '& .nodes, .social': { + borderLeft: '1px solid rgba(0, 0, 0, 0.2)', + + paddingLeft: theme.spacing(1), + marginRight: theme.spacing(2) + } + } + }, + cardActions: { + display: 'flex', + [theme.breakpoints.up('lg')]: { + display: 'none' + } + } +}) diff --git a/webapp/src/components/ProducerHealthIndicators.js b/webapp/src/components/ProducerHealthIndicators.js index 3695ad9d..c9d04739 100644 --- a/webapp/src/components/ProducerHealthIndicators.js +++ b/webapp/src/components/ProducerHealthIndicators.js @@ -4,24 +4,14 @@ import PropTypes from 'prop-types' import { makeStyles } from '@material-ui/styles' import Tooltip from '@material-ui/core/Tooltip' import { useTranslation } from 'react-i18next' -import WarningIcon from '@material-ui/icons/Warning' - -import DoneAllIcon from '@material-ui/icons/DoneAll' +import DoneOutlinedIcon from '@material-ui/icons/DoneOutlined' +import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined' import { Box, Typography } from '@material-ui/core' const useStyles = makeStyles(() => ({ wrapper: { display: 'flex', - justifyContent: 'space-between' - }, - valid: { - color: 'green' - }, - error: { - color: 'orange' - }, - warning: { - color: 'yellow' + alignItems: 'center' } })) @@ -39,8 +29,8 @@ const ProducerHealthIndicators = ({ producer }) => { > {t(`hs_${item.name}`)} - {item.valid && } - {!item.valid && } + {item.valid && } + {!item.valid && } ))} diff --git a/webapp/src/components/TransactionsLineChart.js b/webapp/src/components/TransactionsLineChart.js index abf9f8d3..fe2c6964 100644 --- a/webapp/src/components/TransactionsLineChart.js +++ b/webapp/src/components/TransactionsLineChart.js @@ -1,152 +1,41 @@ import React from 'react' -import Typography from '@material-ui/core/Typography' -import Box from '@material-ui/core/Box' -import { makeStyles } from '@material-ui/styles' -import { useTheme } from '@material-ui/core/styles' -import Divider from '@material-ui/core/Divider' -import { useTranslation } from 'react-i18next' -import Skeleton from '@material-ui/lab/Skeleton' +import HighchartsReact from 'highcharts-react-official' import PropTypes from 'prop-types' -import { - LineChart, - Line, - YAxis, - Tooltip, - Legend, - ResponsiveContainer -} from 'recharts' +import Highcharts from 'highcharts' +import Box from '@material-ui/core/Box' -const useStyles = makeStyles((theme) => ({ - wrapper: { - backgroundColor: theme.palette.white, - padding: theme.spacing(2), - borderRadius: theme.spacing(1), - boxShadow: theme.shadows[8] - }, - description: { - fontWeight: 'normal' - }, - graphSkeleton: { - width: '100%', - margin: theme.spacing(1), - '& .MuiSkeleton-rect': { - width: '100%', - height: 300 +const LineChart = ({ data, xAxisProps, title }) => { + const options = { + title: { + text: title + }, + chart: { + animation: false, + type: 'spline' }, - '& .MuiBox-root': { - display: 'flex', - justifyContent: 'center', - '& .MuiSkeleton-text': { - margin: theme.spacing(0, 1) - } - } + xAxis: xAxisProps } -})) - -const GraphSkeleton = () => { - const classes = useStyles() return ( - - - - - - + + ) } -const CustomTooltip = ({ active, payload }) => { - const classes = useStyles() - const { t } = useTranslation('transactionsChartComponent') - - if (active && payload && payload?.length > 0) { - return ( - - {t('transactions')} - - {t('perSecond')} - - {t('amount')}:{' '} - {payload[0].payload.tps} - - - {t('blocks')}:{' '} - - {' '} - {payload[0].payload.blocks.tps.join(', ')} - - - {t('perBlock')} - - {t('amount')}:{' '} - {payload[0].payload.tpb} - - - {t('blocks')}:{' '} - - {' '} - {payload[0].payload.blocks.tpb.join(', ')} - - - - ) - } - - return null -} - -CustomTooltip.propTypes = { - active: PropTypes.bool, - payload: PropTypes.array -} - -const TransactionsChart = ({ data, loading }) => { - const theme = useTheme() - const { t } = useTranslation('transactionsChartComponent') - - if (loading) return - - return ( - - - - } /> - - - - - - ) +LineChart.propTypes = { + data: PropTypes.array, + xAxisProps: PropTypes.object, + title: PropTypes.string } -TransactionsChart.propTypes = { - data: PropTypes.array, - loading: PropTypes.bool +LineChart.defaultProps = { + data: [], + xAxisProps: { xAxisVisible: false }, + title: '' } -export default TransactionsChart +export default LineChart diff --git a/webapp/src/routes/BlockProducers.js b/webapp/src/routes/BlockProducers.js index 97299ee6..433135e7 100644 --- a/webapp/src/routes/BlockProducers.js +++ b/webapp/src/routes/BlockProducers.js @@ -11,9 +11,9 @@ import queryString from 'query-string' import { PRODUCERS_QUERY } from '../gql' import ProducerSearch from '../components/ProducerSearch' -import ProducerCard from '../components/ProducerCard' import Tooltip from '../components/Tooltip' import NodeCard from '../components/NodeCard' +import InformationCard from '../components/InformationCard' const useStyles = makeStyles((theme) => ({ searchWrapper: { @@ -122,17 +122,10 @@ const Producers = () => { {loading && } - + {(producers || []).map((producer, index) => ( - - + { useEffect(() => { const majorLength = tps.length > tpb.length ? tps.length : tpb.length - const dataModeled = [] + const trxPerSecond = [] + const trxPerBlock = [] if (!majorLength || pause || option !== '0') return for (let index = 0; index < majorLength; index++) { - dataModeled.push({ - tps: tps[index] ? tps[index].transactions : 0, - tpb: tpb[index] ? tpb[index].transactions : 0, - blocks: { - tps: tps[index] ? tps[index].blocks : [0], - tpb: tpb[index] ? tpb[index].blocks : [0] - } - }) + const labelBlockPS = `Blocks:[${(tps[index] + ? tps[index].blocks + : [''] + ).join()}]` + const labelBlockPB = `Blocks:[${(tpb[index] + ? tpb[index].blocks + : [] + ).join()}]` + + trxPerSecond.push([ + labelBlockPS, + tps[index] ? tps[index].transactions : 0 + ]) + trxPerBlock.push([labelBlockPB, tpb[index] ? tpb[index].transactions : 0]) } - setGraphicData(dataModeled) + setGraphicData([ + { + name: 'Transactions per Second', + color: theme.palette.secondary.main, + data: trxPerSecond + }, + { + name: 'Transactions per Block', + color: '#00C853', + data: trxPerBlock + } + ]) + // eslint-disable-next-line }, [tps, tpb]) useEffect(() => { @@ -74,11 +93,12 @@ const TransactionInfo = ({ t, classes }) => { }, [option, getTransactionHistory]) useEffect(() => { - const dataModeled = [] + const trxPerSecond = [] + const trxPerBlock = [] if (option !== '0') { if (!trxHistory?.block_history?.length) { - setGraphicData(dataModeled) + setGraphicData([]) return } @@ -87,20 +107,26 @@ const TransactionInfo = ({ t, classes }) => { const item = trxHistory.block_history[i] || baseValues const prevItem = trxHistory.block_history[i - 1] || baseValues const trxPer = item.transaction_length + prevItem.transaction_length - const blocks = [item.block_num, prevItem.block_num] - - dataModeled.push({ - tps: trxPer, - tpb: trxPer, - blocks: { - tps: blocks, - tpb: blocks - } - }) + const blocks = `Blocks:[${item.block_num}, ${prevItem.block_num}]` + + trxPerSecond.push([blocks, trxPer]) + trxPerBlock.push([blocks, trxPer]) } - setGraphicData(dataModeled) + setGraphicData([ + { + name: 'Transactions per Second', + color: theme.palette.secondary.main, + data: trxPerSecond + }, + { + name: 'Transactions per Block', + color: '#00C853', + data: trxPerBlock + } + ]) } + // eslint-disable-next-line }, [trxHistoryLoading, trxHistory]) return ( @@ -154,10 +180,7 @@ const TransactionInfo = ({ t, classes }) => { - + diff --git a/webapp/src/routes/NetworkInfo.js b/webapp/src/routes/NetworkInfo.js index f77c6112..fd757eaa 100644 --- a/webapp/src/routes/NetworkInfo.js +++ b/webapp/src/routes/NetworkInfo.js @@ -1,5 +1,5 @@ /* eslint camelcase: 0 */ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { useQuery } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/styles' @@ -8,9 +8,11 @@ import Card from '@material-ui/core/Card' import CardHeader from '@material-ui/core/CardHeader' import CardContent from '@material-ui/core/CardContent' import CardActions from '@material-ui/core/CardActions' +import moment from 'moment' import { useTranslation } from 'react-i18next' -import MultiLineChart from '../components/MultiLineChart' +import TransactionsLineChart from '../components/TransactionsLineChart' + import { NETWORK_STATS } from '../gql' import { generalConfig } from '../config' @@ -34,18 +36,73 @@ const Network = () => { const dispatch = useDispatch() const classes = useStyles() const { t } = useTranslation('networkInfoRoute') + const [missedBlock, setMissedBlock] = useState({}) + const [cpuData, setCpuData] = useState({}) const { data: stats } = useQuery(NETWORK_STATS) + const getDataModeled = (items = [], valueName) => { + if (!items.length) return { data: [], firstDate: null, lastDate: null } + + const itemsSorted = items + .map((block) => ({ + ...block, + value: block[valueName], + time: moment(block.created_at).unix() + })) + .sort((blockA, blockB) => blockA.time - blockB.time) + + const firstDate = itemsSorted[0].time + const lastDate = itemsSorted[itemsSorted.length - 1].time + + const itemsData = itemsSorted.reduce((acc, current) => { + const timeFormat = moment.unix(current.time).format('HH:mm') + + if (!acc.length) { + return [ + { + name: current.account, + data: [[timeFormat, current.value]] + } + ] + } + + const dataIndex = acc.findIndex((item) => item.name === current.account) + + if (dataIndex >= 0) { + acc[dataIndex].data.push([timeFormat, current.value]) + + return acc + } + + return [ + ...acc, + { + name: current.account, + data: [[timeFormat, current.value]] + } + ] + }, []) + + return { data: itemsData, firstDate, lastDate } + } + useEffect(() => { dispatch.eos.startTrackingInfo({ interval: 0 }) - }, [dispatch]) - useEffect(() => { return () => { dispatch.eos.stopTrackingInfo() } }, [dispatch]) + useEffect(() => { + if (stats) { + const { cpu, missed_block } = stats + + setCpuData(getDataModeled(cpu, 'usage')) + setMissedBlock(getDataModeled(missed_block, 'value')) + } + }, [stats]) + return ( {generalConfig.useCpuBenchmark && ( @@ -53,11 +110,7 @@ const Network = () => { - `${value}us`} - /> + @@ -67,11 +120,7 @@ const Network = () => { - `${value} ${t('missedBlocks')}`} - /> +