[Console] Delete unused sense models and unused files (#195344)

This commit is contained in:
Ignacio Rivas 2024-10-09 17:54:43 +02:00 committed by GitHub
parent f2b9348f97
commit d273c07edc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 9 additions and 10277 deletions

View file

@ -74,37 +74,6 @@ under a "BSD" license.
Distributed under the BSD license:
Copyright (c) 2010, Ajax.org B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ajax.org B.V. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
This product includes code that is based on Ace editor, which was available
under a "BSD" license.
Distributed under the BSD license:
Copyright (c) 2010, Ajax.org B.V.
All rights reserved.

View file

@ -44,7 +44,7 @@ POST /_some_endpoint
```
## Architecture
Console uses Ace editor that is wrapped with [`CoreEditor`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/types/core_editor.ts), so that if needed it can easily be replaced with another editor, for example Monaco.
Console uses Monaco editor that is wrapped with [`kbn-monaco`](https://github.com/elastic/kibana/blob/main/packages/kbn-monaco/index.ts), so that if needed it can easily be replaced with another editor.
The autocomplete logic is located in [`autocomplete`](https://github.com/elastic/kibana/blob/main/src/plugins/console/public/lib/autocomplete) folder. Autocomplete rules are computed by classes in `components` sub-folder.
## Autocomplete definitions
@ -317,8 +317,4 @@ Another change is replacing jQuery with the core http client to communicate with
### Outstanding issues
#### Autocomplete suggestions for Kibana API endpoints
Console currently supports autocomplete suggestions for Elasticsearch API endpoints. The autocomplete suggestions for Kibana API endpoints are not supported yet.
Related issue: [#130661](https://github.com/elastic/kibana/issues/130661)
#### Migration to Monaco Editor
Console plugin is currently using Ace Editor and it is planned to migrate to Monaco Editor in the future.
Related issue: [#57435](https://github.com/elastic/kibana/issues/57435)
Related issue: [#130661](https://github.com/elastic/kibana/issues/130661)

View file

@ -8,12 +8,11 @@
*/
import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
import { SenseEditor } from '../../models/sense_editor';
export class EditorRegistry {
private inputEditor: SenseEditor | MonacoEditorActionsProvider | undefined;
private inputEditor: MonacoEditorActionsProvider | undefined;
setInputEditor(inputEditor: SenseEditor | MonacoEditorActionsProvider) {
setInputEditor(inputEditor: MonacoEditorActionsProvider) {
this.inputEditor = inputEditor;
}

View file

@ -8,7 +8,6 @@
*/
export { useSetInputEditor } from './use_set_input_editor';
export { useRestoreRequestFromHistory } from './use_restore_request_from_history';
export { useSendCurrentRequest, sendRequest } from './use_send_current_request';
export { sendRequest } from './use_send_current_request';
export { useSaveCurrentTextObject } from './use_save_current_text_object';
export { useDataInit } from './use_data_init';

View file

@ -1,10 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { useRestoreRequestFromHistory } from './use_restore_request_from_history';

View file

@ -1,48 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import RowParser from '../../../lib/row_parser';
import { ESRequest } from '../../../types';
import { SenseEditor } from '../../models/sense_editor';
import { formatRequestBodyDoc } from '../../../lib/utils';
export function restoreRequestFromHistory(editor: SenseEditor, req: ESRequest) {
const coreEditor = editor.getCoreEditor();
let pos = coreEditor.getCurrentPosition();
let prefix = '';
let suffix = '\n';
const parser = new RowParser(coreEditor);
if (parser.isStartRequestRow(pos.lineNumber)) {
pos.column = 1;
suffix += '\n';
} else if (parser.isEndRequestRow(pos.lineNumber)) {
const line = coreEditor.getLineValue(pos.lineNumber);
pos.column = line.length + 1;
prefix = '\n\n';
} else if (parser.isInBetweenRequestsRow(pos.lineNumber)) {
pos.column = 1;
} else {
pos = editor.nextRequestEnd(pos);
prefix = '\n\n';
}
let s = prefix + req.method + ' ' + req.endpoint;
if (req.data) {
const indent = true;
const formattedData = formatRequestBodyDoc([req.data], indent);
s += '\n' + formattedData.data;
}
s += suffix;
coreEditor.insert(pos, s);
coreEditor.moveCursorToPosition({ lineNumber: pos.lineNumber + prefix.length, column: 1 });
coreEditor.clearSelection();
coreEditor.getContainer().focus();
}

View file

@ -1,25 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { formatRequestBodyDoc } from '../../../lib/utils';
import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
import { ESRequest } from '../../../types';
export async function restoreRequestFromHistoryToMonaco(
provider: MonacoEditorActionsProvider,
req: ESRequest
) {
let s = req.method + ' ' + req.endpoint;
if (req.data) {
const indent = true;
const formattedData = formatRequestBodyDoc([req.data], indent);
s += '\n' + formattedData.data;
}
await provider.restoreRequestFromHistory(s);
}

View file

@ -1,21 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useCallback } from 'react';
import { instance as registry } from '../../contexts/editor_context/editor_registry';
import { ESRequest } from '../../../types';
import { restoreRequestFromHistoryToMonaco } from './restore_request_from_history_to_monaco';
import { MonacoEditorActionsProvider } from '../../containers/editor/monaco_editor_actions_provider';
export const useRestoreRequestFromHistory = () => {
return useCallback(async (req: ESRequest) => {
const editor = registry.getInputEditor();
await restoreRequestFromHistoryToMonaco(editor as MonacoEditorActionsProvider, req);
}, []);
};

View file

@ -7,5 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { useSendCurrentRequest } from './use_send_current_request';
export { sendRequest } from './send_request';

View file

@ -1,34 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SenseEditor } from '../../models/sense_editor';
import { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position';
import { MetricsTracker } from '../../../types';
export const track = (
requests: Array<{ method: string }>,
editor: SenseEditor,
trackUiMetric: MetricsTracker
) => {
const coreEditor = editor.getCoreEditor();
// `getEndpointFromPosition` gets values from the server-side generated JSON files which
// are a combination of JS, automatically generated JSON and manual overrides. That means
// the metrics reported from here will be tied to the definitions in those files.
// See src/legacy/core_plugins/console/server/api_server/spec
const endpointDescription = getEndpointFromPosition(
coreEditor,
coreEditor.getCurrentPosition(),
editor.parser
);
if (requests[0] && endpointDescription) {
const eventName = `${requests[0].method}_${endpointDescription.id ?? 'unknown'}`;
trackUiMetric.count(eventName);
}
};

View file

@ -1,130 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
jest.mock('./send_request', () => ({ sendRequest: jest.fn() }));
jest.mock('../../contexts/editor_context/editor_registry', () => ({
instance: { getInputEditor: jest.fn() },
}));
jest.mock('./track', () => ({ track: jest.fn() }));
jest.mock('../../contexts/request_context', () => ({ useRequestActionContext: jest.fn() }));
jest.mock('../../../lib/utils', () => ({ replaceVariables: jest.fn() }));
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { ContextValue, ServicesContextProvider } from '../../contexts';
import { serviceContextMock } from '../../contexts/services_context.mock';
import { useRequestActionContext } from '../../contexts/request_context';
import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry';
import * as utils from '../../../lib/utils';
import { sendRequest } from './send_request';
import { useSendCurrentRequest } from './use_send_current_request';
describe('useSendCurrentRequest', () => {
let mockContextValue: ContextValue;
let dispatch: (...args: unknown[]) => void;
const contexts = ({ children }: { children: JSX.Element }) => (
<ServicesContextProvider value={mockContextValue}>{children}</ServicesContextProvider>
);
beforeEach(() => {
mockContextValue = serviceContextMock.create();
dispatch = jest.fn();
(useRequestActionContext as jest.Mock).mockReturnValue(dispatch);
(utils.replaceVariables as jest.Mock).mockReturnValue(['test']);
});
afterEach(() => {
jest.resetAllMocks();
});
it('calls send request', async () => {
// Set up mocks
(mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({});
// This request should succeed
(sendRequest as jest.Mock).mockResolvedValue([]);
(editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
getRequestsInRange: () => ['test'],
}));
const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
await act(() => result.current());
expect(sendRequest).toHaveBeenCalledWith({
http: mockContextValue.services.http,
requests: ['test'],
});
// Second call should be the request success
const [, [requestSucceededCall]] = (dispatch as jest.Mock).mock.calls;
expect(requestSucceededCall).toEqual({ type: 'requestSuccess', payload: { data: [] } });
});
it('handles known errors', async () => {
// Set up mocks
(sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' });
(editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
getRequestsInRange: () => ['test'],
}));
const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
await act(() => result.current());
// Second call should be the request failure
const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls;
// The request must have concluded
expect(requestFailedCall).toEqual({ type: 'requestFail', payload: { response: 'nada' } });
});
it('handles unknown errors', async () => {
// Set up mocks
(sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */);
(editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
getRequestsInRange: () => ['test'],
}));
const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
await act(() => result.current());
// Second call should be the request failure
const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls;
// The request must have concluded
expect(requestFailedCall).toEqual({ type: 'requestFail', payload: undefined });
// It also notified the user
expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledWith(NaN, {
title: 'Unknown Request Error',
});
});
it('notifies the user about save to history errors once only', async () => {
// Set up mocks
(sendRequest as jest.Mock).mockReturnValue(
[{ request: {} }, { request: {} }] /* two responses to save history */
);
(mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({
isHistoryEnabled: true,
});
(mockContextValue.services.history.addToHistory as jest.Mock).mockImplementation(() => {
// Mock throwing
throw new Error('cannot save!');
});
(editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({
getRequestsInRange: () => ['test', 'test'],
}));
const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts });
await act(() => result.current());
expect(dispatch).toHaveBeenCalledTimes(2);
expect(mockContextValue.services.history.addToHistory).toHaveBeenCalledTimes(2);
// It only called notification once
expect(mockContextValue.services.notifications.toasts.addError).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,148 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import { useCallback } from 'react';
import { toMountPoint } from '../../../shared_imports';
import { isQuotaExceededError } from '../../../services/history';
import { instance as registry } from '../../contexts/editor_context/editor_registry';
import { useRequestActionContext, useServicesContext } from '../../contexts';
import { StorageQuotaError } from '../../components/storage_quota_error';
import { sendRequest } from './send_request';
import { track } from './track';
import { replaceVariables } from '../../../lib/utils';
import { StorageKeys } from '../../../services';
import { DEFAULT_VARIABLES } from '../../../../common/constants';
import { SenseEditor } from '../../models';
export const useSendCurrentRequest = () => {
const {
services: { history, settings, notifications, trackUiMetric, http, autocompleteInfo, storage },
...startServices
} = useServicesContext();
const dispatch = useRequestActionContext();
return useCallback(async () => {
try {
const editor = registry.getInputEditor() as SenseEditor;
const variables = storage.get(StorageKeys.VARIABLES, DEFAULT_VARIABLES);
let requests = await editor.getRequestsInRange();
requests = replaceVariables(requests, variables);
if (!requests.length) {
notifications.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 });
// Fire and forget
setTimeout(() => track(requests, editor as SenseEditor, trackUiMetric), 0);
const results = await sendRequest({ http, requests });
let saveToHistoryError: undefined | Error;
const { isHistoryEnabled } = settings.toJSON();
if (isHistoryEnabled) {
results.forEach(({ request: { path, method, data } }) => {
try {
history.addToHistory(path, method, data);
} catch (e) {
// Grab only the first error
if (!saveToHistoryError) {
saveToHistoryError = e;
}
}
});
}
if (saveToHistoryError) {
const errorTitle = i18n.translate('console.notification.error.couldNotSaveRequestTitle', {
defaultMessage: 'Could not save request to Console history.',
});
if (isQuotaExceededError(saveToHistoryError)) {
const toast = notifications.toasts.addWarning({
title: i18n.translate('console.notification.error.historyQuotaReachedMessage', {
defaultMessage:
'Request history is full. Clear the console history or disable saving new requests.',
}),
text: toMountPoint(
StorageQuotaError({
onClearHistory: () => {
history.clearHistory();
notifications.toasts.remove(toast);
},
onDisableSavingToHistory: () => {
settings.setIsHistoryEnabled(false);
notifications.toasts.remove(toast);
},
}),
startServices
),
});
} else {
// Best effort, but still notify the user.
notifications.toasts.addError(saveToHistoryError, {
title: errorTitle,
});
}
}
const { polling } = settings.toJSON();
if (polling) {
// If the user has submitted a request against ES, something in the fields, indices, aliases,
// or templates may have changed, so we'll need to update this data. Assume that if
// the user disables polling they're trying to optimize performance or otherwise
// preserve resources, so they won't want this request sent either.
autocompleteInfo.retrieve(settings, settings.getAutocomplete());
}
dispatch({
type: 'requestSuccess',
payload: {
data: results,
},
});
} catch (e) {
if (e?.response) {
dispatch({
type: 'requestFail',
payload: e,
});
} else {
dispatch({
type: 'requestFail',
payload: undefined,
});
notifications.toasts.addError(e, {
title: i18n.translate('console.notification.error.unknownErrorTitle', {
defaultMessage: 'Unknown Request Error',
}),
});
}
}
}, [
storage,
dispatch,
http,
settings,
notifications.toasts,
trackUiMetric,
history,
autocompleteInfo,
startServices,
]);
};

View file

@ -10,14 +10,13 @@
import { useCallback } from 'react';
import { useEditorActionContext } from '../contexts/editor_context';
import { instance as registry } from '../contexts/editor_context/editor_registry';
import { SenseEditor } from '../models';
import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider';
export const useSetInputEditor = () => {
const dispatch = useEditorActionContext();
return useCallback(
(editor: SenseEditor | MonacoEditorActionsProvider) => {
(editor: MonacoEditorActionsProvider) => {
dispatch({ type: 'setInputEditor', payload: editor });
registry.setInputEditor(editor);
},

View file

@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './legacy_core_editor/legacy_core_editor';
export * from './sense_editor';

View file

@ -1,20 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import { LegacyCoreEditor } from './legacy_core_editor';
export const create = (el: HTMLElement) => {
const actions = document.querySelector<HTMLElement>('#ConAppEditorActions');
if (!actions) {
throw new Error('Could not find ConAppEditorActions element!');
}
const aceEditor = ace.edit(el);
return new LegacyCoreEditor(aceEditor, actions);
};

View file

@ -1,81 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import _ from 'lodash';
import ace from 'brace';
import { Mode } from './mode/output';
import smartResize from './smart_resize';
export interface CustomAceEditor extends ace.Editor {
update: (text: string, mode?: string | Mode, cb?: () => void) => void;
append: (text: string, foldPrevious?: boolean, cb?: () => void) => void;
}
/**
* Note: using read-only ace editor leaks the Ace editor API - use this as sparingly as possible or
* create an interface for it so that we don't rely directly on vendor APIs.
*/
export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor {
const output: CustomAceEditor = ace.acequire('ace/ace').edit(element);
const outputMode = new Mode();
output.$blockScrolling = Infinity;
output.resize = smartResize(output);
output.update = (val, mode, cb) => {
if (typeof mode === 'function') {
cb = mode as () => void;
mode = void 0;
}
const session = output.getSession();
const currentMode = val ? mode || outputMode : 'ace/mode/text';
// @ts-ignore
// ignore ts error here due to type definition mistake in brace for setMode(mode: string): void;
// this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467
session.setMode(currentMode);
session.setValue(val);
if (typeof cb === 'function') {
setTimeout(cb);
}
};
output.append = (val: string, foldPrevious?: boolean, cb?: () => void) => {
if (typeof foldPrevious === 'function') {
cb = foldPrevious;
foldPrevious = true;
}
if (_.isUndefined(foldPrevious)) {
foldPrevious = true;
}
const session = output.getSession();
const lastLine = session.getLength();
if (foldPrevious) {
output.moveCursorTo(Math.max(0, lastLine - 1), 0);
}
session.insert({ row: lastLine, column: 0 }, '\n' + val);
output.moveCursorTo(lastLine + 1, 0);
if (typeof cb === 'function') {
setTimeout(cb);
}
};
(function setupSession(session) {
session.setMode('ace/mode/text');
(session as unknown as { setFoldStyle: (v: string) => void }).setFoldStyle('markbeginend');
session.setTabSize(2);
session.setUseWrapMode(true);
})(output.getSession());
output.setShowPrintMargin(false);
output.setReadOnly(true);
return output;
}

View file

@ -1,18 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import 'brace';
import 'brace/ext/language_tools';
import 'brace/ext/searchbox';
import 'brace/mode/json';
import 'brace/mode/text';
export * from './legacy_core_editor';
export * from './create_readonly';
export * from './create';

View file

@ -1,559 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './legacy_core_editor.test.mocks';
import RowParser from '../../../lib/row_parser';
import { createTokenIterator } from '../../factories';
import $ from 'jquery';
import { create } from './create';
describe('Input', () => {
let coreEditor;
beforeEach(() => {
// Set up our document body
document.body.innerHTML = `<div>
<div id="ConAppEditor" />
<div id="ConAppEditorActions" />
<div id="ConCopyAsCurl" />
</div>`;
coreEditor = create(document.querySelector('#ConAppEditor'));
$(coreEditor.getContainer()).show();
});
afterEach(() => {
$(coreEditor.getContainer()).hide();
});
describe('.getLineCount', () => {
it('returns the correct line length', async () => {
await coreEditor.setValue('1\n2\n3\n4', true);
expect(coreEditor.getLineCount()).toBe(4);
});
});
describe('Tokenization', () => {
function tokensAsList() {
const iter = createTokenIterator({
editor: coreEditor,
position: { lineNumber: 1, column: 1 },
});
const ret = [];
let t = iter.getCurrentToken();
const parser = new RowParser(coreEditor);
if (parser.isEmptyToken(t)) {
t = parser.nextNonEmptyToken(iter);
}
while (t) {
ret.push({ value: t.value, type: t.type });
t = parser.nextNonEmptyToken(iter);
}
return ret;
}
let testCount = 0;
function tokenTest(tokenList, prefix, data) {
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 3);
}
if (data) {
if (prefix) {
data = prefix + '\n' + data;
}
} else {
data = prefix;
}
test('Token test ' + testCount++ + ' prefix: ' + prefix, async function () {
await coreEditor.setValue(data, true);
const tokens = tokensAsList();
const normTokenList = [];
for (let i = 0; i < tokenList.length; i++) {
normTokenList.push({ type: tokenList[i++], value: tokenList[i] });
}
expect(tokens).toEqual(normTokenList);
});
}
tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search');
tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search');
tokenTest(
[
'method',
'GET',
'url.protocol_host',
'http://somehost',
'url.slash',
'/',
'url.part',
'_search',
],
'GET http://somehost/_search'
);
tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost');
tokenTest(
['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'],
'GET http://somehost/'
);
tokenTest(
['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'],
'GET http://test:user@somehost/'
);
tokenTest(
['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'],
'GET _cluster/nodes'
);
tokenTest(
[
'method',
'GET',
'url.slash',
'/',
'url.part',
'_cluster',
'url.slash',
'/',
'url.part',
'nodes',
],
'GET /_cluster/nodes'
);
tokenTest(
['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'],
'GET index/_search'
);
tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index');
tokenTest(
['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'],
'GET index/type'
);
tokenTest(
[
'method',
'GET',
'url.slash',
'/',
'url.part',
'index',
'url.slash',
'/',
'url.part',
'type',
'url.slash',
'/',
],
'GET /index/type/'
);
tokenTest(
[
'method',
'GET',
'url.part',
'index',
'url.slash',
'/',
'url.part',
'type',
'url.slash',
'/',
'url.part',
'_search',
],
'GET index/type/_search'
);
tokenTest(
[
'method',
'GET',
'url.part',
'index',
'url.slash',
'/',
'url.part',
'type',
'url.slash',
'/',
'url.part',
'_search',
'url.questionmark',
'?',
'url.param',
'value',
'url.equal',
'=',
'url.value',
'1',
],
'GET index/type/_search?value=1'
);
tokenTest(
[
'method',
'GET',
'url.part',
'index',
'url.slash',
'/',
'url.part',
'type',
'url.slash',
'/',
'url.part',
'1',
],
'GET index/type/1'
);
tokenTest(
[
'method',
'GET',
'url.slash',
'/',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
'url.slash',
'/',
],
'GET /index1,index2/'
);
tokenTest(
[
'method',
'GET',
'url.slash',
'/',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
'url.slash',
'/',
'url.part',
'_search',
],
'GET /index1,index2/_search'
);
tokenTest(
[
'method',
'GET',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
'url.slash',
'/',
'url.part',
'_search',
],
'GET index1,index2/_search'
);
tokenTest(
[
'method',
'GET',
'url.slash',
'/',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
],
'GET /index1,index2'
);
tokenTest(
['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'],
'GET index1,index2'
);
tokenTest(
['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','],
'GET /index1,'
);
tokenTest(
['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'],
'PUT /index/'
);
tokenTest(
['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'],
'GET index/_search '
);
tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index');
tokenTest(
[
'method',
'PUT',
'url.slash',
'/',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
'url.slash',
'/',
'url.part',
'type1',
'url.comma',
',',
'url.part',
'type2',
],
'PUT /index1,index2/type1,type2'
);
tokenTest(
[
'method',
'PUT',
'url.slash',
'/',
'url.part',
'index1',
'url.slash',
'/',
'url.part',
'type1',
'url.comma',
',',
'url.part',
'type2',
'url.comma',
',',
],
'PUT /index1/type1,type2,'
);
tokenTest(
[
'method',
'PUT',
'url.part',
'index1',
'url.comma',
',',
'url.part',
'index2',
'url.slash',
'/',
'url.part',
'type1',
'url.comma',
',',
'url.part',
'type2',
'url.slash',
'/',
'url.part',
'1234',
],
'PUT index1,index2/type1,type2/1234'
);
tokenTest(
[
'method',
'POST',
'url.part',
'_search',
'paren.lparen',
'{',
'variable',
'"q"',
'punctuation.colon',
':',
'paren.lparen',
'{',
'paren.rparen',
'}',
'paren.rparen',
'}',
],
'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}'
);
tokenTest(
[
'method',
'POST',
'url.part',
'_search',
'paren.lparen',
'{',
'variable',
'"q"',
'punctuation.colon',
':',
'paren.lparen',
'{',
'variable',
'"s"',
'punctuation.colon',
':',
'paren.lparen',
'{',
'paren.rparen',
'}',
'paren.rparen',
'}',
'paren.rparen',
'}',
],
'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}'
);
function statesAsList() {
const ret = [];
const maxLine = coreEditor.getLineCount();
for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line));
return ret;
}
function statesTest(statesList, prefix, data) {
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 3);
}
if (data) {
if (prefix) {
data = prefix + '\n' + data;
}
} else {
data = prefix;
}
test('States test ' + testCount++ + ' prefix: ' + prefix, async function () {
await coreEditor.setValue(data, true);
const modes = statesAsList();
expect(modes).toEqual(statesList);
});
}
statesTest(
['start', 'json', 'json', 'start'],
'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}'
);
statesTest(
['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'],
'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}'
);
statesTest(
['start', 'json', 'json', 'start'],
'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}'
);
statesTest(
['start', 'json', 'json', 'start'],
'POST _search\n' + '{\n' + ' "script": ""\n' + '}'
);
statesTest(
['start', 'json', ['json', 'json'], 'json', 'start'],
'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}'
);
statesTest(
[
'start',
'json',
['script-start', 'json', 'json', 'json'],
['script-start', 'json', 'json', 'json'],
['json', 'json'],
'json',
'start',
],
'POST _search\n' +
'{\n' +
' "test": { "script": """\n' +
' test script\n' +
' """\n' +
' }\n' +
'}'
);
statesTest(
['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'],
'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}'
);
statesTest(
['start', 'json', 'json', 'start'],
'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}'
);
statesTest(
['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'],
'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}'
);
statesTest(
[
'start',
'json',
['string_literal', 'json', 'json', 'json'],
['string_literal', 'json', 'json', 'json'],
['json', 'json'],
['json', 'json'],
'json',
'start',
],
'POST _search\n' +
'{\n' +
' "something": { "f" : """\n' +
' test script\n' +
' """,\n' +
' "g": 1\n' +
' }\n' +
'}'
);
statesTest(
['start', 'json', 'json', 'start'],
'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}'
);
});
});

View file

@ -1,29 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
jest.mock('./mode/worker', () => {
return { workerModule: { id: 'sense_editor/mode/worker', src: '' } };
});
import '@kbn/web-worker-stub';
// @ts-ignore
window.URL = {
createObjectURL: () => {
return '';
},
};
import 'brace';
import 'brace/ext/language_tools';
import 'brace/ext/searchbox';
import 'brace/mode/json';
import 'brace/mode/text';
document.queryCommandSupported = () => true;

View file

@ -1,511 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace, { type Annotation } from 'brace';
import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace';
import $ from 'jquery';
import {
CoreEditor,
Position,
Range,
Token,
TokensProvider,
EditorEvent,
AutoCompleterFunction,
} from '../../../types';
import { AceTokensProvider } from '../../../lib/ace_token_provider';
import * as curl from '../sense_editor/curl';
import smartResize from './smart_resize';
import * as InputMode from './mode/input';
const _AceRange = ace.acequire('ace/range').Range;
const rangeToAceRange = ({ start, end }: Range) =>
new _AceRange(start.lineNumber - 1, start.column - 1, end.lineNumber - 1, end.column - 1);
export class LegacyCoreEditor implements CoreEditor {
private _aceOnPaste: Function;
$actions: JQuery<HTMLElement>;
resize: () => void;
constructor(private readonly editor: IAceEditor, actions: HTMLElement) {
this.$actions = $(actions);
this.editor.setShowPrintMargin(false);
const session = this.editor.getSession();
// @ts-expect-error
// ignore ts error here due to type definition mistake in brace for setMode(mode: string): void;
// this method accepts string or SyntaxMode which is an object. See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L467
session.setMode(new InputMode.Mode());
(session as unknown as { setFoldStyle: (style: string) => void }).setFoldStyle('markbeginend');
session.setTabSize(2);
session.setUseWrapMode(true);
this.resize = smartResize(this.editor);
// Intercept ace on paste handler.
this._aceOnPaste = this.editor.onPaste;
this.editor.onPaste = this.DO_NOT_USE_onPaste.bind(this);
this.editor.setOptions({
enableBasicAutocompletion: true,
});
this.editor.$blockScrolling = Infinity;
this.hideActionsBar();
this.editor.focus();
}
// dirty check for tokenizer state, uses a lot less cycles
// than listening for tokenizerUpdate
waitForLatestTokens(): Promise<void> {
return new Promise<void>((resolve) => {
const session = this.editor.getSession();
const checkInterval = 25;
const check = () => {
// If the bgTokenizer doesn't exist, we can assume that the underlying editor has been
// torn down, e.g. by closing the History tab, and we don't need to do anything further.
if (session.bgTokenizer) {
// Wait until the bgTokenizer is done running before executing the callback.
if ((session.bgTokenizer as unknown as { running: boolean }).running) {
setTimeout(check, checkInterval);
} else {
resolve();
}
}
};
setTimeout(check, 0);
});
}
getLineState(lineNumber: number) {
const session = this.editor.getSession();
return session.getState(lineNumber - 1);
}
getValueInRange(range: Range): string {
return this.editor.getSession().getTextRange(rangeToAceRange(range));
}
getTokenProvider(): TokensProvider {
return new AceTokensProvider(this.editor.getSession());
}
getValue(): string {
return this.editor.getValue();
}
async setValue(text: string, forceRetokenize: boolean): Promise<void> {
const session = this.editor.getSession();
session.setValue(text);
if (forceRetokenize) {
await this.forceRetokenize();
}
}
getLineValue(lineNumber: number): string {
const session = this.editor.getSession();
return session.getLine(lineNumber - 1);
}
getCurrentPosition(): Position {
const cursorPosition = this.editor.getCursorPosition();
return {
lineNumber: cursorPosition.row + 1,
column: cursorPosition.column + 1,
};
}
clearSelection(): void {
this.editor.clearSelection();
}
getTokenAt(pos: Position): Token | null {
const provider = this.getTokenProvider();
return provider.getTokenAt(pos);
}
insert(valueOrPos: string | Position, value?: string): void {
if (typeof valueOrPos === 'string') {
this.editor.insert(valueOrPos);
return;
}
const document = this.editor.getSession().getDocument();
document.insert(
{
column: valueOrPos.column - 1,
row: valueOrPos.lineNumber - 1,
},
value || ''
);
}
moveCursorToPosition(pos: Position): void {
this.editor.moveCursorToPosition({ row: pos.lineNumber - 1, column: pos.column - 1 });
}
replace(range: Range, value: string): void {
const session = this.editor.getSession();
session.replace(rangeToAceRange(range), value);
}
getLines(startLine: number, endLine: number): string[] {
const session = this.editor.getSession();
return session.getLines(startLine - 1, endLine - 1);
}
replaceRange(range: Range, value: string) {
const pos = this.editor.getCursorPosition();
this.editor.getSession().replace(rangeToAceRange(range), value);
const maxRow = Math.max(range.start.lineNumber - 1 + value.split('\n').length - 1, 1);
pos.row = Math.min(pos.row, maxRow);
this.editor.moveCursorToPosition(pos);
// ACE UPGRADE - check if needed - at the moment the above may trigger a selection.
this.editor.clearSelection();
}
getSelectionRange() {
const result = this.editor.getSelectionRange();
return {
start: {
lineNumber: result.start.row + 1,
column: result.start.column + 1,
},
end: {
lineNumber: result.end.row + 1,
column: result.end.column + 1,
},
};
}
getLineCount() {
// Only use this function to return line count as it uses
// a cache.
return this.editor.getSession().getLength();
}
addMarker(range: Range) {
return this.editor
.getSession()
.addMarker(rangeToAceRange(range), 'ace_snippet-marker', 'fullLine', false);
}
removeMarker(ref: number) {
this.editor.getSession().removeMarker(ref);
}
getWrapLimit(): number {
return this.editor.getSession().getWrapLimit();
}
on(event: EditorEvent, listener: () => void) {
if (event === 'changeCursor') {
this.editor.getSession().selection.on(event, listener);
} else if (event === 'changeSelection') {
this.editor.on(event, listener);
} else {
this.editor.getSession().on(event, listener);
}
}
off(event: EditorEvent, listener: () => void) {
if (event === 'changeSelection') {
this.editor.off(event, listener);
}
}
isCompleterActive() {
return Boolean(
(this.editor as unknown as { completer: { activated: unknown } }).completer &&
(this.editor as unknown as { completer: { activated: unknown } }).completer.activated
);
}
detachCompleter() {
// In some situations we need to detach the autocomplete suggestions element manually,
// such as when navigating away from Console when the suggestions list is open.
const completer = (this.editor as unknown as { completer: { detach(): void } }).completer;
return completer?.detach();
}
private forceRetokenize() {
const session = this.editor.getSession();
return new Promise<void>((resolve) => {
// force update of tokens, but not on this thread to allow for ace rendering.
setTimeout(function () {
let i;
for (i = 0; i < session.getLength(); i++) {
session.getTokens(i);
}
resolve();
});
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
private DO_NOT_USE_onPaste(text: string) {
if (text && curl.detectCURL(text)) {
const curlInput = curl.parseCURL(text);
this.editor.insert(curlInput);
return;
}
this._aceOnPaste.call(this.editor, text);
}
private setActionsBar = (value: number | null, topOrBottom: 'top' | 'bottom' = 'top') => {
if (value === null) {
this.$actions.css('visibility', 'hidden');
} else {
if (topOrBottom === 'top') {
this.$actions.css({
bottom: 'auto',
top: value,
visibility: 'visible',
});
} else {
this.$actions.css({
top: 'auto',
bottom: value,
visibility: 'visible',
});
}
}
};
private hideActionsBar = () => {
this.setActionsBar(null);
};
execCommand(cmd: string) {
this.editor.execCommand(cmd);
}
getContainer(): HTMLDivElement {
return this.editor.container as HTMLDivElement;
}
setStyles(styles: { wrapLines: boolean; fontSize: string }) {
this.editor.getSession().setUseWrapMode(styles.wrapLines);
this.editor.container.style.fontSize = styles.fontSize;
}
registerKeyboardShortcut(opts: { keys: string; fn: () => void; name: string }): void {
this.editor.commands.addCommand({
exec: opts.fn,
name: opts.name,
bindKey: opts.keys,
});
}
unregisterKeyboardShortcut(command: string) {
// @ts-ignore
this.editor.commands.removeCommand(command);
}
legacyUpdateUI(range: Range) {
if (!this.$actions) {
return;
}
if (range) {
// elements are positioned relative to the editor's container
// pageY is relative to page, so subtract the offset
// from pageY to get the new top value
const offsetFromPage = $(this.editor.container).offset()!.top;
const startLine = range.start.lineNumber;
const startColumn = range.start.column;
const firstLine = this.getLineValue(startLine);
const maxLineLength = this.getWrapLimit() - 5;
const isWrapping = firstLine.length > maxLineLength;
const totalOffset = offsetFromPage - (window.pageYOffset || 0);
const getScreenCoords = (line: number) =>
this.editor.renderer.textToScreenCoordinates(line - 1, startColumn).pageY - totalOffset;
const topOfReq = getScreenCoords(startLine);
if (topOfReq >= 0) {
const { bottom: maxBottom } = this.editor.container.getBoundingClientRect();
if (topOfReq > maxBottom - totalOffset) {
this.setActionsBar(0, 'bottom');
return;
}
let offset = 0;
if (isWrapping) {
// Try get the line height of the text area in pixels.
const textArea = $(this.editor.container.querySelector('textArea')!);
const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength;
if (textArea && hasRoomOnNextLine) {
// Line height + the number of wraps we have on a line.
offset += this.getLineValue(startLine).length * textArea.height()!;
} else {
if (startLine > 1) {
this.setActionsBar(getScreenCoords(startLine - 1));
return;
}
this.setActionsBar(getScreenCoords(startLine + 1));
return;
}
}
this.setActionsBar(topOfReq + offset);
return;
}
const bottomOfReq =
this.editor.renderer.textToScreenCoordinates(range.end.lineNumber, range.end.column).pageY -
offsetFromPage;
if (bottomOfReq >= 0) {
this.setActionsBar(0);
return;
}
}
}
registerAutocompleter(autocompleter: AutoCompleterFunction): void {
// Hook into Ace
// disable standard context based autocompletion.
// @ts-ignore
ace.define(
'ace/autocomplete/text_completer',
['require', 'exports', 'module'],
function (
require: unknown,
exports: {
getCompletions: (
innerEditor: unknown,
session: unknown,
pos: unknown,
prefix: unknown,
callback: (e: null | Error, values: string[]) => void
) => void;
}
) {
exports.getCompletions = function (innerEditor, session, pos, prefix, callback) {
callback(null, []);
};
}
);
const langTools = ace.acequire('ace/ext/language_tools');
langTools.setCompleters([
{
identifierRegexps: [
/[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character
],
getCompletions: (
// eslint-disable-next-line @typescript-eslint/naming-convention
DO_NOT_USE_1: IAceEditor,
aceEditSession: IAceEditSession,
pos: { row: number; column: number },
prefix: string,
callback: (...args: unknown[]) => void
) => {
const position: Position = {
lineNumber: pos.row + 1,
column: pos.column + 1,
};
const getAnnotationControls = () => {
let customAnnotation: Annotation;
return {
setAnnotation(text: string) {
const annotations = aceEditSession.getAnnotations();
customAnnotation = {
text,
row: pos.row,
column: pos.column,
type: 'warning',
};
aceEditSession.setAnnotations([...annotations, customAnnotation]);
},
removeAnnotation() {
aceEditSession.setAnnotations(
aceEditSession.getAnnotations().filter((a: Annotation) => a !== customAnnotation)
);
},
};
};
autocompleter(position, prefix, callback, getAnnotationControls());
},
},
]);
}
destroy() {
this.editor.destroy();
}
/**
* Formats body of the request in the editor by removing the extra whitespaces at the beginning of lines,
* And adds the correct indentation for each line
* @param reqRange request range to indent
*/
autoIndent(reqRange: Range) {
const session = this.editor.getSession();
const mode = session.getMode();
const startRow = reqRange.start.lineNumber;
const endRow = reqRange.end.lineNumber;
const tab = session.getTabString();
for (let row = startRow; row <= endRow; row++) {
let prevLineState = '';
let prevLineIndent = '';
if (row > 0) {
prevLineState = session.getState(row - 1);
const prevLine = session.getLine(row - 1);
prevLineIndent = mode.getNextLineIndent(prevLineState, prevLine, tab);
}
const line = session.getLine(row);
// @ts-ignore
// Brace does not expose type definition for mode.$getIndent, though we have access to this method provided by the underlying Ace editor.
// See https://github.com/ajaxorg/ace/blob/87ce087ed1cf20eeabe56fb0894e048d9bc9c481/lib/ace/mode/text.js#L259
const currLineIndent = mode.$getIndent(line);
if (prevLineIndent !== currLineIndent) {
if (currLineIndent.length > 0) {
// If current line has indentation, remove it.
// Next we will add the correct indentation by looking at the previous line
const range = new _AceRange(row, 0, row, currLineIndent.length);
session.remove(range);
}
if (prevLineIndent.length > 0) {
// If previous line has indentation, add indentation at the current line
session.insert({ row, column: 0 }, prevLineIndent);
}
}
// Lastly outdent any closing braces
mode.autoOutdent(prevLineState, session, row);
}
}
getAllFoldRanges(): Range[] {
const session = this.editor.getSession();
// @ts-ignore
// Brace does not expose type definition for session.getAllFolds, though we have access to this method provided by the underlying Ace editor.
// See https://github.com/ajaxorg/ace/blob/13dc911dbc0ea31ca343d5744b3f472767458fc3/ace.d.ts#L82
return session.getAllFolds().map((fold) => fold.range);
}
addFoldsAtRanges(foldRanges: Range[]) {
const session = this.editor.getSession();
foldRanges.forEach((range) => {
try {
session.addFold('...', _AceRange.fromPoints(range.start, range.end));
} catch (e) {
// ignore the error if a fold fails
}
});
}
}

View file

@ -1,79 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import { workerModule } from './worker';
import { ScriptMode } from './script';
const TextMode = ace.acequire('ace/mode/text').Mode;
const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
const WorkerClient = ace.acequire('ace/worker/worker_client').WorkerClient;
const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer;
import { InputHighlightRules } from './input_highlight_rules';
export class Mode extends TextMode {
constructor() {
super();
this.$tokenizer = new AceTokenizer(new InputHighlightRules().getRules());
this.$outdent = new MatchingBraceOutdent();
this.$behaviour = new CstyleBehaviour();
this.foldingRules = new CStyleFoldMode();
this.createModeDelegates({
'script-': ScriptMode,
});
}
}
(function (this: Mode) {
this.getCompletions = function () {
// autocomplete is done by the autocomplete module.
return [];
};
this.getNextLineIndent = function (state: string, line: string, tab: string) {
let indent = this.$getIndent(line);
if (state !== 'string_literal') {
const match = line.match(/^.*[\{\(\[]\s*$/);
if (match) {
indent += tab;
}
}
return indent;
};
this.checkOutdent = function (state: unknown, line: string, input: string) {
return this.$outdent.checkOutdent(line, input);
};
this.autoOutdent = function (state: unknown, doc: string, row: string) {
this.$outdent.autoOutdent(doc, row);
};
this.createWorker = function (session: {
getDocument: () => string;
setAnnotations: (arg0: unknown) => void;
}) {
const worker = new WorkerClient(['ace', 'sense_editor'], workerModule, 'SenseWorker');
worker.attachToDocument(session.getDocument());
worker.on('error', function (e: { data: unknown }) {
session.setAnnotations([e.data]);
});
worker.on('ok', function (anno: { data: unknown }) {
session.setAnnotations(anno.data);
});
return worker;
};
}).call(Mode.prototype);

View file

@ -1,180 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import { addXJsonToRules } from '@kbn/ace';
type Token =
| string
| { token?: string; regex?: string; next?: string; push?: boolean; include?: string };
export function addEOL(
tokens: Token[],
reg: string | RegExp,
nextIfEOL: string,
normalNext?: string
) {
if (typeof reg === 'object') {
reg = reg.source;
}
return [
{ token: tokens.concat(['whitespace']), regex: reg + '(\\s*)$', next: nextIfEOL },
{ token: tokens, regex: reg, next: normalNext },
];
}
export const mergeTokens = (...args: any[]) => [].concat.apply([], args);
const TextHighlightRules = ace.acequire('ace/mode/text_highlight_rules').TextHighlightRules;
// translating this to monaco
export class InputHighlightRules extends TextHighlightRules {
constructor() {
super();
this.$rules = {
// TODO
'start-sql': [
{ token: 'whitespace', regex: '\\s+' },
{ token: 'paren.lparen', regex: '{', next: 'json-sql', push: true },
{ regex: '', next: 'start' },
],
start: mergeTokens(
[
// done
{ token: 'warning', regex: '#!.*$' },
// done
{ include: 'comments' },
// done
{ token: 'paren.lparen', regex: '{', next: 'json', push: true },
],
// done
addEOL(['method'], /([a-zA-Z]+)/, 'start', 'method_sep'),
[
// done
{
token: 'whitespace',
regex: '\\s+',
},
// done
{
token: 'text',
regex: '.+?',
},
]
),
method_sep: mergeTokens(
// done
addEOL(
['whitespace', 'url.protocol_host', 'url.slash'],
/(\s+)(https?:\/\/[^?\/,]+)(\/)/,
'start',
'url'
),
// done
addEOL(['whitespace', 'variable.template'], /(\s+)(\${\w+})/, 'start', 'url'),
// done
addEOL(['whitespace', 'url.protocol_host'], /(\s+)(https?:\/\/[^?\/,]+)/, 'start', 'url'),
// done
addEOL(['whitespace', 'url.slash'], /(\s+)(\/)/, 'start', 'url'),
// done
addEOL(['whitespace'], /(\s+)/, 'start', 'url')
),
url: mergeTokens(
// done
addEOL(['variable.template'], /(\${\w+})/, 'start'),
// TODO
addEOL(['url.part'], /(_sql)/, 'start-sql', 'url-sql'),
// done
addEOL(['url.part'], /([^?\/,\s]+)/, 'start'),
// done
addEOL(['url.comma'], /(,)/, 'start'),
// done
addEOL(['url.slash'], /(\/)/, 'start'),
// done
addEOL(['url.questionmark'], /(\?)/, 'start', 'urlParams'),
// done
addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start')
),
urlParams: mergeTokens(
// done
addEOL(['url.param', 'url.equal', 'variable.template'], /([^&=]+)(=)(\${\w+})/, 'start'),
// done
addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start'),
// done
addEOL(['url.param'], /([^&=]+)/, 'start'),
// done
addEOL(['url.amp'], /(&)/, 'start'),
// done
addEOL(['whitespace', 'comment.punctuation', 'comment.line'], /(\s+)(\/\/)(.*$)/, 'start')
),
// TODO
'url-sql': mergeTokens(
addEOL(['url.part'], /([^?\/,\s]+)/, 'start-sql'),
addEOL(['url.comma'], /(,)/, 'start-sql'),
addEOL(['url.slash'], /(\/)/, 'start-sql'),
addEOL(['url.questionmark'], /(\?)/, 'start-sql', 'urlParams-sql')
),
// TODO
'urlParams-sql': mergeTokens(
addEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'start-sql'),
addEOL(['url.param'], /([^&=]+)/, 'start-sql'),
addEOL(['url.amp'], /(&)/, 'start-sql')
),
/**
* Each key in this.$rules considered to be a state in state machine. Regular expressions define the tokens for the current state, as well as the transitions into another state.
* See for more details https://cloud9-sdk.readme.io/docs/highlighting-rules#section-defining-states
* *
* Define a state for comments, these comment rules then can be included in other states. E.g. in 'start' and 'json' states by including { include: 'comments' }
* This will avoid duplicating the same rules in other states
*/
comments: [
{
// Capture a line comment, indicated by #
// done
token: ['comment.punctuation', 'comment.line'],
regex: /(#)(.*$)/,
},
{
// Begin capturing a block comment, indicated by /*
// done
token: 'comment.punctuation',
regex: /\/\*/,
push: [
{
// Finish capturing a block comment, indicated by */
// done
token: 'comment.punctuation',
regex: /\*\//,
next: 'pop',
},
{
// done
defaultToken: 'comment.block',
},
],
},
{
// Capture a line comment, indicated by //
// done
token: ['comment.punctuation', 'comment.line'],
regex: /(\/\/)(.*$)/,
},
],
};
addXJsonToRules(this, 'json');
// Add comment rules to json rule set
this.$rules.json.unshift({ include: 'comments' });
this.$rules.json.unshift({ token: 'variable.template', regex: /("\${\w+}")/ });
if (this instanceof InputHighlightRules) {
this.normalizeRules();
}
}
}

View file

@ -1,37 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import { OutputJsonHighlightRules } from './output_highlight_rules';
const JSONMode = ace.acequire('ace/mode/json').Mode;
const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
ace.acequire('ace/worker/worker_client');
const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer;
export class Mode extends JSONMode {
constructor() {
super();
this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules());
this.$outdent = new MatchingBraceOutdent();
this.$behaviour = new CstyleBehaviour();
this.foldingRules = new CStyleFoldMode();
}
}
(function (this: Mode) {
this.createWorker = function () {
return null;
};
this.$id = 'sense/mode/input';
}).call(Mode.prototype);

View file

@ -1,56 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { mapStatusCodeToBadge } from './output_highlight_rules';
describe('mapStatusCodeToBadge', () => {
const testCases = [
{
description: 'treats 100 as as default',
value: '# PUT test-index 100 Continue',
badge: 'badge.badge--default',
},
{
description: 'treats 200 as success',
value: '# PUT test-index 200 OK',
badge: 'badge.badge--success',
},
{
description: 'treats 301 as primary',
value: '# PUT test-index 301 Moved Permanently',
badge: 'badge.badge--primary',
},
{
description: 'treats 400 as warning',
value: '# PUT test-index 404 Not Found',
badge: 'badge.badge--warning',
},
{
description: 'treats 502 as danger',
value: '# PUT test-index 502 Bad Gateway',
badge: 'badge.badge--danger',
},
{
description: 'treats unexpected numbers as danger',
value: '# PUT test-index 666 Demonic Invasion',
badge: 'badge.badge--danger',
},
{
description: 'treats no numbers as undefined',
value: '# PUT test-index',
badge: undefined,
},
];
testCases.forEach(({ description, value, badge }) => {
test(description, () => {
expect(mapStatusCodeToBadge(value)).toBe(badge);
});
});
});

View file

@ -1,64 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import 'brace/mode/json';
import { addXJsonToRules } from '@kbn/ace';
const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules;
export const mapStatusCodeToBadge = (value?: string) => {
const regExpMatchArray = value?.match(/\d+/);
if (regExpMatchArray) {
const status = parseInt(regExpMatchArray[0], 10);
if (status <= 199) {
return 'badge.badge--default';
}
if (status <= 299) {
return 'badge.badge--success';
}
if (status <= 399) {
return 'badge.badge--primary';
}
if (status <= 499) {
return 'badge.badge--warning';
}
return 'badge.badge--danger';
}
};
export class OutputJsonHighlightRules extends JsonHighlightRules {
constructor() {
super();
this.$rules = {};
addXJsonToRules(this, 'start');
this.$rules.start.unshift(
{
token: 'warning',
regex: '#!.*$',
},
{
token: 'comment',
// match a comment starting with a hash at the start of the line
// ignore status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK)
regex: /#(.*?)(?=[1-5][0-9][0-9]\s(?:[\sA-Za-z]+)|(?:[1-5][0-9][0-9])|$)/,
},
{
token: mapStatusCodeToBadge,
// match status codes and status texts at the end of the line (e.g. # GET _search/foo 200, # GET _search/foo 200 OK)
// this rule allows us to highlight them with the corresponding badge color (e.g. 200 OK -> badge.badge--success)
regex: /([1-5][0-9][0-9]\s?[\sA-Za-z]+$)/,
}
);
if (this instanceof OutputJsonHighlightRules) {
this.normalizeRules();
}
}
}

View file

@ -1,48 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
import { ScriptHighlightRules } from '@kbn/ace';
const TextMode = ace.acequire('ace/mode/text').Mode;
const MatchingBraceOutdent = ace.acequire('ace/mode/matching_brace_outdent').MatchingBraceOutdent;
const CstyleBehaviour = ace.acequire('ace/mode/behaviour/cstyle').CstyleBehaviour;
const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode;
ace.acequire('ace/tokenizer');
export class ScriptMode extends TextMode {
constructor() {
super();
this.$outdent = new MatchingBraceOutdent();
this.$behaviour = new CstyleBehaviour();
this.foldingRules = new CStyleFoldMode();
}
}
(function (this: ScriptMode) {
this.HighlightRules = ScriptHighlightRules;
this.getNextLineIndent = function (state: unknown, line: string, tab: string) {
let indent = this.$getIndent(line);
const match = line.match(/^.*[\{\[]\s*$/);
if (match) {
indent += tab;
}
return indent;
};
this.checkOutdent = function (state: unknown, line: string, input: string) {
return this.$outdent.checkOutdent(line, input);
};
this.autoOutdent = function (state: unknown, doc: string, row: string) {
this.$outdent.autoOutdent(doc, row);
};
}).call(ScriptMode.prototype);

View file

@ -1,10 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export declare const workerModule: { id: string; src: string };

View file

@ -1,15 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import src from '!!raw-loader!./worker';
export const workerModule = {
id: 'sense_editor/mode/worker',
src,
};

View file

@ -1,91 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './legacy_core_editor.test.mocks';
import $ from 'jquery';
import RowParser from '../../../lib/row_parser';
import ace from 'brace';
import { createReadOnlyAceEditor } from './create_readonly';
let output;
const tokenIterator = ace.acequire('ace/token_iterator');
describe('Output Tokenization', () => {
beforeEach(() => {
output = createReadOnlyAceEditor(document.querySelector('#ConAppOutput'));
$(output.container).show();
});
afterEach(() => {
$(output.container).hide();
});
function tokensAsList() {
const iter = new tokenIterator.TokenIterator(output.getSession(), 0, 0);
const ret = [];
let t = iter.getCurrentToken();
const parser = new RowParser(output);
if (parser.isEmptyToken(t)) {
t = parser.nextNonEmptyToken(iter);
}
while (t) {
ret.push({ value: t.value, type: t.type });
t = parser.nextNonEmptyToken(iter);
}
return ret;
}
let testCount = 0;
function tokenTest(tokenList, data) {
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 3);
}
test('Token test ' + testCount++, function (done) {
output.update(data, function () {
const tokens = tokensAsList();
const normTokenList = [];
for (let i = 0; i < tokenList.length; i++) {
normTokenList.push({ type: tokenList[i++], value: tokenList[i] });
}
expect(tokens).toEqual(normTokenList);
done();
});
});
}
tokenTest(
['warning', '#! warning', 'comment', '# GET url', 'paren.lparen', '{', 'paren.rparen', '}'],
'#! warning\n' + '# GET url\n' + '{}'
);
tokenTest(
[
'comment',
'# GET url',
'paren.lparen',
'{',
'variable',
'"f"',
'punctuation.colon',
':',
'punctuation.start_triple_quote',
'"""',
'multi_string',
'raw',
'punctuation.end_triple_quote',
'"""',
'paren.rparen',
'}',
],
'# GET url\n' + '{ "f": """raw""" }'
);
});

View file

@ -1,27 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { get, throttle } from 'lodash';
import type { Editor } from 'brace';
// eslint-disable-next-line import/no-default-export
export default function (editor: Editor) {
const resize = editor.resize;
const throttledResize = throttle(() => {
resize.call(editor, false);
// Keep current top line in view when resizing to avoid losing user context
const userRow = get(throttledResize, 'topRow', 0);
if (userRow !== 0) {
editor.renderer.scrollToLine(userRow, false, false, () => {});
}
}, 35);
return throttledResize;
}

View file

@ -1,123 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ace from 'brace';
ace.define('ace/theme/sense-dark', ['require', 'exports', 'module'], function (require, exports) {
exports.isDark = true;
exports.cssClass = 'ace-sense-dark';
exports.cssText =
'.ace-sense-dark .ace_gutter {\
background: #2e3236;\
color: #bbbfc2;\
}\
.ace-sense-dark .ace_print-margin {\
width: 1px;\
background: #555651\
}\
.ace-sense-dark .ace_scroller {\
background-color: #202328;\
}\
.ace-sense-dark .ace_content {\
}\
.ace-sense-dark .ace_text-layer {\
color: #F8F8F2\
}\
.ace-sense-dark .ace_cursor {\
border-left: 2px solid #F8F8F0\
}\
.ace-sense-dark .ace_overwrite-cursors .ace_cursor {\
border-left: 0px;\
border-bottom: 1px solid #F8F8F0\
}\
.ace-sense-dark .ace_marker-layer .ace_selection {\
background: #222\
}\
.ace-sense-dark.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #272822;\
border-radius: 2px\
}\
.ace-sense-dark .ace_marker-layer .ace_step {\
background: rgb(102, 82, 0)\
}\
.ace-sense-dark .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #49483E\
}\
.ace-sense-dark .ace_marker-layer .ace_active-line {\
background: #202020\
}\
.ace-sense-dark .ace_gutter-active-line {\
background-color: #272727\
}\
.ace-sense-dark .ace_marker-layer .ace_selected-word {\
border: 1px solid #49483E\
}\
.ace-sense-dark .ace_invisible {\
color: #49483E\
}\
.ace-sense-dark .ace_entity.ace_name.ace_tag,\
.ace-sense-dark .ace_keyword,\
.ace-sense-dark .ace_meta,\
.ace-sense-dark .ace_storage {\
color: #F92672\
}\
.ace-sense-dark .ace_constant.ace_character,\
.ace-sense-dark .ace_constant.ace_language,\
.ace-sense-dark .ace_constant.ace_numeric,\
.ace-sense-dark .ace_constant.ace_other {\
color: #AE81FF\
}\
.ace-sense-dark .ace_invalid {\
color: #F8F8F0;\
background-color: #F92672\
}\
.ace-sense-dark .ace_invalid.ace_deprecated {\
color: #F8F8F0;\
background-color: #AE81FF\
}\
.ace-sense-dark .ace_support.ace_constant,\
.ace-sense-dark .ace_support.ace_function {\
color: #66D9EF\
}\
.ace-sense-dark .ace_fold {\
background-color: #A6E22E;\
border-color: #F8F8F2\
}\
.ace-sense-dark .ace_storage.ace_type,\
.ace-sense-dark .ace_support.ace_class,\
.ace-sense-dark .ace_support.ace_type {\
font-style: italic;\
color: #66D9EF\
}\
.ace-sense-dark .ace_entity.ace_name.ace_function,\
.ace-sense-dark .ace_entity.ace_other.ace_attribute-name,\
.ace-sense-dark .ace_variable {\
color: #A6E22E\
}\
.ace-sense-dark .ace_variable.ace_parameter {\
font-style: italic;\
color: #FD971F\
}\
.ace-sense-dark .ace_string {\
color: #E6DB74\
}\
.ace-sense-dark .ace_comment {\
color: #629755\
}\
.ace-sense-dark .ace_markup.ace_underline {\
text-decoration: underline\
}\
.ace-sense-dark .ace_indent-guide {\
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ11D6z7Bq1ar/ABCKBG6g04U2AAAAAElFTkSuQmCC) right repeat-y\
}';
const dom = require('ace/lib/dom');
dom.importCssString(exports.cssText, exports.cssClass);
});

View file

@ -1,37 +0,0 @@
GET _search
{
"query": { "match_all": {} }
}
#preceeding comment
GET _stats?level=shards
#in between comment
PUT index_1/type1/1
{
"f": 1
}
PUT index_1/type1/2
{
"f": 2
}
# comment
GET index_1/type1/1/_source?_source_include=f
DELETE index_2
POST /_sql?format=txt
{
"query": "SELECT prenom FROM claude_index WHERE prenom = 'claude' ",
"fetch_size": 1
}
GET <index_1-{now/d-2d}>,<index_1-{now/d-1d}>,<index_1-{now/d}>/_search?pretty
GET kbn:/api/spaces/space

View file

@ -1,22 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SenseEditor } from './sense_editor';
import * as core from '../legacy_core_editor';
export function create(element: HTMLElement) {
const coreEditor = core.create(element);
const senseEditor = new SenseEditor(coreEditor);
/**
* Init the editor
*/
senseEditor.highlightCurrentRequestsAndUpdateActionBar();
return senseEditor;
}

View file

@ -1,194 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
function detectCURLinLine(line: string) {
// returns true if text matches a curl request
return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/);
}
export function detectCURL(text: string) {
// returns true if text matches a curl request
if (!text) return false;
for (const line of text.split('\n')) {
if (detectCURLinLine(line)) {
return true;
}
}
return false;
}
export function parseCURL(text: string) {
let state = 'NONE';
const out = [];
let body: string[] = [];
let line = '';
const lines = text.trim().split('\n');
let matches;
const EmptyLine = /^\s*$/;
const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/;
const ExecutionComment = /^\s*#!/;
const ClosingSingleQuote = /^([^']*)'/;
const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/;
const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
const LooksLikeCurl = /^\s*curl\s+/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
const HasProtocol = /[\s"']https?:\/\//;
const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
const CurlData = /^.+\s(--data|-d)\s*/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
if (lines.length > 0 && ExecutionComment.test(lines[0])) {
lines.shift();
}
function nextLine() {
if (line.length > 0) {
return true;
}
if (lines.length === 0) {
return false;
}
line = lines.shift()!.replace(/[\r\n]+/g, '\n') + '\n';
return true;
}
function unescapeLastBodyEl() {
const str = body.pop()!.replace(/\\([\\"'])/g, '$1');
body.push(str);
}
// Is the next char a single or double quote?
// If so remove it
function detectQuote() {
if (line.substr(0, 1) === "'") {
line = line.substr(1);
state = 'SINGLE_QUOTE';
} else if (line.substr(0, 1) === '"') {
line = line.substr(1);
state = 'DOUBLE_QUOTE';
} else {
state = 'UNQUOTED';
}
}
// Body is finished - append to output with final LF
function addBodyToOut() {
if (body.length > 0) {
out.push(body.join(''));
body = [];
}
state = 'LF';
out.push('\n');
}
// If the pattern matches, then the state is about to change,
// so add the capture to the body and detect the next state
// Otherwise add the whole line
function consumeMatching(pattern: string | RegExp) {
const result = line.match(pattern);
if (result) {
body.push(result[1]);
line = line.substr(result[0].length);
detectQuote();
} else {
body.push(line);
line = '';
}
}
function parseCurlLine() {
let verb = 'GET';
let request = '';
let result;
if ((result = line.match(CurlVerb))) {
verb = result[1];
}
// JS regexen don't support possessive quantifiers, so
// we need two distinct patterns
const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto;
if ((result = line.match(pattern))) {
request = result[1];
}
out.push(verb + ' /' + request + '\n');
if ((result = line.match(CurlData))) {
line = line.substr(result[0].length);
detectQuote();
if (EmptyLine.test(line)) {
line = '';
}
} else {
state = 'NONE';
line = '';
out.push('');
}
}
while (nextLine()) {
if (state === 'SINGLE_QUOTE') {
consumeMatching(ClosingSingleQuote);
} else if (state === 'DOUBLE_QUOTE') {
consumeMatching(ClosingDoubleQuote);
unescapeLastBodyEl();
} else if (state === 'UNQUOTED') {
consumeMatching(EscapedQuotes);
if (body.length) {
unescapeLastBodyEl();
}
if (state === 'UNQUOTED') {
addBodyToOut();
line = '';
}
}
// the BODY state (used to match the body of a Sense request)
// can be terminated early if it encounters
// a comment or an empty line
else if (state === 'BODY') {
if (Comment.test(line) || EmptyLine.test(line)) {
addBodyToOut();
} else {
body.push(line);
line = '';
}
} else if (EmptyLine.test(line)) {
if (state !== 'LF') {
out.push('\n');
state = 'LF';
}
line = '';
} else if ((matches = line.match(Comment))) {
out.push('#' + matches[1] + '\n');
state = 'NONE';
line = '';
} else if (LooksLikeCurl.test(line)) {
parseCurlLine();
} else if ((matches = line.match(SenseLine))) {
out.push(matches[1] + ' /' + matches[2] + '\n');
line = '';
state = 'BODY';
}
// Nothing else matches, so output with a prefix of ### for debugging purposes
else {
out.push('### ' + line);
line = '';
}
}
addBodyToOut();
return out.join('').trim();
}

View file

@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './create';
export * from '../legacy_core_editor/create_readonly';
export { MODE } from '../../../lib/row_parser';
export { SenseEditor } from './sense_editor';
export { getEndpointFromPosition } from '../../../lib/autocomplete/get_endpoint_from_position';

View file

@ -1,641 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './sense_editor.test.mocks';
import $ from 'jquery';
import _ from 'lodash';
import { URL } from 'url';
import { create } from './create';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import editorInput1 from './__fixtures__/editor_input1.txt';
import { setStorage, createStorage } from '../../../services';
const { collapseLiteralStrings } = XJson;
describe('Editor', () => {
let input;
let oldUrl;
let olldWindow;
let storage;
beforeEach(function () {
// Set up our document body
document.body.innerHTML = `<div>
<div id="ConAppEditor" />
<div id="ConAppEditorActions" />
<div id="ConCopyAsCurl" />
</div>`;
input = create(document.querySelector('#ConAppEditor'));
$(input.getCoreEditor().getContainer()).show();
input.autocomplete._test.removeChangeListener();
oldUrl = global.URL;
olldWindow = { ...global.window };
global.URL = URL;
Object.defineProperty(global, 'window', {
value: Object.create(window),
writable: true,
});
Object.defineProperty(window, 'location', {
value: {
origin: 'http://localhost:5620',
},
});
storage = createStorage({
engine: global.window.localStorage,
prefix: 'console_test',
});
setStorage(storage);
});
afterEach(function () {
global.URL = oldUrl;
global.window = olldWindow;
$(input.getCoreEditor().getContainer()).hide();
input.autocomplete._test.addChangeListener();
setStorage(null);
});
let testCount = 0;
const callWithEditorMethod = (editorMethod, fn) => async (done) => {
const results = await input[editorMethod]();
fn(results, done);
};
function utilsTest(name, prefix, data, testToRun) {
const id = testCount++;
if (typeof data === 'function') {
testToRun = data;
data = null;
}
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 3);
}
if (data) {
if (prefix) {
data = prefix + '\n' + data;
}
} else {
data = prefix;
}
test('Utils test ' + id + ' : ' + name, function (done) {
input.update(data, true).then(() => {
testToRun(done);
});
});
}
function compareRequest(requests, expected) {
if (!Array.isArray(requests)) {
requests = [requests];
expected = [expected];
}
_.each(requests, function (r) {
delete r.range;
});
expect(requests).toEqual(expected);
}
const simpleRequest = {
prefix: 'POST _search',
data: ['{', ' "query": { "match_all": {} }', '}'].join('\n'),
};
const singleLineRequest = {
prefix: 'POST _search',
data: '{ "query": { "match_all": {} } }',
};
const getRequestNoData = {
prefix: 'GET _stats',
};
const multiDocRequest = {
prefix: 'POST _bulk',
data_as_array: ['{ "index": { "_index": "index", "_type":"type" } }', '{ "field": 1 }'],
};
multiDocRequest.data = multiDocRequest.data_as_array.join('\n');
utilsTest(
'simple request range',
simpleRequest.prefix,
simpleRequest.data,
callWithEditorMethod('getRequestRange', (range, done) => {
compareRequest(range, {
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 4, column: 2 },
});
done();
})
);
utilsTest(
'simple request data',
simpleRequest.prefix,
simpleRequest.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: '_search',
data: [simpleRequest.data],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'simple request range, prefixed with spaces',
' ' + simpleRequest.prefix,
simpleRequest.data,
callWithEditorMethod('getRequestRange', (range, done) => {
expect(range).toEqual({
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 4, column: 2 },
});
done();
})
);
utilsTest(
'simple request data, prefixed with spaces',
' ' + simpleRequest.prefix,
simpleRequest.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: '_search',
data: [simpleRequest.data],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'simple request range, suffixed with spaces',
simpleRequest.prefix + ' ',
simpleRequest.data + ' ',
callWithEditorMethod('getRequestRange', (range, done) => {
compareRequest(range, {
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 4, column: 2 },
});
done();
})
);
utilsTest(
'simple request data, suffixed with spaces',
simpleRequest.prefix + ' ',
simpleRequest.data + ' ',
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: '_search',
data: [simpleRequest.data],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'single line request range',
singleLineRequest.prefix,
singleLineRequest.data,
callWithEditorMethod('getRequestRange', (range, done) => {
compareRequest(range, {
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 2, column: 33 },
});
done();
})
);
utilsTest(
'full url: single line request data',
'POST https://somehost/_search',
singleLineRequest.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: 'https://somehost/_search',
data: [singleLineRequest.data],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'request with no data followed by a new line',
getRequestNoData.prefix,
'\n',
callWithEditorMethod('getRequestRange', (range, done) => {
compareRequest(range, {
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 1, column: 11 },
});
done();
})
);
utilsTest(
'request with no data followed by a new line (data)',
getRequestNoData.prefix,
'\n',
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'GET',
url: '_stats',
data: [],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'request with no data',
getRequestNoData.prefix,
getRequestNoData.data,
callWithEditorMethod('getRequestRange', (range, done) => {
expect(range).toEqual({
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 1, column: 11 },
});
done();
})
);
utilsTest(
'request with no data (data)',
getRequestNoData.prefix,
getRequestNoData.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'GET',
url: '_stats',
data: [],
};
compareRequest(request, expected);
done();
})
);
utilsTest(
'multi doc request range',
multiDocRequest.prefix,
multiDocRequest.data,
callWithEditorMethod('getRequestRange', (range, done) => {
expect(range).toEqual({
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 3, column: 15 },
});
done();
})
);
utilsTest(
'multi doc request data',
multiDocRequest.prefix,
multiDocRequest.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: '_bulk',
data: multiDocRequest.data_as_array,
};
compareRequest(request, expected);
done();
})
);
const scriptRequest = {
prefix: 'POST _search',
data: ['{', ' "query": { "script": """', ' some script ', ' """}', '}'].join('\n'),
};
utilsTest(
'script request range',
scriptRequest.prefix,
scriptRequest.data,
callWithEditorMethod('getRequestRange', (range, done) => {
compareRequest(range, {
start: { lineNumber: 1, column: 1 },
end: { lineNumber: 6, column: 2 },
});
done();
})
);
utilsTest(
'simple request data',
simpleRequest.prefix,
simpleRequest.data,
callWithEditorMethod('getRequest', (request, done) => {
const expected = {
method: 'POST',
url: '_search',
data: [collapseLiteralStrings(simpleRequest.data)],
};
compareRequest(request, expected);
done();
})
);
function multiReqTest(name, editorInput, range, expected) {
utilsTest('multi request select - ' + name, editorInput, async function (done) {
const requests = await input.getRequestsInRange(range, false);
// convert to format returned by request.
_.each(expected, function (req) {
req.data = req.data == null ? [] : [JSON.stringify(req.data, null, 2)];
});
compareRequest(requests, expected);
done();
});
}
multiReqTest(
'mid body to mid body',
editorInput1,
{ start: { lineNumber: 13 }, end: { lineNumber: 18 } },
[
{
method: 'PUT',
url: 'index_1/type1/1',
data: {
f: 1,
},
},
{
method: 'PUT',
url: 'index_1/type1/2',
data: {
f: 2,
},
},
]
);
multiReqTest(
'single request start to end',
editorInput1,
{ start: { lineNumber: 11 }, end: { lineNumber: 14 } },
[
{
method: 'PUT',
url: 'index_1/type1/1',
data: {
f: 1,
},
},
]
);
multiReqTest(
'start to end, with comment',
editorInput1,
{ start: { lineNumber: 7 }, end: { lineNumber: 14 } },
[
{
method: 'GET',
url: '_stats?level=shards',
data: null,
},
{
method: 'PUT',
url: 'index_1/type1/1',
data: {
f: 1,
},
},
]
);
multiReqTest(
'before start to after end, with comments',
editorInput1,
{ start: { lineNumber: 5 }, end: { lineNumber: 15 } },
[
{
method: 'GET',
url: '_stats?level=shards',
data: null,
},
{
method: 'PUT',
url: 'index_1/type1/1',
data: {
f: 1,
},
},
]
);
multiReqTest(
'between requests',
editorInput1,
{ start: { lineNumber: 22 }, end: { lineNumber: 23 } },
[]
);
multiReqTest(
'between requests - with comment',
editorInput1,
{ start: { lineNumber: 21 }, end: { lineNumber: 23 } },
[]
);
multiReqTest(
'between requests - before comment',
editorInput1,
{ start: { lineNumber: 20 }, end: { lineNumber: 23 } },
[]
);
function multiReqCopyAsCurlTest(name, editorInput, range, expected) {
utilsTest('multi request copy as curl - ' + name, editorInput, async function (done) {
const curl = await input.getRequestsAsCURL('http://localhost:9200', range);
expect(curl).toEqual(expected);
done();
});
}
multiReqCopyAsCurlTest(
'start to end, with comment',
editorInput1,
{ start: { lineNumber: 7 }, end: { lineNumber: 14 } },
`
curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting"
#in between comment
curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d'
{
"f": 1
}'`.trim()
);
multiReqCopyAsCurlTest(
'with single quotes',
editorInput1,
{ start: { lineNumber: 29 }, end: { lineNumber: 33 } },
`
curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d'
{
"query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ",
"fetch_size": 1
}'`.trim()
);
multiReqCopyAsCurlTest(
'with date math index',
editorInput1,
{ start: { lineNumber: 35 }, end: { lineNumber: 35 } },
`
curl -XGET "http://localhost:9200/%3Cindex_1-%7Bnow%2Fd-2d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd-1d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd%7D%3E%2F_search?pretty" -H "kbn-xsrf: reporting"`.trim()
);
multiReqCopyAsCurlTest(
'with Kibana API request',
editorInput1,
{ start: { lineNumber: 37 }, end: { lineNumber: 37 } },
`
curl -XGET "http://localhost:5620/api/spaces/space" -H \"kbn-xsrf: reporting\"`.trim()
);
describe('getRequestsAsCURL', () => {
it('should return empty string if no requests', async () => {
input?.getCoreEditor().setValue('', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 1 },
});
expect(curl).toEqual('');
});
it('should replace variables in the URL', async () => {
storage.set('variables', [{ name: 'exampleVariableA', value: 'valueA' }]);
input?.getCoreEditor().setValue('GET ${exampleVariableA}', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 1 },
});
expect(curl).toContain('valueA');
});
it('should replace variables in the body', async () => {
storage.set('variables', [{ name: 'exampleVariableB', value: 'valueB' }]);
console.log(storage.get('variables'));
input
?.getCoreEditor()
.setValue('GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableB}": ""\n\t}\n}', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 6 },
});
expect(curl).toContain('valueB');
});
it('should strip comments in the URL', async () => {
input?.getCoreEditor().setValue('GET _search // comment', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 6 },
});
expect(curl).not.toContain('comment');
});
it('should strip comments in the body', async () => {
input
?.getCoreEditor()
.setValue('{\n\t"query": {\n\t\t"match_all": {} // comment \n\t}\n}', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 8 },
});
console.log('curl', curl);
expect(curl).not.toContain('comment');
});
it('should strip multi-line comments in the body', async () => {
input
?.getCoreEditor()
.setValue('{\n\t"query": {\n\t\t"match_all": {} /* comment */\n\t}\n}', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 8 },
});
console.log('curl', curl);
expect(curl).not.toContain('comment');
});
it('should replace multiple variables in the URL', async () => {
storage.set('variables', [
{ name: 'exampleVariableA', value: 'valueA' },
{ name: 'exampleVariableB', value: 'valueB' },
]);
input?.getCoreEditor().setValue('GET ${exampleVariableA}/${exampleVariableB}', false);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 1 },
});
expect(curl).toContain('valueA');
expect(curl).toContain('valueB');
});
it('should replace multiple variables in the body', async () => {
storage.set('variables', [
{ name: 'exampleVariableA', value: 'valueA' },
{ name: 'exampleVariableB', value: 'valueB' },
]);
input
?.getCoreEditor()
.setValue(
'GET _search\n{\t\t"query": {\n\t\t\t"${exampleVariableA}": "${exampleVariableB}"\n\t}\n}',
false
);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 6 },
});
expect(curl).toContain('valueA');
expect(curl).toContain('valueB');
});
it('should replace variables in bulk request', async () => {
storage.set('variables', [
{ name: 'exampleVariableA', value: 'valueA' },
{ name: 'exampleVariableB', value: 'valueB' },
]);
input
?.getCoreEditor()
.setValue(
'POST _bulk\n{"index": {"_id": "0"}}\n{"field" : "${exampleVariableA}"}\n{"index": {"_id": "1"}}\n{"field" : "${exampleVariableB}"}\n',
false
);
const curl = await input.getRequestsAsCURL('http://localhost:9200', {
start: { lineNumber: 1 },
end: { lineNumber: 4 },
});
expect(curl).toContain('valueA');
expect(curl).toContain('valueB');
});
});
});

