Skip to content

Commit d62d2ee

Browse files
committed
refactor: extract duplicated error boundary logic to shared utility module
- Create error-boundary-utils.tsx with createRouteErrorHandler() and createBrokenPageFallback() functions - Extract sessionStorage error handler logic shared across layouts - Extract broken page fallback UI component logic shared across layouts - Update default-layout.tsx, app-layout.tsx, and full-screen-layout.tsx to use shared utilities - Remove duplicated error handling code and unused imports from all three layouts - Improves code reuse and maintainability across console layouts
1 parent 4731552 commit d62d2ee

File tree

11 files changed

+473
-147
lines changed

11 files changed

+473
-147
lines changed

apps/console/src/layouts/app-layout.tsx

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,16 @@
1818

1919
import { PreLoader } from "@wso2is/admin.core.v1/components/pre-loader";
2020
import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route";
21-
import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui";
2221
import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants";
2322
import { AppState, store } from "@wso2is/admin.core.v1/store";
2423
import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils";
24+
import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils";
2525
import { RouteInterface } from "@wso2is/core/models";
26-
import { CommonUtils } from "@wso2is/core/utils";
2726
import {
2827
CookieConsentBanner,
29-
EmptyPlaceholder,
30-
ErrorBoundary,
31-
LinkButton
28+
ErrorBoundary
3229
} from "@wso2is/react-components";
33-
import React, { FunctionComponent, ReactElement, Suspense, useEffect, useState } from "react";
30+
import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react";
3431
import { Trans, useTranslation } from "react-i18next";
3532
import { useSelector } from "react-redux";
3633
import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom";
@@ -52,6 +49,10 @@ const AppLayout: FunctionComponent<Record<string, unknown>> = (): ReactElement =
5249
});
5350
const appHomePath: string = useSelector((state: AppState) => state.config.deployment.appHomePath);
5451

