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:
parent
c7b16c9aa8
commit
62b45a76dc
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -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/*'
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,25 +12,34 @@ 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 (
|
||||
<ThemeProvider>
|
||||
<PWAModeProvider>
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<AuthProvider>
|
||||
<LogoProvider>
|
||||
<PfpProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Outlet />
|
||||
<ToastContainer />
|
||||
<KeyboardShortcutsHelp />
|
||||
</I18nextProvider>
|
||||
</PfpProvider>
|
||||
</LogoProvider>
|
||||
</AuthProvider>
|
||||
</Suspense>
|
||||
</PWAModeProvider>
|
||||
</ThemeProvider>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorBoundaryFallback}
|
||||
onError={console.error}
|
||||
resetKeys={[location.pathname]}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<PWAModeProvider>
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<AuthProvider>
|
||||
<LogoProvider>
|
||||
<PfpProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Outlet />
|
||||
<ToastContainer />
|
||||
<KeyboardShortcutsHelp />
|
||||
</I18nextProvider>
|
||||
</PfpProvider>
|
||||
</LogoProvider>
|
||||
</AuthProvider>
|
||||
</Suspense>
|
||||
</PWAModeProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
93
frontend/src/components/ErrorBoundaryFallback/index.jsx
Normal file
93
frontend/src/components/ErrorBoundaryFallback/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 /> };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@ -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
|
||||
</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="/"
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user