Skip to content

Comments

CONSOLE-5012: Migrate confirmModal to overlay pattern#15948

Open
rhamilto wants to merge 1 commit intoopenshift:mainfrom
rhamilto:CONSOLE-5012-5
Open

CONSOLE-5012: Migrate confirmModal to overlay pattern#15948
rhamilto wants to merge 1 commit intoopenshift:mainfrom
rhamilto:CONSOLE-5012-5

Conversation

@rhamilto
Copy link
Member

@rhamilto rhamilto commented Jan 23, 2026

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects with Error('User cancelled') on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook
  • Race condition guards: Only clears launcher if still active
  • Comprehensive JSDoc documentation with examples
  • Warning icon is optional via titleIconVariant prop (defaults to 'warning' per PatternFly)

Unified modal launcher initialization with race condition protection

  • ✅ Refactored SyncErrorModalLauncherSyncModalLaunchers in error-modal-handler.tsx
  • ✅ Single component syncs both error and warning modal launchers
  • Added reference-based cleanup guards to prevent race conditions
  • ✅ Mounted once in app.tsx after OverlayProvider

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display
  • Proper error handling distinguishing user cancellation from actual errors
  • Hides warning icon for move/remove operations via titleIconVariant: null

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:753-761
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod (no references found - 20 lines removed)
  • ✅ Removed getDownloadAllLogsCallback (no references found - 111 lines removed)
  • ✅ Removed unused i18n key "Error downloading logs."

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Throws clear errors when launchers not initialized
  • Race condition protection - Reference-based cleanup prevents clearing newer launchers
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations
  • Well-tested - Comprehensive unit tests for all scenarios
  • Well-documented - Clear JSDoc with examples and proper error types
  • Flexible icon display - Warning icon can be hidden when not needed

Technical Details

Optional Warning Icon

The ControlledWarningModal wrapper now passes through the titleIconVariant prop, allowing callers to:

  • Use default warning icon - Don't specify titleIconVariant (PatternFly defaults to 'warning')
  • Hide the icon - Pass titleIconVariant: null
  • Use a different icon - Pass titleIconVariant: 'danger' | 'success' | 'info' or a custom icon

Example usage in moveNodeToGroup.tsx:

launchWarningModal({
  title: i18next.t(titleKey),
  children: message,
  confirmButtonLabel: i18next.t(btnTextKey),
  titleIconVariant: null, // Hides the warning icon
})

Race Condition Protection

Problem: If components mount/unmount out of order, the cleanup could clear a newer launcher.

Solution: Reference-based cleanup in both launchers (error-modal-handler.tsx:194-205, warning-modal-handler.tsx:278-289):

useEffect(() => {
  const errorModalLauncher = (props: ErrorModalProps) => {
    launcher<ErrorModalProps>(ErrorModal, props);
  };
  moduleErrorModalLauncher = errorModalLauncher;
  
  return () => {
    // Only clear if we're still the active launcher
    if (moduleErrorModalLauncher === errorModalLauncher) {
      moduleErrorModalLauncher = null;
    }
  };
}, [launcher]);

This prevents race conditions where unmounting could clear a newer launcher that was set by a remounting component.

Error Handling Improvements

Promise Rejection with Proper Error Objects:

// Before: rejected with undefined
onClose: () => {
  reject();
}

// After: rejects with Error object
onClose: () => {
  reject(new Error('User cancelled'));
}

This allows downstream code to properly detect user cancellation:

.catch((error) => {
  // Only show error modal if it's not a user cancellation
  if (error.message && error.message !== 'User cancelled') {
    launchErrorModal({ error: error.message });
  }
});

Type Safety for Optional nodeMaintenance

Problem: useStopNodeMaintenanceModal was called with potentially undefined nodeMaintenance but didn't handle it.

Solution: Updated signature and added guards (StopNodeMaintenanceModal.tsx:509-526):

export const useStopNodeMaintenanceModal = (nodeMaintenance: K8sResourceKind | undefined) => {
  const reason = nodeMaintenance ? getNodeMaintenanceReason(nodeMaintenance) : '';
  const nodeName = nodeMaintenance ? getNodeMaintenanceNodeName(nodeMaintenance) : '';
  // ...
  onConfirm: nodeMaintenance
    ? () => k8sKill(getMaintenanceModel(nodeMaintenance), nodeMaintenance)
    : () => Promise.resolve(),
}

Comprehensive Documentation

JSDoc with Clear Type Information:

/**
 * Launch a warning modal from non-React contexts (callbacks, promises, utilities).
 * 
 * @param props - Warning modal properties (title, children, confirmButtonLabel, etc.)
 * @returns Promise that resolves (undefined) when confirmed, rejects with Error('User cancelled') when canceled/closed
 * 
 * @example
 * ```tsx
 * launchWarningModal({
 *   title: 'Delete Resource',
 *   children: 'Are you sure you want to delete this resource?',
 *   confirmButtonLabel: 'Delete',
 * }).then(() => {
 *   // User confirmed - proceed with action
 *   deleteResource();
 * }).catch((error) => {
 *   // User canceled - error.message === 'User cancelled'
 *   console.log('Action cancelled by user');
 * });
 * ```
 */

Test Coverage

6 Comprehensive Test Cases (warning-modal-handler.spec.tsx):

  1. SyncModalLaunchers - should sync the warning launcher on mount
  2. SyncModalLaunchers - should cleanup warning launcher on unmount
  3. useWarningModalLauncher - should return a function that launches warning modals
  4. launchWarningModal - should launch warning modal when launcher is initialized
  5. launchWarningModal - should throw error when launcher is not initialized
  6. launchWarningModal - should resolve promise when onConfirm is called
  7. launchWarningModal - should reject promise with Error when onClose is called

Test Patterns:

  • Follows same structure as error-modal-handler.spec.tsx
  • Uses React Testing Library
  • Mocks useOverlay for isolation
  • Tests both sync and async behaviors
  • Validates error handling and race condition scenarios

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:753-761):

// Blur active element to prevent aria-hidden focus violations when modal opens
// This fixes the browser warning: "Blocked aria-hidden on an element because its
// descendant retained focus" when focus remains on SVG elements during drag-drop
if (
  document.activeElement instanceof HTMLElement ||
  document.activeElement instanceof SVGElement
) {
  document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
  titleKey,
  message,
  btnTextKey,
  close: () => reject(),
  cancel: () => reject(),
  executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
  <Trans ns="topology">
    Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
  </Trans>
) : (
  <Trans ns="topology">
    Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
  </Trans>
);

launchWarningModal({
  title: i18next.t(titleKey),
  children: message,
  confirmButtonLabel: i18next.t(btnTextKey),
  titleIconVariant: null, // Hide the warning icon for simple confirmation
})
  .then(() => updateTopologyResourceApplication(...))
  .catch((err) => {
    if (err.message !== 'User cancelled') {
      launchErrorModal({ error: err.message });
    }
  });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling
  • Proper error type discrimination
  • Optional icon display

Single SyncModalLaunchers Component

// Before: Single error launcher
<OverlayProvider>
  <SyncErrorModalLauncher />
  <YourApp />
</OverlayProvider>

// After: Unified component for both error and warning
<OverlayProvider>
  <SyncModalLaunchers />
  <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:224-227):

export const SyncModalLaunchers = () => {
  useSyncErrorModalLauncher();
  useSyncWarningModalLauncher();
  return null;
};

Mock Architecture & Circular Import Handling

Documentation in Mock (mocks/error-modal-handler.tsx):

The mock provides a simplified SyncModalLaunchers that doesn't call useSyncWarningModalLauncher. This is intentional and acceptable:

  • Mock Purpose: Simplifies tests that don't need real modal rendering
  • Explicit Dependencies: Tests needing warning modals should explicitly mock or import
  • No Circular Import: Import chain is unidirectional (error → warning)
  • Test Isolation: Each test suite mocks only what it needs

Why This Design Works:

  1. Runtime code has clear, unidirectional dependencies
  2. Mocks document their limitations
  3. Tests are explicit about their needs
  4. Follows Jest best practices

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly
  • Unit tests pass (error-modal-handler.spec.tsx: 5/5)
  • New test suite created (warning-modal-handler.spec.tsx: 6 test cases)

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify modal displays without warning icon for move/remove operations
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jan 23, 2026
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Jan 23, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR migrates all instances of confirmModal to use the modern overlay pattern with useWarningModal, completing the modal migration effort.