View file

@ -1,20 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
/* eslint no-undef: 0 */
import '../legacy_core_editor/legacy_core_editor.test.mocks';
import jQuery from 'jquery';
jest.spyOn(jQuery, 'ajax').mockImplementation(
() =>
new Promise(() => {
// never resolve
}) as any
);

View file

@ -1,534 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import _ from 'lodash';
import { parse } from 'hjson';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import RowParser from '../../../lib/row_parser';
import * as utils from '../../../lib/utils';
import { constructUrl } from '../../../lib/es/es';
import { CoreEditor, Position, Range } from '../../../types';
import { createTokenIterator } from '../../factories';
import createAutocompleter from '../../../lib/autocomplete/autocomplete';
import { getStorage, StorageKeys } from '../../../services';
import { DEFAULT_VARIABLES } from '../../../../common/constants';
const { collapseLiteralStrings } = XJson;
export class SenseEditor {
currentReqRange: (Range & { markerRef: unknown }) | null;
parser: RowParser;
private readonly autocomplete: ReturnType<typeof createAutocompleter>;
constructor(private readonly coreEditor: CoreEditor) {
this.currentReqRange = null;
this.parser = new RowParser(this.coreEditor);
this.autocomplete = createAutocompleter({
coreEditor,
parser: this.parser,
});
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
this.coreEditor.on(
'tokenizerUpdate',
this.highlightCurrentRequestsAndUpdateActionBar.bind(this)
);
this.coreEditor.on('changeCursor', this.highlightCurrentRequestsAndUpdateActionBar.bind(this));
this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this));
}
prevRequestStart = (rowOrPos?: number | Position): Position => {
let curRow: number;
if (rowOrPos == null) {
curRow = this.coreEditor.getCurrentPosition().lineNumber;
} else if (_.isObject(rowOrPos)) {
curRow = (rowOrPos as Position).lineNumber;
} else {
curRow = rowOrPos as number;
}
while (curRow > 0 && !this.parser.isStartRequestRow(curRow, this.coreEditor)) curRow--;
return {
lineNumber: curRow,
column: 1,
};
};
nextRequestStart = (rowOrPos?: number | Position) => {
let curRow: number;
if (rowOrPos == null) {
curRow = this.coreEditor.getCurrentPosition().lineNumber;
} else if (_.isObject(rowOrPos)) {
curRow = (rowOrPos as Position).lineNumber;
} else {
curRow = rowOrPos as number;
}
const maxLines = this.coreEditor.getLineCount();
for (; curRow < maxLines - 1; curRow++) {
if (this.parser.isStartRequestRow(curRow, this.coreEditor)) {
break;
}
}
return {
row: curRow,
column: 0,
};
};
autoIndent = _.debounce(async () => {
await this.coreEditor.waitForLatestTokens();
const reqRange = await this.getRequestRange();
if (!reqRange) {
return;
}
const parsedReq = await this.getRequest();
if (!parsedReq) {
return;
}
if (parsedReq.data.some((doc) => utils.hasComments(doc))) {
/**
* Comments require different approach for indentation and do not have condensed format
* We need to delegate indentation logic to coreEditor since it has access to session and other methods used for formatting and indenting the comments
*/
this.coreEditor.autoIndent(parsedReq.range);
return;
}
if (parsedReq.data && parsedReq.data.length > 0) {
let indent = parsedReq.data.length === 1; // unindent multi docs by default
let formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent);
if (!formattedData.changed) {
// toggle.
indent = !indent;
formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent);
}
parsedReq.data = formattedData.data;
this.replaceRequestRange(parsedReq, reqRange);
}
}, 25);
update = async (data: string, reTokenizeAll = false) => {
return this.coreEditor.setValue(data, reTokenizeAll);
};
replaceRequestRange = (
newRequest: { method: string; url: string; data: string | string[] },
requestRange: Range
) => {
const text = utils.textFromRequest(newRequest);
if (requestRange) {
this.coreEditor.replaceRange(requestRange, text);
} else {
// just insert where we are
this.coreEditor.insert(this.coreEditor.getCurrentPosition(), text);
}
};
getRequestRange = async (lineNumber?: number): Promise<Range | null> => {
await this.coreEditor.waitForLatestTokens();
if (this.parser.isInBetweenRequestsRow(lineNumber)) {
return null;
}
const reqStart = this.prevRequestStart(lineNumber);
const reqEnd = this.nextRequestEnd(reqStart);
return {
start: {
...reqStart,
},
end: {
...reqEnd,
},
};
};
expandRangeToRequestEdges = async (
range = this.coreEditor.getSelectionRange()
): Promise<Range | null> => {
await this.coreEditor.waitForLatestTokens();
let startLineNumber = range.start.lineNumber;
let endLineNumber = range.end.lineNumber;
const maxLine = Math.max(1, this.coreEditor.getLineCount());
if (this.parser.isInBetweenRequestsRow(startLineNumber)) {
/* Do nothing... */
} else {
for (; startLineNumber >= 1; startLineNumber--) {
if (this.parser.isStartRequestRow(startLineNumber)) {
break;
}
}
}
if (startLineNumber < 1 || startLineNumber > endLineNumber) {
return null;
}
// move end row to the previous request end if between requests, otherwise walk forward
if (this.parser.isInBetweenRequestsRow(endLineNumber)) {
for (; endLineNumber >= startLineNumber; endLineNumber--) {
if (this.parser.isEndRequestRow(endLineNumber)) {
break;
}
}
} else {
for (; endLineNumber <= maxLine; endLineNumber++) {
if (this.parser.isEndRequestRow(endLineNumber)) {
break;
}
}
}
if (endLineNumber < startLineNumber || endLineNumber > maxLine) {
return null;
}
const endColumn =
(this.coreEditor.getLineValue(endLineNumber) || '').replace(/\s+$/, '').length + 1;
return {
start: {
lineNumber: startLineNumber,
column: 1,
},
end: {
lineNumber: endLineNumber,
column: endColumn,
},
};
};
getRequestInRange = async (range?: Range) => {
await this.coreEditor.waitForLatestTokens();
if (!range) {
return null;
}
const request: {
method: string;
data: string[];
url: string;
range: Range;
} = {
method: '',
data: [],
url: '',
range,
};
const pos = range.start;
const tokenIter = createTokenIterator({ editor: this.coreEditor, position: pos });
let t = tokenIter.getCurrentToken();
if (this.parser.isEmptyToken(t)) {
// if the row starts with some spaces, skip them.
t = this.parser.nextNonEmptyToken(tokenIter);
}
if (t == null) {
return null;
}
request.method = t.value;
t = this.parser.nextNonEmptyToken(tokenIter);
if (!t || t.type === 'method') {
return null;
}
request.url = '';
while (t && t.type && (t.type.indexOf('url') === 0 || t.type === 'variable.template')) {
request.url += t.value;
t = tokenIter.stepForward();
}
if (this.parser.isEmptyToken(t)) {
// if the url row ends with some spaces, skip them.
t = this.parser.nextNonEmptyToken(tokenIter);
}
// If the url row ends with a comment, skip it
while (this.parser.isCommentToken(t)) {
t = tokenIter.stepForward();
}
let bodyStartLineNumber = (t ? 0 : 1) + tokenIter.getCurrentPosition().lineNumber; // artificially increase end of docs.
let dataEndPos: Position;
while (
bodyStartLineNumber < range.end.lineNumber ||
(bodyStartLineNumber === range.end.lineNumber && 1 < range.end.column)
) {
dataEndPos = this.nextDataDocEnd({
lineNumber: bodyStartLineNumber,
column: 1,
});
const bodyRange: Range = {
start: {
lineNumber: bodyStartLineNumber,
column: 1,
},
end: dataEndPos,
};
const data = this.coreEditor.getValueInRange(bodyRange)!;
request.data.push(data.trim());
bodyStartLineNumber = dataEndPos.lineNumber + 1;
}
return request;
};
getRequestsInRange = async (
range = this.coreEditor.getSelectionRange(),
includeNonRequestBlocks = false
): Promise<any[]> => {
await this.coreEditor.waitForLatestTokens();
if (!range) {
return [];
}
const expandedRange = await this.expandRangeToRequestEdges(range);
if (!expandedRange) {
return [];
}
const requests: unknown[] = [];
let rangeStartCursor = expandedRange.start.lineNumber;
const endLineNumber = expandedRange.end.lineNumber;
// move to the next request start (during the second iterations this may not be exactly on a request
let currentLineNumber = expandedRange.start.lineNumber;
const flushNonRequestBlock = () => {
if (includeNonRequestBlocks) {
const nonRequestPrefixBlock = this.coreEditor
.getLines(rangeStartCursor, currentLineNumber - 1)
.join('\n');
if (nonRequestPrefixBlock) {
requests.push(nonRequestPrefixBlock);
}
}
};
while (currentLineNumber <= endLineNumber) {
if (this.parser.isStartRequestRow(currentLineNumber)) {
flushNonRequestBlock();
const request = await this.getRequest(currentLineNumber);
if (!request) {
// Something has probably gone wrong.
return requests;
} else {
requests.push(request);
rangeStartCursor = currentLineNumber = request.range.end.lineNumber + 1;
}
} else {
++currentLineNumber;
}
}
flushNonRequestBlock();
return requests;
};
getRequest = async (row?: number) => {
await this.coreEditor.waitForLatestTokens();
if (this.parser.isInBetweenRequestsRow(row)) {
return null;
}
const range = await this.getRequestRange(row);
return this.getRequestInRange(range!);
};
moveToPreviousRequestEdge = async () => {
await this.coreEditor.waitForLatestTokens();
const pos = this.coreEditor.getCurrentPosition();
for (
pos.lineNumber--;
pos.lineNumber > 1 && !this.parser.isRequestEdge(pos.lineNumber);
pos.lineNumber--
) {
// loop for side effects
}
this.coreEditor.moveCursorToPosition({
lineNumber: pos.lineNumber,
column: 1,
});
};
moveToNextRequestEdge = async (moveOnlyIfNotOnEdge: boolean) => {
await this.coreEditor.waitForLatestTokens();
const pos = this.coreEditor.getCurrentPosition();
const maxRow = this.coreEditor.getLineCount();
if (!moveOnlyIfNotOnEdge) {
pos.lineNumber++;
}
for (
;
pos.lineNumber < maxRow && !this.parser.isRequestEdge(pos.lineNumber);
pos.lineNumber++
) {
// loop for side effects
}
this.coreEditor.moveCursorToPosition({
lineNumber: pos.lineNumber,
column: 1,
});
};
nextRequestEnd = (pos: Position): Position => {
pos = pos || this.coreEditor.getCurrentPosition();
const maxLines = this.coreEditor.getLineCount();
let curLineNumber = pos.lineNumber;
for (; curLineNumber <= maxLines; ++curLineNumber) {
const curRowMode = this.parser.getRowParseMode(curLineNumber);
// eslint-disable-next-line no-bitwise
if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) {
break;
}
// eslint-disable-next-line no-bitwise
if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) {
break;
}
}
const column =
(this.coreEditor.getLineValue(curLineNumber) || '').replace(/\s+$/, '').length + 1;
return {
lineNumber: curLineNumber,
column,
};
};
nextDataDocEnd = (pos: Position): Position => {
pos = pos || this.coreEditor.getCurrentPosition();
let curLineNumber = pos.lineNumber;
const maxLines = this.coreEditor.getLineCount();
for (; curLineNumber < maxLines; curLineNumber++) {
const curRowMode = this.parser.getRowParseMode(curLineNumber);
// eslint-disable-next-line no-bitwise
if ((curRowMode & this.parser.MODE.REQUEST_END) > 0) {
break;
}
// eslint-disable-next-line no-bitwise
if ((curRowMode & this.parser.MODE.MULTI_DOC_CUR_DOC_END) > 0) {
break;
}
// eslint-disable-next-line no-bitwise
if (curLineNumber !== pos.lineNumber && (curRowMode & this.parser.MODE.REQUEST_START) > 0) {
break;
}
}
const column =
(this.coreEditor.getLineValue(curLineNumber) || '').length +
1; /* Range goes to 1 after last char */
return {
lineNumber: curLineNumber,
column,
};
};
highlightCurrentRequestsAndUpdateActionBar = _.debounce(async () => {
await this.coreEditor.waitForLatestTokens();
const expandedRange = await this.expandRangeToRequestEdges();
if (expandedRange === null && this.currentReqRange === null) {
return;
}
if (
expandedRange !== null &&
this.currentReqRange !== null &&
expandedRange.start.lineNumber === this.currentReqRange.start.lineNumber &&
expandedRange.end.lineNumber === this.currentReqRange.end.lineNumber
) {
// same request, now see if we are on the first line and update the action bar
const cursorLineNumber = this.coreEditor.getCurrentPosition().lineNumber;
if (cursorLineNumber === this.currentReqRange.start.lineNumber) {
this.updateActionsBar();
}
return; // nothing to do..
}
if (this.currentReqRange) {
this.coreEditor.removeMarker(this.currentReqRange.markerRef);
}
this.currentReqRange = expandedRange as any;
if (this.currentReqRange) {
this.currentReqRange.markerRef = this.coreEditor.addMarker(this.currentReqRange);
}
this.updateActionsBar();
}, 25);
getRequestsAsCURL = async (elasticsearchBaseUrl: string, range?: Range): Promise<string> => {
const variables = getStorage().get(StorageKeys.VARIABLES, DEFAULT_VARIABLES);
let requests = await this.getRequestsInRange(range, true);
requests = utils.replaceVariables(requests, variables);
const result = _.map(requests, (req) => {
if (typeof req === 'string') {
// no request block
return req;
}
const path = req.url;
const method = req.method;
const data = req.data;
// this is the first url defined in elasticsearch.hosts
const url = constructUrl(elasticsearchBaseUrl, path);
// Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections
let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`;
if (data && data.length) {
const joinedData = data.join('\n');
let dataAsString: string;
try {
ret += ` -H "Content-Type: application/json" -d'\n`;
if (utils.hasComments(joinedData)) {
// if there are comments in the data, we need to strip them out
const dataWithoutComments = parse(joinedData);
dataAsString = collapseLiteralStrings(JSON.stringify(dataWithoutComments, null, 2));
} else {
dataAsString = collapseLiteralStrings(joinedData);
}
// We escape single quoted strings that are wrapped in single quoted strings
ret += dataAsString.replace(/'/g, "'\\''");
if (data.length > 1) {
ret += '\n';
} // end with a new line
ret += "'";
} catch (e) {
throw new Error(`Error parsing data: ${e.message}`);
}
}
return ret;
});
return result.join('\n');
};
updateActionsBar = () => {
return this.coreEditor.legacyUpdateUI(this.currentReqRange);
};
getCoreEditor() {
return this.coreEditor;
}
}

