From 6ca0b802c5f41414bccaab217a3058b545ea1344 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 26 Dec 2024 19:41:00 +0000 Subject: [PATCH 01/10] wip: commit progress on new error boundary --- site/src/components/Button/Button.tsx | 5 +- .../ErrorBoundary/GlobalErrorBoundary.tsx | 148 ++++++++++++++++++ site/src/pages/LoginPage/LoginPage.tsx | 10 +- site/src/router.tsx | 6 +- 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index b0a460eca8a3c..ca773cf0a93d1 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -51,9 +51,12 @@ export const Button: FC = forwardRef< const Comp = asChild ? Slot : "button"; return ( ); }); diff --git a/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx b/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx new file mode 100644 index 0000000000000..f409e2689b02a --- /dev/null +++ b/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx @@ -0,0 +1,148 @@ +/** + * @file A global error boundary designed to work with React Router. + * + * This is not documented well, but because of React Router works, it will + * automatically intercept any render errors produced in routes, and will + * "swallow" them, preventing the errors from bubbling up to any error + * boundaries above the router. The global error boundary must be explicitly + * bound to a route to work as expected. + */ +import type { Interpolation } from "@emotion/react"; +import { Button } from "components/Button/Button"; +import { useState, type FC } from "react"; +import { + type ErrorResponse, + isRouteErrorResponse, + Link, + useLocation, + useRouteError, +} from "react-router-dom"; + +export const GlobalErrorBoundary: FC = () => { + const error = useRouteError(); + return ; +}; + +type GlobalErrorBoundaryInnerProps = Readonly<{ error: unknown }>; +export const GlobalErrorBoundaryInner: FC = ({ + error, +}) => { + const [showErrorMessage, setShowErrorMessage] = useState(false); + const location = useLocation(); + const isRenderableError = + error instanceof Error || isRouteErrorResponse(error); + + return ( +
+
+
+ + Coder logo + + + + + + + + + + + + + + + +
+

Unexpected error

+

+ Your Coder deployment has run into a UI error – we apologize + for the inconvenience. If this error continues, please reach out + to the Coder team. +

+
+
+ +
+ + + {isRenderableError && ( + + )} +
+ + {isRenderableError && showErrorMessage && } +
+
+ ); +}; + +type ErrorStackProps = Readonly<{ error: Error | ErrorResponse }>; +const ErrorStack: FC = ({ error }) => { + return ( + + ); +}; + +function serializeDataAsJson(data: unknown): string | null { + try { + return JSON.stringify(data, null, 2); + } catch { + return null; + } +} diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 9a367c1c13801..9d69ce4844b5d 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -2,7 +2,7 @@ import { buildInfo } from "api/queries/buildInfo"; import { authMethods } from "api/queries/users"; import { useAuthContext } from "contexts/auth/AuthProvider"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { type FC, useEffect } from "react"; +import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { Navigate, useLocation, useNavigate } from "react-router-dom"; @@ -12,6 +12,11 @@ import { sendDeploymentEvent } from "utils/telemetry"; import { LoginPageView } from "./LoginPageView"; export const LoginPage: FC = () => { + const [blowUp, setBlowUp] = useState(true); + if (blowUp) { + throw new Error("Blah"); + } + const location = useLocation(); const { isLoading, @@ -80,6 +85,9 @@ export const LoginPage: FC = () => { Sign in to {applicationName} + { export const router = createBrowserRouter( createRoutesFromChildren( - }> + } + errorElement={} + > } /> } /> From f8c5827225538696982052197c88506ac5102021 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 26 Dec 2024 21:44:07 +0000 Subject: [PATCH 02/10] chore: finish initial version of new error boundary --- site/src/App.tsx | 9 +- .../ErrorBoundary/GlobalErrorBoundary.tsx | 133 ++++++++++++------ site/src/pages/LoginPage/LoginPage.tsx | 8 -- 3 files changed, 96 insertions(+), 54 deletions(-) diff --git a/site/src/App.tsx b/site/src/App.tsx index 56cd193029472..be40da7eb0c0c 100644 --- a/site/src/App.tsx +++ b/site/src/App.tsx @@ -10,7 +10,6 @@ import { import { HelmetProvider } from "react-helmet-async"; import { QueryClient, QueryClientProvider } from "react-query"; import { RouterProvider } from "react-router-dom"; -import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"; import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"; import { ThemeProvider } from "./contexts/ThemeProvider"; import { AuthProvider } from "./contexts/auth/AuthProvider"; @@ -81,11 +80,9 @@ export const AppProviders: FC = ({ export const App: FC = () => { return ( - - - - - + + + ); }; diff --git a/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx b/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx index f409e2689b02a..ee204df5a3168 100644 --- a/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx +++ b/site/src/components/ErrorBoundary/GlobalErrorBoundary.tsx @@ -8,16 +8,22 @@ * bound to a route to work as expected. */ import type { Interpolation } from "@emotion/react"; +import Link from "@mui/material/Link"; import { Button } from "components/Button/Button"; +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useState, type FC } from "react"; +import { Helmet } from "react-helmet-async"; import { - type ErrorResponse, isRouteErrorResponse, - Link, useLocation, useRouteError, + type ErrorResponse, } from "react-router-dom"; +const errorPageTitle = "Something went wrong"; + +// Mocking React Router's error-handling logic is a pain; the next best thing is +// to split it off from the rest of the code, and pass the value via props export const GlobalErrorBoundary: FC = () => { const error = useRouteError(); return ; @@ -28,67 +34,77 @@ export const GlobalErrorBoundaryInner: FC = ({ error, }) => { const [showErrorMessage, setShowErrorMessage] = useState(false); + const { metadata } = useEmbeddedMetadata(); const location = useLocation(); + + const coderVersion = metadata["build-info"].value?.version; const isRenderableError = error instanceof Error || isRouteErrorResponse(error); return ( -
+
+ + {errorPageTitle} + +
-
+
Coder logo - - - - - - + + + + + + - +
-

Unexpected error

+

{errorPageTitle}

- Your Coder deployment has run into a UI error – we apologize - for the inconvenience. If this error continues, please reach out - to the Coder team. + Please try reloading the page. If reloading does not work, you can + ask for help in the{" "} + + Coder Discord community + (link opens in a new tab) + {" "} + or{" "} + + open an issue on GitHub + (link opens in a new tab) + + .

- {isRenderableError && ( @@ -111,14 +127,14 @@ export const GlobalErrorBoundaryInner: FC = ({ type ErrorStackProps = Readonly<{ error: Error | ErrorResponse }>; const ErrorStack: FC = ({ error }) => { return ( -