Changes

  • ✅ Removed deprecated confirmModal launcher and confirm-modal.tsx file
  • ✅ Migrated topology moveNodeToGroup to use useWarningModal with proper cancel handling
  • ✅ Updated StopNodeMaintenanceModal to remove deprecated imperative API
  • ✅ Fixed useWarningModal to properly handle closeOverlay prop
  • ✅ Added try-catch in topology drag-drop to prevent optimistic update on cancel
  • ✅ Updated all metal3-plugin maintenance components to use hook-based approach

Technical Details

Topology Move Node Migration

The topology moveNodeToGroup function used confirmModal imperatively. Since it's not a React component, I implemented a global handler pattern similar to the existing error handler:

  • Added setMoveNodeToGroupConfirmHandler to set a global confirmation handler
  • Created useSetupMoveNodeToGroupHandlers hook that uses useWarningModal
  • Added cancel callback support to properly reject the promise when user cancels
  • Fixed drag-drop component to use try-catch and prevent optimistic update on rejection

StopNodeMaintenanceModal Migration

  • Removed the deprecated stopNodeMaintenanceModal function
  • Kept only the useStopNodeMaintenanceModal hook that already used useWarningModal
  • Updated all call sites to use the hook instead of the imperative function

useWarningModal Fix

  • Fixed closeOverlay prop warning by properly excluding it from the props spread
  • Added closeOverlay() calls in both onClose and onConfirm handlers to properly remove the modal from DOM

Dependencies

This PR builds upon and depends on:

Test Plan

  • Verify topology node drag-and-drop with confirmation works
  • Verify clicking "Cancel" restores node to original position
  • Verify clicking confirm button moves the node
  • Verify metal3-plugin node maintenance modals work
  • Verify no React DOM warnings about closeOverlay prop
  • Verify modal properly closes and removes from DOM on both confirm and cancel

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto rhamilto changed the title CONSOLE-5012: Migrate confirmModal to overlay pattern [WIP] CONSOLE-5012: Migrate confirmModal to overlay pattern Jan 23, 2026
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 23, 2026
@openshift-ci openshift-ci bot requested review from jhadvig and sg00dwin January 23, 2026 20:59
@openshift-ci openshift-ci bot added component/core Related to console core functionality component/dev-console Related to dev-console approved Indicates a PR has been approved by an approver from all required OWNERS files. component/knative Related to knative-plugin component/metal3 Related to metal3-plugin component/sdk Related to console-plugin-sdk component/shared Related to console-shared component/topology Related to topology kind/i18n Indicates issue or PR relates to internationalization or has content that needs to be translated labels Jan 23, 2026
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Jan 23, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR migrates all instances of confirmModal to use the modern overlay pattern with useWarningModal, completing the modal migration effort.

Changes

  • ✅ Removed deprecated confirmModal launcher and confirm-modal.tsx file
  • ✅ Migrated topology moveNodeToGroup to use useWarningModal with proper cancel handling
  • ✅ Updated StopNodeMaintenanceModal to remove deprecated imperative API
  • ✅ Fixed useWarningModal to properly handle closeOverlay prop
  • ✅ Added try-catch in topology drag-drop to prevent optimistic update on cancel
  • ✅ Updated all metal3-plugin maintenance components to use hook-based approach

Technical Details

Topology Move Node Migration

The topology moveNodeToGroup function used confirmModal imperatively. Since it's not a React component, I implemented a global handler pattern similar to the existing error handler:

  • Added setMoveNodeToGroupConfirmHandler to set a global confirmation handler
  • Created useSetupMoveNodeToGroupHandlers hook that uses useWarningModal
  • Added cancel callback support to properly reject the promise when user cancels
  • Fixed drag-drop component to use try-catch and prevent optimistic update on rejection

StopNodeMaintenanceModal Migration

  • Removed the deprecated stopNodeMaintenanceModal function
  • Kept only the useStopNodeMaintenanceModal hook that already used useWarningModal
  • Updated all call sites to use the hook instead of the imperative function

useWarningModal Fix

  • Fixed closeOverlay prop warning by properly excluding it from the props spread
  • Added closeOverlay() calls in both onClose and onConfirm handlers to properly remove the modal from DOM

Dependencies

This PR builds upon and depends on:

Test Plan

  • Verify topology node drag-and-drop with confirmation works
  • Verify clicking "Cancel" restores node to original position
  • Verify clicking confirm button moves the node
  • Verify metal3-plugin node maintenance modals work
  • Verify no React DOM warnings about closeOverlay prop
  • Verify modal properly closes and removes from DOM on both confirm and cancel

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

  • Introduced global error modal handler for consistent error notifications across the application.

  • Implemented lazy-loading for modals to improve initial page load performance.

  • Bug Fixes

  • Enhanced modal overlay management for improved accessibility and proper focus handling.

  • Refactor

  • Consolidated modal system to use provider-based architecture.

  • Optimized component memoization to reduce unnecessary re-renders.

  • Simplified modal invocation patterns across the application.

✏️ Tip: You can customize this high-level summary in your review settings.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a modal infrastructure refactor across the OpenShift Console codebase, shifting from a direct modal launcher pattern (via createModalLauncher) to an overlay-based provider pattern with React lazy loading. The changes include: introducing OverlayComponent-based modal providers wrapping existing modal components in ModalWrapper; implementing useOverlay() hooks to obtain launchModal functions for programmatic modal invocation; consolidating error handling through a new global error modal handler utility; creating lazy-loaded modal exports for code-splitting; removing launchModal from dependency arrays with ESLint suppressions to address potential max-depth errors; and updating action creators and modal invocation sites across frontend packages to use the new launchModal(ProviderComponent, props) pattern instead of direct function calls.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'CONSOLE-5012: Migrate confirmModal to overlay pattern' accurately summarizes the primary change—removing the legacy confirmModal launcher and migrating it to the overlay/useWarningModal pattern.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
frontend/packages/console-shared/src/components/formik-fields/EnvironmentField.tsx (1)

64-80: Stabilize launchModal reference instead of suppressing hook deps

The dependency exhaustive-deps suppression masks a real issue: useOverlay() returns an unstable reference because OverlayContext.Provider creates a new value object on every render, even though the underlying callback is memoized. Adding launchModal to deps would trigger infinite re-runs (max depth exceeded), so the ref pattern below is the right approach at the consumer level.

♻️ Proposed refactor
-import { useMemo, useState, useCallback, useEffect } from 'react';
+import { useMemo, useState, useCallback, useEffect, useRef } from 'react';

 const { t } = useTranslation();
 const launchModal = useOverlay();
