Frontend error handlers (#312)
* Add componentDidCatch around routes with template, and outside routes at the top level. * Remove error from ErrorWrap on navigation.
This commit is contained in:
parent
a271724266
commit
1905dfe7bf
|
@ -11,6 +11,9 @@ import {
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
import AuthRoute from 'components/AuthRoute';
|
import AuthRoute from 'components/AuthRoute';
|
||||||
import Template, { TemplateProps } from 'components/Template';
|
import Template, { TemplateProps } from 'components/Template';
|
||||||
|
import ErrorWrap from 'components/ErrorWrap';
|
||||||
|
import Loader from 'components/Loader';
|
||||||
|
import 'styles/style.less';
|
||||||
|
|
||||||
// wrap components in loadable...import & they will be split
|
// wrap components in loadable...import & they will be split
|
||||||
// Make sure you specify chunkname! Must replace slashes with dashes.
|
// Make sure you specify chunkname! Must replace slashes with dashes.
|
||||||
|
@ -38,9 +41,6 @@ const ArbiterEmail = loadable(() => import('pages/email-arbiter'), opts);
|
||||||
const RFP = loadable(() => import('pages/rfp'), opts);
|
const RFP = loadable(() => import('pages/rfp'), opts);
|
||||||
const RFPs = loadable(() => import('pages/rfps'), opts);
|
const RFPs = loadable(() => import('pages/rfps'), opts);
|
||||||
|
|
||||||
import 'styles/style.less';
|
|
||||||
import Loader from 'components/Loader';
|
|
||||||
|
|
||||||
interface RouteConfig extends RouteProps {
|
interface RouteConfig extends RouteProps {
|
||||||
route: RouteProps;
|
route: RouteProps;
|
||||||
template: TemplateProps;
|
template: TemplateProps;
|
||||||
|
@ -336,7 +336,9 @@ class Routes extends React.PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template {...currentRoute.template}>
|
<Template {...currentRoute.template}>
|
||||||
|
<ErrorWrap key={currentRoute.route.path as string}>
|
||||||
<Switch>{routeComponents}</Switch>
|
<Switch>{routeComponents}</Switch>
|
||||||
|
</ErrorWrap>
|
||||||
</Template>
|
</Template>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
@import '~styles/variables.less';
|
||||||
|
|
||||||
|
.ErrorScreen {
|
||||||
|
padding: 2rem 0;
|
||||||
|
|
||||||
|
&-desc {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: rgba(#000, 0.03);
|
||||||
|
border: 1px solid rgba(#000, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-buttons {
|
||||||
|
.ant-btn {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antd overrides
|
||||||
|
.antd-pro-exception-exception {
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.antd-pro-exception-imgBlock {
|
||||||
|
flex: 0 0 45%;
|
||||||
|
width: 45%;
|
||||||
|
|
||||||
|
@media @tablet-query {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.antd-pro-exception-content {
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
@media @tablet-query {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as Sentry from '@sentry/browser';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import Exception from 'ant-design-pro/lib/Exception';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ErrorScreen extends React.PureComponent<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { error } = this.props;
|
||||||
|
Sentry.captureException(error);
|
||||||
|
console.error('Error screen showing due to the following error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { error } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="ErrorScreen">
|
||||||
|
<Exception
|
||||||
|
type="404"
|
||||||
|
title="Whoa nelly."
|
||||||
|
desc={
|
||||||
|
<div className="ErrorScreen-desc">
|
||||||
|
<p>Something went wrong, and we've logged the following error:</p>
|
||||||
|
<code className="ErrorScreen-desc-error">{error.message}</code>
|
||||||
|
<p>
|
||||||
|
Our developers will get right on fixing it. You can either return home and
|
||||||
|
try again, or open an issue on Github to provide us some more details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<div className="ErrorScreen-buttons">
|
||||||
|
<a href="/">
|
||||||
|
<Button icon="home" size="large" type="primary">
|
||||||
|
Return home
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/grant-project/zcash-grant-system/issues/new">
|
||||||
|
<Button icon="github" size="large">
|
||||||
|
Open an issue
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ErrorScreen from './ErrorScreen';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isFullscreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ErrorWrap extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidCatch(error: Error) {
|
||||||
|
this.setState({ error });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let style;
|
||||||
|
if (this.props.isFullscreen) {
|
||||||
|
style = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100vw',
|
||||||
|
minHeight: '100vh',
|
||||||
|
padding: '0 2rem',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.error) {
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<ErrorScreen error={this.state.error} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,8 +51,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-logo {
|
&-logo {
|
||||||
height: 1.8rem;
|
height: 6.8rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.5rem;
|
||||||
|
margin-top: -1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@import '~styles/variables.less';
|
@import '~styles/variables.less';
|
||||||
@header-height: 62px;
|
@header-height: 68px;
|
||||||
@header-transition: 200ms;
|
@header-transition: 200ms;
|
||||||
@link-padding: 0.7rem;
|
@link-padding: 0.7rem;
|
||||||
@small-query: ~'(max-width: 820px)';
|
@small-query: ~'(max-width: 820px)';
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-logo {
|
&-logo {
|
||||||
height: 1.4rem;
|
height: 3.6rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
transition: transform @header-transition ease;
|
transition: transform @header-transition ease;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import history from 'store/history';
|
||||||
import { massageSerializedState } from 'utils/api';
|
import { massageSerializedState } from 'utils/api';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
|
import ErrorWrap from 'components/ErrorWrap';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.SENTRY_DSN,
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
@ -26,6 +27,7 @@ i18n.changeLanguage(i18nLanguage.locale);
|
||||||
i18n.addResourceBundle(i18nLanguage.locale, 'common', i18nLanguage.resources, true);
|
i18n.addResourceBundle(i18nLanguage.locale, 'common', i18nLanguage.resources, true);
|
||||||
|
|
||||||
const App = hot(module)(() => (
|
const App = hot(module)(() => (
|
||||||
|
<ErrorWrap isFullscreen>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
|
@ -33,6 +35,7 @@ const App = hot(module)(() => (
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
</ErrorWrap>
|
||||||
));
|
));
|
||||||
|
|
||||||
loadableReady(() => {
|
loadableReady(() => {
|
||||||
|
|
|
@ -7,9 +7,6 @@ import { withRouter, RouteComponentProps } from 'react-router';
|
||||||
type RouteProps = RouteComponentProps<any>;
|
type RouteProps = RouteComponentProps<any>;
|
||||||
|
|
||||||
class ProposalPage extends Component<RouteProps> {
|
class ProposalPage extends Component<RouteProps> {
|
||||||
constructor(props: RouteProps) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
render() {
|
render() {
|
||||||
const proposalId = extractIdFromSlug(this.props.match.params.id);
|
const proposalId = extractIdFromSlug(this.props.match.params.id);
|
||||||
return <Proposal proposalId={proposalId} />;
|
return <Proposal proposalId={proposalId} />;
|
||||||
|
|
Loading…
Reference in New Issue