mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
57080cb78d
commit
b5b68bb601
10 changed files with 238 additions and 58 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue