55import type { FunctionComponent , MouseEvent } from 'react' ;
66import { useState , useEffect , useRef } from 'react' ;
77import path from 'path-browserify' ;
8- import * as monaco from 'monaco-editor' ;
9- import { loader } from '@monaco-editor/react' ;
108
119// Import PatternFly components
1210import { CodeEditor } from '@patternfly/react-code-editor' ;
1311import {
12+ Bullseye ,
1413 Button ,
1514 getResizeObserver ,
1615 ModalBody ,
1716 ModalFooter ,
1817 ModalHeader ,
18+ Spinner ,
1919 Stack ,
2020 StackItem
2121} from '@patternfly/react-core' ;
2222import FileDetails , { extensionToLanguage } from '../FileDetails' ;
2323import { ChatbotDisplayMode } from '../Chatbot' ;
2424import ChatbotModal from '../ChatbotModal/ChatbotModal' ;
2525
26- // Configure Monaco loader to use the npm package instead of CDN
27- loader . config ( { monaco } ) ;
26+ // Try to lazy load - some consumers need to be below a certain bundle size, but can't use the CDN and don't have webpack
27+ let monacoInstance : typeof import ( 'monaco-editor' ) | null = null ;
28+ const loadMonaco = async ( ) => {
29+ if ( ! monacoInstance ) {
30+ const [ monaco , { loader } ] = await Promise . all ( [ import ( 'monaco-editor' ) , import ( '@monaco-editor/react' ) ] ) ;
31+ monacoInstance = monaco ;
32+ loader . config ( { monaco } ) ;
33+ }
34+ return monacoInstance ;
35+ } ;
2836
2937export interface CodeModalProps {
3038 /** Class applied to code editor */
@@ -63,6 +71,8 @@ export interface CodeModalProps {
6371 modalBodyClassName ?: string ;
6472 /** Class applied to modal footer */
6573 modalFooterClassName ?: string ;
74+ /** Aria label applied to spinner when loading Monaco */
75+ spinnerAriaLabel ?: string ;
6676}
6777
6878export const CodeModal : FunctionComponent < CodeModalProps > = ( {
@@ -84,13 +94,32 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
8494 modalHeaderClassName,
8595 modalBodyClassName,
8696 modalFooterClassName,
97+ spinnerAriaLabel = 'Loading' ,
8798 ...props
8899} : CodeModalProps ) => {
89100 const [ newCode , setNewCode ] = useState ( code ) ;
90- const [ editorInstance , setEditorInstance ] = useState < monaco . editor . IStandaloneCodeEditor | null > ( null ) ;
101+ const [ editorInstance , setEditorInstance ] = useState < any > ( null ) ;
91102 const [ isEditorReady , setIsEditorReady ] = useState ( false ) ;
103+ const [ isMonacoLoading , setIsMonacoLoading ] = useState ( false ) ;
104+ const [ isMonacoLoaded , setIsMonacoLoaded ] = useState ( false ) ;
92105 const containerRef = useRef < HTMLDivElement > ( null ) ;
93106
107+ useEffect ( ( ) => {
108+ if ( isModalOpen && ! isMonacoLoaded && ! isMonacoLoading ) {
109+ setIsMonacoLoading ( true ) ;
110+ loadMonaco ( )
111+ . then ( ( ) => {
112+ setIsMonacoLoaded ( true ) ;
113+ setIsMonacoLoading ( false ) ;
114+ } )
115+ . catch ( ( error ) => {
116+ // eslint-disable-next-line no-console
117+ console . error ( 'Failed to load Monaco editor:' , error ) ;
118+ setIsMonacoLoading ( false ) ;
119+ } ) ;
120+ }
121+ } , [ isModalOpen , isMonacoLoaded , isMonacoLoading ] ) ;
122+
94123 useEffect ( ( ) => {
95124 if ( ! isModalOpen || ! isEditorReady || ! editorInstance || ! containerRef . current ) {
96125 return ;
@@ -148,6 +177,42 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
148177 }
149178 } ;
150179
180+ const renderMonacoEditor = ( ) => {
181+ if ( isMonacoLoading ) {
182+ return (
183+ < Bullseye >
184+ < Spinner aria-label = { spinnerAriaLabel } />
185+ </ Bullseye >
186+ ) ;
187+ }
188+ if ( isMonacoLoaded ) {
189+ return (
190+ < CodeEditor
191+ isDarkTheme
192+ isLineNumbersVisible = { isLineNumbersVisible }
193+ isLanguageLabelVisible
194+ isCopyEnabled = { isCopyEnabled }
195+ isReadOnly = { isReadOnly }
196+ code = { newCode }
197+ language = { extensionToLanguage [ path . extname ( fileName ) . slice ( 1 ) ] }
198+ onEditorDidMount = { onEditorDidMount }
199+ onCodeChange = { onCodeChange }
200+ className = { codeEditorClassName }
201+ isFullHeight
202+ options = { {
203+ glyphMargin : false ,
204+ folding : false ,
205+ // prevents Monaco from handling resizing itself
206+ // was causing ResizeObserver issues
207+ automaticLayout : false
208+ } }
209+ { ...props }
210+ />
211+ ) ;
212+ }
213+ return null ;
214+ } ;
215+
151216 const modal = (
152217 < ChatbotModal
153218 isOpen = { isModalOpen }
@@ -166,27 +231,7 @@ export const CodeModal: FunctionComponent<CodeModalProps> = ({
166231 < FileDetails fileName = { fileName } />
167232 </ StackItem >
168233 < div className = "pf-v6-l-stack__item pf-chatbot__code-modal-editor" ref = { containerRef } >
169- < CodeEditor
170- isDarkTheme
171- isLineNumbersVisible = { isLineNumbersVisible }
172- isLanguageLabelVisible
173- isCopyEnabled = { isCopyEnabled }
174- isReadOnly = { isReadOnly }
175- code = { newCode }
176- language = { extensionToLanguage [ path . extname ( fileName ) . slice ( 1 ) ] }
177- onEditorDidMount = { onEditorDidMount }
178- onCodeChange = { onCodeChange }
179- className = { codeEditorClassName }
180- isFullHeight
181- options = { {
182- glyphMargin : false ,
183- folding : false ,
184- // prevents Monaco from handling resizing itself
185- // was causing ResizeObserver issues
186- automaticLayout : false
187- } }
188- { ...props }
189- />
234+ { renderMonacoEditor ( ) }
190235 </ div >
191236 </ Stack >
192237 </ ModalBody >
0 commit comments