ohif-viewer/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx

199 lines
5.5 KiB
TypeScript
Raw Normal View History

2024-11-06 13:15:27 +01:00
import React, { useState, useEffect } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '../Dialog/Dialog';
import { ScrollArea } from '../ScrollArea/ScrollArea';
import { Icons } from '../Icons';
const isProduction = process.env.NODE_ENV === 'production';
interface ErrorBoundaryError extends Error {
message: string;
stack?: string;
}
interface DefaultFallbackProps {
error: ErrorBoundaryError;
context: string;
resetErrorBoundary: () => void;
}
interface ErrorBoundaryProps {
context?: string;
onReset?: () => void;
onError?: (error: ErrorBoundaryError, componentStack: string, context: string) => void;
fallbackComponent?: React.ComponentType<DefaultFallbackProps>;
children: React.ReactNode;
fallbackRoute?: string | null;
isPage?: boolean;
}
const DefaultFallback = ({
error,
context,
resetErrorBoundary = () => {},
}: DefaultFallbackProps) => {
const { t } = useTranslation('ErrorBoundary');
const [showDetails, setShowDetails] = useState(false);
const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`;
const subtitle = t('Sorry, something went wrong there. Try again.');
const copyErrorDetails = () => {
const errorDetails = `
Context: ${context}
Error Message: ${error.message}
Stack: ${error.stack}
`;
navigator.clipboard.writeText(errorDetails);
toast.success(t('Copied to clipboard'));
};
useEffect(() => {
toast.error(title, {
description: subtitle,
action: {
label: t('Show Details'),
onClick: () => setShowDetails(true),
},
duration: 0,
});
}, [error]);
if (isProduction) {
return null;
}
return (
<>
<Dialog
open={showDetails}
onOpenChange={setShowDetails}
>
<DialogContent className="border-input h-[50vh] w-[90vw] border-2 sm:max-w-[900px]">
<DialogHeader>
<DialogTitle className="text-muted-foreground flex justify-between text-xl">
<div className="flex items-center">{title}</div>
<button
onClick={() => {
copyErrorDetails();
setShowDetails(false);
}}
className="text-aqua-pale hover:text-aqua-pale/80 flex items-center gap-2 rounded bg-gray-800 px-4 py-2 font-light"
>
<Icons.Code className="h-4 w-4" />
{t('Copy Details')}
</button>
</DialogTitle>
2024-11-06 13:15:27 +01:00
<DialogDescription className="text-lg">{subtitle}</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[100%]">
2024-11-06 13:15:27 +01:00
<div className="space-y-4 pr-4 font-mono text-base">
<div className="space-y-4">
<p className="text-aqua-pale break-words text-lg">
{t('Context')}: {context}
</p>
<p className="text-aqua-pale break-words text-lg">
{t('Error Message')}: {error.message}
</p>
<pre className="text-aqua-pale whitespace-pre-wrap break-words rounded bg-gray-900 p-4">
Stack: {error.stack}
</pre>
</div>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
</>
);
};
const ErrorBoundary = ({
context = 'OHIF',
onReset = () => {},
onError = () => {},
fallbackComponent: FallbackComponent = DefaultFallback,
children,
fallbackRoute = null,
isPage,
}: ErrorBoundaryProps) => {
const [error, setError] = useState<ErrorBoundaryError | null>(null);
const onResetHandler = () => {
setError(null);
onReset();
};
// Add error event listener to window
useEffect(() => {
let errorTimeout: NodeJS.Timeout;
const handleError = (event: ErrorEvent) => {
event.preventDefault();
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
setError(event.error);
onErrorHandler(event.error, null);
}, 100);
};
const handleRejection = (event: PromiseRejectionEvent) => {
event.preventDefault();
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
setError(event.reason);
onErrorHandler(event.reason, null);
}, 100);
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleRejection);
return () => {
clearTimeout(errorTimeout);
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleRejection);
};
}, []);
const onErrorHandler = (error: ErrorBoundaryError, componentStack: string) => {
console.debug(`${context} Error Boundary`, error, componentStack, context);
onError(error, componentStack, context);
};
return (
<ReactErrorBoundary
fallbackRender={props => (
<FallbackComponent
error={props.error}
context={context}
resetErrorBoundary={props.resetErrorBoundary}
/>
)}
onReset={onResetHandler}
onError={onErrorHandler}
>
<>
{children}
{error && (
<FallbackComponent
error={error}
context={context}
resetErrorBoundary={() => setError(null)}
/>
)}
</>
</ReactErrorBoundary>
);
};
export { ErrorBoundary };