From 108bd2810240ab29b663f2a0de0a26c8af6080d3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 8 May 2024 07:32:01 +1000 Subject: [PATCH] [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 --- src/frontend/src/components/Boundary.tsx | 44 ++++ src/frontend/src/components/forms/ApiForm.tsx | 145 ++++++----- src/frontend/src/components/nav/Layout.tsx | 14 +- .../src/components/nav/PanelGroup.tsx | 133 +++++----- .../src/components/nav/SearchDrawer.tsx | 75 +++--- src/frontend/src/tables/InvenTreeTable.tsx | 245 +++++++++--------- 6 files changed, 363 insertions(+), 293 deletions(-) create mode 100644 src/frontend/src/components/Boundary.tsx diff --git a/src/frontend/src/components/Boundary.tsx b/src/frontend/src/components/Boundary.tsx new file mode 100644 index 0000000000..6f317efb03 --- /dev/null +++ b/src/frontend/src/components/Boundary.tsx @@ -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 ( + } + title={t`Error rendering component` + `: ${title}`} + > + {t`An error occurred while rendering this component. Refer to the console for more information.`} + + ); +} + +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 ( + } + onError={onError} + > + {children} + + ); +} diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 20258df17c..74a3047048 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -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,81 +473,89 @@ export function ApiForm({ return ( - {/* Show loading overlay while fetching fields */} - {/* zIndex used to force overlay on top of modal header bar */} - + + {/* Show loading overlay while fetching fields */} + {/* zIndex used to force overlay on top of modal header bar */} + - {/* Attempt at making fixed footer with scroll area */} - -
- {/* Form Fields */} - - {(!isValid || nonFieldErrors.length > 0) && ( - - {nonFieldErrors.length > 0 && ( - - {nonFieldErrors.map((message) => ( - {message} - ))} - + {/* Attempt at making fixed footer with scroll area */} + +
+ {/* Form Fields */} + + {(!isValid || nonFieldErrors.length > 0) && ( + + {nonFieldErrors.length > 0 && ( + + {nonFieldErrors.map((message) => ( + {message} + ))} + + )} + + )} + + {props.preFormContent} + {props.preFormSuccess && ( + + {props.preFormSuccess} + )} - - )} - {props.preFormContent} - {props.preFormSuccess && ( - - {props.preFormSuccess} - - )} - {props.preFormWarning && ( - - {props.preFormWarning} - - )} - - - {!optionsLoading && - Object.entries(fields).map(([fieldName, field]) => ( - - ))} - - - {props.postFormContent} - -
-
+ {props.preFormWarning && ( + + {props.preFormWarning} + + )} + + + + + {!optionsLoading && + Object.entries(fields).map(([fieldName, field]) => ( + + ))} + + + + + {props.postFormContent} + +
+
+
- {/* Footer with Action Buttons */} - -
- - {props.actions?.map((action, i) => ( + {/* Footer with Action Buttons */} + +
+ + {props.actions?.map((action, i) => ( + + ))} - ))} - - -
+
+
+
); } diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx index d1e8a69264..1290d4ee25 100644 --- a/src/frontend/src/components/nav/Layout.tsx +++ b/src/frontend/src/components/nav/Layout.tsx @@ -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(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() {
- + + + + {/* */}