From 87f7949d73c8a2c1a016d532e1dc874e4c43702a Mon Sep 17 00:00:00 2001 From: Kevin Cormier Date: Fri, 30 Aug 2024 16:43:50 -0400 Subject: [PATCH] WIP Update SyncEditor for newer package versions Signed-off-by: Kevin Cormier --- .../src/components/SyncEditor/SyncEditor.tsx | 734 ++++++++++-------- .../src/components/SyncEditor/decorate.ts | 64 +- frontend/src/components/SyncEditor/process.ts | 13 +- .../TemplateEditor/components/YamlEditor.js | 14 +- 4 files changed, 438 insertions(+), 387 deletions(-) diff --git a/frontend/src/components/SyncEditor/SyncEditor.tsx b/frontend/src/components/SyncEditor/SyncEditor.tsx index d0d398bde7a..30c3c021ace 100644 --- a/frontend/src/components/SyncEditor/SyncEditor.tsx +++ b/frontend/src/components/SyncEditor/SyncEditor.tsx @@ -1,5 +1,4 @@ /* Copyright Contributors to the Open Cluster Management project */ -/* eslint-disable react-hooks/exhaustive-deps */ import { HTMLProps, ReactNode, useRef, useEffect, useState, useCallback, useMemo } from 'react' import useResizeObserver from '@react-hook/resize-observer' import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor' @@ -15,6 +14,11 @@ import { setFormValues, updateReferences } from './synchronize' import './SyncEditor.css' import { useTranslation } from '../../lib/acm-i18next' import { ChangeHandler } from 'react-monaco-editor' +import * as monaco from 'monaco-editor' +import { editor as editorTypes } from 'monaco-editor' +import { loader, Monaco } from '@monaco-editor/react' + +loader.config({ monaco }) export interface SyncEditorProps extends HTMLProps { variant?: string @@ -55,31 +59,36 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { onClose, } = props const pageRef = useRef(null) - const editorRef = useRef(null) - const monacoRef = useRef(null) + const [editor, setEditor] = useState(null) + const [monaco, setMonaco] = useState(null) if (mock) { - if (!monacoRef.current) { - monacoRef.current = { + if (!monaco) { + setMonaco({ editor: { setTheme: () => {}, }, languages: { registerHoverProvider: () => {}, }, - } - editorRef.current = { + } as any as Monaco) + } + if (!editor) { + setEditor({ getModel: () => {}, onMouseDown: () => {}, onKeyDown: () => {}, onDidBlurEditorWidget: () => {}, - } + } as any as editorTypes.IStandaloneCodeEditor) } } const { t } = useTranslation() const editorHadFocus = useRef(false) - const defaultCopy: ReactNode = {t('Copy')} - const copiedCopy: ReactNode = {t('Selection copied')} - const allCopiedCopy: ReactNode = {t('All copied')} + const defaultCopy = useMemo(() => {t('Copy')}, [t]) + const copiedCopy = useMemo( + () => {t('Selection copied')}, + [t] + ) + const allCopiedCopy = useMemo(() => {t('All copied')}, [t]) const [copyHint, setCopyHint] = useState(defaultCopy) const [prohibited, setProhibited] = useState([]) const [filteredRows, setFilteredRows] = useState([]) @@ -125,7 +134,7 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { validationRef.current = compileAjvSchemas(schema) } - function onEditorDidMount(editor: any, monaco: any) { + function onEditorDidMount(editor: editorTypes.IStandaloneCodeEditor, monaco: Monaco) { // create 'resource-editor' theme monaco.editor.defineTheme('resource-editor', { base: 'vs-dark', @@ -162,6 +171,8 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { }, }) + //monaco.editor.onDidChangeMarkers + // a little breathing space above top line editor.changeViewZones( (changeAccessor: { @@ -176,89 +187,96 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { } ) - editorRef.current = editor window.getEditorValue = () => editor.getValue() - monacoRef.current = monaco + setEditor(editor) + setMonaco(monaco) } useEffect(() => { - // if user is pasting a certificate, fix the indent - const domNode = editorRef.current.getDomNode() - domNode.addEventListener( - 'paste', - (event: { stopPropagation: () => void; preventDefault: () => void; originalEvent: any; target: any }) => { - const selection = editorRef.current.getSelection() - const pasteText = (event.originalEvent || event).clipboardData.getData('text/plain').trim() - const model = editorRef.current.getModel() - const lines = pasteText.split(/\r?\n/) - - if (selection.selectionStartLineNumber - 1 > 0 && pasteText.startsWith('-----BEGIN')) { - event.stopPropagation() - event.preventDefault() - const lines = pasteText.split(/\r?\n/) - const spaces = model.getLineContent(selection.selectionStartLineNumber - 1).search(/\S/) + 2 - const leadSpaces = spaces - selection.selectionStartColumn + 1 - const lead = ' '.repeat(leadSpaces < 0 ? spaces : leadSpaces) - const spacer = ' '.repeat(spaces) - const joint = `\r\n${spacer}` - const text = `${lead}${lines.map((line: string) => line.trim()).join(joint)}\r\n` - editorRef.current.executeEdits('my-source', [{ range: selection, text: text, forceMoveMarkers: true }]) - } - // when user is pasting in a complete yaml, do we need to make sure the resource has a namespace - if ( - autoCreateNs && // make sure resource has namespace - selection.startColumn === 1 && - selection.endLineNumber === model.getLineCount() - ) { - let nameInx - let hasMetadata = false - let hasNamespace = false - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('metadata:')) { - hasMetadata = true - } - if (hasMetadata) { - if (lines[i].includes(' name:')) { - nameInx = i + if (editor && monaco) { + // if user is pasting a certificate, fix the indent + const domNode = editor.getDomNode() + domNode?.addEventListener( + 'paste', + (event: ClipboardEvent) => { + const selection = editor.getSelection() + const pasteText = event.clipboardData?.getData('text/plain').trim() + const model = editor.getModel() + const lines = pasteText?.split(/\r?\n/) + + if (selection?.selectionStartLineNumber - 1 > 0 && pasteText.startsWith('-----BEGIN')) { + event.stopPropagation() + event.preventDefault() + const lines = pasteText.split(/\r?\n/) + const spaces = model.getLineContent(selection.selectionStartLineNumber - 1).search(/\S/) + 2 + const leadSpaces = spaces - selection.selectionStartColumn + 1 + const lead = ' '.repeat(leadSpaces < 0 ? spaces : leadSpaces) + const spacer = ' '.repeat(spaces) + const joint = `\r\n${spacer}` + const text = `${lead}${lines.map((line: string) => line.trim()).join(joint)}\r\n` + editor.executeEdits('my-source', [{ range: selection, text: text, forceMoveMarkers: true }]) + } + // when user is pasting in a complete yaml, do we need to make sure the resource has a namespace + if ( + autoCreateNs && // make sure resource has namespace + selection.startColumn === 1 && + selection.endLineNumber === model.getLineCount() + ) { + let nameInx + let hasMetadata = false + let hasNamespace = false + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('metadata:')) { + hasMetadata = true + } + if (hasMetadata) { + if (lines[i].includes(' name:')) { + nameInx = i + } + if (lines[i].includes(' namespace:')) { + hasNamespace = true + } } - if (lines[i].includes(' namespace:')) { - hasNamespace = true + if (hasNamespace || lines[i].startsWith('spec:')) { + break } } - if (hasNamespace || lines[i].startsWith('spec:')) { - break + if (nameInx && !hasNamespace) { + // add missing namespace + event.stopPropagation() + event.preventDefault() + lines.splice(nameInx + 1, 0, ' namespace: ""') + const text = lines.join('\r\n') + editor.executeEdits('my-source', [{ range: selection, text: text, forceMoveMarkers: true }]) } } - if (nameInx && !hasNamespace) { - // add missing namespace - event.stopPropagation() - event.preventDefault() - lines.splice(nameInx + 1, 0, ' namespace: ""') - const text = lines.join('\r\n') - editorRef.current.executeEdits('my-source', [{ range: selection, text: text, forceMoveMarkers: true }]) - } - } - }, - true - ) - // clear our the getEditorValue method - return () => { - window.getEditorValue = undefined + }, + true + ) + // clear our the getEditorValue method + return () => { + window.getEditorValue = undefined + } } - }, []) + }, [autoCreateNs, editor, monaco]) useEffect(() => { - monacoRef.current.editor.setTheme(readonly ? 'readonly-resource-editor' : 'resource-editor') - }, [readonly]) + if (monaco) { + monaco.editor.setTheme(readonly ? 'readonly-resource-editor' : 'resource-editor') + } + }, [monaco, readonly]) // prevent editor from flashing when typing in form - useEffect(() => { - const model = editorRef.current.getModel() - model?.onDidChangeContent(() => { - model?.forceTokenization(model?.getLineCount()) - }) - }, []) - + // useEffect(() => { + // if (editor) { + // const model = editor.getModel() + // model?.onDidChangeContent(() => { + // model?.forceTokenization(model?.getLineCount()) + // }) + // } + // }, [editor]) + + // eslint-disable-next-line react-hooks/exhaustive-deps const onMouseDown = useCallback( debounce((e) => { // if clicking on a filtered row, toggle the @@ -277,179 +295,190 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { if (mouseDownHandle) { mouseDownHandle.dispose() } - const handle = editorRef.current.onMouseDown(onMouseDown) - setMouseDownHandle(handle) - }, [filteredRows, showFiltered]) + if (editor) { + const handle = editor.onMouseDown(onMouseDown) + setMouseDownHandle(handle) + } + }, [editor, filteredRows, mouseDownHandle, onMouseDown, showFiltered]) // show tooltips over errors useEffect(() => { if (hoverProviderHandle) { hoverProviderHandle.dispose() } - const handle = monacoRef.current.languages.registerHoverProvider('yaml', { - provideHover: (_model: any, position: any) => { - return new Promise((resolve) => { - squigglyTooltips.forEach((tip: { range: { containsPosition: (arg0: any) => any }; message: string }) => { - if (tip.range.containsPosition(position)) { - return resolve({ contents: [{ value: '```html\n' + tip.message + ' \n```' }] }) - } + if (monaco) { + const handle = monaco.languages.registerHoverProvider('yaml', { + provideHover: (_model: any, position: any) => { + return new Promise((resolve) => { + squigglyTooltips.forEach((tip: { range: { containsPosition: (arg0: any) => any }; message: string }) => { + if (tip.range.containsPosition(position)) { + return resolve({ contents: [{ value: '```html\n' + tip.message + ' \n```' }] }) + } + }) + return resolve(null) }) - return resolve([]) - }) - }, - }) - setHoverProviderHandle(handle) - }, [squigglyTooltips]) + }, + }) + setHoverProviderHandle(handle) + } + }, [hoverProviderHandle, monaco, squigglyTooltips]) // prevent user from changing protected text useEffect(() => { if (keyDownHandle) { keyDownHandle.dispose() } - const handle = editorRef.current.onKeyDown( - (e: { - code: string - ctrlKey: boolean - metaKey: boolean - stopPropagation: () => void - preventDefault: () => void - }) => { - const selections = editorRef.current.getSelections() - const editor = editorRef.current - const model = editor.getModel() - const isAllSelected = - selections.length === 1 && - selections[0].startColumn === 1 && - selections[0].endLineNumber === model.getLineCount() - // if user presses enter, add new key: below this line - let endOfLineEnter = false - if (e.code === 'Enter') { - const pos = editor.getPosition() - const thisLine = model.getLineContent(pos.lineNumber) - endOfLineEnter = thisLine.length < pos.column - } - if ( - // if user clicks on readonly area, ignore - !(e.code === 'KeyC' && (e.ctrlKey || e.metaKey)) && - e.code !== 'ArrowDown' && - e.code !== 'ArrowUp' && - e.code !== 'ArrowLeft' && - e.code !== 'ArrowRight' && - !endOfLineEnter && - !isAllSelected && - !prohibited.every((prohibit: { intersectRanges: (arg: any) => any }) => { - return selections.findIndex((range: any) => prohibit.intersectRanges(range)) === -1 - }) - ) { - e.stopPropagation() - e.preventDefault() + if (editor) { + const handle = editor.onKeyDown( + (e: { + code: string + ctrlKey: boolean + metaKey: boolean + stopPropagation: () => void + preventDefault: () => void + }) => { + const selections = editor.getSelections() + const model = editor.getModel() + const isAllSelected = + selections?.length === 1 && + selections[0].startColumn === 1 && + selections[0].endLineNumber === model?.getLineCount() + // if user presses enter, add new key: below this line + let endOfLineEnter = false + if (e.code === 'Enter') { + const pos = editor.getPosition() + if (model && pos) { + const thisLine = model.getLineContent(pos.lineNumber) + endOfLineEnter = thisLine.length < pos.column + } + } + if ( + // if user clicks on readonly area, ignore + !(e.code === 'KeyC' && (e.ctrlKey || e.metaKey)) && + e.code !== 'ArrowDown' && + e.code !== 'ArrowUp' && + e.code !== 'ArrowLeft' && + e.code !== 'ArrowRight' && + !endOfLineEnter && + !isAllSelected && + !prohibited.every((prohibit: { intersectRanges: (arg: any) => any }) => { + return selections?.findIndex((range: any) => prohibit.intersectRanges(range)) === -1 + }) + ) { + e.stopPropagation() + e.preventDefault() + } } - } - ) - setKeyDownHandle(handle) - }, [prohibited]) + ) + setKeyDownHandle(handle) + } + }, [editor, keyDownHandle, prohibited]) // if editor loses focus, do form changes immediately useEffect(() => { - editorRef.current.onDidBlurEditorWidget(() => { - const editorHasFocus = !!document.querySelector('.monaco-editor.focused') - const activeId = document.activeElement?.id as string - if (!editorHasFocus && ['undo-button', 'redo-button'].indexOf(activeId) === -1) { - setClickedOnFilteredLine(false) - setEditorHasFocus(false) - } - }) - }, [setClickedOnFilteredLine, setEditorHasFocus]) + if (editor) { + editor.onDidBlurEditorWidget(() => { + const editorHasFocus = !!document.querySelector('.monaco-editor.focused') + const activeId = document.activeElement?.id as string + if (!editorHasFocus && ['undo-button', 'redo-button'].indexOf(activeId) === -1) { + setClickedOnFilteredLine(false) + setEditorHasFocus(false) + } + }) + } + }, [editor, setClickedOnFilteredLine, setEditorHasFocus]) // react to changes from form useEffect(() => { - // debounce changes from form - const formChange = () => { - if (editorHasFocus || editorHasErrors) { - return - } - // parse/validate/secrets - const model = editorRef.current?.getModel() - const { - yaml, - protectedRanges, - filteredRows, - errors, - comparison: formComparison, - change, - unredactedChange, - xreferences, - } = processForm( - monacoRef, - code, - resources, - changeStack, - showSecrets ? undefined : secrets, - showFiltered, - filters, - immutables, - readonly === true, - userEdits, - validationRef.current, - model.getValue(), - editableUidSiblings - ) - setProhibited(protectedRanges) - setFilteredRows(filteredRows) - setLastUnredactedChange(unredactedChange) - setXReferences(xreferences) - - const allErrors = [...errors.validation, ...errors.syntax] - const { yamlChanges, remainingEdits } = getFormChanges( - allErrors, - change, - userEdits, - formComparison, - lastChange, - lastFormComparison - ) + let changeTimeoutId: NodeJS.Timeout - // update yaml in editor - model.resources = cloneDeep(change.resources) - const saveDecorations = getResourceEditorDecorations(editorRef, false) - const viewState = editorRef.current?.saveViewState() - model.setValue(yaml) - editorRef.current?.restoreViewState(viewState) - editorRef.current.deltaDecorations([], saveDecorations) - setHasRedo(false) - setHasUndo(false) - - // if there were remaining edits, report to form - setStatusChanges(cloneDeep({ changes: remainingEdits, redactedChange: change, errors: allErrors })) - - // user edits that haven't been incorporated into form - setLastUserEdits(remainingEdits) - - // decorate errors, changes - const squigglyTooltips = decorate( - false, - editorHasFocus, - editorRef, - monacoRef, - [...allErrors, ...customValidationErrors], - yamlChanges, - change, - remainingEdits, - protectedRanges, - filteredRows - ) - setSquigglyTooltips(squigglyTooltips) - setLastFormComparison(formComparison) - setLastChange(change) - setUserEdits(remainingEdits) - } // if editor loses focus, update form immediately // otherwise if form already had focus, no need to call formChange - let changeTimeoutId: NodeJS.Timeout + // if editor didn't have focus before and now it does, ignore form change if (!editorHadFocus.current && editorHasFocus) { // ignore - } else { + } else if (editor && monaco) { + // debounce changes from form + const formChange = () => { + if (editorHasFocus || editorHasErrors) { + return + } + // parse/validate/secrets + const model = editor.getModel() + const { + yaml, + protectedRanges, + filteredRows, + errors, + comparison: formComparison, + change, + unredactedChange, + xreferences, + } = processForm( + monaco, + code, + resources, + changeStack, + showSecrets ? undefined : secrets, + showFiltered, + filters, + immutables, + readonly === true, + userEdits, + validationRef.current, + model?.getValue() ?? '', + editableUidSiblings + ) + setProhibited(protectedRanges) + setFilteredRows(filteredRows) + setLastUnredactedChange(unredactedChange) + setXReferences(xreferences) + + const allErrors = [...errors.validation, ...errors.syntax] + const { yamlChanges, remainingEdits } = getFormChanges( + allErrors, + change, + userEdits, + formComparison, + lastChange, + lastFormComparison + ) + + // update yaml in editor + model.resources = cloneDeep(change.resources) + const saveDecorations = getResourceEditorDecorations(editor, false) + const viewState = editor?.saveViewState() + model.setValue(yaml) + editor?.restoreViewState(viewState) + editor.deltaDecorations([], saveDecorations) + setHasRedo(false) + setHasUndo(false) + + // if there were remaining edits, report to form + setStatusChanges(cloneDeep({ changes: remainingEdits, redactedChange: change, errors: allErrors })) + + // user edits that haven't been incorporated into form + setLastUserEdits(remainingEdits) + + // decorate errors, changes + const squigglyTooltips = decorate( + false, + editorHasFocus, + editor, + monaco, + [...allErrors, ...customValidationErrors], + yamlChanges, + change, + remainingEdits, + protectedRanges, + filteredRows + ) + setSquigglyTooltips(squigglyTooltips) + setLastFormComparison(formComparison) + setLastChange(change) + setUserEdits(remainingEdits) + } // if form changed, and editor doesn't have focus (user isn't typing) process form change immediately // if form changed, and editor has focus (user is typing) process form with debounce of 1 s to allow user to type changeTimeoutId = setTimeout(formChange, !clickedOnFilteredLine && editorHasFocus ? 1000 : 100) @@ -471,98 +500,122 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { ]) // react to changes from user editing yaml - const editorChanged = (value: string, e: { isFlush: any }) => { - if (!e.isFlush) { - // parse/validate/secrets - const { - protectedRanges, - filteredRows, - errors, - comparison: userComparison, - change, - unredactedChange, - } = processUser( - monacoRef, - value, - showSecrets ? undefined : secrets, - lastUnredactedChange?.hiddenSecretsValues, - showFiltered, - filters, - lastUnredactedChange?.hiddenFilteredValues, - immutables, - readonly === true, - validationRef.current, - value, - editableUidSiblings - ) - setLastUnredactedChange(unredactedChange) - setProhibited(protectedRanges) - setFilteredRows(filteredRows) - - // determine what changes were made by user so we can decorate - // and know what form changes to block - const allErrors = [...errors.validation, ...errors.syntax] - let changes = getUserChanges(allErrors, change, lastUserEdits, userComparison, lastChange, lastChange?.parsed) - - // using cross reference created in formchange, propagate any user change to - // a path that has cross references to those other references - changes = updateReferences(changes, xreferences, unredactedChange) - - // report new resources/errors/useredits to form - // if there are validation errors still pass it to form - const editorHasErrors = - errors.syntax.length > 0 || errors.validation.filter(({ errorType }) => errorType === 'error').length > 0 - let customErrors = [] - if (!editorHasErrors) { - const clonedUnredactedChange = cloneDeep(unredactedChange) - setResourceChanges(clonedUnredactedChange) - customErrors = setFormValues(syncs, clonedUnredactedChange) || [] - setCustomValidationErrors(customErrors) - } - setEditorHasErrors(editorHasErrors) - setStatusChanges(cloneDeep({ changes, redactedChange: change, errors: allErrors })) - - // decorate errors, changes - const squigglyTooltips = decorate( - true, - editorHasFocus, - editorRef, - monacoRef, - [...allErrors, ...customErrors], - [], - change, - lastUserEdits, - protectedRanges, - filteredRows - ) - setSquigglyTooltips(squigglyTooltips) - setUserEdits(changes) - // don't set last change here--always comparing against last form - //setLastChange(change) - - // set up a change stack that can be used to reconcile user changes typed here and if/when form changes occur - if (allErrors.length === 0) { - setChangeStack({ - baseResources: changeStack?.baseResources ?? unredactedChange?.resources ?? [], - customResources: unredactedChange.resources, - }) - } + const editorChanged = useCallback( + (value: string, e: { isFlush: any }) => { + if (editor && monaco) { + if (!e.isFlush) { + // parse/validate/secrets + const { + protectedRanges, + filteredRows, + errors, + comparison: userComparison, + change, + unredactedChange, + } = processUser( + monaco, + value, + showSecrets ? undefined : secrets, + lastUnredactedChange?.hiddenSecretsValues, + showFiltered, + filters, + lastUnredactedChange?.hiddenFilteredValues, + immutables, + readonly === true, + validationRef.current, + value, + editableUidSiblings + ) + setLastUnredactedChange(unredactedChange) + setProhibited(protectedRanges) + setFilteredRows(filteredRows) + + // determine what changes were made by user so we can decorate + // and know what form changes to block + const allErrors = [...errors.validation, ...errors.syntax] + let changes = getUserChanges(allErrors, change, lastUserEdits, userComparison, lastChange, lastChange?.parsed) + + // using cross reference created in formchange, propagate any user change to + // a path that has cross references to those other references + changes = updateReferences(changes, xreferences, unredactedChange) + + // report new resources/errors/useredits to form + // if there are validation errors still pass it to form + const editorHasErrors = + errors.syntax.length > 0 || errors.validation.filter(({ errorType }) => errorType === 'error').length > 0 + let customErrors = [] + if (!editorHasErrors) { + const clonedUnredactedChange = cloneDeep(unredactedChange) + setResourceChanges(clonedUnredactedChange) + customErrors = setFormValues(syncs, clonedUnredactedChange) || [] + setCustomValidationErrors(customErrors) + } + setEditorHasErrors(editorHasErrors) + setStatusChanges(cloneDeep({ changes, redactedChange: change, errors: allErrors })) + + // decorate errors, changes + const squigglyTooltips = decorate( + true, + editorHasFocus, + editor, + monaco, + [...allErrors, ...customErrors], + [], + change, + lastUserEdits, + protectedRanges, + filteredRows + ) + setSquigglyTooltips(squigglyTooltips) + setUserEdits(changes) + // don't set last change here--always comparing against last form + //setLastChange(change) + + // set up a change stack that can be used to reconcile user changes typed here and if/when form changes occur + if (allErrors.length === 0) { + setChangeStack({ + baseResources: changeStack?.baseResources ?? unredactedChange?.resources ?? [], + customResources: unredactedChange.resources, + }) + } - // undo/redo enable - const model = editorRef.current?.getModel() - const editStack = model['_commandManager'] - setHasRedo(editStack?.future.length > 0) - setHasUndo(editStack?.currentOpenStackElement || editStack?.past.length > 0) - } - } + // undo/redo enable + const model = editor.getModel() + if (model) { + setHasRedo((model as any).canRedo()) + setHasUndo((model as any).canUndo()) + } + } + } + }, + [ + changeStack?.baseResources, + editableUidSiblings, + editor, + editorHasFocus, + filters, + immutables, + lastChange, + lastUnredactedChange?.hiddenFilteredValues, + lastUnredactedChange?.hiddenSecretsValues, + lastUserEdits, + monaco, + readonly, + secrets, + showFiltered, + showSecrets, + syncs, + xreferences, + ] + ) - const debouncedEditorChange = useMemo(() => debounce(editorChanged, 300), [lastChange, lastFormComparison, userEdits]) + const debouncedEditorChange = useMemo(() => debounce(editorChanged, 300), [editorChanged]) useEffect(() => { return () => { debouncedEditorChange.cancel() } - }, []) + }, [debouncedEditorChange]) const editorChange = useCallback( (value, e) => { @@ -584,14 +637,12 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { onEditorChange(editChanges) } } - }, [resourceChanges]) + }, [onEditorChange, resourceChanges, resources]) // report errors/user edits to form useEffect(() => { - if (statusChanges && onStatusChange) { + if (statusChanges && onStatusChange && editor && monaco) { const { changes, errors, redactedChange } = statusChanges - const editor = editorRef?.current - const monaco = monacoRef?.current // report just errors and user changes onStatusChange({ @@ -600,7 +651,7 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { changes: formatChanges(editor, monaco, changes, redactedChange, syncs), }) } - }, [statusChanges]) + }, [editor, monaco, onStatusChange, statusChanges, syncs]) /* eslint-enable react-hooks/exhaustive-deps */ const toolbarControls = useMemo(() => { @@ -617,7 +668,7 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { tooltipProps={{ content: t('Undo') }} isDisabled={!hasUndo} onClick={() => { - editorRef?.current.trigger('source', 'undo') + editor?.trigger('source', 'undo', undefined) }} /> )} @@ -630,7 +681,7 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { tooltipProps={{ content: t('Redo') }} isDisabled={!hasRedo} onClick={() => { - editorRef?.current.trigger('source', 'redo') + editor?.trigger('source', 'redo', undefined) }} /> )} @@ -641,7 +692,7 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { aria-label={t('Find')} tooltipProps={{ content: t('Find') }} onClick={() => { - editorRef?.current.trigger('source', 'actions.find') + editor?.trigger('source', 'actions.find', undefined) }} /> {/* secrets */} @@ -663,12 +714,18 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { aria-label={t('Copy to clipboard')} disabled={false} onClick={() => { - const selectedText = editorRef.current.getModel().getValueInRange(editorRef.current.getSelection()) - navigator.clipboard.writeText(selectedText.length === 0 ? lastUnredactedChange?.yaml : selectedText) - setCopyHint(selectedText.length === 0 ? allCopiedCopy : copiedCopy) - setTimeout(() => { - setCopyHint(defaultCopy) - }, 800) + if (editor && editor.getModel()) { + const model = editor.getModel() + const selection = editor.getSelection() + if (model && selection) { + const selectedText = model.getValueInRange(selection) + navigator.clipboard.writeText(selectedText || lastUnredactedChange?.yaml || '') + setCopyHint(selectedText.length === 0 ? allCopiedCopy : copiedCopy) + setTimeout(() => { + setCopyHint(defaultCopy) + }, 800) + } + } }} exitDelay={600} variant="plain" @@ -687,19 +744,20 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { ) }, [ - allCopiedCopy, - copiedCopy, - copyHint, editorTitle, - defaultCopy, + readonly, + t, hasUndo, hasRedo, - lastUnredactedChange, - onClose, - readonly, secrets, showSecrets, - t, + copyHint, + onClose, + editor, + lastUnredactedChange?.yaml, + allCopiedCopy, + copiedCopy, + defaultCopy, ]) /* eslint-disable react-hooks/exhaustive-deps */ @@ -717,8 +775,10 @@ export function SyncEditor(props: SyncEditorProps): JSX.Element { if (editorHasErrors) { height -= 75 } - editorRef?.current?.layout({ width, height }) - setShowCondensed(width < 500) + if (editor) { + editor.layout({ width, height }) + setShowCondensed(width < 500) + } }) return ( diff --git a/frontend/src/components/SyncEditor/decorate.ts b/frontend/src/components/SyncEditor/decorate.ts index 9260609ac3a..2c28e51bbf1 100644 --- a/frontend/src/components/SyncEditor/decorate.ts +++ b/frontend/src/components/SyncEditor/decorate.ts @@ -1,27 +1,18 @@ /* Copyright Contributors to the Open Cluster Management project */ import { get } from 'lodash' import { ErrorType } from './validation' +import { Monaco } from '@monaco-editor/react' +import { editor as editorTypes } from 'monaco-editor' const startCase = (str: string) => { return str.charAt(0).toUpperCase() + str.slice(1) } -interface Decoration { - id: string - options: { - inlineClassName: string - glyphMarginClassName: string - linesDecorationsClassName: string - className: string - description: string - } -} - export const decorate = ( isCustomEdit: boolean, editorHasFocus: boolean, - editorRef: any, - monacoRef: any, + editor: editorTypes.IStandaloneCodeEditor, + monaco: Monaco, errors: any[], changes: any[], change: { @@ -36,41 +27,41 @@ export const decorate = ( const squigglyTooltips: any[] = [] // errors/warnings - addErrorDecorations(monacoRef, errors, decorations, squigglyTooltips) + addErrorDecorations(monaco, errors, decorations, squigglyTooltips) // add change decorations - addChangeDecorations(isCustomEdit, monacoRef, changes, change, decorations) + addChangeDecorations(isCustomEdit, monaco, changes, change, decorations) // if form is making changes, layer any editor changes decorations on top of form changes if (preservedUserEdits.length) { - addChangeDecorations(true, monacoRef, preservedUserEdits, change, decorations) + addChangeDecorations(true, monaco, preservedUserEdits, change, decorations) } // add protected decorations - addProtectedDecorations(monacoRef, protectedRanges, decorations) + addProtectedDecorations(monaco, protectedRanges, decorations) // add filter row toggle decorations - addFilteredDecorations(monacoRef, filteredRows, decorations) + addFilteredDecorations(monaco, filteredRows, decorations) // add decorations to editor const hasErrors = errors.length > 0 - const handles = getResourceEditorDecorations(editorRef, hasErrors).map((decoration: { id: any }) => decoration.id) - editorRef.current.deltaDecorations(handles, decorations) + const handles = getResourceEditorDecorations(editor, hasErrors).map((decoration: { id: any }) => decoration.id) + editor.deltaDecorations(handles, decorations) // scroll to best line to show if (!editorHasFocus) { - scrollToChangeDecoration(editorRef, errors, decorations) + scrollToChangeDecoration(editor, errors, decorations) } return squigglyTooltips } -const addProtectedDecorations = (monacoRef: any, protectedRanges: any[], decorations: any[]) => { +const addProtectedDecorations = (monaco: Monaco, protectedRanges: any[], decorations: any[]) => { protectedRanges?.forEach((range) => { const start = range.startLineNumber const end = range.endLineNumber - 1 decorations.push({ - range: new monacoRef.current.Range(start, 1, end, 132), + range: new monaco.Range(start, 1, end, 132), options: { inlineClassName: 'protectedDecoration', description: 'resource-editor', @@ -79,10 +70,10 @@ const addProtectedDecorations = (monacoRef: any, protectedRanges: any[], decorat }) } -const addFilteredDecorations = (monacoRef: any, filteredRows: any[], decorations: any[]) => { +const addFilteredDecorations = (monaco: Monaco, filteredRows: any[], decorations: any[]) => { filteredRows?.forEach((row) => { decorations.push({ - range: new monacoRef.current.Range(row, 0, row, 132), + range: new monaco.Range(row, 0, row, 132), options: { after: { content: '\u200b', inlineClassName: 'inline-folded' }, description: 'resource-editor', @@ -91,7 +82,7 @@ const addFilteredDecorations = (monacoRef: any, filteredRows: any[], decorations }) } -const addErrorDecorations = (monacoRef: any, errors: any[], decorations: any[], squigglyTooltips: any[]) => { +const addErrorDecorations = (monaco: Monaco, errors: any[], decorations: any[], squigglyTooltips: any[]) => { errors.forEach((error: { linePos: any; message: any; errorType: ErrorType }) => { const { linePos, message, errorType } = error const start = linePos?.start?.line ?? 0 @@ -124,12 +115,12 @@ const addErrorDecorations = (monacoRef: any, errors: any[], decorations: any[], } decorations.push({ - range: new monacoRef.current.Range(start, 0, start, 132), + range: new monaco.Range(start, 0, start, 132), options, }) // squiggly line under error - const range = new monacoRef.current.Range( + const range = new monaco.Range( start, linePos?.start?.col ?? 0, linePos?.end?.line ?? start, @@ -150,7 +141,7 @@ const addErrorDecorations = (monacoRef: any, errors: any[], decorations: any[], const addChangeDecorations = ( isCustomEdit: boolean, - monacoRef: any, + monaco: Monaco, changes: any[], change: { parsed: { [name: string]: any[] } @@ -163,7 +154,7 @@ const addChangeDecorations = ( const obj: any = get(change.mappings, $a) if (obj) { decorations.push({ - range: new monacoRef.current.Range(obj.$r, 0, obj.$r + ($t === 'N' ? obj.$l - 1 : 0), 0), + range: new monaco.Range(obj.$r, 0, obj.$r + ($t === 'N' ? obj.$l - 1 : 0), 0), options: { isWholeLine: true, linesDecorationsClassName: isCustomEdit ? 'customLineDecoration' : 'insertedLineDecoration', @@ -174,7 +165,7 @@ const addChangeDecorations = ( }) if ($f != null && $f.toString().length < 132 && !obj.$s) { decorations.push({ - range: new monacoRef.current.Range(obj.$r, 0, obj.$r, 132), + range: new monaco.Range(obj.$r, 0, obj.$r, 132), options: { after: { content: ` # ${$f}`, inlineClassName: 'protectedDecoration' }, description: 'resource-editor', @@ -185,16 +176,16 @@ const addChangeDecorations = ( }) } -export const getResourceEditorDecorations = (editorRef: any, hasErrors: boolean) => { +export const getResourceEditorDecorations = (editor: editorTypes.IStandaloneCodeEditor, hasErrors: boolean) => { // clear resource-editor decorations // don't filter protectedDecoration if there are errors because parser doesn't know where protected // areas are so only previous decorations do - const model = editorRef.current?.getModel() - let decorations: Decoration[] = model ? model.getAllDecorations() : [] + const model = editor?.getModel() + let decorations = model ? model.getAllDecorations() : [] decorations = decorations.filter(({ options }) => { return ( options?.className?.startsWith('squiggly-') || - ['customLineDecoration', 'insertedLineDecoration'].includes(options?.linesDecorationsClassName) || + ['customLineDecoration', 'insertedLineDecoration'].includes(options?.linesDecorationsClassName ?? '') || (!!options?.glyphMarginClassName && (options?.inlineClassName !== 'protectedDecoration' || !hasErrors)) ) }) @@ -202,8 +193,7 @@ export const getResourceEditorDecorations = (editorRef: any, hasErrors: boolean) return decorations } -const scrollToChangeDecoration = (editorRef: any, errors: any[], decorations: any[]) => { - const editor = editorRef.current +const scrollToChangeDecoration = (editor: editorTypes.IStandaloneCodeEditor, errors: any[], decorations: any[]) => { const visibleRange = editor.getVisibleRanges()[0] if (visibleRange) { // if any errors and not in visible range, and first error isn't visible, scroll to it diff --git a/frontend/src/components/SyncEditor/process.ts b/frontend/src/components/SyncEditor/process.ts index 55096b91346..c8c091b7d36 100644 --- a/frontend/src/components/SyncEditor/process.ts +++ b/frontend/src/components/SyncEditor/process.ts @@ -5,6 +5,7 @@ import { getErrors, validate } from './validation' import { getMatchingValues, getUidSiblings, crossReference, getPathArray } from './synchronize' import { reconcile } from './reconcile' import { ChangeType } from './changes' +import { Monaco } from '@monaco-editor/react' export interface ProcessedType { parsed: { @@ -36,7 +37,7 @@ export interface CachedValuesType { } export const processForm = ( - monacoRef: any, + monaco: Monaco, code: string | undefined, resourceArr: unknown, changeStack: @@ -87,7 +88,7 @@ export const processForm = ( comparison, xreferences: crossReference(paths), ...process( - monacoRef, + monaco, yaml, documents, syntaxErrors, @@ -107,7 +108,7 @@ export const processForm = ( } export const processUser = ( - monacoRef: any, + monaco: Monaco, yaml: string, secrets: (string | string[])[] | undefined, cachedSecrets: CachedValuesType[] | undefined, @@ -132,7 +133,7 @@ export const processUser = ( return { comparison, ...process( - monacoRef, + monaco, yaml, documents, syntaxErrors, @@ -152,7 +153,7 @@ export const processUser = ( } const process = ( - monacoRef: any, + monaco: Monaco, yaml: string, documents: any, syntaxErrors: any[], @@ -241,7 +242,7 @@ const process = ( if (value && value.$p) { const range = get(mappings, getPathArray(value.$p)) if (range?.$r) { - protectedRanges.push(new monacoRef.current.Range(range.$r, 0, range.$r + range.$l, 0)) + protectedRanges.push(new monaco.Range(range.$r, 0, range.$r + range.$l, 0)) } } }) diff --git a/frontend/src/components/TemplateEditor/components/YamlEditor.js b/frontend/src/components/TemplateEditor/components/YamlEditor.js index 9b3061c6e06..1f443ff848f 100644 --- a/frontend/src/components/TemplateEditor/components/YamlEditor.js +++ b/frontend/src/components/TemplateEditor/components/YamlEditor.js @@ -172,13 +172,13 @@ class YamlEditor extends React.Component { ) } - componentDidUpdate() { - // stop flickering - if (this.editor && this.editor.getModel()) { - const model = this.editor.getModel() - model.forceTokenization(model.getLineCount()) - } - } + // componentDidUpdate() { + // // stop flickering + // if (this.editor && this.editor.getModel()) { + // const model = this.editor.getModel() + // model.forceTokenization(model.getLineCount()) + // } + // } render() { const { yaml, readOnly, hide = false, showCondensed } = this.props