Add error handling component which supports React Router and Kernel applications.
This commit is contained in:
parent
7bde352ea7
commit
4a7e77a8f3
5 changed files with 166 additions and 4 deletions
|
@ -5,6 +5,7 @@ import {createBrowserRouter} from "react-router-dom";
|
||||||
import {Kernel} from "../src/Application/Kernel";
|
import {Kernel} from "../src/Application/Kernel";
|
||||||
import {NavTest} from "./NavTest";
|
import {NavTest} from "./NavTest";
|
||||||
import {Avocado} from "@phosphor-icons/react";
|
import {Avocado} from "@phosphor-icons/react";
|
||||||
|
import {ApplicationError} from "../src/Application/ApplicationError";
|
||||||
|
|
||||||
// Router initialization.
|
// Router initialization.
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
@ -17,6 +18,7 @@ const router = createBrowserRouter([
|
||||||
element: <NavTest />,
|
element: <NavTest />,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
errorElement: <ApplicationError />,
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {ApplicationError, ApplicationErrorBoundary} from "./ApplicationError";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Kernel UI application.
|
* Main Kernel UI application.
|
||||||
*/
|
*/
|
||||||
export function Application({children}: React.PropsWithChildren<{}>)
|
export function Application({errorElement, children}: React.PropsWithChildren<{
|
||||||
|
errorElement?: React.ReactNode;
|
||||||
|
}>)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
|
<ApplicationErrorBoundary errorElement={errorElement ?? <ApplicationError />}>
|
||||||
<main className={"app"}>
|
<main className={"app"}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
</ApplicationErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
94
src/Application/ApplicationError.tsx
Normal file
94
src/Application/ApplicationError.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React, {useContext, useState} from "react";
|
||||||
|
import {ArrowsClockwise, Bug, BugDroid} from "@phosphor-icons/react";
|
||||||
|
import {useRouteError} from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application error context.
|
||||||
|
*/
|
||||||
|
const ApplicationErrorContext = React.createContext<Error>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current error from context or router.
|
||||||
|
*/
|
||||||
|
export function useApplicationError(): Error
|
||||||
|
{
|
||||||
|
// Get error from context or router.
|
||||||
|
const error = useContext(ApplicationErrorContext);
|
||||||
|
const routeError = useRouteError() as Error;
|
||||||
|
return error ?? routeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application error component.
|
||||||
|
*/
|
||||||
|
export function ApplicationError({children}: {
|
||||||
|
children?: (error: Error) => React.ReactElement;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// Get error from context.
|
||||||
|
const error = useApplicationError();
|
||||||
|
|
||||||
|
// Show details state.
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={"error"}>
|
||||||
|
{children ? children(error) : (
|
||||||
|
<>
|
||||||
|
<Bug size={64} weight={"duotone"} />
|
||||||
|
<h1>Error</h1>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2>{error.name}</h2>
|
||||||
|
|
||||||
|
<p>An unexpected error happened and the application was forced to quit.</p>
|
||||||
|
|
||||||
|
<pre className={"error"}>{error.message}</pre>
|
||||||
|
|
||||||
|
<button onClick={() => window.location.reload()}><ArrowsClockwise size={20} /> Restart application</button>
|
||||||
|
|
||||||
|
<div className={"details"}>
|
||||||
|
<button onClick={() => setShowDetails(!showDetails)}><BugDroid size={18} /> {showDetails ? "Hide" : "Show"} details</button>
|
||||||
|
|
||||||
|
{ // Show details if required.
|
||||||
|
showDetails && (
|
||||||
|
<pre>
|
||||||
|
{error.stack}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error boundary component for the application.
|
||||||
|
*/
|
||||||
|
export class ApplicationErrorBoundary extends React.Component<React.PropsWithChildren<{
|
||||||
|
errorElement: React.ReactNode;
|
||||||
|
}>, {
|
||||||
|
error: Error;
|
||||||
|
}>
|
||||||
|
{
|
||||||
|
static getDerivedStateFromError(error: Error)
|
||||||
|
{
|
||||||
|
return { error: error };
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
if (this.state?.error)
|
||||||
|
// An error happened, showing the application error content.
|
||||||
|
return (
|
||||||
|
<ApplicationErrorContext.Provider value={this.state?.error}>
|
||||||
|
{this.props.errorElement}
|
||||||
|
</ApplicationErrorContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
@import "components/_button";
|
@import "components/_button";
|
||||||
@import "components/_card";
|
@import "components/_card";
|
||||||
@import "components/_dates";
|
@import "components/_dates";
|
||||||
|
@import "components/_errors";
|
||||||
@import "components/_floating";
|
@import "components/_floating";
|
||||||
@import "components/_form";
|
@import "components/_form";
|
||||||
@import "components/_headings";
|
@import "components/_headings";
|
||||||
|
|
60
src/styles/components/_errors.less
Normal file
60
src/styles/components/_errors.less
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
main.error
|
||||||
|
{
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
> svg
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
margin: 1em auto;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr
|
||||||
|
{
|
||||||
|
margin: 1em auto;
|
||||||
|
|
||||||
|
width: 20em;
|
||||||
|
max-width: 50%;
|
||||||
|
height: 0.25em;
|
||||||
|
border-radius: 0.1em;
|
||||||
|
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
background: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6
|
||||||
|
{
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, pre
|
||||||
|
{
|
||||||
|
margin: 1em auto;
|
||||||
|
width: 40em;
|
||||||
|
max-width: 95%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
margin: 1em auto;
|
||||||
|
padding: 0.66em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details
|
||||||
|
{
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
button
|
||||||
|
{}
|
||||||
|
|
||||||
|
pre
|
||||||
|
{
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue