Skip to content

Commit

Permalink
feat: add ErrorBoundary (#549)
Browse files Browse the repository at this point in the history
  • Loading branch information
artemmufazalov authored Jan 12, 2024
1 parent 27c4094 commit f5ad224
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 8 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"numeral": "2.0.6",
"path-to-regexp": "3.0.0",
"qs": "^6.11.0",
"react-error-boundary": "^4.0.12",
"react-json-inspector": "7.1.1",
"react-list": "0.8.11",
"react-monaco-editor": "0.30.1",
Expand Down
32 changes: 32 additions & 0 deletions src/assets/illustrations/dark/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/assets/illustrations/light/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions src/components/ErrorBoundary/ErrorBoundary.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@import '../../styles/mixins.scss';

.ydb-error-boundary {
display: flex;
flex-direction: row;
align-items: flex-start;

padding: 20px;

@include body-2-typography();

&__illustration {
width: 230px;
height: 230px;
margin-right: 20px;
}
&__error-title {
margin-top: 44px;
@include lead-typography();
}
&__error-description {
margin-top: 12px;
}
&__show-details {
margin-top: 8px;
}
&__error-details {
padding: 13px 18px;

border: 1px solid var(--g-color-line-generic);
background-color: var(--g-color-base-generic-ultralight);
}
&__actions {
display: flex;
flex-direction: row;
gap: 10px;

margin-top: 20px;
}
}
62 changes: 62 additions & 0 deletions src/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type {ReactNode} from 'react';
import {ErrorBoundary as ErrorBoundaryBase} from 'react-error-boundary';
import cn from 'bem-cn-lite';

import {Button, Disclosure} from '@gravity-ui/uikit';

import {registerError} from '../../utils/registerError';
import {Illustration} from '../Illustration';
import i18n from './i18n';
import './ErrorBoundary.scss';

const b = cn('ydb-error-boundary');

interface ErrorBoundaryProps {
children?: ReactNode;
useRetry?: boolean;
onReportProblem?: (error?: Error) => void;
}

export const ErrorBoundary = ({children, useRetry = true, onReportProblem}: ErrorBoundaryProps) => {
return (
<ErrorBoundaryBase
onError={(error, info) => {
registerError(error, info.componentStack, 'error-boundary');
}}
fallbackRender={({error, resetErrorBoundary}) => {
return (
<div className={b(null)}>
<Illustration name="error" className={b('illustration')} />
<div className={b('content')}>
<h2 className={b('error-title')}>{i18n('error-title')}</h2>
<div className={b('error-description')}>
{i18n('error-description')}
</div>
<Disclosure
summary={i18n('show-details')}
className={b('show-details')}
size="m"
>
<pre className={b('error-details')}>{error.stack}</pre>
</Disclosure>
<div className={b('actions')}>
{useRetry && (
<Button view="outlined" onClick={resetErrorBoundary}>
{i18n('button-reset')}
</Button>
)}
{onReportProblem && (
<Button view="outlined" onClick={() => onReportProblem(error)}>
{i18n('report-problem')}
</Button>
)}
</div>
</div>
</div>
);
}}
>
{children}
</ErrorBoundaryBase>
);
};
7 changes: 7 additions & 0 deletions src/components/ErrorBoundary/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"error-title": "Something went wrong",
"error-description": "We have something broken, but don't worry, it won't last long",
"show-details": "Show details",
"report-problem": "Report a problem",
"button-reset": "Try again"
}
11 changes: 11 additions & 0 deletions src/components/ErrorBoundary/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {i18n, Lang} from '../../../utils/i18n';

import en from './en.json';
import ru from './ru.json';

const COMPONENT = 'ydb-error-boundary';

i18n.registerKeyset(Lang.En, COMPONENT, en);
i18n.registerKeyset(Lang.Ru, COMPONENT, ru);

export default i18n.keyset(COMPONENT);
7 changes: 7 additions & 0 deletions src/components/ErrorBoundary/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"error-title": "Что-то пошло не так",
"error-description": "У нас что-то сломалось, но не переживайте, это ненадолго",
"show-details": "Показать детали",
"report-problem": "Сообщить о проблеме",
"button-reset": "Попробовать снова"
}
4 changes: 3 additions & 1 deletion src/components/Illustration/Illustration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ const store: IllustrationStore = {
light: {
403: () => import('../../assets/illustrations/light/403.svg'),
thumbsUp: () => import('../../assets/illustrations/light/thumbsUp.svg'),
error: () => import('../../assets/illustrations/light/error.svg'),
},
dark: {
403: () => import('../../assets/illustrations/dark/403.svg'),
thumbsUp: () => import('../../assets/illustrations/light/thumbsUp.svg'),
thumbsUp: () => import('../../assets/illustrations/dark/thumbsUp.svg'),
error: () => import('../../assets/illustrations/dark/error.svg'),
},
};

Expand Down
7 changes: 5 additions & 2 deletions src/containers/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AsideNavigation from '../AsideNavigation/AsideNavigation';

import {getUser} from '../../store/reducers/authentication/authentication';
import {registerLanguages} from '../../utils/monaco';
import {ErrorBoundary} from '../../components/ErrorBoundary/ErrorBoundary';

import './App.scss';

Expand Down Expand Up @@ -38,8 +39,10 @@ class App extends React.Component {
const {singleClusterMode, clusterName} = this.props;
return (
<AsideNavigation>
<Content singleClusterMode={singleClusterMode} clusterName={clusterName} />
<div id="fullscreen-root"></div>
<ErrorBoundary>
<Content singleClusterMode={singleClusterMode} clusterName={clusterName} />
<div id="fullscreen-root"></div>
</ErrorBoundary>
</AsideNavigation>
);
}
Expand Down
13 changes: 8 additions & 5 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import App from './containers/App/App';
import configureStore from './store';
import reportWebVitals from './reportWebVitals';
import HistoryContext from './contexts/HistoryContext';
import {ErrorBoundary} from './components/ErrorBoundary/ErrorBoundary';

import './styles/themes.scss';
import './styles/constants.scss';
Expand All @@ -18,11 +19,13 @@ window.store = store;

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<HistoryContext.Provider value={history}>
<App />
</HistoryContext.Provider>
</Provider>
<ErrorBoundary>
<Provider store={store}>
<HistoryContext.Provider value={history}>
<App />
</HistoryContext.Provider>
</Provider>
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root'),
);
Expand Down
18 changes: 18 additions & 0 deletions src/utils/registerError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function registerError(error: Error, message?: string, type = 'error') {
if (typeof window !== 'undefined' && window.Ya?.Rum) {
window.Ya.Rum.logError(
{
additional: {
url: window.location.href,
},
type,
message,
level: window.Ya.Rum.ERROR_LEVEL.ERROR,
},
error,
);
} else {
// eslint-disable-next-line no-console
console.error(error);
}
}

0 comments on commit f5ad224

Please sign in to comment.