Bug/various bugs (#499)

* The ErrorBoundary is super powerful and useful.  There is a default implementation that seems to be recommended now, so dropping our homegrown one for the standard one.

We can now render custom components when an error happens within an error boundary, and we can use error boundaries within sub-components as we now do in the reactFormBuilder which will capture form rendering errors, and allow you to fix the error and retry.

The more global ErrorBoundary set in the "ContainerForExtensions" now users a the ErrorBoundaryFallack to render the error - which looks a little cleaner, and tries to offer a little more information about what went wrong.
This commit is contained in:
Dan Funk 2023-09-19 09:08:57 -04:00 committed by GitHub
parent 43760689d0
commit 8eb93802aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 45 deletions

View File

@ -53,6 +53,7 @@
"react-datepicker": "^4.8.0",
"react-devtools": "^4.27.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-icons": "^4.4.0",
"react-jsonschema-form": "^1.8.1",
"react-router": "^6.3.0",
@ -25438,6 +25439,17 @@
"react": "^18.2.0"
}
},
"node_modules/react-error-boundary": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
"integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
@ -50937,6 +50949,14 @@
"scheduler": "^0.23.0"
}
},
"react-error-boundary": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
"integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
"requires": {
"@babel/runtime": "^7.12.5"
}
},
"react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",

View File

@ -57,7 +57,8 @@
"timepicker": "^1.13.18",
"typescript": "^4.7.4",
"use-debounce": "^9.0.4",
"web-vitals": "^3.0.2"
"web-vitals": "^3.0.2",
"react-error-boundary": "^4.0.11"
},
"overrides": {
"postcss-preset-env": {

View File

@ -1,11 +1,11 @@
import { Content } from '@carbon/react';
import { Routes, Route } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import NavigationBar from './components/NavigationBar';
import HomePageRoutes from './routes/HomePageRoutes';
import About from './routes/About';
import ErrorBoundary from './components/ErrorBoundary';
import AdminRoutes from './routes/AdminRoutes';
import ScrollToTop from './components/ScrollToTop';
@ -19,6 +19,7 @@ import {
UiSchemaUxElement,
} from './extension_ui_schema_interfaces';
import HttpService from './services/HttpService';
import { ErrorBoundaryFallback } from './ErrorBoundaryFallack';
export default function ContainerForExtensions() {
const [extensionUxElements, setExtensionNavigationItems] = useState<
@ -84,7 +85,7 @@ export default function ContainerForExtensions() {
<NavigationBar extensionUxElements={extensionUxElements} />
<Content className={contentClassName}>
<ScrollToTop />
<ErrorBoundary>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<Routes>
<Route path="/*" element={<HomePageRoutes />} />
<Route path="/about" element={<About />} />

View File

@ -0,0 +1,133 @@
import { Button, Content } from '@carbon/react';
import { Routes, Route } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
import NavigationBar from './components/NavigationBar';
import HomePageRoutes from './routes/HomePageRoutes';
import About from './routes/About';
import AdminRoutes from './routes/AdminRoutes';
import ScrollToTop from './components/ScrollToTop';
import EditorRoutes from './routes/EditorRoutes';
import Extension from './routes/Extension';
import { useUriListForPermissions } from './hooks/UriListForPermissions';
import { PermissionsToCheck, ProcessFile, ProcessModel } from './interfaces';
import { usePermissionFetcher } from './hooks/PermissionService';
import {
ExtensionUiSchema,
UiSchemaUxElement,
} from './extension_ui_schema_interfaces';
import HttpService from './services/HttpService';
import { Notification } from './components/Notification';
type ErrorProps = {
error: Error;
};
export function ErrorBoundaryFallback({ error }: ErrorProps) {
// This is displayed if the ErrorBoundary catches an error when rendering the form.
const { resetBoundary } = useErrorBoundary();
return (
<Notification
title="Something Went Wrong. "
onClose={() => resetBoundary()}
type="error"
>
<p>
We encountered an unexpected error. Please try again. If the problem
persists, please contact your administrator.
</p>
<p>{error.message}</p>
<Button onClick={resetBoundary}>Try again</Button>
</Notification>
);
}
export default function ContainerForExtensions() {
const [extensionUxElements, setExtensionNavigationItems] = useState<
UiSchemaUxElement[] | null
>(null);
let contentClassName = 'main-site-body-centered';
if (window.location.pathname.startsWith('/editor/')) {
contentClassName = 'no-center-stuff';
}
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.extensionListPath]: ['GET'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (!permissionsLoaded) {
return;
}
const processExtensionResult = (processModels: ProcessModel[]) => {
const eni: UiSchemaUxElement[] = processModels
.map((processModel: ProcessModel) => {
const extensionUiSchemaFile = processModel.files.find(
(file: ProcessFile) => file.name === 'extension_uischema.json'
);
if (extensionUiSchemaFile && extensionUiSchemaFile.file_contents) {
try {
const extensionUiSchema: ExtensionUiSchema = JSON.parse(
extensionUiSchemaFile.file_contents
);
if (extensionUiSchema.ux_elements) {
return extensionUiSchema.ux_elements;
}
} catch (jsonParseError: any) {
console.error(
`Unable to get navigation items for ${processModel.id}`
);
}
}
return [] as UiSchemaUxElement[];
})
.flat();
if (eni) {
setExtensionNavigationItems(eni);
}
};
if (ability.can('GET', targetUris.extensionListPath)) {
HttpService.makeCallToBackend({
path: targetUris.extensionListPath,
successCallback: processExtensionResult,
});
}
}, [targetUris.extensionListPath, permissionsLoaded, ability]);
return (
<>
<NavigationBar extensionUxElements={extensionUxElements} />
<Content className={contentClassName}>
<ScrollToTop />
<ErrorBoundary fallback={<h1>Something went wrong.</h1>}>
<Routes>
<Route path="/*" element={<HomePageRoutes />} />
<Route path="/about" element={<About />} />
<Route path="/tasks/*" element={<HomePageRoutes />} />
<Route
path="/admin/*"
element={
<AdminRoutes extensionUxElements={extensionUxElements} />
}
/>
<Route path="/editor/*" element={<EditorRoutes />} />
<Route
path="/extensions/:page_identifier"
element={<Extension />}
/>
</Routes>
</ErrorBoundary>
</Content>
</>
);
}

View File

@ -1,40 +0,0 @@
import React from 'react';
type Props = {
children?: React.ReactNode;
};
type State = any;
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: any) {
return { hasError: true, error };
}
componentDidCatch(error: any, errorInfo: any) {
console.error(error, errorInfo);
if (error.constructor.name === 'AggregateError') {
console.error(error.message);
console.error(error.name);
console.error(error.errors);
}
}
render() {
const { hasError } = this.state;
const { children } = this.props;
if (hasError) {
return <h1>Something went wrong.</h1>;
}
return children;
}
}
export default ErrorBoundary;

