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

View File

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

View File

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

View File

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

View File

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