View file

@ -12,7 +12,6 @@ import { produce } from 'immer';
import { identity } from 'fp-ts/lib/function';
import { DevToolsSettings, DEFAULT_SETTINGS } from '../../services';
import { TextObject } from '../../../common/text_object';
import { SenseEditor } from '../models';
import { SHELL_TAB_ID } from '../containers/main/constants';
import { MonacoEditorActionsProvider } from '../containers/editor/monaco_editor_actions_provider';
import { RequestToRestore } from '../../types';
@ -39,7 +38,7 @@ export const initialValue: Store = produce<Store>(
);
export type Action =
| { type: 'setInputEditor'; payload: SenseEditor | MonacoEditorActionsProvider }
| { type: 'setInputEditor'; payload: MonacoEditorActionsProvider }
| { type: 'setCurrentTextObject'; payload: TextObject }
| { type: 'updateSettings'; payload: DevToolsSettings }
| { type: 'setCurrentView'; payload: string }

View file

@ -1,10 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './token_provider';

View file

@ -1,223 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import '../../application/models/sense_editor/sense_editor.test.mocks';
import $ from 'jquery';
// TODO:
// We import from application models as a convenient way to bootstrap loading up of an editor using
// this lib. We also need to import application specific mocks which is not ideal.
// In this situation, the token provider lib knows about app models in tests, which it really shouldn't. Should create
// a better sandbox in future.
import { create, SenseEditor } from '../../application/models/sense_editor';
import { Position, Token, TokensProvider } from '../../types';
interface RunTestArgs {
input: string;
done?: () => void;
}
describe('Ace (legacy) token provider', () => {
let senseEditor: SenseEditor;
let tokenProvider: TokensProvider;
beforeEach(() => {
// Set up our document body
document.body.innerHTML = `<div>
<div id="ConAppEditor" />
<div id="ConAppEditorActions" />
<div id="ConCopyAsCurl" />
</div>`;
senseEditor = create(document.querySelector<HTMLElement>('#ConAppEditor')!);
$(senseEditor.getCoreEditor().getContainer())!.show();
(senseEditor as any).autocomplete._test.removeChangeListener();
tokenProvider = senseEditor.getCoreEditor().getTokenProvider();
});
afterEach(async () => {
$(senseEditor.getCoreEditor().getContainer())!.hide();
(senseEditor as any).autocomplete._test.addChangeListener();
await senseEditor.update('', true);
});
describe('#getTokens', () => {
const runTest = ({
input,
expectedTokens,
done,
lineNumber = 1,
}: RunTestArgs & { expectedTokens: Token[] | null; lineNumber?: number }) => {
senseEditor.update(input, true).then(() => {
const tokens = tokenProvider.getTokens(lineNumber);
expect(tokens).toEqual(expectedTokens);
if (done) done();
});
};
describe('base cases', () => {
test('case 1 - only url', (done) => {
runTest({
input: `GET http://somehost/_search`,
expectedTokens: [
{ type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } },
{ type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } },
{
type: 'url.protocol_host',
value: 'http://somehost',
position: { lineNumber: 1, column: 5 },
},
{ type: 'url.slash', value: '/', position: { lineNumber: 1, column: 20 } },
{ type: 'url.part', value: '_search', position: { lineNumber: 1, column: 21 } },
],
done,
});
});
test('case 2 - basic auth in host name', (done) => {
runTest({
input: `GET http://test:user@somehost/`,
expectedTokens: [
{ type: 'method', value: 'GET', position: { lineNumber: 1, column: 1 } },
{ type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 4 } },
{
type: 'url.protocol_host',
value: 'http://test:user@somehost',
position: { lineNumber: 1, column: 5 },
},
{ type: 'url.slash', value: '/', position: { lineNumber: 1, column: 30 } },
],
done,
});
});
test('case 3 - handles empty lines', (done) => {
runTest({
input: `POST abc
{
`,
expectedTokens: [
{ type: 'method', value: 'POST', position: { lineNumber: 1, column: 1 } },
{ type: 'whitespace', value: ' ', position: { lineNumber: 1, column: 5 } },
{ type: 'url.part', value: 'abc', position: { lineNumber: 1, column: 6 } },
],
done,
lineNumber: 1,
});
});
});
describe('with newlines', () => {
test('case 1 - newlines base case', (done) => {
runTest({
input: `GET http://test:user@somehost/
{
"wudup": "!"
}`,
expectedTokens: [
{ type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 1 } },
{ type: 'variable', value: '"wudup"', position: { lineNumber: 3, column: 3 } },
{ type: 'punctuation.colon', value: ':', position: { lineNumber: 3, column: 10 } },
{ type: 'whitespace', value: ' ', position: { lineNumber: 3, column: 11 } },
{ type: 'string', value: '"!"', position: { lineNumber: 3, column: 12 } },
],
done,
lineNumber: 3,
});
});
});
describe('edge cases', () => {
test('case 1 - getting token outside of document', (done) => {
runTest({
input: `GET http://test:user@somehost/
{
"wudup": "!"
}`,
expectedTokens: null,
done,
lineNumber: 100,
});
});
test('case 2 - empty lines', (done) => {
runTest({
input: `GET http://test:user@somehost/
{
"wudup": "!"
}`,
expectedTokens: [],
done,
lineNumber: 5,
});
});
});
});
describe('#getTokenAt', () => {
const runTest = ({
input,
expectedToken,
done,
position,
}: RunTestArgs & { expectedToken: Token | null; position: Position }) => {
senseEditor.update(input, true).then(() => {
const tokens = tokenProvider.getTokenAt(position);
expect(tokens).toEqual(expectedToken);
if (done) done();
});
};
describe('base cases', () => {
it('case 1 - gets a token from the url', (done) => {
const input = `GET http://test:user@somehost/`;
runTest({
input,
expectedToken: {
position: { lineNumber: 1, column: 4 },
type: 'whitespace',
value: ' ',
},
position: { lineNumber: 1, column: 5 },
});
runTest({
input,
expectedToken: {
position: { lineNumber: 1, column: 5 },
type: 'url.protocol_host',
value: 'http://test:user@somehost',
},
position: { lineNumber: 1, column: input.length },
done,
});
});
});
describe('special cases', () => {
it('case 1 - handles input outside of range', (done) => {
runTest({
input: `GET abc`,
expectedToken: null,
done,
position: { lineNumber: 1, column: 99 },
});
});
});
});
});

