From f7aa632be21a6ced9dc45cbe1d311241326e82d0 Mon Sep 17 00:00:00 2001 From: logonoff Date: Wed, 20 May 2026 17:03:46 -0400 Subject: [PATCH 1/2] CONSOLE-5300: Parallelize context detection to reduce initial load time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate DetectPerspective, DetectNamespace, DetectLanguage, and AppWithExtensions into a single DetectContext component. Previously these ran sequentially — namespace detection could not start until perspective detection completed, and neither could start until extension resolution finished. Now all hooks fire at the same React component level, reducing load time from the sum of all operations to the duration of the longest one. - Merge detect-perspective/, detect-namespace/, and detect-language/ into a single detect-context/ directory - Replace bare LoadingBox spinners with a PageSkeleton that renders an empty PatternFly Page layout during initialization - Dynamically track pending subsystems in the skeleton blame label - Preserve plugin context provider nesting order to avoid breaking dynamic plugins Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/packages/console-app/package.json | 2 +- .../detect-context/DetectContext.tsx | 179 ++++++++++++++++++ .../PerspectiveConfiguration.tsx | 0 .../PerspectiveDetector.tsx | 0 .../__tests__/DetectContext.spec.tsx} | 41 +++- .../__tests__/PerspectiveDetector.spec.tsx | 0 .../__tests__/checkNamespaceExists.spec.ts | 0 .../__tests__/getValueForNamespace.spec.ts | 0 .../__tests__/namespace.spec.ts | 0 .../useValuesForPerspectiveContext.spec.ts | 0 .../checkNamespaceExists.ts | 0 .../getValueForNamespace.ts | 0 .../namespace.ts | 0 .../useLastNamespace.ts | 0 .../useLastPerspective.ts | 0 .../useValuesForPerspectiveContext.ts | 0 .../detect-language/DetectLanguage.tsx | 11 -- .../detect-namespace/DetectNamespace.tsx | 20 -- .../detect-perspective/DetectPerspective.tsx | 54 ------ .../src/api/internal-api.ts | 2 +- .../src/hooks/useActiveNamespace.ts | 2 +- frontend/public/components/app.tsx | 85 ++------- frontend/public/style/_layout.scss | 7 + 23 files changed, 243 insertions(+), 160 deletions(-) create mode 100644 frontend/packages/console-app/src/components/detect-context/DetectContext.tsx rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/PerspectiveConfiguration.tsx (100%) rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/PerspectiveDetector.tsx (100%) rename frontend/packages/console-app/src/components/{detect-perspective/__tests__/DetectPerspective.spec.tsx => detect-context/__tests__/DetectContext.spec.tsx} (65%) rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/__tests__/PerspectiveDetector.spec.tsx (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/__tests__/checkNamespaceExists.spec.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/__tests__/getValueForNamespace.spec.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/__tests__/namespace.spec.ts (100%) rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/__tests__/useValuesForPerspectiveContext.spec.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/checkNamespaceExists.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/getValueForNamespace.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/namespace.ts (100%) rename frontend/packages/console-app/src/components/{detect-namespace => detect-context}/useLastNamespace.ts (100%) rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/useLastPerspective.ts (100%) rename frontend/packages/console-app/src/components/{detect-perspective => detect-context}/useValuesForPerspectiveContext.ts (100%) delete mode 100644 frontend/packages/console-app/src/components/detect-language/DetectLanguage.tsx delete mode 100644 frontend/packages/console-app/src/components/detect-namespace/DetectNamespace.tsx delete mode 100644 frontend/packages/console-app/src/components/detect-perspective/DetectPerspective.tsx diff --git a/frontend/packages/console-app/package.json b/frontend/packages/console-app/package.json index 802e5162b81..337f6902119 100644 --- a/frontend/packages/console-app/package.json +++ b/frontend/packages/console-app/package.json @@ -59,7 +59,7 @@ "NamespaceDropdown": "src/components/user-preferences/namespace/NamespaceDropdown.tsx", "LanguageDropdown": "src/components/user-preferences/language/LanguageDropdown.tsx", "perspective": "src/utils/perspective.tsx", - "perspectiveConfiguration": "src/components/detect-perspective/PerspectiveConfiguration.tsx", + "perspectiveConfiguration": "src/components/detect-context/PerspectiveConfiguration.tsx", "DynamicPluginsPopover": "src/components/dashboards-page/dynamic-plugins-health-resource/DynamicPluginsPopover.tsx", "getDynamicPluginHealthState": "src/components/dashboards-page/dynamic-plugins-health-resource/status.ts", "k8sHealth": "src/components/dashboards-page/status.ts", diff --git a/frontend/packages/console-app/src/components/detect-context/DetectContext.tsx b/frontend/packages/console-app/src/components/detect-context/DetectContext.tsx new file mode 100644 index 00000000000..99fbf0705be --- /dev/null +++ b/frontend/packages/console-app/src/components/detect-context/DetectContext.tsx @@ -0,0 +1,179 @@ +import type { FC, Provider as ProviderComponent, ReactNode } from 'react'; +import { createContext, Suspense, useContext, useEffect } from 'react'; +import type { LoadedAndResolvedExtension } from '@openshift/dynamic-plugin-sdk'; +import { + Masthead, + MastheadContent, + MastheadMain, + Page, + PageSection, + PageSidebar, + PageSidebarBody, +} from '@patternfly/react-core'; +import { createPath, useLocation } from 'react-router'; +import type { Perspective, ReduxReducer, ContextProvider } from '@console/dynamic-plugin-sdk'; +import { + PerspectiveContext, + useResolvedExtensions, + isContextProvider, + isReduxReducer, +} from '@console/dynamic-plugin-sdk'; +import { applyReduxExtensions } from '@console/internal/redux'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; +import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; +import { useLanguage } from '../user-preferences/language/useLanguage'; +import { usePreferredLanguage } from '../user-preferences/language/usePreferredLanguage'; +import { NamespaceContext, useValuesForNamespaceContext } from './namespace'; +import PerspectiveDetector from './PerspectiveDetector'; +import { useValuesForPerspectiveContext } from './useValuesForPerspectiveContext'; + +const getPerspectiveURLParam = (perspectives: Perspective[]) => { + const perspectiveIDs = perspectives.map( + (nextPerspective: Perspective) => nextPerspective.properties.id, + ); + + const urlParams = new URLSearchParams(window.location.search); + const perspectiveParam = urlParams.get('perspective'); + return perspectiveParam && perspectiveIDs.includes(perspectiveParam) ? perspectiveParam : ''; +}; + +const ContextProviderExtensionsContext = createContext< + LoadedAndResolvedExtension[] +>([]); + +const EnhancedProvider: FC<{ + provider: ProviderComponent; + useValueHook: () => any; + children: ReactNode; +}> = ({ provider: Component, useValueHook, children }) => { + const value = useValueHook(); + return {children}; +}; + +const PF_BREAKPOINT_XL = 1200; + +/** Empty PatternFly Page shell shown while DetectContext is initializing. */ +export const PageSkeleton: FC<{ blame: string }> = ({ blame }) => ( + + + +
+ + + } + sidebar={ + = PF_BREAKPOINT_XL}> + + + } + > + + + + +); + +/** Wraps children in plugin-provided context providers resolved by DetectContext. */ +export const ContextProviderExtensionWrapper: FC<{ children: ReactNode }> = ({ children }) => { + const contextProviderExtensions = useContext(ContextProviderExtensionsContext); + return ( + }> + {contextProviderExtensions.reduce( + (acc, e) => ( + + {acc} + + ), + children, + )} + + ); +}; + +/** + * Bootstraps the console by running all detection and resolution hooks at the + * same component level so their async work executes in parallel: + * + * - Detect the active perspective (user prefs, URL param, or auto-detection) + * - Detect the active namespace (user prefs, URL, or K8s API fallback) + * - Detect the preferred language (user prefs, then apply via i18n) + * - Resolve all ReduxReducer and ContextProvider plugin extensions + * + * Once ready, provides the resolved values via PerspectiveContext, + * NamespaceContext, and ContextProviderExtensionsContext. + */ +export const DetectContext: FC<{ children: ReactNode }> = ({ children }) => { + const [ + activePerspective, + setActivePerspective, + perspectiveLoaded, + ] = useValuesForPerspectiveContext(); + const { namespace, setNamespace, loaded: namespaceLoaded } = useValuesForNamespaceContext(); + + const [preferredLanguage, , preferredLanguageLoaded] = usePreferredLanguage(); + useLanguage(preferredLanguage, preferredLanguageLoaded); + + const [reduxReducerExtensions, reducersResolved] = useResolvedExtensions( + isReduxReducer, + ); + const [contextProviderExtensions, providersResolved] = useResolvedExtensions( + isContextProvider, + ); + + const perspectiveExtensions = usePerspectives(); + const perspectiveParam = getPerspectiveURLParam(perspectiveExtensions); + const location = useLocation(); + + useEffect(() => { + if (perspectiveParam && perspectiveParam !== activePerspective) { + setActivePerspective(perspectiveParam, createPath(location)); + } + }, [perspectiveParam, activePerspective, setActivePerspective, location]); + + useEffect(() => { + if (reducersResolved) { + applyReduxExtensions(reduxReducerExtensions); + } + }, [reducersResolved, reduxReducerExtensions]); + + const needsPerspectiveDetection = perspectiveLoaded && !activePerspective; + const ready = + perspectiveLoaded && + !!activePerspective && + namespaceLoaded && + reducersResolved && + providersResolved && + preferredLanguageLoaded; + + if (!ready) { + const pending: string[] = []; + if (!perspectiveLoaded) pending.push('Perspective'); + if (needsPerspectiveDetection) pending.push('PerspectiveDetection'); + if (!namespaceLoaded) pending.push('Namespace'); + if (!reducersResolved) pending.push('Reducers'); + if (!providersResolved) pending.push('Providers'); + if (!preferredLanguageLoaded) pending.push('Language'); + + return ( + <> + {needsPerspectiveDetection && ( + + )} + + + ); + } + + return ( + + + + {children} + + + + ); +}; diff --git a/frontend/packages/console-app/src/components/detect-perspective/PerspectiveConfiguration.tsx b/frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/PerspectiveConfiguration.tsx rename to frontend/packages/console-app/src/components/detect-context/PerspectiveConfiguration.tsx diff --git a/frontend/packages/console-app/src/components/detect-perspective/PerspectiveDetector.tsx b/frontend/packages/console-app/src/components/detect-context/PerspectiveDetector.tsx similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/PerspectiveDetector.tsx rename to frontend/packages/console-app/src/components/detect-context/PerspectiveDetector.tsx diff --git a/frontend/packages/console-app/src/components/detect-perspective/__tests__/DetectPerspective.spec.tsx b/frontend/packages/console-app/src/components/detect-context/__tests__/DetectContext.spec.tsx similarity index 65% rename from frontend/packages/console-app/src/components/detect-perspective/__tests__/DetectPerspective.spec.tsx rename to frontend/packages/console-app/src/components/detect-context/__tests__/DetectContext.spec.tsx index 3fff46721b4..96c867bd785 100644 --- a/frontend/packages/console-app/src/components/detect-perspective/__tests__/DetectPerspective.spec.tsx +++ b/frontend/packages/console-app/src/components/detect-context/__tests__/DetectContext.spec.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { useLocation } from 'react-router'; import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; -import DetectPerspective from '../DetectPerspective'; +import { DetectContext } from '../DetectContext'; import { useValuesForPerspectiveContext } from '../useValuesForPerspectiveContext'; const MockApp = () =>

