Skip to content

Commit

Permalink
[Console Monaco] Improve highlighting for status codes in multiple re…
Browse files Browse the repository at this point in the history
…sponses (elastic#189210)

Closes elastic#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
elastic#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.
  • Loading branch information
ElenaStoeva authored Aug 2, 2024
1 parent 183a2ea commit 495bb30
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 12 deletions.
6 changes: 4 additions & 2 deletions packages/kbn-monaco/src/console/lexer_rules/console_output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
],
},
};
16 changes: 8 additions & 8 deletions packages/kbn-monaco/src/console/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]
);
Expand All @@ -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(
Expand Down Expand Up @@ -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('');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /\/+/;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,3 +33,4 @@ export {
shouldTriggerSuggestions,
} from './autocomplete_utils';
export { getLineTokens, containsUrlParams } from './tokens_utils';
export { getStatusCodeDecorations } from './status_code_decoration_utils';
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit 495bb30

Please sign in to comment.