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()}
+
+ )}
+
+
+
+ this.setState({ hasError: false })}
+ leftIcon={}
+ variant="light"
+ >
+ {this.props.t('card.buttons.tryAgain')}
+
+
+
+
+
+ );
+ }
+
+ // Return children components in case of no error
+ return this.props.children;
+ }
+}
+
+export default withTranslation('widgets/error-boundary')(ErrorBoundary);