+const launchModalRef = useRef(launchModal);
+useEffect(() => {
+  launchModalRef.current = launchModal;
+}, [launchModal]);

 useEffect(() => {
   Promise.all([k8sGet(ConfigMapModel, null, namespace), k8sGet(SecretModel, null, namespace)])
     .then(([nsConfigMaps, nsSecrets]) => {
       setConfigMaps(nsConfigMaps);
       setSecrets(nsSecrets);
     })
     .catch(async (err) => {
       if (err?.response?.status !== 403) {
         try {
-          await launchModal(ErrorModal, { error: err?.message });
+          await launchModalRef.current(ErrorModal, { error: err?.message });
           // eslint-disable-next-line no-empty
         } catch (e) {}
       }
     });
-  // missing launchModal dependency, that causes max depth exceeded error
-  // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [namespace]);
frontend/packages/topology/src/utils/moveNodeToGroup.tsx (1)

41-108: Fix the type contract: targetGroup should be optional and updateTopologyResourceApplication must accept string | null.

The function declares targetGroup: Node as required, but the implementation treats it as optional (see line 54's targetGroup?.getLabel() and line 58's conditional check). Line 82 passes null to updateTopologyResourceApplication, which expects application: string per the function signature in topology-utils.ts:137–140. Additionally, line 102 will crash if sourceGroup is falsy and targetGroup is somehow undefined, calling .getLabel() on a falsy value.

Update the type signature to match the implementation, and widen updateTopologyResourceApplication's application parameter to string | null to safely handle the "remove from application" case:

Proposed fix
export const moveNodeToGroup = (
  node: Node,
-  targetGroup: Node,
+  targetGroup: Node | null,
  onError?: (error: string) => void,
): Promise<void> => {

Then in topology-utils.ts:

export const updateTopologyResourceApplication = (
  item: Node,
-  application: string,
+  application: string | null,
): Promise<any> => {

and add an early guard:

  const itemData = item?.getData();
  const resource = getResource(item);
-  if (!item || !resource || !_.size(itemData.resources)) {
+  if (!item || !resource || application === null || !_.size(itemData.resources)) {
     return Promise.reject();
   }
frontend/packages/shipwright-plugin/src/actions/useBuildRunActions.ts (1)

4-4: Typo: Double slash in import path.

The import path contains actions//hooks with a double slash. While most bundlers normalize this, it's a minor inconsistency that should be cleaned up.

Suggested fix
-import { useCommonResourceActions } from '@console/app/src/actions//hooks/useCommonResourceActions';
+import { useCommonResourceActions } from '@console/app/src/actions/hooks/useCommonResourceActions';
frontend/packages/shipwright-plugin/src/actions/useBuildActions.ts (1)

75-89: Remove the duplicate Delete action at line 89.

The useCommonResourceActions hook returns all common actions including Delete via Object.values(commonActions) at line 75. Explicitly pushing commonActions.Delete again at line 89 creates a duplicate entry in the actions menu.

🤖 Fix all issues with AI agents
In
`@frontend/packages/console-app/src/components/modals/clone/clone-pvc-modal.tsx`:
- Around line 279-283: The spread props currently come after explicit close
props in ClonePVCModalProvider which allows props.closeOverlay to be overridden;
update the JSX so {...props} is spread first and then pass
cancel={props.closeOverlay} and close={props.closeOverlay} (affecting the
ClonePVCModal inside ModalWrapper) to ensure closeOverlay always wins and cannot
be overridden by incoming props.

In
`@frontend/packages/console-app/src/components/modals/restore-pvc/restore-pvc-modal.tsx`:
- Around line 282-287: The provider currently spreads props after passing
cancel/close to RestorePVCModal, allowing a caller to override those with their
own cancel/close; change RestorePVCModalProvider so you destructure closeOverlay
from props first (e.g., const { closeOverlay, ...rest } = props) then render
<RestorePVCModal cancel={closeOverlay} close={closeOverlay} {...rest} /> inside
ModalWrapper, ensuring props.closeOverlay cannot be overridden by
caller-supplied props.

In
`@frontend/packages/console-app/src/components/nodes/modals/ConfigureUnschedulableModal.tsx`:
- Around line 51-64: The provider currently spreads props after assigning cancel
and close which allows caller props to override them; update
ConfigureUnschedulableModalProvider to destructure props into { closeOverlay,
...rest } (or extract known keys) and then render ConfigureUnschedulableModal
with {...rest} followed by explicit cancel={closeOverlay} and
close={closeOverlay} so the overlay's close handlers cannot be overridden;
ensure ModalWrapper still uses closeOverlay for onClose and pass the same
closeOverlay to ConfigureUnschedulableModal.

In `@frontend/packages/console-shared/src/utils/error-modal-handler.ts`:
- Around line 35-50: The cleanup unconditionally clears the global launcher
causing newer launchers to be removed; fix useSetupGlobalErrorModalLauncher by
capturing the specific errorModalLauncher reference you set via
setGlobalErrorModalLauncher and, in the return cleanup, only call
setGlobalErrorModalLauncher(null) if the current global launcher equals that
captured reference (so use a stable local/ref for errorModalLauncher and guard
the cleanup), referencing the existing functions errorModalLauncher,
useSetupGlobalErrorModalLauncher, and setGlobalErrorModalLauncher to locate and
implement the guarded cleanup.

In `@frontend/packages/knative-plugin/src/actions/creators.ts`:
- Around line 4-5: The SDK currently forces imports from an internal path to
reach useOverlay; update the SDK public exports so useOverlay can be imported
from '@console/dynamic-plugin-sdk' by re-exporting the modal overlay API: add an
export for the modal-support module (e.g., export * from './modal-support') from
the SDK app index or alternatively export the dynamic-core-api module from the
SDK root so that the symbol useOverlay is publicly available; modify the module
that aggregates app exports to re-export useOverlay (and related modal types) so
consumers can import { useOverlay } from '@console/dynamic-plugin-sdk'.

In `@frontend/public/components/modals/configure-update-strategy-modal.tsx`:
- Around line 241-250: The provider currently spreads {...props} after setting
cancel/close so a caller can override cancel/close; update
ConfigureUpdateStrategyModalProvider to spread props first into
ConfigureUpdateStrategyModal (or omit cancel/close from the spread) and then
pass cancel={props.closeOverlay} and close={props.closeOverlay} to enforce using
props.closeOverlay; locate the ConfigureUpdateStrategyModalProvider and
ConfigureUpdateStrategyModal usages to apply this change so closeOverlay cannot
be overridden by caller props.

In `@frontend/public/components/modals/delete-modal.tsx`:
- Around line 183-188: The DeleteModalProvider currently spreads props after
passing cancel and close, allowing a caller to override cancel/close; in the
DeleteModalProvider component (and the ModalWrapper usage) spread {...props}
first and then pass cancel={props.closeOverlay} and close={props.closeOverlay}
so the enforced overlay handlers cannot be overridden by caller-provided props
(refer to DeleteModalProvider, ModalWrapper, DeleteModal, and
props.closeOverlay).

In `@frontend/public/components/modals/delete-pvc-modal.tsx`:
- Around line 87-91: The DeletePVCModalProvider currently spreads {...props}
after explicit cancel/close props which allows callers to override
props.closeOverlay; change the prop order in the JSX so you spread {...props}
first and then pass cancel={props.closeOverlay} close={props.closeOverlay} (on
ModalWrapper keep onClose={props.closeOverlay}) to ensure DeletePVCModal and
ModalWrapper always use props.closeOverlay for teardown and cannot be overridden
by incoming props.
♻️ Duplicate comments (2)
frontend/packages/console-app/src/actions/hooks/useJobActions.ts (1)

55-57: Same launcher-dep suppression pattern—please confirm stability.

If useConfigureCountModal() returns a new function over time, omitting it from deps can leave the action stale. This mirrors the earlier launcher-dep concern.

frontend/packages/console-app/src/actions/providers/build-config-provider.ts (1)

36-38: Launcher-dep suppression repeated here—confirm the hook’s launcher is stable.

Same dependency omission pattern as in other action hooks; please ensure the launcher identity is stable or the action can become stale.

Also applies to: 71-73

🧹 Nitpick comments (16)
frontend/public/components/utils/webhooks.tsx (1)

90-115: The isLoaded dependency may cause an unnecessary double-fetch.

The effect sets isLoaded = true at line 111, and isLoaded is also listed in the dependency array. On initial mount:

  1. isLoaded is false → effect runs → secrets fetched → setLoaded(true)
  2. isLoaded changes to true → effect re-runs → secrets fetched again

Consider adding a guard or removing isLoaded from deps if it's only meant to track completion:

  useEffect(() => {
-   if (!canGetSecret) {
+   if (!canGetSecret || isLoaded) {
      return;
    }

Alternatively, if the intent is to allow re-fetching when secretNames change, isLoaded should be removed from the dependency array entirely—it's not a meaningful trigger.


On the launchModal suppression: I see this pattern across the PR for avoiding the max-depth error. For long-term maintainability, the useOverlay hook should ideally return a stable (memoized) function reference. If that's not feasible right now, a useRef wrapper is a cleaner alternative to eslint-disable:

const launchModalRef = useRef(launchModal);
launchModalRef.current = launchModal;
// then use launchModalRef.current inside the effect

This keeps the linter happy and documents the intent more explicitly than a suppression comment.

frontend/packages/console-shared/src/hooks/useWarningModal.tsx (1)

37-52: Consider documenting memoization expectations.

The dependency array at line 50 includes props, which is an object reference. If callers pass inline objects (e.g., useWarningModal({ title: 'Title' })), the callback will be recreated on every render. This is fine for most use cases, but callers needing a stable callback reference should memoize their props.

Consider adding a note in the JSDoc (around line 30-36) mentioning this behavior, especially since other parts of the codebase may rely on stable callback references for effect dependencies.

📝 Suggested documentation addition
 /**
  * useWarningModal is a hook that provides a way to launch a WarningModal.
  * Supports two usage patterns:
  * - Pass props at hook initialization time: useWarningModal({ title: 'Title' })
  * - Pass props in the callback: const launch = useWarningModal(); launch({ title: 'Title' })
  * - Mix both (overrides have priority): const launch = useWarningModal({ title: 'Title' }); launch({ children: <Content /> })
+ *
+ * Note: If a stable callback reference is required, memoize the props object passed
+ * to this hook using useMemo to prevent the callback from being recreated on each render.
  */
frontend/packages/knative-plugin/src/components/add/SecretKeySelector.tsx (1)

51-63: Confirm useOverlay() provides a stable function or capture it safely.
Line 51–63 suppresses exhaustive-deps and drops launchModal. If useOverlay() ever returns a new function (e.g., provider change), this effect will keep a stale launcher and may miss error modals. Please confirm stability; if not guaranteed, capture it via a ref while keeping the namespace-only fetch behavior.

♻️ Optional safer pattern (ref keeps latest launcher)
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
...
-  const launchModal = useOverlay();
+  const launchModal = useOverlay();
+  const launchModalRef = useRef(launchModal);
+  useEffect(() => {
+    launchModalRef.current = launchModal;
+  }, [launchModal]);
...
-        if (err?.response?.status !== 403) {
-          launchModal(ErrorModal, { error: err?.message });
-        }
+        if (err?.response?.status !== 403) {
+          launchModalRef.current(ErrorModal, { error: err?.message });
+        }
frontend/packages/metal3-plugin/src/components/maintenance/actions.tsx (2)

42-42: Minor inconsistency in CTA assignment pattern

Line 34 assigns cta: startNodeMaintenanceModal directly as a function reference, whereas here you wrap with cta: () => stopNodeMaintenanceModalLauncher(). Both work, but the inconsistency is a bit puzzling.

If both launchers have the same signature (no-arg functions returning void/Promise), prefer consistency—either direct reference for both or arrow wrappers for both. This aids readability and avoids questions about subtle differences.

♻️ Suggested alignment (if signatures match)
-        cta: () => stopNodeMaintenanceModalLauncher(),
+        cta: stopNodeMaintenanceModalLauncher,

27-28: Guard the useStopNodeMaintenanceModal hook call or add undefined handling

findNodeMaintenance returns undefined when no matching maintenance is found, yet useStopNodeMaintenanceModal(nodeMaintenance) is invoked unconditionally at line 28. The hook immediately computes modal content by calling getNodeMaintenanceReason(nodeMaintenance) and getNodeMaintenanceNodeName(nodeMaintenance) with potentially undefined data. Additionally, getMaintenanceModel(nodeMaintenance) (invoked in the onConfirm callback) accesses nodeMaintenance.apiVersion without checking if it exists.

While the launcher is only invoked when nodeMaintenance is truthy (line 38), this pattern wastes renders and violates the hook's contract—the type signature doesn't reflect that undefined is acceptable. This can be fragile if the selector functions don't handle undefined gracefully.

Either guard the hook call with a conditional or update useStopNodeMaintenanceModal to explicitly handle and return a no-op launcher when nodeMaintenance is undefined.

frontend/packages/knative-plugin/src/actions/creators.ts (1)

130-130: Unnecessary template literal.

Use a plain string literal here since there's no interpolation.

✏️ Suggested fix
-      id: `delete-resource`,
+      id: 'delete-resource',
frontend/packages/console-app/src/actions/hooks/useRetryRolloutAction.ts (1)

98-100: Re-enable react-hooks/exhaustive-deps and include launchModal in the dependency array.

useOverlay returns a stable launcher—launchOverlay is memoized with useCallback(..., []) in OverlayProvider (line 40), so its identity never changes. The suppression is unnecessary; including it in the deps array will not cause infinite loops. Keeping the lint rule enabled ensures future maintainers catch unintended stale closures.

frontend/packages/console-app/src/actions/hooks/useCommonActions.ts (1)

47-85: Avoid suppressing exhaustive-deps; verify useOverlay stability.

launchModal is omitted from deps with an eslint-disable. If useOverlay ever returns a new function, this can capture a stale launcher. Prefer stabilizing the hook output (or memoizing) so the dependency can be included and the suppression removed.

Also applies to: 174-176

frontend/packages/console-app/src/actions/hooks/usePVCActions.ts (1)

7-11: Inconsistent lazy-loading for modal providers.

LazyExpandPVCModalProvider and LazyClonePVCModalProvider are lazy-loaded, but DeletePVCModalProvider is imported directly. For consistency and code-splitting benefits, consider using a lazy-loaded variant for DeletePVCModalProvider as well.

Does React lazy loading improve bundle size for modals?
frontend/public/components/modals/managed-resource-save-modal.tsx (1)

53-54: Consider excluding closeOverlay from prop spread.

The {...props} spread will pass closeOverlay to the inner ManagedResourceSaveModal, which doesn't use it (it uses close instead). While React ignores unknown props on function components, explicitly destructuring to exclude closeOverlay would be cleaner:

♻️ Suggested refinement
 export const ManagedResourceSaveModalProvider: OverlayComponent<ManagedResourceSaveModalProps> = (
   props,
 ) => {
+  const { closeOverlay, ...rest } = props;
   return (
-    <ModalWrapper blocking onClose={props.closeOverlay}>
-      <ManagedResourceSaveModal close={props.closeOverlay} {...props} />
+    <ModalWrapper blocking onClose={closeOverlay}>
+      <ManagedResourceSaveModal close={closeOverlay} {...rest} />
     </ModalWrapper>
   );
 };
frontend/public/components/namespace.jsx (1)

878-882: Simplify: pullSecret: undefined is redundant.

Passing pullSecret: undefined explicitly is unnecessary since the prop is already optional in ConfigureNamespacePullSecretProps. This adds noise without semantic value.

♻️ Suggested simplification
   const modal = () =>
     launchModal(LazyConfigureNamespacePullSecretModalProvider, {
       namespace,
-      pullSecret: undefined,
     });
frontend/packages/console-app/src/components/modals/resource-limits/index.ts (1)

4-9: The .then(m => ({ default: m.default })) is redundant.

Since ResourceLimitsModalLauncher already exports ResourceLimitsModalProvider as its default export, the transform is unnecessary. React.lazy expects a module with a default export, which is already satisfied.

♻️ Simplified lazy export
 export const LazyResourceLimitsModalProvider = lazy(() =>
-  import('./ResourceLimitsModalLauncher' /* webpackChunkName: "resource-limits-modal" */).then(
-    (m) => ({
-      default: m.default,
-    }),
-  ),
+  import('./ResourceLimitsModalLauncher' /* webpackChunkName: "resource-limits-modal" */),
 );

That said, if this pattern is intentional for consistency across the codebase or to handle potential named-export scenarios in the future, feel free to keep it.

frontend/packages/dev-console/src/actions/context-menu.ts (1)

14-40: Inconsistency: DeleteApplicationAction still uses i18next.t() directly.

This function uses i18next.t() for translations (line 23), while the new useDeleteResourceAction hook uses useTranslation(). Since DeleteApplicationAction is not a hook, it can't use useTranslation, but this creates an inconsistency in how translations are handled within the same file.

Consider migrating DeleteApplicationAction to a hook (useDeleteApplicationAction) in a future iteration to align with the hook-based pattern if the broader migration effort supports it.

frontend/packages/console-app/src/components/modals/resource-limits/ResourceLimitsModalLauncher.tsx (1)

64-70: Prop spread order could inadvertently override close.

The spread {...props} after close={props.closeOverlay} means if props somehow contains a close property, it would override the intended value. While OverlayComponent props shouldn't include close (they provide closeOverlay), defensive ordering is safer:

♻️ Defensive prop ordering
 export const ResourceLimitsModalProvider: OverlayComponent<ResourceLimitsModalLauncherProps> = (
   props,
 ) => (
   <ModalWrapper blocking onClose={props.closeOverlay}>
-    <ResourceLimitsModalLauncher close={props.closeOverlay} {...props} />
+    <ResourceLimitsModalLauncher {...props} close={props.closeOverlay} />
   </ModalWrapper>
 );

This ensures close is always bound to closeOverlay regardless of what's in props.

frontend/public/components/modals/configure-ns-pull-secret-modal.tsx (1)

317-329: Same prop spread order concern; also closeOverlay leaks into child props.

Two observations:

  1. Prop ordering: The spread {...props} after explicit cancel and close assignments could override them if props contains those keys.

  2. closeOverlay prop leakage: The OverlayComponent type injects closeOverlay into props. Spreading {...props} passes closeOverlay to ConfigureNamespacePullSecret, which doesn't expect it. This may cause a React warning about unknown DOM attributes if the prop propagates further.

♻️ Destructure to exclude closeOverlay and fix ordering
-export const ConfigureNamespacePullSecretModalProvider: OverlayComponent<ConfigureNamespacePullSecretProps> = (
-  props,
-) => {
+export const ConfigureNamespacePullSecretModalProvider: OverlayComponent<ConfigureNamespacePullSecretProps> = ({
+  closeOverlay,
+  ...restProps
+}) => {
   return (
-    <ModalWrapper blocking onClose={props.closeOverlay}>
+    <ModalWrapper blocking onClose={closeOverlay}>
       <ConfigureNamespacePullSecret
-        cancel={props.closeOverlay}
-        close={props.closeOverlay}
-        {...props}
+        {...restProps}
+        cancel={closeOverlay}
+        close={closeOverlay}
       />
     </ModalWrapper>
   );
 };

This prevents closeOverlay from leaking and ensures cancel/close are always correctly bound.

frontend/public/components/modals/index.ts (1)

87-92: Minor formatting inconsistency.

LazyClonePVCModalProvider and LazyRestorePVCModalProvider use single-line .then((m) => ({ default: m.default })) while the other six lazy providers use multi-line formatting. Consider aligning for consistency and easier diffing across providers.

📝 Suggested formatting alignment
 // 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) => ({ default: m.default })),
+  ).then((m) => ({
+    default: m.default,
+  })),
 );
 // 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) => ({ default: m.default })),
+  ).then((m) => ({
+    default: m.default,
+  })),
 );

Also applies to: 113-118

@rhamilto
Copy link
Member Author

/retest

2 similar comments
@rhamilto
Copy link
Member Author

/retest

@rhamilto
Copy link
Member Author

/retest

@rhamilto rhamilto force-pushed the CONSOLE-5012-5 branch 3 times, most recently from d102d05 to 274b735 Compare January 27, 2026 20:39
@rhamilto
Copy link
Member Author

/retest

1 similar comment
@rhamilto
Copy link
Member Author

/retest

@rhamilto
Copy link
Member Author

/retest

@rhamilto
Copy link
Member Author

rhamilto commented Feb 2, 2026

/assign @yanpzhan
/assign @vojtechszocs

Tech debt, so assigning labels
/label px-approved
/label docs-approved

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Note: This PR builds on:

Changes

Removed deprecated confirmModal

  • ✅ Removed confirmModal usage from moveNodeToGroup.tsx
  • ✅ Removed confirmModal from modals/index.ts exports
  • ✅ Eliminated createModalLauncher pattern usage

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal() from CONSOLE-5012: Create shared error modal launcher using React Context #15946
  • ✅ Promise-based API: resolves on confirm, rejects on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Code cleanup

  • ✅ Removed incomplete test file (moveNodeToGroup.spec.tsx) that tested non-existent functionality
  • ✅ Removed non-existent component imports from Topology.tsx
  • ✅ Converted JSX <Trans> to i18next.t() calls in moveNodeToGroup (required for imperative API)

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations

Technical Details

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal:

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement || 
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal
launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation:

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto rhamilto force-pushed the CONSOLE-5012-5 branch 3 times, most recently from cfb2db3 to 6ba05ee Compare February 18, 2026 15:03
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Note: This PR builds on:

Changes

Removed deprecated confirmModal

  • ✅ Removed confirmModal usage from moveNodeToGroup.tsx
  • ✅ Removed confirmModal from modals/index.ts exports
  • ✅ Eliminated createModalLauncher pattern usage

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal() from CONSOLE-5012: Create shared error modal launcher using React Context #15946
  • ✅ Promise-based API: resolves on confirm, rejects on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Code cleanup

  • ✅ Removed incomplete test file (moveNodeToGroup.spec.tsx) that tested non-existent functionality
  • ✅ Removed non-existent component imports from Topology.tsx
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Updated i18n translation keys to include <1> placeholders for formatting
  • ✅ Consolidated mock files (removed duplicate root-level mocks)
  • ✅ Removed orphaned functions: getOrderedStepsFromPod and getDownloadAllLogsCallback

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations

Technical Details

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal:

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement || 
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation:

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Note: This PR builds on:

Changes

Removed deprecated confirmModal

  • ✅ Removed confirmModal usage from moveNodeToGroup.tsx
  • ✅ Removed confirmModal from modals/index.ts exports
  • ✅ Eliminated createModalLauncher pattern usage

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal() from CONSOLE-5012: Create shared error modal launcher using React Context #15946
  • ✅ Promise-based API: resolves on confirm, rejects on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Code cleanup

  • ✅ Removed incomplete test file (moveNodeToGroup.spec.tsx) that tested non-existent functionality
  • ✅ Removed non-existent component imports from Topology.tsx
  • ✅ Consolidated mock files (removed duplicate root-level mocks)
  • ✅ Removed orphaned functions: getOrderedStepsFromPod and getDownloadAllLogsCallback

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations

Technical Details

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal:

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement || 
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation:

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component in error-modal-handler.tsx that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:41-49
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

  • ✅ Updated StopNodeMaintenanceModal.tsx to use useWarningModal hook
  • ✅ Updated maintenance action popover components to use useWarningModal hook
  • ✅ Removed unused i18n keys

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod and getDownloadAllLogsCallback (no references found)
  • ✅ Removed unused i18n keys

Updated i18n translations

  • ✅ Updated topology translation keys to include <1> placeholders for <strong> tags
  • ✅ Example: "Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations

Technical Details

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:41-49):

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement ||
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:42-46):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto
Copy link
Member Author

@logonoff, please take another look. Ended up refactoring so we only have a single SyncModalLaunchers component for both error and confirm modals.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component in error-modal-handler.tsx that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:41-49
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

  • ✅ Updated StopNodeMaintenanceModal.tsx to use useWarningModal hook
  • ✅ Updated maintenance action popover components to use useWarningModal hook
  • ✅ Removed unused i18n keys

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod and getDownloadAllLogsCallback (no references found)
  • ✅ Removed unused i18n keys

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations

Technical Details

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:41-49):

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement ||
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:42-46):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects with Error('User cancelled') on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook
  • Comprehensive JSDoc documentation with examples

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component in error-modal-handler.tsx that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider
  • ✅ Deprecated SyncErrorModalLauncher as alias for backwards compatibility

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display
  • Proper error handling distinguishing user cancellation from actual errors

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:41-49
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

  • ✅ Updated StopNodeMaintenanceModal.tsx to use useWarningModal hook
  • ✅ Updated maintenance action popover components to use useWarningModal hook
  • ✅ Removed unused i18n keys

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod and getDownloadAllLogsCallback (no references found)
  • ✅ Removed unused i18n keys

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations
  • Well-tested - Comprehensive unit tests for all scenarios
  • Well-documented - Clear JSDoc with examples and proper error types

Technical Details

Error Handling Improvements

Promise Rejection with Proper Error Objects:

// Before: rejected with undefined
onClose: () => {
 reject();
}

// After: rejects with Error object
onClose: () => {
 reject(new Error('User cancelled'));
}

This allows downstream code to properly detect user cancellation:

.catch((error) => {
 // Only show error modal if it's not a user cancellation
 if (error.message && error.message !== 'User cancelled') {
   launchErrorModal({ error: error.message });
 }
});

Comprehensive Documentation

JSDoc with Clear Type Information:

/**
* Launch a warning modal from non-React contexts (callbacks, promises, utilities).
* 
* @param props - Warning modal properties (title, children, confirmButtonLabel, etc.)
* @returns Promise that resolves (undefined) when confirmed, rejects with Error('User cancelled') when canceled/closed
* 
* @example
* ```tsx
* launchWarningModal({
*   title: 'Delete Resource',
*   children: 'Are you sure you want to delete this resource?',
*   confirmButtonLabel: 'Delete',
* }).then(() => {
*   // User confirmed - proceed with action
*   deleteResource();
* }).catch((error) => {
*   // User canceled - error.message === 'User cancelled'
*   console.log('Action cancelled by user');
* });
* ```
*/

Test Coverage

6 Comprehensive Test Cases (warning-modal-handler.spec.tsx):

  1. SyncModalLaunchers - should sync the warning launcher on mount
  2. SyncModalLaunchers - should cleanup warning launcher on unmount
  3. useWarningModalLauncher - should return a function that launches warning modals
  4. launchWarningModal - should launch warning modal when launcher is initialized
  5. launchWarningModal - should throw error when launcher is not initialized
  6. launchWarningModal - should resolve promise when onConfirm is called
  7. launchWarningModal - should reject promise with Error when onClose is called

Test Patterns:

  • Follows same structure as error-modal-handler.spec.tsx
  • Uses React Testing Library
  • Mocks useOverlay for isolation
  • Tests both sync and async behaviors
  • Validates error handling

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:41-49):

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement ||
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling
  • Proper error type discrimination

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:42-46):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

