mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Console] Monaco migration: send request and copy as curl buttons (#179808)
## Summary Closes https://github.com/elastic/kibana/issues/178990 This PR adds the actions buttons to the monaco editor and the functionality for the buttons to send a request and copy the request as a curl command. When the cursor or user selection doesn't overlap or contain any requests on the selected line, the actions buttons are hidden. When the cursor or selection changes, the buttons are displayed on the 1st line of the 1st selected request. Several requests can be sent at once. Only the 1st request is copied as a curl command. There are also some minor UI and copy changes (see screenshots below) as suggested by @MichaelMarcialis in the review. ### Screenshot #### Before <img width="882" alt="Screenshot 2024-04-15 at 15 16 59" src="8fa95a5b
-51c6-4220-8837-38adc4696602"> <img width="296" alt="Screenshot 2024-04-15 at 15 17 07" src="6c99a4db
-f7a6-4872-a24a-cddb56e0ec3d"> #### After <img width="916" alt="Screenshot 2024-04-15 at 15 18 08" src="1ae0b161
-c731-42e1-90af-69468e9f0905"> <img width="367" alt="Screenshot 2024-04-15 at 15 18 14" src="bb8818bd
-38d4-4ec3-8bf4-30e24afd3664"> ### How to test - Check that the actions buttons are not displayed when there are no requests in the input - Check that the actions buttons are not displayed when no requests are selected by the cursor or the selection - Check that the selected requests are highlighted - Check that the buttons are displayed on the 1st line of the 1st selected request - Check that the position of the buttons is updated when the editor is scrolled up or down ### Follow up issues - The functionality for the button to open a documentation for the 1st selected request - The functionality for the button to auto-indent input - The input highlighting is temporarily reset when sending a request - Sent requests need to be saved to history - After sending a request, the autocomplete polling needs to be restarted - Add more unit tests
This commit is contained in:
parent
721d354a13
commit
0b4e60e7d4
18 changed files with 995 additions and 43 deletions
|
@ -37,4 +37,8 @@ export {
|
|||
CONSOLE_OUTPUT_LANG_ID,
|
||||
CONSOLE_THEME_ID,
|
||||
CONSOLE_OUTPUT_THEME_ID,
|
||||
getParsedRequestsProvider,
|
||||
ConsoleParsedRequestsProvider,
|
||||
} from './src/console';
|
||||
|
||||
export type { ParsedRequest } from './src/console';
|
||||
|
|
63
packages/kbn-monaco/src/console/console_errors_provider.ts
Normal file
63
packages/kbn-monaco/src/console/console_errors_provider.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
/*
|
||||
* 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<void> => {
|
||||
if (model.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
const parserResult = await workerProxyService.getParserResult(model.uri);
|
||||
|
||||
if (!parserResult) {
|
||||
return;
|
||||
}
|
||||
const { errors } = parserResult;
|
||||
monaco.editor.setModelMarkers(
|
||||
model,
|
||||
CONSOLE_LANG_ID,
|
||||
errors.map(({ offset, text }) => {
|
||||
const { column, lineNumber } = model.getPositionAt(offset);
|
||||
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);
|
||||
};
|
|
@ -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 { 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,
|
||||
private model: monaco.editor.ITextModel | null
|
||||
) {}
|
||||
public async getRequests(): Promise<ParsedRequest[]> {
|
||||
if (!this.model) {
|
||||
return [];
|
||||
}
|
||||
const parserResult = await this.workerProxyService.getParserResult(this.model.uri);
|
||||
return parserResult?.requests ?? [];
|
||||
}
|
||||
}
|
37
packages/kbn-monaco/src/console/console_worker_proxy.ts
Normal file
37
packages/kbn-monaco/src/console/console_worker_proxy.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 '../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<ConsoleWorkerDefinition> | undefined;
|
||||
|
||||
public async getParserResult(modelUri: monaco.Uri): Promise<ConsoleParserResult | undefined> {
|
||||
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.getParserResult(modelUri.toString());
|
||||
}
|
||||
|
||||
public setup() {
|
||||
this.worker = monaco.editor.createWebWorker({ label: CONSOLE_LANG_ID, moduleId: '' });
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.worker) this.worker.dispose();
|
||||
}
|
||||
}
|
|
@ -40,3 +40,7 @@ export const ConsoleOutputLang: LangModuleType = {
|
|||
lexerRules: consoleOutputLexerRules,
|
||||
languageConfiguration: consoleOutputLanguageConfiguration,
|
||||
};
|
||||
|
||||
export type { ParsedRequest } from './types';
|
||||
export { getParsedRequestsProvider } from './language';
|
||||
export { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider';
|
||||
|
|
|
@ -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<ConsoleWorker>();
|
||||
monaco.languages.onLanguage(CONSOLE_LANG_ID, async () => {
|
||||
setupWorker(CONSOLE_LANG_ID, OWNER, wps);
|
||||
workerProxyService.setup();
|
||||
setupConsoleErrorsProvider(workerProxyService);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ export const createParser = () => {
|
|||
|
||||
let at, // The index of the current character
|
||||
ch, // The current character
|
||||
annos, // annotations
|
||||
escapee = {
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
|
@ -24,8 +23,44 @@ 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 });
|
||||
},
|
||||
requests,
|
||||
requestStartOffset,
|
||||
requestEndOffset,
|
||||
getLastRequest = function() {
|
||||
return requests.length > 0 ? requests.pop() : {};
|
||||
},
|
||||
addRequestStart = function() {
|
||||
requestStartOffset = at - 1;
|
||||
requests.push({ startOffset: requestStartOffset });
|
||||
},
|
||||
addRequestMethod = function(method) {
|
||||
const lastRequest = getLastRequest();
|
||||
lastRequest.method = method;
|
||||
requests.push(lastRequest);
|
||||
requestEndOffset = at - 1;
|
||||
},
|
||||
addRequestUrl = function(url) {
|
||||
const lastRequest = getLastRequest();
|
||||
lastRequest.url = url;
|
||||
requests.push(lastRequest);
|
||||
requestEndOffset = at - 1;
|
||||
},
|
||||
addRequestData = function(data) {
|
||||
const lastRequest = getLastRequest();
|
||||
const dataArray = lastRequest.data || [];
|
||||
dataArray.push(data);
|
||||
lastRequest.data = dataArray;
|
||||
requests.push(lastRequest);
|
||||
requestEndOffset = at - 1;
|
||||
},
|
||||
addRequestEnd = function() {
|
||||
const lastRequest = getLastRequest();
|
||||
lastRequest.endOffset = requestEndOffset;
|
||||
requests.push(lastRequest);
|
||||
},
|
||||
error = function (m) {
|
||||
throw {
|
||||
|
@ -373,14 +408,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 +427,13 @@ export const createParser = () => {
|
|||
strictWhite();
|
||||
while (ch == '{') {
|
||||
// another object
|
||||
object();
|
||||
const parsedObject = object();
|
||||
addRequestData(parsedObject);
|
||||
strictWhite();
|
||||
newLine();
|
||||
strictWhite();
|
||||
}
|
||||
addRequestEnd();
|
||||
},
|
||||
comment = function () {
|
||||
while (ch == '#') {
|
||||
|
@ -417,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);
|
||||
|
@ -432,15 +473,16 @@ export const createParser = () => {
|
|||
|
||||
text = source;
|
||||
at = 0;
|
||||
annos = [];
|
||||
errors = [];
|
||||
requests = [];
|
||||
next();
|
||||
multi_request();
|
||||
white();
|
||||
if (ch) {
|
||||
annotate('error', 'Syntax error');
|
||||
addError('Syntax error');
|
||||
}
|
||||
|
||||
result = { annotations: annos };
|
||||
result = { errors, requests };
|
||||
|
||||
return typeof reviver === 'function'
|
||||
? (function walk(holder, key) {
|
||||
|
|
59
packages/kbn-monaco/src/console/parser.test.ts
Normal file
59
packages/kbn-monaco/src/console/parser.test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
29
packages/kbn-monaco/src/console/types.ts
Normal file
29
packages/kbn-monaco/src/console/types.ts
Normal file
|
@ -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 {
|
||||
offset: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ParsedRequest {
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
method: string;
|
||||
url: string;
|
||||
data?: Array<Record<string, unknown>>;
|
||||
}
|
||||
export interface ConsoleParserResult {
|
||||
errors: ErrorAnnotation[];
|
||||
requests: ParsedRequest[];
|
||||
}
|
||||
|
||||
export interface ConsoleWorkerDefinition {
|
||||
getParserResult: (modelUri: string) => ConsoleParserResult | undefined;
|
||||
}
|
||||
export type ConsoleParser = (source: string) => ConsoleParserResult | undefined;
|
|
@ -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 { ConsoleParserResult, ConsoleWorkerDefinition, ConsoleParser } from '../types';
|
||||
import { createParser } from '../parser';
|
||||
|
||||
export class ConsoleWorker {
|
||||
constructor(private ctx: monaco.worker.IWorkerContext) {}
|
||||
private parser: Parser | undefined;
|
||||
export class ConsoleWorker implements ConsoleWorkerDefinition {
|
||||
private parser: ConsoleParser | undefined;
|
||||
private parserResult: ConsoleParserResult | undefined;
|
||||
|
||||
async parse(modelUri: string): Promise<ParseResult | undefined> {
|
||||
constructor(private ctx: monaco.worker.IWorkerContext) {}
|
||||
|
||||
getParserResult(modelUri: string): ConsoleParserResult | 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.parserResult = this.parser(model.getValue());
|
||||
}
|
||||
return this.parserResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ export class ConsoleMenu extends Component<Props, State> {
|
|||
defaultMessage: 'Request options',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="wrench" />
|
||||
<EuiIcon type="boxesVertical" />
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
|
@ -135,10 +135,22 @@ export class ConsoleMenu extends Component<Props, State> {
|
|||
this.closePopover();
|
||||
this.copyAsCurl();
|
||||
}}
|
||||
icon="copyClipboard"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.requestOptions.copyAsUrlButtonLabel"
|
||||
defaultMessage="Copy as cURL"
|
||||
defaultMessage="Copy cURL command"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="consoleMenuAutoIndent"
|
||||
key="Auto indent"
|
||||
onClick={this.autoIndent}
|
||||
icon="arrowEnd"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.requestOptions.autoIndentButtonLabel"
|
||||
defaultMessage="Apply indentations"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
|
@ -147,20 +159,11 @@ export class ConsoleMenu extends Component<Props, State> {
|
|||
onClick={() => {
|
||||
this.openDocs();
|
||||
}}
|
||||
icon="documentation"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.requestOptions.openDocumentationButtonLabel"
|
||||
defaultMessage="Open documentation"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="consoleMenuAutoIndent"
|
||||
key="Auto indent"
|
||||
onClick={this.autoIndent}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.requestOptions.autoIndentButtonLabel"
|
||||
defaultMessage="Auto indent"
|
||||
defaultMessage="View documentation"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
|
|
|
@ -288,14 +288,14 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) {
|
|||
})}
|
||||
>
|
||||
<EuiLink
|
||||
color="success"
|
||||
color="primary"
|
||||
onClick={sendCurrentRequest}
|
||||
data-test-subj="sendRequestButton"
|
||||
aria-label={i18n.translate('console.sendRequestButtonTooltip', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="playFilled" />
|
||||
<EuiIcon type="play" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -6,14 +6,22 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from '@kbn/monaco';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { CONSOLE_LANG_ID, CONSOLE_THEME_ID, monaco } from '@kbn/monaco';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ConsoleMenu } from '../../../components';
|
||||
import {
|
||||
useServicesContext,
|
||||
useEditorReadContext,
|
||||
useRequestActionContext,
|
||||
} from '../../../contexts';
|
||||
import { useSetInitialValue } from './use_set_initial_value';
|
||||
import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider';
|
||||
import { useSetupAutocompletePolling } from './use_setup_autocomplete_polling';
|
||||
import { useSetupAutosave } from './use_setup_autosave';
|
||||
import { useServicesContext, useEditorReadContext } from '../../../contexts';
|
||||
|
||||
export interface EditorProps {
|
||||
initialTextValue: string;
|
||||
|
@ -22,12 +30,32 @@ export interface EditorProps {
|
|||
export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
||||
const {
|
||||
services: {
|
||||
notifications: { toasts },
|
||||
notifications,
|
||||
esHostService,
|
||||
trackUiMetric,
|
||||
http,
|
||||
settings: settingsService,
|
||||
autocompleteInfo,
|
||||
},
|
||||
} = useServicesContext();
|
||||
const { toasts } = notifications;
|
||||
const { settings } = useEditorReadContext();
|
||||
const dispatch = useRequestActionContext();
|
||||
const actionsProvider = useRef<MonacoEditorActionsProvider | null>(null);
|
||||
const [editorActionsCss, setEditorActionsCss] = useState<CSSProperties>({});
|
||||
|
||||
const editorDidMountCallback = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
actionsProvider.current = new MonacoEditorActionsProvider(editor, setEditorActionsCss);
|
||||
}, []);
|
||||
|
||||
const getCurlCallback = useCallback(async (): Promise<string> => {
|
||||
const curl = await actionsProvider.current?.getCurl(esHostService.getHost());
|
||||
return curl ?? '';
|
||||
}, [esHostService]);
|
||||
|
||||
const sendRequestsCallback = useCallback(async () => {
|
||||
await actionsProvider.current?.sendRequests(toasts, dispatch, trackUiMetric, http);
|
||||
}, [dispatch, http, toasts, trackUiMetric]);
|
||||
|
||||
const [value, setValue] = useState(initialTextValue);
|
||||
|
||||
|
@ -47,6 +75,42 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
|||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
className="conApp__editorActions"
|
||||
id="ConAppEditorActions"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
style={editorActionsCss}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('console.sendRequestButtonTooltip', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
onClick={sendRequestsCallback}
|
||||
data-test-subj="sendRequestButton"
|
||||
aria-label={i18n.translate('console.sendRequestButtonTooltip', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="play" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ConsoleMenu
|
||||
getCurl={getCurlCallback}
|
||||
getDocumentation={() => {
|
||||
return Promise.resolve(null);
|
||||
}}
|
||||
autoIndent={() => {}}
|
||||
notifications={notifications}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<CodeEditor
|
||||
languageId={CONSOLE_LANG_ID}
|
||||
value={value}
|
||||
|
@ -58,6 +122,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
|||
wordWrap: settings.wrapMode === true ? 'on' : 'off',
|
||||
theme: CONSOLE_THEME_ID,
|
||||
}}
|
||||
editorDidMount={editorDidMountCallback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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<monaco.editor.IStandaloneCodeEditor>;
|
||||
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<monaco.editor.IStandaloneCodeEditor>;
|
||||
|
||||
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"');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { CSSProperties, Dispatch } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
ConsoleParsedRequestsProvider,
|
||||
getParsedRequestsProvider,
|
||||
monaco,
|
||||
ParsedRequest,
|
||||
} from '@kbn/monaco';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
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 {
|
||||
stringifyRequest,
|
||||
replaceRequestVariables,
|
||||
getCurlRequest,
|
||||
trackSentRequests,
|
||||
} from './utils';
|
||||
|
||||
const selectedRequestsClass = 'console__monaco_editor__selectedRequests';
|
||||
|
||||
export interface EditorRequest {
|
||||
method: string;
|
||||
url: string;
|
||||
data: string[];
|
||||
}
|
||||
|
||||
export class MonacoEditorActionsProvider {
|
||||
private parsedRequestsProvider: ConsoleParsedRequestsProvider;
|
||||
private highlightedLines: monaco.editor.IEditorDecorationsCollection;
|
||||
constructor(
|
||||
private editor: monaco.editor.IStandaloneCodeEditor,
|
||||
private setEditorActionsCss: (css: CSSProperties) => void
|
||||
) {
|
||||
this.parsedRequestsProvider = getParsedRequestsProvider(this.editor.getModel());
|
||||
this.highlightedLines = this.editor.createDecorationsCollection();
|
||||
this.editor.focus();
|
||||
|
||||
const debouncedHighlightRequests = debounce(() => this.highlightRequests(), 200, {
|
||||
leading: true,
|
||||
});
|
||||
debouncedHighlightRequests();
|
||||
|
||||
// init all listeners
|
||||
editor.onDidChangeCursorPosition(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidScrollChange(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidChangeCursorSelection(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidContentSizeChange(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
}
|
||||
|
||||
private updateEditorActions(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 highlightRequests(): Promise<void> {
|
||||
// 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;
|
||||
// display the actions buttons on the 1st line of the 1st selected request
|
||||
this.updateEditorActions(selectedRequestStartLine);
|
||||
this.highlightedLines.set([
|
||||
{
|
||||
range: selectedRange,
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: selectedRequestsClass,
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
// if no requests are selected, hide actions buttons and remove highlighted lines
|
||||
this.updateEditorActions();
|
||||
this.highlightedLines.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({
|
||||
parsedRequests: [],
|
||||
range: selection ?? new monaco.Range(1, 1, 1, 1),
|
||||
});
|
||||
}
|
||||
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;
|
||||
const { lineNumber: requestStartLine } = model.getPositionAt(requestStart);
|
||||
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;
|
||||
}
|
||||
if (requestStartLine > endLineNumber) {
|
||||
// request is past the selection, no need to check further requests
|
||||
break;
|
||||
}
|
||||
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 {
|
||||
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,
|
||||
selectionEndLine,
|
||||
model.getLineMaxColumn(selectionEndLine)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private async getRequests() {
|
||||
const { parsedRequests } = await this.getSelectedParsedRequestsAndRange();
|
||||
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<string> {
|
||||
const requests = await this.getRequests();
|
||||
const curlRequests = requests.map((request) => getCurlRequest(request, elasticsearchBaseUrl));
|
||||
return curlRequests.join('\n');
|
||||
}
|
||||
|
||||
public async sendRequests(
|
||||
toasts: IToasts,
|
||||
dispatch: Dispatch<Actions>,
|
||||
trackUiMetric: MetricsTracker,
|
||||
http: HttpSetup
|
||||
): Promise<void> {
|
||||
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',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<MetricsTracker> = { 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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -125,3 +125,10 @@
|
|||
.conApp__tabsExtension {
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
||||
/*
|
||||
* The highlighting for the selected requests in the monaco editor
|
||||
*/
|
||||
.console__monaco_editor__selectedRequests {
|
||||
background: transparentize($euiColorLightShade, .3);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue