[Console] Improve UX around handling multiple requests (#132494)

* Render status badges in the output panel

* Add missing dependancy for useEffect hook

* Lint

* Add functional tests

* Update tests

* Fix tests

* Update selectAll command key for firefox

* Fix selecting all requests in tests

* Convert output modes to TS

* Fix checks

* Fix types for editor session.update method

* Add tests for mapStatusCodeToBadge function

* Address comments

* Change .ace-badge font family to $euiFontFamily

Co-authored-by: Muhammad Ibragimov <muhammad.ibragimov@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Muhammad Ibragimov 2022-06-06 13:34:26 +05:00 committed by GitHub
parent 57080cb78d
commit b5b68bb601
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 238 additions and 58 deletions

View file

@ -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) => {

View file

@ -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<RequestResult[]> {
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<RequestResult[]> {
}
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<RequestResult[]> {
}
if (isMultiRequest) {
value = '# ' + req.method + ' ' + req.url + '\n' + value;
value = `# ${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`;
}
const result = {

View file

@ -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);

View file

@ -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;
};

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 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);

View file

@ -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);
});
});
});

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import 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();
}
}
}

View file

@ -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,

View file

@ -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);
});
});
});
}

View file

@ -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;
}
}
}