Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a global lazyLoading error handling #7227

Merged
merged 9 commits into from
Nov 22, 2024
3 changes: 2 additions & 1 deletion src/core/i18n/en/translation.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2556,7 +2556,8 @@
"line3": "If the error persists please contact support.",
"buttons": {
"reload": "Reload"
}
},
"dynamicError": "network error or a newer version available. Please reload the page."
},
"four-ou-four": {
"title": "Something went wrong...",
Expand Down
38 changes: 38 additions & 0 deletions src/core/lazyLoading/GlobalErrorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';

type GlobalErrorContextType = {
error: Error | null;
setError: (error: Error | null) => void;
};

// Global function to set error (to be initialized by provider)
let setGlobalError: ((error: Error | null) => void) | null = null;
ccanos marked this conversation as resolved.
Show resolved Hide resolved

export const useGlobalError = (): GlobalErrorContextType => {
const context = useContext(GlobalErrorContext);
if (!context) {
throw new Error('useGlobalError must be used within a GlobalErrorProvider');
}
return context;
};

const GlobalErrorContext = createContext<GlobalErrorContextType | undefined>(undefined);

// GlobalErrorProvider but used only for LazyLoading ATM
// see SentryErrorBoundary for global error handling
export const GlobalErrorProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [error, setError] = useState<Error | null>(null);

// Set the global error function during initialization
setGlobalError = setError;

return <GlobalErrorContext.Provider value={{ error, setError }}>{children}</GlobalErrorContext.Provider>;
};
bobbykolev marked this conversation as resolved.
Show resolved Hide resolved

// the global error setter
export const getGlobalErrorSetter = (): ((error: Error | null) => void) => {
if (!setGlobalError) {
throw new Error('GlobalErrorProvider is not initialized.');
}
return setGlobalError;
};
45 changes: 45 additions & 0 deletions src/core/lazyLoading/GlobalErrorDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Box, Button, Dialog, DialogContent } from '@mui/material';
import { useGlobalError } from './GlobalErrorContext';
import DialogHeader from '../ui/dialog/DialogHeader';
import { BlockTitle } from '../ui/typography';