Mock Architecture & Circular Import Handling

Documentation in Mock (mocks/error-modal-handler.tsx):

The mock provides a simplified SyncModalLaunchers that doesn't call useSyncWarningModalLauncher. This is intentional and acceptable:

  • Mock Purpose: Simplifies tests that don't need real modal rendering
  • Explicit Dependencies: Tests needing warning modals should explicitly mock or import
  • No Circular Import: Import chain is unidirectional (error → warning)
  • Test Isolation: Each test suite mocks only what it needs

Why This Design Works:

  1. Runtime code has clear, unidirectional dependencies
  2. Mocks document their limitations
  3. Tests are explicit about their needs
  4. Follows Jest best practices

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly
  • Unit tests pass (error-modal-handler.spec.tsx: 5/5)
  • New test suite created (warning-modal-handler.spec.tsx: 6 test cases)

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects with Error('User cancelled') on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook
  • Comprehensive JSDoc documentation with examples

Unified modal launcher initialization

  • ✅ Created single SyncModalLaunchers component in error-modal-handler.tsx that syncs both error and warning modal launchers
  • ✅ Replaces two separate sync components with one unified component
  • ✅ Mounted once in app.tsx after OverlayProvider

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display
  • Proper error handling distinguishing user cancellation from actual errors

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:41-49
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

  • ✅ Updated StopNodeMaintenanceModal.tsx to use useWarningModal hook
  • ✅ Updated maintenance action popover components to use useWarningModal hook
  • ✅ Removed unused i18n keys

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod and getDownloadAllLogsCallback (no references found)
  • ✅ Removed unused i18n keys

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Clear error messages when launchers not initialized
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations
  • Well-tested - Comprehensive unit tests for all scenarios
  • Well-documented - Clear JSDoc with examples and proper error types

