From bb3d4fb77ed928a85819bbad1350559977c10103 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Tue, 26 Mar 2024 16:47:13 +0100 Subject: [PATCH 01/12] [Console] Implement a sense editor for monaco --- packages/kbn-monaco/index.ts | 9 +- .../src/ace_migration/setup_worker.ts | 2 +- .../src/console/console_errors_provider.ts | 56 ++++++++++++ .../console_parsed_requests_provider.ts | 25 ++++++ .../src/console/console_worker_proxy.ts | 39 +++++++++ packages/kbn-monaco/src/console/index.ts | 4 + packages/kbn-monaco/src/console/language.ts | 17 ++-- packages/kbn-monaco/src/console/parser.js | 46 ++++++++-- packages/kbn-monaco/src/console/types.ts | 29 +++++++ .../src/console/worker/console_worker.ts | 13 +-- .../editor/monaco/monaco_editor.tsx | 52 +++++++++++- .../models/monaco_sense_editor/index.ts | 11 +++ .../monaco_sense_editor/init_sense_editor.ts | 14 +++ .../monaco_sense_editor.ts | 85 +++++++++++++++++++ .../models/monaco_sense_editor/utils.ts | 31 +++++++ 15 files changed, 411 insertions(+), 22 deletions(-) create mode 100644 packages/kbn-monaco/src/console/console_errors_provider.ts create mode 100644 packages/kbn-monaco/src/console/console_parsed_requests_provider.ts create mode 100644 packages/kbn-monaco/src/console/console_worker_proxy.ts create mode 100644 packages/kbn-monaco/src/console/types.ts create mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/index.ts create mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts create mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts create mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/utils.ts diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index 25f683aacb247..d1e5e75a923dd 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -32,4 +32,11 @@ import { registerLanguage } from './src/helpers'; export { BarePluginApi, registerLanguage }; export * from './src/types'; -export { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from './src/console'; +export { + CONSOLE_LANG_ID, + CONSOLE_THEME_ID, + getParsedRequestsProvider, + ConsoleParsedRequestsProvider, +} from './src/console'; + +export type { ParsedRequest } from './src/console'; diff --git a/packages/kbn-monaco/src/ace_migration/setup_worker.ts b/packages/kbn-monaco/src/ace_migration/setup_worker.ts index 86c815398b08f..4ad6b1dabc906 100644 --- a/packages/kbn-monaco/src/ace_migration/setup_worker.ts +++ b/packages/kbn-monaco/src/ace_migration/setup_worker.ts @@ -49,7 +49,7 @@ export const setupWorker = ( } const { dispose } = model.onDidChangeContent(async () => { - updateAnnotations(model); + await updateAnnotations(model); }); model.onWillDispose(() => { diff --git a/packages/kbn-monaco/src/console/console_errors_provider.ts b/packages/kbn-monaco/src/console/console_errors_provider.ts new file mode 100644 index 0000000000000..e3a4e1acc2948 --- /dev/null +++ b/packages/kbn-monaco/src/console/console_errors_provider.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ConsoleWorkerProxyService } from './console_worker_proxy'; +import { CONSOLE_LANG_ID } from './constants'; +import { monaco } from '../monaco_imports'; + +export const setupConsoleErrorsProvider = (workerProxyService: ConsoleWorkerProxyService) => { + const updateErrorMarkers = async (model: monaco.editor.IModel): Promise => { + if (model.isDisposed()) { + return; + } + const parseResult = await workerProxyService.getParseResult(model.uri); + + if (!parseResult) { + return; + } + const { errors } = parseResult; + monaco.editor.setModelMarkers( + model, + CONSOLE_LANG_ID, + errors.map(({ at, text }) => { + const { column, lineNumber } = model.getPositionAt(at); + return { + startLineNumber: lineNumber, + startColumn: column, + endLineNumber: lineNumber, + endColumn: column, + message: text, + severity: monaco.MarkerSeverity.Error, + }; + }) + ); + }; + const onModelAdd = (model: monaco.editor.IModel) => { + if (model.getLanguageId() !== CONSOLE_LANG_ID) { + return; + } + + const { dispose } = model.onDidChangeContent(async () => { + await updateErrorMarkers(model); + }); + + model.onWillDispose(() => { + dispose(); + }); + + updateErrorMarkers(model); + }; + monaco.editor.onDidCreateModel(onModelAdd); +}; diff --git a/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts new file mode 100644 index 0000000000000..7802612e7179f --- /dev/null +++ b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ConsoleWorkerProxyService } from './console_worker_proxy'; +import { ParsedRequest } from './types'; +import { monaco } from '../monaco_imports'; + +export class ConsoleParsedRequestsProvider { + constructor( + private workerProxyService: ConsoleWorkerProxyService, + private model: monaco.editor.ITextModel | null + ) {} + public async getRequests(): Promise { + if (!this.model) { + return []; + } + const parseResult = await this.workerProxyService.getParseResult(this.model.uri); + return parseResult?.requests ?? []; + } +} diff --git a/packages/kbn-monaco/src/console/console_worker_proxy.ts b/packages/kbn-monaco/src/console/console_worker_proxy.ts new file mode 100644 index 0000000000000..bf8c9b3decded --- /dev/null +++ b/packages/kbn-monaco/src/console/console_worker_proxy.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CONSOLE_LANG_ID } from '../..'; +import { monaco } from '../monaco_imports'; +import { ConsoleParseResult, ConsoleWorkerDefinition } from './types'; + +export class ConsoleWorkerProxyService { + private worker: monaco.editor.MonacoWebWorker | undefined; + + public async getParseResult(modelUri: monaco.Uri): Promise { + if (!this.worker) { + throw new Error('Worker Proxy Service has not been setup!'); + } + await this.worker.withSyncedResources([modelUri]); + const parser = await this.worker.getProxy(); + return parser.getParseResult(modelUri.toString()); + } + + public async getWorker(modelUri: monaco.Uri): Promise { + if (!this.worker) { + throw new Error('Worker Proxy Service has not been setup!'); + } + await this.worker.withSyncedResources([modelUri]); + return this.worker.getProxy(); + } + public setup() { + this.worker = monaco.editor.createWebWorker({ label: CONSOLE_LANG_ID, moduleId: '' }); + } + + public stop() { + if (this.worker) this.worker.dispose(); + } +} diff --git a/packages/kbn-monaco/src/console/index.ts b/packages/kbn-monaco/src/console/index.ts index e94ebdd0ccae5..91f8937aa0ab4 100644 --- a/packages/kbn-monaco/src/console/index.ts +++ b/packages/kbn-monaco/src/console/index.ts @@ -24,3 +24,7 @@ export const ConsoleLang: LangModuleType = { lexerRules, languageConfiguration, }; + +export type { ParsedRequest } from './types'; +export { getParsedRequestsProvider } from './language'; +export { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider'; diff --git a/packages/kbn-monaco/src/console/language.ts b/packages/kbn-monaco/src/console/language.ts index 64a9dee1996b7..ba597ea2f7078 100644 --- a/packages/kbn-monaco/src/console/language.ts +++ b/packages/kbn-monaco/src/console/language.ts @@ -6,14 +6,19 @@ * Side Public License, v 1. */ -import { ConsoleWorker } from './worker'; -import { WorkerProxyService } from '../ace_migration/worker_proxy'; +import { setupConsoleErrorsProvider } from './console_errors_provider'; +import { ConsoleWorkerProxyService } from './console_worker_proxy'; import { monaco } from '../monaco_imports'; import { CONSOLE_LANG_ID } from './constants'; -import { setupWorker } from '../ace_migration/setup_worker'; +import { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider'; + +const workerProxyService = new ConsoleWorkerProxyService(); + +export const getParsedRequestsProvider = (model: monaco.editor.ITextModel | null) => { + return new ConsoleParsedRequestsProvider(workerProxyService, model); +}; -const OWNER = 'CONSOLE_GRAMMAR_CHECKER'; -const wps = new WorkerProxyService(); monaco.languages.onLanguage(CONSOLE_LANG_ID, async () => { - setupWorker(CONSOLE_LANG_ID, OWNER, wps); + workerProxyService.setup(); + setupConsoleErrorsProvider(workerProxyService); }); diff --git a/packages/kbn-monaco/src/console/parser.js b/packages/kbn-monaco/src/console/parser.js index 20884180b4260..0c9d2b5ef16b2 100644 --- a/packages/kbn-monaco/src/console/parser.js +++ b/packages/kbn-monaco/src/console/parser.js @@ -27,6 +27,35 @@ export const createParser = () => { annotate = function (type, text) { annos.push({ type: type, text: text, at: at }); }, + requestEvents, + getLastRequest = function() { + return requestEvents.length > 0 ? requestEvents.pop() : {}; + }, + addRequestStart = function() { + requestEvents.push({startOffset: at}); + }, + addRequestEnd = function() { + const lastRequest = getLastRequest(); + lastRequest.endOffset = at; + requestEvents.push(lastRequest); + }, + addRequestMethod = function(method) { + const lastRequest = getLastRequest(); + lastRequest.method = method; + requestEvents.push(lastRequest); + }, + addRequestUrl = function(url) { + const lastRequest = getLastRequest(); + lastRequest.url = url; + requestEvents.push(lastRequest); + }, + addRequestData = function(data) { + const lastRequest = getLastRequest(); + const dataArray = lastRequest.data || []; + dataArray.push(data); + lastRequest.data = dataArray; + requestEvents.push(lastRequest); + }, error = function (m) { throw { name: 'SyntaxError', @@ -373,14 +402,18 @@ export const createParser = () => { }, request = function () { white(); - method(); + addRequestStart(); + const parsedMethod = method(); + addRequestMethod(parsedMethod); strictWhite(); - url(); + const parsedUrl = url(); + addRequestUrl(parsedUrl ); strictWhite(); // advance to one new line newLine(); strictWhite(); if (ch == '{') { - object(); + const parsedObject = object(); + addRequestData(parsedObject); } // multi doc request strictWhite(); // advance to one new line @@ -388,11 +421,13 @@ export const createParser = () => { strictWhite(); while (ch == '{') { // another object - object(); + const parsedObject = object(); + addRequestData(parsedObject); strictWhite(); newLine(); strictWhite(); } + addRequestEnd(); }, comment = function () { while (ch == '#') { @@ -433,6 +468,7 @@ export const createParser = () => { text = source; at = 0; annos = []; + requestEvents = []; next(); multi_request(); white(); @@ -440,7 +476,7 @@ export const createParser = () => { annotate('error', 'Syntax error'); } - result = { annotations: annos }; + result = { errors: annos, requests: requestEvents }; return typeof reviver === 'function' ? (function walk(holder, key) { diff --git a/packages/kbn-monaco/src/console/types.ts b/packages/kbn-monaco/src/console/types.ts new file mode 100644 index 0000000000000..777e75763fd28 --- /dev/null +++ b/packages/kbn-monaco/src/console/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface ErrorAnnotation { + at: number; + text: string; +} + +export interface ParsedRequest { + startOffset: number; + endOffset: number; + method: string; + url: string; + data: Array>; +} +export interface ConsoleParseResult { + errors: ErrorAnnotation[]; + requests: ParsedRequest[]; +} + +export interface ConsoleWorkerDefinition { + getParseResult: (modelUri: string) => ConsoleParseResult | undefined; +} +export type ConsoleParser = (source: string) => ConsoleParseResult | undefined; diff --git a/packages/kbn-monaco/src/console/worker/console_worker.ts b/packages/kbn-monaco/src/console/worker/console_worker.ts index 0e62994ad86f0..3cd799c69a9d9 100644 --- a/packages/kbn-monaco/src/console/worker/console_worker.ts +++ b/packages/kbn-monaco/src/console/worker/console_worker.ts @@ -8,20 +8,23 @@ /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { Parser, ParseResult } from '../../ace_migration/types'; +import { ConsoleParseResult, ConsoleWorkerDefinition, ConsoleParser } from '../types'; import { createParser } from '../parser'; -export class ConsoleWorker { +export class ConsoleWorker implements ConsoleWorkerDefinition { + private parser: ConsoleParser | undefined; + private parseResult: ConsoleParseResult | undefined; + constructor(private ctx: monaco.worker.IWorkerContext) {} - private parser: Parser | undefined; - async parse(modelUri: string): Promise { + getParseResult(modelUri: string): ConsoleParseResult | undefined { if (!this.parser) { this.parser = createParser(); } const model = this.ctx.getMirrorModels().find((m) => m.uri.toString() === modelUri); if (model) { - return this.parser(model.getValue()); + this.parseResult = this.parser(model.getValue()); } + return this.parseResult; } } diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 292cf6a422172..99adc9a93a12e 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -6,12 +6,16 @@ * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { CodeEditor } from '@kbn/code-editor'; import { css } from '@emotion/react'; import { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from '@kbn/monaco'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MonacoSenseEditor, initSenseEditor } from '../../../models/monaco_sense_editor'; import { useSetInitialValue } from './use_set_initial_value'; import { useServicesContext, useEditorReadContext } from '../../../contexts'; +import { ConsoleMenu } from '../../../components'; export interface EditorProps { initialTextValue: string; @@ -19,11 +23,11 @@ export interface EditorProps { export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const { - services: { - notifications: { toasts }, - }, + services: { notifications, esHostService }, } = useServicesContext(); + const { toasts } = notifications; const { settings } = useEditorReadContext(); + const editorInstanceRef = useRef(null); const [value, setValue] = useState(initialTextValue); @@ -39,6 +43,43 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { width: 100%; `} > + + + + {}} + data-test-subj="sendRequestButton" + aria-label={i18n.translate('console.sendRequestButtonTooltip', { + defaultMessage: 'Click to send request', + })} + > + + + + + + { + return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost()); + }} + getDocumentation={() => { + return Promise.resolve(null); + }} + autoIndent={() => {}} + notifications={notifications} + /> + + { wordWrap: settings.wrapMode === true ? 'on' : 'off', theme: CONSOLE_THEME_ID, }} + editorDidMount={async (editor) => { + editorInstanceRef.current = await initSenseEditor(editor); + }} /> ); diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/index.ts b/src/plugins/console/public/application/models/monaco_sense_editor/index.ts new file mode 100644 index 0000000000000..9dd939ad1f210 --- /dev/null +++ b/src/plugins/console/public/application/models/monaco_sense_editor/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { initSenseEditor } from './init_sense_editor'; +export { MonacoSenseEditor } from './monaco_sense_editor'; +export { removeTrailingWhitespaces, replaceVariables } from './utils'; diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts b/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts new file mode 100644 index 0000000000000..0fff793be4ba0 --- /dev/null +++ b/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { monaco } from '@kbn/monaco'; +import { MonacoSenseEditor } from './monaco_sense_editor'; + +export async function initSenseEditor(editor: monaco.editor.IStandaloneCodeEditor) { + return new MonacoSenseEditor(editor); +} diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts b/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts new file mode 100644 index 0000000000000..73553b1113967 --- /dev/null +++ b/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + ConsoleParsedRequestsProvider, + getParsedRequestsProvider, + monaco, + ParsedRequest, +} from '@kbn/monaco'; +import { removeTrailingWhitespaces, replaceVariables } from './utils'; +import { constructUrl } from '../../../lib/es/es'; +import { getStorage, StorageKeys } from '../../../services'; +import { DEFAULT_VARIABLES } from '../../../../common/constants'; +import { DevToolsVariable } from '../../components'; + +const getCurlRequest = ( + parsedRequest: ParsedRequest, + elasticsearchBaseUrl: string, + variables: DevToolsVariable[] +) => { + let url = removeTrailingWhitespaces(parsedRequest.url); + url = replaceVariables(url, variables); + url = constructUrl(elasticsearchBaseUrl, url); + let curlRequest = `curl -X${parsedRequest.method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; + if (parsedRequest.data && parsedRequest.data.length > 0) { + curlRequest += ` -H "Content-Type: application/json" -d'\n`; + for (const data of parsedRequest.data) { + curlRequest += replaceVariables(JSON.stringify(data, null, 2), variables); + } + curlRequest += "'"; + } + return curlRequest; +}; +export class MonacoSenseEditor { + private parsedRequestsProvider: ConsoleParsedRequestsProvider; + constructor(private editor: monaco.editor.IStandaloneCodeEditor) { + this.parsedRequestsProvider = getParsedRequestsProvider(editor.getModel()); + } + + private async getRequestsInRange(): Promise { + const model = this.editor.getModel(); + const selection = this.editor.getSelection(); + if (!model || !selection) { + return Promise.resolve([]); + } + const { startLineNumber, startColumn, endLineNumber, endColumn } = selection; + const selectionStartOffset = model.getOffsetAt({ + lineNumber: startLineNumber, + column: startColumn, + }); + const selectionEndOffset = model.getOffsetAt({ lineNumber: endLineNumber, column: endColumn }); + const parsedRequests = await this.parsedRequestsProvider.getRequests(); + const selectedRequests = []; + for (const parsedRequest of parsedRequests) { + const { startOffset: requestStart, endOffset: requestEnd } = parsedRequest; + if (requestStart - 1 >= selectionEndOffset) { + // request is past the selection, no need to check further requests + break; + } + if (requestEnd - 1 < selectionStartOffset) { + // request is before the selection, do nothing + } else { + // request is selected + selectedRequests.push(parsedRequest); + } + } + return selectedRequests; + } + public async getRequestsAsCURL(elasticsearchBaseUrl: string): Promise { + // get variables values + const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); + // get selected requests + const requests = await this.getRequestsInRange(); + + const curlRequests = requests.map((request) => + getCurlRequest(request, elasticsearchBaseUrl, variables) + ); + return curlRequests.join('\n'); + } +} diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts b/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts new file mode 100644 index 0000000000000..096b35f31ec33 --- /dev/null +++ b/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DevToolsVariable } from '../../components'; + +const whitespacesRegex = /\s/; +export const removeTrailingWhitespaces = (url: string): string => { + /* + * This helper removes any trailing inline comments, for example + * "_search // comment" -> "_search" + * Ideally the parser removes those comments initially + */ + return url.trim().split(whitespacesRegex)[0]; +}; + +const variableRegex = /\${(\w+)}/g; +export const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { + if (variableRegex.test(text)) { + text = text.replaceAll(variableRegex, (match, key) => { + const variable = variables.find(({ name }) => name === key); + + return variable?.value ?? match; + }); + } + return text; +}; From 059277d1b23ea31edc6e8ee2bf949d33035d31b2 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Tue, 2 Apr 2024 17:28:31 +0200 Subject: [PATCH 02/12] [Console] Implement "send current request" --- packages/kbn-monaco/src/console/types.ts | 2 +- .../editor/monaco/monaco_editor.tsx | 40 +++-- .../monaco/monaco_editor_actions_provider.ts | 140 ++++++++++++++++++ .../containers/editor/monaco/utils.ts | 76 ++++++++++ .../models/monaco_sense_editor/index.ts | 11 -- .../monaco_sense_editor/init_sense_editor.ts | 14 -- .../monaco_sense_editor.ts | 85 ----------- .../models/monaco_sense_editor/utils.ts | 31 ---- 8 files changed, 243 insertions(+), 156 deletions(-) create mode 100644 src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts create mode 100644 src/plugins/console/public/application/containers/editor/monaco/utils.ts delete mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/index.ts delete mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts delete mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts delete mode 100644 src/plugins/console/public/application/models/monaco_sense_editor/utils.ts diff --git a/packages/kbn-monaco/src/console/types.ts b/packages/kbn-monaco/src/console/types.ts index 777e75763fd28..aa5ebc4fce053 100644 --- a/packages/kbn-monaco/src/console/types.ts +++ b/packages/kbn-monaco/src/console/types.ts @@ -16,7 +16,7 @@ export interface ParsedRequest { endOffset: number; method: string; url: string; - data: Array>; + data?: Array>; } export interface ConsoleParseResult { errors: ErrorAnnotation[]; diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 99adc9a93a12e..7805a2e02c05f 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -6,16 +6,20 @@ * Side Public License, v 1. */ -import React, { useEffect, useRef, useState } from 'react'; -import { CodeEditor } from '@kbn/code-editor'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { css } from '@emotion/react'; +import { CodeEditor } from '@kbn/code-editor'; import { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from '@kbn/monaco'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MonacoSenseEditor, initSenseEditor } from '../../../models/monaco_sense_editor'; -import { useSetInitialValue } from './use_set_initial_value'; -import { useServicesContext, useEditorReadContext } from '../../../contexts'; import { ConsoleMenu } from '../../../components'; +import { + useServicesContext, + useEditorReadContext, + useRequestActionContext, +} from '../../../contexts'; +import { useSetInitialValue } from './use_set_initial_value'; +import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider'; export interface EditorProps { initialTextValue: string; @@ -23,12 +27,22 @@ export interface EditorProps { export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const { - services: { notifications, esHostService }, + services: { notifications, esHostService, trackUiMetric, http }, } = useServicesContext(); const { toasts } = notifications; const { settings } = useEditorReadContext(); - const editorInstanceRef = useRef(null); + const dispatch = useRequestActionContext(); + const actionsProvider = useRef(null); + const getCurl = useCallback(async (): Promise => { + return actionsProvider.current + ? actionsProvider.current.getCurl(esHostService.getHost()) + : Promise.resolve(''); + }, [esHostService]); + + const sendRequests = useCallback(async () => { + await actionsProvider.current?.sendRequests(toasts, dispatch, trackUiMetric, http); + }, [dispatch, http, toasts, trackUiMetric]); const [value, setValue] = useState(initialTextValue); const setInitialValue = useSetInitialValue({ initialTextValue, setValue, toasts }); @@ -57,7 +71,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { > {}} + onClick={sendRequests} data-test-subj="sendRequestButton" aria-label={i18n.translate('console.sendRequestButtonTooltip', { defaultMessage: 'Click to send request', @@ -69,9 +83,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { { - return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost()); - }} + getCurl={getCurl} getDocumentation={() => { return Promise.resolve(null); }} @@ -91,8 +103,8 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { wordWrap: settings.wrapMode === true ? 'on' : 'off', theme: CONSOLE_THEME_ID, }} - editorDidMount={async (editor) => { - editorInstanceRef.current = await initSenseEditor(editor); + editorDidMount={(editor) => { + actionsProvider.current = new MonacoEditorActionsProvider(editor); }} /> diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts new file mode 100644 index 0000000000000..84c42b962093f --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + ConsoleParsedRequestsProvider, + getParsedRequestsProvider, + monaco, + ParsedRequest, +} from '@kbn/monaco'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { i18n } from '@kbn/i18n'; +import { Dispatch } from 'react'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import { sendRequest } from '../../../hooks/use_send_current_request/send_request'; +import { DEFAULT_VARIABLES } from '../../../../../common/constants'; +import { MetricsTracker } from '../../../../types'; +import { Actions } from '../../../stores/request'; +import { getStorage, StorageKeys } from '../../../../services'; +import { + stringifyRequest, + replaceRequestVariables, + getCurlRequest, + trackSentRequests, +} from './utils'; + +export interface EditorRequest { + method: string; + url: string; + data: string[]; +} +export class MonacoEditorActionsProvider { + private parsedRequestsProvider: ConsoleParsedRequestsProvider; + constructor(private editor: monaco.editor.IStandaloneCodeEditor) { + this.parsedRequestsProvider = getParsedRequestsProvider(editor.getModel()); + } + + private async getParsedRequests(): Promise { + const model = this.editor.getModel(); + const selection = this.editor.getSelection(); + if (!model || !selection) { + return Promise.resolve([]); + } + const { startLineNumber, startColumn, endLineNumber, endColumn } = selection; + const selectionStartOffset = model.getOffsetAt({ + lineNumber: startLineNumber, + column: startColumn, + }); + const selectionEndOffset = model.getOffsetAt({ lineNumber: endLineNumber, column: endColumn }); + const parsedRequests = await this.parsedRequestsProvider.getRequests(); + const selectedRequests = []; + for (const parsedRequest of parsedRequests) { + const { startOffset: requestStart, endOffset: requestEnd } = parsedRequest; + if (requestStart - 1 >= selectionEndOffset) { + // request is past the selection, no need to check further requests + break; + } + if (requestEnd - 1 < selectionStartOffset) { + // request is before the selection, do nothing + } else { + // request is selected + selectedRequests.push(parsedRequest); + } + } + return selectedRequests; + } + + private async getRequests() { + const parsedRequests = await this.getParsedRequests(); + const stringifiedRequests = parsedRequests.map((parsedRequest) => + stringifyRequest(parsedRequest) + ); + // get variables values + const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); + return stringifiedRequests.map((request) => replaceRequestVariables(request, variables)); + } + + public async getCurl(elasticsearchBaseUrl: string): Promise { + const requests = await this.getRequests(); + const curlRequests = requests.map((request) => getCurlRequest(request, elasticsearchBaseUrl)); + return curlRequests.join('\n'); + } + + public async sendRequests( + toasts: IToasts, + dispatch: Dispatch, + trackUiMetric: MetricsTracker, + http: HttpSetup + ): Promise { + try { + const requests = await this.getRequests(); + if (!requests.length) { + toasts.add( + i18n.translate('console.notification.error.noRequestSelectedTitle', { + defaultMessage: + 'No request selected. Select a request by placing the cursor inside it.', + }) + ); + return; + } + + dispatch({ type: 'sendRequest', payload: undefined }); + + // track the requests + setTimeout(() => trackSentRequests(requests, trackUiMetric), 0); + + const results = await sendRequest({ http, requests }); + + // TODO save to history + // TODO restart autocomplete polling + dispatch({ + type: 'requestSuccess', + payload: { + data: results, + }, + }); + } catch (e) { + if (e?.response) { + dispatch({ + type: 'requestFail', + payload: e, + }); + } else { + dispatch({ + type: 'requestFail', + payload: undefined, + }); + toasts.addError(e, { + title: i18n.translate('console.notification.error.unknownErrorTitle', { + defaultMessage: 'Unknown Request Error', + }), + }); + } + } + } +} diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils.ts new file mode 100644 index 0000000000000..f5725ad271fc6 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ParsedRequest } from '@kbn/monaco'; +import { constructUrl } from '../../../../lib/es'; +import type { DevToolsVariable } from '../../../components'; +import { EditorRequest } from './monaco_editor_actions_provider'; +import { MetricsTracker } from '../../../../types'; + +const whitespacesRegex = /\s/; +export const removeTrailingWhitespaces = (url: string): string => { + /* + * This helper removes any trailing inline comments, for example + * "_search // comment" -> "_search" + * Ideally the parser removes those comments initially + */ + return url.trim().split(whitespacesRegex)[0]; +}; + +export const stringifyRequest = (parsedRequest: ParsedRequest): EditorRequest => { + const url = removeTrailingWhitespaces(parsedRequest.url); + const method = parsedRequest.method.toUpperCase(); + const data = parsedRequest.data?.map((parsedData) => JSON.stringify(parsedData, null, 2)); + return { url, method, data: data ?? [] }; +}; + +const variableTemplateRegex = /\${(\w+)}/g; +const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { + if (variableTemplateRegex.test(text)) { + text = text.replaceAll(variableTemplateRegex, (match, key) => { + const variable = variables.find(({ name }) => name === key); + + return variable?.value ?? match; + }); + } + return text; +}; +export const replaceRequestVariables = ( + { method, url, data }: EditorRequest, + variables: DevToolsVariable[] +): EditorRequest => { + return { + method, + url: replaceVariables(url, variables), + data: data.map((dataObject) => replaceVariables(dataObject, variables)), + }; +}; + +export const getCurlRequest = ( + { method, url, data }: EditorRequest, + elasticsearchBaseUrl: string +): string => { + const curlUrl = constructUrl(elasticsearchBaseUrl, url); + let curlRequest = `curl -X${method} "${curlUrl}" -H "kbn-xsrf: reporting"`; + if (data.length > 0) { + curlRequest += ` -H "Content-Type: application/json" -d'\n`; + curlRequest += data.join('\n'); + curlRequest += "'"; + } + return curlRequest; +}; + +export const trackSentRequests = ( + requests: EditorRequest[], + trackUiMetric: MetricsTracker +): void => { + requests.map(({ method, url }) => { + const eventName = `${method}_${url}`; + trackUiMetric.count(eventName); + }); +}; diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/index.ts b/src/plugins/console/public/application/models/monaco_sense_editor/index.ts deleted file mode 100644 index 9dd939ad1f210..0000000000000 --- a/src/plugins/console/public/application/models/monaco_sense_editor/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { initSenseEditor } from './init_sense_editor'; -export { MonacoSenseEditor } from './monaco_sense_editor'; -export { removeTrailingWhitespaces, replaceVariables } from './utils'; diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts b/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts deleted file mode 100644 index 0fff793be4ba0..0000000000000 --- a/src/plugins/console/public/application/models/monaco_sense_editor/init_sense_editor.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { monaco } from '@kbn/monaco'; -import { MonacoSenseEditor } from './monaco_sense_editor'; - -export async function initSenseEditor(editor: monaco.editor.IStandaloneCodeEditor) { - return new MonacoSenseEditor(editor); -} diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts b/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts deleted file mode 100644 index 73553b1113967..0000000000000 --- a/src/plugins/console/public/application/models/monaco_sense_editor/monaco_sense_editor.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - ConsoleParsedRequestsProvider, - getParsedRequestsProvider, - monaco, - ParsedRequest, -} from '@kbn/monaco'; -import { removeTrailingWhitespaces, replaceVariables } from './utils'; -import { constructUrl } from '../../../lib/es/es'; -import { getStorage, StorageKeys } from '../../../services'; -import { DEFAULT_VARIABLES } from '../../../../common/constants'; -import { DevToolsVariable } from '../../components'; - -const getCurlRequest = ( - parsedRequest: ParsedRequest, - elasticsearchBaseUrl: string, - variables: DevToolsVariable[] -) => { - let url = removeTrailingWhitespaces(parsedRequest.url); - url = replaceVariables(url, variables); - url = constructUrl(elasticsearchBaseUrl, url); - let curlRequest = `curl -X${parsedRequest.method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; - if (parsedRequest.data && parsedRequest.data.length > 0) { - curlRequest += ` -H "Content-Type: application/json" -d'\n`; - for (const data of parsedRequest.data) { - curlRequest += replaceVariables(JSON.stringify(data, null, 2), variables); - } - curlRequest += "'"; - } - return curlRequest; -}; -export class MonacoSenseEditor { - private parsedRequestsProvider: ConsoleParsedRequestsProvider; - constructor(private editor: monaco.editor.IStandaloneCodeEditor) { - this.parsedRequestsProvider = getParsedRequestsProvider(editor.getModel()); - } - - private async getRequestsInRange(): Promise { - const model = this.editor.getModel(); - const selection = this.editor.getSelection(); - if (!model || !selection) { - return Promise.resolve([]); - } - const { startLineNumber, startColumn, endLineNumber, endColumn } = selection; - const selectionStartOffset = model.getOffsetAt({ - lineNumber: startLineNumber, - column: startColumn, - }); - const selectionEndOffset = model.getOffsetAt({ lineNumber: endLineNumber, column: endColumn }); - const parsedRequests = await this.parsedRequestsProvider.getRequests(); - const selectedRequests = []; - for (const parsedRequest of parsedRequests) { - const { startOffset: requestStart, endOffset: requestEnd } = parsedRequest; - if (requestStart - 1 >= selectionEndOffset) { - // request is past the selection, no need to check further requests - break; - } - if (requestEnd - 1 < selectionStartOffset) { - // request is before the selection, do nothing - } else { - // request is selected - selectedRequests.push(parsedRequest); - } - } - return selectedRequests; - } - public async getRequestsAsCURL(elasticsearchBaseUrl: string): Promise { - // get variables values - const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES); - // get selected requests - const requests = await this.getRequestsInRange(); - - const curlRequests = requests.map((request) => - getCurlRequest(request, elasticsearchBaseUrl, variables) - ); - return curlRequests.join('\n'); - } -} diff --git a/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts b/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts deleted file mode 100644 index 096b35f31ec33..0000000000000 --- a/src/plugins/console/public/application/models/monaco_sense_editor/utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { DevToolsVariable } from '../../components'; - -const whitespacesRegex = /\s/; -export const removeTrailingWhitespaces = (url: string): string => { - /* - * This helper removes any trailing inline comments, for example - * "_search // comment" -> "_search" - * Ideally the parser removes those comments initially - */ - return url.trim().split(whitespacesRegex)[0]; -}; - -const variableRegex = /\${(\w+)}/g; -export const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { - if (variableRegex.test(text)) { - text = text.replaceAll(variableRegex, (match, key) => { - const variable = variables.find(({ name }) => name === key); - - return variable?.value ?? match; - }); - } - return text; -}; From 9aaf9892319d89db4a8927f92b5dcec0a08aa25a Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Wed, 3 Apr 2024 19:10:04 +0200 Subject: [PATCH 03/12] [Console] Update editor actions buttons --- packages/kbn-monaco/src/console/parser.js | 10 +- .../editor/monaco/monaco_editor.tsx | 6 +- .../monaco/monaco_editor_actions_provider.ts | 102 +++++++++++++++--- src/plugins/console/public/styles/_app.scss | 8 ++ 4 files changed, 107 insertions(+), 19 deletions(-) diff --git a/packages/kbn-monaco/src/console/parser.js b/packages/kbn-monaco/src/console/parser.js index 0c9d2b5ef16b2..ab9a499696bdf 100644 --- a/packages/kbn-monaco/src/console/parser.js +++ b/packages/kbn-monaco/src/console/parser.js @@ -28,26 +28,31 @@ export const createParser = () => { annos.push({ type: type, text: text, at: at }); }, requestEvents, + requestStartOffset, + requestEndOffset, getLastRequest = function() { return requestEvents.length > 0 ? requestEvents.pop() : {}; }, addRequestStart = function() { - requestEvents.push({startOffset: at}); + requestStartOffset = at - 1; + requestEvents.push({startOffset: requestStartOffset}); }, addRequestEnd = function() { const lastRequest = getLastRequest(); - lastRequest.endOffset = at; + lastRequest.endOffset = requestEndOffset; requestEvents.push(lastRequest); }, addRequestMethod = function(method) { const lastRequest = getLastRequest(); lastRequest.method = method; requestEvents.push(lastRequest); + requestEndOffset = at - 1; }, addRequestUrl = function(url) { const lastRequest = getLastRequest(); lastRequest.url = url; requestEvents.push(lastRequest); + requestEndOffset = at - 1; }, addRequestData = function(data) { const lastRequest = getLastRequest(); @@ -55,6 +60,7 @@ export const createParser = () => { dataArray.push(data); lastRequest.data = dataArray; requestEvents.push(lastRequest); + requestEndOffset = at - 1; }, error = function (m) { throw { diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 9392dd369f9fd..5465bba8e33e1 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { css } from '@emotion/react'; import { CodeEditor } from '@kbn/code-editor'; @@ -33,6 +33,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const { settings } = useEditorReadContext(); const dispatch = useRequestActionContext(); const actionsProvider = useRef(null); + const [editorActionsCss, setEditorActionsCss] = useState({}); const getCurl = useCallback(async (): Promise => { return actionsProvider.current @@ -62,6 +63,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { id="ConAppEditorActions" gutterSize="none" responsive={false} + style={editorActionsCss} > { theme: CONSOLE_THEME_ID, }} editorDidMount={(editor) => { - actionsProvider.current = new MonacoEditorActionsProvider(editor); + actionsProvider.current = new MonacoEditorActionsProvider(editor, setEditorActionsCss); }} /> diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts index 84c42b962093f..1d6bac0066b3c 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -14,7 +14,7 @@ import { } from '@kbn/monaco'; import { IToasts } from '@kbn/core-notifications-browser'; import { i18n } from '@kbn/i18n'; -import { Dispatch } from 'react'; +import { CSSProperties, Dispatch } from 'react'; import type { HttpSetup } from '@kbn/core-http-browser'; import { sendRequest } from '../../../hooks/use_send_current_request/send_request'; import { DEFAULT_VARIABLES } from '../../../../../common/constants'; @@ -33,44 +33,116 @@ export interface EditorRequest { url: string; data: string[]; } +const selectedRequestsClass = 'console__monaco_editor__selectedRequests'; export class MonacoEditorActionsProvider { private parsedRequestsProvider: ConsoleParsedRequestsProvider; - constructor(private editor: monaco.editor.IStandaloneCodeEditor) { - this.parsedRequestsProvider = getParsedRequestsProvider(editor.getModel()); + private decorations: monaco.editor.IEditorDecorationsCollection; + constructor( + private editor: monaco.editor.IStandaloneCodeEditor, + private setEditorActionsCss: (css: CSSProperties) => void + ) { + this.parsedRequestsProvider = getParsedRequestsProvider(this.editor.getModel()); + this.decorations = this.editor.createDecorationsCollection(); + this.editor.focus(); + this.highlightCurrentRequests(); + editor.onDidChangeCursorPosition(async (event) => { + await this.highlightCurrentRequests(); + }); + // update actions bar on scroll change and size of the editor change + } + + private hideEditorActions() { + this.setEditorActionsCss({ + visibility: 'hidden', + }); } - private async getParsedRequests(): Promise { + private updateEditorActions(topOffset: number) { + this.setEditorActionsCss({ + visibility: 'visible', + top: topOffset, + }); + } + + private async highlightCurrentRequests(): Promise { + const { range: selectedRange, parsedRequests } = await this.getSelectedParsedRequestsAndRange(); + if (parsedRequests.length > 0) { + const topLine = selectedRange.startLineNumber; + const topOffset = this.editor.getTopForLineNumber(topLine); + this.updateEditorActions(topOffset); + this.decorations.set([ + { + range: selectedRange, + options: { + isWholeLine: true, + className: selectedRequestsClass, + }, + }, + ]); + } else { + this.hideEditorActions(); + this.decorations.clear(); + } + } + + private async getSelectedParsedRequestsAndRange(): Promise<{ + parsedRequests: ParsedRequest[]; + range: monaco.IRange; + }> { const model = this.editor.getModel(); const selection = this.editor.getSelection(); if (!model || !selection) { - return Promise.resolve([]); + return Promise.resolve({ + parsedRequests: [], + range: selection ?? new monaco.Range(1, 1, 1, 1), + }); } - const { startLineNumber, startColumn, endLineNumber, endColumn } = selection; - const selectionStartOffset = model.getOffsetAt({ - lineNumber: startLineNumber, - column: startColumn, - }); - const selectionEndOffset = model.getOffsetAt({ lineNumber: endLineNumber, column: endColumn }); + const { startLineNumber, endLineNumber } = selection; const parsedRequests = await this.parsedRequestsProvider.getRequests(); const selectedRequests = []; + let selectionStartLine = startLineNumber; + let selectionEndLine = endLineNumber; for (const parsedRequest of parsedRequests) { const { startOffset: requestStart, endOffset: requestEnd } = parsedRequest; - if (requestStart - 1 >= selectionEndOffset) { + const { lineNumber: requestStartLine } = model.getPositionAt(requestStart); + let { lineNumber: requestEndLine } = model.getPositionAt(requestEnd); + const requestEndLineContent = model.getLineContent(requestEndLine); + + if (requestEndLineContent.trim().length < 1) { + requestEndLine = requestEndLine - 1; + } + if (requestStartLine > endLineNumber) { // request is past the selection, no need to check further requests break; } - if (requestEnd - 1 < selectionStartOffset) { + if (requestEndLine < startLineNumber) { // request is before the selection, do nothing } else { // request is selected selectedRequests.push(parsedRequest); + // expand the start of the selection to the request start + if (selectionStartLine > requestStartLine) { + selectionStartLine = requestStartLine; + } + // expand the end of the selection to the request end + if (selectionEndLine < requestEndLine) { + selectionEndLine = requestEndLine; + } } } - return selectedRequests; + return { + parsedRequests: selectedRequests, + range: new monaco.Range( + selectionStartLine, + 1, + selectionEndLine, + model.getLineMaxColumn(selectionEndLine) + ), + }; } private async getRequests() { - const parsedRequests = await this.getParsedRequests(); + const { parsedRequests } = await this.getSelectedParsedRequestsAndRange(); const stringifiedRequests = parsedRequests.map((parsedRequest) => stringifyRequest(parsedRequest) ); diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 2f4340f1de0ab..9f2d1a9db82a1 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -125,3 +125,11 @@ .conApp__tabsExtension { border-bottom: $euiBorderThin; } + +/* + * The highlighting for the selected requests in the monaco editor + */ +.console__monaco_editor__selectedRequests { + /* Make sure to use transparent colors for the selection to work */ + background: transparentize($euiColorLightShade, .3); +} From 5a109a46a8c3eb06de6662d4e44dcca3cb2fa5e9 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Thu, 4 Apr 2024 15:39:32 +0200 Subject: [PATCH 04/12] [Console] Added more listeners and debounce --- .../monaco/monaco_editor_actions_provider.ts | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts index 1d6bac0066b3c..33dbfcf2bdf46 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { CSSProperties, Dispatch } from 'react'; +import { debounce } from 'lodash'; import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, @@ -14,13 +16,12 @@ import { } from '@kbn/monaco'; import { IToasts } from '@kbn/core-notifications-browser'; import { i18n } from '@kbn/i18n'; -import { CSSProperties, Dispatch } from 'react'; import type { HttpSetup } from '@kbn/core-http-browser'; -import { sendRequest } from '../../../hooks/use_send_current_request/send_request'; import { DEFAULT_VARIABLES } from '../../../../../common/constants'; +import { getStorage, StorageKeys } from '../../../../services'; +import { sendRequest } from '../../../hooks/use_send_current_request/send_request'; import { MetricsTracker } from '../../../../types'; import { Actions } from '../../../stores/request'; -import { getStorage, StorageKeys } from '../../../../services'; import { stringifyRequest, replaceRequestVariables, @@ -44,32 +45,47 @@ export class MonacoEditorActionsProvider { this.parsedRequestsProvider = getParsedRequestsProvider(this.editor.getModel()); this.decorations = this.editor.createDecorationsCollection(); this.editor.focus(); - this.highlightCurrentRequests(); + const debouncedHighlight = debounce(() => this.highlightCurrentRequests(), 200, { + leading: true, + }); + debouncedHighlight(); editor.onDidChangeCursorPosition(async (event) => { - await this.highlightCurrentRequests(); + await debouncedHighlight(); }); - // update actions bar on scroll change and size of the editor change - } - - private hideEditorActions() { - this.setEditorActionsCss({ - visibility: 'hidden', + editor.onDidScrollChange(async (event) => { + await debouncedHighlight(); + }); + editor.onDidChangeCursorSelection(async (event) => { + await debouncedHighlight(); + }); + editor.onDidContentSizeChange(async (event) => { + await debouncedHighlight(); }); } - private updateEditorActions(topOffset: number) { - this.setEditorActionsCss({ - visibility: 'visible', - top: topOffset, - }); + private updateEditorActionsPosition(lineNumber?: number) { + // if no request is currently selected, hide the actions buttons + if (!lineNumber) { + this.setEditorActionsCss({ + visibility: 'hidden', + }); + } else { + // if a request is selected, the actions buttons are placed at lineNumberOffset - scrollOffset + const offset = this.editor.getTopForLineNumber(lineNumber) - this.editor.getScrollTop(); + this.setEditorActionsCss({ + visibility: 'visible', + top: offset, + }); + } } private async highlightCurrentRequests(): Promise { + // get the requests in the selected range const { range: selectedRange, parsedRequests } = await this.getSelectedParsedRequestsAndRange(); + // if any requests are selected, highlight the lines and update the position of actions buttons if (parsedRequests.length > 0) { - const topLine = selectedRange.startLineNumber; - const topOffset = this.editor.getTopForLineNumber(topLine); - this.updateEditorActions(topOffset); + const selectedRequestStartLine = selectedRange.startLineNumber; + this.updateEditorActionsPosition(selectedRequestStartLine); this.decorations.set([ { range: selectedRange, @@ -80,7 +96,8 @@ export class MonacoEditorActionsProvider { }, ]); } else { - this.hideEditorActions(); + // if no requests are selected, hide actions buttons and remove highlighted lines + this.updateEditorActionsPosition(); this.decorations.clear(); } } From e8d3c2efc15b03a86745f715811219deeb69d51a Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Thu, 4 Apr 2024 15:59:55 +0200 Subject: [PATCH 05/12] Clean up --- packages/kbn-monaco/index.ts | 2 +- .../src/ace_migration/setup_worker.ts | 2 +- .../src/console/console_errors_provider.ts | 10 ++--- .../console_parsed_requests_provider.ts | 4 +- .../src/console/console_worker_proxy.ts | 15 ++------ packages/kbn-monaco/src/console/parser.js | 38 +++++++++---------- packages/kbn-monaco/src/console/types.ts | 8 ++-- 7 files changed, 36 insertions(+), 43 deletions(-) diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index a8f0bb0f1bbcb..3ba4a5afb3aea 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -34,8 +34,8 @@ export * from './src/types'; export { CONSOLE_LANG_ID, - CONSOLE_THEME_ID, CONSOLE_OUTPUT_LANG_ID, + CONSOLE_THEME_ID, CONSOLE_OUTPUT_THEME_ID, getParsedRequestsProvider, ConsoleParsedRequestsProvider, diff --git a/packages/kbn-monaco/src/ace_migration/setup_worker.ts b/packages/kbn-monaco/src/ace_migration/setup_worker.ts index 4ad6b1dabc906..86c815398b08f 100644 --- a/packages/kbn-monaco/src/ace_migration/setup_worker.ts +++ b/packages/kbn-monaco/src/ace_migration/setup_worker.ts @@ -49,7 +49,7 @@ export const setupWorker = ( } const { dispose } = model.onDidChangeContent(async () => { - await updateAnnotations(model); + updateAnnotations(model); }); model.onWillDispose(() => { diff --git a/packages/kbn-monaco/src/console/console_errors_provider.ts b/packages/kbn-monaco/src/console/console_errors_provider.ts index e3a4e1acc2948..92c6193241108 100644 --- a/packages/kbn-monaco/src/console/console_errors_provider.ts +++ b/packages/kbn-monaco/src/console/console_errors_provider.ts @@ -15,17 +15,17 @@ export const setupConsoleErrorsProvider = (workerProxyService: ConsoleWorkerProx if (model.isDisposed()) { return; } - const parseResult = await workerProxyService.getParseResult(model.uri); + const parserResult = await workerProxyService.getParserResult(model.uri); - if (!parseResult) { + if (!parserResult) { return; } - const { errors } = parseResult; + const { errors } = parserResult; monaco.editor.setModelMarkers( model, CONSOLE_LANG_ID, - errors.map(({ at, text }) => { - const { column, lineNumber } = model.getPositionAt(at); + errors.map(({ offset, text }) => { + const { column, lineNumber } = model.getPositionAt(offset); return { startLineNumber: lineNumber, startColumn: column, diff --git a/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts index 7802612e7179f..600660e0ff360 100644 --- a/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts +++ b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts @@ -19,7 +19,7 @@ export class ConsoleParsedRequestsProvider { if (!this.model) { return []; } - const parseResult = await this.workerProxyService.getParseResult(this.model.uri); - return parseResult?.requests ?? []; + const parserResult = await this.workerProxyService.getParserResult(this.model.uri); + return parserResult?.requests ?? []; } } diff --git a/packages/kbn-monaco/src/console/console_worker_proxy.ts b/packages/kbn-monaco/src/console/console_worker_proxy.ts index bf8c9b3decded..e23ba45a984f2 100644 --- a/packages/kbn-monaco/src/console/console_worker_proxy.ts +++ b/packages/kbn-monaco/src/console/console_worker_proxy.ts @@ -6,29 +6,22 @@ * Side Public License, v 1. */ -import { CONSOLE_LANG_ID } from '../..'; import { monaco } from '../monaco_imports'; -import { ConsoleParseResult, ConsoleWorkerDefinition } from './types'; +import { CONSOLE_LANG_ID } from './constants'; +import { ConsoleParserResult, ConsoleWorkerDefinition } from './types'; export class ConsoleWorkerProxyService { private worker: monaco.editor.MonacoWebWorker | undefined; - public async getParseResult(modelUri: monaco.Uri): Promise { + public async getParserResult(modelUri: monaco.Uri): Promise { if (!this.worker) { throw new Error('Worker Proxy Service has not been setup!'); } await this.worker.withSyncedResources([modelUri]); const parser = await this.worker.getProxy(); - return parser.getParseResult(modelUri.toString()); + return parser.getParserResult(modelUri.toString()); } - public async getWorker(modelUri: monaco.Uri): Promise { - if (!this.worker) { - throw new Error('Worker Proxy Service has not been setup!'); - } - await this.worker.withSyncedResources([modelUri]); - return this.worker.getProxy(); - } public setup() { this.worker = monaco.editor.createWebWorker({ label: CONSOLE_LANG_ID, moduleId: '' }); } diff --git a/packages/kbn-monaco/src/console/parser.js b/packages/kbn-monaco/src/console/parser.js index ab9a499696bdf..5de69ffce0acc 100644 --- a/packages/kbn-monaco/src/console/parser.js +++ b/packages/kbn-monaco/src/console/parser.js @@ -12,7 +12,6 @@ export const createParser = () => { let at, // The index of the current character ch, // The current character - annos, // annotations escapee = { '"': '"', '\\': '\\', @@ -24,34 +23,30 @@ export const createParser = () => { t: '\t', }, text, - annotate = function (type, text) { - annos.push({ type: type, text: text, at: at }); + errors, + addError = function (text) { + errors.push({ text: text, offset: at }); }, - requestEvents, + requests, requestStartOffset, requestEndOffset, getLastRequest = function() { - return requestEvents.length > 0 ? requestEvents.pop() : {}; + return requests.length > 0 ? requests.pop() : {}; }, addRequestStart = function() { requestStartOffset = at - 1; - requestEvents.push({startOffset: requestStartOffset}); - }, - addRequestEnd = function() { - const lastRequest = getLastRequest(); - lastRequest.endOffset = requestEndOffset; - requestEvents.push(lastRequest); + requests.push({ startOffset: requestStartOffset }); }, addRequestMethod = function(method) { const lastRequest = getLastRequest(); lastRequest.method = method; - requestEvents.push(lastRequest); + requests.push(lastRequest); requestEndOffset = at - 1; }, addRequestUrl = function(url) { const lastRequest = getLastRequest(); lastRequest.url = url; - requestEvents.push(lastRequest); + requests.push(lastRequest); requestEndOffset = at - 1; }, addRequestData = function(data) { @@ -59,9 +54,14 @@ export const createParser = () => { const dataArray = lastRequest.data || []; dataArray.push(data); lastRequest.data = dataArray; - requestEvents.push(lastRequest); + requests.push(lastRequest); requestEndOffset = at - 1; }, + addRequestEnd = function() { + const lastRequest = getLastRequest(); + lastRequest.endOffset = requestEndOffset; + requests.push(lastRequest); + }, error = function (m) { throw { name: 'SyntaxError', @@ -458,7 +458,7 @@ export const createParser = () => { request(); white(); } catch (e) { - annotate('error', e.message); + addError(e.message); // snap const substring = text.substr(at); const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m); @@ -473,16 +473,16 @@ export const createParser = () => { text = source; at = 0; - annos = []; - requestEvents = []; + errors = []; + requests = []; next(); multi_request(); white(); if (ch) { - annotate('error', 'Syntax error'); + addError('Syntax error'); } - result = { errors: annos, requests: requestEvents }; + result = { errors, requests }; return typeof reviver === 'function' ? (function walk(holder, key) { diff --git a/packages/kbn-monaco/src/console/types.ts b/packages/kbn-monaco/src/console/types.ts index aa5ebc4fce053..000bff3526d0c 100644 --- a/packages/kbn-monaco/src/console/types.ts +++ b/packages/kbn-monaco/src/console/types.ts @@ -7,7 +7,7 @@ */ export interface ErrorAnnotation { - at: number; + offset: number; text: string; } @@ -18,12 +18,12 @@ export interface ParsedRequest { url: string; data?: Array>; } -export interface ConsoleParseResult { +export interface ConsoleParserResult { errors: ErrorAnnotation[]; requests: ParsedRequest[]; } export interface ConsoleWorkerDefinition { - getParseResult: (modelUri: string) => ConsoleParseResult | undefined; + getParserResult: (modelUri: string) => ConsoleParserResult | undefined; } -export type ConsoleParser = (source: string) => ConsoleParseResult | undefined; +export type ConsoleParser = (source: string) => ConsoleParserResult | undefined; From d93ca6d2d4d760843ccf12c38c5a88b0a5e38692 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Thu, 4 Apr 2024 16:10:42 +0200 Subject: [PATCH 06/12] Clean up --- .../src/console/worker/console_worker.ts | 10 ++++---- .../editor/monaco/monaco_editor.tsx | 24 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/kbn-monaco/src/console/worker/console_worker.ts b/packages/kbn-monaco/src/console/worker/console_worker.ts index 3cd799c69a9d9..85e846e7b826f 100644 --- a/packages/kbn-monaco/src/console/worker/console_worker.ts +++ b/packages/kbn-monaco/src/console/worker/console_worker.ts @@ -8,23 +8,23 @@ /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { ConsoleParseResult, ConsoleWorkerDefinition, ConsoleParser } from '../types'; +import { ConsoleParserResult, ConsoleWorkerDefinition, ConsoleParser } from '../types'; import { createParser } from '../parser'; export class ConsoleWorker implements ConsoleWorkerDefinition { private parser: ConsoleParser | undefined; - private parseResult: ConsoleParseResult | undefined; + private parserResult: ConsoleParserResult | undefined; constructor(private ctx: monaco.worker.IWorkerContext) {} - getParseResult(modelUri: string): ConsoleParseResult | undefined { + getParserResult(modelUri: string): ConsoleParserResult | undefined { if (!this.parser) { this.parser = createParser(); } const model = this.ctx.getMirrorModels().find((m) => m.uri.toString() === modelUri); if (model) { - this.parseResult = this.parser(model.getValue()); + this.parserResult = this.parser(model.getValue()); } - return this.parseResult; + return this.parserResult; } } diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 5465bba8e33e1..f1028f4e920aa 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -10,7 +10,7 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from ' import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { css } from '@emotion/react'; import { CodeEditor } from '@kbn/code-editor'; -import { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from '@kbn/monaco'; +import { CONSOLE_LANG_ID, CONSOLE_THEME_ID, monaco } from '@kbn/monaco'; import { i18n } from '@kbn/i18n'; import { ConsoleMenu } from '../../../components'; import { @@ -35,15 +35,19 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const actionsProvider = useRef(null); const [editorActionsCss, setEditorActionsCss] = useState({}); - const getCurl = useCallback(async (): Promise => { - return actionsProvider.current - ? actionsProvider.current.getCurl(esHostService.getHost()) - : Promise.resolve(''); + const editorDidMountCallback = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => { + actionsProvider.current = new MonacoEditorActionsProvider(editor, setEditorActionsCss); + }, []); + + const getCurlCallback = useCallback(async (): Promise => { + const curl = await actionsProvider.current?.getCurl(esHostService.getHost()); + return curl ?? ''; }, [esHostService]); - const sendRequests = useCallback(async () => { + const sendRequestsCallback = useCallback(async () => { await actionsProvider.current?.sendRequests(toasts, dispatch, trackUiMetric, http); }, [dispatch, http, toasts, trackUiMetric]); + const [value, setValue] = useState(initialTextValue); const setInitialValue = useSetInitialValue; @@ -73,7 +77,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { > { { return Promise.resolve(null); }} @@ -105,9 +109,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { wordWrap: settings.wrapMode === true ? 'on' : 'off', theme: CONSOLE_THEME_ID, }} - editorDidMount={(editor) => { - actionsProvider.current = new MonacoEditorActionsProvider(editor, setEditorActionsCss); - }} + editorDidMount={editorDidMountCallback} /> ); From d57a527673313b7e5e468023c9f2e6c5dca1acf4 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Thu, 4 Apr 2024 16:24:25 +0200 Subject: [PATCH 07/12] Clean up --- .../monaco/monaco_editor_actions_provider.ts | 39 ++++++++++++------- src/plugins/console/public/styles/_app.scss | 1 - 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts index 33dbfcf2bdf46..af87f7fb68d24 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -29,41 +29,46 @@ import { trackSentRequests, } from './utils'; +const selectedRequestsClass = 'console__monaco_editor__selectedRequests'; + export interface EditorRequest { method: string; url: string; data: string[]; } -const selectedRequestsClass = 'console__monaco_editor__selectedRequests'; + export class MonacoEditorActionsProvider { private parsedRequestsProvider: ConsoleParsedRequestsProvider; - private decorations: monaco.editor.IEditorDecorationsCollection; + private highlightedLines: monaco.editor.IEditorDecorationsCollection; constructor( private editor: monaco.editor.IStandaloneCodeEditor, private setEditorActionsCss: (css: CSSProperties) => void ) { this.parsedRequestsProvider = getParsedRequestsProvider(this.editor.getModel()); - this.decorations = this.editor.createDecorationsCollection(); + this.highlightedLines = this.editor.createDecorationsCollection(); this.editor.focus(); - const debouncedHighlight = debounce(() => this.highlightCurrentRequests(), 200, { + + const debouncedHighlightRequests = debounce(() => this.highlightRequests(), 200, { leading: true, }); - debouncedHighlight(); + debouncedHighlightRequests(); + + // init all listeners editor.onDidChangeCursorPosition(async (event) => { - await debouncedHighlight(); + await debouncedHighlightRequests(); }); editor.onDidScrollChange(async (event) => { - await debouncedHighlight(); + await debouncedHighlightRequests(); }); editor.onDidChangeCursorSelection(async (event) => { - await debouncedHighlight(); + await debouncedHighlightRequests(); }); editor.onDidContentSizeChange(async (event) => { - await debouncedHighlight(); + await debouncedHighlightRequests(); }); } - private updateEditorActionsPosition(lineNumber?: number) { + private updateEditorActions(lineNumber?: number) { // if no request is currently selected, hide the actions buttons if (!lineNumber) { this.setEditorActionsCss({ @@ -79,14 +84,15 @@ export class MonacoEditorActionsProvider { } } - private async highlightCurrentRequests(): Promise { + private async highlightRequests(): Promise { // get the requests in the selected range const { range: selectedRange, parsedRequests } = await this.getSelectedParsedRequestsAndRange(); // if any requests are selected, highlight the lines and update the position of actions buttons if (parsedRequests.length > 0) { const selectedRequestStartLine = selectedRange.startLineNumber; - this.updateEditorActionsPosition(selectedRequestStartLine); - this.decorations.set([ + // display the actions buttons on the 1st line of the 1st selected request + this.updateEditorActions(selectedRequestStartLine); + this.highlightedLines.set([ { range: selectedRange, options: { @@ -97,8 +103,8 @@ export class MonacoEditorActionsProvider { ]); } else { // if no requests are selected, hide actions buttons and remove highlighted lines - this.updateEditorActionsPosition(); - this.decorations.clear(); + this.updateEditorActions(); + this.highlightedLines.clear(); } } @@ -125,6 +131,7 @@ export class MonacoEditorActionsProvider { let { lineNumber: requestEndLine } = model.getPositionAt(requestEnd); const requestEndLineContent = model.getLineContent(requestEndLine); + // sometimes the parser includes a trailing empty line into the request if (requestEndLineContent.trim().length < 1) { requestEndLine = requestEndLine - 1; } @@ -149,6 +156,8 @@ export class MonacoEditorActionsProvider { } return { parsedRequests: selectedRequests, + // the expanded selected range goes from the 1st char of the start line of the 1st request + // to the last char of the last line of the last request range: new monaco.Range( selectionStartLine, 1, diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 9f2d1a9db82a1..37f2753b1a99b 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -130,6 +130,5 @@ * The highlighting for the selected requests in the monaco editor */ .console__monaco_editor__selectedRequests { - /* Make sure to use transparent colors for the selection to work */ background: transparentize($euiColorLightShade, .3); } From c4e984a5da9c09e5b0d157398f5baee433bc3f46 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Thu, 4 Apr 2024 18:25:48 +0200 Subject: [PATCH 08/12] Add tests --- .../kbn-monaco/src/console/parser.test.ts | 42 +++++++ .../monaco_editor_actions_provider.test.ts | 104 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 packages/kbn-monaco/src/console/parser.test.ts create mode 100644 src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts diff --git a/packages/kbn-monaco/src/console/parser.test.ts b/packages/kbn-monaco/src/console/parser.test.ts new file mode 100644 index 0000000000000..566c401736f64 --- /dev/null +++ b/packages/kbn-monaco/src/console/parser.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createParser } from './parser'; +import { ConsoleParserResult } from './types'; + +const parser = createParser(); +describe('console parser', () => { + it('returns errors if input is not correct', () => { + const input = 'Incorrect input'; + const parserResult = parser(input) as ConsoleParserResult; + // the parser logs 2 errors: for the unexpected method and a general syntax error + expect(parserResult.errors.length).toBe(2); + // the parser logs a beginning of the request that it's trying to parse + expect(parserResult.requests.length).toBe(1); + }); + + it('returns parsedRequests if the input is correct', () => { + const input = 'GET _search'; + const { requests, errors } = parser(input) as ConsoleParserResult; + expect(requests.length).toBe(1); + expect(errors.length).toBe(0); + const { method, url, startOffset, endOffset } = requests[0]; + expect(method).toBe('GET'); + expect(url).toBe('_search'); + // the start offset of the request is the beginning of the string + expect(startOffset).toBe(0); + // the end offset of the request is the end of the string + expect(endOffset).toBe(11); + }); + + it('parses several requests', () => { + const input = 'GET _search\nPOST _test_index'; + const { requests } = parser(input) as ConsoleParserResult; + expect(requests.length).toBe(2); + }); +}); diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts new file mode 100644 index 0000000000000..ee2be44ec812e --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* + * Mock kbn/monaco to provide the console parser code directly without a web worker + */ +const mockGetParsedRequests = jest.fn(); +jest.mock('@kbn/monaco', () => { + const original = jest.requireActual('@kbn/monaco'); + return { + ...original, + getParsedRequestsProvider: () => { + return { + getRequests: mockGetParsedRequests, + }; + }, + }; +}); + +jest.mock('../../../../services', () => { + return { + getStorage: () => ({ + get: () => [], + }), + StorageKeys: { + VARIABLES: 'test', + }, + }; +}); + +import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider'; +import { monaco } from '@kbn/monaco'; + +describe('Editor actions provider', () => { + let editorActionsProvider: MonacoEditorActionsProvider; + let editor: jest.Mocked; + beforeEach(() => { + editor = { + getModel: jest.fn(), + createDecorationsCollection: () => ({ + clear: jest.fn(), + set: jest.fn(), + }), + focus: jest.fn(), + onDidChangeCursorPosition: jest.fn(), + onDidScrollChange: jest.fn(), + onDidChangeCursorSelection: jest.fn(), + onDidContentSizeChange: jest.fn(), + getSelection: jest.fn(), + getTopForLineNumber: jest.fn(), + getScrollTop: jest.fn(), + } as unknown as jest.Mocked; + + editor.getModel.mockReturnValue({ + getLineMaxColumn: () => 10, + getPositionAt: () => ({ lineNumber: 1 }), + getLineContent: () => 'GET _search', + } as unknown as monaco.editor.ITextModel); + editor.getSelection.mockReturnValue({ + startLineNumber: 1, + endLineNumber: 1, + } as unknown as monaco.Selection); + mockGetParsedRequests.mockResolvedValue([ + { + startOffset: 0, + endOffset: 11, + method: 'GET', + url: '_search', + }, + ]); + + const setEditorActionsCssMock = jest.fn(); + + editorActionsProvider = new MonacoEditorActionsProvider(editor, setEditorActionsCssMock); + }); + + describe('getCurl', () => { + it('returns an empty string if no requests', async () => { + mockGetParsedRequests.mockResolvedValue([]); + const curl = await editorActionsProvider.getCurl('http://localhost'); + expect(curl).toBe(''); + }); + + it('returns an empty string if there is a request but not in the selection range', async () => { + editor.getSelection.mockReturnValue({ + // the request is on line 1, the user selected line 2 + startLineNumber: 2, + endLineNumber: 2, + } as unknown as monaco.Selection); + const curl = await editorActionsProvider.getCurl('http://localhost'); + expect(curl).toBe(''); + }); + + it('returns the correct string if there is a request in the selection range', async () => { + const curl = await editorActionsProvider.getCurl('http://localhost'); + expect(curl).toBe('curl -XGET "http://localhost/_search" -H "kbn-xsrf: reporting"'); + }); + }); +}); From e5d92299d684145a626116e987700e14f523aedb Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Mon, 15 Apr 2024 15:19:11 +0200 Subject: [PATCH 09/12] [Console] Address code review comments --- .../application/components/console_menu.tsx | 29 ++++++++++--------- .../editor/legacy/console_editor/editor.tsx | 4 +-- .../editor/monaco/monaco_editor.tsx | 4 +-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/plugins/console/public/application/components/console_menu.tsx b/src/plugins/console/public/application/components/console_menu.tsx index b22fd7db9baab..6fe755dcef2be 100644 --- a/src/plugins/console/public/application/components/console_menu.tsx +++ b/src/plugins/console/public/application/components/console_menu.tsx @@ -121,7 +121,7 @@ export class ConsoleMenu extends Component { defaultMessage: 'Request options', })} > - + ); @@ -135,10 +135,22 @@ export class ConsoleMenu extends Component { this.closePopover(); this.copyAsCurl(); }} + icon="copyClipboard" > + , + + , { onClick={() => { this.openDocs(); }} + icon="documentation" > - , - - , ]; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 6963a0ed2b21e..8de241aee705b 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -288,14 +288,14 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { })} > - + diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index f1028f4e920aa..f90f05f230cb4 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -76,14 +76,14 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { })} > - + From 5efc0137cf72d681df837e4a57a174d8db954a91 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Mon, 15 Apr 2024 16:06:40 +0200 Subject: [PATCH 10/12] [Console] Address code review comments --- packages/kbn-monaco/src/console/console_errors_provider.ts | 7 +++++++ .../src/console/console_parsed_requests_provider.ts | 6 ++++++ packages/kbn-monaco/src/console/console_worker_proxy.ts | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/packages/kbn-monaco/src/console/console_errors_provider.ts b/packages/kbn-monaco/src/console/console_errors_provider.ts index 92c6193241108..c92161f309969 100644 --- a/packages/kbn-monaco/src/console/console_errors_provider.ts +++ b/packages/kbn-monaco/src/console/console_errors_provider.ts @@ -10,6 +10,13 @@ import { ConsoleWorkerProxyService } from './console_worker_proxy'; import { CONSOLE_LANG_ID } from './constants'; import { monaco } from '../monaco_imports'; +/* + * This setup function runs when the Console language is registered into the Monaco editor. + * It adds a listener that is attached to the editor input when the Monaco editor is used + * with the Console language. + * The Console parser that runs in a web worker analyzes the editor input when it changes and + * if any errors are found, they are added as "error markers" to the Monaco editor. + */ export const setupConsoleErrorsProvider = (workerProxyService: ConsoleWorkerProxyService) => { const updateErrorMarkers = async (model: monaco.editor.IModel): Promise => { if (model.isDisposed()) { diff --git a/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts index 600660e0ff360..da0689b3ab863 100644 --- a/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts +++ b/packages/kbn-monaco/src/console/console_parsed_requests_provider.ts @@ -10,6 +10,12 @@ import { ConsoleWorkerProxyService } from './console_worker_proxy'; import { ParsedRequest } from './types'; import { monaco } from '../monaco_imports'; +/* + * This class is a helper interface that is used in the Console plugin. + * The provider access the Console parser that runs in a web worker and analyzes the editor input + * when it changes. + * The parsed result contains the requests and errors which are used in the Console plugin. + */ export class ConsoleParsedRequestsProvider { constructor( private workerProxyService: ConsoleWorkerProxyService, diff --git a/packages/kbn-monaco/src/console/console_worker_proxy.ts b/packages/kbn-monaco/src/console/console_worker_proxy.ts index e23ba45a984f2..3438f2f98f6f2 100644 --- a/packages/kbn-monaco/src/console/console_worker_proxy.ts +++ b/packages/kbn-monaco/src/console/console_worker_proxy.ts @@ -10,6 +10,11 @@ import { monaco } from '../monaco_imports'; import { CONSOLE_LANG_ID } from './constants'; import { ConsoleParserResult, ConsoleWorkerDefinition } from './types'; +/* + * This class contains logic to create a web worker where the code for the Console parser can + * execute without blocking the main thread. The parser only runs when the Monaco editor + * is used with the Console language. The parser can only be accessed via this proxy service class. + */ export class ConsoleWorkerProxyService { private worker: monaco.editor.MonacoWebWorker | undefined; From 080362223e2d1815d4d750dfb9b10758725163b7 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Mon, 15 Apr 2024 18:29:56 +0200 Subject: [PATCH 11/12] [Console] Address code review comments --- .../containers/editor/monaco/utils.test.ts | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/plugins/console/public/application/containers/editor/monaco/utils.test.ts diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts new file mode 100644 index 0000000000000..e2aa1543d8b6f --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + getCurlRequest, + removeTrailingWhitespaces, + replaceRequestVariables, + stringifyRequest, + trackSentRequests, +} from './utils'; +import { MetricsTracker } from '../../../../types'; + +describe('monaco editor utils', () => { + const dataObjects = [ + { + query: { + match_all: {}, + }, + }, + { + test: 'test', + }, + ]; + describe('removeTrailingWhitespaces', () => { + it(`works with an empty string`, () => { + const url = ''; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it(`doesn't change the string if no trailing whitespaces`, () => { + const url = '_search'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it(`removes any text after the first whitespace`, () => { + const url = '_search some_text'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('_search'); + }); + }); + + describe('stringifyRequest', () => { + const request = { + startOffset: 0, + endOffset: 11, + method: 'get', + url: '_search some_text', + }; + it('calls the "removeTrailingWhitespaces" on the url', () => { + const stringifiedRequest = stringifyRequest(request); + expect(stringifiedRequest.url).toBe('_search'); + }); + + it('normalizes the method to upper case', () => { + const stringifiedRequest = stringifyRequest(request); + expect(stringifiedRequest.method).toBe('GET'); + }); + it('stringifies the request body', () => { + const result = stringifyRequest({ ...request, data: [dataObjects[0]] }); + expect(result.data.length).toBe(1); + expect(result.data[0]).toBe(JSON.stringify(dataObjects[0], null, 2)); + }); + + it('works for several request bodies', () => { + const result = stringifyRequest({ ...request, data: dataObjects }); + expect(result.data.length).toBe(2); + expect(result.data[0]).toBe(JSON.stringify(dataObjects[0], null, 2)); + expect(result.data[1]).toBe(JSON.stringify(dataObjects[1], null, 2)); + }); + }); + + describe('replaceRequestVariables', () => { + const variables = [ + { id: '1', name: 'variable1', value: 'test1' }, + { + id: '2', + name: 'variable2', + value: 'test2', + }, + ]; + + describe('replaces variables in the url', () => { + const request = { + method: 'GET', + url: '${variable1}', + data: [], + }; + it('when there is no other text', () => { + const result = replaceRequestVariables(request, variables); + expect(result.url).toBe('test1'); + }); + it('inside a string', () => { + const result = replaceRequestVariables( + { ...request, url: 'test_${variable1}_test' }, + variables + ); + expect(result.url).toBe('test_test1_test'); + }); + it('works with several variables', () => { + const result = replaceRequestVariables( + { ...request, url: '${variable1}_${variable2}' }, + variables + ); + expect(result.url).toBe('test1_test2'); + }); + }); + + describe('replaces variables in the request body', () => { + const request = { + method: 'GET', + url: '${variable1}', + data: [JSON.stringify({ '${variable1}': '${variable2}' }, null, 2)], + }; + it('works with several variables', () => { + const result = replaceRequestVariables(request, variables); + expect(result.data[0]).toBe(JSON.stringify({ test1: 'test2' }, null, 2)); + }); + }); + }); + + describe('getCurlRequest', () => { + it('works without a request body', () => { + const request = { method: 'GET', url: '_search', data: [] }; + const result = getCurlRequest(request, 'http://test.com'); + expect(result).toBe('curl -XGET "http://test.com/_search" -H "kbn-xsrf: reporting"'); + }); + it('works with a request body', () => { + const request = { + method: 'GET', + url: '_search', + data: [JSON.stringify(dataObjects[0], null, 2)], + }; + const result = getCurlRequest(request, 'http://test.com'); + expect(result).toBe( + 'curl -XGET "http://test.com/_search" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d\'\n' + + '{\n' + + ' "query": {\n' + + ' "match_all": {}\n' + + ' }\n' + + "}'" + ); + }); + it('works with several request bodies', () => { + const request = { + method: 'GET', + url: '_search', + data: [JSON.stringify(dataObjects[0], null, 2), JSON.stringify(dataObjects[1], null, 2)], + }; + const result = getCurlRequest(request, 'http://test.com'); + expect(result).toBe( + 'curl -XGET "http://test.com/_search" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d\'\n' + + '{\n' + + ' "query": {\n' + + ' "match_all": {}\n' + + ' }\n' + + '}\n' + + '{\n' + + ' "test": "test"\n' + + "}'" + ); + }); + }); + + describe('trackSentRequests', () => { + it('tracks each request correctly', () => { + const requests = [ + { method: 'GET', url: '_search', data: [] }, + { method: 'POST', url: '_test', data: [] }, + ]; + const mockMetricsTracker: jest.Mocked = { count: jest.fn(), load: jest.fn() }; + trackSentRequests(requests, mockMetricsTracker); + expect(mockMetricsTracker.count).toHaveBeenCalledTimes(2); + expect(mockMetricsTracker.count).toHaveBeenNthCalledWith(1, 'GET__search'); + expect(mockMetricsTracker.count).toHaveBeenNthCalledWith(2, 'POST__test'); + }); + }); +}); From 90f1789f84bff72cfe92e4eb73085ac874345bb7 Mon Sep 17 00:00:00 2001 From: Yulia Cech Date: Mon, 15 Apr 2024 18:36:47 +0200 Subject: [PATCH 12/12] [Console] Address code review comments --- packages/kbn-monaco/src/console/parser.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/kbn-monaco/src/console/parser.test.ts b/packages/kbn-monaco/src/console/parser.test.ts index 566c401736f64..417fdd12e2c18 100644 --- a/packages/kbn-monaco/src/console/parser.test.ts +++ b/packages/kbn-monaco/src/console/parser.test.ts @@ -39,4 +39,21 @@ describe('console parser', () => { const { requests } = parser(input) as ConsoleParserResult; expect(requests.length).toBe(2); }); + + it('parses a request with a request body', () => { + const input = + 'GET _search\n' + '{\n' + ' "query": {\n' + ' "match_all": {}\n' + ' }\n' + '}'; + const { requests } = parser(input) as ConsoleParserResult; + expect(requests.length).toBe(1); + const { method, url, data } = requests[0]; + expect(method).toBe('GET'); + expect(url).toBe('_search'); + expect(data).toEqual([ + { + query: { + match_all: {}, + }, + }, + ]); + }); });