Improve error boundary granularity with page and section level boundaries
Implement three-level error boundary strategy: - Top-level (app shell): catches critical failures - Page-level: preserves navigation when page crashes - Section-level: graceful degradation for charts/tables Create new components: - PageErrorBoundary: wraps page routes - SectionErrorBoundary: wraps data-heavy sections Enhance ErrorBoundary with customizable titles, messages, and reload behavior. Apply page boundaries to all route handlers in App.tsx. Apply section boundaries to: - DashboardPage: server status, ban trend, country charts, ban list - JailsPage: jail overview, ban/unban form, IP lookup - MapPage: world map, ban table - ConfigPage: configuration editor - HistoryPage: history table, IP detail view - BlocklistsPage: sources, schedule, import log Update Web-Development.md with error boundary strategy documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
* React error boundary component.
|
||||
*
|
||||
* Catches render-time exceptions in child components and shows a fallback UI.
|
||||
* This is the base component; use PageErrorBoundary or SectionErrorBoundary
|
||||
* for page and section-level boundaries.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Button, makeStyles, Text, tokens } from "@fluentui/react-components";
|
||||
@@ -13,14 +15,22 @@ interface ErrorBoundaryState {
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
message?: string;
|
||||
showReloadButton?: boolean;
|
||||
isFullPage?: boolean;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryFallbackProps {
|
||||
title: string;
|
||||
message: string;
|
||||
showReloadButton: boolean;
|
||||
isFullPage: boolean;
|
||||
onReload: () => void;
|
||||
}
|
||||
|
||||
const useFallbackStyles = makeStyles({
|
||||
const useFullPageStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -36,20 +46,46 @@ const useFallbackStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
function ErrorBoundaryFallback({ message, onReload }: ErrorBoundaryFallbackProps): React.ReactElement {
|
||||
const styles = useFallbackStyles();
|
||||
const useSectionStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
padding: tokens.spacingVerticalM,
|
||||
backgroundColor: tokens.colorNeutralBackground3,
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
border: `1px solid ${tokens.colorStatusWarningForeground1}`,
|
||||
gap: tokens.spacingVerticalM,
|
||||
},
|
||||
message: {
|
||||
color: tokens.colorNeutralForeground1,
|
||||
},
|
||||
});
|
||||
|
||||
function ErrorBoundaryFallback({
|
||||
title,
|
||||
message,
|
||||
showReloadButton,
|
||||
isFullPage,
|
||||
onReload,
|
||||
}: ErrorBoundaryFallbackProps): React.ReactElement {
|
||||
const fullPageStyles = useFullPageStyles();
|
||||
const sectionStyles = useSectionStyles();
|
||||
const styles = isFullPage ? fullPageStyles : sectionStyles;
|
||||
|
||||
return (
|
||||
<div className={styles.root} role="alert">
|
||||
<Text as="h1" size={700} weight="semibold">
|
||||
Something went wrong
|
||||
<Text as={isFullPage ? "h1" : "h2"} size={isFullPage ? 700 : 500} weight="semibold">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size={300} className={styles.message}>
|
||||
<Text size={isFullPage ? 300 : 200} className={isFullPage ? fullPageStyles.message : sectionStyles.message}>
|
||||
{message}
|
||||
</Text>
|
||||
<Button appearance="primary" onClick={onReload}>
|
||||
Reload
|
||||
</Button>
|
||||
{showReloadButton && (
|
||||
<Button appearance="primary" onClick={onReload}>
|
||||
{isFullPage ? "Reload Page" : "Retry"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,18 +101,37 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
console.error("ErrorBoundary caught an error", { error, errorInfo });
|
||||
const { onError } = this.props;
|
||||
if (onError) {
|
||||
onError(error, errorInfo);
|
||||
} else {
|
||||
console.error("ErrorBoundary caught an error", { error, errorInfo });
|
||||
}
|
||||
}
|
||||
|
||||
handleReload = (): void => {
|
||||
window.location.reload();
|
||||
if (this.props.isFullPage) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.setState({ hasError: false, errorMessage: null });
|
||||
}
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
const {
|
||||
title = "Something went wrong",
|
||||
message = "Please try again or contact support if the problem persists.",
|
||||
showReloadButton = true,
|
||||
isFullPage = true,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ErrorBoundaryFallback
|
||||
message={this.state.errorMessage ?? "Please try reloading the page."}
|
||||
title={title}
|
||||
message={message}
|
||||
showReloadButton={showReloadButton}
|
||||
isFullPage={isFullPage}
|
||||
onReload={this.handleReload}
|
||||
/>
|
||||
);
|
||||
|
||||
42
frontend/src/components/PageErrorBoundary.tsx
Normal file
42
frontend/src/components/PageErrorBoundary.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Page-level error boundary.
|
||||
*
|
||||
* Wraps entire page components to catch rendering errors while preserving
|
||||
* the app shell (navigation, theme, auth). When an error occurs, shows a
|
||||
* full-page fallback but the user can still navigate away via the sidebar.
|
||||
*
|
||||
* Use this for wrapping page components in App.tsx routes.
|
||||
*/
|
||||
import React from "react";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
interface PageErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
pageName?: string;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a page component with error boundary protection.
|
||||
*
|
||||
* @param children - Page component to wrap
|
||||
* @param pageName - Name of page for error message (default: "Page")
|
||||
* @param onError - Optional callback for error logging
|
||||
*/
|
||||
export function PageErrorBoundary({
|
||||
children,
|
||||
pageName = "Page",
|
||||
onError,
|
||||
}: PageErrorBoundaryProps): React.JSX.Element {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
title={`${pageName} Error`}
|
||||
message={`The ${pageName.toLowerCase()} encountered an error and could not load. Please try navigating to another page or reloading.`}
|
||||
showReloadButton={true}
|
||||
isFullPage={false}
|
||||
onError={onError}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
46
frontend/src/components/SectionErrorBoundary.tsx
Normal file
46
frontend/src/components/SectionErrorBoundary.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Section-level error boundary.
|
||||
*
|
||||
* Wraps individual data-heavy sections (charts, tables, forms) within a page
|
||||
* to provide graceful degradation. When an error occurs, only that section
|
||||
* fails to render; the rest of the page remains functional.
|
||||
*
|
||||
* Use this to wrap:
|
||||
* - Charts (BanTrendChart, TopCountriesBarChart, etc.)
|
||||
* - Data tables (BanTable, JailOverviewSection, etc.)
|
||||
* - Forms with complex logic
|
||||
* - Any component that fetches data or has risky side effects
|
||||
*/
|
||||
import React from "react";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
interface SectionErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
sectionName?: string;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a section component with error boundary protection.
|
||||
*
|
||||
* @param children - Section component to wrap
|
||||
* @param sectionName - Name of section for error message (default: "Section")
|
||||
* @param onError - Optional callback for error logging
|
||||
*/
|
||||
export function SectionErrorBoundary({
|
||||
children,
|
||||
sectionName = "Section",
|
||||
onError,
|
||||
}: SectionErrorBoundaryProps): React.JSX.Element {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
title={`${sectionName} Unavailable`}
|
||||
message={`Could not load ${sectionName.toLowerCase()}. The rest of the page is still functional.`}
|
||||
showReloadButton={true}
|
||||
isFullPage={false}
|
||||
onError={onError}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user