View file

@ -1,84 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { IEditSession, TokenInfo as BraceTokenInfo } from 'brace';
import { TokensProvider, Token, Position } from '../../types';
// Brace's token information types are not accurate.
interface TokenInfo extends BraceTokenInfo {
type: string;
}
const toToken = (lineNumber: number, column: number, token: TokenInfo): Token => ({
type: token.type,
value: token.value,
position: {
lineNumber,
column,
},
});
const toTokens = (lineNumber: number, tokens: TokenInfo[]): Token[] => {
let acc = '';
return tokens.map((token) => {
const column = acc.length + 1;
acc += token.value;
return toToken(lineNumber, column, token);
});
};
const extractTokenFromAceTokenRow = (
lineNumber: number,
column: number,
aceTokens: TokenInfo[]
) => {
let acc = '';
for (const token of aceTokens) {
const start = acc.length + 1;
acc += token.value;
const end = acc.length;
if (column < start) continue;
if (column > end + 1) continue;
return toToken(lineNumber, start, token);
}
return null;
};
export class AceTokensProvider implements TokensProvider {
constructor(private readonly session: IEditSession) {}
getTokens(lineNumber: number): Token[] | null {
if (lineNumber < 1) return null;
// Important: must use a .session.getLength because this is a cached value.
// Calculating line length here will lead to performance issues because this function
// may be called inside of tight loops.
const lineCount = this.session.getLength();
if (lineNumber > lineCount) {
return null;
}
const tokens = this.session.getTokens(lineNumber - 1) as unknown as TokenInfo[];
if (!tokens || !tokens.length) {
// We are inside of the document but have no tokens for this line. Return an empty
// array to represent this empty line.
return [];
}
return toTokens(lineNumber, tokens);
}
getTokenAt(pos: Position): Token | null {
const tokens = this.session.getTokens(pos.lineNumber - 1) as unknown as TokenInfo[];
if (tokens) {
return extractTokenFromAceTokenRow(pos.lineNumber, pos.column, tokens);
}
return null;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,33 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { CoreEditor, Position } from '../../types';
import { getCurrentMethodAndTokenPaths } from './autocomplete';
import type RowParser from '../row_parser';
import { getTopLevelUrlCompleteComponents } from '../kb/kb';
import { populateContext } from './engine';
export function getEndpointFromPosition(editor: CoreEditor, pos: Position, parser: RowParser) {
const lineValue = editor.getLineValue(pos.lineNumber);
const context = {
...getCurrentMethodAndTokenPaths(
editor,
{
column: lineValue.length + 1 /* Go to the very end of the line */,
lineNumber: pos.lineNumber,
},
parser,
true
),
};
const components = getTopLevelUrlCompleteComponents(context.method);
populateContext(context.urlTokenPath, context, editor, true, components);
return context.endpoint;
}

View file

@ -1,224 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import '../../application/models/sense_editor/sense_editor.test.mocks';
import { looksLikeTypingIn } from './looks_like_typing_in';
import { create } from '../../application/models';
import type { SenseEditor } from '../../application/models';
import type { CoreEditor, Position, Token, TokensProvider } from '../../types';
describe('looksLikeTypingIn', () => {
let editor: SenseEditor;
let coreEditor: CoreEditor;
let tokenProvider: TokensProvider;
beforeEach(() => {
document.body.innerHTML = `<div>
<div id="ConAppEditor" />
<div id="ConAppEditorActions" />
<div id="ConCopyAsCurl" />
</div>`;
editor = create(document.getElementById('ConAppEditor')!);
coreEditor = editor.getCoreEditor();
tokenProvider = coreEditor.getTokenProvider();
});
afterEach(async () => {
await editor.update('', true);
});
describe('general typing in', () => {
interface RunTestArgs {
preamble: string;
autocomplete?: string;
input: string;
}
const runTest = async ({ preamble, autocomplete, input }: RunTestArgs) => {
const pos: Position = { lineNumber: 1, column: 1 };
await editor.update(preamble, true);
pos.column += preamble.length;
const lastEvaluatedToken = tokenProvider.getTokenAt(pos);
if (autocomplete !== undefined) {
await editor.update(coreEditor.getValue() + autocomplete, true);
pos.column += autocomplete.length;
}
await editor.update(coreEditor.getValue() + input, true);
pos.column += input.length;
const currentToken = tokenProvider.getTokenAt(pos);
expect(lastEvaluatedToken).not.toBeNull();
expect(currentToken).not.toBeNull();
expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(true);
};
const cases: RunTestArgs[] = [
{ preamble: 'G', input: 'E' },
{ preamble: 'GET .kibana', input: '/' },
{ preamble: 'GET .kibana', input: ',' },
{ preamble: 'GET .kibana', input: '?' },
{ preamble: 'GET .kibana/', input: '_' },
{ preamble: 'GET .kibana/', input: '?' },
{ preamble: 'GET .kibana,', input: '.' },
{ preamble: 'GET .kibana,', input: '?' },
{ preamble: 'GET .kibana?', input: 'k' },
{ preamble: 'GET .kibana?k', input: '=' },
{ preamble: 'GET .kibana?k=', input: 'v' },
{ preamble: 'GET .kibana?k=v', input: '&' },
{ preamble: 'GET .kibana?k', input: '&' },
{ preamble: 'GET .kibana?k&', input: 'k' },
{ preamble: 'GET ', autocomplete: '.kibana', input: '/' },
{ preamble: 'GET ', autocomplete: '.kibana', input: ',' },
{ preamble: 'GET ', autocomplete: '.kibana', input: '?' },
{ preamble: 'GET .ki', autocomplete: 'bana', input: '/' },
{ preamble: 'GET .ki', autocomplete: 'bana', input: ',' },
{ preamble: 'GET .ki', autocomplete: 'bana', input: '?' },
{ preamble: 'GET _nodes/', autocomplete: 'stats', input: '/' },
{ preamble: 'GET _nodes/sta', autocomplete: 'ts', input: '/' },
{ preamble: 'GET _nodes/', autocomplete: 'jvm', input: ',' },
{ preamble: 'GET _nodes/j', autocomplete: 'vm', input: ',' },
{ preamble: 'GET _nodes/jvm,', autocomplete: 'os', input: ',' },
{ preamble: 'GET .kibana,', autocomplete: '.security', input: ',' },
{ preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: ',' },
{ preamble: 'GET .kibana,', autocomplete: '.security', input: '/' },
{ preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '/' },
{ preamble: 'GET .kibana,', autocomplete: '.security', input: '?' },
{ preamble: 'GET .kibana,.sec', autocomplete: 'urity', input: '?' },
{ preamble: 'GET .kibana/', autocomplete: '_search', input: '?' },
{ preamble: 'GET .kibana/_se', autocomplete: 'arch', input: '?' },
{ preamble: 'GET .kibana/_search?', autocomplete: 'expand_wildcards', input: '=' },
{ preamble: 'GET .kibana/_search?exp', autocomplete: 'and_wildcards', input: '=' },
{ preamble: 'GET .kibana/_search?expand_wildcards=', autocomplete: 'all', input: '&' },
{ preamble: 'GET .kibana/_search?expand_wildcards=a', autocomplete: 'll', input: '&' },
{ preamble: 'GET _cat/indices?s=index&', autocomplete: 'expand_wildcards', input: '=' },
{ preamble: 'GET _cat/indices?s=index&exp', autocomplete: 'and_wildcards', input: '=' },
{ preamble: 'GET _cat/indices?v&', autocomplete: 'expand_wildcards', input: '=' },
{ preamble: 'GET _cat/indices?v&exp', autocomplete: 'and_wildcards', input: '=' },
// autocomplete skips one iteration of token evaluation if user types in every letter
{ preamble: 'GET .kibana', autocomplete: '/', input: '_' }, // token '/' may not be evaluated
{ preamble: 'GET .kibana', autocomplete: ',', input: '.' }, // token ',' may not be evaluated
{ preamble: 'GET .kibana', autocomplete: '?', input: 'k' }, // token '?' may not be evaluated
];
for (const c of cases) {
const name =
c.autocomplete === undefined
? `'${c.preamble}' -> '${c.input}'`
: `'${c.preamble}' -> '${c.autocomplete}' (autocomplte) -> '${c.input}'`;
test(name, async () => runTest(c));
}
});
describe('first typing in', () => {
test(`'' -> 'G'`, () => {
// this is based on an implementation within the evaluateCurrentTokenAfterAChange function
const lastEvaluatedToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' };
lastEvaluatedToken.position.lineNumber = coreEditor.getCurrentPosition().lineNumber;
const currentToken = { position: { column: 1, lineNumber: 1 }, value: 'G', type: 'method' };
expect(looksLikeTypingIn(lastEvaluatedToken, currentToken, coreEditor)).toBe(true);
});
});
const matrices = [
`
GET .kibana/
`
.slice(1, -1)
.split('\n'),
`
POST test/_doc
{"message": "test"}
GET /_cat/indices?v&s=
DE
`
.slice(1, -1)
.split('\n'),
`
PUT test/_doc/1
{"field": "value"}
`
.slice(1, -1)
.split('\n'),
];
describe('navigating the editor via keyboard arrow keys', () => {
const runHorizontalZigzagWalkTest = async (matrix: string[]) => {
const width = matrix[0].length;
const height = matrix.length;
await editor.update(matrix.join('\n'), true);
let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition());
let currentToken: Token | null;
for (let i = 1; i < height * width * 2; i++) {
const pos = {
column: 1 + (i % width),
lineNumber: 1 + Math.floor(i / width),
};
if (pos.lineNumber % 2 === 0) {
pos.column = width - pos.column + 1;
}
if (pos.lineNumber > height) {
pos.lineNumber = 2 * height - pos.lineNumber + 1;
}
currentToken = tokenProvider.getTokenAt(pos);
expect(lastEvaluatedToken).not.toBeNull();
expect(currentToken).not.toBeNull();
expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false);
lastEvaluatedToken = currentToken;
}
};
for (const matrix of matrices) {
test(`horizontal zigzag walk ${matrix[0].length}x${matrix.length} map`, () =>
runHorizontalZigzagWalkTest(matrix));
}
});
describe('clicking around the editor', () => {
const runRandomClickingTest = async (matrix: string[], attempts: number) => {
const width = matrix[0].length;
const height = matrix.length;
await editor.update(matrix.join('\n'), true);
let lastEvaluatedToken = tokenProvider.getTokenAt(coreEditor.getCurrentPosition());
let currentToken: Token | null;
for (let i = 1; i < attempts; i++) {
const pos = {
column: Math.ceil(Math.random() * width),
lineNumber: Math.ceil(Math.random() * height),
};
currentToken = tokenProvider.getTokenAt(pos);
expect(lastEvaluatedToken).not.toBeNull();
expect(currentToken).not.toBeNull();
expect(looksLikeTypingIn(lastEvaluatedToken!, currentToken!, coreEditor)).toBe(false);
lastEvaluatedToken = currentToken;
}
};
for (const matrix of matrices) {
const attempts = 4 * matrix[0].length * matrix.length;
test(`random clicking ${matrix[0].length}x${matrix.length} map ${attempts} times`, () =>
runRandomClickingTest(matrix, attempts));
}
});
});

