Implement Global Error Boundary (#4765)

* Implement global error boundary

* add 404 page for generic path catching

* devbuild

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton 2025-12-12 15:08:12 -08:00 committed by GitHub
parent c7b16c9aa8
commit 62b45a76dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 189 additions and 40 deletions

View File

@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: ['upgrade-multer'] # put your current branch to create a build. Core team only.
branches: ['enhancement-error-boundary'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'

View File

@ -35,6 +35,7 @@
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^6.0.0",
"react-highlight-words": "^0.21.0",
"react-i18next": "^14.1.1",
"react-loading-skeleton": "^3.1.0",

View File

@ -1,5 +1,5 @@
import React, { Suspense } from "react";
import { Outlet } from "react-router-dom";
import { Outlet, useLocation } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { AuthProvider } from "@/AuthContext";
import { ToastContainer } from "react-toastify";
@ -12,9 +12,17 @@ import { FullScreenLoader } from "./components/Preloader";
import { ThemeProvider } from "./ThemeContext";
import { PWAModeProvider } from "./PWAContext";
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
import { ErrorBoundary } from "react-error-boundary";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
export default function App() {
const location = useLocation();
return (
<ErrorBoundary
FallbackComponent={ErrorBoundaryFallback}
onError={console.error}
resetKeys={[location.pathname]}
>
<ThemeProvider>
<PWAModeProvider>
<Suspense fallback={<FullScreenLoader />}>
@ -32,5 +40,6 @@ export default function App() {
</Suspense>
</PWAModeProvider>
</ThemeProvider>
</ErrorBoundary>
);
}

View File

@ -0,0 +1,93 @@
import { NavLink } from "react-router-dom";
import { House, ArrowClockwise, Copy, Check } from "@phosphor-icons/react";
import { useState } from "react";
export default function ErrorBoundaryFallback({ error, resetErrorBoundary }) {
const [copied, setCopied] = useState(false);
const copyErrorDetails = async () => {
const details = {
url: window.location.href,
error: error?.name || "Unknown Error",
message: error?.message || "No message available",
stack: error?.stack || "No stack trace available",
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
const formattedDetails = `
Error Report
============
Timestamp: ${details.timestamp}
URL: ${details.url}
User Agent: ${details.userAgent}
Error: ${details.error}
Message: ${details.message}
Stack Trace:
${details.stack}
`.trim();
try {
await navigator.clipboard.writeText(formattedDetails);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy error details:", err);
}
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full">
<h1 className="text-xl md:text-2xl font-bold text-center">
An error occurred.
</h1>
<p className="text-theme-text-secondary text-center px-4">
{error?.message}
</p>
{import.meta.env.DEV && (
<div className="w-full max-w-4xl">
<div className="flex justify-end mb-2">
<button
onClick={copyErrorDetails}
className="flex items-center gap-2 px-3 py-1.5 bg-theme-bg-secondary text-theme-text-primary rounded hover:bg-theme-sidebar-item-hover transition-all duration-200 text-xs font-medium"
title="Copy error details"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5" weight="bold" />
Copied!
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
Copy Details
</>
)}
</button>
</div>
<pre className="w-full text-xs md:text-sm text-theme-text-secondary bg-theme-bg-secondary p-4 md:p-6 rounded-lg overflow-x-auto overflow-y-auto max-h-[60vh] md:max-h-[70vh] whitespace-pre-wrap break-words font-mono border border-theme-border shadow-sm">
{error?.stack}
</pre>
</div>
)}
<div className="flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto">
<button
onClick={resetErrorBoundary}
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
<ArrowClockwise className="w-4 h-4" />
Reset
</button>
<NavLink
to="/"
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
<House className="w-4 h-4" />
Home
</NavLink>
</div>
</div>
);
}

View File

@ -372,6 +372,14 @@ const router = createBrowserRouter([
return { element: <ManagerRoute Component={MobileConnections} /> };
},
},
// Catch-all route for 404s
{
path: "*",
lazy: async () => {
const { default: NotFound } = await import("@/pages/404");
return { element: <NotFound /> };
},
},
],
},
]);

View File

@ -1,24 +1,25 @@
import Header from "../components/Header";
import Footer from "../components/Footer";
import { NavLink } from "react-router-dom";
import { House, MagnifyingGlass } from "@phosphor-icons/react";
export default function Contact() {
export default function NotFound() {
return (
<div className="text-black">
<Header />
<div className="flex flex-col justify-center mx-auto mt-52 text-center max-w-2x1">
<h1 className="text-3xl font-bold tracking-tight text-black md:text-5xl">
404 Unavailable
<div className="flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full">
<MagnifyingGlass className="w-16 h-16 text-theme-text-secondary" />
<h1 className="text-xl md:text-2xl font-bold text-center">
404 - Page Not Found
</h1>
<br />
<a
className="w-64 p-1 mx-auto font-bold text-center text-black border border-gray-500 rounded-lg sm:p-4"
href="/"
<p className="text-theme-text-secondary text-center px-4">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto">
<NavLink
to="/"
className="flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto"
>
Return Home
</a>
<House className="w-4 h-4" />
Go Home
</NavLink>
</div>
<div className="mt-64"></div>
<Footer />
</div>
);
}

View File

@ -183,6 +183,11 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/runtime@^7.12.5":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
@ -3206,6 +3211,13 @@ react-dropzone@^14.2.3:
file-selector "^0.6.0"
prop-types "^15.8.1"
react-error-boundary@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.0.0.tgz#a9e552146958fa77d873b587aa6a5e97544ee954"
integrity sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==
dependencies:
"@babel/runtime" "^7.12.5"
react-highlight-words@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.21.0.tgz#a109acdf7dc6fac3ed7db82e9cba94e8d65c281c"
@ -3583,7 +3595,16 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -3655,7 +3676,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -4196,7 +4224,16 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==