From 617eea7e07ab69dfac1bc8adb839652ebd2ccb9f Mon Sep 17 00:00:00 2001 From: Jaco Date: Fri, 23 Dec 2022 14:18:36 +0200 Subject: [PATCH] Add ErrorBoundary to charts (#8613) --- packages/page-staking/src/Query/Chart.tsx | 19 +++++---- .../page-staking/src/Query/ChartPoints.tsx | 36 ++++++++--------- .../page-staking/src/Query/ChartPrefs.tsx | 35 ++++++++--------- .../page-staking/src/Query/ChartRewards.tsx | 39 +++++++++---------- .../page-staking/src/Query/ChartStake.tsx | 39 +++++++++---------- packages/page-staking/src/Query/Validator.tsx | 22 ++++++++--- packages/page-staking/src/Query/index.tsx | 35 +++++++++++++---- packages/page-staking/src/Query/types.ts | 1 + packages/react-components/src/Chart/Line.tsx | 15 ++++--- 9 files changed, 137 insertions(+), 104 deletions(-) diff --git a/packages/page-staking/src/Query/Chart.tsx b/packages/page-staking/src/Query/Chart.tsx index 8fe38f6494..880410e9c3 100644 --- a/packages/page-staking/src/Query/Chart.tsx +++ b/packages/page-staking/src/Query/Chart.tsx @@ -1,7 +1,7 @@ // Copyright 2017-2022 @polkadot/app-staking authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ChartInfo } from './types'; +import type { LineData } from './types'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -9,16 +9,17 @@ import styled from 'styled-components'; import { Chart, Spinner } from '@polkadot/react-components'; interface Props { - chart: ChartInfo; className?: string; colors: (string | undefined)[]; header: string; + labels: string[]; legends: string[]; + values: LineData; } -function ChartDisplay ({ chart: { labels, values }, className = '', colors, header, legends }: Props): React.ReactElement { +function ChartDisplay ({ className = '', colors, header, labels, legends, values }: Props): React.ReactElement { const isLoading = useMemo( - () => labels.length === 0 || values.length === 0 || !values[0] || values[0].length !== labels.length, + () => !labels || labels.length === 0 || !values || values.length === 0 || !values[0] || !values[0].length, [labels, values] ); @@ -31,17 +32,19 @@ function ChartDisplay ({ chart: { labels, values }, className = '', colors, head legends={legends} values={values} /> - {isLoading && } + {isLoading && ( + + )} ); } export default React.memo(styled(ChartDisplay)` - position: relative; - &.isLoading { + position: relative; + canvas, h1 { - opacity: 0.5; + opacity: 0.25; } .ui--Spinner { diff --git a/packages/page-staking/src/Query/ChartPoints.tsx b/packages/page-staking/src/Query/ChartPoints.tsx index 60d0eaffee..2eb11e623f 100644 --- a/packages/page-staking/src/Query/ChartPoints.tsx +++ b/packages/page-staking/src/Query/ChartPoints.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveStakerPoints } from '@polkadot/api-derive/types'; -import type { ChartInfo, LineDataEntry, Props } from './types'; +import type { LineData, Props } from './types'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -13,10 +13,9 @@ import Chart from './Chart'; const COLORS_POINTS = [undefined, '#acacac']; -function extractPoints (points: DeriveStakerPoints[] = []): ChartInfo { - const labels: string[] = []; - const avgSet: LineDataEntry = []; - const idxSet: LineDataEntry = []; +function extractPoints (labels: string[], points: DeriveStakerPoints[]): LineData { + const avgSet = new Array(labels.length); + const idxSet = new Array(labels.length); const [total, avgCount] = points.reduce(([total, avgCount], { points }) => { if (points.gtn(0)) { total += points.toNumber(); @@ -30,33 +29,32 @@ function extractPoints (points: DeriveStakerPoints[] = []): ChartInfo { const avg = avgCount > 0 ? Math.ceil(total * 100 / avgCount) / 100 : 0; + const index = labels.indexOf(era.toHuman()); - labels.push(era.toHuman()); - avgSet.push(avg); - idxSet.push(points); + if (index !== -1) { + avgSet[index] = avg; + idxSet[index] = points.toNumber(); + } }); - return { - labels, - values: [idxSet, avgSet] - }; + return [idxSet, avgSet]; } -function ChartPoints ({ validatorId }: Props): React.ReactElement { +function ChartPoints ({ labels, validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const params = useMemo(() => [validatorId, false], [validatorId]); const stakerPoints = useCall(api.derive.staking.stakerPoints, params); - const [chart, setChart] = useState({ labels: [], values: [] }); + const [values, setValues] = useState([]); useEffect( - () => setChart({ labels: [], values: [] }), + () => setValues([]), [validatorId] ); useEffect( - () => setChart(extractPoints(stakerPoints)), - [stakerPoints] + () => stakerPoints && setValues(extractPoints(labels, stakerPoints)), + [labels, stakerPoints] ); const legendsRef = useRef([ @@ -66,11 +64,11 @@ function ChartPoints ({ validatorId }: Props): React.ReactElement { return ( ('era points')} + labels={labels} legends={legendsRef.current} - values={chart} + values={values} /> ); } diff --git a/packages/page-staking/src/Query/ChartPrefs.tsx b/packages/page-staking/src/Query/ChartPrefs.tsx index 8d34aeee36..3ee5f51f1f 100644 --- a/packages/page-staking/src/Query/ChartPrefs.tsx +++ b/packages/page-staking/src/Query/ChartPrefs.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveStakerPrefs } from '@polkadot/api-derive/types'; -import type { ChartInfo, LineDataEntry, Props } from './types'; +import type { LineData, Props } from './types'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -15,10 +15,9 @@ import Chart from './Chart'; const MULT = new BN(100 * 100); const COLORS_POINTS = [undefined, '#acacac']; -function extractPrefs (prefs: DeriveStakerPrefs[] = []): ChartInfo { - const labels: string[] = []; - const avgSet: LineDataEntry = []; - const idxSet: LineDataEntry = []; +function extractPrefs (labels: string[], prefs: DeriveStakerPrefs[]): LineData { + const avgSet = new Array(labels.length); + const idxSet = new Array(labels.length); const [total, avgCount] = prefs.reduce(([total, avgCount], { validatorPrefs }) => { const comm = validatorPrefs.commission.unwrap().mul(MULT).div(BN_BILLION).toNumber() / 100; @@ -35,33 +34,32 @@ function extractPrefs (prefs: DeriveStakerPrefs[] = []): ChartInfo { const avg = avgCount > 0 ? Math.ceil(total * 100 / avgCount) / 100 : 0; + const index = labels.indexOf(era.toHuman()); - labels.push(era.toHuman()); - avgSet.push(avg); - idxSet.push(comm); + if (index !== -1) { + avgSet[index] = avg; + idxSet[index] = comm; + } }); - return { - labels, - values: [idxSet, avgSet] - }; + return [idxSet, avgSet]; } -function ChartPrefs ({ validatorId }: Props): React.ReactElement { +function ChartPrefs ({ labels, validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const params = useMemo(() => [validatorId, false], [validatorId]); const stakerPrefs = useCall(api.derive.staking.stakerPrefs, params); - const [chart, setChart] = useState({ labels: [], values: [] }); + const [values, setValues] = useState([]); useEffect( - () => setChart({ labels: [], values: [] }), + () => setValues([]), [validatorId] ); useEffect( - () => setChart(extractPrefs(stakerPrefs)), - [stakerPrefs] + () => stakerPrefs && setValues(extractPrefs(labels, stakerPrefs)), + [labels, stakerPrefs] ); const legendsRef = useRef([ @@ -71,10 +69,11 @@ function ChartPrefs ({ validatorId }: Props): React.ReactElement { return ( ('commission')} + labels={labels} legends={legendsRef.current} + values={values} /> ); } diff --git a/packages/page-staking/src/Query/ChartRewards.tsx b/packages/page-staking/src/Query/ChartRewards.tsx index aa00dda5d3..0136b5f7b7 100644 --- a/packages/page-staking/src/Query/ChartRewards.tsx +++ b/packages/page-staking/src/Query/ChartRewards.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveEraRewards, DeriveOwnSlashes, DeriveStakerPoints } from '@polkadot/api-derive/types'; -import type { ChartInfo, LineDataEntry, Props } from './types'; +import type { LineData, Props } from './types'; import React, { useEffect, useMemo, useState } from 'react'; @@ -15,11 +15,10 @@ import { balanceToNumber } from './util'; const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac']; -function extractRewards (erasRewards: DeriveEraRewards[] = [], ownSlashes: DeriveOwnSlashes[] = [], allPoints: DeriveStakerPoints[] = [], divisor: BN): ChartInfo { - const labels: string[] = []; - const slashSet: LineDataEntry = []; - const rewardSet: LineDataEntry = []; - const avgSet: LineDataEntry = []; +function extractRewards (labels: string[], erasRewards: DeriveEraRewards[], ownSlashes: DeriveOwnSlashes[], allPoints: DeriveStakerPoints[], divisor: BN): LineData { + const slashSet = new Array(labels.length); + const rewardSet = new Array(labels.length); + const avgSet = new Array(labels.length); const [total, avgCount] = erasRewards.reduce(([total, avgCount], { era, eraReward }) => { const points = allPoints.find((points) => points.era.eq(era)); const reward = points?.eraPoints.gtn(0) @@ -46,27 +45,26 @@ function extractRewards (erasRewards: DeriveEraRewards[] = [], ownSlashes: Deriv const avg = avgCount > 0 ? Math.ceil(total * 100 / avgCount) / 100 : 0; + const index = labels.indexOf(era.toHuman()); - labels.push(era.toHuman()); - rewardSet.push(reward); - avgSet.push(avg); - slashSet.push(slash); + if (index !== -1) { + rewardSet[index] = reward; + avgSet[index] = avg; + slashSet[index] = slash; + } }); - return { - labels, - values: [slashSet, rewardSet, avgSet] - }; + return [slashSet, rewardSet, avgSet]; } -function ChartRewards ({ validatorId }: Props): React.ReactElement { +function ChartRewards ({ labels, validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const params = useMemo(() => [validatorId, false], [validatorId]); const ownSlashes = useCall(api.derive.staking.ownSlashes, params); const erasRewards = useCall(api.derive.staking.erasRewards); const stakerPoints = useCall(api.derive.staking.stakerPoints, params); - const [chart, setChart] = useState({ labels: [], values: [] }); + const [values, setValues] = useState([]); const { currency, divisor } = useMemo( () => ({ @@ -77,13 +75,13 @@ function ChartRewards ({ validatorId }: Props): React.ReactElement { ); useEffect( - () => setChart({ labels: [], values: [] }), + () => setValues([]), [validatorId] ); useEffect( - () => setChart(extractRewards(erasRewards, ownSlashes, stakerPoints, divisor)), - [divisor, erasRewards, ownSlashes, stakerPoints] + () => erasRewards && ownSlashes && stakerPoints && setValues(extractRewards(labels, erasRewards, ownSlashes, stakerPoints, divisor)), + [labels, divisor, erasRewards, ownSlashes, stakerPoints] ); const legends = useMemo(() => [ @@ -94,10 +92,11 @@ function ChartRewards ({ validatorId }: Props): React.ReactElement { return ( ('rewards & slashes')} + labels={labels} legends={legends} + values={values} /> ); } diff --git a/packages/page-staking/src/Query/ChartStake.tsx b/packages/page-staking/src/Query/ChartStake.tsx index a186f8c603..d6315926d0 100644 --- a/packages/page-staking/src/Query/ChartStake.tsx +++ b/packages/page-staking/src/Query/ChartStake.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { DeriveOwnExposure } from '@polkadot/api-derive/types'; -import type { ChartInfo, LineDataEntry, Props } from './types'; +import type { LineData, Props } from './types'; import React, { useEffect, useMemo, useState } from 'react'; @@ -15,11 +15,10 @@ import { balanceToNumber } from './util'; const COLORS_STAKE = [undefined, '#8c2200', '#acacac']; -function extractStake (exposures: DeriveOwnExposure[] = [], divisor: BN): ChartInfo { - const labels: string[] = []; - const cliSet: LineDataEntry = []; - const expSet: LineDataEntry = []; - const avgSet: LineDataEntry = []; +function extractStake (labels: string[], exposures: DeriveOwnExposure[], divisor: BN): LineData { + const cliSet = new Array(labels.length); + const expSet = new Array(labels.length); + const avgSet = new Array(labels.length); const [total, avgCount] = exposures.reduce(([total, avgCount], { clipped }) => { const cli = balanceToNumber(clipped.total?.unwrap(), divisor); @@ -38,25 +37,24 @@ function extractStake (exposures: DeriveOwnExposure[] = [], divisor: BN): ChartI const avg = avgCount > 0 ? Math.ceil(total * 100 / avgCount) / 100 : 0; + const index = labels.indexOf(era.toHuman()); - labels.push(era.toHuman()); - avgSet.push(avg); - cliSet.push(cli); - expSet.push(exp); + if (index !== -1) { + avgSet[index] = avg; + cliSet[index] = cli; + expSet[index] = exp; + } }); - return { - labels, - values: [cliSet, expSet, avgSet] - }; + return [cliSet, expSet, avgSet]; } -function ChartStake ({ validatorId }: Props): React.ReactElement { +function ChartStake ({ labels, validatorId }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const params = useMemo(() => [validatorId, false], [validatorId]); const ownExposures = useCall(api.derive.staking.ownExposures, params); - const [chart, setChart] = useState({ labels: [], values: [] }); + const [values, setValues] = useState([]); const { currency, divisor } = useMemo( () => ({ @@ -67,13 +65,13 @@ function ChartStake ({ validatorId }: Props): React.ReactElement { ); useEffect( - () => setChart({ labels: [], values: [] }), + () => setValues([]), [validatorId] ); useEffect( - () => setChart(extractStake(ownExposures, divisor)), - [divisor, ownExposures] + () => ownExposures && setValues(extractStake(labels, ownExposures, divisor)), + [labels, divisor, ownExposures] ); const legends = useMemo(() => [ @@ -84,10 +82,11 @@ function ChartStake ({ validatorId }: Props): React.ReactElement { return ( ('elected stake')} + labels={labels} legends={legends} + values={values} /> ); } diff --git a/packages/page-staking/src/Query/Validator.tsx b/packages/page-staking/src/Query/Validator.tsx index 973e067172..55cfbf1b5f 100644 --- a/packages/page-staking/src/Query/Validator.tsx +++ b/packages/page-staking/src/Query/Validator.tsx @@ -13,16 +13,28 @@ import ChartPrefs from './ChartPrefs'; import ChartRewards from './ChartRewards'; import ChartStake from './ChartStake'; -function Validator ({ className = '', validatorId }: Props): React.ReactElement { +function Validator ({ className = '', labels, validatorId }: Props): React.ReactElement | null { return ( - - + + - - + + ); diff --git a/packages/page-staking/src/Query/index.tsx b/packages/page-staking/src/Query/index.tsx index ab34aee604..0e0cd46299 100644 --- a/packages/page-staking/src/Query/index.tsx +++ b/packages/page-staking/src/Query/index.tsx @@ -1,10 +1,13 @@ // Copyright 2017-2022 @polkadot/app-staking authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useCallback, useState } from 'react'; +import type { INumber } from '@polkadot/types/types'; + +import React, { useCallback, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { Button, InputAddressSimple } from '@polkadot/react-components'; +import { Button, InputAddressSimple, Spinner } from '@polkadot/react-components'; +import { useApi, useCall } from '@polkadot/react-hooks'; import { useTranslation } from '../translate'; import Validator from './Validator'; @@ -13,20 +16,33 @@ interface Props { className?: string; } +function doQuery (validatorId?: string | null): void { + if (validatorId) { + window.location.hash = `/staking/query/${validatorId}`; + } +} + function Query ({ className }: Props): React.ReactElement { const { t } = useTranslation(); + const { api } = useApi(); const { value } = useParams<{ value: string }>(); const [validatorId, setValidatorId] = useState(value || null); + const eras = useCall(api.derive.staking.erasHistoric); + + const labels = useMemo( + () => eras && eras.map((e) => e.toHuman() as string), + [eras] + ); const _onQuery = useCallback( - (): void => { - if (validatorId) { - window.location.hash = `/staking/query/${validatorId}`; - } - }, + () => doQuery(validatorId), [validatorId] ); + if (!labels) { + return ; + } + return (
{ /> {value && ( - + )}
); diff --git a/packages/page-staking/src/Query/types.ts b/packages/page-staking/src/Query/types.ts index 6e478d60a3..a31549e5b5 100644 --- a/packages/page-staking/src/Query/types.ts +++ b/packages/page-staking/src/Query/types.ts @@ -5,6 +5,7 @@ import type { BN } from '@polkadot/util'; export interface Props { className?: string; + labels: string[]; validatorId: string; } diff --git a/packages/react-components/src/Chart/Line.tsx b/packages/react-components/src/Chart/Line.tsx index d9d62a2df3..134cd18360 100644 --- a/packages/react-components/src/Chart/Line.tsx +++ b/packages/react-components/src/Chart/Line.tsx @@ -10,6 +10,7 @@ import * as Chart from 'react-chartjs-2'; import { isBn, objectSpread } from '@polkadot/util'; +import ErrorBoundary from '../ErrorBoundary'; import { alphaColor } from './utils'; interface State { @@ -126,12 +127,14 @@ function LineChart ({ className, colors, labels, legends, options, values }: Lin return (
- + + +
); }