From b755d8d040323889d3e8fbf0edfe880eb6b242ea Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Mon, 29 Apr 2024 17:16:33 +0200 Subject: [PATCH 01/12] WIP --- packages/ra-core/src/core/CoreAdmin.tsx | 2 + packages/ra-core/src/core/CoreAdminUI.tsx | 152 ++++++++++++++-------- packages/ra-ui-materialui/src/AdminUI.tsx | 49 +++++-- packages/react-admin/src/Admin.tsx | 2 + 4 files changed, 138 insertions(+), 67 deletions(-) diff --git a/packages/ra-core/src/core/CoreAdmin.tsx b/packages/ra-core/src/core/CoreAdmin.tsx index ba0690c076f..d3ab0b2db61 100644 --- a/packages/ra-core/src/core/CoreAdmin.tsx +++ b/packages/ra-core/src/core/CoreAdmin.tsx @@ -91,6 +91,7 @@ export const CoreAdmin = (props: CoreAdminProps) => { dashboard, dataProvider, disableTelemetry, + error, i18nProvider, queryClient, layout, @@ -117,6 +118,7 @@ export const CoreAdmin = (props: CoreAdminProps) => { catchAll={catchAll} title={title} loading={loading} + error={error} loginPage={loginPage} requireAuth={requireAuth} ready={ready} diff --git a/packages/ra-core/src/core/CoreAdminUI.tsx b/packages/ra-core/src/core/CoreAdminUI.tsx index ec6b7543c08..e7bd8082850 100644 --- a/packages/ra-core/src/core/CoreAdminUI.tsx +++ b/packages/ra-core/src/core/CoreAdminUI.tsx @@ -1,6 +1,15 @@ import * as React from 'react'; -import { ComponentType, useEffect, isValidElement, createElement } from 'react'; +import { + ComponentType, + useEffect, + isValidElement, + createElement, + useState, + ErrorInfo, + ReactElement, +} from 'react'; import { Routes, Route } from 'react-router-dom'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { CoreAdminRoutes } from './CoreAdminRoutes'; import { Ready } from '../util'; @@ -22,6 +31,28 @@ const DefaultLayout = ({ children }: { children: React.ReactNode }) => ( ); export interface CoreAdminUIProps { + /** + * The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers + * + * @see https://marmelab.com/react-admin/Admin.html#authcallbackpage + * @example + * import { Admin } from 'react-admin'; + * import { dataProvider } from './dataProvider'; + * import { authProvider } from './authProvider'; + * import MyAuthCallbackPage from './MyAuthCallbackPage'; + * + * const App = () => ( + * + * ... + * + * ); + */ + authCallbackPage?: ComponentType | boolean; + /** * A catch-all react component to display when the URL does not match any * @@ -89,6 +120,14 @@ export interface CoreAdminUIProps { */ disableTelemetry?: boolean; + /** + * The component displayed when an error is caught in a child component + * @see https://marmelab.com/react-admin/Admin.html#error + * @example + * TODO: add an example + */ + error?: (props: FallbackProps) => ReactElement; + /** * The main app layout component * @@ -115,28 +154,6 @@ export interface CoreAdminUIProps { */ loading?: LoadingComponent; - /** - * The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers - * - * @see https://marmelab.com/react-admin/Admin.html#authcallbackpage - * @example - * import { Admin } from 'react-admin'; - * import { dataProvider } from './dataProvider'; - * import { authProvider } from './authProvider'; - * import MyAuthCallbackPage from './MyAuthCallbackPage'; - * - * const App = () => ( - * - * ... - * - * ); - */ - authCallbackPage?: ComponentType | boolean; - /** * The component displayed when the user visits the /login page * @see https://marmelab.com/react-admin/Admin.html#loginpage @@ -158,6 +175,14 @@ export interface CoreAdminUIProps { */ loginPage?: LoginComponent | boolean; + /** + * The function called when an error is caught in a child component + * @see https://marmelab.com/react-admin/Admin.html#onerror + * @example + * TODO: add an example + */ + onError?: (error: Error, info: ErrorInfo) => void; + /** * Flag to require authentication for all routes. Defaults to false. * @@ -218,18 +243,29 @@ export interface CoreAdminUIProps { } export const CoreAdminUI = (props: CoreAdminUIProps) => { + const [errorInfo, setErrorInfo] = useState( + undefined + ); const { + authCallbackPage: LoginCallbackPage = false, catchAll = Noop, children, dashboard, disableTelemetry = false, + error = ({ error }) => ( +
+

Error: {error?.message}

+

ErrorInfo: {JSON.stringify(errorInfo)}

+

ComponentStack: {errorInfo?.componentStack}

+
+ ), layout = DefaultLayout, loading = Noop, loginPage: LoginPage = false, - authCallbackPage: LoginCallbackPage = false, + onError, ready = Ready, - title = 'React Admin', requireAuth = false, + title = 'React Admin', } = props; useEffect(() => { @@ -246,39 +282,49 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { img.src = `https://react-admin-telemetry.marmelab.com/react-admin-telemetry?domain=${window.location.hostname}`; }, [disableTelemetry]); + const handleError = (error: Error, info: ErrorInfo) => { + setErrorInfo(info); + }; + return ( - - {LoginPage !== false && LoginPage !== true ? ( - - ) : null} + + + {LoginPage !== false && LoginPage !== true ? ( + + ) : null} + + {LoginCallbackPage !== false && + LoginCallbackPage !== true ? ( + + ) : null} - {LoginCallbackPage !== false && LoginCallbackPage !== true ? ( + {children} + + } /> - ) : null} - - - {children} - - } - /> - + + ); }; diff --git a/packages/ra-ui-materialui/src/AdminUI.tsx b/packages/ra-ui-materialui/src/AdminUI.tsx index 0cb88e7c913..67fca1f6119 100644 --- a/packages/ra-ui-materialui/src/AdminUI.tsx +++ b/packages/ra-ui-materialui/src/AdminUI.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createElement, ComponentType } from 'react'; +import { createElement, ComponentType, useState, ErrorInfo } from 'react'; import { CoreAdminUI, CoreAdminUIProps } from 'ra-core'; import { ScopedCssBaseline } from '@mui/material'; @@ -8,6 +8,7 @@ import { LoadingPage, NotFound, Notification, + Error, } from './layout'; import { Login, AuthCallback } from './auth'; @@ -18,20 +19,40 @@ export const AdminUI = ({ loginPage = Login, authCallbackPage = AuthCallback, notification = Notification, + error: errorComponent, ...props -}: AdminUIProps) => ( - - - {createElement(notification)} - -); +}: AdminUIProps) => { + const [errorInfo, setErrorInfo] = useState( + undefined + ); + + const handleError = (error: Error, info: ErrorInfo) => { + setErrorInfo(info); + }; + + return ( + + ( + + )} + onError={handleError} + {...props} + /> + {createElement(notification)} + + ); +}; export interface AdminUIProps extends CoreAdminUIProps { /** diff --git a/packages/react-admin/src/Admin.tsx b/packages/react-admin/src/Admin.tsx index 0cddaa13dea..63a4bb4f82c 100644 --- a/packages/react-admin/src/Admin.tsx +++ b/packages/react-admin/src/Admin.tsx @@ -102,6 +102,7 @@ export const Admin = (props: AdminProps) => { dashboard, dataProvider, disableTelemetry, + error, i18nProvider = defaultI18nProvider, layout, loading, @@ -143,6 +144,7 @@ export const Admin = (props: AdminProps) => { dashboard={dashboard} disableTelemetry={disableTelemetry} catchAll={catchAll} + error={error} title={title} loading={loading} loginPage={loginPage} From 6be41e0f146b9653f56ccf4fb5155dc30aaf2f4a Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 11:28:41 +0200 Subject: [PATCH 02/12] add stories --- .../ra-core/src/core/CoreAdmin.stories.tsx | 29 +++++++++++++++++++ packages/react-admin/src/Admin.stories.tsx | 12 ++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/ra-core/src/core/CoreAdmin.stories.tsx diff --git a/packages/ra-core/src/core/CoreAdmin.stories.tsx b/packages/ra-core/src/core/CoreAdmin.stories.tsx new file mode 100644 index 00000000000..fba2f39511f --- /dev/null +++ b/packages/ra-core/src/core/CoreAdmin.stories.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Route } from 'react-router'; +import { CoreAdmin } from './CoreAdmin'; +import { useAuthenticated, useLogin } from '../auth'; +import { CustomRoutes } from './CustomRoutes'; +import { Resource } from './Resource'; +import { FakeBrowserDecorator } from '../storybook/FakeBrowser'; + +export default { + title: 'ra-core/Admin', + decorators: [FakeBrowserDecorator], + parameters: { + initialEntries: ['/error'], + }, +}; + +const ErrorComponent = () => { + throw Error(); +}; + +export const Error = () => { + return ( + + + } /> + + + ); +}; diff --git a/packages/react-admin/src/Admin.stories.tsx b/packages/react-admin/src/Admin.stories.tsx index 946f18ba805..2d76c06105e 100644 --- a/packages/react-admin/src/Admin.stories.tsx +++ b/packages/react-admin/src/Admin.stories.tsx @@ -3,6 +3,7 @@ import { Routes, Route, Link } from 'react-router-dom'; import { Admin } from './Admin'; import { Resource, testDataProvider, TestMemoryRouter } from 'ra-core'; +import { AppBar, Layout } from 'ra-ui-materialui'; export default { title: 'react-admin/Admin', @@ -53,3 +54,14 @@ export const SubPath = () => ( ); + +// @ts-ignore +const MyAppBar = () => ; + +const MyLayout = props => ; + +export const Error = () => ( + + + +); From 96fb0dd5046657e0c66fda729253527acd840fca Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 11:43:49 +0200 Subject: [PATCH 03/12] add custom error stories --- .../ra-core/src/core/CoreAdmin.stories.tsx | 24 +++++++++------ packages/react-admin/src/Admin.stories.tsx | 29 +++++++++++++++++-- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/ra-core/src/core/CoreAdmin.stories.tsx b/packages/ra-core/src/core/CoreAdmin.stories.tsx index fba2f39511f..72c924739e4 100644 --- a/packages/ra-core/src/core/CoreAdmin.stories.tsx +++ b/packages/ra-core/src/core/CoreAdmin.stories.tsx @@ -18,12 +18,18 @@ const ErrorComponent = () => { throw Error(); }; -export const Error = () => { - return ( - - - } /> - - - ); -}; +export const Error = () => ( + + + } /> + + +); + +export const CustomError = () => ( +

Something went wrong...

}> + + } /> + +
+); diff --git a/packages/react-admin/src/Admin.stories.tsx b/packages/react-admin/src/Admin.stories.tsx index 2d76c06105e..f8de5c15941 100644 --- a/packages/react-admin/src/Admin.stories.tsx +++ b/packages/react-admin/src/Admin.stories.tsx @@ -4,6 +4,7 @@ import { Routes, Route, Link } from 'react-router-dom'; import { Admin } from './Admin'; import { Resource, testDataProvider, TestMemoryRouter } from 'ra-core'; import { AppBar, Layout } from 'ra-ui-materialui'; +import { Box, Typography } from '@mui/material'; export default { title: 'react-admin/Admin', @@ -56,12 +57,34 @@ export const SubPath = () => ( ); // @ts-ignore -const MyAppBar = () => ; +const FailedAppBar = () => ; -const MyLayout = props => ; +const FailedLayout = props => ; export const Error = () => ( - + + + +); + +const ErrorPage = () => ( + + + Error + + +); + +export const CustomError = () => ( + }> ); From bd849e669bf9abfdbfdbe60403e3c7539ef58fbd Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 12:26:12 +0200 Subject: [PATCH 04/12] doc --- docs/Admin.md | 149 +++++++++++++++--- docs/Layout.md | 2 + docs/img/adminError.png | Bin 0 -> 40033 bytes .../ra-core/src/core/CoreAdmin.stories.tsx | 2 - packages/ra-core/src/core/CoreAdmin.tsx | 2 + 5 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 docs/img/adminError.png diff --git a/docs/Admin.md b/docs/Admin.md index 4d1a314f534..be0c2ca02b9 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -136,28 +136,30 @@ Three main props lets you configure the core features of the `` component Here are all the props accepted by the component: -| Prop | Required | Type | Default | Description | -|------------------- |----------|----------------|----------------|----------------------------------------------------------| -| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources | -| `children` | Required | `ReactNode` | - | The routes to render | -| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page | -| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions | -| `basename` | Optional | `string` | - | The base path for all URLs | -| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes | -| `dashboard` | Optional | `Component` | - | The content of the dashboard page | -| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration | -| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme | -| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection | -| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations | -| `layout` | Optional | `Component` | `Layout` | The content of the layout | -| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page | -| `notification` | Optional | `Component` | `Notification` | The notification component | -| `queryClient` | Optional | `QueryClient` | - | The react-query client | -| `ready` | Optional | `Component` | `Ready` | The content of the ready page | -| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes | -| `store` | Optional | `Store` | - | The Store for managing user preferences | -| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration | -| `title` | Optional | `string` | - | The error page title | +| Prop | Required | Type | Default | Description | +|------------------- |----------|------------------------------------------ |--------------------- |---------------------------------------------------------------- | +| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources | +| `children` | Required | `ReactNode` | - | The routes to render | +| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page | +| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions | +| `basename` | Optional | `string` | - | The base path for all URLs | +| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes | +| `dashboard` | Optional | `Component` | - | The content of the dashboard page | +| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration | +| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme | +| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection | +| `error` | Optional | `(props: FallbackProps) => Component` | - | A React component rendered in the content area in case of error | +| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations | +| `layout` | Optional | `Component` | `Layout` | The content of the layout | +| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page | +| `notification` | Optional | `Component` | `Notification` | The notification component | +| `onError` | Optional | `(error: Error, info: ErrorInfo) => void` | - | A function called when an error appears | +| `queryClient` | Optional | `QueryClient` | - | The react-query client | +| `ready` | Optional | `Component` | `Ready` | The content of the ready page | +| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes | +| `store` | Optional | `Store` | - | The Store for managing user preferences | +| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration | +| `title` | Optional | `string` | - | The error page title | ## `dataProvider` @@ -521,6 +523,70 @@ const App = () => ( ``` +## `error` + +Whenever some client-side errors happens in react-admin, the user sees an error page. React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render this page when any component in the page throws an unrecoverable error. + +![Default error page](./img/adminError.png) + +If you want to customize this page, or log the error to a third-party service, create your own `` component, and pass it to a custom Layout, as follows: + +```jsx +// in src/App.js +import { Admin } from 'react-admin'; +import { MyError } from './MyError'; + +export const MyLayout = ({ children }) => ( + }> + {children} + +); +``` + +```jsx +// in src/MyError.js +import * as React from 'react'; +import Button from '@mui/material/Button'; +import ErrorIcon from '@mui/icons-material/Report'; +import History from '@mui/icons-material/History'; +import { useLocation } from 'react-router-dom'; + +export const MyError = ({ + error, + resetErrorBoundary, + ...rest +}) => { + const { pathname } = useLocation(); + const originalPathname = useRef(pathname); + + // Effect that resets the error state whenever the location changes + useEffect(() => { + if (pathname !== originalPathname.current) { + resetErrorBoundary(); + } + }, [pathname, resetErrorBoundary]); + + return ( +
+

Something Went Wrong

+
A client error occurred and your request couldn't be completed.
+
+ +
+
+ ); +}; +``` + +**Tip:** Some errors appear in a lower level than the admin. To customize this error page too, you can use the [`` `error` prop](./Layout.md#error). + + ## `i18nProvider` The `i18nProvider` props let you translate the GUI. For instance, to switch the UI to French instead of the default English: @@ -691,6 +757,45 @@ const App = () => ( ); ``` +## `onError` + +When a error apears, you can override the react-admin function called to grab some informations as follows: + +```jsx +// in src/App.js +import * as React from 'react'; +import { Admin } from 'react-admin'; +import { MyError } from './MyError'; + +export const MyLayout = ({ children }) => { + const [errorInfo, setErrorInfo] = React.useState(undefined); + return ( + } + onError={(error: Error, info: ErrorInfo) => setErrorInfo(info)} + > + {children} + + ); +} +``` + +```jsx +// in src/MyError.js +import * as React from 'react'; +import ErrorIcon from '@mui/icons-material/Report'; + +export const MyError = ({errorInfo: React.ErrorInfo}) => ( +
+

Something Went Wrong

+
+

A client error occurred and your request couldn't be completed.

+

Error informations: {JSON.stringify(errorInfo)}

+
+
+); +``` + ## `notification` You can override the notification component, for instance to change the notification duration. A common use case is to change the `autoHideDuration`, and force the notification to remain on screen longer than the default 4 seconds. For instance, to create a custom Notification component with a 5 seconds default: diff --git a/docs/Layout.md b/docs/Layout.md index ea27584ded5..86150692055 100644 --- a/docs/Layout.md +++ b/docs/Layout.md @@ -211,6 +211,8 @@ export const MyError = ({ **Tip:** [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) are used internally to display the Error Page whenever an error occurs. Error Boundaries only catch errors during rendering, in lifecycle methods, and in constructors of the components tree. This implies in particular that errors during event callbacks (such as 'onClick') are not concerned. Also note that the Error Boundary component is only set around the main container of React Admin. In particular, you won't see it for errors thrown by the [sidebar Menu](./Menu.md), nor the [AppBar](#adding-a-custom-context). This ensures the user is always able to navigate away from the Error Page. +**Tip:** Some errors appear in a higher level than the layout. To customize this error page too, you can use the [`` `error` prop](./Admin.md#error). + ## `menu` Lets you override the menu. diff --git a/docs/img/adminError.png b/docs/img/adminError.png new file mode 100644 index 0000000000000000000000000000000000000000..e4cbe04eb2a91003963d65d182c2c50892364e8d GIT binary patch literal 40033 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 literal 0 HcmV?d00001 diff --git a/packages/ra-core/src/core/CoreAdmin.stories.tsx b/packages/ra-core/src/core/CoreAdmin.stories.tsx index 72c924739e4..65196cfb0d6 100644 --- a/packages/ra-core/src/core/CoreAdmin.stories.tsx +++ b/packages/ra-core/src/core/CoreAdmin.stories.tsx @@ -1,9 +1,7 @@ import * as React from 'react'; import { Route } from 'react-router'; import { CoreAdmin } from './CoreAdmin'; -import { useAuthenticated, useLogin } from '../auth'; import { CustomRoutes } from './CustomRoutes'; -import { Resource } from './Resource'; import { FakeBrowserDecorator } from '../storybook/FakeBrowser'; export default { diff --git a/packages/ra-core/src/core/CoreAdmin.tsx b/packages/ra-core/src/core/CoreAdmin.tsx index d3ab0b2db61..a96d30bcaec 100644 --- a/packages/ra-core/src/core/CoreAdmin.tsx +++ b/packages/ra-core/src/core/CoreAdmin.tsx @@ -92,6 +92,7 @@ export const CoreAdmin = (props: CoreAdminProps) => { dataProvider, disableTelemetry, error, + onError, i18nProvider, queryClient, layout, @@ -119,6 +120,7 @@ export const CoreAdmin = (props: CoreAdminProps) => { title={title} loading={loading} error={error} + onError={onError} loginPage={loginPage} requireAuth={requireAuth} ready={ready} From f2774e81e41330208c4f08d42fc5ef31ab61c5be Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 12:28:19 +0200 Subject: [PATCH 05/12] JS Doc --- packages/ra-core/src/core/CoreAdminUI.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/core/CoreAdminUI.tsx b/packages/ra-core/src/core/CoreAdminUI.tsx index e7bd8082850..453e1e200bf 100644 --- a/packages/ra-core/src/core/CoreAdminUI.tsx +++ b/packages/ra-core/src/core/CoreAdminUI.tsx @@ -124,7 +124,14 @@ export interface CoreAdminUIProps { * The component displayed when an error is caught in a child component * @see https://marmelab.com/react-admin/Admin.html#error * @example - * TODO: add an example + * import { Admin } from 'react-admin'; + * import { MyError } from './error'; + * + * const App = () => ( + * + * ... + * + * ); */ error?: (props: FallbackProps) => ReactElement; @@ -179,7 +186,17 @@ export interface CoreAdminUIProps { * The function called when an error is caught in a child component * @see https://marmelab.com/react-admin/Admin.html#onerror * @example - * TODO: add an example + * import { Admin } from 'react-admin'; + * import { MyError, onError } from './error'; + * + * const App = () => ( + * MyError} + * onError={onError} + * > + * ... + * + * ); */ onError?: (error: Error, info: ErrorInfo) => void; From ee4ab2e86b02ef7ffa40b3bc85d6581f73713cc5 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 16:59:37 +0200 Subject: [PATCH 06/12] remove onError + create const for default error --- .../ra-core/src/core/CoreAdmin.stories.tsx | 9 ++- packages/ra-core/src/core/CoreAdmin.tsx | 2 - packages/ra-core/src/core/CoreAdminUI.tsx | 59 ++++++++----------- packages/ra-ui-materialui/src/AdminUI.tsx | 55 +++++++---------- packages/react-admin/src/Admin.stories.tsx | 16 +++-- 5 files changed, 65 insertions(+), 76 deletions(-) diff --git a/packages/ra-core/src/core/CoreAdmin.stories.tsx b/packages/ra-core/src/core/CoreAdmin.stories.tsx index 65196cfb0d6..18e6b940393 100644 --- a/packages/ra-core/src/core/CoreAdmin.stories.tsx +++ b/packages/ra-core/src/core/CoreAdmin.stories.tsx @@ -24,8 +24,15 @@ export const Error = () => ( ); +const MyError = ({ errorInfo }: { errorInfo?: React.ErrorInfo }) => ( +
+

Something went wrong...

+

{errorInfo?.componentStack}

+
+); + export const CustomError = () => ( -

Something went wrong...

}> + } /> diff --git a/packages/ra-core/src/core/CoreAdmin.tsx b/packages/ra-core/src/core/CoreAdmin.tsx index a96d30bcaec..d3ab0b2db61 100644 --- a/packages/ra-core/src/core/CoreAdmin.tsx +++ b/packages/ra-core/src/core/CoreAdmin.tsx @@ -92,7 +92,6 @@ export const CoreAdmin = (props: CoreAdminProps) => { dataProvider, disableTelemetry, error, - onError, i18nProvider, queryClient, layout, @@ -120,7 +119,6 @@ export const CoreAdmin = (props: CoreAdminProps) => { title={title} loading={loading} error={error} - onError={onError} loginPage={loginPage} requireAuth={requireAuth} ready={ready} diff --git a/packages/ra-core/src/core/CoreAdminUI.tsx b/packages/ra-core/src/core/CoreAdminUI.tsx index 453e1e200bf..3a8dafc90ff 100644 --- a/packages/ra-core/src/core/CoreAdminUI.tsx +++ b/packages/ra-core/src/core/CoreAdminUI.tsx @@ -9,7 +9,7 @@ import { ReactElement, } from 'react'; import { Routes, Route } from 'react-router-dom'; -import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { ErrorBoundary } from 'react-error-boundary'; import { CoreAdminRoutes } from './CoreAdminRoutes'; import { Ready } from '../util'; @@ -29,6 +29,14 @@ export type ChildrenFunction = () => ComponentType[]; const DefaultLayout = ({ children }: { children: React.ReactNode }) => ( <>{children} ); +const DefaultError = ({ errorInfo }) => ( +
+

Error

+

+ ComponentStack: {errorInfo?.componentStack} +

+
+); export interface CoreAdminUIProps { /** @@ -133,7 +141,15 @@ export interface CoreAdminUIProps { *
* ); */ - error?: (props: FallbackProps) => ReactElement; + error?: ({ + errorInfo, + error, + resetErrorBoundary, + }: { + errorInfo?: ErrorInfo; + error?: Error; + resetErrorBoundary?: (args) => void; + }) => ReactElement; /** * The main app layout component @@ -182,24 +198,6 @@ export interface CoreAdminUIProps { */ loginPage?: LoginComponent | boolean; - /** - * The function called when an error is caught in a child component - * @see https://marmelab.com/react-admin/Admin.html#onerror - * @example - * import { Admin } from 'react-admin'; - * import { MyError, onError } from './error'; - * - * const App = () => ( - * MyError} - * onError={onError} - * > - * ... - * - * ); - */ - onError?: (error: Error, info: ErrorInfo) => void; - /** * Flag to require authentication for all routes. Defaults to false. * @@ -260,26 +258,17 @@ export interface CoreAdminUIProps { } export const CoreAdminUI = (props: CoreAdminUIProps) => { - const [errorInfo, setErrorInfo] = useState( - undefined - ); + const [errorInfo, setErrorInfo] = useState({}); const { authCallbackPage: LoginCallbackPage = false, catchAll = Noop, children, dashboard, disableTelemetry = false, - error = ({ error }) => ( -
-

Error: {error?.message}

-

ErrorInfo: {JSON.stringify(errorInfo)}

-

ComponentStack: {errorInfo?.componentStack}

-
- ), + error = DefaultError, layout = DefaultLayout, loading = Noop, loginPage: LoginPage = false, - onError, ready = Ready, requireAuth = false, title = 'React Admin', @@ -299,15 +288,13 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { img.src = `https://react-admin-telemetry.marmelab.com/react-admin-telemetry?domain=${window.location.hostname}`; }, [disableTelemetry]); - const handleError = (error: Error, info: ErrorInfo) => { - setErrorInfo(info); - }; + const handleError = (error: Error, info: ErrorInfo) => setErrorInfo(info); return ( error({ errorInfo, ...props })} > {LoginPage !== false && LoginPage !== true ? ( diff --git a/packages/ra-ui-materialui/src/AdminUI.tsx b/packages/ra-ui-materialui/src/AdminUI.tsx index 67fca1f6119..3ca4bf65302 100644 --- a/packages/ra-ui-materialui/src/AdminUI.tsx +++ b/packages/ra-ui-materialui/src/AdminUI.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { createElement, ComponentType, useState, ErrorInfo } from 'react'; +import { createElement, ComponentType } from 'react'; import { CoreAdminUI, CoreAdminUIProps } from 'ra-core'; import { ScopedCssBaseline } from '@mui/material'; @@ -21,38 +21,27 @@ export const AdminUI = ({ notification = Notification, error: errorComponent, ...props -}: AdminUIProps) => { - const [errorInfo, setErrorInfo] = useState( - undefined - ); - - const handleError = (error: Error, info: ErrorInfo) => { - setErrorInfo(info); - }; - - return ( - - ( - - )} - onError={handleError} - {...props} - /> - {createElement(notification)} - - ); -}; +}: AdminUIProps) => ( + + ( + + )} + {...props} + /> + {createElement(notification)} + +); export interface AdminUIProps extends CoreAdminUIProps { /** diff --git a/packages/react-admin/src/Admin.stories.tsx b/packages/react-admin/src/Admin.stories.tsx index f8de5c15941..20f7065353d 100644 --- a/packages/react-admin/src/Admin.stories.tsx +++ b/packages/react-admin/src/Admin.stories.tsx @@ -67,24 +67,32 @@ export const Error = () => (
); -const ErrorPage = () => ( +const ErrorPage = ({ errorInfo }: { errorInfo?: React.ErrorInfo }) => ( - Error + Error +
    + {errorInfo?.componentStack + ?.split(' at ') + ?.slice(1) + ?.map((line, index) => ( +
  • At {line}
  • + ))} +
); export const CustomError = () => ( - }> + ); From ca375866d2e4b7518972c0f786df88389e5f66a1 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Tue, 30 Apr 2024 17:11:49 +0200 Subject: [PATCH 07/12] update doc --- docs/Admin.md | 100 ++++++++++++++++--------------------------------- docs/Layout.md | 2 +- 2 files changed, 34 insertions(+), 68 deletions(-) diff --git a/docs/Admin.md b/docs/Admin.md index be0c2ca02b9..91ea79d5039 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -136,30 +136,29 @@ Three main props lets you configure the core features of the `` component Here are all the props accepted by the component: -| Prop | Required | Type | Default | Description | -|------------------- |----------|------------------------------------------ |--------------------- |---------------------------------------------------------------- | -| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources | -| `children` | Required | `ReactNode` | - | The routes to render | -| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page | -| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions | -| `basename` | Optional | `string` | - | The base path for all URLs | -| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes | -| `dashboard` | Optional | `Component` | - | The content of the dashboard page | -| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration | -| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme | -| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection | -| `error` | Optional | `(props: FallbackProps) => Component` | - | A React component rendered in the content area in case of error | -| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations | -| `layout` | Optional | `Component` | `Layout` | The content of the layout | -| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page | -| `notification` | Optional | `Component` | `Notification` | The notification component | -| `onError` | Optional | `(error: Error, info: ErrorInfo) => void` | - | A function called when an error appears | -| `queryClient` | Optional | `QueryClient` | - | The react-query client | -| `ready` | Optional | `Component` | `Ready` | The content of the ready page | -| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes | -| `store` | Optional | `Store` | - | The Store for managing user preferences | -| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration | -| `title` | Optional | `string` | - | The error page title | +| Prop | Required | Type | Default | Description | +|------------------- |----------|---------------- |--------------------- |---------------------------------------------------------------- | +| `dataProvider` | Required | `DataProvider` | - | The data provider for fetching resources | +| `children` | Required | `ReactNode` | - | The routes to render | +| `authCallbackPage` | Optional | `Component` | `AuthCallback` | The content of the authentication callback page | +| `authProvider` | Optional | `AuthProvider` | - | The authentication provider for security and permissions | +| `basename` | Optional | `string` | - | The base path for all URLs | +| `catchAll` | Optional | `Component` | `NotFound` | The fallback component for unknown routes | +| `dashboard` | Optional | `Component` | - | The content of the dashboard page | +| `darkTheme` | Optional | `object` | `default DarkTheme` | The dark theme configuration | +| `defaultTheme` | Optional | `boolean` | `false` | Flag to default to the light theme | +| `disableTelemetry` | Optional | `boolean` | `false` | Set to `true` to disable telemetry collection | +| `error` | Optional | `Component` | - | A React component rendered in the content area in case of error | +| `i18nProvider` | Optional | `I18NProvider` | - | The internationalization provider for translations | +| `layout` | Optional | `Component` | `Layout` | The content of the layout | +| `loginPage` | Optional | `Component` | `LoginPage` | The content of the login page | +| `notification` | Optional | `Component` | `Notification` | The notification component | +| `queryClient` | Optional | `QueryClient` | - | The react-query client | +| `ready` | Optional | `Component` | `Ready` | The content of the ready page | +| `requireAuth` | Optional | `boolean` | `false` | Flag to require authentication for all routes | +| `store` | Optional | `Store` | - | The Store for managing user preferences | +| `theme` | Optional | `object` | `default LightTheme` | The main (light) theme configuration | +| `title` | Optional | `string` | - | The error page title | ## `dataProvider` @@ -525,7 +524,7 @@ const App = () => ( ## `error` -Whenever some client-side errors happens in react-admin, the user sees an error page. React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render this page when any component in the page throws an unrecoverable error. +React-admin uses [React's Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to render client-side errors happens in react-admin. ![Default error page](./img/adminError.png) @@ -537,7 +536,7 @@ import { Admin } from 'react-admin'; import { MyError } from './MyError'; export const MyLayout = ({ children }) => ( - }> + {children} ); @@ -554,7 +553,7 @@ import { useLocation } from 'react-router-dom'; export const MyError = ({ error, resetErrorBoundary, - ...rest + errorInfo, }) => { const { pathname } = useLocation(); const originalPathname = useRef(pathname); @@ -570,6 +569,12 @@ export const MyError = ({

Something Went Wrong

A client error occurred and your request couldn't be completed.
+ {process.env.NODE_ENV !== 'production' && ( +
+

{error.toString()}

+ {errorInfo.componentStack} +
+ )}
diff --git a/docs/Layout.md b/docs/Layout.md index 67333643c06..c0d719106f0 100644 --- a/docs/Layout.md +++ b/docs/Layout.md @@ -163,27 +163,17 @@ Here is an example of a custom error component: ```jsx // in src/MyError.js -import * as React from 'react'; import Button from '@mui/material/Button'; import ErrorIcon from '@mui/icons-material/Report'; import History from '@mui/icons-material/History'; -import { Title, useTranslate, useDefaultTitle } from 'react-admin'; -import { useLocation } from 'react-router-dom'; +import { Title, useTranslate, useDefaultTitle, useResetErrorBoundaryOnLocationChange } from 'react-admin'; export const MyError = ({ error, resetErrorBoundary, ...rest }) => { - const { pathname } = useLocation(); - const originalPathname = useRef(pathname); - - // Effect that resets the error state whenever the location changes - useEffect(() => { - if (pathname !== originalPathname.current) { - resetErrorBoundary(); - } - }, [pathname, resetErrorBoundary]); + useResetErrorBoundaryOnLocationChange(resetErrorBoundary); const translate = useTranslate(); const defaultTitle = useDefaultTitle(); @@ -194,7 +184,7 @@ export const MyError = ({
A client error occurred and your request couldn't be completed.
{process.env.NODE_ENV !== 'production' && (
-

{translate(error.toString())}

+

{translate(error.message)}

{errorInfo.componentStack}
)} From e90ada5ef6ac80bf23e9be5196c5d7e5a73dd207 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 3 May 2024 10:48:20 +0200 Subject: [PATCH 11/12] Fix bad import --- packages/ra-ui-materialui/src/AdminUI.tsx | 11 ++--------- packages/ra-ui-materialui/src/layout/Error.tsx | 7 ++++--- packages/react-admin/src/Admin.stories.tsx | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/ra-ui-materialui/src/AdminUI.tsx b/packages/ra-ui-materialui/src/AdminUI.tsx index 3ca4bf65302..083f884ee72 100644 --- a/packages/ra-ui-materialui/src/AdminUI.tsx +++ b/packages/ra-ui-materialui/src/AdminUI.tsx @@ -19,7 +19,7 @@ export const AdminUI = ({ loginPage = Login, authCallbackPage = AuthCallback, notification = Notification, - error: errorComponent, + error = Error, ...props }: AdminUIProps) => ( @@ -29,14 +29,7 @@ export const AdminUI = ({ loading={loading} loginPage={loginPage} authCallbackPage={authCallbackPage} - error={({ error, resetErrorBoundary, errorInfo }) => ( - - )} + error={error} {...props} /> {createElement(notification)} diff --git a/packages/ra-ui-materialui/src/layout/Error.tsx b/packages/ra-ui-materialui/src/layout/Error.tsx index a8629209746..bb5cc3f1984 100644 --- a/packages/ra-ui-materialui/src/layout/Error.tsx +++ b/packages/ra-ui-materialui/src/layout/Error.tsx @@ -13,11 +13,12 @@ import { import ErrorIcon from '@mui/icons-material/Report'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import History from '@mui/icons-material/History'; -import { useTranslate, useDefaultTitle } from 'ra-core'; -import type { - TitleComponent, +import { + useTranslate, + useDefaultTitle, useResetErrorBoundaryOnLocationChange, } from 'ra-core'; +import type { TitleComponent } from 'ra-core'; import { Title, TitlePropType } from './Title'; diff --git a/packages/react-admin/src/Admin.stories.tsx b/packages/react-admin/src/Admin.stories.tsx index 20f7065353d..a55ce4f5273 100644 --- a/packages/react-admin/src/Admin.stories.tsx +++ b/packages/react-admin/src/Admin.stories.tsx @@ -61,7 +61,7 @@ const FailedAppBar = () => ; const FailedLayout = props => ; -export const Error = () => ( +export const DefaultError = () => ( From 8c4061b094e27df27712adfee5337cefbfdff577 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 3 May 2024 10:54:56 +0200 Subject: [PATCH 12/12] Add unit test --- packages/react-admin/src/Admin.spec.tsx | 10 +++++++++- packages/react-admin/src/Admin.stories.tsx | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react-admin/src/Admin.spec.tsx b/packages/react-admin/src/Admin.spec.tsx index 2fb13265577..985f5540253 100644 --- a/packages/react-admin/src/Admin.spec.tsx +++ b/packages/react-admin/src/Admin.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; -import { Basic, InsideRouter, SubPath } from './Admin.stories'; +import { Basic, InsideRouter, SubPath, DefaultError } from './Admin.stories'; describe('', () => { beforeEach(() => { @@ -30,4 +30,12 @@ describe('', () => { screen.getAllByText('Comments')[0].click(); await screen.findByText('Comment List'); }); + + describe('error handling', () => { + it('renders the error component when an error is thrown', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + render(); + await screen.findByText('Something went wrong'); + }); + }); }); diff --git a/packages/react-admin/src/Admin.stories.tsx b/packages/react-admin/src/Admin.stories.tsx index a55ce4f5273..e489132ca10 100644 --- a/packages/react-admin/src/Admin.stories.tsx +++ b/packages/react-admin/src/Admin.stories.tsx @@ -57,9 +57,11 @@ export const SubPath = () => ( ); // @ts-ignore -const FailedAppBar = () => ; +const FailingAppBar = () => { + throw new Error('AppBar rendering failed'); +}; -const FailedLayout = props => ; +const FailedLayout = props => ; export const DefaultError = () => (