diff --git a/frontend-react/package.json b/frontend-react/package.json
index e1f5d66f..4f8a0d66 100644
--- a/frontend-react/package.json
+++ b/frontend-react/package.json
@@ -68,6 +68,7 @@
"nats.ws": "^1.19.1",
"react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0",
+ "react-error-boundary": "^4.0.12",
"react-router-dom": "^6.19.0",
"zod": "^3.22.4"
},
diff --git a/frontend-react/pnpm-lock.yaml b/frontend-react/pnpm-lock.yaml
index 73f77182..beb9728d 100644
--- a/frontend-react/pnpm-lock.yaml
+++ b/frontend-react/pnpm-lock.yaml
@@ -26,6 +26,9 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ react-error-boundary:
+ specifier: ^4.0.12
+ version: 4.0.12(react@18.2.0)
react-router-dom:
specifier: ^6.19.0
version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
@@ -3366,6 +3369,15 @@ packages:
scheduler: 0.23.0
dev: false
+ /react-error-boundary@4.0.12(react@18.2.0):
+ resolution: {integrity: sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==}
+ peerDependencies:
+ react: '>=16.13.1'
+ dependencies:
+ '@babel/runtime': 7.23.5
+ react: 18.2.0
+ dev: false
+
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
diff --git a/frontend-react/src/app/index.tsx b/frontend-react/src/app/index.tsx
index e0448edc..3f1d2716 100644
--- a/frontend-react/src/app/index.tsx
+++ b/frontend-react/src/app/index.tsx
@@ -1,5 +1,6 @@
import { initTelestion, registerWidgets, UserData } from '../lib';
import { simpleWidget } from './widgets/simple-widget';
+import { errorWidget } from './widgets/error-widget';
const defaultUserData: UserData = {
version: '0.0.1',
@@ -8,7 +9,7 @@ const defaultUserData: UserData = {
title: 'Default Dashboard',
layout: [
['.', '8fj2o4', '.'],
- ['.', '.', '.']
+ ['.', '9fj2o4', '9fj2o4']
]
}
},
@@ -18,11 +19,15 @@ const defaultUserData: UserData = {
configuration: {
text: 'Hello World!'
}
+ },
+ '9fj2o4': {
+ type: 'error-widget',
+ configuration: {}
}
}
};
-registerWidgets(simpleWidget);
+registerWidgets(simpleWidget, errorWidget);
await initTelestion({
version: '0.0.1',
diff --git a/frontend-react/src/app/widgets/error-widget/error-widget.tsx b/frontend-react/src/app/widgets/error-widget/error-widget.tsx
new file mode 100644
index 00000000..37aabaea
--- /dev/null
+++ b/frontend-react/src/app/widgets/error-widget/error-widget.tsx
@@ -0,0 +1,24 @@
+import { useEffect, useState } from 'react';
+
+export function ErrorWidget() {
+ const [showError, setShowError] = useState(false);
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setShowError(true);
+ }, 3_000);
+ return () => clearTimeout(timeout);
+ }, []);
+
+ if (showError) {
+ throw new Error(
+ `Test error thrown by the error widget at ${new Date().toISOString()}`
+ );
+ }
+
+ return (
+
+
This widget will throw an error after three seconds
+
It is used to test the error handling of the dashboard.
+
+ );
+}
diff --git a/frontend-react/src/app/widgets/error-widget/index.tsx b/frontend-react/src/app/widgets/error-widget/index.tsx
new file mode 100644
index 00000000..660ff20a
--- /dev/null
+++ b/frontend-react/src/app/widgets/error-widget/index.tsx
@@ -0,0 +1,14 @@
+import { ErrorWidget } from './error-widget.tsx';
+import { Widget } from '../../../lib';
+
+export const errorWidget: Widget = {
+ id: 'error-widget',
+ label: 'Error Widget',
+
+ createConfig() {
+ return {};
+ },
+
+ element: ,
+ configElement: Config
+};
diff --git a/frontend-react/src/lib/application/index.tsx b/frontend-react/src/lib/application/index.tsx
index a33d4e47..21524773 100644
--- a/frontend-react/src/lib/application/index.tsx
+++ b/frontend-react/src/lib/application/index.tsx
@@ -1,3 +1,7 @@
+// Import styles first to allow overriding bootstrap styles in CSS modules
+import 'bootstrap-icons/font/bootstrap-icons.min.css';
+import './index.scss';
+
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
@@ -6,9 +10,6 @@ import { registerWidgets } from '../widget';
import { TelestionOptions } from './model.ts';
import { createTelestionRouter } from './telestion-router.tsx';
-import 'bootstrap-icons/font/bootstrap-icons.min.css';
-import './index.scss';
-
export * from './model.ts';
export * from './hooks';
diff --git a/frontend-react/src/lib/widget/component/error-fallback.module.css b/frontend-react/src/lib/widget/component/error-fallback.module.css
new file mode 100644
index 00000000..bc3b6fe1
--- /dev/null
+++ b/frontend-react/src/lib/widget/component/error-fallback.module.css
@@ -0,0 +1,5 @@
+.alert {
+ height: 100%;
+ margin: 0;
+ overflow-y: auto;
+}
diff --git a/frontend-react/src/lib/widget/component/error-fallback.tsx b/frontend-react/src/lib/widget/component/error-fallback.tsx
new file mode 100644
index 00000000..4e5ace92
--- /dev/null
+++ b/frontend-react/src/lib/widget/component/error-fallback.tsx
@@ -0,0 +1,31 @@
+import { Alert, AlertHeading, Button } from 'react-bootstrap';
+import styles from './error-fallback.module.css';
+
+export function ErrorFallback({
+ error,
+ resetErrorBoundary
+}: {
+ error: Error;
+ resetErrorBoundary: () => void;
+}) {
+ return (
+
+ Widget Error
+
+ Unfortunately, the widget encountered an error and cannot be displayed.
+
+ Please try again or contact your developer if the problem persists.
+
+ Details
+ {error.stack}
+
+
+
+ );
+}
diff --git a/frontend-react/src/lib/widget/component/widget-renderer.tsx b/frontend-react/src/lib/widget/component/widget-renderer.tsx
index 1912c137..ac2d2fa5 100644
--- a/frontend-react/src/lib/widget/component/widget-renderer.tsx
+++ b/frontend-react/src/lib/widget/component/widget-renderer.tsx
@@ -3,6 +3,8 @@ import { getWidgetById } from '../state.ts';
import { getUserData } from '../../user-data';
import styles from './widget-renderer.module.css';
+import { ErrorBoundary } from 'react-error-boundary';
+import { ErrorFallback } from './error-fallback.tsx';
export interface WidgetRendererProps {
widgetInstanceId: string;
@@ -44,7 +46,9 @@ registerWidget({
key={`renderer-${widgetInstanceId}`}
value={widgetInstance.configuration}
>
- {widget.element}
+
+ {widget.element}
+
) : (