[Console Monaco] Improve highlighting for status codes in multiple responses (#189210)

Closes https://github.com/elastic/kibana/issues/184010

## Summary

This PR adds color badges for the status codes in the output panel when
there are multiple responses.


https://github.com/user-attachments/assets/be07b34d-7d04-4448-bf5d-b0d6fa36f2d7

**How to test:**
Send the following requests at once (try arranging them in different
orders):

Success badge 🟢 : `GET _search`

Client error (warning) badge 🟡 : `GET _test`

Server error (danger) badge 🔴 : 
```
PUT /library/_bulk?refresh
{"index":{"_id":"Leviathan Wakes"}}
{"name":"Leviathan Wakes","author":"James S.A. Corey","release_date":"2011-06-02","page_count":561}
```
^ This request should usually succeed and it was fixed in
https://github.com/elastic/kibana/pull/188552, but these changes aren't
in this branch yet so the request still fails. I couldn't find another
example that returns a 5** status code.


Note: AFAIK Es currently only uses 2**, 4**, and 5** status codes (200
OK, 201 Created, 202 Accepted, 204 No Content, 400 Bad Request, 401
Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 429 Too Many
Requests, 500 Internal Server Error, 503 Service Unavailable).
Practically only the success, warning, and danger badges will be used,
but added badges for the rest status codes in case they are added in Es.
This commit is contained in:
Elena Stoeva 2024-08-02 11:47:00 +01:00 committed by GitHub
parent 183a2ea8cb
commit 495bb30a44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 346 additions and 12 deletions

View file

@ -35,8 +35,10 @@ export const consoleOutputLexerRules: monaco.languages.IMonarchLanguage = {
matchTokensWithEOL('status.success', /\b2\d{2}(?: \w+)*$/, 'root'),
// Redirection messages (status codes 300 399)
matchTokensWithEOL('status.redirect', /\b3\d{2}(?: \w+)*$/, 'root'),
// Client and server error responses (status codes 400 599)
matchTokensWithEOL('status.error', /\b[4-5]\d{2}(?: \w+)*$/, 'root'),
// Client error responses (status codes 400 499)
matchTokensWithEOL('status.warning', /\b4\d{2}(?: \w+)*$/, 'root'),
// Server error responses (status codes 500 599)
matchTokensWithEOL('status.error', /\b5\d{2}(?: \w+)*$/, 'root'),
],
},
};

View file

@ -39,23 +39,23 @@ export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => {
),
...buildRuleGroup(
['status.info'],
makeHighContrastColor(euiThemeVars.euiColorWarningText)(background),
true
makeHighContrastColor(euiThemeVars.euiTextColor)(background)
),
...buildRuleGroup(
['status.success'],
makeHighContrastColor(euiThemeVars.euiColorSuccessText)(background),
true
makeHighContrastColor(euiThemeVars.euiTextColor)(euiThemeVars.euiColorSuccess)
),
...buildRuleGroup(
['status.redirect'],
makeHighContrastColor(euiThemeVars.euiColorWarningText)(background),
true
makeHighContrastColor(euiThemeVars.euiTextColor)(background)
),
...buildRuleGroup(
['status.warning'],
makeHighContrastColor(euiThemeVars.euiTextColor)(euiThemeVars.euiColorWarning)
),
...buildRuleGroup(
['status.error'],
makeHighContrastColor(euiThemeVars.euiColorDangerText)(background),
true
makeHighContrastColor('#FFFFFF')(euiThemeVars.euiColorDanger)
),
...buildRuleGroup(['method'], makeHighContrastColor(methodTextColor)(background)),
...buildRuleGroup(['url'], makeHighContrastColor(urlTextColor)(background)),

View file

