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:
|
on:
|
||||||
push:
|
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:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'cloud-deployments/*'
|
- 'cloud-deployments/*'
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
"react-device-detect": "^2.2.2",
|
"react-device-detect": "^2.2.2",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-highlight-words": "^0.21.0",
|
"react-highlight-words": "^0.21.0",
|
||||||
"react-i18next": "^14.1.1",
|
"react-i18next": "^14.1.1",
|
||||||
"react-loading-skeleton": "^3.1.0",
|
"react-loading-skeleton": "^3.1.0",
|
||||||
@ -75,4 +76,4 @@
|
|||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"vite": "^4.3.0"
|
"vite": "^4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { Suspense } from "react";
|
import React, { Suspense } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import { AuthProvider } from "@/AuthContext";
|
import { AuthProvider } from "@/AuthContext";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
@ -12,25 +12,34 @@ import { FullScreenLoader } from "./components/Preloader";
|
|||||||
import { ThemeProvider } from "./ThemeContext";
|
import { ThemeProvider } from "./ThemeContext";
|
||||||
import { PWAModeProvider } from "./PWAContext";
|
import { PWAModeProvider } from "./PWAContext";
|
||||||
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
|
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ErrorBoundary
|
||||||
<PWAModeProvider>
|
FallbackComponent={ErrorBoundaryFallback}
|
||||||
<Suspense fallback={<FullScreenLoader />}>
|
onError={console.error}
|
||||||
<AuthProvider>
|
resetKeys={[location.pathname]}
|
||||||
<LogoProvider>
|
>
|
||||||
<PfpProvider>
|
<ThemeProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<PWAModeProvider>
|
||||||
<Outlet />
|
<Suspense fallback={<FullScreenLoader />}>
|
||||||
<ToastContainer />
|
<AuthProvider>
|
||||||
<KeyboardShortcutsHelp />
|
<LogoProvider>
|
||||||
</I18nextProvider>
|
<PfpProvider>
|
||||||
</PfpProvider>
|
<I18nextProvider i18n={i18n}>
|
||||||
</LogoProvider>
|
<Outlet />
|
||||||
</AuthProvider>
|
<ToastContainer />
|
||||||
</Suspense>
|
<KeyboardShortcutsHelp />
|
||||||
</PWAModeProvider>
|
</I18nextProvider>
|
||||||
</ThemeProvider>
|
</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} /> };
|
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 { NavLink } from "react-router-dom";
|
||||||
import Footer from "../components/Footer";
|
import { House, MagnifyingGlass } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function Contact() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="text-black">
|
<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">
|
||||||
<Header />
|
<MagnifyingGlass className="w-16 h-16 text-theme-text-secondary" />
|
||||||
<div className="flex flex-col justify-center mx-auto mt-52 text-center max-w-2x1">
|
<h1 className="text-xl md:text-2xl font-bold text-center">
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-black md:text-5xl">
|
404 - Page Not Found
|
||||||
404 – Unavailable
|
</h1>
|
||||||
</h1>
|
<p className="text-theme-text-secondary text-center px-4">
|
||||||
<br />
|
The page you're looking for doesn't exist or has been moved.
|
||||||
<a
|
</p>
|
||||||
className="w-64 p-1 mx-auto font-bold text-center text-black border border-gray-500 rounded-lg sm:p-4"
|
<div className="flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto">
|
||||||
href="/"
|
<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
|
<House className="w-4 h-4" />
|
||||||
</a>
|
Go Home
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-64"></div>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,6 +183,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.24.7"
|
"@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":
|
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.26.10"
|
version "7.26.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
|
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"
|
file-selector "^0.6.0"
|
||||||
prop-types "^15.8.1"
|
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:
|
react-highlight-words@^0.21.0:
|
||||||
version "0.21.0"
|
version "0.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.21.0.tgz#a109acdf7dc6fac3ed7db82e9cba94e8d65c281c"
|
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"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
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"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -3655,7 +3676,14 @@ string.prototype.trimstart@^1.0.8:
|
|||||||
define-properties "^1.2.1"
|
define-properties "^1.2.1"
|
||||||
es-object-atoms "^1.0.0"
|
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"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
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"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
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"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user