diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx index 0817b6df5f7ef..eb085248f4a3e 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -11,6 +11,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef } from 'react'; import { convertMapboxVectorTileToJson } from './mapbox_vector_tile'; +import { Mode } from '../../../../models/legacy_core_editor/mode/output'; // Ensure the modes we might switch to dynamically are available import 'brace/mode/text'; @@ -83,7 +84,10 @@ function EditorOutputUI() { useEffect(() => { const editor = editorInstanceRef.current!; if (data) { - const mode = modeForContentType(data[0].response.contentType); + const isMultipleRequest = data.length > 1; + const mode = isMultipleRequest + ? new Mode() + : modeForContentType(data[0].response.contentType); editor.update( data .map((result) => { diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts index 1ac47df30fca5..3247c8aed164e 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts @@ -9,8 +9,7 @@ import type { HttpSetup, IHttpFetchError } from '@kbn/core/public'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; import { extractWarningMessages } from '../../../lib/utils'; -// @ts-ignore -import * as es from '../../../lib/es/es'; +import { send } from '../../../lib/es/es'; import { BaseResponseType } from '../../../types'; const { collapseLiteralStrings } = XJson; @@ -72,7 +71,7 @@ export function sendRequest(args: RequestArgs): Promise { const startTime = Date.now(); try { - const { response, body } = await es.send({ + const { response, body } = await send({ http: args.http, method, path, @@ -106,7 +105,7 @@ export function sendRequest(args: RequestArgs): Promise { } if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + value = `# ${req.method} ${req.url} ${response.status} ${response.statusText}\n${value}`; } results.push({ @@ -141,7 +140,7 @@ export function sendRequest(args: RequestArgs): Promise { } if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + value = `# ${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`; } const result = { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts index 2b87331d5f47d..7924b06e8b15f 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/create_readonly.ts @@ -8,12 +8,11 @@ import _ from 'lodash'; import ace from 'brace'; -// @ts-ignore -import * as OutputMode from './mode/output'; +import { Mode } from './mode/output'; import smartResize from './smart_resize'; export interface CustomAceEditor extends ace.Editor { - update: (text: string, mode?: unknown, cb?: () => void) => void; + update: (text: string, mode?: string | Mode, cb?: () => void) => void; append: (text: string, foldPrevious?: boolean, cb?: () => void) => void; } @@ -24,19 +23,23 @@ export interface CustomAceEditor extends ace.Editor { export function createReadOnlyAceEditor(element: HTMLElement): CustomAceEditor { const output: CustomAceEditor = ace.acequire('ace/ace').edit(element); - const outputMode = new OutputMode.Mode(); + const outputMode = new Mode(); output.$blockScrolling = Infinity; output.resize = smartResize(output); - output.update = (val: string, mode?: unknown, cb?: () => void) => { + 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'; - session.setMode(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); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts similarity index 75% rename from src/plugins/console/public/application/models/legacy_core_editor/mode/output.js rename to src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts index b769505e81335..234d57b830a7b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output.ts @@ -10,7 +10,6 @@ import ace from 'brace'; import { OutputJsonHighlightRules } from './output_highlight_rules'; -const oop = ace.acequire('ace/lib/oop'); 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; @@ -18,15 +17,17 @@ const CStyleFoldMode = ace.acequire('ace/mode/folding/cstyle').FoldMode; ace.acequire('ace/worker/worker_client'); const AceTokenizer = ace.acequire('ace/tokenizer').Tokenizer; -export function Mode() { - this.$tokenizer = new AceTokenizer(new OutputJsonHighlightRules().getRules()); - this.$outdent = new MatchingBraceOutdent(); - this.$behaviour = new CstyleBehaviour(); - this.foldingRules = new CStyleFoldMode(); +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(); + } } -oop.inherits(Mode, JSONMode); -(function () { +(function (this: Mode) { this.createWorker = function () { return null; }; diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js deleted file mode 100644 index ebcce29da9e1e..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.js +++ /dev/null @@ -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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import ace from 'brace'; -import 'brace/mode/json'; -import { addXJsonToRules } from '@kbn/ace'; - -const oop = ace.acequire('ace/lib/oop'); -const JsonHighlightRules = ace.acequire('ace/mode/json_highlight_rules').JsonHighlightRules; - -export function OutputJsonHighlightRules() { - this.$rules = {}; - - addXJsonToRules(this, 'start'); - - this.$rules.start.unshift( - { - token: 'warning', - regex: '#!.*$', - }, - { - token: 'comment', - regex: '#.*$', - } - ); - - if (this.constructor === OutputJsonHighlightRules) { - this.normalizeRules(); - } -} - -oop.inherits(OutputJsonHighlightRules, JsonHighlightRules); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts new file mode 100644 index 0000000000000..cdbbd4bc7b178 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { 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); + }); + }); +}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts new file mode 100644 index 0000000000000..925bcde746b85 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/output_highlight_rules.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import 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', + regex: /#(.*?)(?=\d+\s(?:[\sA-Za-z]+)|$)/, + }, + { + token: mapStatusCodeToBadge, + regex: /(\d+\s[\sA-Za-z]+$)/, + } + ); + + if (this instanceof OutputJsonHighlightRules) { + this.normalizeRules(); + } + } +} diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 61dc31138c768..2490bb29f0fb7 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -33,6 +33,46 @@ .conApp__output { 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: $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, diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 52218b88be60d..126788c3312d2 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; const DEFAULT_REQUEST = ` @@ -24,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'console']); + const PageObjects = getPageObjects(['common', 'console', 'header']); const toasts = getService('toasts'); describe('console app', function describeIndexTests() { @@ -122,5 +123,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('multiple requests output', () => { + const sendMultipleRequests = async (requests: string[]) => { + await asyncForEach(requests, async (request) => { + await PageObjects.console.enterRequest(request); + }); + await PageObjects.console.selectAllRequests(); + await PageObjects.console.clickPlay(); + }; + + beforeEach(async () => { + await PageObjects.console.clearTextArea(); + }); + + it('should contain comments starting with # symbol', async () => { + await sendMultipleRequests(['\n PUT test-index', '\n DELETE test-index']); + await retry.try(async () => { + const response = await PageObjects.console.getResponse(); + log.debug(response); + expect(response).to.contain('# PUT test-index 200 OK'); + expect(response).to.contain('# DELETE test-index 200 OK'); + }); + }); + + it('should display status badges', async () => { + await sendMultipleRequests(['\n GET _search/test', '\n GET _search']); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.console.hasWarningBadge()).to.be(true); + expect(await PageObjects.console.hasSuccessBadge()).to.be(true); + }); + }); }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 218a1077d63ef..e8467ce714ff8 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -163,4 +163,28 @@ export class ConsolePageObject extends FtrService { return lines.length === 1 && text.trim() === ''; }); } + + public async selectAllRequests() { + const editor = await this.getEditorTextArea(); + const selectionKey = Key[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL']; + await editor.pressKeys([selectionKey, 'a']); + } + + public async hasSuccessBadge() { + try { + const responseEditor = await this.testSubjects.find('response-editor'); + return Boolean(await responseEditor.findByCssSelector('.ace_badge--success')); + } catch (e) { + return false; + } + } + + public async hasWarningBadge() { + try { + const responseEditor = await this.testSubjects.find('response-editor'); + return Boolean(await responseEditor.findByCssSelector('.ace_badge--warning')); + } catch (e) { + return false; + } + } }