Technical Details

Error Handling Improvements

Promise Rejection with Proper Error Objects:

// Before: rejected with undefined
onClose: () => {
 reject();
}

// After: rejects with Error object
onClose: () => {
 reject(new Error('User cancelled'));
}

This allows downstream code to properly detect user cancellation:

.catch((error) => {
 // Only show error modal if it's not a user cancellation
 if (error.message && error.message !== 'User cancelled') {
   launchErrorModal({ error: error.message });
 }
});

Comprehensive Documentation

JSDoc with Clear Type Information:

/**
* Launch a warning modal from non-React contexts (callbacks, promises, utilities).
* 
* @param props - Warning modal properties (title, children, confirmButtonLabel, etc.)
* @returns Promise that resolves (undefined) when confirmed, rejects with Error('User cancelled') when canceled/closed
* 
* @example
* ```tsx
* launchWarningModal({
*   title: 'Delete Resource',
*   children: 'Are you sure you want to delete this resource?',
*   confirmButtonLabel: 'Delete',
* }).then(() => {
*   // User confirmed - proceed with action
*   deleteResource();
* }).catch((error) => {
*   // User canceled - error.message === 'User cancelled'
*   console.log('Action cancelled by user');
* });
* ```
*/

Test Coverage

6 Comprehensive Test Cases (warning-modal-handler.spec.tsx):

  1. SyncModalLaunchers - should sync the warning launcher on mount
  2. SyncModalLaunchers - should cleanup warning launcher on unmount
  3. useWarningModalLauncher - should return a function that launches warning modals
  4. launchWarningModal - should launch warning modal when launcher is initialized
  5. launchWarningModal - should throw error when launcher is not initialized
  6. launchWarningModal - should resolve promise when onConfirm is called
  7. launchWarningModal - should reject promise with Error when onClose is called

