import React, { ErrorInfo, PropsWithChildren } from "react"; import { AlertCircleIcon, ShieldAlertIcon, RefreshCwIcon, ArrowLeftIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { iconSize } from "../icons/Icon"; import { Typography } from "./Typography"; import { Button } from "./Button"; import { cls } from "../util"; import { isChunkLoadError } from "../util/lazy_chunk"; /** * The one error a user can fix themselves. A deploy replaces the hashed chunk * files, so a tab opened before it cannot load any lazy view it has not already * fetched — with the browser's raw message ("Failed to fetch dynamically * imported module: …") this reads as a broken build rather than a stale tab. */ const CHUNK_ERROR_DESCRIPTION = "This app was updated while the tab was open, so part of it could not be loaded. Reload to continue."; /** * Checks whether the error message relates to missing permissions or * authorization failures. Used to show a friendlier message in fullPage mode. */ function isPermissionError(error: Error | null): boolean { if (!error) return false; const msg = error.message?.toLowerCase() ?? ""; const code = (error as { code?: string }).code?.toLowerCase() ?? ""; return msg.includes("permission") || msg.includes("insufficient") || msg.includes("unauthorized") || msg.includes("forbidden") || msg.includes("access denied") || code === "permission_denied" || code === "insufficient_permissions" || (error as { status?: number }).status === 403; } export interface ErrorBoundaryProps { /** * When true, renders a full-page centered error screen instead of the * compact inline error. Intended for wrapping top-level app roots or * major route sections. */ fullPage?: boolean; /** * Optional callback invoked when the user clicks "Try again". If provided, * the boundary resets its error state and calls this handler. If omitted, * the "Try again" button resets the boundary state, re-mounting children. */ onReset?: () => void; } interface ErrorBoundaryState { error: Error | null; showDetails: boolean; } export class ErrorBoundary extends React.Component, ErrorBoundaryState> { constructor(props: PropsWithChildren) { super(props); this.state = { error: null, showDetails: false }; } static getDerivedStateFromError(error: Error) { return { error }; } componentDidCatch(error: Error, _errorInfo: ErrorInfo) { console.error(error); } private handleReset = () => { this.setState({ error: null, showDetails: false }); this.props.onReset?.(); }; private handleReload = () => { window.location.reload(); }; private toggleDetails = () => { this.setState(prev => ({ showDetails: !prev.showDetails })); }; render() { if (this.state.error) { if (this.props.fullPage) { return this.renderFullPage(); } return this.renderInline(); } return this.props.children; } private renderInline() { const isStaleChunk = isChunkLoadError(this.state.error); return (
{isStaleChunk ? : }
{isStaleChunk ? "New version available" : "Error"}
{isStaleChunk ? CHUNK_ERROR_DESCRIPTION : this.state.error?.message ?? "See the error in the console"} {isStaleChunk && }
); } private renderFullPage() { const { error, showDetails } = this.state; const isPermission = isPermissionError(error); // Neither a fault nor a denial: the app is fine, this tab is behind it. const isStaleChunk = !isPermission && isChunkLoadError(error); const Icon = isPermission ? ShieldAlertIcon : isStaleChunk ? RefreshCwIcon : AlertCircleIcon; const title = isPermission ? "Access denied" : isStaleChunk ? "New version available" : "Something went wrong"; const description = isPermission ? "You don't have permission to access this resource. Please check your account permissions or contact your administrator." : isStaleChunk ? CHUNK_ERROR_DESCRIPTION : "An unexpected error occurred. You can try reloading the page or going back."; return (
{title} {description}
{/* React caches a `lazy()` rejection: re-mounting the children re-throws without re-running the import, so "Try again" cannot recover a missing chunk. */} {!isStaleChunk && }
{/* For a stale chunk the message *is* the description above; repeating it under a disclosure adds nothing. */} {!isStaleChunk && error?.message && (
{showDetails && (
{error.message}
)}
)}
); } }