From 406a5058712d0079f9d7f8b16e2877509287c8c1 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Tue, 10 Dec 2019 11:30:40 +0100 Subject: [PATCH] Add ErrorBoundary component (#2026) * Add ErrorBoundary component * Decode call via effect * Cleanups --- packages/react-components/src/Call.tsx | 49 ++++++---- .../react-components/src/ErrorBoundary.tsx | 55 ++++++++++++ packages/react-components/src/index.tsx | 1 + packages/react-params/src/Param/Proposal.tsx | 46 +++------- packages/react-params/src/index.tsx | 42 +++++---- packages/react-signer/src/Modal.tsx | 19 ++-- packages/react-signer/src/Transaction.tsx | 89 +++++++++---------- 7 files changed, 184 insertions(+), 117 deletions(-) create mode 100644 packages/react-components/src/ErrorBoundary.tsx diff --git a/packages/react-components/src/Call.tsx b/packages/react-components/src/Call.tsx index d0032acdef..69dba13ab6 100644 --- a/packages/react-components/src/Call.tsx +++ b/packages/react-components/src/Call.tsx @@ -2,11 +2,12 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { Hash } from '@polkadot/types/interfaces'; import { Codec, IExtrinsic, IMethod, TypeDef } from '@polkadot/types/types'; import { BareProps, I18nProps } from './types'; import BN from 'bn.js'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { GenericCall, getTypeDef } from '@polkadot/types'; import Params from '@polkadot/react-params'; @@ -19,24 +20,41 @@ import translate from './translate'; export interface Props extends I18nProps, BareProps { children?: React.ReactNode; labelHash?: React.ReactNode; + mortality?: string; + onError?: () => void; value: IExtrinsic | IMethod; withHash?: boolean; - mortality?: string; tip?: BN; } -function Call ({ children, className, labelHash, style, mortality, tip, value, withHash, t }: Props): React.ReactElement { - const params = GenericCall.filterOrigin(value.meta).map(({ name, type }): { name: string; type: TypeDef } => ({ - name: name.toString(), - type: getTypeDef(type.toString()) - })); - const values = value.args.map((value): { isValid: boolean; value: Codec } => ({ - isValid: true, - value - })); - const hash = withHash - ? (value as IExtrinsic).hash - : null; +interface Param { + name: string; + type: TypeDef; +} + +interface Value { + isValid: boolean; + value: Codec; +} + +function Call ({ children, className, labelHash, mortality, onError, style, tip, value, withHash, t }: Props): React.ReactElement { + const [{ hash, params, values }, setExtracted] = useState<{ hash: Hash | null; params: Param[]; values: Value[] }>({ hash: null, params: [], values: [] }); + + useEffect((): void => { + const params = GenericCall.filterOrigin(value.meta).map(({ name, type }): Param => ({ + name: name.toString(), + type: getTypeDef(type.toString()) + })); + const values = value.args.map((value): Value => ({ + isValid: true, + value + })); + const hash = withHash + ? value.hash + : null; + + setExtracted({ hash, params, values }); + }, [value, withHash]); return (
@@ -66,7 +85,7 @@ function Call ({ children, className, labelHash, style, mortality, tip, value, w {mortality} )} - {tip && tip.gtn(0) && ( + {tip?.gtn(0) && ( void; +} + +interface State { + hasError: boolean; +} + +class ErrorBoundary extends React.Component { + state: State = { hasError: false }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static getDerivedStateFromError (_error: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public componentDidCatch (error: Error, _errorInfo: any): void { + const { doThrow, onError } = this.props; + + onError && onError(); + + if (doThrow) { + throw error; + } + } + + public render (): React.ReactNode { + const { children, t } = this.props; + const { hasError } = this.state; + + return hasError + ? ( +
+ {t('Uncaught error. Something went wrong with the data extraction, resulting in an error log.')} +
+ ) + : children; + } +} + +export default translate(ErrorBoundary); diff --git a/packages/react-components/src/index.tsx b/packages/react-components/src/index.tsx index 5545b636bc..b72f880131 100644 --- a/packages/react-components/src/index.tsx +++ b/packages/react-components/src/index.tsx @@ -28,6 +28,7 @@ export { default as CopyButton } from './CopyButton'; export { default as CryptoType } from './CryptoType'; export { default as Dropdown } from './Dropdown'; export { default as Editor } from './Editor'; +export { default as ErrorBoundary } from './ErrorBoundary'; export { default as Event } from './Event'; export { default as Expander } from './Expander'; export { default as Extrinsic } from './Extrinsic'; diff --git a/packages/react-params/src/Param/Proposal.tsx b/packages/react-params/src/Param/Proposal.tsx index cb2ca7fb3d..6f239fe5d5 100644 --- a/packages/react-params/src/Param/Proposal.tsx +++ b/packages/react-params/src/Param/Proposal.tsx @@ -23,37 +23,19 @@ export default function Proposal (props: Props): React.ReactElement { } const proposal = value as Extrinsic; + const { method, section } = registry.findMetaCall(proposal.callIndex); - try { - const { method, section } = registry.findMetaCall(proposal.callIndex); - - return ( - - - {section}.{method} - - - - ); - } catch (error) { - console.error(error); - - return ( - - - Unable to render extrinsic. - - - ); - } + return ( + + + {section}.{method} + + + + ); } diff --git a/packages/react-params/src/index.tsx b/packages/react-params/src/index.tsx index 490087b9c3..e5eb454634 100644 --- a/packages/react-params/src/index.tsx +++ b/packages/react-params/src/index.tsx @@ -9,6 +9,7 @@ import './Params.css'; import React from 'react'; import styled from 'styled-components'; +import { ErrorBoundary } from '@polkadot/react-components'; import { classes } from '@polkadot/react-components/util'; import ParamComp from './ParamComp'; @@ -19,6 +20,7 @@ interface Props extends I18nProps { isDisabled?: boolean; onChange?: (value: RawParams) => void; onEnter?: () => void; + onError?: () => void; onEscape?: () => void; overrides?: ComponentMap; params: ParamDef[]; @@ -85,22 +87,24 @@ class Params extends React.PureComponent { className={classes('ui--Params', className)} style={style} > -
- {values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => ( - - ))} -
+ +
+ {values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => ( + + ))} +
+
); } @@ -136,6 +140,12 @@ class Params extends React.PureComponent { onChange && onChange(values); } + + private onRenderError = (): void => { + const { onError } = this.props; + + onError && onError(); + } } export default translate( diff --git a/packages/react-signer/src/Modal.tsx b/packages/react-signer/src/Modal.tsx index 7fdad0462d..c623adebfa 100644 --- a/packages/react-signer/src/Modal.tsx +++ b/packages/react-signer/src/Modal.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { SubmittableResult } from '@polkadot/api'; import { web3FromSource } from '@polkadot/extension-dapp'; import { createType } from '@polkadot/types'; -import { Button, InputBalance, Modal, Toggle } from '@polkadot/react-components'; +import { Button, InputBalance, Modal, Toggle, ErrorBoundary } from '@polkadot/react-components'; import { registry, withApi, withMulti, withObservable } from '@polkadot/react-api'; import keyring from '@polkadot/ui-keyring'; import { assert, isFunction } from '@polkadot/util'; @@ -43,6 +43,7 @@ interface State { currentItem?: QueueTx; isQrScanning: boolean; isQrVisible: boolean; + isRenderError: boolean; isSendable: boolean; isV2?: boolean; password: string; @@ -94,9 +95,10 @@ async function makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerC class Signer extends React.PureComponent { public state: State = { - isSendable: false, isQrScanning: false, isQrVisible: false, + isRenderError: false, + isSendable: false, password: '', qrAddress: '', qrPayload: new Uint8Array(), @@ -164,7 +166,9 @@ class Signer extends React.PureComponent { className='ui--signer-Signer' open > - {this.renderContent()} + + {this.renderContent()} + {this.renderButtons()} ); @@ -172,7 +176,7 @@ class Signer extends React.PureComponent { private renderButtons (): React.ReactNode { const { t } = this.props; - const { currentItem, isQrScanning, isQrVisible, isSendable } = this.state; + const { currentItem, isQrScanning, isQrVisible, isRenderError, isSendable } = this.state; if (!currentItem) { return null; @@ -194,7 +198,7 @@ class Signer extends React.PureComponent { label={t('Cancel')} icon='cancel' /> - {(!isQrVisible || !isQrScanning) && ( + {!isRenderError && (!isQrVisible || !isQrScanning) && ( <>