Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/packages/console-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ContextProvider>[]
>([]);

const EnhancedProvider: FC<{
provider: ProviderComponent<any>;
useValueHook: () => any;
children: ReactNode;
}> = ({ provider: Component, useValueHook, children }) => {
const value = useValueHook();
return <Component value={value}>{children}</Component>;
};

const PF_BREAKPOINT_XL = 1200;

/** Empty PatternFly Page shell shown while DetectContext is initializing. */
export const PageSkeleton: FC<{ blame: string }> = ({ blame }) => (
<Page
isContentFilled
masthead={
<Masthead>
<MastheadMain />
<MastheadContent>
<div className="co-page-skeleton__masthead-spacer" />
</MastheadContent>
</Masthead>
}
sidebar={
<PageSidebar isSidebarOpen={window.innerWidth >= PF_BREAKPOINT_XL}>
<PageSidebarBody />
</PageSidebar>
}
>
<PageSection isFilled hasBodyWrapper={false}>
<LoadingBox blame={blame} />
</PageSection>
</Page>
);

/** Wraps children in plugin-provided context providers resolved by DetectContext. */
export const ContextProviderExtensionWrapper: FC<{ children: ReactNode }> = ({ children }) => {
const contextProviderExtensions = useContext(ContextProviderExtensionsContext);
return (
<Suspense fallback={<PageSkeleton blame="ContextProviderExtensions" />}>
{contextProviderExtensions.reduce(
(acc, e) => (
<EnhancedProvider key={e.uid} {...e.properties}>
{acc}
</EnhancedProvider>
),
children,
)}
Comment thread
logonoff marked this conversation as resolved.
</Suspense>
);
};

/**
* 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);
Comment thread
logonoff marked this conversation as resolved.

const [reduxReducerExtensions, reducersResolved] = useResolvedExtensions<ReduxReducer>(
isReduxReducer,
);
const [contextProviderExtensions, providersResolved] = useResolvedExtensions<ContextProvider>(
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 && (
<PerspectiveDetector setActivePerspective={setActivePerspective} />
)}
<PageSkeleton blame={pending.join(', ')} />
</>
);
}

return (
<PerspectiveContext.Provider value={{ activePerspective, setActivePerspective }}>
<NamespaceContext.Provider value={{ namespace, setNamespace }}>
<ContextProviderExtensionsContext.Provider value={contextProviderExtensions}>
{children}
</ContextProviderExtensionsContext.Provider>
</NamespaceContext.Provider>
</PerspectiveContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -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 = () => <h1>App</h1>;
Expand All @@ -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();
Expand All @@ -44,9 +73,9 @@ describe('DetectPerspective', () => {
]);

render(
<DetectPerspective>
<DetectContext>
<MockApp />
</DetectPerspective>,
</DetectContext>,
);

expect(screen.getByRole('heading', { name: 'App' })).toBeVisible();
Expand All @@ -61,9 +90,9 @@ describe('DetectPerspective', () => {
]);

render(
<DetectPerspective>
<DetectContext>
<MockApp />
</DetectPerspective>,
</DetectContext>,
);

expect(screen.getByText('PerspectiveDetector')).toBeVisible();
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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: <
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down
Loading