diff --git a/frontend/package.json b/frontend/package.json index f927121e497..e049b614164 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", "react-router": "5.3.x", "react-router-dom": "5.3.x", "react-router-dom-v5-compat": "^6.11.2", diff --git a/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts b/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts index e0cfb837bbb..823a41e2c91 100644 --- a/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useBindingActions.ts @@ -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'; @@ -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'; @@ -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], diff --git a/frontend/packages/console-app/src/actions/hooks/useGroupActions.ts b/frontend/packages/console-app/src/actions/hooks/useGroupActions.ts index 530dcc92512..0404ffb7e1d 100644 --- a/frontend/packages/console-app/src/actions/hooks/useGroupActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useGroupActions.ts @@ -1,6 +1,5 @@ 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'; @@ -8,7 +7,8 @@ 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'; /** @@ -16,10 +16,10 @@ import AddGroupUsersModal from '../../components/modals/add-group-users-modal'; */ 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)), diff --git a/frontend/packages/console-app/src/actions/providers/user-provider.ts b/frontend/packages/console-app/src/actions/providers/user-provider.ts index cb5a0ae30c3..06ad8b385fa 100644 --- a/frontend/packages/console-app/src/actions/providers/user-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/user-provider.ts @@ -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( () => ({ diff --git a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionHookResolver.tsx b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionHookResolver.tsx index b435a1e41ec..57295b190bb 100644 --- a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionHookResolver.tsx +++ b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionHookResolver.tsx @@ -6,12 +6,10 @@ type FeatureFlagExtensionHookResolverProps = { setFeatureFlag: SetFeatureFlag; }; -const FeatureFlagExtensionHookResolver: FC = ({ +export const FeatureFlagExtensionHookResolver: FC = ({ handler, setFeatureFlag, }) => { handler(setFeatureFlag); return null; }; - -export default FeatureFlagExtensionHookResolver; diff --git a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx index 71ca4e94d9c..48fb2e348b9 100644 --- a/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx +++ b/frontend/packages/console-app/src/components/flags/FeatureFlagExtensionLoader.tsx @@ -1,16 +1,49 @@ import type { FC } from 'react'; +import { useCallback, useRef, useEffect } 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 + // if the flags change between render and the callback being invoked + const flagsRef = useRef(flags); + + useEffect(() => { + flagsRef.current = flags; + }, [flags]); + + return useCallback( + (flag, enabled) => { + // Defer dispatch to next event loop tick to avoid "Cannot update a component + // while rendering a different component" error + queueMicrotask(() => { + if (flagsRef.current.get(flag) === enabled) { + return; + } + dispatch(setFlag(flag, enabled)); + }); + }, + [dispatch], + ); +}; + +export const FeatureFlagExtensionLoader: FC = () => { const [flagProvider, flagProviderResolved] = useResolvedExtensions( isFeatureFlagHookProvider, ); + const featureFlagController = useFeatureFlagController(); + if (flagProviderResolved) { return ( <> @@ -32,4 +65,3 @@ const FeatureFlagExtensionLoader: FC = () => { } return null; }; -export default FeatureFlagExtensionLoader; diff --git a/frontend/packages/console-dynamic-plugin-sdk/README.md b/frontend/packages/console-dynamic-plugin-sdk/README.md index f28cb8d7e5b..a600422d5a0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/README.md +++ b/frontend/packages/console-dynamic-plugin-sdk/README.md @@ -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 diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts index f505a6a9192..92f643d78c3 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/redux-types.ts @@ -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'; @@ -27,3 +29,5 @@ export type SDKStoreState = { sdkCore: CoreState; k8s: K8sState; }; + +export type SDKDispatch = ThunkDispatch; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts index 68899278aa0..1605792787b 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource.ts @@ -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'; 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'; @@ -33,7 +33,7 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => { const reduxID = useMemo(() => getIDAndDispatch(resource, k8sModel), [k8sModel, resource]); - const dispatch = useDispatch(); + const dispatch = useDispatch(); useEffect(() => { if (reduxID) { @@ -46,9 +46,9 @@ export const useK8sWatchResource: UseK8sWatchResource = (initResource) => { }; }, [dispatch, reduxID]); - const resourceK8s = useSelector>((state) => + const resourceK8s = useSelector((state) => reduxID ? getReduxIdPayload(state, reduxID.id) : null, - ); + ) as ImmutableMap; return useMemo(() => { if (!resource) { diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts index 10530e12936..1cc5d0a2531 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResources.ts @@ -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'; 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, @@ -38,9 +39,9 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { const resources = useDeepCompareMemoize(initResources, true); const modelsLoaded = useModelsLoaded(); - const allK8sModels = useSelector>( - (state: OpenShiftReduxRootState) => state.k8s.getIn(['RESOURCES', 'models']), - ); + const allK8sModels = useSelector((state) => + state.k8s.getIn(['RESOURCES', 'models']), + ) as ImmutableMap; const prevK8sModels = usePrevious(allK8sModels); const prevResources = usePrevious(resources); @@ -98,7 +99,7 @@ export const useK8sWatchResources: UseK8sWatchResources = (initResources) => { [k8sModels, modelsLoaded, resources], ); - const dispatch = useDispatch(); + const dispatch = useDispatch(); useEffect(() => { const reduxIDKeys = Object.keys(reduxIDs || {}); reduxIDKeys.forEach((k) => { diff --git a/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts b/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts index 7f45aeb9d43..d971d225014 100644 --- a/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts +++ b/frontend/packages/console-shared/src/components/dashboard/utilization-card/prometheus-hook.ts @@ -1,6 +1,5 @@ import { useEffect, useMemo } from 'react'; import { Map as ImmutableMap } from 'immutable'; -import { useSelector, useDispatch } from 'react-redux'; import { watchPrometheusQuery, stopWatchPrometheusQuery, @@ -8,11 +7,12 @@ import { 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 () => { @@ -20,9 +20,9 @@ export const usePrometheusQuery: UsePrometheusQuery = (query, humanize) => { }; }, [dispatch, query]); - const queryResult = useSelector>(({ dashboards }) => + const queryResult = useConsoleSelector(({ dashboards }) => dashboards.getIn([RESULTS_TYPE.PROMETHEUS, query]), - ); + ) as ImmutableMap; const results = useMemo<[HumanizeResult, any, number]>(() => { if (!queryResult || !queryResult.get('data')) { return [{}, null, null] as [HumanizeResult, any, number]; diff --git a/frontend/packages/console-shared/src/hooks/useConsoleDispatch.ts b/frontend/packages/console-shared/src/hooks/useConsoleDispatch.ts new file mode 100644 index 00000000000..0373cee92d7 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useConsoleDispatch.ts @@ -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 = useDispatch; diff --git a/frontend/packages/console-shared/src/hooks/useConsoleSelector.ts b/frontend/packages/console-shared/src/hooks/useConsoleSelector.ts new file mode 100644 index 00000000000..3eabf9ffd03 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useConsoleSelector.ts @@ -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 = useSelector; diff --git a/frontend/packages/console-shared/src/hooks/useConsoleStore.ts b/frontend/packages/console-shared/src/hooks/useConsoleStore.ts new file mode 100644 index 00000000000..c1b36c4f823 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useConsoleStore.ts @@ -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; diff --git a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts index ebe16ee8ee4..766bd638b44 100644 --- a/frontend/packages/console-shared/src/hooks/useDashboardResources.ts +++ b/frontend/packages/console-shared/src/hooks/useDashboardResources.ts @@ -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 = ({ @@ -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)), @@ -39,10 +35,8 @@ export const useDashboardResources: UseDashboardResources = ({ }; }, [dispatch, prometheusQueries, urls]); - const urlResults = useSelector>((state) => - state.dashboards.get(RESULTS_TYPE.URL), - ); - const prometheusResults = useSelector>((state) => + const urlResults = useConsoleSelector((state) => state.dashboards.get(RESULTS_TYPE.URL)); + const prometheusResults = useConsoleSelector((state) => state.dashboards.get(RESULTS_TYPE.PROMETHEUS), ); diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx index 3f8bf038083..0afe7c756a3 100644 --- a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx +++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.tsx @@ -6,7 +6,6 @@ import { sortable } from '@patternfly/react-table'; import { JSONSchema7 } from 'json-schema'; import * as _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import { useParams, useLocation, useNavigate } from 'react-router-dom-v5-compat'; import { ListPageBody, K8sModel } from '@console/dynamic-plugin-sdk'; import { getResources } from '@console/internal/actions/k8s'; @@ -70,6 +69,7 @@ import { KEBAB_COLUMN_CLASS } from '@console/shared/src/components/actions/LazyA import ErrorAlert from '@console/shared/src/components/alerts/error'; import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; +import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch'; import { useK8sModel } from '@console/shared/src/hooks/useK8sModel'; import { useK8sModels } from '@console/shared/src/hooks/useK8sModels'; import { useResourceDetailsPage } from '@console/shared/src/hooks/useResourceDetailsPage'; @@ -360,7 +360,7 @@ export const ProvidedAPIsPage = (props: ProvidedAPIsPageProps) => { } = props; const [models, inFlight] = useK8sModels(); const navigate = useNavigate(); - const dispatch = useDispatch(); + const dispatch = useConsoleDispatch(); const [apiRefreshed, setAPIRefreshed] = useState(false); // Map APIs provided by this CSV to Firehose resources. Exclude APIs that do not have a model. @@ -549,7 +549,7 @@ export const ProvidedAPIPage = (props: ProvidedAPIPageProps) => { const [namespace] = useActiveNamespace(); const [k8sModel, inFlight] = useK8sModel(props.kind); const [apiRefreshed, setAPIRefreshed] = useState(false); - const dispatch = useDispatch(); + const dispatch = useConsoleDispatch(); // Refresh API definitions if model is missing and the definitions have not already been refreshed. const apiMightBeOutdated = !inFlight && !k8sModel; diff --git a/frontend/public/actions/dashboards.ts b/frontend/public/actions/dashboards.ts index 569064aa088..4fb21e7cf79 100644 --- a/frontend/public/actions/dashboards.ts +++ b/frontend/public/actions/dashboards.ts @@ -133,8 +133,11 @@ export type WatchPrometheusQueryAction = ( namespace?: string, timespan?: number, ) => ThunkAction; -export type StopWatchURLAction = (url: string) => void; -export type StopWatchPrometheusAction = (query: string, timespan?: number) => void; +export type StopWatchURLAction = (url: string) => ReturnType; +export type StopWatchPrometheusAction = ( + query: string, + timespan?: number, +) => ReturnType; type FetchPeriodically = ( dispatch: Dispatch, diff --git a/frontend/public/actions/features.ts b/frontend/public/actions/features.ts index 7a5922d0440..8553cd83b79 100644 --- a/frontend/public/actions/features.ts +++ b/frontend/public/actions/features.ts @@ -137,8 +137,16 @@ export const detectFeatures = () => (dispatch: Dispatch) => { ].forEach((detect) => detect(dispatch)); }; -export const featureFlagController: SetFeatureFlag = (flag, enabled) => { - store.dispatch(setFlag(flag, enabled)); +const featureFlagController: SetFeatureFlag = (flag, enabled) => { + // Defer dispatch to next event loop tick to avoid "Cannot update a component + // while rendering a different component" error + queueMicrotask(() => { + const currentValue = store.getState().FLAGS.get(flag); + if (currentValue === enabled) { + return; + } + store.dispatch(setFlag(flag, enabled)); + }); }; subscribeToExtensions( diff --git a/frontend/public/components/api-explorer.tsx b/frontend/public/components/api-explorer.tsx index 3b73ca90242..c1b2e1b6a7c 100644 --- a/frontend/public/components/api-explorer.tsx +++ b/frontend/public/components/api-explorer.tsx @@ -1,5 +1,4 @@ import { FC, MouseEvent, useEffect, useMemo, useRef, FormEvent, useState } from 'react'; -import { withRouter } from 'react-router-dom'; import { useLocation, useParams, @@ -8,7 +7,7 @@ import { useNavigate, } from 'react-router-dom-v5-compat'; import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; import * as _ from 'lodash-es'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; import { Map as ImmutableMap } from 'immutable'; @@ -136,10 +135,6 @@ const Group: FC<{ value: string }> = ({ value }) => { ); }; -const stateToProps = ({ k8s }) => ({ - models: k8s.getIn(['RESOURCES', 'models']), -}); - const BodyEmpty: FC<{ label: string; colSpan: number }> = ({ label, colSpan }) => { const { t } = useTranslation(); return ( @@ -155,10 +150,11 @@ const BodyEmpty: FC<{ label: string; colSpan: number }> = ({ label, colSpan }) = ); }; -const APIResourcesList = compose( - withRouter, - connect(stateToProps), -)(({ models, location }) => { +const APIResourcesList: FC = () => { + const location = useLocation(); + const models: ImmutableMap = useConsoleSelector((state) => + state.k8s.getIn(['RESOURCES', 'models']), + ); const ALL = '#all#'; const GROUP_PARAM = 'g'; const VERSION_PARAM = 'v'; @@ -516,7 +512,7 @@ const APIResourcesList = compose( ); -}); +}; APIResourcesList.displayName = 'APIResourcesList'; export const APIExplorerPage: FC<{}> = () => { @@ -1040,10 +1036,6 @@ type APIResourceLinkStateProps = { activeNamespace: string; }; -type APIResourcesListPropsFromState = { - models: ImmutableMap; -}; - type APIResourceLinkOwnProps = { model: K8sKind; }; diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 86a826d10dd..75dca21bdc7 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -30,7 +30,7 @@ import CloudShellDrawer from '@console/webterminal-plugin/src/components/cloud-s 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 FeatureFlagExtensionLoader from '@console/app/src/components/flags/FeatureFlagExtensionLoader'; +import { FeatureFlagExtensionLoader } from '@console/app/src/components/flags/FeatureFlagExtensionLoader'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { useResolvedExtensions, diff --git a/frontend/public/components/masthead/masthead-toolbar.tsx b/frontend/public/components/masthead/masthead-toolbar.tsx index 736ba24a87d..3ca912c9b30 100644 --- a/frontend/public/components/masthead/masthead-toolbar.tsx +++ b/frontend/public/components/masthead/masthead-toolbar.tsx @@ -1,7 +1,8 @@ import { Fragment, useContext, useState, useRef, useCallback, useEffect } from 'react'; import * as _ from 'lodash-es'; -import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; +import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; +import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch'; import { useNavigate } from 'react-router-dom-v5-compat'; import { BellIcon } from '@patternfly/react-icons/dist/esm/icons/bell-icon'; import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; @@ -44,7 +45,6 @@ import { getReportBugLink } from '../../module/k8s/cluster-settings'; import redhatLogoImg from '../../imgs/logos/redhat.svg'; import { TourContext, TourActions } from '@console/app/src/components/tour'; import { ClusterVersionModel, ConsoleLinkModel } from '../../models'; -import { RootState } from '../../redux'; import { FeedbackModal } from '@patternfly/react-user-feedback'; import '@patternfly/react-user-feedback/dist/esm/Feedback/Feedback.css'; import { useFeedbackLocal } from './feedback-local'; @@ -160,7 +160,7 @@ const MastheadToolbarContents: React.FCC = ({ const consoleCLIDownloadFlag = useFlag(FLAGS.CONSOLE_CLI_DOWNLOAD); const openshiftFlag = useFlag(FLAGS.OPENSHIFT); const quickstartFlag = useFlag(FLAGS.CONSOLE_QUICKSTART); - const dispatch = useDispatch(); + const dispatch = useConsoleDispatch(); const [activeNamespace] = useActiveNamespace(); const [activePerspective] = useActivePerspective(); const [requestTokenURL, externalLoginCommand] = useCopyLoginCommands(); @@ -168,7 +168,7 @@ const MastheadToolbarContents: React.FCC = ({ t('public~Login with this command'), externalLoginCommand, ); - const { clusterID, alertCount, canAccessNS, impersonate } = useSelector((state: RootState) => ({ + const { clusterID, alertCount, canAccessNS, impersonate } = useConsoleSelector((state) => ({ clusterID: state.UI.get('clusterID'), alertCount: state.observe.getIn(['alertCount']), canAccessNS: !!state[featureReducerName].get(FLAGS.CAN_GET_NS), diff --git a/frontend/public/components/useImpersonateRefreshFeatures.ts b/frontend/public/components/useImpersonateRefreshFeatures.ts index 5d249a5a230..49e2c81252b 100644 --- a/frontend/public/components/useImpersonateRefreshFeatures.ts +++ b/frontend/public/components/useImpersonateRefreshFeatures.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash-es'; import { useEffect, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useConsoleDispatch } from '@console/shared/src/hooks/useConsoleDispatch'; +import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector'; import { getImpersonate } from '@console/dynamic-plugin-sdk'; import * as UIActions from '../actions/ui'; @@ -12,8 +13,8 @@ import * as UIActions from '../actions/ui'; * clearing them to a PENDING state. */ export const useImpersonateRefreshFeatures = () => { - const dispatch = useDispatch(); - const impersonate = useSelector(getImpersonate); + const dispatch = useConsoleDispatch(); + const impersonate = useConsoleSelector(getImpersonate); const prevImpersonate = useRef(impersonate); useEffect(() => { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a522a386ba0..032a8f4a9fa 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -875,12 +875,10 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== - dependencies: - regenerator-runtime "^0.14.0" +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/template@^7.10.4", "@babel/template@^7.27.2", "@babel/template@^7.4.4": version "7.27.2" @@ -2970,10 +2968,10 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== -"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": - version "3.3.6" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010" - integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw== +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" @@ -3150,16 +3148,6 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.20": - version "7.1.34" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" - integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react-router-dom@5.3.x": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" @@ -3284,6 +3272,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/uuid@^3.4.6": version "3.4.9" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.9.tgz#fcf01997bbc9f7c09ae5f91383af076d466594e1" @@ -13964,12 +13957,12 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1, react-is@^17.0.2: +react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.3.1: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -14024,17 +14017,17 @@ react-modal@^3.16.3: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-redux@7.2.9: - version "7.2.9" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" - integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== +react-redux@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" react-refresh@^0.10.0: version "0.10.0" @@ -14243,7 +14236,7 @@ redux-thunk@2.4.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.0.tgz#ac89e1d6b9bdb9ee49ce69a69071be41bbd82d67" integrity sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA== -redux@^4.0.0, redux@^4.0.4: +redux@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== @@ -14276,11 +14269,6 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== - regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -16591,6 +16579,11 @@ url@^0.11.3, url@~0.11.0: punycode "^1.4.1" qs "^6.12.3" +use-sync-external-store@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"