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) => ( + + {action.text} + + ))} - {action.text} + {props.submitText ?? t`Submit`} - ))} - - {props.submitText ?? t`Submit`} - - - + + + ); } 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() { - + + + + {/* */} diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index 687d0f1b32..26779a3266 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -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,77 +104,81 @@ function BasePanelGroup({ const [expanded, setExpanded] = useState(true); return ( - - - + + + + + {panels.map( + (panel) => + !panel.hidden && ( + + )} // Enable when implementing Icon manager everywhere + icon={panel.icon} + hidden={panel.hidden} + disabled={panel.disabled} + style={{ cursor: panel.disabled ? 'unset' : 'pointer' }} + > + {expanded && panel.label} + + + ) + )} + {collapsible && ( + setExpanded(!expanded)} + > + {expanded ? ( + + ) : ( + + )} + + )} + {panels.map( (panel) => !panel.hidden && ( - - )} // Enable when implementing Icon manager everywhere - icon={panel.icon} - hidden={panel.hidden} - disabled={panel.disabled} - style={{ cursor: panel.disabled ? 'unset' : 'pointer' }} - > - {expanded && panel.label} - - + + {panel.showHeadline !== false && ( + <> + {panel.label} + + > + )} + + {panel.content ?? } + + + ) )} - {collapsible && ( - setExpanded(!expanded)} - > - {expanded ? ( - - ) : ( - - )} - - )} - - {panels.map( - (panel) => - !panel.hidden && ( - - - {panel.showHeadline !== false && ( - <> - {panel.label} - - > - )} - {panel.content ?? } - - - ) - )} - - + + + ); } diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index d6b491f926..5381f07da6 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -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,48 +387,50 @@ export function SearchDrawer({ } > - {searchQuery.isFetching && ( - - - - )} - {!searchQuery.isFetching && !searchQuery.isError && ( - - {queryResults.map((query, idx) => ( - removeResults(query)} - onResultClick={(query, pk) => onResultClick(query, pk)} - /> - ))} - - )} - {searchQuery.isError && ( - } - > - An error occurred during search query - - )} - {searchText && - !searchQuery.isFetching && - !searchQuery.isError && - queryResults.length == 0 && ( + + {searchQuery.isFetching && ( + + + + )} + {!searchQuery.isFetching && !searchQuery.isError && ( + + {queryResults.map((query, idx) => ( + removeResults(query)} + onResultClick={(query, pk) => onResultClick(query, pk)} + /> + ))} + + )} + {searchQuery.isError && ( } + title={t`Error`} + icon={} > - No results available for search query + An error occurred during search query )} + {searchText && + !searchQuery.isFetching && + !searchQuery.isError && + queryResults.length == 0 && ( + } + > + No results available for search query + + )} + ); } diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 8f6e0d64b3..083a3d5511 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -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,131 +558,135 @@ export function InvenTreeTable({ onClose={() => setFiltersVisible(false)} /> )} - - - - {tableProps.tableActions?.map((group, idx) => ( - {group} - ))} - {(tableProps.barcodeActions?.length ?? 0 > 0) && ( - } - label={t`Barcode actions`} - tooltip={t`Barcode actions`} - actions={tableProps.barcodeActions ?? []} - /> - )} - {(tableProps.printingActions?.length ?? 0 > 0) && ( - } - label={t`Print actions`} - tooltip={t`Print actions`} - actions={tableProps.printingActions ?? []} - /> - )} - {(tableProps.enableBulkDelete ?? false) && ( - } - color="red" - tooltip={t`Delete selected records`} - onClick={deleteSelectedRecords} - /> - )} - - - - {tableProps.enableSearch && ( - - tableState.setSearchTerm(term) - } - /> - )} - {tableProps.enableRefresh && ( - - - refetch()} /> - - - )} - {hasSwitchableColumns && ( - - )} - {tableProps.enableFilters && filters.length > 0 && ( - + + + + + {tableProps.tableActions?.map((group, idx) => ( + {group} + ))} + {(tableProps.barcodeActions?.length ?? 0 > 0) && ( + } + label={t`Barcode actions`} + tooltip={t`Barcode actions`} + actions={tableProps.barcodeActions ?? []} + /> + )} + {(tableProps.printingActions?.length ?? 0 > 0) && ( + } + label={t`Print actions`} + tooltip={t`Print actions`} + actions={tableProps.printingActions ?? []} + /> + )} + {(tableProps.enableBulkDelete ?? false) && ( + } + color="red" + tooltip={t`Delete selected records`} + onClick={deleteSelectedRecords} + /> + )} + + + + {tableProps.enableSearch && ( + + tableState.setSearchTerm(term) + } + /> + )} + {tableProps.enableRefresh && ( - - setFiltersVisible(!filtersVisible)} - /> + + refetch()} /> - - )} - {tableProps.enableDownload && ( - - )} + )} + {hasSwitchableColumns && ( + + )} + {tableProps.enableFilters && filters.length > 0 && ( + + + + setFiltersVisible(!filtersVisible)} + /> + + + + )} + {tableProps.enableDownload && ( + + )} + - - - - - + - - + /> + + + + + > ); }