mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Console] Fix parsing requests with errors (#215568)
Fixes https://github.com/elastic/kibana/issues/211031 ## Summary This PR fixes the selection of requests in Console when a request contains an error. It also adds an error toast when the user tries to send a request containing an error, as the response from Elasticsearch is usually too long and not very helpful. https://github.com/user-attachments/assets/4de10953-9ee5-489b-94fb-fd8a772bd598
This commit is contained in:
parent
c8fc5e74d9
commit
4557b73959
6 changed files with 128 additions and 27 deletions
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { ConsoleWorkerProxyService } from './console_worker_proxy';
|
||||
import { ParsedRequest } from './types';
|
||||
import { ErrorAnnotation, ParsedRequest } from './types';
|
||||
import { monaco } from '../../monaco_imports';
|
||||
|
||||
/*
|
||||
|
@ -29,4 +29,11 @@ export class ConsoleParsedRequestsProvider {
|
|||
const parserResult = await this.workerProxyService.getParserResult(this.model.uri);
|
||||
return parserResult?.requests ?? [];
|
||||
}
|
||||
public async getErrors(): Promise<ErrorAnnotation[]> {
|
||||
if (!this.model) {
|
||||
return [];
|
||||
}
|
||||
const parserResult = await this.workerProxyService.getParserResult(this.model.uri);
|
||||
return parserResult?.errors ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ export const createParser = () => {
|
|||
},
|
||||
reset = function (newAt) {
|
||||
ch = text.charAt(newAt);
|
||||
updateRequestEnd();
|
||||
addRequestEnd();
|
||||
at = newAt + 1;
|
||||
},
|
||||
next = function (c) {
|
||||
|
@ -413,10 +415,16 @@ export const createParser = () => {
|
|||
} catch (e) {
|
||||
addError(e.message);
|
||||
// snap
|
||||
const substring = text.substr(at);
|
||||
const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m);
|
||||
if (nextMatch < 1) return;
|
||||
reset(at + nextMatch);
|
||||
const remainingText = text.substr(at);
|
||||
const nextMethodIndex = remainingText.search(/^\s*(POST|HEAD|GET|PUT|DELETE|PATCH)\b/mi);
|
||||
const nextCommentLine = remainingText.search(/^\s*(#|\/\*|\/\/).*$/m);
|
||||
if (nextMethodIndex === -1 && nextCommentLine === -1) {
|
||||
// If there are no comments or other requests after the error, there is no point in parsing more so we stop here
|
||||
return;
|
||||
}
|
||||
// Reset parser at the next request or the next comment, whichever comes first
|
||||
at += Math.min(...[nextMethodIndex, nextCommentLine].filter(i => i !== -1));
|
||||
reset(at);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -53,6 +53,48 @@ describe('console parser', () => {
|
|||
expect(endOffset).toBe(52);
|
||||
});
|
||||
|
||||
it('parses requests with an error', () => {
|
||||
const input =
|
||||
'GET _search\n' +
|
||||
'{ERROR\n' +
|
||||
' "query": {\n' +
|
||||
' "match_all": {}\n' +
|
||||
' }\n' +
|
||||
'}\n\n' +
|
||||
'POST _test_index';
|
||||
const { requests, errors } = parser(input) as ConsoleParserResult;
|
||||
expect(requests.length).toBe(2);
|
||||
expect(requests[0].startOffset).toBe(0);
|
||||
expect(requests[0].endOffset).toBe(57);
|
||||
expect(requests[1].startOffset).toBe(59);
|
||||
expect(requests[1].endOffset).toBe(75);
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].offset).toBe(14);
|
||||
expect(errors[0].text).toBe('Bad string');
|
||||
});
|
||||
|
||||
it('parses requests with an error and a comment before the next request', () => {
|
||||
const input =
|
||||
'GET _search\n' +
|
||||
'{ERROR\n' +
|
||||
' "query": {\n' +
|
||||
' "match_all": {}\n' +
|
||||
' }\n' +
|
||||
'}\n\n' +
|
||||
'# This is a comment\n' +
|
||||
'POST _test_index\n' +
|
||||
'// Another comment\n';
|
||||
const { requests, errors } = parser(input) as ConsoleParserResult;
|
||||
expect(requests.length).toBe(2);
|
||||
expect(requests[0].startOffset).toBe(0);
|
||||
expect(requests[0].endOffset).toBe(57);
|
||||
expect(requests[1].startOffset).toBe(79); // The next request should start after the comment
|
||||
expect(requests[1].endOffset).toBe(95);
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].offset).toBe(14);
|
||||
expect(errors[0].text).toBe('Bad string');
|
||||
});
|
||||
|
||||
describe('case insensitive methods', () => {
|
||||
const expectedRequests = [
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, monaco } from
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { XJson } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { ErrorAnnotation } from '@kbn/monaco/src/languages/console/types';
|
||||
import { isQuotaExceededError } from '../../../services/history';
|
||||
import { DEFAULT_VARIABLES, KIBANA_API_PREFIX } from '../../../../common/constants';
|
||||
import { getStorage, StorageKeys } from '../../../services';
|
||||
|
@ -233,6 +234,30 @@ export class MonacoEditorActionsProvider {
|
|||
return selectedRequests;
|
||||
}
|
||||
|
||||
private async getErrorsBetweenLines(
|
||||
startLineNumber: number,
|
||||
endLineNumber: number
|
||||
): Promise<ErrorAnnotation[]> {
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return [];
|
||||
}
|
||||
const parsedErrors = await this.parsedRequestsProvider.getErrors();
|
||||
const selectedErrors: ErrorAnnotation[] = [];
|
||||
for (const parsedError of parsedErrors) {
|
||||
const errorLine = model.getPositionAt(parsedError.offset).lineNumber;
|
||||
if (errorLine > endLineNumber) {
|
||||
// error is past the selection, no need to check further errors
|
||||
break;
|
||||
}
|
||||
if (errorLine >= startLineNumber) {
|
||||
// error is selected
|
||||
selectedErrors.push(parsedError);
|
||||
}
|
||||
}
|
||||
return selectedErrors;
|
||||
}
|
||||
|
||||
public async getRequests() {
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
|
@ -276,6 +301,25 @@ export class MonacoEditorActionsProvider {
|
|||
try {
|
||||
const allRequests = await this.getRequests();
|
||||
const selectedRequests = await this.getSelectedParsedRequests();
|
||||
if (selectedRequests.length) {
|
||||
const selectedErrors = await this.getErrorsBetweenLines(
|
||||
selectedRequests.at(0)!.startLineNumber,
|
||||
selectedRequests.at(-1)!.endLineNumber
|
||||
);
|
||||
if (selectedErrors.length) {
|
||||
toasts.addDanger(
|
||||
i18n.translate('console.notification.monaco.error.errorInSelection', {
|
||||
defaultMessage:
|
||||
'The selected {requestCount, plural, one {request contains} other {requests contain}} {errorCount, plural, one {an error} other {errors}}. Please resolve {errorCount, plural, one {it} other {them}} and try again.',
|
||||
values: {
|
||||
requestCount: selectedRequests.length,
|
||||
errorCount: selectedErrors.length,
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const requests = allRequests
|
||||
// if any request doesnt have a method then we gonna treat it as a non-valid
|
||||
|
|
|
@ -75,20 +75,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.console.getEditorText()).to.be.empty();
|
||||
});
|
||||
|
||||
it('should return statusCode 400 to unsupported HTTP verbs', async () => {
|
||||
const expectedResponseContains = '"statusCode": 400';
|
||||
await PageObjects.console.clearEditorText();
|
||||
await PageObjects.console.enterText('OPTIONS /');
|
||||
await PageObjects.console.clickPlay();
|
||||
await retry.try(async () => {
|
||||
const actualResponse = await PageObjects.console.getOutputText();
|
||||
log.debug(actualResponse);
|
||||
expect(actualResponse).to.contain(expectedResponseContains);
|
||||
|
||||
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs navigation', () => {
|
||||
let currentUrl: string;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const retry = getService('retry');
|
||||
const browser = getService('browser');
|
||||
const PageObjects = getPageObjects(['common', 'console', 'header']);
|
||||
const toasts = getService('toasts');
|
||||
|
||||
describe('misc console behavior', function testMiscConsoleBehavior() {
|
||||
before(async () => {
|
||||
|
@ -204,17 +205,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
it(`should include an invalid json when sending a request`, async () => {
|
||||
it(`should display an error toast when sending a request with invalid body`, async () => {
|
||||
await PageObjects.console.clearEditorText();
|
||||
await PageObjects.console.enterText(invalidRequestText);
|
||||
await PageObjects.console.selectCurrentRequest();
|
||||
await PageObjects.console.pressCtrlEnter();
|
||||
await PageObjects.console.clickPlay();
|
||||
|
||||
await retry.try(async () => {
|
||||
const actualResponse = await PageObjects.console.getOutputText();
|
||||
expect(actualResponse).to.contain('parsing_exception');
|
||||
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
|
||||
});
|
||||
expect(await toasts.getCount()).to.be(1);
|
||||
const resultToast = await toasts.getElementByIndex(1);
|
||||
const toastText = await resultToast.getVisibleText();
|
||||
expect(toastText).to.be(
|
||||
'The selected request contains an error. Please resolve it and try again.'
|
||||
);
|
||||
await toasts.dismissAll();
|
||||
});
|
||||
|
||||
it('should display an error toast to unsupported HTTP verbs', async () => {
|
||||
await PageObjects.console.clearEditorText();
|
||||
await PageObjects.console.enterText('OPTIONS /');
|
||||
await PageObjects.console.clickPlay();
|
||||
expect(await toasts.getCount()).to.be(1);
|
||||
const resultToast = await toasts.getElementByIndex(1);
|
||||
const toastText = await resultToast.getVisibleText();
|
||||
expect(toastText).to.be(
|
||||
'The selected request contains errors. Please resolve them and try again.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue