From 114d974e10fe3b514704be61b493e8d21bf2f4fd Mon Sep 17 00:00:00 2001 From: logonoff Date: Fri, 20 Mar 2026 19:14:09 -0400 Subject: [PATCH 1/2] OCPBUGS-79000: Fix perspective switcher icon suspending the whole app Replace bare React.lazy() usage with AsyncComponent, which provides its own Suspense boundary and retry logic. The MenuToggle icon was missing a Suspense boundary, causing the lazy-loaded perspective icon to suspend an ancestor boundary and block the entire app content from rendering when concurrent features are enabled via createRoot. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/nav/NavHeader.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/packages/console-app/src/components/nav/NavHeader.tsx b/frontend/packages/console-app/src/components/nav/NavHeader.tsx index 14641adb0d3..6df73064db4 100644 --- a/frontend/packages/console-app/src/components/nav/NavHeader.tsx +++ b/frontend/packages/console-app/src/components/nav/NavHeader.tsx @@ -1,11 +1,12 @@ import type { FC, MouseEvent, Ref } from 'react'; -import { useMemo, lazy, useState, useCallback, Suspense } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import type { MenuToggleElement } from '@patternfly/react-core'; import { MenuToggle, Select, SelectList, SelectOption, Title } from '@patternfly/react-core'; import { CogsIcon } from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; import { t } from 'i18next'; import type { Perspective } from '@console/dynamic-plugin-sdk'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; +import { AsyncComponent } from '@console/internal/components/utils/async'; import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; export type NavHeaderProps = { @@ -19,8 +20,9 @@ type PerspectiveDropdownItemProps = { onClick: (perspective: string) => void; }; +const IconLoadingComponent: FC = () => <> ; + const PerspectiveDropdownItem: FC = ({ perspective, onClick }) => { - const LazyIcon = useMemo(() => lazy(perspective.properties.icon), [perspective.properties.icon]); return ( = ({ perspective onClick(perspective.properties.id); }} icon={ -  }> - - + perspective.properties.icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> } > @@ -75,8 +78,6 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { [activePerspective, perspectiveExtensions], ); - const LazyIcon = useMemo(() => icon && lazy(icon), [icon]); - return perspectiveDropdownItems.length > 1 ? ( <div className="oc-nav-header" @@ -94,7 +95,14 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { isExpanded={isPerspectiveDropdownOpen} ref={toggleRef} onClick={() => togglePerspectiveOpen()} - icon={<LazyIcon />} + icon={ + icon && ( + <AsyncComponent + loader={() => icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> + ) + } > {name && ( <Title headingLevel="h2" size="md"> From 8177e0eaeaeba4a4ccfb1166cc8ca556fad3aaf3 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Fri, 20 Mar 2026 19:15:14 -0400 Subject: [PATCH 2/2] OCPBUGS-79000: Add suspense to AppContent this allows the blame to be shifted away from "contextProviderExtensions suspense", so we know if it is a contextProviderExtensions suspending or if it is the AppContent as a whole --- frontend/public/components/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 22e7caef504..69a4acadc37 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -253,7 +253,7 @@ const App: FC<{ }; const content = ( - <> + <Suspense fallback={<LoadingBox blame="App content suspense" />}> <ConsoleNotifier location="BannerTop" /> <QuickStartDrawer> <CloudShellDrawer> @@ -308,7 +308,7 @@ const App: FC<{ </QuickStartDrawer> <ConsoleNotifier location="BannerBottom" /> <FeatureFlagExtensionLoader /> - </> + </Suspense> ); return (