diff --git a/apps/console/src/layouts/app-layout.tsx b/apps/console/src/layouts/app-layout.tsx index 8c59ca90f8f..0206a1ffdbf 100644 --- a/apps/console/src/layouts/app-layout.tsx +++ b/apps/console/src/layouts/app-layout.tsx @@ -18,19 +18,16 @@ import { PreLoader } from "@wso2is/admin.core.v1/components/pre-loader"; import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route"; -import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { AppState, store } from "@wso2is/admin.core.v1/store"; import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils"; +import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils"; import { RouteInterface } from "@wso2is/core/models"; -import { CommonUtils } from "@wso2is/core/utils"; import { CookieConsentBanner, - EmptyPlaceholder, - ErrorBoundary, - LinkButton + ErrorBoundary } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, Suspense, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; @@ -52,6 +49,10 @@ const AppLayout: FunctionComponent> = (): ReactElement = }); const appHomePath: string = useSelector((state: AppState) => state.config.deployment.appHomePath); + const handleRouteChunkError: ((_error: Error, _errorInfo: React.ErrorInfo) => void) = createRouteErrorHandler(appHomePath); + + const brokenPageFallback: ReactNode = createBrokenPageFallback(t); + /** * Listen for base name changes and updated the layout routes. */ @@ -63,25 +64,8 @@ const AppLayout: FunctionComponent> = (): ReactElement = <> { - sessionStorage.setItem("auth_callback_url_console", appHomePath); - } } - fallback={ ( - CommonUtils.refreshPage() }> - { t("console:common.placeholders.brokenPage.action") } - - ) } - image={ getEmptyPlaceholderIllustrations().brokenPage } - imageSize="tiny" - subtitle={ [ - t("console:common.placeholders.brokenPage.subtitles.0"), - t("console:common.placeholders.brokenPage.subtitles.1") - ] } - title={ t("console:common.placeholders.brokenPage.title") } - /> - ) } + handleError={ handleRouteChunkError } + fallback={ brokenPageFallback } > }> @@ -101,11 +85,22 @@ const AppLayout: FunctionComponent> = (): ReactElement = : ( - route.component - ? - : null - } + render={ (renderProps: RouteComponentProps) => { + if (!route.component) { + return null; + } + + return ( + + + + ); + } } key={ index } exact={ route.exact } /> diff --git a/apps/console/src/layouts/dashboard-layout.tsx b/apps/console/src/layouts/dashboard-layout.tsx index 57aa9e88ee9..ada84d96818 100644 --- a/apps/console/src/layouts/dashboard-layout.tsx +++ b/apps/console/src/layouts/dashboard-layout.tsx @@ -24,7 +24,6 @@ import { FeatureStatus, useCheckFeatureStatus } from "@wso2is/access-control"; import { getProfileInformation } from "@wso2is/admin.authentication.v1/store"; import Header from "@wso2is/admin.core.v1/components/header"; import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route"; -import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { UIConstants } from "@wso2is/admin.core.v1/constants/ui-constants"; import { history } from "@wso2is/admin.core.v1/helpers/history"; @@ -33,6 +32,7 @@ import { ConfigReducerStateInterface } from "@wso2is/admin.core.v1/models/reduce import { AppState } from "@wso2is/admin.core.v1/store"; import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils"; import { CommonUtils as ConsoleCommonUtils } from "@wso2is/admin.core.v1/utils/common-utils"; +import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils"; import { RouteUtils } from "@wso2is/admin.core.v1/utils/route-utils"; import { applicationConfig } from "@wso2is/admin.extensions.v1"; import FeatureGateConstants from "@wso2is/admin.feature-gate.v1/constants/feature-gate-constants"; @@ -55,10 +55,8 @@ import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils" import { Alert, ContentLoader, - EmptyPlaceholder, ErrorBoundary, - GenericIcon, - LinkButton + GenericIcon } from "@wso2is/react-components"; import isEmpty from "lodash-es/isEmpty"; import kebabCase from "lodash-es/kebabCase"; @@ -194,6 +192,11 @@ const DashboardLayout: FunctionComponent = ( setPreferences({ leftNavbarCollapsed: !leftNavbarCollapsed }); }; + const handleRouteChunkError: ((_error: Error, _errorInfo: React.ErrorInfo) => void) = + createRouteErrorHandler(config.deployment.appHomePath); + + const brokenPageFallback: ReactNode = createBrokenPageFallback(t); + /** * Conditionally renders a route. If a route has defined a Redirect to * URL, it will be directed to the specified one. If the route is stated @@ -216,11 +219,22 @@ const DashboardLayout: FunctionComponent = ( ) : ( - route.component ? ( - - ) : null - } + render={ (renderProps: RouteComponentProps): ReactNode => { + if (!route.component) { + return null; + } + + return ( + + + + ); + } } key={ key } exact={ route.exact } /> @@ -434,37 +448,8 @@ const DashboardLayout: FunctionComponent = ( > { - sessionStorage.setItem("auth_callback_url_console", config.deployment.appHomePath); - } } - fallback={ - ( CommonUtils.refreshPage() } - > - { t( - "console:common.placeholders.brokenPage.action" - ) } - ) - } - image={ - getEmptyPlaceholderIllustrations().brokenPage - } - imageSize="tiny" - subtitle={ [ - t( - "console:common.placeholders.brokenPage.subtitles.0" - ), - t( - "console:common.placeholders.brokenPage.subtitles.1" - ) - ] } - title={ t( - "console:common.placeholders.brokenPage.title" - ) } - />) - } + handleError={ handleRouteChunkError } + fallback={ brokenPageFallback } > }> { diff --git a/apps/console/src/layouts/default-layout.tsx b/apps/console/src/layouts/default-layout.tsx index 1e9b5b0c8a4..92ba29d867f 100644 --- a/apps/console/src/layouts/default-layout.tsx +++ b/apps/console/src/layouts/default-layout.tsx @@ -24,19 +24,20 @@ import { FeatureAccessConfigInterface, useRequiredScopes } from "@wso2is/access- import { getProfileInformation } from "@wso2is/admin.authentication.v1/store"; import Header from "@wso2is/admin.core.v1/components/header"; import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route"; -import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; + import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { UIConstants } from "@wso2is/admin.core.v1/constants/ui-constants"; import { history } from "@wso2is/admin.core.v1/helpers/history"; import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config"; import { AppState } from "@wso2is/admin.core.v1/store"; import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils"; +import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils"; import { RouteUtils } from "@wso2is/admin.core.v1/utils/route-utils"; import { applicationConfig } from "@wso2is/admin.extensions.v1"; import { AlertInterface, ProfileInfoInterface, RouteInterface } from "@wso2is/core/models"; import { initializeAlertSystem } from "@wso2is/core/store"; -import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils"; -import { Alert, ContentLoader, EmptyPlaceholder, ErrorBoundary, LinkButton } from "@wso2is/react-components"; +import { RouteUtils as CommonRouteUtils } from "@wso2is/core/utils"; +import { Alert, ContentLoader, ErrorBoundary } from "@wso2is/react-components"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -124,6 +125,10 @@ export const DefaultLayout: FunctionComponent = ({ * @param key - Index of the route. * @returns Resolved route to be rendered. */ + const handleRouteChunkError: ((_error: Error, _errorInfo: React.ErrorInfo) => void) = createRouteErrorHandler(appHomePath); + + const brokenPageFallback: ReactNode = createBrokenPageFallback(t); + const renderRoute = (route: RouteInterface, key: number): ReactNode => route.redirectTo ? ( @@ -137,9 +142,22 @@ export const DefaultLayout: FunctionComponent = ({ ) : ( - route.component ? : null - } + render={ (renderProps: RouteComponentProps): ReactNode => { + if (!route.component) { + return null; + } + + return ( + + + + ); + } } key={ key } exact={ route.exact } /> @@ -202,25 +220,8 @@ export const DefaultLayout: FunctionComponent = ({ > { - sessionStorage.setItem("auth_callback_url_console", appHomePath); - } } - fallback={ - ( CommonUtils.refreshPage() }> - { t("console:common.placeholders.brokenPage.action") } - ) - } - image={ getEmptyPlaceholderIllustrations().brokenPage } - imageSize="tiny" - subtitle={ [ - t("console:common.placeholders.brokenPage.subtitles.0"), - t("console:common.placeholders.brokenPage.subtitles.1") - ] } - title={ t("console:common.placeholders.brokenPage.title") } - />) - } + handleError={ handleRouteChunkError } + fallback={ brokenPageFallback } > }> { isMarketingConsentBannerEnabled && applicationConfig.marketingConsent.getBannerComponent() } diff --git a/apps/console/src/layouts/full-screen-layout.tsx b/apps/console/src/layouts/full-screen-layout.tsx index 2d4bd78aeaf..a929cb9e585 100644 --- a/apps/console/src/layouts/full-screen-layout.tsx +++ b/apps/console/src/layouts/full-screen-layout.tsx @@ -17,20 +17,18 @@ */ import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route"; -import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config"; import { AppState } from "@wso2is/admin.core.v1/store"; import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils"; +import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils"; import { RouteUtils } from "@wso2is/admin.core.v1/utils/route-utils"; import { RouteInterface } from "@wso2is/core/models"; -import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils"; +import { RouteUtils as CommonRouteUtils } from "@wso2is/core/utils"; import { ContentLoader, - EmptyPlaceholder, ErrorBoundary, - FullScreenLayout as FullScreenLayoutSkeleton, - LinkButton + FullScreenLayout as FullScreenLayoutSkeleton } from "@wso2is/react-components"; import isEmpty from "lodash-es/isEmpty"; import React, { @@ -108,6 +106,10 @@ const FullScreenLayout: FunctionComponent = ( * @param key - Index of the route. * @returns Resolved route to be rendered. */ + const handleRouteChunkError: ((_error: Error, _errorInfo: React.ErrorInfo) => void) = createRouteErrorHandler(appHomePath); + + const brokenPageFallback: ReactNode = createBrokenPageFallback(t); + const renderRoute = (route: RouteInterface, key: number): ReactNode => ( route.redirectTo ? @@ -123,11 +125,22 @@ const FullScreenLayout: FunctionComponent = ( : ( - route.component - ? - : null - } + render={ (renderProps: RouteComponentProps): ReactNode => { + if (!route.component) { + return null; + } + + return ( + + + + ); + } } key={ key } exact={ route.exact } /> @@ -165,25 +178,8 @@ const FullScreenLayout: FunctionComponent = ( { - sessionStorage.setItem("auth_callback_url_console", appHomePath); - } } - fallback={ ( - CommonUtils.refreshPage() }> - { t("console:common.placeholders.brokenPage.action") } - - ) } - image={ getEmptyPlaceholderIllustrations().brokenPage } - imageSize="tiny" - subtitle={ [ - t("console:common.placeholders.brokenPage.subtitles.0"), - t("console:common.placeholders.brokenPage.subtitles.1") - ] } - title={ t("console:common.placeholders.brokenPage.title") } - /> - ) } + handleError={ handleRouteChunkError } + fallback={ brokenPageFallback } > }> diff --git a/apps/myaccount/src/components/shared/protected-route.tsx b/apps/myaccount/src/components/shared/protected-route.tsx index 45255280702..61e2be941e6 100644 --- a/apps/myaccount/src/components/shared/protected-route.tsx +++ b/apps/myaccount/src/components/shared/protected-route.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -22,6 +22,7 @@ import { AuthenticateUtils } from "@wso2is/core/utils"; import React, { FunctionComponent, ReactElement } from "react"; import { useSelector } from "react-redux"; import { Redirect, Route, RouteComponentProps, RouteProps } from "react-router-dom"; +import RouteErrorBoundary from "./route-error-boundary"; import { AppConstants } from "../../constants"; import { AppState } from "../../store"; @@ -77,11 +78,25 @@ export const ProtectedRoute: FunctionComponent = ( const scopes: string[] = allowedScopes?.split(" "); if (!route?.scope) { - return (); + return ( + + + + ); } if (scopes?.includes(route?.scope)) { - return ; + return ( + + + + ); } else { return ; } diff --git a/apps/myaccount/src/components/shared/route-error-boundary.tsx b/apps/myaccount/src/components/shared/route-error-boundary.tsx new file mode 100644 index 00000000000..60839cc6e8b --- /dev/null +++ b/apps/myaccount/src/components/shared/route-error-boundary.tsx @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommonUtils } from "@wso2is/core/utils"; +import { EmptyPlaceholder, ErrorBoundary, LinkButton } from "@wso2is/react-components"; +import React, { ReactNode } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getEmptyPlaceholderIllustrations } from "../../configs/ui"; +import { AppUtils } from "../../utils/app-utils"; +import { EventPublisher } from "../../utils/event-publisher"; + +interface RouteErrorBoundaryProps { + children: ReactNode; + routeName?: string; +} + +const ROUTE_ERROR_EVENT_ID: string = "route-error-boundary"; + +/** + * Route-level Error Boundary that isolates errors to specific routes. + * + * @param props - Props for the component. + * @returns Route error boundary component. + */ +const RouteErrorBoundary: React.FC = ({ children, routeName }: RouteErrorBoundaryProps) => { + const { t } = useTranslation(); + + const handleRetry = (): void => { + CommonUtils.refreshPage(); + }; + + const handleError = (error: Error, errorInfo: React.ErrorInfo): void => { + EventPublisher.getInstance().publish(ROUTE_ERROR_EVENT_ID, { + componentStack: errorInfo?.componentStack ?? "N/A", + message: error?.message ?? "Unknown error", + name: error?.name ?? "Error", + route: routeName ?? "unknown" + }); + }; + + const renderFallback = (): ReactNode => { + const genericErrorSubtitles: ReactNode[] = [ + ( + + Something went wrong while displaying this page. + + ), + ( + + You can try navigating to other sections using the side panel. + + ) + ]; + + return ( +
+ + { t("myAccount:placeholders.genericError.action") } + + ) } + image={ getEmptyPlaceholderIllustrations().genericError } + imageSize="tiny" + subtitle={ genericErrorSubtitles } + title={ t("myAccount:placeholders.genericError.title") } + /> +
+ ); + }; + + return ( + + { children } + + ); +}; + +export default RouteErrorBoundary; diff --git a/apps/myaccount/src/layouts/app.tsx b/apps/myaccount/src/layouts/app.tsx index dd2f4dbdf7e..881883a6ae0 100644 --- a/apps/myaccount/src/layouts/app.tsx +++ b/apps/myaccount/src/layouts/app.tsx @@ -25,7 +25,7 @@ import { ErrorBoundary, LinkButton } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, Suspense, useEffect, useState } from "react"; +import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; @@ -51,6 +51,25 @@ export const AppLayout: FunctionComponent> = (): ReactEl return state.config.ui.isCookieConsentBannerEnabled; }); + const genericErrorSubtitles: string[] = [ + t("myAccount:placeholders.genericError.subtitles.0"), + t("myAccount:placeholders.genericError.subtitles.1") + ]; + + const genericErrorFallback: ReactNode = ( + CommonUtils.refreshPage() }> + { t("myAccount:placeholders.genericError.action") } + + ) } + image={ getEmptyPlaceholderIllustrations().genericError } + imageSize="tiny" + subtitle={ genericErrorSubtitles } + title={ t("myAccount:placeholders.genericError.title") } + /> + ); + /** * Listen for base name changes and updated the routes. */ @@ -62,22 +81,7 @@ export const AppLayout: FunctionComponent> = (): ReactEl CommonUtils.refreshPage() }> - { t("myAccount:placeholders.genericError.action") } - - ) } - image={ getEmptyPlaceholderIllustrations().genericError } - imageSize="tiny" - subtitle={ [ - t("myAccount:placeholders.genericError.subtitles.0"), - t("myAccount:placeholders.genericError.subtitles.1") - ] } - title={ t("myAccount:placeholders.genericError.title") } - /> - ) } + fallback={ genericErrorFallback } > }> @@ -97,11 +101,21 @@ export const AppLayout: FunctionComponent> = (): ReactEl : ( - route.component - ? - : null - } + render={ (renderProps: RouteComponentProps) => { + if (!route.component) { + return null; + } + + return ( + + + + ); + } } key={ index } exact={ route.exact } /> diff --git a/apps/myaccount/src/layouts/default.tsx b/apps/myaccount/src/layouts/default.tsx index f2724bdbd1e..7745250b7c6 100644 --- a/apps/myaccount/src/layouts/default.tsx +++ b/apps/myaccount/src/layouts/default.tsx @@ -19,9 +19,13 @@ import AppShell from "@oxygen-ui/react/AppShell"; import { AlertInterface, RouteInterface } from "@wso2is/core/models"; import { initializeAlertSystem } from "@wso2is/core/store"; +import { CommonUtils } from "@wso2is/core/utils"; import { Alert, - ContentLoader + ContentLoader, + EmptyPlaceholder, + ErrorBoundary, + LinkButton } from "@wso2is/react-components"; import React, { FunctionComponent, @@ -30,6 +34,7 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { System } from "react-notification-system"; import { useDispatch, useSelector } from "react-redux"; import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; @@ -38,8 +43,10 @@ import { Header, ProtectedRoute } from "../components"; import { getDefaultLayoutRoutes } from "../configs"; +import { getEmptyPlaceholderIllustrations } from "../configs/ui"; import { AppConstants, UIConstants } from "../constants"; import { AppState } from "../store"; +import { AppUtils } from "../utils"; import "./default.scss"; /** @@ -51,6 +58,7 @@ import "./default.scss"; */ export const DefaultLayout: FunctionComponent = (): ReactElement => { const dispatch: Dispatch = useDispatch(); + const { t } = useTranslation(); const alert: AlertInterface = useSelector( (state: AppState) => state.global.alert @@ -104,7 +112,28 @@ export const DefaultLayout: FunctionComponent = (): ReactElement => { path={ route.path } render={ (renderProps: RouteComponentProps) => route.component ? ( - + CommonUtils.refreshPage() }> + { t("myAccount:placeholders.genericError.action") } + + ) } + image={ getEmptyPlaceholderIllustrations().genericError } + imageSize="tiny" + subtitle={ [ + t("myAccount:placeholders.genericError.subtitles.0"), + t("myAccount:placeholders.genericError.subtitles.1") + ] } + title={ t("myAccount:placeholders.genericError.title") } + /> + ) } + > + + ) : null } key={ index } diff --git a/features/admin.core.v1/components/protected-route.tsx b/features/admin.core.v1/components/protected-route.tsx index 17f459b8450..cd2e8863fef 100644 --- a/features/admin.core.v1/components/protected-route.tsx +++ b/features/admin.core.v1/components/protected-route.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2020-2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -21,6 +21,7 @@ import { AuthenticateUtils } from "@wso2is/core/utils"; import React, { FunctionComponent, ReactElement } from "react"; import { useSelector } from "react-redux"; import { Redirect, Route, RouteComponentProps, RouteProps } from "react-router-dom"; +import RouteErrorBoundary from "./route-error-boundary"; import { AppConstants } from "../constants/app-constants"; import { AppState } from "../store"; @@ -59,7 +60,14 @@ export const ProtectedRoute: FunctionComponent = (props: RouteProps) render={ (renderProps: RouteComponentProps) => isAuthenticated ? Component - ? + ? ( + + + + ) : null : } diff --git a/features/admin.core.v1/components/route-error-boundary.tsx b/features/admin.core.v1/components/route-error-boundary.tsx new file mode 100644 index 00000000000..e6acab0f26f --- /dev/null +++ b/features/admin.core.v1/components/route-error-boundary.tsx @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommonUtils } from "@wso2is/core/utils"; +import { EmptyPlaceholder, ErrorBoundary, LinkButton } from "@wso2is/react-components"; +import React, { ReactNode } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { getEmptyPlaceholderIllustrations } from "../configs/ui"; +import { AppUtils } from "../utils/app-utils"; +import { EventPublisher } from "../utils/event-publisher"; + +interface RouteErrorBoundaryProps { + children: ReactNode; + routeName?: string; +} + +const ROUTE_ERROR_EVENT_ID: string = "route-error-boundary"; + +/** + * Route-level Error Boundary that isolates errors to specific routes. + * + * @param props - Props for the component. + * @returns Route error boundary component. + */ +const RouteErrorBoundary: React.FC = ({ children, routeName }: RouteErrorBoundaryProps) => { + const { t } = useTranslation(); + + const handleRetry = (): void => { + CommonUtils.refreshPage(); + }; + + const handleError = (error: Error, errorInfo: React.ErrorInfo): void => { + EventPublisher.getInstance().publish(ROUTE_ERROR_EVENT_ID, { + componentStack: errorInfo?.componentStack ?? "N/A", + message: error?.message ?? "Unknown error", + name: error?.name ?? "Error", + route: routeName ?? "unknown" + }); + }; + + const renderFallback = (): ReactNode => { + const brokenPageSubtitles: ReactNode[] = [ + ( + + Something went wrong while displaying this page. + + ), + ( + + You can try navigating to other sections using the side panel. + + ) + ]; + + return ( +
+ + { t("console:common.placeholders.brokenPage.action") } + + ) } + image={ getEmptyPlaceholderIllustrations().brokenPage } + imageSize="tiny" + subtitle={ brokenPageSubtitles } + title={ t("console:common.placeholders.brokenPage.title") } + /> +
+ ); + }; + + return ( + + { children } + + ); +}; + +export default RouteErrorBoundary; diff --git a/features/admin.core.v1/utils/error-boundary-utils.tsx b/features/admin.core.v1/utils/error-boundary-utils.tsx new file mode 100644 index 00000000000..a9afbe2d6a7 --- /dev/null +++ b/features/admin.core.v1/utils/error-boundary-utils.tsx @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommonUtils } from "@wso2is/core/utils"; +import { EmptyPlaceholder, LinkButton } from "@wso2is/react-components"; +import React, { ReactNode } from "react"; +import { getEmptyPlaceholderIllustrations } from "../configs/ui"; + +/** + * Creates a route chunk error handler that stores the app home path in sessionStorage. + * + * @param appHomePath - The application home path to store. + * @returns A callback function for handling route chunk errors. + */ +export const createRouteErrorHandler = ( + appHomePath: string +): ((_error: Error, _errorInfo: React.ErrorInfo) => void) => { + return (_error: Error, _errorInfo: React.ErrorInfo): void => { + sessionStorage.setItem("auth_callback_url_console", appHomePath); + }; +}; + +/** + * Creates a broken page fallback component with error message and retry action. + * + * @param t - The translation function from i18next. + * @returns A ReactNode representing the broken page fallback UI. + */ +export const createBrokenPageFallback = ( + t: (key: string, defaultValue?: string) => string +): ReactNode => { + const brokenPageSubtitles: string[] = [ + t("console:common.placeholders.brokenPage.subtitles.0"), + t("console:common.placeholders.brokenPage.subtitles.1") + ]; + + return ( + CommonUtils.refreshPage() }> + { t("console:common.placeholders.brokenPage.action") } + + ) } + image={ getEmptyPlaceholderIllustrations().brokenPage } + imageSize="tiny" + subtitle={ brokenPageSubtitles } + title={ t("console:common.placeholders.brokenPage.title") } + /> + ); +};