Add ErrorBoundary to charts (#8613)

This commit is contained in:
Jaco 2022-12-23 14:18:36 +02:00 committed by GitHub
parent d3dd0ccbac
commit 617eea7e07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 104 deletions

View File

@ -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<Props> {
function ChartDisplay ({ className = '', colors, header, labels, legends, values }: Props): React.ReactElement<Props> {
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 && <Spinner />}
{isLoading && (
<Spinner />
)}
</div>
);
}
export default React.memo(styled(ChartDisplay)`
position: relative;
&.isLoading {
position: relative;
canvas, h1 {
opacity: 0.5;
opacity: 0.25;
}
.ui--Spinner {

View File

@ -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<number>(labels.length);
const idxSet = new Array<number>(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<Props> {
function ChartPoints ({ labels, validatorId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const params = useMemo(() => [validatorId, false], [validatorId]);
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints, params);
const [chart, setChart] = useState<ChartInfo>({ labels: [], values: [] });
const [values, setValues] = useState<LineData>([]);
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<Props> {
return (
<Chart
chart={chart}
colors={COLORS_POINTS}
header={t<string>('era points')}
labels={labels}
legends={legendsRef.current}
values={chart}
values={values}
/>
);
}

View File

@ -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<number>(labels.length);
const idxSet = new Array<number>(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<Props> {
function ChartPrefs ({ labels, validatorId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const params = useMemo(() => [validatorId, false], [validatorId]);
const stakerPrefs = useCall<DeriveStakerPrefs[]>(api.derive.staking.stakerPrefs, params);
const [chart, setChart] = useState<ChartInfo>({ labels: [], values: [] });
const [values, setValues] = useState<LineData>([]);
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<Props> {
return (
<Chart
chart={chart}
colors={COLORS_POINTS}
header={t<string>('commission')}
labels={labels}
legends={legendsRef.current}
values={values}
/>
);
}

View File

@ -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<number>(labels.length);
const rewardSet = new Array<number>(labels.length);
const avgSet = new Array<number>(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<Props> {
function ChartRewards ({ labels, validatorId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const params = useMemo(() => [validatorId, false], [validatorId]);
const ownSlashes = useCall<DeriveOwnSlashes[]>(api.derive.staking.ownSlashes, params);
const erasRewards = useCall<DeriveEraRewards[]>(api.derive.staking.erasRewards);
const stakerPoints = useCall<DeriveStakerPoints[]>(api.derive.staking.stakerPoints, params);
const [chart, setChart] = useState<ChartInfo>({ labels: [], values: [] });
const [values, setValues] = useState<LineData>([]);
const { currency, divisor } = useMemo(
() => ({
@ -77,13 +75,13 @@ function ChartRewards ({ validatorId }: Props): React.ReactElement<Props> {
);
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<Props> {
return (
<Chart
chart={chart}
colors={COLORS_REWARD}
header={t<string>('rewards & slashes')}
labels={labels}
legends={legends}
values={values}
/>
);
}

View File

@ -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<number>(labels.length);
const expSet = new Array<number>(labels.length);
const avgSet = new Array<number>(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<Props> {
function ChartStake ({ labels, validatorId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const params = useMemo(() => [validatorId, false], [validatorId]);
const ownExposures = useCall<DeriveOwnExposure[]>(api.derive.staking.ownExposures, params);
const [chart, setChart] = useState<ChartInfo>({ labels: [], values: [] });
const [values, setValues] = useState<LineData>([]);
const { currency, divisor } = useMemo(
() => ({
@ -67,13 +65,13 @@ function ChartStake ({ validatorId }: Props): React.ReactElement<Props> {
);
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<Props> {
return (
<Chart
chart={chart}
colors={COLORS_STAKE}
header={t<string>('elected stake')}
labels={labels}
legends={legends}
values={values}
/>
);
}

View File

@ -13,16 +13,28 @@ import ChartPrefs from './ChartPrefs';
import ChartRewards from './ChartRewards';
import ChartStake from './ChartStake';
function Validator ({ className = '', validatorId }: Props): React.ReactElement<Props> {
function Validator ({ className = '', labels, validatorId }: Props): React.ReactElement<Props> | null {
return (
<Columar className={className}>
<Columar.Column>
<ChartPoints validatorId={validatorId} />
<ChartRewards validatorId={validatorId} />
<ChartPoints
labels={labels}
validatorId={validatorId}
/>
<ChartRewards
labels={labels}
validatorId={validatorId}
/>
</Columar.Column>
<Columar.Column>
<ChartStake validatorId={validatorId} />
<ChartPrefs validatorId={validatorId} />
<ChartStake
labels={labels}
validatorId={validatorId}
/>
<ChartPrefs
labels={labels}
validatorId={validatorId}
/>
</Columar.Column>
</Columar>
);

View File

@ -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<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { value } = useParams<{ value: string }>();
const [validatorId, setValidatorId] = useState<string | null>(value || null);
const eras = useCall<INumber[]>(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 <Spinner />;
}
return (
<div className={className}>
<InputAddressSimple
@ -44,7 +60,10 @@ function Query ({ className }: Props): React.ReactElement<Props> {
/>
</InputAddressSimple>
{value && (
<Validator validatorId={value} />
<Validator
labels={labels}
validatorId={value}
/>
)}
</div>
);

View File

@ -5,6 +5,7 @@ import type { BN } from '@polkadot/util';
export interface Props {
className?: string;
labels: string[];
validatorId: string;
}

View File

@ -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 (
<div className={className}>
<Chart.Line
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data={chartData as any}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
options={chartOptions as any}
/>
<ErrorBoundary>
<Chart.Line
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data={chartData as any}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
options={chartOptions as any}
/>
</ErrorBoundary>
</div>
);
}