52+
const handleRouteChunkError = createRouteErrorHandler(appHomePath);
53+
54+
const brokenPageFallback: ReactNode = createBrokenPageFallback(t);
55+
5556
/**
5657
* Listen for base name changes and updated the layout routes.
5758
*/
@@ -63,25 +64,8 @@ const AppLayout: FunctionComponent<Record<string, unknown>> = (): ReactElement =
6364
<>
6465
<ErrorBoundary
6566
onChunkLoadError={ AppUtils.onChunkLoadError }
66-
handleError={ (_error: Error, _errorInfo: React.ErrorInfo) => {
67-
sessionStorage.setItem("auth_callback_url_console", appHomePath);
68-
} }
69-
fallback={ (
70-
<EmptyPlaceholder
71-
action={ (
72-
<LinkButton onClick={ () => CommonUtils.refreshPage() }>
73-
{ t("console:common.placeholders.brokenPage.action") }
74-
</LinkButton>
75-
) }
76-
image={ getEmptyPlaceholderIllustrations().brokenPage }
77-
imageSize="tiny"
78-
subtitle={ [
79-
t("console:common.placeholders.brokenPage.subtitles.0"),
80-
t("console:common.placeholders.brokenPage.subtitles.1")
81-
] }
82-
title={ t("console:common.placeholders.brokenPage.title") }
83-
/>
84-
) }
67+
handleError={ handleRouteChunkError }
68+
fallback={ brokenPageFallback }
8569
>
8670
<Suspense fallback={ <PreLoader /> }>
8771
<Switch>
@@ -101,11 +85,22 @@ const AppLayout: FunctionComponent<Record<string, unknown>> = (): ReactElement =
10185
: (
10286
<Route
10387
path={ route.path }
104-
render={ (renderProps: RouteComponentProps) =>
105-
route.component
106-
? <route.component { ...renderProps } />
107-
: null
108-
}
88+
render={ (renderProps: RouteComponentProps) => {
89+
if (!route.component) {
90+
return null;
91+
}
92+
93+
return (
94+
<ErrorBoundary
95+
key={ renderProps.location.pathname }
96+
onChunkLoadError={ AppUtils.onChunkLoadError }
97+
handleError={ handleRouteChunkError }
98+
fallback={ brokenPageFallback }
99+
>
100+
<route.component { ...renderProps } />
101+
</ErrorBoundary>
102+
);
103+
} }
109104
key={ index }
110105
exact={ route.exact }
111106
/>

apps/console/src/layouts/dashboard-layout.tsx

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,29 @@ const DashboardLayout: FunctionComponent<RouteComponentProps> = (
194194
setPreferences({ leftNavbarCollapsed: !leftNavbarCollapsed });
195195
};
196196

197+
const handleRouteChunkError = (_error: Error, _errorInfo: React.ErrorInfo): void => {
198+
sessionStorage.setItem("auth_callback_url_console", config.deployment.appHomePath);
199+
};
200+
201+
const brokenPageSubtitles: string[] = [
202+
t("console:common.placeholders.brokenPage.subtitles.0"),
203+
t("console:common.placeholders.brokenPage.subtitles.1")
204+
];
205+
206+
const brokenPageFallback: ReactNode = (
207+
<EmptyPlaceholder
208+
action={ (
209+
<LinkButton onClick={ () => CommonUtils.refreshPage() }>
210+
{ t("console:common.placeholders.brokenPage.action") }
211+
</LinkButton>
212+
) }
213+
image={ getEmptyPlaceholderIllustrations().brokenPage }
214+
imageSize="tiny"
215+
subtitle={ brokenPageSubtitles }
216+
title={ t("console:common.placeholders.brokenPage.title") }
217+
/>
218+
);
219+
197220
/**
198221
* Conditionally renders a route. If a route has defined a Redirect to
199222
* URL, it will be directed to the specified one. If the route is stated
@@ -216,11 +239,22 @@ const DashboardLayout: FunctionComponent<RouteComponentProps> = (
216239
) : (
217240
<Route
218241
path={ route.path }
219-
render={ (renderProps: RouteComponentProps): ReactNode =>
220-
route.component ? (
221-
<route.component { ...renderProps } />
222-
) : null
223-
}
242+
render={ (renderProps: RouteComponentProps): ReactNode => {
243+
if (!route.component) {
244+
return null;
245+
}
246+
247+
return (
248+
<ErrorBoundary
249+
key={ renderProps.location.pathname }
250+
onChunkLoadError={ AppUtils.onChunkLoadError }
251+
handleError={ handleRouteChunkError }
252+
fallback={ brokenPageFallback }
253+
>
254+
<route.component { ...renderProps } />
255+
</ErrorBoundary>
256+
);
257+
} }
224258
key={ key }
225259
exact={ route.exact }
226260
/>
@@ -434,37 +468,8 @@ const DashboardLayout: FunctionComponent<RouteComponentProps> = (
434468
>
435469
<ErrorBoundary
436470
onChunkLoadError={ AppUtils.onChunkLoadError }
437-
handleError={ (_error: Error, _errorInfo: React.ErrorInfo) => {
438-
sessionStorage.setItem("auth_callback_url_console", config.deployment.appHomePath);
439-
} }
440-
fallback={
441-
(<EmptyPlaceholder
442-
action={
443-
(<LinkButton
444-
onClick={ () => CommonUtils.refreshPage() }
445-
>
446-
{ t(
447-
"console:common.placeholders.brokenPage.action"
448-
) }
449-
</LinkButton>)
450-
}
451-
image={
452-
getEmptyPlaceholderIllustrations().brokenPage
453-
}
454-
imageSize="tiny"
455-
subtitle={ [
456-
t(
457-
"console:common.placeholders.brokenPage.subtitles.0"
458-
),
459-
t(
460-
"console:common.placeholders.brokenPage.subtitles.1"
461-
)
462-
] }
463-
title={ t(
464-
"console:common.placeholders.brokenPage.title"
465-
) }
466-
/>)
467-
}
471+
handleError={ handleRouteChunkError }
472+
fallback={ brokenPageFallback }
468473
>
469474
<Suspense fallback={ <ContentLoader dimmer={ false } /> }>
470475
{

apps/console/src/layouts/default-layout.tsx

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ import { history } from "@wso2is/admin.core.v1/helpers/history";
3131
import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config";
3232
import { AppState } from "@wso2is/admin.core.v1/store";
3333
import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils";
34+
import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils";
3435
import { RouteUtils } from "@wso2is/admin.core.v1/utils/route-utils";
3536
import { applicationConfig } from "@wso2is/admin.extensions.v1";
3637
import { AlertInterface, ProfileInfoInterface, RouteInterface } from "@wso2is/core/models";
3738
import { initializeAlertSystem } from "@wso2is/core/store";
3839
import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils";
39-
import { Alert, ContentLoader, EmptyPlaceholder, ErrorBoundary, LinkButton } from "@wso2is/react-components";
40+
import { Alert, ContentLoader, ErrorBoundary } from "@wso2is/react-components";
4041
import isEmpty from "lodash-es/isEmpty";
4142
import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react";
4243
import { useTranslation } from "react-i18next";
@@ -124,6 +125,10 @@ export const DefaultLayout: FunctionComponent<DefaultLayoutPropsInterface> = ({
124125
* @param key - Index of the route.
125126
* @returns Resolved route to be rendered.
126127
*/
128+
const handleRouteChunkError = createRouteErrorHandler(appHomePath);
129+
130+
const brokenPageFallback: ReactNode = createBrokenPageFallback(t);
131+
127132
const renderRoute = (route: RouteInterface, key: number): ReactNode =>
128133
route.redirectTo ? (
129134
<Redirect key={ key } to={ route.redirectTo } />
@@ -137,9 +142,22 @@ export const DefaultLayout: FunctionComponent<DefaultLayoutPropsInterface> = ({
137142
) : (
138143
<Route
139144
path={ route.path }
140-
render={ (renderProps: RouteComponentProps): ReactNode =>
141-
route.component ? <route.component { ...renderProps } /> : null
142-
}
145+
render={ (renderProps: RouteComponentProps): ReactNode => {
146+
if (!route.component) {
147+
return null;
148+
}
149+
150+
return (
151+
<ErrorBoundary
152+
key={ renderProps.location.pathname }
153+
onChunkLoadError={ AppUtils.onChunkLoadError }
154+
handleError={ handleRouteChunkError }
155+
fallback={ brokenPageFallback }
156+
>
157+
<route.component { ...renderProps } />
158+
</ErrorBoundary>
159+
);
160+
} }
143161
key={ key }
144162
exact={ route.exact }
145163
/>
@@ -202,25 +220,8 @@ export const DefaultLayout: FunctionComponent<DefaultLayoutPropsInterface> = ({
202220
>
203221
<ErrorBoundary
204222
onChunkLoadError={ AppUtils.onChunkLoadError }
205-
handleError={ (_error: Error, _errorInfo: React.ErrorInfo) => {
206-
sessionStorage.setItem("auth_callback_url_console", appHomePath);
207-
} }
208-
fallback={
209-
(<EmptyPlaceholder
210-
action={
211-
(<LinkButton onClick={ () => CommonUtils.refreshPage() }>
212-
{ t("console:common.placeholders.brokenPage.action") }
213-
</LinkButton>)
214-
}
215-
image={ getEmptyPlaceholderIllustrations().brokenPage }
216-
imageSize="tiny"
217-
subtitle={ [
218-
t("console:common.placeholders.brokenPage.subtitles.0"),
219-
t("console:common.placeholders.brokenPage.subtitles.1")
220-
] }
221-
title={ t("console:common.placeholders.brokenPage.title") }
222-
/>)
223-
}
223+
handleError={ handleRouteChunkError }
224+
fallback={ brokenPageFallback }
224225
>
225226
<Suspense fallback={ <ContentLoader dimmer={ false } /> }>
226227
{ isMarketingConsentBannerEnabled && applicationConfig.marketingConsent.getBannerComponent() }

apps/console/src/layouts/full-screen-layout.tsx

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,18 @@
1717
*/
1818

1919
import { ProtectedRoute } from "@wso2is/admin.core.v1/components/protected-route";
20-
import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui";
2120
import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants";
2221
import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config";
2322
import { AppState } from "@wso2is/admin.core.v1/store";
2423
import { AppUtils } from "@wso2is/admin.core.v1/utils/app-utils";
24+
import { createBrokenPageFallback, createRouteErrorHandler } from "@wso2is/admin.core.v1/utils/error-boundary-utils";
2525
import { RouteUtils } from "@wso2is/admin.core.v1/utils/route-utils";
2626
import { RouteInterface } from "@wso2is/core/models";
27-
import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils";
27+
import { RouteUtils as CommonRouteUtils } from "@wso2is/core/utils";
2828
import {
2929
ContentLoader,
30-
EmptyPlaceholder,
3130
ErrorBoundary,
32-
FullScreenLayout as FullScreenLayoutSkeleton,
33-
LinkButton
31+
FullScreenLayout as FullScreenLayoutSkeleton
3432
} from "@wso2is/react-components";
3533
import isEmpty from "lodash-es/isEmpty";
3634
import React, {
@@ -108,6 +106,10 @@ const FullScreenLayout: FunctionComponent<FullScreenLayoutPropsInterface> = (
108106
* @param key - Index of the route.
109107
* @returns Resolved route to be rendered.
110108
*/
109+
const handleRouteChunkError = createRouteErrorHandler(appHomePath);
110+
111+
const brokenPageFallback: ReactNode = createBrokenPageFallback(t);
112+
111113
const renderRoute = (route: RouteInterface, key: number): ReactNode => (
112114
route.redirectTo
113115
? <Redirect key={ key } to={ route.redirectTo }/>
@@ -123,11 +125,22 @@ const FullScreenLayout: FunctionComponent<FullScreenLayoutPropsInterface> = (
123125
: (
124126
<Route
125127
path={ route.path }
126-
render={ (renderProps: RouteComponentProps): ReactNode =>
127-
route.component
128-
? <route.component { ...renderProps } />
129-
: null
130-
}
128+
render={ (renderProps: RouteComponentProps): ReactNode => {
129+
if (!route.component) {
130+
return null;
131+
}
132+
133+
return (
134+
<ErrorBoundary
135+
key={ renderProps.location.pathname }
136+
onChunkLoadError={ AppUtils.onChunkLoadError }
137+
handleError={ handleRouteChunkError }
138+
fallback={ brokenPageFallback }
139+
>
140+
<route.component { ...renderProps } />
141+
</ErrorBoundary>
142+
);
143+
} }
131144
key={ key }
132145
exact={ route.exact }
133146
/>
@@ -165,25 +178,8 @@ const FullScreenLayout: FunctionComponent<FullScreenLayoutPropsInterface> = (
165178
<FullScreenLayoutSkeleton>
166179
<ErrorBoundary
167180
onChunkLoadError={ AppUtils.onChunkLoadError }
168-
handleError={ (_error: Error, _errorInfo: React.ErrorInfo) => {
169-
sessionStorage.setItem("auth_callback_url_console", appHomePath);
170-
} }
171-
fallback={ (
172-
<EmptyPlaceholder
173-
action={ (
174-
<LinkButton onClick={ () => CommonUtils.refreshPage() }>
175-
{ t("console:common.placeholders.brokenPage.action") }
176-
</LinkButton>
177-
) }
178-
image={ getEmptyPlaceholderIllustrations().brokenPage }
179-
imageSize="tiny"
180-
subtitle={ [
181-
t("console:common.placeholders.brokenPage.subtitles.0"),
182-
t("console:common.placeholders.brokenPage.subtitles.1")
183-
] }
184-
title={ t("console:common.placeholders.brokenPage.title") }
185-
/>
186-
) }
181+
handleError={ handleRouteChunkError }
182+
fallback={ brokenPageFallback }
187183
>
188184
<Suspense fallback={ <ContentLoader dimmer={ false } /> }>
189185
<Switch>

apps/myaccount/src/components/shared/protected-route.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2019, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
33
*
44
* WSO2 LLC. licenses this file to you under the Apache License,
55
* Version 2.0 (the "License"); you may not use this file except
@@ -22,6 +22,7 @@ import { AuthenticateUtils } from "@wso2is/core/utils";
2222
import React, { FunctionComponent, ReactElement } from "react";
2323
import { useSelector } from "react-redux";
2424
import { Redirect, Route, RouteComponentProps, RouteProps } from "react-router-dom";
25+
import RouteErrorBoundary from "./route-error-boundary";
2526
import { AppConstants } from "../../constants";
2627
import { AppState } from "../../store";
2728

@@ -77,11 +78,19 @@ export const ProtectedRoute: FunctionComponent<ProtectedRoutePropsInterface> = (
7778
const scopes: string[] = allowedScopes?.split(" ");
7879

7980
if (!route?.scope) {
80-
return (<Component { ...props } />);
81+
return (
82+
<RouteErrorBoundary routeName={ props.match?.path }>
83+
<Component { ...props } />
84+
</RouteErrorBoundary>
85+
);
8186
}
8287

8388
if (scopes?.includes(route?.scope)) {
84-
return <Component { ...props } />;
89+
return (
90+
<RouteErrorBoundary routeName={ props.match?.path }>
91+
<Component { ...props } />
92+
</RouteErrorBoundary>
93+
);
8594
} else {
8695
return <Redirect to={ AppConstants.getPaths().get("ACCESS_DENIED_ERROR") } />;
8796
}

0 commit comments

Comments
 (0)