Add widget error boundary (#753)

This commit is contained in:
Manuel
2023-03-18 12:29:10 +01:00
committed by GitHub
parent c41f547af3
commit 4c28a77e00
4 changed files with 149 additions and 4 deletions

View File

@@ -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"
}
}

View File

@@ -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'];

View File

@@ -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 (
<ErrorBoundary>
<HomarrCardWrapper className={className}>
<WidgetsMenu integration={widgetId} widget={widgetWithDefaultProps} />
<WidgetComponent widget={widgetWithDefaultProps} />
</HomarrCardWrapper>
</ErrorBoundary>
);
};

127
src/widgets/boundary.tsx Normal file
View File

@@ -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<ErrorBoundaryProps, ErrorBoundaryState> {
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 (
<Card
m={10}
sx={(theme) => ({
backgroundColor: theme.colors.red[5],
})}
radius="lg"
shadow="sm"
withBorder
>
<Center>
<Stack align="center">
<IconBug color="white" />
<Stack spacing={0} align="center">
<Title order={4} color="white" align="center">
{this.props.t('card.title')}
</Title>
{this.state.error && (
<Text color="white" align="center" size="sm">
{this.state.error.toString()}
</Text>
)}
</Stack>
<Group>
<Button
onClick={() =>
openModal({
title: 'Your widget had an error',
children: (
<>
<Text size="sm" mb="sm">
{this.props.t('modal.text')}
</Text>
{this.state.error && (
<>
<Text weight="bold" size="sm">
{this.props.t('modal.label')}
</Text>
<Code block>{this.state.error.toString()}</Code>
</>
)}
<Button
sx={(theme) => ({
backgroundColor: theme.colors.gray[8],
'&:hover': {
backgroundColor: theme.colors.gray[9],
},
})}
leftIcon={<IconBrandGithub />}
component="a"
href="https://github.com/ajnart/homarr/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug.yml&title=New%20bug"
target="_blank"
mt="md"
fullWidth
>
{(this.props.t('modal.reportButton'))}
</Button>
</>
),
})
}
leftIcon={<IconInfoCircle size={16} />}
variant="light"
>
{this.props.t('card.buttons.details')}
</Button>
<Button
onClick={() => this.setState({ hasError: false })}
leftIcon={<IconRefresh size={16} />}
variant="light"
>
{this.props.t('card.buttons.tryAgain')}
</Button>
</Group>
</Stack>
</Center>
</Card>
);
}
// Return children components in case of no error
return this.props.children;
}
}
export default withTranslation('widgets/error-boundary')(ErrorBoundary);