@ -14,6 +14,7 @@ import Protobuf from 'pbf';
import { i18n } from '@kbn/i18n';
import { EuiScreenReaderOnly } from '@elastic/eui';
import { CONSOLE_THEME_ID, CONSOLE_OUTPUT_LANG_ID, monaco } from '@kbn/monaco';
import { getStatusCodeDecorations } from './utils';
import { useEditorReadContext, useRequestReadContext } from '../../../contexts';
import { convertMapboxVectorTileToJson } from '../legacy/console_editor/mapbox_vector_tile';
import {
@ -33,10 +34,12 @@ export const MonacoEditorOutput: FunctionComponent = () => {
const [mode, setMode] = useState('text');
const divRef = useRef<HTMLDivElement | null>(null);
const { setupResizeChecker, destroyResizeChecker } = useResizeCheckerUtils();
const lineDecorations = useRef<monaco.editor.IEditorDecorationsCollection | null>(null);
const editorDidMountCallback = useCallback(
(editor: monaco.editor.IStandaloneCodeEditor) => {
setupResizeChecker(divRef.current!, editor);
lineDecorations.current = editor.createDecorationsCollection();
},
[setupResizeChecker]
);
@ -46,6 +49,8 @@ export const MonacoEditorOutput: FunctionComponent = () => {
}, [destroyResizeChecker]);
useEffect(() => {
// Clean up any existing line decorations
lineDecorations.current?.clear();
if (data) {
const isMultipleRequest = data.length > 1;
setMode(
@ -73,6 +78,11 @@ export const MonacoEditorOutput: FunctionComponent = () => {
})
.join('\n')
);
if (isMultipleRequest) {
// If there are multiple responses, add decorations for their status codes
const decorations = getStatusCodeDecorations(data);
lineDecorations.current?.set(decorations);
}
} else {
setValue('');
}

View file

@ -13,6 +13,15 @@ import { i18n } from '@kbn/i18n';
*/
export const SELECTED_REQUESTS_CLASSNAME = 'console__monaco_editor__selectedRequests';
/*
* CSS class names used for the styling of multiple-response status codes
*/
export const PRIMARY_STATUS_BADGE_CLASSNAME = 'monaco__status_badge--primary';
export const SUCCESS_STATUS_BADGE_CLASSNAME = 'monaco__status_badge--success';
export const DEFAULT_STATUS_BADGE_CLASSNAME = 'monaco__status_badge--default';
export const WARNING_STATUS_BADGE_CLASSNAME = 'monaco__status_badge--warning';
export const DANGER_STATUS_BADGE_CLASSNAME = 'monaco__status_badge--danger';
export const whitespacesRegex = /\s+/;
export const newLineRegex = /\n/;
export const slashesRegex = /\/+/;

View file

@ -6,7 +6,15 @@
* Side Public License, v 1.
*/
export { AutocompleteType, SELECTED_REQUESTS_CLASSNAME } from './constants';
export {
AutocompleteType,
SELECTED_REQUESTS_CLASSNAME,
SUCCESS_STATUS_BADGE_CLASSNAME,
WARNING_STATUS_BADGE_CLASSNAME,
PRIMARY_STATUS_BADGE_CLASSNAME,
DEFAULT_STATUS_BADGE_CLASSNAME,
DANGER_STATUS_BADGE_CLASSNAME,
} from './constants';
export {
getRequestStartLineNumber,
getRequestEndLineNumber,
@ -25,3 +33,4 @@ export {
shouldTriggerSuggestions,
} from './autocomplete_utils';
export { getLineTokens, containsUrlParams } from './tokens_utils';
export { getStatusCodeDecorations } from './status_code_decoration_utils';

View file

@ -0,0 +1,197 @@
/*
* 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 { getStatusCodeDecorations } from './status_code_decoration_utils';
import {
SUCCESS_STATUS_BADGE_CLASSNAME,
WARNING_STATUS_BADGE_CLASSNAME,
DANGER_STATUS_BADGE_CLASSNAME,
} from './constants';
import { RequestResult } from '../../../../hooks/use_send_current_request/send_request';
describe('getStatusCodeDecorations', () => {
it('correctly returns all decorations on full data', () => {
// Sample multiple-response data returned from ES:
// 1 # GET _search 200 OK
// 2 {
// 3 "took": 1,
// 4 "timed_out": false,
// 5 "hits": {
// 6 "total": {
// 7 "value": 0,
// 8 "relation": "eq"
// 9 }
// 10 }
// 11 }
// 12 # GET _test 400 Bad Request
// 13 {
// 14 "error": {
// 15 "root_cause": [],
// 16 "status": 400
// 17 }
// 18 # PUT /library/_bulk 500 Internal Server Error
// 19 {
// 20 "error": {
// 21 "root_cause": [],
// 22 "status": 500
// 23 }
const SAMPLE_COMPLETE_DATA: RequestResult[] = [
{
response: {
timeMs: 50,
statusCode: 200,
statusText: 'OK',
contentType: 'application/json',
value:
'# GET _search 200 OK\n{\n"took": 1,\n"timed_out": false,\n"hits": {\n"total": {\n"value": 0,\n"relation": "eq"\n}\n}\n}',
},
request: {
data: '',
method: 'GET',
path: '_search',
},
},
{
response: {
timeMs: 22,
statusCode: 400,
statusText: 'Bad Request',
contentType: 'application/json',
value: '# GET _test 400 Bad Request\n{\n"error": {\n"root_cause": [],\n"status": 400\n}',
},
request: {
data: '',
method: 'GET',
path: '_test',
},
},
{
response: {
timeMs: 23,
statusCode: 500,
statusText: 'Internal Server Error',
contentType: 'application/json',
value:
'# PUT /library/_bulk 500 Internal Server Error\n{\n"error": {\n"root_cause": [],\n"status": 500\n}',
},
request: {
data: '',
method: 'PUT',
path: '/library/_bulk?refresh',
},
},
];
const EXPECTED_DECORATIONS = [
{
range: {
endColumn: 21,
endLineNumber: 1,
startColumn: 15,
startLineNumber: 1,
},
options: {
inlineClassName: SUCCESS_STATUS_BADGE_CLASSNAME,
},
},
{
range: {
endColumn: 28,
endLineNumber: 12,
startColumn: 13,
startLineNumber: 12,
},
options: {
inlineClassName: WARNING_STATUS_BADGE_CLASSNAME,
},
},
{
range: {
endColumn: 47,
endLineNumber: 18,
startColumn: 22,
startLineNumber: 18,
},
options: {
inlineClassName: DANGER_STATUS_BADGE_CLASSNAME,
},
},
];
expect(getStatusCodeDecorations(SAMPLE_COMPLETE_DATA)).toEqual(EXPECTED_DECORATIONS);
});
it('only returns decorations for data with complete status code and text', () => {
// This sample data is same as in previous test but some of it has incomplete status code or status text
const SAMPLE_INCOMPLETE_DATA: RequestResult[] = [
{
response: {
timeMs: 50,
// @ts-ignore
statusCode: undefined,
statusText: 'OK',
contentType: 'application/json',
value:
'# GET _search OK\n{\n"took": 1,\n"timed_out": false,\n"hits": {\n"total": {\n"value": 0,\n"relation": "eq"\n}\n}\n}',
},
request: {
data: '',
method: 'GET',
path: '_search',
},
},
{
response: {
timeMs: 22,
statusCode: 400,
statusText: 'Bad Request',
contentType: 'application/json',
value: '# GET _test 400 Bad Request\n{\n"error": {\n"root_cause": [],\n"status": 400\n}',
},
request: {
data: '',
method: 'GET',
path: '_test',
},
},
{
response: {
timeMs: 23,
// @ts-ignore
statusCode: undefined,
// @ts-ignore
statusText: undefined,
contentType: 'application/json',
value: '# PUT /library/_bulk\n{\n"error": {\n"root_cause": [],\n"status": 500\n}',
},
request: {
data: '',
method: 'PUT',
path: '/library/_bulk?refresh',
},
},
];
// Only the second response has complete status code and text
const EXPECTED_DECORATIONS = [
{
range: {
endColumn: 28,
endLineNumber: 12,
startColumn: 13,
startLineNumber: 12,
},
options: {
inlineClassName: WARNING_STATUS_BADGE_CLASSNAME,
},
},
];
expect(getStatusCodeDecorations(SAMPLE_INCOMPLETE_DATA)).toEqual(EXPECTED_DECORATIONS);
});
});

View file

@ -0,0 +1,65 @@
/*
* 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 { monaco } from '@kbn/monaco';
import { RequestResult } from '../../../../hooks/use_send_current_request/send_request';
import {
DEFAULT_STATUS_BADGE_CLASSNAME,
SUCCESS_STATUS_BADGE_CLASSNAME,
PRIMARY_STATUS_BADGE_CLASSNAME,
WARNING_STATUS_BADGE_CLASSNAME,
DANGER_STATUS_BADGE_CLASSNAME,
} from './constants';
const getStatusCodeClassName = (statusCode: number) => {
if (statusCode <= 199) {
return DEFAULT_STATUS_BADGE_CLASSNAME;
}
if (statusCode <= 299) {
return SUCCESS_STATUS_BADGE_CLASSNAME;
}
if (statusCode <= 399) {
return PRIMARY_STATUS_BADGE_CLASSNAME;
}
if (statusCode <= 499) {
return WARNING_STATUS_BADGE_CLASSNAME;
}
return DANGER_STATUS_BADGE_CLASSNAME;
};
export const getStatusCodeDecorations = (data: RequestResult[]) => {
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
let lastResponseEndLine = 0;
data.forEach(({ response }) => {
if (response?.value) {
const totalStatus =
response.statusCode && response.statusText
? response.statusCode + ' ' + response.statusText
: '';
const startColumn = (response.value as string).indexOf(totalStatus) + 1;
if (totalStatus && startColumn !== 0) {
const range = {
startLineNumber: lastResponseEndLine + 1,
startColumn,
endLineNumber: lastResponseEndLine + 1,
endColumn: startColumn + totalStatus.length,
};
decorations.push({
range,
options: {
inlineClassName: getStatusCodeClassName(response.statusCode),
},
});
}
lastResponseEndLine += (response.value as string).split(/\\n|\n/).length;
}
});
return decorations;
};

View file

@ -133,10 +133,52 @@
.console__monaco_editor__selectedRequests {
background: transparentize($euiColorPrimary, .9);
}
/*
* The styling for the multiple-response status code decorations
*/
%monaco__status_badge {
font-family: $euiFontFamily;
font-size: $euiFontSizeS;
font-weight: $euiFontWeightMedium;
line-height: $euiLineHeight;
padding: calc($euiSizeXS / 2) $euiSizeXS;
display: inline-block;
border-radius: calc($euiBorderRadius / 2);
white-space: nowrap;
vertical-align: top;
cursor: default;
max-width: 100%;
}
.monaco__status_badge--primary {
@extend %monaco__status_badge;
background-color: $euiColorVis1;
}
.monaco__status_badge--success {
@extend %monaco__status_badge;
background-color: $euiColorSuccess;
}
.monaco__status_badge--default {
@extend %monaco__status_badge;
background-color: $euiColorLightShade;
}
.monaco__status_badge--warning {
@extend %monaco__status_badge;
background-color: $euiColorWarning;
}
.monaco__status_badge--danger {
@extend %monaco__status_badge;
background-color: $euiColorDanger;
}
/*
* The z-index for the autocomplete suggestions popup
*/
.kibanaCodeEditor .monaco-editor .suggest-widget {
// the value needs to be above the z-index of the resizer bar
z-index: $euiZLevel1 + 2;