Test Patterns:

  • Follows same structure as error-modal-handler.spec.tsx
  • Uses React Testing Library
  • Mocks useOverlay for isolation
  • Tests both sync and async behaviors
  • Validates error handling

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:41-49):

// Blur active element to prevent aria-hidden focus violations when modal opens
if (document.activeElement instanceof HTMLElement ||
   document.activeElement instanceof SVGElement) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling
  • Proper error type discrimination

Single SyncModalLaunchers Component

// Before: Two separate sync components
<OverlayProvider>
 <SyncErrorModalLauncher />
 <SyncWarningModalLauncher />
 <YourApp />
</OverlayProvider>

// After: One unified component
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:42-46):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

Mock Architecture & Circular Import Handling

Documentation in Mock (mocks/error-modal-handler.tsx):

The mock provides a simplified SyncModalLaunchers that doesn't call useSyncWarningModalLauncher. This is intentional and acceptable:

  • Mock Purpose: Simplifies tests that don't need real modal rendering
  • Explicit Dependencies: Tests needing warning modals should explicitly mock or import
  • No Circular Import: Import chain is unidirectional (error → warning)
  • Test Isolation: Each test suite mocks only what it needs

Why This Design Works:

  1. Runtime code has clear, unidirectional dependencies
  2. Mocks document their limitations
  3. Tests are explicit about their needs
  4. Follows Jest best practices

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly
  • Unit tests pass (error-modal-handler.spec.tsx: 5/5)
  • New test suite created (warning-modal-handler.spec.tsx: 6 test cases)

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto rhamilto force-pushed the CONSOLE-5012-5 branch 2 times, most recently from 4481a7b to eb9b49f Compare February 18, 2026 16:25
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects with Error('User cancelled') on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook
  • Race condition guards: Only clears launcher if still active
  • Comprehensive JSDoc documentation with examples

Unified modal launcher initialization with race condition protection

  • ✅ Refactored SyncErrorModalLauncherSyncModalLaunchers in error-modal-handler.tsx
  • ✅ Single component syncs both error and warning modal launchers
  • Added reference-based cleanup guards to prevent race conditions
  • ✅ Mounted once in app.tsx after OverlayProvider

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display
  • Proper error handling distinguishing user cancellation from actual errors

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:753-761
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod (no references found - 20 lines removed)
  • ✅ Removed getDownloadAllLogsCallback (no references found - 111 lines removed)
  • ✅ Removed unused i18n key "Error downloading logs."

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Throws clear errors when launchers not initialized
  • Race condition protection - Reference-based cleanup prevents clearing newer launchers
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations
  • Well-tested - Comprehensive unit tests for all scenarios
  • Well-documented - Clear JSDoc with examples and proper error types

Technical Details

Race Condition Protection

Problem: If components mount/unmount out of order, the cleanup could clear a newer launcher.

Solution: Reference-based cleanup in both launchers (error-modal-handler.tsx:194-205, warning-modal-handler.tsx:278-289):

useEffect(() => {
 const errorModalLauncher = (props: ErrorModalProps) => {
   launcher<ErrorModalProps>(ErrorModal, props);
 };
 moduleErrorModalLauncher = errorModalLauncher;
 
 return () => {
   // Only clear if we're still the active launcher
   if (moduleErrorModalLauncher === errorModalLauncher) {
     moduleErrorModalLauncher = null;
   }
 };
}, [launcher]);

This prevents race conditions where unmounting could clear a newer launcher that was set by a remounting component.

Error Handling Improvements

Promise Rejection with Proper Error Objects:

// Before: rejected with undefined
onClose: () => {
 reject();
}

// After: rejects with Error object
onClose: () => {
 reject(new Error('User cancelled'));
}

This allows downstream code to properly detect user cancellation:

.catch((error) => {
 // Only show error modal if it's not a user cancellation
 if (error.message && error.message !== 'User cancelled') {
   launchErrorModal({ error: error.message });
 }
});

Type Safety for Optional nodeMaintenance

Problem: useStopNodeMaintenanceModal was called with potentially undefined nodeMaintenance but didn't handle it.

Solution: Updated signature and added guards (StopNodeMaintenanceModal.tsx:509-526):

export const useStopNodeMaintenanceModal = (nodeMaintenance: K8sResourceKind | undefined) => {
 const reason = nodeMaintenance ? getNodeMaintenanceReason(nodeMaintenance) : '';
 const nodeName = nodeMaintenance ? getNodeMaintenanceNodeName(nodeMaintenance) : '';
 // ...
 onConfirm: nodeMaintenance
   ? () => k8sKill(getMaintenanceModel(nodeMaintenance), nodeMaintenance)
   : () => Promise.resolve(),
}

Comprehensive Documentation

JSDoc with Clear Type Information:

/**
* Launch a warning modal from non-React contexts (callbacks, promises, utilities).
* 
* @param props - Warning modal properties (title, children, confirmButtonLabel, etc.)
* @returns Promise that resolves (undefined) when confirmed, rejects with Error('User cancelled') when canceled/closed
* 
* @example
* ```tsx
* launchWarningModal({
*   title: 'Delete Resource',
*   children: 'Are you sure you want to delete this resource?',
*   confirmButtonLabel: 'Delete',
* }).then(() => {
*   // User confirmed - proceed with action
*   deleteResource();
* }).catch((error) => {
*   // User canceled - error.message === 'User cancelled'
*   console.log('Action cancelled by user');
* });
* ```
*/

Test Coverage