View file

@ -1,109 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { CoreEditor, Position, Token } from '../../types';
enum Move {
ForwardOneCharacter = 1,
ForwardOneToken, // the column position may jump to the next token by autocomplete
ForwardTwoTokens, // the column position could jump two tokens due to autocomplete
}
const knownTypingInTokenTypes = new Map<Move, Map<string, Set<string>>>([
[
Move.ForwardOneCharacter,
new Map<string, Set<string>>([
// a pair of the last evaluated token type and a set of the current token types
['', new Set(['method'])],
['url.amp', new Set(['url.param'])],
['url.comma', new Set(['url.part', 'url.questionmark'])],
['url.equal', new Set(['url.value'])],
['url.param', new Set(['url.amp', 'url.equal'])],
['url.questionmark', new Set(['url.param'])],
['url.slash', new Set(['url.part', 'url.questionmark'])],
['url.value', new Set(['url.amp'])],
]),
],
[
Move.ForwardOneToken,
new Map<string, Set<string>>([
['method', new Set(['url.part'])],
['url.amp', new Set(['url.amp', 'url.equal'])],
['url.comma', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
['url.equal', new Set(['url.amp'])],
['url.param', new Set(['url.equal'])],
['url.part', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
['url.questionmark', new Set(['url.equal'])],
['url.slash', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
['url.value', new Set(['url.amp'])],
['whitespace', new Set(['url.comma', 'url.questionmark', 'url.slash'])],
]),
],
[
Move.ForwardTwoTokens,
new Map<string, Set<string>>([['url.part', new Set(['url.param', 'url.part'])]]),
],
]);
const getOneCharacterNextOnTheRight = (pos: Position, coreEditor: CoreEditor): string => {
const range = {
start: { column: pos.column + 1, lineNumber: pos.lineNumber },
end: { column: pos.column + 2, lineNumber: pos.lineNumber },
};
return coreEditor.getValueInRange(range);
};
/**
* Examines a change from the last evaluated to the current token and one
* character next to the current token position on the right. Returns true if
* the change looks like typing in, false otherwise.
*
* This function is supposed to filter out situations where autocomplete is not
* preferable, such as clicking around the editor, navigating the editor via
* keyboard arrow keys, etc.
*/
export const looksLikeTypingIn = (
lastEvaluatedToken: Token,
currentToken: Token,
coreEditor: CoreEditor
): boolean => {
// if the column position moves to the right in the same line and the current
// token length is 1, then user is possibly typing in a character.
if (
lastEvaluatedToken.position.column < currentToken.position.column &&
lastEvaluatedToken.position.lineNumber === currentToken.position.lineNumber &&
currentToken.value.length === 1 &&
getOneCharacterNextOnTheRight(currentToken.position, coreEditor) === ''
) {
const moves =
lastEvaluatedToken.position.column + 1 === currentToken.position.column
? [Move.ForwardOneCharacter]
: [Move.ForwardOneToken, Move.ForwardTwoTokens];
for (const move of moves) {
const tokenTypesPairs = knownTypingInTokenTypes.get(move) ?? new Map<string, Set<string>>();
const currentTokenTypes = tokenTypesPairs.get(lastEvaluatedToken.type) ?? new Set<string>();
if (currentTokenTypes.has(currentToken.type)) {
return true;
}
}
}
// if the column or the line number have changed for the last token or
// user did not provided a new value, then we should not show autocomplete
// this guards against triggering autocomplete when clicking around the editor
if (
lastEvaluatedToken.position.column !== currentToken.position.column ||
lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber ||
lastEvaluatedToken.value === currentToken.value
) {
return false;
}
return true;
};

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import '../../application/models/sense_editor/sense_editor.test.mocks';
import { setAutocompleteInfo, AutocompleteInfo } from '../../services';
import { expandAliases } from './expand_aliases';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';

View file

@ -1,146 +0,0 @@
==========
Curl 1
-------------------------------------
curl -XPUT 'http://localhost:9200/twitter/tweet/1' -d '{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elastic Search"
}'
-------------------------------------
PUT /twitter/tweet/1
{
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elastic Search"
}
==========
Curl 2
-------------------------------------
curl -XGET "localhost/twitter/tweet/1?version=2" -d '{
"message" : "elasticsearch now has versioning support, double cool!"
}'
-------------------------------------
GET /twitter/tweet/1?version=2
{
"message" : "elasticsearch now has versioning support, double cool!"
}
===========
Curl 3
-------------------------------------
curl -XPOST https://localhost/twitter/tweet/1?version=2 -d '{
"message" : "elasticsearch now has versioning support, double cool!"
}'
-------------------------------------
POST /twitter/tweet/1?version=2
{
"message" : "elasticsearch now has versioning support, double cool!"
}
=========
Curl 4
-------------------------------------
curl -XPOST https://localhost/twitter
-------------------------------------
POST /twitter
==========
Curl 5
-------------------------------------
curl -X POST https://localhost/twitter/
-------------------------------------
POST /twitter/
=============
Curl 6
-------------------------------------
curl -s -XPOST localhost:9200/missing-test -d'
{
"mappings": {
}
}'
-------------------------------------
POST /missing-test
{
"mappings": {
}
}
=========================
Curl 7
-------------------------------------
curl 'localhost:9200/missing-test/doc/_search?pretty' -d'
{
"query": {
},
}'
-------------------------------------
GET /missing-test/doc/_search?pretty
{
"query": {
},
}
===========================
Curl 8
-------------------------------------
curl localhost:9200/ -d'
{
"query": {
}
}'
-------------------------------------
GET /
{
"query": {
}
}
====================================
Curl Script
-------------------------------------
#!bin/sh
// test something
curl 'localhost:9200/missing-test/doc/_search?pretty' -d'
{
"query": {
},
}'
curl -XPOST https://localhost/twitter
#someother comments
curl localhost:9200/ -d'
{
"query": {
}
}'
-------------------
# test something
GET /missing-test/doc/_search?pretty
{
"query": {
},
}
POST /twitter
#someother comments
GET /
{
"query": {
}
}
====================================
Curl with some text
-------------------------------------
This is what I meant:
curl 'localhost:9200/missing-test/doc/_search?'
This, however, does work:
curl 'localhost:9200/missing/doc/_search?'
-------------------
### This is what I meant:
GET /missing-test/doc/_search?
### This, however, does work:
GET /missing/doc/_search?