App

; @@ -15,19 +15,48 @@ jest.mock('../useValuesForPerspectiveContext', () => ({ useValuesForPerspectiveContext: jest.fn(), })); +jest.mock('../namespace', () => ({ + NamespaceContext: { Provider: ({ children }) => children, Consumer: () => null }, + useValuesForNamespaceContext: jest.fn().mockReturnValue({ + namespace: 'default', + setNamespace: jest.fn(), + loaded: true, + }), +})); + +jest.mock('../../user-preferences/language/usePreferredLanguage', () => ({ + usePreferredLanguage: jest.fn().mockReturnValue(['en', jest.fn(), true]), +})); + +jest.mock('../../user-preferences/language/useLanguage', () => ({ + useLanguage: jest.fn(), +})); + jest.mock('@console/shared/src/hooks/usePerspectives', () => ({ usePerspectives: jest.fn(), })); +jest.mock('@console/dynamic-plugin-sdk', () => ({ + PerspectiveContext: { Provider: ({ children }) => children, Consumer: () => null }, + useResolvedExtensions: jest.fn().mockReturnValue([[], true]), + isContextProvider: jest.fn(), + isReduxReducer: jest.fn(), +})); + +jest.mock('@console/internal/redux', () => ({ + applyReduxExtensions: jest.fn(), +})); + jest.mock('react-router', () => ({ useLocation: jest.fn(), + createPath: jest.fn((loc) => loc.pathname), })); const useValuesForPerspectiveContextMock = useValuesForPerspectiveContext as jest.Mock; const usePerspectivesMock = usePerspectives as jest.Mock; const useLocationMock = useLocation as jest.Mock; -describe('DetectPerspective', () => { +describe('DetectContext', () => { beforeEach(() => { useValuesForPerspectiveContextMock.mockClear(); usePerspectivesMock.mockClear(); @@ -44,9 +73,9 @@ describe('DetectPerspective', () => { ]); render( - + - , + , ); expect(screen.getByRole('heading', { name: 'App' })).toBeVisible(); @@ -61,9 +90,9 @@ describe('DetectPerspective', () => { ]); render( - + - , + , ); expect(screen.getByText('PerspectiveDetector')).toBeVisible(); diff --git a/frontend/packages/console-app/src/components/detect-perspective/__tests__/PerspectiveDetector.spec.tsx b/frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/__tests__/PerspectiveDetector.spec.tsx rename to frontend/packages/console-app/src/components/detect-context/__tests__/PerspectiveDetector.spec.tsx diff --git a/frontend/packages/console-app/src/components/detect-namespace/__tests__/checkNamespaceExists.spec.ts b/frontend/packages/console-app/src/components/detect-context/__tests__/checkNamespaceExists.spec.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/__tests__/checkNamespaceExists.spec.ts rename to frontend/packages/console-app/src/components/detect-context/__tests__/checkNamespaceExists.spec.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/__tests__/getValueForNamespace.spec.ts b/frontend/packages/console-app/src/components/detect-context/__tests__/getValueForNamespace.spec.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/__tests__/getValueForNamespace.spec.ts rename to frontend/packages/console-app/src/components/detect-context/__tests__/getValueForNamespace.spec.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/__tests__/namespace.spec.ts b/frontend/packages/console-app/src/components/detect-context/__tests__/namespace.spec.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/__tests__/namespace.spec.ts rename to frontend/packages/console-app/src/components/detect-context/__tests__/namespace.spec.ts diff --git a/frontend/packages/console-app/src/components/detect-perspective/__tests__/useValuesForPerspectiveContext.spec.ts b/frontend/packages/console-app/src/components/detect-context/__tests__/useValuesForPerspectiveContext.spec.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/__tests__/useValuesForPerspectiveContext.spec.ts rename to frontend/packages/console-app/src/components/detect-context/__tests__/useValuesForPerspectiveContext.spec.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/checkNamespaceExists.ts b/frontend/packages/console-app/src/components/detect-context/checkNamespaceExists.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/checkNamespaceExists.ts rename to frontend/packages/console-app/src/components/detect-context/checkNamespaceExists.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/getValueForNamespace.ts b/frontend/packages/console-app/src/components/detect-context/getValueForNamespace.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/getValueForNamespace.ts rename to frontend/packages/console-app/src/components/detect-context/getValueForNamespace.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/namespace.ts b/frontend/packages/console-app/src/components/detect-context/namespace.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/namespace.ts rename to frontend/packages/console-app/src/components/detect-context/namespace.ts diff --git a/frontend/packages/console-app/src/components/detect-namespace/useLastNamespace.ts b/frontend/packages/console-app/src/components/detect-context/useLastNamespace.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-namespace/useLastNamespace.ts rename to frontend/packages/console-app/src/components/detect-context/useLastNamespace.ts diff --git a/frontend/packages/console-app/src/components/detect-perspective/useLastPerspective.ts b/frontend/packages/console-app/src/components/detect-context/useLastPerspective.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/useLastPerspective.ts rename to frontend/packages/console-app/src/components/detect-context/useLastPerspective.ts diff --git a/frontend/packages/console-app/src/components/detect-perspective/useValuesForPerspectiveContext.ts b/frontend/packages/console-app/src/components/detect-context/useValuesForPerspectiveContext.ts similarity index 100% rename from frontend/packages/console-app/src/components/detect-perspective/useValuesForPerspectiveContext.ts rename to frontend/packages/console-app/src/components/detect-context/useValuesForPerspectiveContext.ts diff --git a/frontend/packages/console-app/src/components/detect-language/DetectLanguage.tsx b/frontend/packages/console-app/src/components/detect-language/DetectLanguage.tsx deleted file mode 100644 index 9cdd8ea2123..00000000000 --- a/frontend/packages/console-app/src/components/detect-language/DetectLanguage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { memo } from 'react'; -import { useLanguage } from '../user-preferences/language/useLanguage'; -import { usePreferredLanguage } from '../user-preferences/language/usePreferredLanguage'; - -const DetectLanguage = memo(() => { - const [preferredLanguage, , preferredLanguageLoaded] = usePreferredLanguage(); - useLanguage(preferredLanguage, preferredLanguageLoaded); - return null; -}); - -export default DetectLanguage; diff --git a/frontend/packages/console-app/src/components/detect-namespace/DetectNamespace.tsx b/frontend/packages/console-app/src/components/detect-namespace/DetectNamespace.tsx deleted file mode 100644 index 59a6d4e0c3c..00000000000 --- a/frontend/packages/console-app/src/components/detect-namespace/DetectNamespace.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { FC, ReactNode } from 'react'; -import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; -import { NamespaceContext, useValuesForNamespaceContext } from './namespace'; - -type DetectNamespaceProps = { - children: ReactNode; -}; - -const DetectNamespace: FC = ({ children }) => { - const { namespace, setNamespace, loaded } = useValuesForNamespaceContext(); - return loaded ? ( - - {children} - - ) : ( - - ); -}; - -export default DetectNamespace; diff --git a/frontend/packages/console-app/src/components/detect-perspective/DetectPerspective.tsx b/frontend/packages/console-app/src/components/detect-perspective/DetectPerspective.tsx deleted file mode 100644 index 94c1738d443..00000000000 --- a/frontend/packages/console-app/src/components/detect-perspective/DetectPerspective.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { FC } from 'react'; -import { useEffect } from 'react'; -import { createPath, useLocation } from 'react-router'; -import type { Perspective } from '@console/dynamic-plugin-sdk'; -import { PerspectiveContext } from '@console/dynamic-plugin-sdk'; -import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; -import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; -import PerspectiveDetector from './PerspectiveDetector'; -import { useValuesForPerspectiveContext } from './useValuesForPerspectiveContext'; - -type DetectPerspectiveProps = { - children: React.ReactNode; -}; - -const getPerspectiveURLParam = (perspectives: Perspective[]) => { - const perspectiveIDs = perspectives.map( - (nextPerspective: Perspective) => nextPerspective.properties.id, - ); - - const urlParams = new URLSearchParams(window.location.search); - const perspectiveParam = urlParams.get('perspective'); - return perspectiveParam && perspectiveIDs.includes(perspectiveParam) ? perspectiveParam : ''; -}; - -const DetectPerspective: FC = ({ children }) => { - const [activePerspective, setActivePerspective, loaded] = useValuesForPerspectiveContext(); - const perspectiveExtensions = usePerspectives(); - const perspectiveParam = getPerspectiveURLParam(perspectiveExtensions); - const location = useLocation(); - useEffect(() => { - if (perspectiveParam && perspectiveParam !== activePerspective) { - setActivePerspective(perspectiveParam, createPath(location)); - } - }, [perspectiveParam, activePerspective, setActivePerspective, location]); - - return loaded ? ( - activePerspective ? ( - - {children} - - ) : ( - - ) - ) : ( - - ); -}; - -export default DetectPerspective; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-api.ts b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-api.ts index c75b25b2d34..5a44d8e2c0a 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-api.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/api/internal-api.ts @@ -85,7 +85,7 @@ export const useDashboardResources: UseDashboardResources = require('@console/sh export const useURLPoll: UseURLPoll = require('@console/internal/components/utils/url-poll-hook') .useURLPoll; -export const useLastNamespace: UseLastNamespace = require('@console/app/src/components/detect-namespace/useLastNamespace') +export const useLastNamespace: UseLastNamespace = require('@console/app/src/components/detect-context/useLastNamespace') .useLastNamespace; export const ConsoleDataView: < diff --git a/frontend/packages/console-shared/src/hooks/useActiveNamespace.ts b/frontend/packages/console-shared/src/hooks/useActiveNamespace.ts index fb0c9787267..f3b423112f1 100644 --- a/frontend/packages/console-shared/src/hooks/useActiveNamespace.ts +++ b/frontend/packages/console-shared/src/hooks/useActiveNamespace.ts @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { NamespaceContext } from '@console/app/src/components/detect-namespace/namespace'; +import { NamespaceContext } from '@console/app/src/components/detect-context/namespace'; import type { UseActiveNamespace } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; export const useActiveNamespace: UseActiveNamespace = () => { diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index d72515f51c3..cce3c01e476 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -10,7 +10,7 @@ import { Suspense, useMemo, } from 'react'; -import type { FC, Provider as ProviderComponent, ReactNode } from 'react'; +import type { FC } from 'react'; import { createRoot } from 'react-dom/client'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { linkify } from 'react-linkify'; @@ -19,9 +19,8 @@ import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; import { mapExtensionToRoutes } from '@console/app/src/hooks/usePluginRoutes'; import { BrowserRouter, useParams, useLocation, Routes, Route } from 'react-router'; -import store, { applyReduxExtensions } from '../redux'; +import store from '../redux'; import { useTranslation } from 'react-i18next'; -import type { LoadedAndResolvedExtension } from '@openshift/dynamic-plugin-sdk'; import { PluginStoreProvider } from '@openshift/dynamic-plugin-sdk'; import { detectFeatures } from '../actions/features'; import { setFlag } from '../actions/flags'; @@ -39,21 +38,14 @@ import { receivedResources, startAPIDiscovery } from '../actions/k8s'; import { pluginStore } from '../plugins'; // cloud shell imports must come later than features import CloudShellDrawer from '@console/webterminal-plugin/src/components/cloud-shell/CloudShell'; -import DetectPerspective from '@console/app/src/components/detect-perspective/DetectPerspective'; -import DetectNamespace from '@console/app/src/components/detect-namespace/DetectNamespace'; -import DetectLanguage from '@console/app/src/components/detect-language/DetectLanguage'; +import { + ContextProviderExtensionWrapper, + DetectContext, + PageSkeleton, +} from '@console/app/src/components/detect-context/DetectContext'; import { FeatureFlagExtensionLoader } from '@console/app/src/components/flags/FeatureFlagExtensionLoader'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; -import { - useResolvedExtensions, - isContextProvider, - isReduxReducer, - isStandaloneRoutePage, - getUser, - useActivePerspective, - ReduxReducer, - ContextProvider, -} from '@console/dynamic-plugin-sdk'; +import { isStandaloneRoutePage, getUser, useActivePerspective } from '@console/dynamic-plugin-sdk'; import { GuidedTour } from '@console/app/src/components/tour'; import { QuickStartDrawer } from '@console/app/src/components/quick-starts/QuickStartDrawer'; import { ModalProvider } from '@console/dynamic-plugin-sdk/src/app/modal-support/ModalProvider'; @@ -93,18 +85,7 @@ initI18n(); // Only linkify url strings beginning with a proper protocol scheme. linkify.set({ fuzzyLink: false }); -const EnhancedProvider: FC<{ - provider: ProviderComponent; - useValueHook: () => any; - children: ReactNode; -}> = ({ provider: Component, useValueHook, children }) => { - const value = useValueHook(); - return {children}; -}; - -const App: FC<{ - contextProviderExtensions: LoadedAndResolvedExtension[]; -}> = ({ contextProviderExtensions }) => { +const App: FC = () => { const { t } = useTranslation(); const location = useLocation(); const params = useParams(); @@ -253,7 +234,7 @@ const App: FC<{ }; const content = ( - }> + }> @@ -312,46 +293,18 @@ const App: FC<{ ); return ( - + - - - - - }> - {contextProviderExtensions.reduce( - (children, e) => ( - - {children} - - ), - content, - )} - - - - - - + + + + {content} + + + ); }; -const AppWithExtensions: FC = () => { - const [reduxReducerExtensions, reducersResolved] = useResolvedExtensions( - isReduxReducer, - ); - const [contextProviderExtensions, providersResolved] = useResolvedExtensions( - isContextProvider, - ); - - if (reducersResolved && providersResolved) { - applyReduxExtensions(reduxReducerExtensions); - return ; - } - - return ; -}; - const root = createRoot(document.getElementById('app')!); root.render(); @@ -382,7 +335,7 @@ const AppRouter: FC = () => { */} } /> {standaloneRoutes} - } /> + } /> ); diff --git a/frontend/public/style/_layout.scss b/frontend/public/style/_layout.scss index 52c03bf19e1..6af39f4533e 100644 --- a/frontend/public/style/_layout.scss +++ b/frontend/public/style/_layout.scss @@ -1,3 +1,5 @@ +@use './vars'; + html, body, #app, @@ -12,6 +14,11 @@ body, overflow: auto; } +.co-page-skeleton__masthead-spacer { + height: vars.$co-button-height; + visibility: hidden; +} + .co-p-has-sidebar { display: flex; flex: 1; From 66779e7702b09917e0be0b5f1eb99920e2f64054 Mon Sep 17 00:00:00 2001 From: logonoff Date: Thu, 21 May 2026 13:53:16 -0400 Subject: [PATCH 2/2] CONSOLE-5300: Redirect unauthenticated users before app shell renders Fire a lightweight coFetch to /api/kubernetes/api at module load time, before graphQLReady or DetectContext mount. If the user is not authenticated, coFetch's 401 interceptor triggers authSvc.handle401() and redirects to the login page immediately, preventing a flash of the PageSkeleton for unauthenticated users. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/public/components/app.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index cce3c01e476..7e6dcb7e1a9 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -57,6 +57,7 @@ import { useDebounceCallback } from '@console/shared/src/hooks/useDebounceCallba import { LOGIN_ERROR_PATH } from '@console/internal/module/auth'; import { FLAGS } from '@console/shared/src/constants/common'; import { useFlag } from '@console/shared/src/hooks/useFlag'; +import { coFetch } from '@console/shared/src/utils/console-fetch'; import { addTestError } from '@console/shared/src/utils/test-errors'; import Lightspeed from '@console/app/src/components/lightspeed/Lightspeed'; import { ThemeProvider } from './ThemeProvider'; @@ -79,6 +80,14 @@ import { useCSPViolationDetector } from '@console/app/src/hooks/useCSPViolationD import { useNotificationPoller } from '@console/app/src/hooks/useNotificationPoller'; import { useImpersonateRefreshFeatures } from './useImpersonateRefreshFeatures'; +const root = createRoot(document.getElementById('app')!); +root.render(); + +// Trigger an early authenticated fetch so unauthenticated users are redirected +// to the login page immediately, before the app shell renders. coFetch's 401 +// interceptor calls authSvc.handle401() which handles the redirect. +coFetch(`${window.SERVER_FLAGS.basePath}api/kubernetes/api`).catch(() => {}); + initI18n(); // Disable linkify 'fuzzy links' across the app. @@ -305,9 +314,6 @@ const App: FC = () => { ); }; -const root = createRoot(document.getElementById('app')!); -root.render(); - const AppRouter: FC = () => { const standaloneRouteExtensions = useExtensions(isStandaloneRoutePage);