262 lines
6.7 KiB
TypeScript
262 lines
6.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { Button, Code, Collapse, Group, Paper, rem, Text } from '@mantine/core';
|
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
|
import React, { type PropsWithChildren } from 'react';
|
|
|
|
import errorManager from './ErrorManager';
|
|
|
|
let ErrorBoundaryOptions = {
|
|
disabled: false,
|
|
onError: undefined,
|
|
} as {
|
|
disabled?: boolean;
|
|
onError?: (error: any, errorInfo: any) => void;
|
|
};
|
|
|
|
export const SetErrorBoundaryOptions = (options: typeof ErrorBoundaryOptions) => {
|
|
ErrorBoundaryOptions = { ...ErrorBoundaryOptions, ...options };
|
|
};
|
|
|
|
export const GetErrorBoundaryOptions = () => {
|
|
return { ...ErrorBoundaryOptions };
|
|
};
|
|
|
|
interface ErrorBoundaryProps extends PropsWithChildren {
|
|
namespace?: string;
|
|
onReportClick?: () => void;
|
|
onResetClick?: () => void;
|
|
onRetryClick?: () => void;
|
|
reportAPI?: string;
|
|
}
|
|
|
|
interface ErrorBoundaryState {
|
|
error: any;
|
|
errorInfo: any;
|
|
reported?: boolean;
|
|
showDetail: boolean;
|
|
timer?: NodeJS.Timeout | undefined;
|
|
try: boolean;
|
|
tryCnt: number;
|
|
wasReset?: boolean;
|
|
}
|
|
|
|
export class ReactErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
constructor(props: ErrorBoundaryProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
error: null,
|
|
errorInfo: null,
|
|
showDetail: false,
|
|
timer: undefined,
|
|
try: false,
|
|
tryCnt: 0,
|
|
};
|
|
}
|
|
|
|
componentDidCatch(error: any, errorInfo: any) {
|
|
if (GetErrorBoundaryOptions()?.disabled) {
|
|
throw Error('ErrorBoundary pass through enabled, rethrowing error', {
|
|
cause: error,
|
|
//@ts-ignore
|
|
info: errorInfo,
|
|
});
|
|
}
|
|
// Catch errors in any components below and re-render with error message
|
|
this.setState({
|
|
error,
|
|
errorInfo,
|
|
try: false,
|
|
});
|
|
if (typeof GetErrorBoundaryOptions()?.onError === 'function') {
|
|
GetErrorBoundaryOptions()?.onError?.(error, errorInfo);
|
|
}
|
|
|
|
// Report error to error manager (Sentry, custom API, etc.)
|
|
errorManager.reportError(error, errorInfo, {
|
|
componentStack: errorInfo?.componentStack,
|
|
namespace: this.props.namespace,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (this.state.errorInfo) {
|
|
// Error path
|
|
return (
|
|
<div>
|
|
<h2
|
|
style={{
|
|
color: 'var(--mantine-color-error)',
|
|
display: 'flex',
|
|
gap: '8px',
|
|
}}
|
|
>
|
|
<IconExclamationCircle
|
|
color="var(--mantine-color-error)"
|
|
style={{ height: rem(20), width: rem(20) }}
|
|
/>
|
|
A react error has occurred!
|
|
</h2>
|
|
<h4>You can try one of these solutions to get back on track:</h4>
|
|
<ul>
|
|
<li>Report the error to our team</li>
|
|
<li>Hit the keys Ctrl-Shift-R</li>
|
|
<li>Make sure your web browser is up to date</li>
|
|
<li>Clear your browser cache and cookies</li>
|
|
<li>Try using a different web browser</li>
|
|
<li>Reset your layout and settings</li>
|
|
<li>Retry loading the application</li>
|
|
</ul>
|
|
|
|
<Group>
|
|
<Button
|
|
color="red"
|
|
my="md"
|
|
onClick={() =>
|
|
this.setState((state) => ({
|
|
showDetail: !state.showDetail,
|
|
}))
|
|
}
|
|
size="compact-xs"
|
|
variant="outline"
|
|
>
|
|
More
|
|
</Button>
|
|
<Button
|
|
color="blue"
|
|
disabled={this.state.tryCnt > 2 || this.state.try}
|
|
my="md"
|
|
onClick={() => this.retry()}
|
|
size="compact-xs"
|
|
variant="outline"
|
|
>
|
|
Retry {this.state.tryCnt > 0 ? `(${this.state.tryCnt})` : ''}
|
|
</Button>
|
|
<Button
|
|
color="yellow"
|
|
disabled={this.state.wasReset}
|
|
my="md"
|
|
onClick={() => this.reset()}
|
|
size="compact-xs"
|
|
variant="outline"
|
|
>
|
|
Reset Layout or Settings
|
|
</Button>
|
|
<Button
|
|
color="green"
|
|
disabled={this.state.reported}
|
|
my="md"
|
|
onClick={() => this.report()}
|
|
size="compact-xs"
|
|
variant="outline"
|
|
>
|
|
Report
|
|
</Button>
|
|
</Group>
|
|
|
|
{this.state.error && (
|
|
<Paper
|
|
p="md"
|
|
shadow="xs"
|
|
style={{
|
|
textAlign: 'justify',
|
|
}}
|
|
w="50%"
|
|
withBorder
|
|
>
|
|
{this.props.namespace && <Text size="sm">In: {this.props.namespace}</Text>}
|
|
<Text size="sm">{this.state.error.toString()}</Text>
|
|
</Paper>
|
|
)}
|
|
|
|
<Collapse in={this.state.showDetail}>
|
|
<Code block color="rgba(235, 86, 5, 0.6)" mah="100vh" w="100%">
|
|
{this.state.errorInfo.componentStack}
|
|
</Code>
|
|
</Collapse>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
//Retry code, if you do it too many times, it will stop
|
|
if (this.state.try) {
|
|
if (!this.state.timer) {
|
|
const tm = setTimeout(() => {
|
|
clearTimeout(this.state.timer);
|
|
this.setState((state) => ({
|
|
...state,
|
|
timer: undefined,
|
|
try: false,
|
|
}));
|
|
}, 1000);
|
|
|
|
this.setState((state) => ({
|
|
...state,
|
|
timer: tm,
|
|
}));
|
|
}
|
|
return <div>Retrying {this.state.tryCnt}...</div>;
|
|
}
|
|
|
|
// Normally, just render children
|
|
return this.props.children;
|
|
}
|
|
|
|
async report() {
|
|
if (this.props?.onReportClick) {
|
|
this.props.onReportClick();
|
|
this.setState(() => ({
|
|
reported: true,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Manually report error if user clicks report button
|
|
if (this.state.error && this.state.errorInfo) {
|
|
await errorManager.reportError(this.state.error, this.state.errorInfo, {
|
|
componentStack: this.state.errorInfo?.componentStack,
|
|
namespace: this.props.namespace,
|
|
tags: { reportedBy: 'user' },
|
|
});
|
|
|
|
this.setState(() => ({
|
|
reported: true,
|
|
}));
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
if (this.props?.onResetClick) {
|
|
this.props.onResetClick();
|
|
this.setState(() => ({
|
|
wasReset: true,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
this.setState(() => ({
|
|
wasReset: true,
|
|
}));
|
|
|
|
window.location.reload();
|
|
}
|
|
|
|
retry() {
|
|
this.setState({
|
|
try: true,
|
|
tryCnt: this.state.tryCnt + 1,
|
|
});
|
|
if (this.props?.onRetryClick) {
|
|
this.props.onRetryClick();
|
|
return;
|
|
}
|
|
this.setState({
|
|
error: undefined,
|
|
errorInfo: undefined,
|
|
});
|
|
this.forceUpdate();
|
|
}
|
|
}
|
|
|
|
export default ReactErrorBoundary;
|