View File

@ -16,10 +16,35 @@ import {
Loading,
} from '@carbon/react';
import { useDebounce } from 'use-debounce';
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
import HttpService from '../../services/HttpService';
import ExamplesTable from './ExamplesTable';
import CustomForm from '../CustomForm';
import ErrorBoundary from '../ErrorBoundary';
import { Notification } from '../Notification';
type ErrorProps = {
error: Error;
};
function FormErrorFallback({ error }: ErrorProps) {
// This is displayed if the ErrorBoundary catches an error when rendering the form.
const { resetBoundary } = useErrorBoundary();
return (
<Notification
title="Failed to render form. "
onClose={() => resetBoundary()}
type="error"
>
<p>
The form could not be built with the current schema, UI and data. Please
try to correct the issue and try again.
</p>
<p>{error.message}</p>
<Button onClick={resetBoundary}>Try again</Button>
</Notification>
);
}
type OwnProps = {
processModelId: string;
@ -246,6 +271,7 @@ export default function ReactFormBuilder({
function updateDataFromStr(newDataStr: string) {
try {
setStrFormData(newDataStr);
const newData = JSON.parse(newDataStr);
setFormData(newData);
} catch (e) {
@ -461,7 +487,7 @@ export default function ReactFormBuilder({
<Column sm={4} md={5} lg={8}>
<h2>Form Preview</h2>
<div className="error_info_small">{errorMessage}</div>
<ErrorBoundary>
<ErrorBoundary FallbackComponent={FormErrorFallback}>
<CustomForm
id="custom_form"
formData={formData}