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;