Add ErrorBoundary component (#2026)

* Add ErrorBoundary component

* Decode call via effect

* Cleanups
This commit is contained in:
Jaco Greeff 2019-12-10 11:30:40 +01:00 committed by GitHub
parent f962302c1f
commit 406a505871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 117 deletions

View File

@ -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<Props> {
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<Props> {
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 (
<div
@ -45,6 +63,7 @@ function Call ({ children, className, labelHash, style, mortality, tip, value, w
>
<Params
isDisabled
onError={onError}
params={params}
values={values}
/>
@ -66,7 +85,7 @@ function Call ({ children, className, labelHash, style, mortality, tip, value, w
{mortality}
</Static>
)}
{tip && tip.gtn(0) && (
{tip?.gtn(0) && (
<Static
className='tip'
label={t('tip')}

View File

@ -0,0 +1,55 @@
// Copyright 2017-2019 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from './types';
import React from 'react';
import translate from './translate';
interface Props extends I18nProps {
children: React.ReactNode;
doThrow?: boolean;
onError?: () => void;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends React.Component<Props> {
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
? (
<article className='error'>
{t('Uncaught error. Something went wrong with the data extraction, resulting in an error log.')}
</article>
)
: children;
}
}
export default translate(ErrorBoundary);

View File

@ -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';

View File

@ -23,37 +23,19 @@ export default function Proposal (props: Props): React.ReactElement<Props> {
}
const proposal = value as Extrinsic;
const { method, section } = registry.findMetaCall(proposal.callIndex);
try {
const { method, section } = registry.findMetaCall(proposal.callIndex);
return (
<Bare>
<Static
className={classes(className, 'full')}
label={label}
style={style}
withLabel={withLabel}
>
{section}.{method}
</Static>
<Call value={proposal} />
</Bare>
);
} catch (error) {
console.error(error);
return (
<Bare>
<Static
className={classes(className, 'full')}
label={label}
style={style}
withLabel={withLabel}
>
Unable to render extrinsic.
</Static>
</Bare>
);
}
return (
<Bare>
<Static
className={classes(className, 'full')}
label={label}
style={style}
withLabel={withLabel}
>
{section}.{method}
</Static>
<Call value={proposal} />
</Bare>
);
}

View File

@ -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<Props, State> {
className={classes('ui--Params', className)}
style={style}
>
<div className='ui--Params-Content'>
{values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => (
<ParamComp
defaultValue={values[index]}
index={index}
isDisabled={isDisabled}
key={`${name}:${type}:${index}`}
name={name}
onChange={this.onChangeParam}
onEnter={onEnter}
onEscape={onEscape}
overrides={overrides}
type={type}
/>
))}
</div>
<ErrorBoundary onError={this.onRenderError}>
<div className='ui--Params-Content'>
{values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => (
<ParamComp
defaultValue={values[index]}
index={index}
isDisabled={isDisabled}
key={`${name}:${type}:${index}`}
name={name}
onChange={this.onChangeParam}
onEnter={onEnter}
onEscape={onEscape}
overrides={overrides}
type={type}
/>
))}
</div>
</ErrorBoundary>
</div>
);
}
@ -136,6 +140,12 @@ class Params extends React.PureComponent<Props, State> {
onChange && onChange(values);
}
private onRenderError = (): void => {
const { onError } = this.props;
onError && onError();
}
}
export default translate(

View File

@ -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<Props, State> {
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<Props, State> {
className='ui--signer-Signer'
open
>
{this.renderContent()}
<ErrorBoundary onError={this.onRenderError}>
{this.renderContent()}
</ErrorBoundary>
{this.renderButtons()}
</Modal>
);
@ -172,7 +176,7 @@ class Signer extends React.PureComponent<Props, State> {
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<Props, State> {
label={t('Cancel')}
icon='cancel'
/>
{(!isQrVisible || !isQrScanning) && (
{!isRenderError && (!isQrVisible || !isQrScanning) && (
<>
<Button.Or />
<Button
@ -246,6 +250,7 @@ class Signer extends React.PureComponent<Props, State> {
<Transaction
hideDetails={isQrVisible}
isSendable={isSendable}
onError={this.onRenderError}
tip={tip}
value={currentItem}
>
@ -302,6 +307,10 @@ class Signer extends React.PureComponent<Props, State> {
);
}
private onRenderError = (): void => {
this.setState({ isRenderError: true });
}
private onShowTip = (showTip: boolean): void => {
this.setState({ showTip });
}

View File

@ -17,64 +17,55 @@ interface Props extends I18nProps {
children?: React.ReactNode;
hideDetails?: boolean;
isSendable: boolean;
onError: () => void;
tip?: BN;
value: QueueTx;
}
function Transaction ({ children, hideDetails, isSendable, value: { accountId, extrinsic, isUnsigned }, t, tip }: Props): React.ReactElement<Props> | null {
function Transaction ({ children, hideDetails, isSendable, onError, value: { accountId, extrinsic, isUnsigned }, t, tip }: Props): React.ReactElement<Props> | null {
if (!extrinsic) {
return null;
}
try {
const { meta, method, section } = registry.findMetaCall(extrinsic.callIndex);
const { meta, method, section } = registry.findMetaCall(extrinsic.callIndex);
return (
<>
<Modal.Header>
{section}.{method}
<label><details><summary>{meta?.documentation.join(' ') || t('Details')}</summary></details></label>
</Modal.Header>
<Modal.Content className='ui--signer-Signer-Content'>
{!hideDetails && (
<>
{!isUnsigned && accountId && (
<InputAddress
className='full'
defaultValue={accountId}
isDisabled
isInput
label={t('sending from my account')}
withLabel
/>
)}
<Call value={extrinsic} />
{!isUnsigned && (
<Checks
accountId={accountId}
extrinsic={extrinsic}
isSendable={isSendable}
tip={tip}
/>
)}
</>
)}
{children}
</Modal.Content>
</>
);
} catch (error) {
console.error(error);
return (
<>
<Modal.Header>{t('FATAL')}</Modal.Header>
<Modal.Content className='ui--signer-Signer-Content'>
{t('Unable to render extrinsic, invalid')}
</Modal.Content>
</>
);
}
return (
<>
<Modal.Header>
{section}.{method}
<label><details><summary>{meta?.documentation.join(' ') || t('Details')}</summary></details></label>
</Modal.Header>
<Modal.Content className='ui--signer-Signer-Content'>
{!hideDetails && (
<>
{!isUnsigned && accountId && (
<InputAddress
className='full'
defaultValue={accountId}
isDisabled
isInput
label={t('sending from my account')}
withLabel
/>
)}
<Call
onError={onError}
value={extrinsic}
/>
{!isUnsigned && (
<Checks
accountId={accountId}
extrinsic={extrinsic}
isSendable={isSendable}
tip={tip}
/>
)}
</>
)}
{children}
</Modal.Content>
</>
);
}
export default translate(Transaction);