View file

@ -1,194 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
function detectCURLinLine(line) {
// returns true if text matches a curl request
return line.match(/^\s*?curl\s+(-X[A-Z]+)?\s*['"]?.*?['"]?(\s*$|\s+?-d\s*?['"])/);
}
export function detectCURL(text) {
// returns true if text matches a curl request
if (!text) return false;
for (const line of text.split('\n')) {
if (detectCURLinLine(line)) {
return true;
}
}
return false;
}
export function parseCURL(text) {
let state = 'NONE';
const out = [];
let body = [];
let line = '';
const lines = text.trim().split('\n');
let matches;
const EmptyLine = /^\s*$/;
const Comment = /^\s*(?:#|\/{2,})(.*)\n?$/;
const ExecutionComment = /^\s*#!/;
const ClosingSingleQuote = /^([^']*)'/;
const ClosingDoubleQuote = /^((?:[^\\"]|\\.)*)"/;
const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
const LooksLikeCurl = /^\s*curl\s+/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
const HasProtocol = /[\s"']https?:\/\//;
const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
const CurlData = /^.+\s(--data|-d)\s*/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
if (lines.length > 0 && ExecutionComment.test(lines[0])) {
lines.shift();
}
function nextLine() {
if (line.length > 0) {
return true;
}
if (lines.length === 0) {
return false;
}
line = lines.shift().replace(/[\r\n]+/g, '\n') + '\n';
return true;
}
function unescapeLastBodyEl() {
const str = body.pop().replace(/\\([\\"'])/g, '$1');
body.push(str);
}
// Is the next char a single or double quote?
// If so remove it
function detectQuote() {
if (line.substr(0, 1) === "'") {
line = line.substr(1);
state = 'SINGLE_QUOTE';
} else if (line.substr(0, 1) === '"') {
line = line.substr(1);
state = 'DOUBLE_QUOTE';
} else {
state = 'UNQUOTED';
}
}
// Body is finished - append to output with final LF
function addBodyToOut() {
if (body.length > 0) {
out.push(body.join(''));
body = [];
}
state = 'LF';
out.push('\n');
}
// If the pattern matches, then the state is about to change,
// so add the capture to the body and detect the next state
// Otherwise add the whole line
function consumeMatching(pattern) {
const matches = line.match(pattern);
if (matches) {
body.push(matches[1]);
line = line.substr(matches[0].length);
detectQuote();
} else {
body.push(line);
line = '';
}
}
function parseCurlLine() {
let verb = 'GET';
let request = '';
let matches;
if ((matches = line.match(CurlVerb))) {
verb = matches[1];
}
// JS regexen don't support possessive quantifiers, so
// we need two distinct patterns
const pattern = HasProtocol.test(line) ? CurlRequestWithProto : CurlRequestWithoutProto;
if ((matches = line.match(pattern))) {
request = matches[1];
}
out.push(verb + ' /' + request + '\n');
if ((matches = line.match(CurlData))) {
line = line.substr(matches[0].length);
detectQuote();
if (EmptyLine.test(line)) {
line = '';
}
} else {
state = 'NONE';
line = '';
out.push('');
}
}
while (nextLine()) {
if (state === 'SINGLE_QUOTE') {
consumeMatching(ClosingSingleQuote);
} else if (state === 'DOUBLE_QUOTE') {
consumeMatching(ClosingDoubleQuote);
unescapeLastBodyEl();
} else if (state === 'UNQUOTED') {
consumeMatching(EscapedQuotes);
if (body.length) {
unescapeLastBodyEl();
}
if (state === 'UNQUOTED') {
addBodyToOut();
line = '';
}
}
// the BODY state (used to match the body of a Sense request)
// can be terminated early if it encounters
// a comment or an empty line
else if (state === 'BODY') {
if (Comment.test(line) || EmptyLine.test(line)) {
addBodyToOut();
} else {
body.push(line);
line = '';
}
} else if (EmptyLine.test(line)) {
if (state !== 'LF') {
out.push('\n');
state = 'LF';
}
line = '';
} else if ((matches = line.match(Comment))) {
out.push('#' + matches[1] + '\n');
state = 'NONE';
line = '';
} else if (LooksLikeCurl.test(line)) {
parseCurlLine();
} else if ((matches = line.match(SenseLine))) {
out.push(matches[1] + ' /' + matches[2] + '\n');
line = '';
state = 'BODY';
}
// Nothing else matches, so output with a prefix of !!! for debugging purposes
else {
out.push('### ' + line);
line = '';
}
}
addBodyToOut();
return out.join('').trim();
}

View file

@ -1,37 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import _ from 'lodash';
import { detectCURL, parseCURL } from './curl';
import curlTests from './__fixtures__/curl_parsing.txt';
describe('CURL', () => {
const notCURLS = ['sldhfsljfhs', 's;kdjfsldkfj curl -XDELETE ""', '{ "hello": 1 }'];
_.each(notCURLS, function (notCURL, i) {
test('cURL Detection - broken strings ' + i, function () {
expect(detectCURL(notCURL)).toEqual(false);
});
});
curlTests.split(/^=+$/m).forEach(function (fixture) {
if (fixture.trim() === '') {
return;
}
fixture = fixture.split(/^-+$/m);
const name = fixture[0].trim();
const curlText = fixture[1];
const response = fixture[2].trim();
test('cURL Detection - ' + name, function () {
expect(detectCURL(curlText)).toBe(true);
const r = parseCURL(curlText);
expect(r).toEqual(response);
});
});
});

View file

@ -10,7 +10,6 @@
import _ from 'lodash';
import { populateContext } from '../autocomplete/engine';
import '../../application/models/sense_editor/sense_editor.test.mocks';
import * as kb from '.';
import { AutocompleteInfo, setAutocompleteInfo } from '../../services';

View file

@ -1,107 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import '../application/models/legacy_core_editor/legacy_core_editor.test.mocks';
import RowParser from './row_parser';
import { create, MODE } from '../application/models';
import type { SenseEditor } from '../application/models';
import type { CoreEditor } from '../types';
describe('RowParser', () => {
let editor: SenseEditor | null;
let parser: RowParser | null;
beforeEach(function () {
// Set up our document body
document.body.innerHTML = `<div>
<div id="ConAppEditor" />
<div id="ConAppEditorActions" />
<div id="ConCopyAsCurl" />
</div>`;
editor = create(document.getElementById('ConAppEditor')!);
parser = new RowParser(editor.getCoreEditor() as CoreEditor);
});
afterEach(function () {
editor?.getCoreEditor().destroy();
editor = null;
parser = null;
});
describe('getRowParseMode', () => {
const forceRetokenize = false;
it('should return MODE.BETWEEN_REQUESTS if line is empty', () => {
editor?.getCoreEditor().setValue('', forceRetokenize);
expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS);
});
it('should return MODE.BETWEEN_REQUESTS if line is a comment', () => {
editor?.getCoreEditor().setValue('// comment', forceRetokenize);
expect(parser?.getRowParseMode()).toBe(MODE.BETWEEN_REQUESTS);
});
it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a single line request', () => {
editor?.getCoreEditor().setValue('GET _search', forceRetokenize);
// eslint-disable-next-line no-bitwise
expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
});
it('should return MODE.IN_REQUEST if line is a request with an opening curly brace', () => {
editor?.getCoreEditor().setValue('{', forceRetokenize);
expect(parser?.getRowParseMode()).toBe(MODE.IN_REQUEST);
});
it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST if line is a multi doc request with an opening curly brace', () => {
editor?.getCoreEditor().setValue('GET _msearch\n{}\n{', forceRetokenize);
const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
expect(parser?.getRowParseMode(lineNumber)).toBe(
// eslint-disable-next-line no-bitwise
MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST
);
});
it('should return MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END if line is a multi doc request with a closing curly brace', () => {
editor?.getCoreEditor().setValue('GET _msearch\n{}\n{"foo": 1}\n', forceRetokenize);
const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
expect(parser?.getRowParseMode(lineNumber)).toBe(
// eslint-disable-next-line no-bitwise
MODE.MULTI_DOC_CUR_DOC_END | MODE.REQUEST_END
);
});
it('should return MODE.REQUEST_START | MODE.REQUEST_END if line is a request with variables', () => {
editor?.getCoreEditor().setValue('GET /${exampleVariable}', forceRetokenize);
// eslint-disable-next-line no-bitwise
expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
});
it('should return MODE.REQUEST_START | MODE.REQUEST_END if a single request line ends with a closing curly brace', () => {
editor?.getCoreEditor().setValue('DELETE <foo>/_bar/_baz%{test}', forceRetokenize);
// eslint-disable-next-line no-bitwise
expect(parser?.getRowParseMode()).toBe(MODE.REQUEST_START | MODE.REQUEST_END);
});
it('should return correct modes for multiple bulk requests', () => {
editor
?.getCoreEditor()
.setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize);
expect(parser?.getRowParseMode(0)).toBe(MODE.BETWEEN_REQUESTS);
editor
?.getCoreEditor()
.setValue('POST _bulk\n{"index": {"_index": "test"}}\n{"foo": "bar"}\n', forceRetokenize);
const lineNumber = editor?.getCoreEditor().getLineCount()! - 1;
expect(parser?.getRowParseMode(lineNumber)).toBe(
// eslint-disable-next-line no-bitwise
MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END
);
});
});
});

View file

@ -1,161 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { CoreEditor, Token } from '../types';
import { TokenIterator } from './token_iterator';
export const MODE = {
REQUEST_START: 2,
IN_REQUEST: 4,
MULTI_DOC_CUR_DOC_END: 8,
REQUEST_END: 16,
BETWEEN_REQUESTS: 32,
};
// eslint-disable-next-line import/no-default-export
export default class RowParser {
constructor(private readonly editor: CoreEditor) {}
MODE = MODE;
getRowParseMode(lineNumber = this.editor.getCurrentPosition().lineNumber) {
const linesCount = this.editor.getLineCount();
if (lineNumber > linesCount || lineNumber < 1) {
return MODE.BETWEEN_REQUESTS;
}
const mode = this.editor.getLineState(lineNumber);
if (!mode) {
return MODE.BETWEEN_REQUESTS;
} // shouldn't really happen
// If another "start" mode is added here because we want to allow for new language highlighting
// please see https://github.com/elastic/kibana/pull/51446 for a discussion on why
// should consider a different approach.
if (mode !== 'start' && mode !== 'start-sql') {
return MODE.IN_REQUEST;
}
let line = (this.editor.getLineValue(lineNumber) || '').trim();
if (!line || line.startsWith('#') || line.startsWith('//') || line.startsWith('/*')) {
return MODE.BETWEEN_REQUESTS;
} // empty line or a comment waiting for a new req to start
// Check for multi doc requests
if (line.endsWith('}') && !this.isRequestLine(line)) {
// check for a multi doc request must start a new json doc immediately after this one end.
lineNumber++;
if (lineNumber < linesCount + 1) {
line = (this.editor.getLineValue(lineNumber) || '').trim();
if (line.indexOf('{') === 0) {
// next line is another doc in a multi doc
// eslint-disable-next-line no-bitwise
return MODE.MULTI_DOC_CUR_DOC_END | MODE.IN_REQUEST;
}
}
// eslint-disable-next-line no-bitwise
return MODE.REQUEST_END | MODE.MULTI_DOC_CUR_DOC_END; // end of request
}
// check for single line requests
lineNumber++;
if (lineNumber >= linesCount + 1) {
// eslint-disable-next-line no-bitwise
return MODE.REQUEST_START | MODE.REQUEST_END;
}
line = (this.editor.getLineValue(lineNumber) || '').trim();
if (line.indexOf('{') !== 0) {
// next line is another request
// eslint-disable-next-line no-bitwise
return MODE.REQUEST_START | MODE.REQUEST_END;
}
return MODE.REQUEST_START;
}
rowPredicate(lineNumber: number | undefined, editor: CoreEditor, value: number) {
const mode = this.getRowParseMode(lineNumber);
// eslint-disable-next-line no-bitwise
return (mode & value) > 0;
}
isEndRequestRow(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
return this.rowPredicate(row, editor, MODE.REQUEST_END);
}
isRequestEdge(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
// eslint-disable-next-line no-bitwise
return this.rowPredicate(row, editor, MODE.REQUEST_END | MODE.REQUEST_START);
}
isStartRequestRow(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
return this.rowPredicate(row, editor, MODE.REQUEST_START);
}
isInBetweenRequestsRow(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
return this.rowPredicate(row, editor, MODE.BETWEEN_REQUESTS);
}
isInRequestsRow(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
return this.rowPredicate(row, editor, MODE.IN_REQUEST);
}
isMultiDocDocEndRow(row?: number, _e?: CoreEditor) {
const editor = _e || this.editor;
return this.rowPredicate(row, editor, MODE.MULTI_DOC_CUR_DOC_END);
}
isEmptyToken(tokenOrTokenIter: TokenIterator | Token | null) {
const token =
tokenOrTokenIter && (tokenOrTokenIter as TokenIterator).getCurrentToken
? (tokenOrTokenIter as TokenIterator).getCurrentToken()
: tokenOrTokenIter;
return !token || (token as Token).type === 'whitespace';
}
isUrlOrMethodToken(tokenOrTokenIter: TokenIterator | Token) {
const t = (tokenOrTokenIter as TokenIterator)?.getCurrentToken() ?? (tokenOrTokenIter as Token);
return t && t.type && (t.type === 'method' || t.type.indexOf('url') === 0);
}
nextNonEmptyToken(tokenIter: TokenIterator) {
let t = tokenIter.stepForward();
while (t && this.isEmptyToken(t)) {
t = tokenIter.stepForward();
}
return t;
}
prevNonEmptyToken(tokenIter: TokenIterator) {
let t = tokenIter.stepBackward();
// empty rows return null token.
while ((t || tokenIter.getCurrentPosition().lineNumber > 1) && this.isEmptyToken(t))
t = tokenIter.stepBackward();
return t;
}
isCommentToken(token: Token | null) {
return (
token &&
token.type &&
(token.type === 'comment.punctuation' ||
token.type === 'comment.line' ||
token.type === 'comment.block')
);
}
isRequestLine(line: string) {
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS'];
return methods.some((m) => line.startsWith(m));
}
}

View file

@ -36,8 +36,6 @@
width: 100%;
display: flex;
flex: 0 0 auto;
// Required on IE11 to render ace editor correctly after first input.
position: relative;
&__spinner {
@ -55,46 +53,6 @@
height: 100%;
display: flex;
flex: 1 1 1px;
.ace_badge {
font-family: $euiFontFamily;
font-size: $euiFontSizeXS;
font-weight: $euiFontWeightMedium;
line-height: $euiLineHeight;
padding: 0 $euiSizeS;
display: inline-block;
text-decoration: none;
border-radius: calc($euiBorderRadius / 2);
white-space: nowrap;
vertical-align: middle;
cursor: default;
max-width: 100%;
&--success {
background-color: $euiColorVis0_behindText;
color: chooseLightOrDarkText($euiColorVis0_behindText);
}
&--warning {
background-color: $euiColorVis5_behindText;
color: chooseLightOrDarkText($euiColorVis5_behindText);
}
&--primary {
background-color: $euiColorVis1_behindText;
color: chooseLightOrDarkText($euiColorVis1_behindText);
}
&--default {
background-color: $euiColorLightShade;
color: chooseLightOrDarkText($euiColorLightShade);
}
&--danger {
background-color: $euiColorVis9_behindText;
color: chooseLightOrDarkText($euiColorVis9_behindText);
}
}
}
.conApp__editorContent,
@ -145,17 +103,6 @@
margin-inline: 0;
}
// SASSTODO: This component seems to not be used anymore?
// Possibly replaced by the Ace version
.conApp__autoComplete {
position: absolute;
left: -1000px;
visibility: hidden;
/* by pass any other element in ace and resize bar, but not modal popups */
z-index: $euiZLevel1 + 2;
margin-top: 22px;
}
.conApp__requestProgressBarContainer {
position: relative;
z-index: $euiZLevel2;

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Editor } from 'brace';
import { ResultTerm } from '../lib/autocomplete/types';
import { TokensProvider } from './tokens_provider';
import { Token } from './token';
@ -94,7 +93,7 @@ export enum LINE_MODE {
/**
* The CoreEditor is a component separate from the Editor implementation that provides Console
* app specific business logic. The CoreEditor is an interface to the lower-level editor implementation
* being used which is usually vendor code such as Ace or Monaco.
* being used which is usually vendor code such as Monaco.
*/
export interface CoreEditor {
/**
@ -260,7 +259,7 @@ export interface CoreEditor {
*/
registerKeyboardShortcut(opts: {
keys: string | { win?: string; mac?: string };
fn: (editor: Editor) => void;
fn: (editor: any) => void;
name: string;
}): void;

View file

@ -18,10 +18,8 @@
"@kbn/i18n-react",
"@kbn/shared-ux-utility",
"@kbn/core-http-browser",
"@kbn/ace",
"@kbn/config-schema",
"@kbn/core-http-router-server-internal",
"@kbn/web-worker-stub",
"@kbn/core-elasticsearch-server",
"@kbn/core-http-browser-mocks",
"@kbn/react-kibana-context-theme",

View file

@ -320,8 +320,6 @@
"coloring.dynamicColoring.rangeType.label": "Type de valeur",
"coloring.dynamicColoring.rangeType.number": "Numéro",
"coloring.dynamicColoring.rangeType.percent": "Pourcent",
"console.autocomplete.addMethodMetaText": "méthode",
"console.autocomplete.fieldsFetchingAnnotation": "La récupération des champs est en cours",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "point de terminaison",
"console.autocompleteSuggestions.methodLabel": "méthode",
@ -362,10 +360,6 @@
"console.loadingError.title": "Impossible de charger la console",
"console.notification.clearHistory": "Effacer l'historique",
"console.notification.disableSavingToHistory": "Désactiver l'enregistrement",
"console.notification.error.couldNotSaveRequestTitle": "Impossible d'enregistrer la requête dans l'historique de la console.",
"console.notification.error.historyQuotaReachedMessage": "L'historique des requêtes est arrivé à saturation. Effacez l'historique de la console ou désactivez l'enregistrement de nouvelles requêtes.",
"console.notification.error.noRequestSelectedTitle": "Aucune requête sélectionnée. Sélectionnez une requête en positionnant le curseur dessus.",
"console.notification.error.unknownErrorTitle": "Erreur de requête inconnue",
"console.pageHeading": "Console",
"console.requestInProgressBadgeText": "Requête en cours",
"console.requestOptions.autoIndentButtonLabel": "Appliquer les indentations",

View file

@ -320,8 +320,6 @@
"coloring.dynamicColoring.rangeType.label": "値型",
"coloring.dynamicColoring.rangeType.number": "Number",
"coloring.dynamicColoring.rangeType.percent": "割合(%",
"console.autocomplete.addMethodMetaText": "メソド",
"console.autocomplete.fieldsFetchingAnnotation": "フィールドの取得を実行しています",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "エンドポイント",
"console.autocompleteSuggestions.methodLabel": "メソド",
@ -362,10 +360,6 @@
"console.loadingError.title": "コンソールを読み込めません",
"console.notification.clearHistory": "履歴を消去",
"console.notification.disableSavingToHistory": "保存を無効にする",
"console.notification.error.couldNotSaveRequestTitle": "リクエストをコンソール履歴に保存できませんでした。",
"console.notification.error.historyQuotaReachedMessage": "リクエスト履歴が満杯です。コンソール履歴を消去するか、新しいリクエストの保存を無効にしてください。",
"console.notification.error.noRequestSelectedTitle": "リクエストを選択していません。リクエストの中にカーソルを置いて選択します。",
"console.notification.error.unknownErrorTitle": "不明なリクエストエラー",
"console.pageHeading": "コンソール",
"console.requestInProgressBadgeText": "リクエストが進行中",
"console.requestOptions.autoIndentButtonLabel": "インデントを適用",

View file

@ -319,8 +319,6 @@
"coloring.dynamicColoring.rangeType.label": "值类型",
"coloring.dynamicColoring.rangeType.number": "数字",
"coloring.dynamicColoring.rangeType.percent": "百分比",
"console.autocomplete.addMethodMetaText": "方法",
"console.autocomplete.fieldsFetchingAnnotation": "正在提取字段",
"console.autocompleteSuggestions.apiLabel": "API",
"console.autocompleteSuggestions.endpointLabel": "终端",
"console.autocompleteSuggestions.methodLabel": "方法",
@ -361,10 +359,6 @@
"console.loadingError.title": "无法加载控制台",
"console.notification.clearHistory": "清除历史记录",
"console.notification.disableSavingToHistory": "禁止保存",
"console.notification.error.couldNotSaveRequestTitle": "无法将请求保存到控制台历史记录。",
"console.notification.error.historyQuotaReachedMessage": "请求历史记录已满。请清除控制台历史记录或禁止保存新的请求。",
"console.notification.error.noRequestSelectedTitle": "未选择任何请求。将鼠标置于请求内即可选择。",
"console.notification.error.unknownErrorTitle": "未知请求错误",
"console.pageHeading": "控制台",
"console.requestInProgressBadgeText": "进行中的请求",
"console.requestOptions.autoIndentButtonLabel": "应用行首缩进",