diff --git a/apps/standalone/src/app/routes.tsx b/apps/standalone/src/app/routes.tsx index fe49b19e5..cc7d06014 100644 --- a/apps/standalone/src/app/routes.tsx +++ b/apps/standalone/src/app/routes.tsx @@ -15,6 +15,7 @@ import { type RouteObject, RouterProvider, createBrowserRouter, + useLocation, useParams, useRouteError, } from 'react-router-dom'; @@ -25,96 +26,101 @@ import { useDocumentTitle } from '@flightctl/ui-components/src/hooks/useDocument import { APP_TITLE } from '@flightctl/ui-components/src/constants'; import { useTranslation } from '@flightctl/ui-components/src/hooks/useTranslation'; import ErrorBoundary from '@flightctl/ui-components/src/components/common/ErrorBoundary'; +import { lazyResetter } from '@flightctl/ui-components/src/utils/resettableLazy'; import AppLayout from './components/AppLayout/AppLayout'; import NotFound from './components/AppLayout/NotFound'; import LoginPage from './components/Login/LoginPage'; import { AuthContext } from './context/AuthContext'; -const EnrollmentRequestDetails = React.lazy( +const EnrollmentRequestDetails = lazyResetter( () => import( '@flightctl/ui-components/src/components/EnrollmentRequest/EnrollmentRequestDetails/EnrollmentRequestDetails' ), ); -const DevicesPage = React.lazy(() => import('@flightctl/ui-components/src/components/Device/DevicesPage/DevicesPage')); -const DeviceDetails = React.lazy( +const DevicesPage = lazyResetter( + () => import('@flightctl/ui-components/src/components/Device/DevicesPage/DevicesPage'), +); +const DeviceDetails = lazyResetter( () => import('@flightctl/ui-components/src/components/Device/DeviceDetails/DeviceDetailsPage'), ); -const EditDeviceWizard = React.lazy( +const EditDeviceWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard'), ); -const CreateRepository = React.lazy( +const CreateRepository = lazyResetter( () => import('@flightctl/ui-components/src/components/Repository/CreateRepository/CreateRepository'), ); -const RepositoryList = React.lazy(() => import('@flightctl/ui-components/src/components/Repository/RepositoryList')); -const RepositoryDetails = React.lazy( +const RepositoryList = lazyResetter(() => import('@flightctl/ui-components/src/components/Repository/RepositoryList')); +const RepositoryDetails = lazyResetter( () => import('@flightctl/ui-components/src/components/Repository/RepositoryDetails/RepositoryDetails'), ); -const ResourceSyncToRepository = React.lazy( +const ResourceSyncToRepository = lazyResetter( () => import('@flightctl/ui-components/src/components/ResourceSync/ResourceSyncToRepository'), ); -const ImportFleetWizard = React.lazy( +const ImportFleetWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Fleet/ImportFleetWizard/ImportFleetWizard'), ); -const CreateFleetWizard = React.lazy( +const CreateFleetWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Fleet/CreateFleet/CreateFleetWizard'), ); -const FleetsPage = React.lazy(() => import('@flightctl/ui-components/src/components/Fleet/FleetsPage')); -const FleetDetails = React.lazy( +const FleetsPage = lazyResetter(() => import('@flightctl/ui-components/src/components/Fleet/FleetsPage')); +const FleetDetails = lazyResetter( () => import('@flightctl/ui-components/src/components/Fleet/FleetDetails/FleetDetailsPage'), ); -const OverviewPage = React.lazy(() => import('@flightctl/ui-components/src/components/OverviewPage/OverviewPage')); -const SecurityOverviewPage = React.lazy( +const OverviewPage = lazyResetter(() => import('@flightctl/ui-components/src/components/OverviewPage/OverviewPage')); +const SecurityOverviewPage = lazyResetter( () => import('@flightctl/ui-components/src/components/SecurityOverview/SecurityOverviewPage'), ); -const PendingEnrollmentRequestsBadge = React.lazy( +const PendingEnrollmentRequestsBadge = lazyResetter( () => import('@flightctl/ui-components/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge'), ); -const CommandLineToolsPage = React.lazy( +const CommandLineToolsPage = lazyResetter( () => import('@flightctl/ui-components/src/components/Masthead/CommandLineToolsPage'), ); -const AuthProvidersPage = React.lazy( +const AuthProvidersPage = lazyResetter( () => import('@flightctl/ui-components/src/components/AuthProvider/AuthProvidersPage'), ); -const CreateAuthProvider = React.lazy( +const CreateAuthProvider = lazyResetter( () => import('@flightctl/ui-components/src/components/AuthProvider/CreateAuthProvider/CreateAuthProvider'), ); -const AuthProviderDetails = React.lazy( +const AuthProviderDetails = lazyResetter( () => import('@flightctl/ui-components/src/components/AuthProvider/AuthProviderDetails/AuthProviderDetails'), ); -const ImageBuildsPage = React.lazy(() => import('@flightctl/ui-components/src/components/ImageBuilds/ImageBuildsPage')); -const ImageBuildDetails = React.lazy( +const ImageBuildsPage = lazyResetter( + () => import('@flightctl/ui-components/src/components/ImageBuilds/ImageBuildsPage'), +); +const ImageBuildDetails = lazyResetter( () => import('@flightctl/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage'), ); -const CreateImageBuildWizard = React.lazy( +const CreateImageBuildWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard'), ); -const NewVersionImageBuildWizard = React.lazy( +const NewVersionImageBuildWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/ImageBuilds/NewVersionImageBuildWizard/NewVersionImageBuildWizard'), ); -const CatalogPage = React.lazy(() => import('@flightctl/ui-components/src/components/Catalog/CatalogPage')); -const ImportCatalogWizard = React.lazy( +const CatalogPage = lazyResetter(() => import('@flightctl/ui-components/src/components/Catalog/CatalogPage')); +const ImportCatalogWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Catalog/ImportCatalogWizard/ImportCatalogWizard'), ); -const AddCatalogItemWizard = React.lazy( +const AddCatalogItemWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard'), ); -const CatalogInstallWizard = React.lazy( +const CatalogInstallWizard = lazyResetter( () => import('@flightctl/ui-components/src/components/Catalog/InstallWizard/InstallWizard'), ); -const CatalogEditFleetWizard = React.lazy(() => +const CatalogEditFleetWizard = lazyResetter(() => import('@flightctl/ui-components/src/components/Catalog/EditWizard/EditWizard').then((module) => ({ default: module.EditFleetWizard, })), ); -const CatalogEditDeviceWizard = React.lazy(() => +const CatalogEditDeviceWizard = lazyResetter(() => import('@flightctl/ui-components/src/components/Catalog/EditWizard/EditWizard').then((module) => ({ default: module.EditDeviceWizard, })), @@ -143,6 +149,7 @@ const ErrorPage = () => { }; const TitledRoute = ({ title, children }: React.PropsWithChildren<{ title: string }>) => { + const location = useLocation(); useDocumentTitle(`${APP_TITLE} | ${title}`); return ( } > - {children} + {children} ); }; diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 09cf1a0bb..d38997e0d 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -547,6 +547,7 @@ "New label": "New label", "Add label": "Add label", "Unexpected error occurred": "Unexpected error occurred", + "Reload page": "Reload page", "Please reload the page and try again.": "Please reload the page and try again.", "Resize panel": "Resize panel", "Next": "Next", diff --git a/libs/ui-components/src/components/common/ErrorBoundary.tsx b/libs/ui-components/src/components/common/ErrorBoundary.tsx index 2b9d207a4..937015aa0 100644 --- a/libs/ui-components/src/components/common/ErrorBoundary.tsx +++ b/libs/ui-components/src/components/common/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { type TFunction, withTranslation } from 'react-i18next'; -import { Alert } from '@patternfly/react-core'; +import { Alert, AlertActionLink } from '@patternfly/react-core'; import { getErrorMessage } from '../../utils/error'; @@ -38,7 +38,12 @@ class ErrorBoundary extends React.Component { } return hasError ? ( - + window.location.reload()}>{t('Reload page')}} + > {t('Please reload the page and try again.')}
{getErrorMessage(error)}
diff --git a/libs/ui-components/src/utils/resettableLazy.tsx b/libs/ui-components/src/utils/resettableLazy.tsx new file mode 100644 index 000000000..4ed81ea6a --- /dev/null +++ b/libs/ui-components/src/utils/resettableLazy.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { useLocation } from 'react-router-dom'; + +/** + * Wrapper around React.lazy that creates a fresh lazy loader on each router navigation. + * Prevents the lazy loader from being cached by React.lazy + */ +export function lazyResetter(importer: () => Promise<{ default: React.ComponentType }>): React.FC { + function ResettableLazyPage() { + const { key: locationKey } = useLocation(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: recreate lazy when location.key changes + const Component = React.useMemo(() => React.lazy(importer), [locationKey]); + return ; + } + + return ResettableLazyPage; +}