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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
"react-i18next": "^11.12.0",
"react-linkify": "^0.2.2",
"react-modal": "^3.16.3",
"react-redux": "7.2.9",
"react-redux": "8.1.3",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that react-redux is Console provided shared module, using the default config

{ singleton = true, allowFallback = false }

i.e. plugins are built with the assumption that Console provides v7 impl. without plugin provided fallback.

So this is effectively a breaking change in Console provided shared modules.

"react-router": "5.3.x",
"react-router-dom": "5.3.x",
"react-router-dom-v5-compat": "^6.11.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCallback, useMemo } from 'react';
import { ButtonVariant } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Action, K8sVerb } from '@console/dynamic-plugin-sdk';
import { k8sPatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s';
Expand All @@ -14,6 +13,7 @@ import {
ClusterRoleBindingKind,
referenceFor,
} from '@console/internal/module/k8s';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useK8sModel } from '@console/shared/src/hooks/useK8sModel';
import { useWarningModal } from '@console/shared/src/hooks/useWarningModal';
import { BindingActionCreator, CommonActionCreator } from './types';
Expand All @@ -37,7 +37,7 @@ export const useBindingActions = (
): Action[] => {
const { t } = useTranslation();
const [model] = useK8sModel(referenceFor(obj));
const dispatch = useDispatch();
const dispatch = useConsoleDispatch();
const startImpersonate = useCallback(
(kind, name) => dispatch(UIActions.startImpersonate(kind, name)),
[dispatch],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Action, getImpersonate } from '@console/dynamic-plugin-sdk';
import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay';
import * as UIActions from '@console/internal/actions/ui';
import { asAccessReview } from '@console/internal/components/utils/rbac';
import { GroupModel } from '@console/internal/models';
import { GroupKind } from '@console/internal/module/k8s';
import { RootState } from '@console/internal/redux';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';
import AddGroupUsersModal from '../../components/modals/add-group-users-modal';

/**
* Actions specific to Group resources.
*/
export const useGroupActions = (obj: GroupKind): Action[] => {
const { t } = useTranslation();
const dispatch = useDispatch();
const dispatch = useConsoleDispatch();
const navigate = useNavigate();
const launchOverlay = useOverlay();
const impersonate = useSelector((state: RootState) => getImpersonate(state));
const impersonate = useConsoleSelector((state) => getImpersonate(state));

const startImpersonate = useCallback(
(kind: string, name: string) => dispatch(UIActions.startImpersonate(kind, name)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Action } from '@console/dynamic-plugin-sdk/src';
import * as UIActions from '@console/internal/actions/ui';
import { asAccessReview } from '@console/internal/components/utils';
import { UserModel } from '@console/internal/models';
import { referenceFor, UserKind } from '@console/internal/module/k8s';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useK8sModel } from '@console/shared/src/hooks/useK8sModel';
import { useCommonResourceActions } from '../hooks/useCommonResourceActions';

const useImpersonateAction = (resource: UserKind): Action[] => {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const dispatch = useConsoleDispatch();

const factory = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ type FeatureFlagExtensionHookResolverProps = {
setFeatureFlag: SetFeatureFlag;
};

const FeatureFlagExtensionHookResolver: FC<FeatureFlagExtensionHookResolverProps> = ({
export const FeatureFlagExtensionHookResolver: FC<FeatureFlagExtensionHookResolverProps> = ({
handler,
setFeatureFlag,
}) => {
handler(setFeatureFlag);
return null;
};

export default FeatureFlagExtensionHookResolver;
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
import type { FC } from 'react';
import { useCallback, useRef, useEffect, useLayoutEffect } from 'react';
import {
isFeatureFlagHookProvider,
FeatureFlagHookProvider,
useResolvedExtensions,
SetFeatureFlag,
} from '@console/dynamic-plugin-sdk';
import { featureFlagController } from '@console/internal/actions/features';
import FeatureFlagExtensionHookResolver from './FeatureFlagExtensionHookResolver';
import { setFlag } from '@console/internal/actions/flags';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';
import { FeatureFlagExtensionHookResolver } from './FeatureFlagExtensionHookResolver';

const FeatureFlagExtensionLoader: FC = () => {
const useFeatureFlagController = () => {
const dispatch = useConsoleDispatch();
const flags = useConsoleSelector(({ FLAGS }) => FLAGS);

// Keep a ref to the flags map to avoid time-of-check to time-of-use issues
// and to keep the callback stable across flag updates
const flagsRef = useRef(flags);

// Queue of flag updates to be dispatched after render
const pendingUpdatesRef = useRef<Map<string, boolean>>(new Map());

useEffect(() => {
flagsRef.current = flags;
}, [flags]);

// Process pending flag updates after render completes.
// This avoids "Cannot update a component while rendering" errors with react-redux 8.x
// because handlers are called during render (they use hooks) but dispatches happen after.
useLayoutEffect(() => {
pendingUpdatesRef.current.forEach((enabled, flag) => {
if (flagsRef.current.get(flag) !== enabled) {
dispatch(setFlag(flag, enabled));
}
});
pendingUpdatesRef.current.clear();
});

return useCallback<SetFeatureFlag>((flag, enabled) => {
// Queue the update to be processed after render
pendingUpdatesRef.current.set(flag, enabled);
}, []);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const FeatureFlagExtensionLoader: FC = () => {
const [flagProvider, flagProviderResolved] = useResolvedExtensions<FeatureFlagHookProvider>(
isFeatureFlagHookProvider,
);
const featureFlagController = useFeatureFlagController();

if (flagProviderResolved) {
return (
<>
Expand All @@ -32,4 +71,3 @@ const FeatureFlagExtensionLoader: FC = () => {
}
return null;
};
export default FeatureFlagExtensionLoader;
5 changes: 5 additions & 0 deletions frontend/packages/console-dynamic-plugin-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ This section documents notable changes in Console provided shared modules and ot
- Removed `co-external-link` styling. Use PatternFly Buttons with `variant="link"` instead.
- Removed `co-disabled` styling.

#### Console 4.22.X

- Upgraded from `react-redux` v7 to v8. Plugins must use `react-redux` v8 to be compatible
with Console.

### PatternFly 5+ dynamic modules

Newer versions of `@openshift-console/dynamic-plugin-sdk-webpack` package include support for automatic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Map as ImmutableMap } from 'immutable';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { UserKind } from '@console/internal/module/k8s/types';
import { UserInfo } from '../extensions/console-types';

Expand Down Expand Up @@ -27,3 +29,5 @@ export type SDKStoreState = {
sdkCore: CoreState;
k8s: K8sState;
};

export type SDKDispatch = ThunkDispatch<SDKStoreState, undefined, AnyAction>;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useMemo, useEffect } from 'react';
import { Map as ImmutableMap } from 'immutable';
import { useSelector, useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we update to useConsoleDispatch and useConsoleSelector hook here?

Copy link
Copy Markdown
Member Author

@logonoff logonoff Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't work in the dynamic plugin SDK package because for some reason that package holds its own redux selectors.

IIRC when we build SDK packages, type only imports to internal console code (like for console redux store) become broken because it doesn't get automatically copied over. Typically SDK consumers only access console code via dynamic require imports

I considered adding a useSDKSelector hook but decided against it to reduce our api surface

import * as k8sActions from '../../../app/k8s/actions/k8s';
import { getReduxIdPayload } from '../../../app/k8s/reducers/k8sSelector';
import { SDKStoreState } from '../../../app/redux-types';
import type { SDKDispatch, SDKStoreState } from '../../../app/redux-types';
import { UseK8sWatchResource } from '../../../extensions/console-types';
import { getIDAndDispatch, getReduxData, NoModelError } from './k8s-watcher';
import { useDeepCompareMemoize } from './useDeepCompareMemoize';
Expand Down Expand Up @@ -33,7 +33,7 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => {

const reduxID = useMemo(() => getIDAndDispatch(resource, k8sModel), [k8sModel, resource]);

const dispatch = useDispatch();
const dispatch = useDispatch<SDKDispatch>();

useEffect(() => {
if (reduxID) {
Expand All @@ -46,9 +46,9 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => {
};
}, [dispatch, reduxID]);

const resourceK8s = useSelector<SDKStoreState, ImmutableMap<string, any>>((state) =>
const resourceK8s = useSelector<SDKStoreState>((state) =>
reduxID ? getReduxIdPayload(state, reduxID.id) : null,
);
) as ImmutableMap<string, any>;

return useMemo(() => {
if (!resource) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRef, useMemo, useEffect } from 'react';
import { Map as ImmutableMap, Iterable as ImmutableIterable } from 'immutable';
import { useSelector, useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent with migration pattern: still using react-redux's useSelector.

Despite the past review comment being marked as addressed, lines 3 and 141 still import and use useSelector from react-redux directly instead of useConsoleSelector. This is inconsistent with the migration pattern used throughout this PR (e.g., useBindingActions.ts, masthead-toolbar.tsx, prometheus-hook.ts, useDashboardResources.ts).

🔎 Apply the migration pattern

Remove the react-redux import and use console-specific hooks:

-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
+import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';

Then update line 141:

-  const resourceK8s = useSelector(resourceK8sSelector);
+  const resourceK8s = useConsoleSelector(resourceK8sSelector);

Also applies to: 141-141

🤖 Prompt for AI Agents
In
@frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts
at line 3, Replace direct react-redux usage with the console-specific selector
hook: remove useSelector import from 'react-redux' and import useConsoleSelector
instead, then update any usage of useSelector (e.g., the call at the current
useK8sWatchResources implementation around the reference on line with
useSelector) to call useConsoleSelector with the same selector function; keep
useDispatch unchanged if still required or replace with the console equivalent
if the migration pattern uses a different dispatch hook.

import { createSelectorCreator, defaultMemoize } from 'reselect';
import { K8sModel } from '../../../api/common-types';
import * as k8sActions from '../../../app/k8s/actions/k8s';
import type { SDKDispatch, SDKStoreState } from '../../../app/redux-types';
import { UseK8sWatchResources } from '../../../extensions/console-types';
import {
transformGroupVersionKindToReference,
Expand Down Expand Up @@ -38,9 +39,9 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => {
const resources = useDeepCompareMemoize(initResources, true);
const modelsLoaded = useModelsLoaded();

const allK8sModels = useSelector<OpenShiftReduxRootState, ImmutableMap<string, K8sModel>>(
(state: OpenShiftReduxRootState) => state.k8s.getIn(['RESOURCES', 'models']),
);
const allK8sModels = useSelector<SDKStoreState>((state) =>
state.k8s.getIn(['RESOURCES', 'models']),
) as ImmutableMap<string, K8sModel>;

const prevK8sModels = usePrevious(allK8sModels);
const prevResources = usePrevious(resources);
Expand Down Expand Up @@ -98,7 +99,7 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => {
[k8sModels, modelsLoaded, resources],
);

const dispatch = useDispatch();
const dispatch = useDispatch<SDKDispatch>();
useEffect(() => {
const reduxIDKeys = Object.keys(reduxIDs || {});
reduxIDKeys.forEach((k) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { useEffect, useMemo } from 'react';
import { Map as ImmutableMap } from 'immutable';
import { useSelector, useDispatch } from 'react-redux';
import {
watchPrometheusQuery,
stopWatchPrometheusQuery,
} from '@console/internal/actions/dashboards';
import { getInstantVectorStats } from '@console/internal/components/graphs/utils';
import { Humanize, HumanizeResult } from '@console/internal/components/utils/types';
import { RESULTS_TYPE } from '@console/internal/reducers/dashboard-results';
import { RootState } from '@console/internal/redux';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';

/** @deprecated use usePrometheusPoll() instead */
export const usePrometheusQuery: UsePrometheusQuery = (query, humanize) => {
const dispatch = useDispatch();
const dispatch = useConsoleDispatch();
useEffect(() => {
dispatch(watchPrometheusQuery(query));
return () => {
dispatch(stopWatchPrometheusQuery(query));
};
}, [dispatch, query]);

const queryResult = useSelector<RootState, ImmutableMap<string, any>>(({ dashboards }) =>
const queryResult = useConsoleSelector(({ dashboards }) =>
dashboards.getIn([RESULTS_TYPE.PROMETHEUS, query]),
);
) as ImmutableMap<string, any>;
const results = useMemo<[HumanizeResult, any, number]>(() => {
if (!queryResult || !queryResult.get('data')) {
return [{}, null, null] as [HumanizeResult, any, number];
Expand Down
14 changes: 14 additions & 0 deletions frontend/packages/console-shared/src/hooks/useConsoleDispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useDispatch } from 'react-redux';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { RootState } from '@console/internal/redux';

// TODO: When upgrading to react-redux v9, use the built-in `withTypes` method.
// See: https://github.com/reduxjs/react-redux/releases/tag/v9.1.0

/**
* A hook to access the console redux `dispatch` function.
*
* See {@link useDispatch} for more details.
*/
export const useConsoleDispatch: () => ThunkDispatch<RootState, undefined, AnyAction> = useDispatch;
12 changes: 12 additions & 0 deletions frontend/packages/console-shared/src/hooks/useConsoleSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import type { RootState } from '@console/internal/redux';

// TODO: When upgrading to react-redux v9, use the built-in `withTypes` method.
// See: https://github.com/reduxjs/react-redux/releases/tag/v9.1.0

/**
* A hook to access the console redux state.
*
* See {@link useSelector} for more details.
*/
export const useConsoleSelector: TypedUseSelectorHook<RootState> = useSelector;
12 changes: 12 additions & 0 deletions frontend/packages/console-shared/src/hooks/useConsoleStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useStore } from 'react-redux';
import type store from '@console/internal/redux';

// TODO: When upgrading to react-redux v9, use the built-in `withTypes` method.
// See: https://github.com/reduxjs/react-redux/releases/tag/v9.1.0

/**
* A hook to access the console redux store.
*
* See {@link useStore} for more details.
*/
export const useConsoleStore = useStore as () => typeof store;
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
RequestMap,
UseDashboardResources,
} from '@console/dynamic-plugin-sdk/src/api/internal-types';
import { UseDashboardResources } from '@console/dynamic-plugin-sdk/src/api/internal-types';
import {
stopWatchPrometheusQuery,
stopWatchURL,
watchPrometheusQuery,
watchURL,
} from '@console/internal/actions/dashboards';
import { PrometheusResponse } from '@console/internal/components/graphs';
import { RESULTS_TYPE } from '@console/internal/reducers/dashboard-results';
import { RootState } from 'public/redux';
import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch';
import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';
import { useNotificationAlerts } from './useNotificationAlerts';

export const useDashboardResources: UseDashboardResources = ({
Expand All @@ -24,7 +20,7 @@ export const useDashboardResources: UseDashboardResources = ({
notificationAlertLabelSelectors,
);

const dispatch = useDispatch();
const dispatch = useConsoleDispatch();
useEffect(() => {
prometheusQueries?.forEach((query) =>
dispatch(watchPrometheusQuery(query.query, null, query.timespan)),
Expand All @@ -39,10 +35,8 @@ export const useDashboardResources: UseDashboardResources = ({
};
}, [dispatch, prometheusQueries, urls]);

const urlResults = useSelector<RootState, RequestMap<any>>((state) =>
state.dashboards.get(RESULTS_TYPE.URL),
);
const prometheusResults = useSelector<RootState, RequestMap<PrometheusResponse>>((state) =>
const urlResults = useConsoleSelector((state) => state.dashboards.get(RESULTS_TYPE.URL));
const prometheusResults = useConsoleSelector((state) =>
state.dashboards.get(RESULTS_TYPE.PROMETHEUS),
);

Expand Down
Loading