diff --git a/package.json b/package.json index 4417e936d0d3e9e8f19184dceb1da9616418e7b2..534768d5249fa2ce38680efa401f1a8c23e7056f 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,18 @@ "react": "^17.0.1", "react-chartjs-2": "3.0.4", "react-dom": "^17.0.1", + "react-error-boundary": "^3.1.4", "react-router-dom": "^5.2.0", "react-transition-group": "^4.4.1", "swagger-client": "^3.13.1", "web-vitals": "^0.2.4" }, + "resolutions": { + "//": "See https://github.com/facebook/create-react-app/issues/11773", + "react-scripts/**/react-error-overlay": "6.0.9" + }, "scripts": { + "preinstall": "npx npm-force-resolutions", "start": "react-scripts --openssl-legacy-provider start", "build": "react-scripts --openssl-legacy-provider build", "test": "set DEBUG_PRINT_LIMIT=20000&& react-scripts test", @@ -72,7 +78,8 @@ "eslint-formatter-gitlab": "^2.2.0", "jest-cucumber": "^3.0.1", "react-scripts": "^4.0.3", - "serve": "^11.3.2" + "serve": "^11.3.2", + "react-error-overlay": "6.0.9" }, "cypress-cucumber-preprocessor": { "nonGlobalStepDefinitions": true diff --git a/src/App.js b/src/App.js index 3dc71300459f268639124cc773ccd326d788a70f..f36925e4596b518e03905ea9c62908a6ee056c9f 100644 --- a/src/App.js +++ b/src/App.js @@ -23,29 +23,31 @@ import { HelpView } from "./views/help/HelpView"; import { AboutView } from "./views/about/AboutView"; import { SnackbarProvider } from 'notistack'; import TokenRenew from './components/auth/TokenRenew'; -import {NotificationProvider} from './components/common/notification/Notifications'; +import { NotificationProvider } from './components/common/notification/Notifications'; import NotFound from './components/navigation/NotFound'; import { applicationSubTitle } from './components/common/Helper'; +import { AppErrorBoundary } from "./components/navigation/ErrorBoundary/ErrorBoundary"; //setting up the application (TAB)title function App() { useEffect(() => { document.title = "CCCE" + applicationSubTitle(); - }, []); + }, []); return ( - <SnackbarProvider preventDuplicate maxSnack="5"> - <ThemeProvider theme={theme}> - <CssBaseline /> - <Router> - <APIProvider > - <UserProvider> - <TokenRenew /> - <Switch> - <Route path="/" - render={({ match }) => { - console.log(match) - return ( + <AppErrorBoundary> + <SnackbarProvider preventDuplicate maxSnack="5"> + <ThemeProvider theme={theme}> + <CssBaseline /> + <Router> + <APIProvider > + <UserProvider> + <TokenRenew /> + <Switch> + <Route path="/" + render={({ match }) => { + console.log(match) + return ( <NotificationProvider> <GlobalAppBar> <Switch> @@ -68,19 +70,20 @@ function App() { </Switch> </GlobalAppBar> </NotificationProvider> - ) - } - } - /> - <Route> - <NotFound /> - </Route> - </Switch> - </UserProvider> - </APIProvider> - </Router> - </ThemeProvider> - </SnackbarProvider> + ) + } + } + /> + <Route> + <NotFound /> + </Route> + </Switch> + </UserProvider> + </APIProvider> + </Router> + </ThemeProvider> + </SnackbarProvider> + </AppErrorBoundary> ); } diff --git a/src/components/navigation/ErrorBoundary/ErrorBoundary.js b/src/components/navigation/ErrorBoundary/ErrorBoundary.js new file mode 100644 index 0000000000000000000000000000000000000000..f27ff0fad67a1e6cebbc8677f1343521bd775943 --- /dev/null +++ b/src/components/navigation/ErrorBoundary/ErrorBoundary.js @@ -0,0 +1,91 @@ +import { Box, Button, Container, Grid } from "@material-ui/core"; +import { BugReport } from "@material-ui/icons"; +import React, { useCallback, useState } from "react"; +import { ErrorBoundary, useErrorHandler } from "react-error-boundary"; +import { useEffectOnMount } from "../../../hooks/MountEffects"; +import { theme } from "../../../style/Theme"; + +function AwSnap({ error, resetErrorBoundary }) { + console.log("AwSnap", error); + return ( + <> + <Grid container justifyContent="center"> + <BugReport style={{ fontSize: 128, color: theme.palette.primary.main }} /> + </Grid> + <Grid container justifyContent="center"> + <h1 style={{ textAlign: "center" }}>💥 Whoops! Something went wrong 💥</h1> + </Grid> + <Grid container justifyContent="center"> + <Button + variant="contained" + color="secondary" + style={{ backgroundColor: theme.palette.primary.main }} + onClick={resetErrorBoundary}> + Start Over + </Button> + </Grid> + <Grid container justifyContent="center"> + <Box p={10} overflow="auto"> + <pre>{error.stack}</pre> + </Box> + </Grid > + </> + ); +} + +function WindowErrorRedirect({ children }) { + const handleError = useErrorHandler(); + const wtf = useCallback((event) => { + handleError(event.error); + }, [handleError]); + + useEffectOnMount(() => { + window.addEventListener('error', wtf); + return function cleanup() { + window.removeEventListener('error', wtf); + } + }) + + return children; +} + +export function AppErrorBoundary({ children }) { + return ( + <ErrorBoundary + FallbackComponent={AwSnap} + onReset={() => { + const reload = window.confirm("The application will reload"); + if (reload) { + window.location.href = "/"; + } + }} + > + <WindowErrorRedirect> + {children} + </WindowErrorRedirect> + </ErrorBoundary> + ); +} + +export function Break() { + const [broken, setBroken] = useState(false); + const Bomb = ({ where }) => { + throw new Error(`KABOOM: You broke the UI ${where}`); + } + return ( + <Container> + <Button + variant="contained" onClick={() => setBroken(true)}> + Break the UI during render + </Button> + {broken && <Bomb where="during Render" />} + <Button + variant="contained" onClick={(event) => { + Bomb({ where: "in a Callback" }); + } + }> + Break the UI asynchronously + </Button> + </Container> + ); +} \ No newline at end of file