From 310824ca1f991603ecfa08dd05013c8514b24727 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Tue, 20 Jan 2026 11:37:59 -0500 Subject: [PATCH 1/3] CONSOLE-5012: Migrate PVC modals to overlay pattern Migrates PVC-related modals (expand, clone, delete, restore) from the legacy createModalLauncher pattern to the modern OverlayComponent pattern with lazy loading. Changes include: - Convert modal exports to OverlayComponent providers - Implement lazy loading for modal components - Update action hooks to use useOverlay() and launchModal() - Set react-modal app element globally in App component - Maintain backward compatibility with existing modal exports Co-Authored-By: Claude Sonnet 4.5 --- .../src/actions/hooks/usePVCActions.ts | 24 ++++++++---------- .../actions/hooks/useVolumeSnapshotActions.ts | 9 ++++--- .../modals/clone/clone-pvc-modal.tsx | 14 +++++++++-- .../modals/restore-pvc/restore-pvc-modal.tsx | 14 +++++++++-- .../src/app/modal-support/OverlayProvider.tsx | 6 +++-- frontend/public/components/app.tsx | 9 +++++++ frontend/public/components/factory/modal.tsx | 2 +- .../components/modals/delete-pvc-modal.tsx | 14 +++++++++-- .../components/modals/expand-pvc-modal.tsx | 25 ++++++++++++++----- frontend/public/components/modals/index.ts | 24 ++++++++++++------ 10 files changed, 102 insertions(+), 39 deletions(-) diff --git a/frontend/packages/console-app/src/actions/hooks/usePVCActions.ts b/frontend/packages/console-app/src/actions/hooks/usePVCActions.ts index 47b6d02431e..33198010b57 100644 --- a/frontend/packages/console-app/src/actions/hooks/usePVCActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/usePVCActions.ts @@ -4,8 +4,11 @@ import { ModifyVACModal } from '@console/app/src/components/modals/modify-vac-mo import { Action } from '@console/dynamic-plugin-sdk'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { useDeepCompareMemoize } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useDeepCompareMemoize'; -import { clonePVCModal, expandPVCModal } from '@console/internal/components/modals'; -import deletePVCModal from '@console/internal/components/modals/delete-pvc-modal'; +import { + LazyClonePVCModalProvider, + LazyExpandPVCModalProvider, +} from '@console/internal/components/modals'; +import { DeletePVCModalProvider } from '@console/internal/components/modals/delete-pvc-modal'; import { asAccessReview } from '@console/internal/components/utils/rbac'; import { VolumeSnapshotModel, PersistentVolumeClaimModel } from '@console/internal/models'; import { PersistentVolumeClaimKind } from '@console/internal/module/k8s'; @@ -46,7 +49,7 @@ export const usePVCActions = ( id: 'expand-pvc', label: t('console-app~Expand PVC'), cta: () => - expandPVCModal({ + launchModal(LazyExpandPVCModalProvider, { kind: PersistentVolumeClaimModel, resource: obj, }), @@ -67,11 +70,7 @@ export const usePVCActions = ( label: t('console-app~Clone PVC'), disabled: obj?.status?.phase !== 'Bound', tooltip: obj?.status?.phase !== 'Bound' ? t('console-app~PVC is not Bound') : '', - cta: () => - clonePVCModal({ - kind: PersistentVolumeClaimModel, - resource: obj, - }), + cta: () => launchModal(LazyClonePVCModalProvider, { resource: obj }), accessReview: asAccessReview(PersistentVolumeClaimModel, obj, 'create'), }), [PVCActionCreator.ModifyVAC]: () => ({ @@ -88,14 +87,13 @@ export const usePVCActions = ( [PVCActionCreator.DeletePVC]: () => ({ id: 'delete-pvc', label: t('public~Delete PersistentVolumeClaim'), - cta: () => - deletePVCModal({ - pvc: obj, - }), + cta: () => launchModal(DeletePVCModalProvider, { pvc: obj }), accessReview: asAccessReview(PersistentVolumeClaimModel, obj, 'delete'), }), }), - [t, obj, launchModal], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, obj], ); // filter and initialize requested actions or construct list of all PVCActions diff --git a/frontend/packages/console-app/src/actions/hooks/useVolumeSnapshotActions.ts b/frontend/packages/console-app/src/actions/hooks/useVolumeSnapshotActions.ts index 261475fcd05..66adf588f9f 100644 --- a/frontend/packages/console-app/src/actions/hooks/useVolumeSnapshotActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useVolumeSnapshotActions.ts @@ -1,8 +1,9 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Action } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { useDeepCompareMemoize } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useDeepCompareMemoize'; -import { restorePVCModal } from '@console/internal/components/modals'; +import { LazyRestorePVCModalProvider } from '@console/internal/components/modals'; import { asAccessReview } from '@console/internal/components/utils/rbac'; import { VolumeSnapshotModel } from '@console/internal/models'; import { VolumeSnapshotKind } from '@console/internal/module/k8s'; @@ -32,6 +33,7 @@ export const useVolumeSnapshotActions = ( filterActions?: VolumeSnapshotActionCreator[], ): Action[] => { const { t } = useTranslation(); + const launchModal = useOverlay(); const memoizedFilterActions = useDeepCompareMemoize(filterActions); @@ -43,13 +45,14 @@ export const useVolumeSnapshotActions = ( disabled: !resource?.status?.readyToUse, tooltip: !resource?.status?.readyToUse ? t('console-app~Volume Snapshot is not Ready') : '', cta: () => - restorePVCModal({ - kind: VolumeSnapshotModel, + launchModal(LazyRestorePVCModalProvider, { resource, }), accessReview: asAccessReview(VolumeSnapshotModel, resource, 'create'), }), }), + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps [t, resource], ); diff --git a/frontend/packages/console-app/src/components/modals/clone/clone-pvc-modal.tsx b/frontend/packages/console-app/src/components/modals/clone/clone-pvc-modal.tsx index 3b2df85af25..51b420a7991 100644 --- a/frontend/packages/console-app/src/components/modals/clone/clone-pvc-modal.tsx +++ b/frontend/packages/console-app/src/components/modals/clone/clone-pvc-modal.tsx @@ -8,12 +8,13 @@ import { TextInput, } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; +import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { ModalBody, ModalComponentProps, ModalSubmitFooter, ModalTitle, - createModalLauncher, + ModalWrapper, } from '@console/internal/components/factory'; import { DataPoint } from '@console/internal/components/graphs'; import { PrometheusEndpoint } from '@console/internal/components/graphs/helpers'; @@ -275,4 +276,13 @@ export type ClonePVCModalProps = { resource?: PersistentVolumeClaimKind; } & ModalComponentProps; -export default createModalLauncher(ClonePVCModal); +export const ClonePVCModalProvider: OverlayComponent = (props) => { + return ( + + + + ); +}; + +// For backward compatibility with the index.ts re-export +export default ClonePVCModalProvider; diff --git a/frontend/packages/console-app/src/components/modals/restore-pvc/restore-pvc-modal.tsx b/frontend/packages/console-app/src/components/modals/restore-pvc/restore-pvc-modal.tsx index f3c2492e01c..dcc161df17c 100644 --- a/frontend/packages/console-app/src/components/modals/restore-pvc/restore-pvc-modal.tsx +++ b/frontend/packages/console-app/src/components/modals/restore-pvc/restore-pvc-modal.tsx @@ -11,12 +11,13 @@ import { } from '@patternfly/react-core'; import { Trans, useTranslation } from 'react-i18next'; import { VolumeModeSelector } from '@console/app/src/components/volume-modes/volume-mode'; +import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { ModalBody, ModalComponentProps, ModalSubmitFooter, ModalTitle, - createModalLauncher, + ModalWrapper, } from '@console/internal/components/factory'; import { dropdownUnits, @@ -278,4 +279,13 @@ type RestorePVCModalProps = { resource: VolumeSnapshotKind; } & ModalComponentProps; -export default createModalLauncher(RestorePVCModal); +export const RestorePVCModalProvider: OverlayComponent = (props) => { + return ( + + + + ); +}; + +// For backward compatibility with the index.ts re-export +export default RestorePVCModalProvider; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/OverlayProvider.tsx b/frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/OverlayProvider.tsx index c31b61a756f..961663a6b0a 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/OverlayProvider.tsx +++ b/frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/OverlayProvider.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react'; -import { createContext, useState, useCallback } from 'react'; +import { createContext, useState, useCallback, Suspense } from 'react'; import * as _ from 'lodash'; import { UnknownProps } from '../common-types'; @@ -52,7 +52,9 @@ export const OverlayProvider: FC = ({ children }) => { return ( {_.map(componentsMap, (c, id) => ( - closeOverlay(id)} /> + + closeOverlay(id)} /> + ))} {children} diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 4fae8fd33e0..aa0043953e8 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -5,6 +5,7 @@ import type { FC, Provider as ProviderComponent, ReactNode } from 'react'; import { render } from 'react-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { linkify } from 'react-linkify'; +import * as Modal from 'react-modal'; import { Provider, useSelector, useDispatch } from 'react-redux'; import { Router } from 'react-router-dom'; import { useParams, useLocation, CompatRouter, Routes, Route } from 'react-router-dom-v5-compat'; @@ -136,6 +137,14 @@ const App: FC<{ useCSPViolationDetector(); useNotificationPoller(); + // Initialize react-modal app element for accessibility + useEffect(() => { + const appElement = document.getElementById('app-content'); + if (appElement) { + Modal.setAppElement(appElement); + } + }, []); + useEffect(() => { window.addEventListener('resize', onResize); return () => { diff --git a/frontend/public/components/factory/modal.tsx b/frontend/public/components/factory/modal.tsx index 06b72acea1b..a7c0cbaad14 100644 --- a/frontend/public/components/factory/modal.tsx +++ b/frontend/public/components/factory/modal.tsx @@ -24,7 +24,7 @@ export const createModal: CreateModal = (getModalElement) => { ReactDOM.unmountComponentAtNode(containerElement); resolve(); }; - Modal.setAppElement(document.getElementById('app-content')); + // Modal app element is now set globally in App component containerElement && ReactDOM.render(getModalElement(closeModal), containerElement); }); return { result }; diff --git a/frontend/public/components/modals/delete-pvc-modal.tsx b/frontend/public/components/modals/delete-pvc-modal.tsx index 61a0d733b70..0cb1e55d361 100644 --- a/frontend/public/components/modals/delete-pvc-modal.tsx +++ b/frontend/public/components/modals/delete-pvc-modal.tsx @@ -6,11 +6,12 @@ import { getName } from '@console/shared/src/selectors/common'; import { YellowExclamationTriangleIcon } from '@console/shared/src/components/status/icons'; import { useResolvedExtensions } from '@console/dynamic-plugin-sdk/src/api/useResolvedExtensions'; import { isPVCDelete, PVCDelete } from '@console/dynamic-plugin-sdk/src/extensions/pvc'; +import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; import { - createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter, + ModalWrapper, ModalComponentProps, } from '../factory'; import { k8sKill, PersistentVolumeClaimKind } from '@console/internal/module/k8s'; @@ -83,4 +84,13 @@ export type DeletePVCModalProps = { pvc: PersistentVolumeClaimKind; } & ModalComponentProps; -export default createModalLauncher(DeletePVCModal); +export const DeletePVCModalProvider: OverlayComponent = (props) => { + return ( + + + + ); +}; + +// For backward compatibility with the index.ts re-export +export default DeletePVCModalProvider; diff --git a/frontend/public/components/modals/expand-pvc-modal.tsx b/frontend/public/components/modals/expand-pvc-modal.tsx index 0735da9afab..b0a6b78b1dd 100644 --- a/frontend/public/components/modals/expand-pvc-modal.tsx +++ b/frontend/public/components/modals/expand-pvc-modal.tsx @@ -1,8 +1,14 @@ import { useState, useCallback } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; - -import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory/modal'; +import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; +import { + ModalTitle, + ModalBody, + ModalSubmitFooter, + ModalWrapper, + ModalComponentProps, +} from '../factory/modal'; import { RequestSizeInput } from '../utils/request-size-input'; import { resourceObjPath } from '../utils/resource-link'; import { validate, convertToBaseValue, humanizeBinaryBytesWithoutB } from '../utils/units'; @@ -85,11 +91,18 @@ const ExpandPVCModal = ({ resource, kind, close, cancel }: ExpandPVCModalProps) ); }; -export const expandPVCModal = createModalLauncher(ExpandPVCModal); - export type ExpandPVCModalProps = { kind: K8sKind; resource: K8sResourceKind; - cancel?: () => void; - close: () => void; +} & ModalComponentProps; + +export const ExpandPVCModalProvider: OverlayComponent = (props) => { + return ( + + + + ); }; + +// For backward compatibility with the index.ts re-export +export default ExpandPVCModalProvider; diff --git a/frontend/public/components/modals/index.ts b/frontend/public/components/modals/index.ts index 8b887e54310..a5b52a53740 100644 --- a/frontend/public/components/modals/index.ts +++ b/frontend/public/components/modals/index.ts @@ -1,5 +1,7 @@ // This module utilizes dynamic `import()` to enable lazy-loading for each modal instead of including them in the main bundle. +import { lazy } from 'react'; + // Helper to detect if a modal is open. This is used to disable autofocus in elements under a modal. // TODO: Improve focus and keybinding handling, see https://issues.redhat.com/browse/ODC-3554 export const isModalOpen = () => document.body.classList.contains('ReactModal__Body--open'); @@ -73,15 +75,19 @@ export const tolerationsModal = (props) => m.tolerationsModal(props), ); -export const expandPVCModal = (props) => - import('./expand-pvc-modal' /* webpackChunkName: "expand-pvc-modal" */).then((m) => - m.expandPVCModal(props), - ); +// Lazy-loaded OverlayComponent for Expand PVC Modal +export const LazyExpandPVCModalProvider = lazy(() => + import('./expand-pvc-modal' /* webpackChunkName: "expand-pvc-modal" */).then((m) => ({ + default: m.default, + })), +); -export const clonePVCModal = (props) => +// Lazy-loaded OverlayComponent for Clone PVC Modal +export const LazyClonePVCModalProvider = lazy(() => import( '@console/app/src/components/modals/clone/clone-pvc-modal' /* webpackChunkName: "clone-pvc-modal" */ - ).then((m) => m.default(props)); + ).then((m) => ({ default: m.default })), +); export const configureClusterUpstreamModal = (props) => import( @@ -103,10 +109,12 @@ export const removeUserModal = (props) => m.removeUserModal(props), ); -export const restorePVCModal = (props) => +// Lazy-loaded OverlayComponent for Restore PVC Modal +export const LazyRestorePVCModalProvider = lazy(() => import( '@console/app/src/components/modals/restore-pvc/restore-pvc-modal' /* webpackChunkName: "restore-pvc-modal" */ - ).then((m) => m.default(props)); + ).then((m) => ({ default: m.default })), +); export const managedResourceSaveModal = (props) => import( From 1ccc1daf0b15af0fa046d98f3be07d461e5def4d Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Tue, 20 Jan 2026 15:28:41 -0500 Subject: [PATCH 2/3] CONSOLE-5012: Migrate delete-modal and actions to overlay pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit completes the migration from createModalLauncher to the OverlayComponent pattern for delete modals and related actions. Changes: - Refactored delete-modal.tsx to use OverlayComponent pattern - Added LazyDeleteModalProvider with React.lazy() for code splitting - Converted action creators to hooks to comply with React rules: * DeleteResourceAction → useDeleteResourceAction (context-menu.ts) * deleteKnativeServiceResource → useDeleteKnativeServiceResource (creators.ts) - Updated all consumers to use new hook-based actions: * useCommonActions - uses LazyDeleteModalProvider with launchModal * deployment-provider - uses useDeleteResourceAction hook * deploymentconfig-provider - uses useDeleteResourceAction hook * knative-plugin providers - uses useDeleteKnativeServiceResource hook - Updated public/components/modals/index.ts with lazy export - Eliminated all deleteModal() function calls Note: DeleteApplicationAction remains as a non-hook function creator since it doesn't use modals and doesn't need to comply with hooks rules. This change maintains backward compatibility through default exports and preserves lazy loading functionality. Co-Authored-By: Claude Sonnet 4.5 --- .../src/actions/hooks/useCommonActions.ts | 7 ++- .../actions/providers/deployment-provider.ts | 6 +- .../providers/deploymentconfig-provider.ts | 6 +- .../dev-console/src/actions/context-menu.ts | 35 ++++++++---- .../knative-plugin/src/actions/creators.ts | 57 +++++++++++-------- .../knative-plugin/src/actions/providers.ts | 20 ++++++- .../public/components/modals/delete-modal.tsx | 24 ++++++-- frontend/public/components/modals/index.ts | 8 ++- 8 files changed, 113 insertions(+), 50 deletions(-) diff --git a/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts b/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts index 20e9302b249..2ec1f787a3c 100644 --- a/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useCommonActions.ts @@ -1,10 +1,11 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Action } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { useDeepCompareMemoize } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useDeepCompareMemoize'; import { annotationsModalLauncher, - deleteModal, + LazyDeleteModalProvider, labelsModalLauncher, podSelectorModal, taintsModal, @@ -43,6 +44,7 @@ export const useCommonActions = ( editPath?: string, ): [ActionObject, boolean] => { const { t } = useTranslation(); + const launchModal = useOverlay(); const launchCountModal = useConfigureCountModal({ resourceKind: kind, resource, @@ -75,7 +77,7 @@ export const useCommonActions = ( id: 'delete-resource', label: t('console-app~Delete {{kind}}', { kind: kind?.kind }), cta: () => - deleteModal({ + launchModal(LazyDeleteModalProvider, { kind, resource, message, @@ -169,6 +171,7 @@ export const useCommonActions = ( accessReview: asAccessReview(kind as K8sModel, resource as K8sResourceKind, 'patch'), }), }), + // missing launchModal dependency, that causes max depth exceeded error // eslint-disable-next-line react-hooks/exhaustive-deps [kind, resource, t, message, actualEditPath], ); diff --git a/frontend/packages/console-app/src/actions/providers/deployment-provider.ts b/frontend/packages/console-app/src/actions/providers/deployment-provider.ts index 9802d8ad741..c4a0c4d9077 100644 --- a/frontend/packages/console-app/src/actions/providers/deployment-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/deployment-provider.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { DeleteResourceAction } from '@console/dev-console/src/actions/context-menu'; +import { useDeleteResourceAction } from '@console/dev-console/src/actions/context-menu'; import { Action } from '@console/dynamic-plugin-sdk/src'; import { DeploymentKind, referenceFor } from '@console/internal/module/k8s'; import { useK8sModel } from '@console/shared/src/hooks/useK8sModel'; @@ -14,6 +14,7 @@ export const useDeploymentActionsProvider = (resource: DeploymentKind) => { const [kindObj, inFlight] = useK8sModel(referenceFor(resource)); const [hpaActions, relatedHPAs] = useHPAActions(kindObj, resource); const [pdbActions] = usePDBActions(kindObj, resource); + const deleteResourceAction = useDeleteResourceAction(kindObj, resource); const [deploymentActionsObject, deploymentActionsReady] = useDeploymentActions( kindObj, resource, @@ -54,7 +55,7 @@ export const useDeploymentActionsProvider = (resource: DeploymentKind) => { deploymentActionsObject.EditDeployment, ...(resource.metadata?.annotations?.['openshift.io/generated-by'] === 'OpenShiftWebConsole' - ? [DeleteResourceAction(kindObj, resource)] + ? [deleteResourceAction] : [commonActions.Delete]), ]; }, [ @@ -66,6 +67,7 @@ export const useDeploymentActionsProvider = (resource: DeploymentKind) => { commonActions, isReady, deploymentActionsObject, + deleteResourceAction, ]); return [deploymentActions, !inFlight, undefined]; diff --git a/frontend/packages/console-app/src/actions/providers/deploymentconfig-provider.ts b/frontend/packages/console-app/src/actions/providers/deploymentconfig-provider.ts index 465018427c8..31900dd7355 100644 --- a/frontend/packages/console-app/src/actions/providers/deploymentconfig-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/deploymentconfig-provider.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { DeleteResourceAction } from '@console/dev-console/src/actions/context-menu'; +import { useDeleteResourceAction } from '@console/dev-console/src/actions/context-menu'; import { DeploymentConfigKind, referenceFor } from '@console/internal/module/k8s'; import { useK8sModel } from '@console/shared/src/hooks/useK8sModel'; import { getHealthChecksAction } from '../creators/health-checks-factory'; @@ -15,6 +15,7 @@ export const useDeploymentConfigActionsProvider = (resource: DeploymentConfigKin const [hpaActions, relatedHPAs] = useHPAActions(kindObj, resource); const [pdbActions] = usePDBActions(kindObj, resource); const retryRolloutAction = useRetryRolloutAction(resource); + const deleteResourceAction = useDeleteResourceAction(kindObj, resource); const [deploymentActions, deploymentActionsReady] = useDeploymentActions(kindObj, resource, [ DeploymentActionCreator.StartDCRollout, DeploymentActionCreator.PauseRollout, @@ -50,7 +51,7 @@ export const useDeploymentConfigActionsProvider = (resource: DeploymentConfigKin deploymentActions.EditDeployment, ...(resource.metadata?.annotations?.['openshift.io/generated-by'] === 'OpenShiftWebConsole' - ? [DeleteResourceAction(kindObj, resource)] + ? [deleteResourceAction] : [commonActions.Delete]), ] : [], @@ -64,6 +65,7 @@ export const useDeploymentConfigActionsProvider = (resource: DeploymentConfigKin commonActions, deploymentActions, isReady, + deleteResourceAction, ], ); diff --git a/frontend/packages/dev-console/src/actions/context-menu.ts b/frontend/packages/dev-console/src/actions/context-menu.ts index 4d580b77529..8be43358493 100644 --- a/frontend/packages/dev-console/src/actions/context-menu.ts +++ b/frontend/packages/dev-console/src/actions/context-menu.ts @@ -1,7 +1,10 @@ +import { useMemo } from 'react'; import i18next from 'i18next'; +import { useTranslation } from 'react-i18next'; import { Action, K8sModel } from '@console/dynamic-plugin-sdk'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; import { TopologyApplicationObject } from '@console/dynamic-plugin-sdk/src/extensions/topology-types'; -import { deleteModal } from '@console/internal/components/modals'; +import { LazyDeleteModalProvider } from '@console/internal/components/modals'; import { asAccessReview } from '@console/internal/components/utils'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { deleteResourceModal } from '@console/shared'; @@ -36,14 +39,24 @@ export const DeleteApplicationAction = ( }; }; -export const DeleteResourceAction = (kind: K8sModel, obj: K8sResourceKind): Action => ({ - id: `delete-resource`, - label: i18next.t('devconsole~Delete {{kind}}', { kind: kind.kind }), - cta: () => - deleteModal({ - kind, - resource: obj, - deleteAllResources: () => cleanUpWorkload(obj), +export const useDeleteResourceAction = (kind: K8sModel, obj: K8sResourceKind): Action => { + const { t } = useTranslation(); + const launchModal = useOverlay(); + + return useMemo( + () => ({ + id: `delete-resource`, + label: t('devconsole~Delete {{kind}}', { kind: kind.kind }), + cta: () => + launchModal(LazyDeleteModalProvider, { + kind, + resource: obj, + deleteAllResources: () => cleanUpWorkload(obj), + }), + accessReview: asAccessReview(kind, obj, 'delete'), }), - accessReview: asAccessReview(kind, obj, 'delete'), -}); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, kind, obj], + ); +}; diff --git a/frontend/packages/knative-plugin/src/actions/creators.ts b/frontend/packages/knative-plugin/src/actions/creators.ts index a5d07e7316b..f01eae935bf 100644 --- a/frontend/packages/knative-plugin/src/actions/creators.ts +++ b/frontend/packages/knative-plugin/src/actions/creators.ts @@ -1,7 +1,8 @@ import { useMemo } from 'react'; import i18next from 'i18next'; import { Action } from '@console/dynamic-plugin-sdk'; -import { deleteModal } from '@console/internal/components/modals'; +import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; +import { LazyDeleteModalProvider } from '@console/internal/components/modals'; import { asAccessReview, resourceObjPath } from '@console/internal/components/utils'; import { truncateMiddle } from '@console/internal/components/utils/truncate-middle'; import { K8sKind, K8sResourceKind, referenceForModel } from '@console/internal/module/k8s'; @@ -116,32 +117,42 @@ export const editKnativeServiceResource = ( }; }; -export const deleteKnativeServiceResource = ( +export const useDeleteKnativeServiceResource = ( kind: K8sKind, obj: K8sResourceKind, serviceTypeValue: ServiceTypeValue, serviceCreatedFromWebFlag: boolean, -): Action => ({ - id: `delete-resource`, - label: - serviceTypeValue === ServiceTypeValue.Function - ? i18next.t('knative-plugin~Delete Function') - : i18next.t('knative-plugin~Delete Service'), - cta: () => - deleteModal( - serviceCreatedFromWebFlag - ? { - kind, - resource: obj, - deleteAllResources: () => cleanUpWorkload(obj), - } - : { - kind, - resource: obj, - }, - ), - accessReview: asAccessReview(kind, obj, 'delete'), -}); +): Action => { + const launchModal = useOverlay(); + + return useMemo( + () => ({ + id: `delete-resource`, + label: + serviceTypeValue === ServiceTypeValue.Function + ? i18next.t('knative-plugin~Delete Function') + : i18next.t('knative-plugin~Delete Service'), + cta: () => + launchModal( + LazyDeleteModalProvider, + serviceCreatedFromWebFlag + ? { + kind, + resource: obj, + deleteAllResources: () => cleanUpWorkload(obj), + } + : { + kind, + resource: obj, + }, + ), + accessReview: asAccessReview(kind, obj, 'delete'), + }), + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [kind, obj, serviceTypeValue, serviceCreatedFromWebFlag], + ); +}; export const moveSinkSource = ( model: K8sKind, diff --git a/frontend/packages/knative-plugin/src/actions/providers.ts b/frontend/packages/knative-plugin/src/actions/providers.ts index a43f77c03cf..fac3bdd5af9 100644 --- a/frontend/packages/knative-plugin/src/actions/providers.ts +++ b/frontend/packages/knative-plugin/src/actions/providers.ts @@ -58,7 +58,7 @@ import { editKnativeService, moveSinkSource, editKnativeServiceResource, - deleteKnativeServiceResource, + useDeleteKnativeServiceResource, useDeleteRevisionAction, useSetTrafficDistributionAction, useMoveSinkPubsubAction, @@ -95,6 +95,18 @@ export const useKnativeServiceActionsProvider = (resource: K8sResourceKind) => { const serviceTypeValue = useContext(KnativeServiceTypeContext); const setTrafficDistributionAction = useSetTrafficDistributionAction(kindObj, resource); const testServerlessFunctionAction = useTestServerlessFunctionAction(kindObj, resource); + const deleteKnativeServiceFromWebAction = useDeleteKnativeServiceResource( + kindObj, + resource, + serviceTypeValue, + true, + ); + const deleteKnativeServiceAction = useDeleteKnativeServiceResource( + kindObj, + resource, + serviceTypeValue, + false, + ); const [deploymentActions, deploymentActionsReady] = useDeploymentActions(kindObj, resource, [ DeploymentActionCreator.EditResourceLimits, ] as const); @@ -119,8 +131,8 @@ export const useKnativeServiceActionsProvider = (resource: K8sResourceKind) => { editKnativeServiceResource(kindObj, resource, serviceTypeValue), ...(resource.metadata.annotations?.['openshift.io/generated-by'] === 'OpenShiftWebConsole' - ? [deleteKnativeServiceResource(kindObj, resource, serviceTypeValue, true)] - : [deleteKnativeServiceResource(kindObj, resource, serviceTypeValue, false)]), + ? [deleteKnativeServiceFromWebAction] + : [deleteKnativeServiceAction]), ...(resource?.metadata?.labels?.['function.knative.dev'] === 'true' ? [testServerlessFunctionAction] : []), @@ -134,6 +146,8 @@ export const useKnativeServiceActionsProvider = (resource: K8sResourceKind) => { commonActions, serviceTypeValue, testServerlessFunctionAction, + deleteKnativeServiceFromWebAction, + deleteKnativeServiceAction, ], ); diff --git a/frontend/public/components/modals/delete-modal.tsx b/frontend/public/components/modals/delete-modal.tsx index 0779f965e3c..9fc95f87d86 100644 --- a/frontend/public/components/modals/delete-modal.tsx +++ b/frontend/public/components/modals/delete-modal.tsx @@ -4,7 +4,14 @@ import { useState, useCallback, useEffect } from 'react'; import { Alert, Backdrop, Checkbox, Modal, ModalVariant } from '@patternfly/react-core'; import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory/modal'; +import { OverlayComponent } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider'; +import { + ModalTitle, + ModalBody, + ModalSubmitFooter, + ModalWrapper, + ModalComponentProps, +} from '../factory/modal'; import { resourceListPathFromModel, ResourceLink } from '../utils/resource-link'; import { k8sKill, @@ -173,15 +180,22 @@ export const DeleteOverlay: FC = (props) => { ) : null; }; -export const deleteModal = createModalLauncher(DeleteModal); +export const DeleteModalProvider: OverlayComponent = (props) => { + return ( + + + + ); +}; export type DeleteModalProps = { kind: K8sModel; resource: K8sResourceKind; - close?: () => void; redirectTo?: LocationDescriptor; message?: JSX.Element; - cancel?: () => void; btnText?: ReactNode; deleteAllResources?: () => Promise; -}; +} & ModalComponentProps; + +// For backward compatibility with the index.ts re-export +export default DeleteModalProvider; diff --git a/frontend/public/components/modals/index.ts b/frontend/public/components/modals/index.ts index a5b52a53740..dc85992b80b 100644 --- a/frontend/public/components/modals/index.ts +++ b/frontend/public/components/modals/index.ts @@ -49,8 +49,12 @@ export const configureUpdateStrategyModal = (props) => export const annotationsModalLauncher = (props) => import('./tags' /* webpackChunkName: "tags" */).then((m) => m.annotationsModalLauncher(props)); -export const deleteModal = (props) => - import('./delete-modal' /* webpackChunkName: "delete-modal" */).then((m) => m.deleteModal(props)); +// Lazy-loaded OverlayComponent for Delete Modal +export const LazyDeleteModalProvider = lazy(() => + import('./delete-modal' /* webpackChunkName: "delete-modal" */).then((m) => ({ + default: m.default, + })), +); export const clusterChannelModal = (props) => import('./cluster-channel-modal' /* webpackChunkName: "cluster-channel-modal" */).then((m) => From ebc2c5dd1c33808619f717b4a7732f206068a684 Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Wed, 21 Jan 2026 12:05:46 -0500 Subject: [PATCH 3/3] CONSOLE-5012: Fix launchModal dependency in existing action hooks Remove launchModal from dependency arrays in pre-existing hooks to prevent infinite loops. These files were not modified as part of the modal migration work, but had the same issue. Co-Authored-By: Claude Sonnet 4.5 --- .../console-app/src/actions/hooks/useBuildsActions.ts | 4 +++- .../console-app/src/actions/hooks/useDeploymentActions.ts | 4 +++- .../console-app/src/actions/hooks/useJobActions.ts | 4 +++- .../src/actions/hooks/useRetryRolloutAction.ts | 4 +++- .../src/actions/providers/build-config-provider.ts | 8 ++++++-- .../src/actions/providers/machine-config-pool-provider.ts | 4 +++- .../src/components/formik-fields/EnvironmentField.tsx | 4 +++- .../src/components/add/SecretKeySelector.tsx | 4 +++- .../shipwright-plugin/src/actions/useBuildActions.ts | 4 +++- .../shipwright-plugin/src/actions/useBuildRunActions.ts | 4 +++- frontend/public/components/modals/expand-pvc-modal.tsx | 2 +- frontend/public/components/utils/webhooks.tsx | 4 +++- 12 files changed, 37 insertions(+), 13 deletions(-) diff --git a/frontend/packages/console-app/src/actions/hooks/useBuildsActions.ts b/frontend/packages/console-app/src/actions/hooks/useBuildsActions.ts index a77050d6706..d07edac5f18 100644 --- a/frontend/packages/console-app/src/actions/hooks/useBuildsActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useBuildsActions.ts @@ -110,7 +110,9 @@ export const useBuildsActions = ( accessReview: asAccessReview(kindObj, obj, 'patch'), }), }), - [t, kindObj, obj, launchModal, cancelBuildModal], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, kindObj, obj, cancelBuildModal], ); const buildPhase = obj.status?.phase; diff --git a/frontend/packages/console-app/src/actions/hooks/useDeploymentActions.ts b/frontend/packages/console-app/src/actions/hooks/useDeploymentActions.ts index df952300a0f..68ff670fe9f 100644 --- a/frontend/packages/console-app/src/actions/hooks/useDeploymentActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useDeploymentActions.ts @@ -184,7 +184,9 @@ export const useDeploymentActions = , boolean] => { diff --git a/frontend/packages/console-app/src/actions/hooks/useJobActions.ts b/frontend/packages/console-app/src/actions/hooks/useJobActions.ts index 1e1ebd9ea82..8ede8bd511f 100644 --- a/frontend/packages/console-app/src/actions/hooks/useJobActions.ts +++ b/frontend/packages/console-app/src/actions/hooks/useJobActions.ts @@ -52,7 +52,9 @@ export const useJobActions = (obj: JobKind, filterActions?: JobActionCreator[]): accessReview: asAccessReview(JobModel, obj, 'patch'), }), }), - [t, obj, launchModal], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, obj], ); // filter and initialize requested actions or construct list of all PVCActions diff --git a/frontend/packages/console-app/src/actions/hooks/useRetryRolloutAction.ts b/frontend/packages/console-app/src/actions/hooks/useRetryRolloutAction.ts index 4b04818a503..e665c45e777 100644 --- a/frontend/packages/console-app/src/actions/hooks/useRetryRolloutAction.ts +++ b/frontend/packages/console-app/src/actions/hooks/useRetryRolloutAction.ts @@ -95,6 +95,8 @@ export const useRetryRolloutAction = (resource: DeploymentConfigKind): Action => verb: 'patch', }, }), - [t, dcModel, rcModel, rc, canRetry, resource, launchModal], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t, dcModel, rcModel, rc, canRetry, resource], ); }; diff --git a/frontend/packages/console-app/src/actions/providers/build-config-provider.ts b/frontend/packages/console-app/src/actions/providers/build-config-provider.ts index e27a22ca6ef..e6af5e5d41a 100644 --- a/frontend/packages/console-app/src/actions/providers/build-config-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/build-config-provider.ts @@ -33,7 +33,9 @@ const useStartBuildAction = (obj: BuildConfig): Action[] => { accessReview: asAccessReview(BuildConfigModel, buildConfig, 'create', 'instantiate'), }), }), - [launchModal, t], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [t], ); const actions = useMemo(() => [factory[BuildConfigActionCreator.StartBuild](obj)], [ factory, @@ -66,7 +68,9 @@ const useStartLastBuildAction = ( accessReview: asAccessReview(BuildConfigModel, latestBuild, 'create', 'instantiate'), }), }), - [latestBuild, launchModal, t], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [latestBuild, t], ); const actions = useMemo( () => (latestBuild ? [factory[BuildConfigActionCreator.StartLastRun]()] : []), diff --git a/frontend/packages/console-app/src/actions/providers/machine-config-pool-provider.ts b/frontend/packages/console-app/src/actions/providers/machine-config-pool-provider.ts index 4a02c679526..8b30f2e88ae 100644 --- a/frontend/packages/console-app/src/actions/providers/machine-config-pool-provider.ts +++ b/frontend/packages/console-app/src/actions/providers/machine-config-pool-provider.ts @@ -24,7 +24,9 @@ const usePauseAction = (obj: MachineConfigPoolKind): Action[] => { accessReview: asAccessReview(MachineConfigPoolModel, obj, 'patch'), }), }), - [launchModal, obj, t], + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + [obj, t], ); const action = useMemo(() => [factory.PauseUpdates()], [factory]); diff --git a/frontend/packages/console-shared/src/components/formik-fields/EnvironmentField.tsx b/frontend/packages/console-shared/src/components/formik-fields/EnvironmentField.tsx index b81a4349985..ad7e7cdd994 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/EnvironmentField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/EnvironmentField.tsx @@ -75,7 +75,9 @@ const EnvironmentField: FC = ({ } catch (e) {} } }); - }, [namespace, launchModal]); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [namespace]); return ( diff --git a/frontend/packages/knative-plugin/src/components/add/SecretKeySelector.tsx b/frontend/packages/knative-plugin/src/components/add/SecretKeySelector.tsx index d17af7bb5f1..0b028203c62 100644 --- a/frontend/packages/knative-plugin/src/components/add/SecretKeySelector.tsx +++ b/frontend/packages/knative-plugin/src/components/add/SecretKeySelector.tsx @@ -58,7 +58,9 @@ const SecretKeySelector: FC = ({ launchModal(ErrorModal, { error: err?.message }); } }); - }, [namespace, launchModal]); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [namespace]); return ( diff --git a/frontend/packages/shipwright-plugin/src/actions/useBuildActions.ts b/frontend/packages/shipwright-plugin/src/actions/useBuildActions.ts index df0501c1d3f..6a8f6b3db4c 100644 --- a/frontend/packages/shipwright-plugin/src/actions/useBuildActions.ts +++ b/frontend/packages/shipwright-plugin/src/actions/useBuildActions.ts @@ -88,7 +88,9 @@ const useBuildActions = (build: Build) => { }); actions.push(commonActions.Delete); return actions; - }, [t, build, navigate, commonActions, isReady, launchModal]); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [t, build, navigate, commonActions, isReady]); return [actionsMenu, !inFlight, undefined]; }; diff --git a/frontend/packages/shipwright-plugin/src/actions/useBuildRunActions.ts b/frontend/packages/shipwright-plugin/src/actions/useBuildRunActions.ts index aa9dce0302a..7a508342666 100644 --- a/frontend/packages/shipwright-plugin/src/actions/useBuildRunActions.ts +++ b/frontend/packages/shipwright-plugin/src/actions/useBuildRunActions.ts @@ -42,7 +42,9 @@ const useBuildRunActions = (buildRun: BuildRun) => { }; return [...(canRerunBuildRun(buildRun) ? [rerun] : []), ...commonActions]; - }, [t, buildRun, navigate, commonActions, launchModal]); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [t, buildRun, navigate, commonActions]); return [actions, !inFlight, undefined]; }; diff --git a/frontend/public/components/modals/expand-pvc-modal.tsx b/frontend/public/components/modals/expand-pvc-modal.tsx index b0a6b78b1dd..ae530db9949 100644 --- a/frontend/public/components/modals/expand-pvc-modal.tsx +++ b/frontend/public/components/modals/expand-pvc-modal.tsx @@ -99,7 +99,7 @@ export type ExpandPVCModalProps = { export const ExpandPVCModalProvider: OverlayComponent = (props) => { return ( - + ); }; diff --git a/frontend/public/components/utils/webhooks.tsx b/frontend/public/components/utils/webhooks.tsx index 43b4045e8c4..8f9c72138c6 100644 --- a/frontend/public/components/utils/webhooks.tsx +++ b/frontend/public/components/utils/webhooks.tsx @@ -110,7 +110,9 @@ export const WebhookTriggers: FC = (props) => { setWebhookSecrets(_.compact(secrets)); setLoaded(true); }); - }, [secretNames, isLoaded, canGetSecret, namespace, launchModal]); + // missing launchModal dependency, that causes max depth exceeded error + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [secretNames, isLoaded, canGetSecret, namespace]); if (_.isEmpty(webhookTriggers)) { return null;