From ad5b013d737502bb3b07d13e0bf290e8a84bc5d9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 13 Dec 2019 10:58:16 +0100 Subject: [PATCH] [Console] Fix load from remote (#52814) (#52895) * Fix load remote state * Clean up variable usage, add comment, move forceRetokenize to private method * Optimize sequence of checking hash on initial load --- .../editor/legacy/console_editor/editor.tsx | 77 +++++++++++++++---- .../application/hooks/use_set_input_editor.ts | 13 ++-- .../legacy_core_editor/legacy_core_editor.ts | 35 +++++---- 3 files changed, 90 insertions(+), 35 deletions(-) diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx index 442ed330e9b7a..a52c15c20c902 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx @@ -20,6 +20,11 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; + +// Node v5 querystring for browser. +// @ts-ignore +import * as qs from 'querystring-browser'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useServicesContext, useEditorReadContext } from '../../../../contexts'; @@ -80,17 +85,64 @@ function EditorUI() { useEffect(() => { editorInstanceRef.current = senseEditor.create(editorRef.current!); + const editor = editorInstanceRef.current; + + const readQueryParams = () => { + const [, queryString] = (window.location.hash || '').split('?'); + return qs.parse(queryString || ''); + }; + + const loadBufferFromRemote = (url: string) => { + if (/^https?:\/\//.test(url)) { + const loadFrom: Record = { + url, + // Having dataType here is required as it doesn't allow jQuery to `eval` content + // coming from the external source thereby preventing XSS attack. + dataType: 'text', + kbnXsrfToken: false, + }; + + if (/https?:\/\/api\.github\.com/.test(url)) { + loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' }; + } - const { content: text } = history.getSavedEditorState() || { - content: DEFAULT_INPUT_VALUE, + // Fire and forget. + $.ajax(loadFrom).done(async data => { + const coreEditor = editor.getCoreEditor(); + await editor.update(data, true); + editor.moveToNextRequestEdge(false); + coreEditor.clearSelection(); + editor.highlightCurrentRequestsAndUpdateActionBar(); + coreEditor.getContainer().focus(); + }); + } }; - editorInstanceRef.current.update(text); + + // Support for loading a console snippet from a remote source, like support docs. + const onHashChange = debounce(() => { + const { load_from: url } = readQueryParams(); + if (!url) { + return; + } + loadBufferFromRemote(url); + }, 200); + window.addEventListener('hashchange', onHashChange); + + const initialQueryParams = readQueryParams(); + if (initialQueryParams.load_from) { + loadBufferFromRemote(initialQueryParams.load_from); + } else { + const { content: text } = history.getSavedEditorState() || { + content: DEFAULT_INPUT_VALUE, + }; + editor.update(text); + } function setupAutosave() { let timer: number; const saveDelay = 500; - editorInstanceRef.current!.getCoreEditor().on('change', () => { + editor.getCoreEditor().on('change', () => { if (timer) { clearTimeout(timer); } @@ -100,35 +152,34 @@ function EditorUI() { function saveCurrentState() { try { - const content = editorInstanceRef.current!.getCoreEditor().getValue(); + const content = editor.getCoreEditor().getValue(); history.updateCurrentState(content); } catch (e) { // Ignoring saving error } } - setInputEditor(editorInstanceRef.current); + setInputEditor(editor); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); - const unsubscribeResizer = subscribeResizeChecker( - editorRef.current!, - editorInstanceRef.current.getCoreEditor() - ); + const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor.getCoreEditor()); setupAutosave(); return () => { unsubscribeResizer(); mappings.clearSubscriptions(); + window.removeEventListener('hashchange', onHashChange); }; }, [history, setInputEditor]); useEffect(() => { - applyCurrentSettings(editorInstanceRef.current!.getCoreEditor(), settings); + const { current: editor } = editorInstanceRef; + applyCurrentSettings(editor!.getCoreEditor(), settings); // Preserve legacy focus behavior after settings have updated. - editorInstanceRef - .current!.getCoreEditor() + editor! + .getCoreEditor() .getContainer() .focus(); }, [settings]); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts index 672f3e269ead9..fbd53762c27e6 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts @@ -16,15 +16,18 @@ * specific language governing permissions and limitations * under the License. */ - +import { useCallback } from 'react'; import { useEditorActionContext } from '../contexts/editor_context'; import { instance as registry } from '../contexts/editor_context/editor_registry'; export const useSetInputEditor = () => { const dispatch = useEditorActionContext(); - return (editor: any) => { - dispatch({ type: 'setInputEditor', payload: editor }); - registry.setInputEditor(editor); - }; + return useCallback( + (editor: any) => { + dispatch({ type: 'setInputEditor', payload: editor }); + registry.setInputEditor(editor); + }, + [dispatch] + ); }; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts index 621f4eeb0163e..608c73335b3e5 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts @@ -104,25 +104,12 @@ export class LegacyCoreEditor implements CoreEditor { return this.editor.getValue(); } - setValue(text: string, forceRetokenize: boolean): Promise { + async setValue(text: string, forceRetokenize: boolean): Promise { const session = this.editor.getSession(); session.setValue(text); - return new Promise(resolve => { - if (!forceRetokenize) { - // resolve immediately - resolve(); - return; - } - - // force update of tokens, but not on this thread to allow for ace rendering. - setTimeout(function() { - let i; - for (i = 0; i < session.getLength(); i++) { - session.getTokens(i); - } - resolve(); - }); - }); + if (forceRetokenize) { + await this.forceRetokenize(); + } } getLineValue(lineNumber: number): string { @@ -241,6 +228,20 @@ export class LegacyCoreEditor implements CoreEditor { return Boolean((this.editor as any).completer && (this.editor as any).completer.activated); } + private forceRetokenize() { + const session = this.editor.getSession(); + return new Promise(resolve => { + // force update of tokens, but not on this thread to allow for ace rendering. + setTimeout(function() { + let i; + for (i = 0; i < session.getLength(); i++) { + session.getTokens(i); + } + resolve(); + }); + }); + } + // eslint-disable-next-line @typescript-eslint/camelcase private DO_NOT_USE_onPaste(text: string) { if (text && curl.detectCURL(text)) {