-
Notifications
You must be signed in to change notification settings - Fork 669
CONSOLE-5012: Create shared error modal launcher using React Context #15946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rhamilto
wants to merge
1
commit into
openshift:main
Choose a base branch
from
rhamilto:CONSOLE-5012-4
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+258
−37
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
frontend/packages/console-shared/src/utils/__mocks__/error-modal-handler.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Mock implementation of error-modal-handler for Jest tests | ||
| */ | ||
|
|
||
| export const mockLaunchErrorModal = jest.fn(); | ||
|
|
||
| export const SyncErrorModalLauncher = () => null; | ||
|
|
||
| export const useErrorModalLauncher = jest.fn(() => mockLaunchErrorModal); | ||
|
|
||
| export const launchErrorModal = mockLaunchErrorModal; |
99 changes: 99 additions & 0 deletions
99
frontend/packages/console-shared/src/utils/__tests__/error-modal-handler.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { render } from '@testing-library/react'; | ||
| import { | ||
| SyncErrorModalLauncher, | ||
| useErrorModalLauncher, | ||
| launchErrorModal, | ||
| } from '../error-modal-handler'; | ||
|
|
||
| // Mock useOverlay | ||
| const mockLauncher = jest.fn(); | ||
| jest.mock('@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay', () => ({ | ||
| useOverlay: () => mockLauncher, | ||
| })); | ||
|
|
||
| describe('error-modal-handler', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe('SyncErrorModalLauncher', () => { | ||
| it('should sync the launcher on mount', () => { | ||
| render(<SyncErrorModalLauncher />); | ||
|
|
||
| // Call the module-level function | ||
| launchErrorModal({ error: 'Test error', title: 'Test' }); | ||
|
|
||
| // Should have called the mocked overlay launcher | ||
| expect(mockLauncher).toHaveBeenCalledWith(expect.anything(), { | ||
| error: 'Test error', | ||
| title: 'Test', | ||
| }); | ||
| }); | ||
|
|
||
| it('should cleanup launcher on unmount', () => { | ||
| const { unmount } = render(<SyncErrorModalLauncher />); | ||
|
|
||
| unmount(); | ||
|
|
||
| // Should log error instead of crashing | ||
| const consoleError = jest.spyOn(console, 'error').mockImplementation(); | ||
| launchErrorModal({ error: 'Test error' }); | ||
|
|
||
| expect(consoleError).toHaveBeenCalledWith( | ||
| expect.stringContaining('Error modal launcher not initialized'), | ||
| expect.any(Object), | ||
| ); | ||
|
|
||
| consoleError.mockRestore(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useErrorModalLauncher', () => { | ||
| it('should return a function that launches error modals', () => { | ||
| let capturedLauncher: any; | ||
|
|
||
| const TestComponent = () => { | ||
| capturedLauncher = useErrorModalLauncher(); | ||
| return null; | ||
| }; | ||
|
|
||
| render(<TestComponent />); | ||
|
|
||
| capturedLauncher({ error: 'Test error', title: 'Test Title' }); | ||
|
|
||
| expect(mockLauncher).toHaveBeenCalledWith(expect.anything(), { | ||
| error: 'Test error', | ||
| title: 'Test Title', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('launchErrorModal', () => { | ||
| it('should launch error modal when launcher is initialized', () => { | ||
| render(<SyncErrorModalLauncher />); | ||
|
|
||
| launchErrorModal({ | ||
| error: 'Connection failed', | ||
| title: 'Network Error', | ||
| }); | ||
|
|
||
| expect(mockLauncher).toHaveBeenCalledWith(expect.anything(), { | ||
| error: 'Connection failed', | ||
| title: 'Network Error', | ||
| }); | ||
| }); | ||
|
|
||
| it('should log error when launcher is not initialized', () => { | ||
| const consoleError = jest.spyOn(console, 'error').mockImplementation(); | ||
|
|
||
| launchErrorModal({ error: 'Test error' }); | ||
|
|
||
| expect(consoleError).toHaveBeenCalledWith( | ||
| expect.stringContaining('Error modal launcher not initialized'), | ||
| { error: 'Test error' }, | ||
| ); | ||
|
|
||
| consoleError.mockRestore(); | ||
| }); | ||
| }); | ||
| }); |
99 changes: 99 additions & 0 deletions
99
frontend/packages/console-shared/src/utils/error-modal-handler.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { useCallback, useEffect } from 'react'; | ||
| import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; | ||
| import { ErrorModal, ErrorModalProps } from '@console/internal/components/modals/error-modal'; | ||
|
|
||
| // Module-level reference for non-React contexts | ||
| // This is populated by SyncErrorModalLauncher and should not be set directly | ||
| let moduleErrorModalLauncher: ((props: ErrorModalProps) => void) | null = null; | ||
|
|
||
| /** | ||
| * Component that syncs the error modal launcher to module-level for non-React contexts. | ||
| * This should be mounted once in the app root, after OverlayProvider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const App = () => ( | ||
| * <OverlayProvider> | ||
| * <SyncErrorModalLauncher /> | ||
| * <YourApp /> | ||
| * </OverlayProvider> | ||
| * ); | ||
| * ``` | ||
| */ | ||
| export const SyncErrorModalLauncher = () => { | ||
| const launcher = useOverlay(); | ||
rhamilto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| useEffect(() => { | ||
| moduleErrorModalLauncher = (props: ErrorModalProps) => { | ||
| launcher<ErrorModalProps>(ErrorModal, props); | ||
| }; | ||
|
|
||
| return () => { | ||
| moduleErrorModalLauncher = null; | ||
| }; | ||
| }, [launcher]); | ||
|
|
||
| return null; | ||
| }; | ||
|
|
||
| /** | ||
| * Hook to launch error modals from React components. | ||
| * Must be used within an OverlayProvider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const MyComponent = () => { | ||
| * const launchErrorModal = useErrorModalLauncher(); | ||
| * | ||
| * const handleError = (error: Error) => { | ||
| * launchErrorModal({ | ||
| * title: 'Operation Failed', | ||
| * error: error.message, | ||
| * }); | ||
| * }; | ||
| * | ||
| * // ... | ||
| * }; | ||
| * ``` | ||
| */ | ||
| export const useErrorModalLauncher = (): ((props: ErrorModalProps) => void) => { | ||
| const launcher = useOverlay(); | ||
|
|
||
| return useCallback( | ||
| (props: ErrorModalProps) => { | ||
| launcher<ErrorModalProps>(ErrorModal, props); | ||
| }, | ||
| [launcher], | ||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Launch an error modal from non-React contexts (callbacks, promises, utilities). | ||
| * The SyncErrorModalLauncher component must be mounted in the app root. | ||
| * | ||
| * @deprecated Use React component modals within component code instead. | ||
| * For new code, write modals directly within React components using useOverlay or other modal patterns. | ||
| * This function should only be used for legacy non-React contexts like promise callbacks. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // In a promise callback or utility function | ||
| * createConnection(source, target).catch((error) => { | ||
| * launchErrorModal({ | ||
| * title: 'Connection Failed', | ||
| * error: error.message, | ||
| * }); | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export const launchErrorModal = (props: ErrorModalProps): void => { | ||
rhamilto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (moduleErrorModalLauncher) { | ||
| moduleErrorModalLauncher(props); | ||
| } else { | ||
| // eslint-disable-next-line no-console | ||
| console.error( | ||
| 'Error modal launcher not initialized. Ensure SyncErrorModalLauncher is mounted after OverlayProvider.', | ||
| props, | ||
| ); | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we expect to eventually remove all uses of error modal which are outside of React render flow?
i.e. do we have lots of cases where the error modal is used outside of a React component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude says
Why Non-React Context?
2. Topology - Create Connection
File:
packages/topology/src/components/graph-view/components/componentUtils.ts:350Context:
.catch()callback when creating visual connectorscreateVisualConnector()utility functionCode Example:
Why Non-React Context?
3. Topology - Move Edge Connection
File:
packages/topology/src/components/graph-view/components/componentUtils.ts:313Context:
endhandlerCode Example:
Why Non-React Context?
4. Topology - Delete Edge
File:
packages/topology/src/actions/edgeActions.ts:106Context:
onConfirmcallbackCode Example:
Why Non-React Context?
5. Knative - Event Source Sink Connection
File:
packages/knative-plugin/src/topology/components/knativeComponentUtils.ts:191Context:
Code Example:
Why Non-React Context?
endhandler6. Knative - Kafka Connection (Drag)
File:
packages/knative-plugin/src/topology/components/knativeComponentUtils.ts:224Context:
Code Example:
Why Non-React Context?
7. Knative - PubSub Connection
File:
packages/knative-plugin/src/topology/components/knativeComponentUtils.ts:261Context:
Code Example:
Why Non-React Context?
8. Knative - Create Kafka Connection (Utility)
File:
packages/knative-plugin/src/topology/create-connector-utils.ts:18Context:
Code Example:
Why Non-React Context?
9. Shipwright - Download Logs
File:
packages/shipwright-plugin/src/components/logs/logs-utils.ts:84Context:
Code Example:
Why Non-React Context?
10. Dev Console - Expose Route
File:
packages/dev-console/src/components/pipeline-section/pipeline/pipeline-template-utils.ts:931Context:
Code Example:
Why Non-React Context?
Usage Categories
By Type
By Package
Can These Be Refactored to React Contexts?
Short Answer: No, most cannot be easily refactored.
Detailed Analysis
Drag/Drop Operations (7/10 usages)
Async Utility Functions (2/10 usages)
Modal Callbacks (1/10 usage)
Why the Hybrid Approach is Necessary
The codebase requires both patterns:
useErrorModalLauncher()Hook (React Context)✅ Use in React components
✅ Direct access to
useOverlay()✅ Type-safe, idiomatic React
launchErrorModal()Function (Module-Level)✅ Use in imperative callbacks
✅ Use in async utility functions
✅ Use in third-party library callbacks
✅ Use in promise chains
Architecture Benefits
OverlayProviderSyncErrorModalLaunchermanages initialization/cleanupErrorModalPropsinterfaceConclusion
The current implementation correctly addresses a real architectural need:
The hybrid approach with
SyncErrorModalLauncher+ module-level reference is the appropriate solution for this codebase's requirements.Related
packages/console-shared/src/utils/error-modal-handler.tsxReport generated: 2026-02-03