[PUI] Error boundary (#7176)

* Create error boundary component

- Ref: https://docs.sentry.io/platforms/javascript/guides/react/features/error-boundary/
- Keeps errors container to local components
- Will be critical for plugin support

* Add boundary to API forms
This commit is contained in:
Oliver 2024-05-08 07:32:01 +10:00 committed by GitHub
parent c7351c4064
commit 108bd28102
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 363 additions and 293 deletions

View File

@ -0,0 +1,44 @@
import { t } from '@lingui/macro';
import { Alert } from '@mantine/core';
import { ErrorBoundary, FallbackRender } from '@sentry/react';
import { IconExclamationCircle } from '@tabler/icons-react';
import { ReactNode, useCallback } from 'react';
function DefaultFallback({ title }: { title: String }): ReactNode {
return (
<Alert
color="red"
icon={<IconExclamationCircle />}
title={t`Error rendering component` + `: ${title}`}
>
{t`An error occurred while rendering this component. Refer to the console for more information.`}
</Alert>
);
}
export function Boundary({
children,
label,
fallback
}: {
children: ReactNode;
label: string;
fallback?: React.ReactElement | FallbackRender | undefined;
}): ReactNode {
const onError = useCallback(
(error: Error, componentStack: string, eventId: string) => {
console.error(`Error rendering component: ${label}`);
console.error(error, componentStack);
},
[]
);
return (
<ErrorBoundary
fallback={fallback ?? <DefaultFallback title={label} />}
onError={onError}
>
{children}
</ErrorBoundary>
);
}

View File

@ -36,6 +36,7 @@ import {
import { invalidResponse } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import { PathParams } from '../../states/ApiState';
import { Boundary } from '../Boundary';
import {
ApiFormField,
ApiFormFieldSet,
@ -472,6 +473,7 @@ export function ApiForm({
return (
<Stack>
<Boundary label={`ApiForm-${id}`}>
{/* Show loading overlay while fetching fields */}
{/* zIndex used to force overlay on top of modal header bar */}
<LoadingOverlay visible={isLoading} zIndex={1010} />
@ -492,6 +494,7 @@ export function ApiForm({
)}
</Alert>
)}
<Boundary label={`ApiForm-${id}-PreFormContent`}>
{props.preFormContent}
{props.preFormSuccess && (
<Alert color="green" radius="sm">
@ -503,6 +506,8 @@ export function ApiForm({
{props.preFormWarning}
</Alert>
)}
</Boundary>
<Boundary label={`ApiForm-${id}-FormContent`}>
<FormProvider {...form}>
<Stack spacing="xs">
{!optionsLoading &&
@ -516,7 +521,10 @@ export function ApiForm({
))}
</Stack>
</FormProvider>
</Boundary>
<Boundary label={`ApiForm-${id}-PostFormContent`}>
{props.postFormContent}
</Boundary>
</Stack>
</div>
</Paper>
@ -547,6 +555,7 @@ export function ApiForm({
</Button>
</Group>
</div>
</Boundary>
</Stack>
);
}

View File

@ -8,6 +8,7 @@ import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { getActions } from '../../defaults/actions';
import { InvenTreeStyle } from '../../globalStyle';
import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary';
import { Footer } from './Footer';
import { Header } from './Header';
@ -29,17 +30,17 @@ export default function LayoutComponent() {
const navigate = useNavigate();
const location = useLocation();
const defaultactions = getActions(navigate);
const [actions, setActions] = useState(defaultactions);
const defaultActions = getActions(navigate);
const [actions, setActions] = useState(defaultActions);
const [customActions, setCustomActions] = useState<boolean>(false);
function actionsAreChanging(change: []) {
if (change.length > defaultactions.length) setCustomActions(true);
if (change.length > defaultActions.length) setCustomActions(true);
setActions(change);
}
useEffect(() => {
if (customActions) {
setActions(defaultactions);
setActions(defaultActions);
setCustomActions(false);
}
}, [location]);
@ -57,7 +58,10 @@ export default function LayoutComponent() {
<Flex direction="column" mih="100vh">
<Header />
<Container className={classes.layoutContent} size="100%">
<Boundary label={'layout'}>
<Outlet />
</Boundary>
{/* </ErrorBoundary> */}
</Container>
<Space h="xl" />
<Footer />

View File

@ -20,6 +20,7 @@ import {
} from 'react-router-dom';
import { useLocalState } from '../../states/LocalState';
import { Boundary } from '../Boundary';
import { PlaceholderPanel } from '../items/Placeholder';
import { StylishText } from '../items/StylishText';
@ -103,6 +104,7 @@ function BasePanelGroup({
const [expanded, setExpanded] = useState<boolean>(true);
return (
<Boundary label={`PanelGroup-${pageKey}`}>
<Paper p="sm" radius="xs" shadow="xs">
<Tabs
value={panel}
@ -167,13 +169,16 @@ function BasePanelGroup({
<Divider />
</>
)}
<Boundary label={`PanelContent-${panel.name}`}>
{panel.content ?? <PlaceholderPanel />}
</Boundary>
</Stack>
</Tabs.Panel>
)
)}
</Tabs>
</Paper>
</Boundary>
);
}

View File

@ -36,6 +36,7 @@ import { UserRoles } from '../../enums/Roles';
import { apiUrl } from '../../states/ApiState';
import { useUserSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary';
import { RenderInstance } from '../render/Instance';
import { ModelInformationDict } from '../render/ModelType';
@ -386,6 +387,7 @@ export function SearchDrawer({
</Group>
}
>
<Boundary label="SearchDrawer">
{searchQuery.isFetching && (
<Center>
<Loader />
@ -428,6 +430,7 @@ export function SearchDrawer({
<Trans>No results available for search query</Trans>
</Alert>
)}
</Boundary>
</Drawer>
);
}

View File

@ -29,6 +29,7 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../App';
import { Boundary } from '../components/Boundary';
import { ActionButton } from '../components/buttons/ActionButton';
import { ButtonMenu } from '../components/buttons/ButtonMenu';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
@ -557,6 +558,7 @@ export function InvenTreeTable<T = any>({
onClose={() => setFiltersVisible(false)}
/>
)}
<Boundary label="inventreetable">
<Stack spacing="sm">
<Group position="apart">
<Group position="left" key="custom-actions" spacing={5}>
@ -638,7 +640,9 @@ export function InvenTreeTable<T = any>({
</Group>
<Box pos="relative">
<LoadingOverlay
visible={tableOptionQuery.isLoading || tableOptionQuery.isFetching}
visible={
tableOptionQuery.isLoading || tableOptionQuery.isFetching
}
/>
<DataTable
@ -682,6 +686,7 @@ export function InvenTreeTable<T = any>({
/>
</Box>
</Stack>
</Boundary>
</>
);
}