6 Comprehensive Test Cases (warning-modal-handler.spec.tsx):

  1. SyncModalLaunchers - should sync the warning launcher on mount
  2. SyncModalLaunchers - should cleanup warning launcher on unmount
  3. useWarningModalLauncher - should return a function that launches warning modals
  4. launchWarningModal - should launch warning modal when launcher is initialized
  5. launchWarningModal - should throw error when launcher is not initialized
  6. launchWarningModal - should resolve promise when onConfirm is called
  7. launchWarningModal - should reject promise with Error when onClose is called

Test Patterns:

  • Follows same structure as error-modal-handler.spec.tsx
  • Uses React Testing Library
  • Mocks useOverlay for isolation
  • Tests both sync and async behaviors
  • Validates error handling and race condition scenarios

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:753-761):

// Blur active element to prevent aria-hidden focus violations when modal opens
// This fixes the browser warning: "Blocked aria-hidden on an element because its
// descendant retained focus" when focus remains on SVG elements during drag-drop
if (
 document.activeElement instanceof HTMLElement ||
 document.activeElement instanceof SVGElement
) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling
  • Proper error type discrimination

Single SyncModalLaunchers Component

// Before: Single error launcher
<OverlayProvider>
 <SyncErrorModalLauncher />
 <YourApp />
</OverlayProvider>

// After: Unified component for both error and warning
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:224-227):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

Mock Architecture & Circular Import Handling

Documentation in Mock (mocks/error-modal-handler.tsx):

The mock provides a simplified SyncModalLaunchers that doesn't call useSyncWarningModalLauncher. This is intentional and acceptable:

  • Mock Purpose: Simplifies tests that don't need real modal rendering
  • Explicit Dependencies: Tests needing warning modals should explicitly mock or import
  • No Circular Import: Import chain is unidirectional (error → warning)
  • Test Isolation: Each test suite mocks only what it needs

Why This Design Works:

  1. Runtime code has clear, unidirectional dependencies
  2. Mocks document their limitations
  3. Tests are explicit about their needs
  4. Follows Jest best practices

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly
  • Unit tests pass (error-modal-handler.spec.tsx: 5/5)
  • New test suite created (warning-modal-handler.spec.tsx: 6 test cases)

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto
Copy link
Member Author

/hold

Just realized there are cosmetic differences in the modal since I repurposed warning.

@openshift-ci openshift-ci bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 18, 2026
- Add error-modal-handler.tsx to console-shared package with:
  - SyncErrorModalLauncher - Zero-render component that syncs launcher for non-React contexts
  - useErrorModalLauncher() - Hook for launching error modals from React components
  - launchErrorModal() - Function for launching from non-React contexts (callbacks, promises)

- Add SyncErrorModalLauncher to app.tsx inside OverlayProvider
  - Single initialization point for the entire application
  - Reuses existing OverlayProvider instead of creating new context

- Migrate packages to use launchErrorModal:
  - topology: componentUtils.ts, edgeActions.ts
  - knative-plugin: knativeComponentUtils.ts, create-connector-utils.ts
  - shipwright-plugin: logs-utils.ts
  - dev-console: pipeline-template-utils.ts

- Update moveNodeToGroup.tsx to support error handling:
  - Add setMoveNodeToGroupErrorHandler() for managing error handler
  - Add useSetupMoveNodeToGroupErrorHandler() hook for Topology component
  - Update moveNodeToGroup() to accept optional onError callback
  - Deprecate useMoveNodeToGroup() hook

- Add comprehensive test coverage:
  - error-modal-handler.spec.tsx - 5 unit tests covering all scenarios
  - __mocks__/errorModalHandler.ts - Global mock for testing

- Fixed useWarningModal to properly call closeOverlay() on modal close/confirm

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Feb 18, 2026

@rhamilto: This pull request references CONSOLE-5012 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

Summary

This PR completes the modal migration effort by removing the deprecated confirmModal (which used createModalLauncher) and migrating to the modern overlay pattern with an imperative launchWarningModal() API.

Changes

Removed deprecated confirmModal

Created imperative warning modal API

  • ✅ Created warning-modal-handler.tsx with launchWarningModal() function
  • ✅ Follows same pattern as launchErrorModal()
  • ✅ Promise-based API: resolves on confirm, rejects with Error('User cancelled') on cancel
  • ✅ Works from non-React contexts (drag-drop callbacks, utility functions)
  • ✅ Uses modern overlay system via useOverlay hook
  • Race condition guards: Only clears launcher if still active
  • Comprehensive JSDoc documentation with examples
  • Warning icon is optional via titleIconVariant prop (defaults to 'warning' per PatternFly)

Unified modal launcher initialization with race condition protection

  • ✅ Refactored SyncErrorModalLauncherSyncModalLaunchers in error-modal-handler.tsx
  • ✅ Single component syncs both error and warning modal launchers
  • Added reference-based cleanup guards to prevent race conditions
  • ✅ Mounted once in app.tsx after OverlayProvider

Migrated moveNodeToGroup to use launchWarningModal

  • ✅ Updated moveNodeToGroup.tsx to use launchWarningModal() instead of confirmModal()
  • ✅ Preserved <Trans> components with <strong> tags for bold text formatting
  • ✅ Added onError parameter for custom error handling
  • ✅ Integrated with launchErrorModal() for error display
  • Proper error handling distinguishing user cancellation from actual errors
  • Hides warning icon for move/remove operations via titleIconVariant: null

Fixed accessibility violation

  • ✅ Blur active element before launching confirmation modal in moveNodeToGroup.tsx:753-761
  • ✅ Prevents focus from remaining on SVG elements in topology graph
  • ✅ Fixes browser warning: "Blocked aria-hidden on an element because its descendant retained focus"

Fixed useWarningModal type safety

  • ✅ Exported ControlledWarningModal component from useWarningModal.tsx for reuse
  • ✅ Uses proper OverlayComponent type instead of manual prop definition
  • ✅ Properly calls closeOverlay() on modal dismiss

Updated metal3-plugin modals

Updated shipwright-plugin

  • ✅ Cleaned up orphaned functions in logs-utils.ts
  • ✅ Removed getOrderedStepsFromPod (no references found - 20 lines removed)
  • ✅ Removed getDownloadAllLogsCallback (no references found - 111 lines removed)
  • ✅ Removed unused i18n key "Error downloading logs."

Test infrastructure

Architecture Benefits

  • Consistent pattern - Both error and warning modals use the same imperative launcher pattern
  • Simpler than React Context - No provider/consumer boilerplate needed
  • Single initialization point - One SyncModalLaunchers component for all imperative modals
  • Works everywhere - Usable from both React components and non-React contexts
  • Type-safe - Throws clear errors when launchers not initialized
  • Race condition protection - Reference-based cleanup prevents clearing newer launchers
  • Proper cleanup - Via React lifecycle, not manual cleanup
  • WCAG 2.1 Level A compliant - No aria-hidden focus violations
  • Well-tested - Comprehensive unit tests for all scenarios
  • Well-documented - Clear JSDoc with examples and proper error types
  • Flexible icon display - Warning icon can be hidden when not needed

Technical Details

Optional Warning Icon

The ControlledWarningModal wrapper now passes through the titleIconVariant prop, allowing callers to:

  • Use default warning icon - Don't specify titleIconVariant (PatternFly defaults to 'warning')
  • Hide the icon - Pass titleIconVariant: null
  • Use a different icon - Pass titleIconVariant: 'danger' | 'success' | 'info' or a custom icon

Example usage in moveNodeToGroup.tsx:

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
 titleIconVariant: null, // Hides the warning icon
})

Race Condition Protection

Problem: If components mount/unmount out of order, the cleanup could clear a newer launcher.

Solution: Reference-based cleanup in both launchers (error-modal-handler.tsx:194-205, warning-modal-handler.tsx:278-289):

useEffect(() => {
 const errorModalLauncher = (props: ErrorModalProps) => {
   launcher<ErrorModalProps>(ErrorModal, props);
 };
 moduleErrorModalLauncher = errorModalLauncher;
 
 return () => {
   // Only clear if we're still the active launcher
   if (moduleErrorModalLauncher === errorModalLauncher) {
     moduleErrorModalLauncher = null;
   }
 };
}, [launcher]);

This prevents race conditions where unmounting could clear a newer launcher that was set by a remounting component.

Error Handling Improvements

Promise Rejection with Proper Error Objects:

// Before: rejected with undefined
onClose: () => {
 reject();
}

// After: rejects with Error object
onClose: () => {
 reject(new Error('User cancelled'));
}

This allows downstream code to properly detect user cancellation:

.catch((error) => {
 // Only show error modal if it's not a user cancellation
 if (error.message && error.message !== 'User cancelled') {
   launchErrorModal({ error: error.message });
 }
});

Type Safety for Optional nodeMaintenance

Problem: useStopNodeMaintenanceModal was called with potentially undefined nodeMaintenance but didn't handle it.

Solution: Updated signature and added guards (StopNodeMaintenanceModal.tsx:509-526):

export const useStopNodeMaintenanceModal = (nodeMaintenance: K8sResourceKind | undefined) => {
 const reason = nodeMaintenance ? getNodeMaintenanceReason(nodeMaintenance) : '';
 const nodeName = nodeMaintenance ? getNodeMaintenanceNodeName(nodeMaintenance) : '';
 // ...
 onConfirm: nodeMaintenance
   ? () => k8sKill(getMaintenanceModel(nodeMaintenance), nodeMaintenance)
   : () => Promise.resolve(),
}

Comprehensive Documentation

JSDoc with Clear Type Information:

/**
* Launch a warning modal from non-React contexts (callbacks, promises, utilities).
* 
* @param props - Warning modal properties (title, children, confirmButtonLabel, etc.)
* @returns Promise that resolves (undefined) when confirmed, rejects with Error('User cancelled') when canceled/closed
* 
* @example
* ```tsx
* launchWarningModal({
*   title: 'Delete Resource',
*   children: 'Are you sure you want to delete this resource?',
*   confirmButtonLabel: 'Delete',
* }).then(() => {
*   // User confirmed - proceed with action
*   deleteResource();
* }).catch((error) => {
*   // User canceled - error.message === 'User cancelled'
*   console.log('Action cancelled by user');
* });
* ```
*/

Test Coverage

6 Comprehensive Test Cases (warning-modal-handler.spec.tsx):

  1. SyncModalLaunchers - should sync the warning launcher on mount
  2. SyncModalLaunchers - should cleanup warning launcher on unmount
  3. useWarningModalLauncher - should return a function that launches warning modals
  4. launchWarningModal - should launch warning modal when launcher is initialized
  5. launchWarningModal - should throw error when launcher is not initialized
  6. launchWarningModal - should resolve promise when onConfirm is called
  7. launchWarningModal - should reject promise with Error when onClose is called

Test Patterns:

  • Follows same structure as error-modal-handler.spec.tsx
  • Uses React Testing Library
  • Mocks useOverlay for isolation
  • Tests both sync and async behaviors
  • Validates error handling and race condition scenarios

Accessibility Fix - aria-hidden Focus Violation

Problem: When the confirmation modal opened during drag-drop operations, focus remained on an SVG <g> element in the topology graph. PatternFly's Modal sets aria-hidden="true" on the #app div, creating an accessibility violation where a focused element is hidden from assistive technology users.

Browser Warning (Fixed):

Blocked aria-hidden on an element because its descendant retained focus.
The focus must not be hidden from assistive technology users.
Element with focus: <g>
Ancestor with aria-hidden: <div#app>

Solution: Blur the active element before launching the modal (moveNodeToGroup.tsx:753-761):

// Blur active element to prevent aria-hidden focus violations when modal opens
// This fixes the browser warning: "Blocked aria-hidden on an element because its
// descendant retained focus" when focus remains on SVG elements during drag-drop
if (
 document.activeElement instanceof HTMLElement ||
 document.activeElement instanceof SVGElement
) {
 document.activeElement.blur();
}

Imperative Warning Modal Pattern

Before (deprecated createModalLauncher):

// ❌ Old pattern using createModalLauncher
confirmModal({
 titleKey,
 message,
 btnTextKey,
 close: () => reject(),
 cancel: () => reject(),
 executeFn: () => updateTopologyResourceApplication(...),
});

After (modern overlay pattern):

// ✅ New pattern using launchWarningModal with Trans for formatting
const message = targetGroup ? (
 <Trans ns="topology">
   Are you sure you want to move <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }} to {{ targetLabel }}?
 </Trans>
) : (
 <Trans ns="topology">
   Are you sure you want to remove <strong>{{ nodeLabel }}</strong> from {{ sourceLabel }}?
 </Trans>
);

launchWarningModal({
 title: i18next.t(titleKey),
 children: message,
 confirmButtonLabel: i18next.t(btnTextKey),
 titleIconVariant: null, // Hide the warning icon for simple confirmation
})
 .then(() => updateTopologyResourceApplication(...))
 .catch((err) => {
   if (err.message !== 'User cancelled') {
     launchErrorModal({ error: err.message });
   }
 });

Key Improvements:

  • Promise-based instead of callback-based
  • Uses modern overlay system
  • Consistent with launchErrorModal() pattern
  • No deprecated createModalLauncher usage
  • Preserves <Trans> for formatted text with bold styling
  • Proper error type discrimination
  • Optional icon display

Single SyncModalLaunchers Component

// Before: Single error launcher
<OverlayProvider>
 <SyncErrorModalLauncher />
 <YourApp />
</OverlayProvider>

// After: Unified component for both error and warning
<OverlayProvider>
 <SyncModalLaunchers />
 <YourApp />
</OverlayProvider>

Implementation (error-modal-handler.tsx:224-227):

export const SyncModalLaunchers = () => {
 useSyncErrorModalLauncher();
 useSyncWarningModalLauncher();
 return null;
};

Mock Architecture & Circular Import Handling

Documentation in Mock (mocks/error-modal-handler.tsx):

The mock provides a simplified SyncModalLaunchers that doesn't call useSyncWarningModalLauncher. This is intentional and acceptable:

  • Mock Purpose: Simplifies tests that don't need real modal rendering
  • Explicit Dependencies: Tests needing warning modals should explicitly mock or import
  • No Circular Import: Import chain is unidirectional (error → warning)
  • Test Isolation: Each test suite mocks only what it needs

Why This Design Works:

  1. Runtime code has clear, unidirectional dependencies
  2. Mocks document their limitations
  3. Tests are explicit about their needs
  4. Follows Jest best practices

i18n Translation Updates

The <Trans> components with <strong> tags are properly detected by the i18n tooling:

// Before
"Are you sure you want to move {{nodeLabel}} from {{sourceLabel}} to {{targetLabel}}?"

// After
"Are you sure you want to move <1>{{nodeLabel}}</1> from {{sourceLabel}} to {{targetLabel}}?"

The <1> placeholder represents the <strong> tag, ensuring translated text maintains proper formatting.

Test Plan

Automated Tests ✅

  • TypeScript compilation succeeds
  • ESLint passes
  • Build succeeds
  • i18n keys updated correctly
  • Unit tests pass (error-modal-handler.spec.tsx: 5/5)
  • New test suite created (warning-modal-handler.spec.tsx: 6 test cases)

Manual Testing Checklist

  • Verify topology node drag-and-drop with confirmation modal works
  • Verify clicking "Cancel" in confirmation modal restores node to original position
  • Verify clicking "Move"/"Remove" confirms the operation
  • Verify error modals display correctly for failed operations
  • Verify modals properly close and remove from DOM on both confirm and cancel
  • Verify bold text formatting appears correctly in modal messages
  • Verify no aria-hidden accessibility warnings in browser console
  • Verify modal receives focus when opened from drag-drop operation
  • Verify modal displays without warning icon for move/remove operations
  • Verify metal3-plugin maintenance modals work correctly
  • Verify "Stop maintenance" modal displays and functions properly

Breaking Changes

None - this is a refactoring that maintains the same functionality.

Related

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@rhamilto
Copy link
Member Author

/hold

Just realized there are cosmetic differences in the modal since I repurposed warning.

/hold cancel

Removed the warning icon from the confirm modal title so we match what we had before

Screen.Recording.2026-02-18.at.1.38.52.PM.mov

@openshift-ci openshift-ci bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 18, 2026
@rhamilto
Copy link
Member Author

/retest

1 similar comment
@rhamilto
Copy link
Member Author

/retest

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 20, 2026

@rhamilto: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-gcp-console 1132d49 link true /test e2e-gcp-console

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 20, 2026
@openshift-merge-robot
Copy link
Contributor

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

acknowledge-critical-fixes-only Indicates if the issuer of the label is OK with the policy. approved Indicates a PR has been approved by an approver from all required OWNERS files. component/core Related to console core functionality component/dev-console Related to dev-console component/knative Related to knative-plugin component/metal3 Related to metal3-plugin component/sdk Related to console-plugin-sdk component/shared Related to console-shared component/topology Related to topology docs-approved Signifies that Docs has signed off on this PR jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. kind/i18n Indicates issue or PR relates to internationalization or has content that needs to be translated needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. px-approved Signifies that Product Support has signed off on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants