mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
c7351c4064
commit
108bd28102
44
src/frontend/src/components/Boundary.tsx
Normal file
44
src/frontend/src/components/Boundary.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user