From 4c28a77e00f7f3ef70c6ea1228f5b700723ea5b8 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Sat, 18 Mar 2023 12:29:10 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20widget=20error=20boundary=20(?= =?UTF-8?q?#753)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/widgets/error-boundary.json | 14 ++ src/tools/server/translation-namespaces.ts | 1 + src/widgets/WidgetWrapper.tsx | 11 +- src/widgets/boundary.tsx | 127 ++++++++++++++++++ 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 public/locales/en/widgets/error-boundary.json create mode 100644 src/widgets/boundary.tsx diff --git a/public/locales/en/widgets/error-boundary.json b/public/locales/en/widgets/error-boundary.json new file mode 100644 index 000000000..9b75f4080 --- /dev/null +++ b/public/locales/en/widgets/error-boundary.json @@ -0,0 +1,14 @@ +{ + "card": { + "title": "Oops, there was an error!", + "buttons": { + "details": "Details", + "tryAgain": "Try again" + } + }, + "modal": { + "text": "We're sorry for the inconvinience! This shouln't happen - please report this issue on GitHub.", + "label": "Your error", + "reportButton": "Report this error" + } +} \ No newline at end of file diff --git a/src/tools/server/translation-namespaces.ts b/src/tools/server/translation-namespaces.ts index 5fe821c3f..90aedada9 100644 --- a/src/tools/server/translation-namespaces.ts +++ b/src/tools/server/translation-namespaces.ts @@ -36,6 +36,7 @@ export const dashboardNamespaces = [ 'modules/media-server', 'modules/common-media-cards', 'modules/video-stream', + 'widgets/error-boundary', ]; export const loginNamespaces = ['authentication/login']; diff --git a/src/widgets/WidgetWrapper.tsx b/src/widgets/WidgetWrapper.tsx index bb7323073..457890537 100644 --- a/src/widgets/WidgetWrapper.tsx +++ b/src/widgets/WidgetWrapper.tsx @@ -2,6 +2,7 @@ import { ComponentType, useMemo } from 'react'; import Widgets from '.'; import { HomarrCardWrapper } from '../components/Dashboard/Tiles/HomarrCardWrapper'; import { WidgetsMenu } from '../components/Dashboard/Tiles/Widgets/WidgetsMenu'; +import ErrorBoundary from './boundary'; import { IWidget } from './widgets'; interface WidgetWrapperProps { @@ -40,9 +41,11 @@ export const WidgetWrapper = ({ const widgetWithDefaultProps = useWidget(widget); return ( - - - - + + + + + + ); }; diff --git a/src/widgets/boundary.tsx b/src/widgets/boundary.tsx new file mode 100644 index 000000000..21530f9f3 --- /dev/null +++ b/src/widgets/boundary.tsx @@ -0,0 +1,127 @@ +import Consola from 'consola'; +import React, { ReactNode } from 'react'; +import { openModal } from '@mantine/modals'; +import { withTranslation } from 'next-i18next'; +import { Button, Card, Center, Code, Group, Stack, Text, Title } from '@mantine/core'; +import { IconBrandGithub, IconBug, IconInfoCircle, IconRefresh } from '@tabler/icons'; + +type ErrorBoundaryState = { + hasError: boolean; + error: Error | undefined; +}; + +type ErrorBoundaryProps = { + t: (key: string) => string; + children: ReactNode; +}; + +/** + * A custom error boundary, that catches errors within widgets and renders an error component. + * The error component can be refreshed and shows a modal with error details + */ +class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props); + + // Define a state variable to track whether is an error or not + this.state = { hasError: false, error: undefined }; + } + + static getDerivedStateFromError(error: Error) { + // Update state so the next render will show the fallback UI + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: any) { + Consola.error(`Error while rendering widget, ${error}: ${errorInfo}`); + } + + render() { + // Check if the error is thrown + if (this.state.hasError) { + return ( + ({ + backgroundColor: theme.colors.red[5], + })} + radius="lg" + shadow="sm" + withBorder + > +
+ + + + + {this.props.t('card.title')} + + {this.state.error && ( + + {this.state.error.toString()} + + )} + + + + + ), + }) + } + leftIcon={} + variant="light" + > + {this.props.t('card.buttons.details')} + + + + +
+
+ ); + } + + // Return children components in case of no error + return this.props.children; + } +} + +export default withTranslation('widgets/error-boundary')(ErrorBoundary);