From 495bb30a4435202bc8d90b585db0cefbe1c9be7a Mon Sep 17 00:00:00 2001 From: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:47:00 +0100 Subject: [PATCH] [Console Monaco] Improve highlighting for status codes in multiple responses (#189210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/console/lexer_rules/console_output.ts | 6 +- packages/kbn-monaco/src/console/theme.ts | 16 +- .../editor/monaco/monaco_editor_output.tsx | 10 + .../editor/monaco/utils/constants.ts | 9 + .../containers/editor/monaco/utils/index.ts | 11 +- .../status_code_decoration_utils.test.ts | 197 ++++++++++++++++++ .../utils/status_code_decoration_utils.ts | 65 ++++++ src/plugins/console/public/styles/_app.scss | 44 +++- 8 files changed, 346 insertions(+), 12 deletions(-) create mode 100644 src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.test.ts create mode 100644 src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.ts diff --git a/packages/kbn-monaco/src/console/lexer_rules/console_output.ts b/packages/kbn-monaco/src/console/lexer_rules/console_output.ts index 8209820930910..697a594d0cd19 100644 --- a/packages/kbn-monaco/src/console/lexer_rules/console_output.ts +++ b/packages/kbn-monaco/src/console/lexer_rules/console_output.ts @@ -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'), ], }, }; diff --git a/packages/kbn-monaco/src/console/theme.ts b/packages/kbn-monaco/src/console/theme.ts index 3a90771354a53..4c9d4c2e03885 100644 --- a/packages/kbn-monaco/src/console/theme.ts +++ b/packages/kbn-monaco/src/console/theme.ts @@ -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)), diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx index dfbcfd7e4aa75..e1a8a692f40db 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx @@ -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(null); const { setupResizeChecker, destroyResizeChecker } = useResizeCheckerUtils(); + const lineDecorations = useRef(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(''); } diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts index 5d6088653f243..0f4664be48994 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts @@ -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 = /\/+/; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts index 069f99552222a..0997aa682b630 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts @@ -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'; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.test.ts new file mode 100644 index 0000000000000..c65f55c2fed35 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.test.ts @@ -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); + }); +}); diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.ts new file mode 100644 index 0000000000000..3a0724c874ca5 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/status_code_decoration_utils.ts @@ -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; +}; diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 161fd913c32ae..fdeaf67963fa1 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -133,10 +133,52 @@ .console__monaco_editor__selectedRequests { background: transparentize($euiColorPrimary, .9); } + /* - * The z-index for the autocomplete suggestions popup + * 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;