export const GlobalErrorDialog: React.FC = () => {
const { t } = useTranslation();
const { error, setError } = useGlobalError();

if (!error) return null;

return (
<Dialog open={!!error} aria-labelledby="global-error-dialog" onClose={() => setError(null)}>
<DialogHeader onClose={() => setError(null)}>
<BlockTitle>{t('pages.error.title')}</BlockTitle>
</DialogHeader>
<DialogContent>
<Box>
<Trans
i18nKey="pages.error.line1"
values={{
message: error.message?.includes('dynamic') ? t('pages.error.dynamicError') : null,
}}
components={{
italic: <i />,
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', marginTop: '20px' }}>
<Button
variant="contained"
onClick={() => {
setError(null);
window.location.reload();
}}
>
{t('pages.error.buttons.reload')}
</Button>
</Box>
</DialogContent>
</Dialog>
);
bobbykolev marked this conversation as resolved.
Show resolved Hide resolved
};
25 changes: 25 additions & 0 deletions src/core/lazyLoading/lazyWithGlobalErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { getGlobalErrorSetter } from './GlobalErrorContext';

type ImportFunc<T> = () => Promise<{ default: React.ComponentType<T> }>;

export const lazyWithGlobalErrorHandler = <T>(
importFunc: ImportFunc<T>
): React.LazyExoticComponent<React.ComponentType<T>> => {
return React.lazy(async () => {
try {
return await importFunc();
} catch (error) {
const setError = getGlobalErrorSetter();
setError(error as Error);
bobbykolev marked this conversation as resolved.
Show resolved Hide resolved

// it looks like this error is already logged by the useErrorLoggerLink (network error)

// Instead of throwing, return a fallback component to prevent
// catching it in the ErrorBoundary
return {
default: () => null,
};
}
});
};
31 changes: 18 additions & 13 deletions src/main/routing/TopLevelRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@ import { EntityPageLayoutHolder, NotFoundPageLayout, RenderPoint } from '../../d
import RedirectToWelcomeSite from '../../domain/platform/routes/RedirectToWelcomeSite';
import { TopLevelRoutePath } from './TopLevelRoutePath';
import Loading from '../../core/ui/loading/Loading';
import { lazyWithGlobalErrorHandler } from '../../core/lazyLoading/lazyWithGlobalErrorHandler';

const DocumentationPage = React.lazy(() => import('../../domain/documentation/DocumentationPage'));
const SpaceExplorerPage = React.lazy(() => import('../topLevelPages/topLevelSpaces/SpaceExplorerPage'));
const InnovationLibraryPage = React.lazy(() => import('../topLevelPages/InnovationLibraryPage/InnovationLibraryPage'));
const ContributorsPage = React.lazy(() => import('../../domain/community/user/ContributorsPage'));
const AdminRoute = React.lazy(() => import('../../domain/platform/admin/routing/AdminRoute'));
const UserRoute = React.lazy(() => import('../../domain/community/user/routing/UserRoute'));
const OrganizationRoute = React.lazy(
const DocumentationPage = lazyWithGlobalErrorHandler(() => import('../../domain/documentation/DocumentationPage'));
const SpaceExplorerPage = lazyWithGlobalErrorHandler(() => import('../topLevelPages/topLevelSpaces/SpaceExplorerPage'));
const InnovationLibraryPage = lazyWithGlobalErrorHandler(
() => import('../topLevelPages/InnovationLibraryPage/InnovationLibraryPage')
);
const ContributorsPage = lazyWithGlobalErrorHandler(() => import('../../domain/community/user/ContributorsPage'));
const AdminRoute = lazyWithGlobalErrorHandler(() => import('../../domain/platform/admin/routing/AdminRoute'));
const UserRoute = lazyWithGlobalErrorHandler(() => import('../../domain/community/user/routing/UserRoute'));
const OrganizationRoute = lazyWithGlobalErrorHandler(
() => import('../../domain/community/contributor/organization/routing/OrganizationRoute')
);
const VCRoute = React.lazy(() => import('../../domain/community/virtualContributor/VCRoute'));
const ForumRoute = React.lazy(() => import('../../domain/communication/discussion/routing/ForumRoute'));
const InnovationPackRoute = React.lazy(() => import('../../domain/InnovationPack/InnovationPackRoute'));
const ProfileRoute = React.lazy(() => import('../../domain/community/profile/routing/ProfileRoute'));
const CreateSpaceDialog = React.lazy(() => import('../../domain/journey/space/createSpace/CreateSpaceDialog'));
const SpaceRoute = React.lazy(() => import('../../domain/journey/space/routing/SpaceRoute'));
const VCRoute = lazyWithGlobalErrorHandler(() => import('../../domain/community/virtualContributor/VCRoute'));
const ForumRoute = lazyWithGlobalErrorHandler(() => import('../../domain/communication/discussion/routing/ForumRoute'));
const InnovationPackRoute = lazyWithGlobalErrorHandler(() => import('../../domain/InnovationPack/InnovationPackRoute'));
const ProfileRoute = lazyWithGlobalErrorHandler(() => import('../../domain/community/profile/routing/ProfileRoute'));
const CreateSpaceDialog = lazyWithGlobalErrorHandler(
() => import('../../domain/journey/space/createSpace/CreateSpaceDialog')
);
const SpaceRoute = lazyWithGlobalErrorHandler(() => import('../../domain/journey/space/routing/SpaceRoute'));

export const TopLevelRoutes = () => {
useRedirectToIdentityDomain();
Expand Down
3 changes: 2 additions & 1 deletion src/main/topLevelPages/Home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import InnovationHubHomePage from '../../../domain/innovationHub/InnovationHubHo
import Loading from '../../../core/ui/loading/Loading';
import useInnovationHub from '../../../domain/innovationHub/useInnovationHub/useInnovationHub';
import PageContent from '../../../core/ui/content/PageContent';
import { lazyWithGlobalErrorHandler } from '../../../core/lazyLoading/lazyWithGlobalErrorHandler';

const MyDashboard = React.lazy(() => import('../myDashboard/MyDashboard'));
const MyDashboard = lazyWithGlobalErrorHandler(() => import('../myDashboard/MyDashboard'));

const HomePage = () => {
const { innovationHub, innovationHubLoading } = useInnovationHub();
Expand Down
7 changes: 4 additions & 3 deletions src/main/topLevelPages/myDashboard/MyDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useLatestContributionsSpacesFlatQuery } from '../../../core/apollo/gene
import Loading from '../../../core/ui/loading/Loading';
import { useAuthenticationContext } from '../../../core/auth/authentication/hooks/useAuthenticationContext';
import { DashboardProvider } from './DashboardContext';
import { lazyWithGlobalErrorHandler } from '../../../core/lazyLoading/lazyWithGlobalErrorHandler';

const MyDashboardUnauthenticated = React.lazy(() => import('./MyDashboardUnauthenticated'));
const MyDashboardWithMemberships = React.lazy(() => import('./MyDashboardWithMemberships'));
const MyDashboardWithoutMemberships = React.lazy(() => import('./MyDashboardWithoutMemberships'));
const MyDashboardUnauthenticated = lazyWithGlobalErrorHandler(() => import('./MyDashboardUnauthenticated'));
const MyDashboardWithMemberships = lazyWithGlobalErrorHandler(() => import('./MyDashboardWithMemberships'));
const MyDashboardWithoutMemberships = lazyWithGlobalErrorHandler(() => import('./MyDashboardWithoutMemberships'));

export const MyDashboard = () => {
const { isAuthenticated, loading: isLoadingAuthentication } = useAuthenticationContext();
Expand Down
16 changes: 10 additions & 6 deletions src/main/topLevelPages/myDashboard/MyDashboardWithMemberships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import ContentColumn from '../../../core/ui/content/ContentColumn';
import { useDashboardContext } from './DashboardContext';
import MyResources from './myResources/MyResources';
import { Theme, useMediaQuery } from '@mui/material';
import { lazyWithGlobalErrorHandler } from '../../../core/lazyLoading/lazyWithGlobalErrorHandler';
import Loading from '../../../core/ui/loading/Loading';

const DashboardDialogs = React.lazy(() => import('./DashboardDialogs/DashboardDialogs'));
const DashboardActivity = React.lazy(() => import('./DashboardWithMemberships/DashboardActivity'));
const DashboardSpaces = React.lazy(() => import('./DashboardWithMemberships/DashboardSpaces/DashboardSpaces'));
const DashboardDialogs = lazyWithGlobalErrorHandler(() => import('./DashboardDialogs/DashboardDialogs'));
const DashboardActivity = lazyWithGlobalErrorHandler(() => import('./DashboardWithMemberships/DashboardActivity'));
const DashboardSpaces = lazyWithGlobalErrorHandler(
() => import('./DashboardWithMemberships/DashboardSpaces/DashboardSpaces')
);

const MyDashboardWithMemberships = () => {
const { activityEnabled } = useDashboardContext();
Expand All @@ -36,17 +40,17 @@ const MyDashboardWithMemberships = () => {
{data?.platform.latestReleaseDiscussion && <ReleaseNotesBanner />}
<CampaignBlock />
{!activityEnabled && (
<Suspense fallback={null}>
<Suspense fallback={<Loading />}>
<DashboardSpaces />
</Suspense>
)}
{activityEnabled && (
<Suspense fallback={null}>
<Suspense fallback={<Loading />}>
<DashboardActivity />
</Suspense>
)}
</ContentColumn>
<Suspense fallback={null}>
<Suspense fallback={<Loading />}>
<DashboardDialogs />
</Suspense>
</PageContentColumn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import { InvitationsBlock } from './InvitationsBlock/InvitationsBlock';
import { SpaceIcon } from '../../../domain/journey/space/icon/SpaceIcon';
import RouterLink from '../../../core/ui/link/RouterLink';
import { useCreateSpaceLink } from './useCreateSpaceLink/useCreateSpaceLink';
import { lazyWithGlobalErrorHandler } from '../../../core/lazyLoading/lazyWithGlobalErrorHandler';
import Loading from '../../../core/ui/loading/Loading';

const DashboardDialogs = React.lazy(() => import('./DashboardDialogs/DashboardDialogs'));
const DashboardDialogs = lazyWithGlobalErrorHandler(() => import('./DashboardDialogs/DashboardDialogs'));

const MyDashboardWithoutMemberships = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -57,7 +59,7 @@ const MyDashboardWithoutMemberships = () => {
{t('buttons.createOwnSpace')}
</Button>
</ContentColumn>
<Suspense fallback={null}>
<Suspense fallback={<Loading />}>
<DashboardDialogs />
</Suspense>
</PageContentColumn>
Expand Down
63 changes: 34 additions & 29 deletions src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { PendingMembershipsDialogProvider } from './domain/community/pendingMemb
import { NotFoundErrorBoundary } from './core/notFound/NotFoundErrorBoundary';
import { Error404 } from './core/pages/Errors/Error404';
import TopLevelLayout from './main/ui/layout/TopLevelLayout';
import { GlobalErrorProvider } from './core/lazyLoading/GlobalErrorContext';
import { GlobalErrorDialog } from './core/lazyLoading/GlobalErrorDialog';

const useGlobalStyles = makeStyles(theme => ({
'@global': {
Expand Down Expand Up @@ -75,35 +77,38 @@ const Root: FC = () => {
<ConfigProvider url={publicGraphQLEndpoint}>
<ServerMetadataProvider url={publicGraphQLEndpoint}>
<SentryTransactionScopeContextProvider>
<SentryErrorBoundaryProvider>
<GlobalStateProvider>
<BrowserRouter>
<AuthenticationProvider>
<UserGeoProvider>
<ApmProvider>
<AlkemioApolloProvider apiUrl={privateGraphQLEndpoint}>
<UserProvider>
<PendingMembershipsDialogProvider>
<ApmUserSetter />
<ScrollToTop />
<NotFoundErrorBoundary
errorComponent={
<TopLevelLayout>
<Error404 />
</TopLevelLayout>
}
>
<TopLevelRoutes />
</NotFoundErrorBoundary>
</PendingMembershipsDialogProvider>
</UserProvider>
</AlkemioApolloProvider>
</ApmProvider>
</UserGeoProvider>
</AuthenticationProvider>
</BrowserRouter>
</GlobalStateProvider>
</SentryErrorBoundaryProvider>
<GlobalErrorProvider>
<SentryErrorBoundaryProvider>
<GlobalStateProvider>
<BrowserRouter>
<AuthenticationProvider>
<UserGeoProvider>
<ApmProvider>
<AlkemioApolloProvider apiUrl={privateGraphQLEndpoint}>
<UserProvider>
<PendingMembershipsDialogProvider>
<ApmUserSetter />
<ScrollToTop />
<NotFoundErrorBoundary
errorComponent={
<TopLevelLayout>
<Error404 />
</TopLevelLayout>
}
>
<TopLevelRoutes />
<GlobalErrorDialog />
</NotFoundErrorBoundary>
</PendingMembershipsDialogProvider>
</UserProvider>
</AlkemioApolloProvider>
</ApmProvider>
</UserGeoProvider>
</AuthenticationProvider>
</BrowserRouter>
</GlobalStateProvider>
</SentryErrorBoundaryProvider>
</GlobalErrorProvider>
</SentryTransactionScopeContextProvider>
</ServerMetadataProvider>
</ConfigProvider>
Expand Down