Add ErrorBoundary component (#2026)
* Add ErrorBoundary component * Decode call via effect * Cleanups
This commit is contained in:
parent
f962302c1f
commit
406a505871
@ -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')}
|
||||
|
||||
55
packages/react-components/src/ErrorBoundary.tsx
Normal file
55
packages/react-components/src/ErrorBoundary.tsx
Normal 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);
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user