Add ErrorBoundary to charts (#8613)
This commit is contained in:
parent
d3dd0ccbac
commit
617eea7e07
@ -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 {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import type { BN } from '@polkadot/util';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
labels: string[];
|
||||
validatorId: string;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user