[Console] Improve status code highlighting (#192888)

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

## Summary

This PR improves the status code highlighting when multiple requests are
sent.


https://github.com/user-attachments/assets/a0e8ba07-534d-4a48-aa7a-3964adc0e540


Dark mode:
<img width="1495" alt="Screenshot 2024-09-17 at 12 02 36"
src="https://github.com/user-attachments/assets/140e211d-57c2-48b7-abb8-09cfb4e24366">

All code statuses:
<img width="860" alt="Screenshot 2024-09-17 at 11 42 22"
src="https://github.com/user-attachments/assets/9509f9cd-ec60-44e2-b700-afaea595ad13">
This commit is contained in:
Elena Stoeva 2024-09-18 09:28:15 +01:00 committed by GitHub
parent e546302048
commit 61d0b7f0d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 141 additions and 165 deletions

View file

@ -25,21 +25,24 @@ export const consoleOutputLexerRules: monaco.languages.IMonarchLanguage = {
comments: [
// Line comment indicated by #
// Everything after the # character is matched, stopping right before the status code and status text at the end if they are present
matchTokensWithEOL('comment', /# .+?(?=\s+\d{3}(?: \w+)*$)/, 'root', 'status'),
matchTokensWithEOL('comment.default', /# .+?(?=\s+\[\b1\d{2}(?: \w+)*\]$)/, 'root', 'status'),
matchTokensWithEOL('comment.success', /# .+?(?=\s+\[\b2\d{2}(?: \w+)*\]$)/, 'root', 'status'),
matchTokensWithEOL('comment.primary', /# .+?(?=\s+\[\b3\d{2}(?: \w+)*\]$)/, 'root', 'status'),
matchTokensWithEOL('comment.warning', /# .+?(?=\s+\[\b4\d{2}(?: \w+)*\]$)/, 'root', 'status'),
matchTokensWithEOL('comment.danger', /# .+?(?=\s+\[\b5\d{2}(?: \w+)*\]$)/, 'root', 'status'),
...consoleSharedLexerRules.tokenizer.comments,
],
status: [
// Following HTTP response status codes conventions
// Informational responses (status codes 100 199)
matchTokensWithEOL('status.info', /\b1\d{2}(?: \w+)*$/, 'root'),
// Successful responses (status codes 200 299)
matchTokensWithEOL('status.success', /\b2\d{2}(?: \w+)*$/, 'root'),
// Redirection messages (status codes 300 399)
matchTokensWithEOL('status.redirect', /\b3\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'),
// Status codes 100 199
matchTokensWithEOL('status.default', /\[\b1\d{2}(?: \w+)*\]$/, 'root'),
// Status codes 200 299
matchTokensWithEOL('status.success', /\[\b2\d{2}(?: \w+)*\]$/, 'root'),
// Status codes 300 399
matchTokensWithEOL('status.primary', /\[\b3\d{2}(?: \w+)*\]$/, 'root'),
// Status codes 400 499
matchTokensWithEOL('status.warning', /\[\b4\d{2}(?: \w+)*\]$/, 'root'),
// Status codes 500 599
matchTokensWithEOL('status.danger', /\[\b5\d{2}(?: \w+)*\]$/, 'root'),
],
},
};

View file

@ -20,6 +20,11 @@ const background = euiThemeVars.euiFormBackgroundColor;
const booleanTextColor = '#585CF6';
const methodTextColor = '#DD0A73';
const urlTextColor = '#00A69B';
const defaultStatusBackgroundColor = darkMode ? '#191B20' : '#F7F8FA';
const successStatusBackgroundColor = darkMode ? '#212B30' : '#E7F5F5';
const primaryStatusBackgroundColor = darkMode ? '#1E232D' : '#EBF1F7';
const warningStatusBackgroundColor = darkMode ? '#2C2B25' : '#FBF6E9';
const dangerStatusBackgroundColor = darkMode ? '#2E2024' : '#F6E6E7';
export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => {
const euiTheme = darkMode ? buildDarkTheme() : buildLightTheme();
return {
@ -39,27 +44,56 @@ export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => {
makeHighContrastColor(euiThemeVars.euiColorAccentText)(background)
),
...buildRuleGroup(
['status.info'],
makeHighContrastColor(euiThemeVars.euiTextColor)(background)
['comment.default'],
makeHighContrastColor(euiThemeVars.euiTextColor)(defaultStatusBackgroundColor)
),
...buildRuleGroup(
['comment.success'],
makeHighContrastColor(euiThemeVars.euiColorSuccessText)(successStatusBackgroundColor)
),
...buildRuleGroup(
['comment.primary'],
makeHighContrastColor(euiThemeVars.euiTextColor)(primaryStatusBackgroundColor)
),
...buildRuleGroup(
['comment.warning'],
makeHighContrastColor(euiThemeVars.euiColorWarningText)(warningStatusBackgroundColor)
),
...buildRuleGroup(
['comment.danger'],
makeHighContrastColor(euiThemeVars.euiColorDangerText)(dangerStatusBackgroundColor)
),
...buildRuleGroup(
['status.default'],
makeHighContrastColor(euiThemeVars.euiTextColor)(defaultStatusBackgroundColor),
true
),
...buildRuleGroup(
['status.success'],
makeHighContrastColor(euiThemeVars.euiTextColor)(euiThemeVars.euiColorSuccess)
makeHighContrastColor(euiThemeVars.euiColorSuccessText)(successStatusBackgroundColor),
true
),
...buildRuleGroup(
['status.redirect'],
makeHighContrastColor(euiThemeVars.euiTextColor)(background)
['status.primary'],
makeHighContrastColor(euiThemeVars.euiTextColor)(primaryStatusBackgroundColor),
true
),
...buildRuleGroup(
['status.warning'],
makeHighContrastColor(euiThemeVars.euiTextColor)(euiThemeVars.euiColorWarning)
makeHighContrastColor(euiThemeVars.euiColorWarningText)(warningStatusBackgroundColor),
true
),
...buildRuleGroup(
['status.error'],
makeHighContrastColor('#FFFFFF')(euiThemeVars.euiColorDanger)
['status.danger'],
makeHighContrastColor(euiThemeVars.euiColorDangerText)(dangerStatusBackgroundColor),
true
),
...buildRuleGroup(['method'], makeHighContrastColor(methodTextColor)(background)),
...buildRuleGroup(['url'], makeHighContrastColor(urlTextColor)(background)),
],
colors: {
...euiTheme.colors,
'editorLineNumber.foreground': euiThemeVars.euiTextColor,
},
};
};

View file

@ -17,11 +17,7 @@ export const SELECTED_REQUESTS_CLASSNAME = 'console__monaco_editor__selectedRequ
/*
* 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 STATUS_CODE_LINE_CLASSNAME = 'monaco__status_code_line';
export const whitespacesRegex = /\s+/;
export const newLineRegex = /\n/;

View file

@ -10,11 +10,7 @@
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,
STATUS_CODE_LINE_CLASSNAME,
} from './constants';
export {
getRequestStartLineNumber,

View file

@ -8,13 +8,17 @@
*/
import { getStatusCodeDecorations } from './status_code_decoration_utils';
import {
SUCCESS_STATUS_BADGE_CLASSNAME,
WARNING_STATUS_BADGE_CLASSNAME,
DANGER_STATUS_BADGE_CLASSNAME,
} from './constants';
import { STATUS_CODE_LINE_CLASSNAME } from './constants';
import { RequestResult } from '../../../hooks/use_send_current_request/send_request';
const SUCCESS_STATUS_CODE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}--success`;
const WARNING_STATUS_CODE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}--warning`;
const DANGER_STATUS_CODE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}--danger`;
const SUCCESS_STATUS_CODE_LINE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}_number--success`;
const WARNING_STATUS_CODE_LINE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}_number--warning`;
const DANGER_STATUS_CODE_LINE_CLASSNAME = `${STATUS_CODE_LINE_CLASSNAME}_number--danger`;
describe('getStatusCodeDecorations', () => {
it('correctly returns all decorations on full data', () => {
// Sample multiple-response data returned from ES:
@ -91,108 +95,45 @@ describe('getStatusCodeDecorations', () => {
const EXPECTED_DECORATIONS = [
{
range: {
endColumn: 21,
endColumn: 1,
endLineNumber: 1,
startColumn: 15,
startColumn: 1,
startLineNumber: 1,
},
options: {
inlineClassName: SUCCESS_STATUS_BADGE_CLASSNAME,
isWholeLine: true,
blockClassName: SUCCESS_STATUS_CODE_CLASSNAME,
marginClassName: SUCCESS_STATUS_CODE_LINE_CLASSNAME,
},
},
{
range: {
endColumn: 28,
endColumn: 1,
endLineNumber: 12,
startColumn: 13,
startColumn: 1,
startLineNumber: 12,
},
options: {
inlineClassName: WARNING_STATUS_BADGE_CLASSNAME,
isWholeLine: true,
blockClassName: WARNING_STATUS_CODE_CLASSNAME,
marginClassName: WARNING_STATUS_CODE_LINE_CLASSNAME,
},
},
{
range: {
endColumn: 47,
endColumn: 1,
endLineNumber: 18,
startColumn: 22,
startColumn: 1,
startLineNumber: 18,
},
options: {
inlineClassName: DANGER_STATUS_BADGE_CLASSNAME,
isWholeLine: true,
blockClassName: DANGER_STATUS_CODE_CLASSNAME,
marginClassName: DANGER_STATUS_CODE_LINE_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

@ -9,28 +9,22 @@
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';
import { STATUS_CODE_LINE_CLASSNAME } from './constants';
const getStatusCodeClassName = (statusCode: number) => {
const getStatusCodeClassNameSuffix = (statusCode: number) => {
if (statusCode <= 199) {
return DEFAULT_STATUS_BADGE_CLASSNAME;
return '--default';
}
if (statusCode <= 299) {
return SUCCESS_STATUS_BADGE_CLASSNAME;
return '--success';
}
if (statusCode <= 399) {
return PRIMARY_STATUS_BADGE_CLASSNAME;
return '--primary';
}
if (statusCode <= 499) {
return WARNING_STATUS_BADGE_CLASSNAME;
return '--warning';
}
return DANGER_STATUS_BADGE_CLASSNAME;
return '--danger';
};
export const getStatusCodeDecorations = (data: RequestResult[]) => {
@ -39,25 +33,21 @@ export const getStatusCodeDecorations = (data: RequestResult[]) => {
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),
},
});
}
const range = {
startLineNumber: lastResponseEndLine + 1,
startColumn: 1,
endLineNumber: lastResponseEndLine + 1,
endColumn: 1, // It doesn't matter what endColumn we set as the decoration will be applied to the whole line
};
const classNameSuffix = getStatusCodeClassNameSuffix(response.statusCode);
decorations.push({
range,
options: {
isWholeLine: true,
blockClassName: `${STATUS_CODE_LINE_CLASSNAME}${classNameSuffix}`,
marginClassName: `${STATUS_CODE_LINE_CLASSNAME}_number${classNameSuffix}`,
},
});
lastResponseEndLine += (response.value as string).split(/\\n|\n/).length;
}
});

View file

@ -130,7 +130,7 @@ export function sendRequest(args: RequestArgs): Promise<RequestResult[]> {
if (isMultiRequest) {
const lineNumber = req.lineNumber ? `${req.lineNumber}: ` : '';
value = `# ${lineNumber}${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`;
value = `# ${lineNumber}${req.method} ${req.url} [${statusCode} ${statusText}]\n${value}`;
}
results.push({
@ -164,7 +164,8 @@ export function sendRequest(args: RequestArgs): Promise<RequestResult[]> {
}
if (isMultiRequest) {
value = `# ${req.method} ${req.url} ${statusCode} ${statusText}\n${value}`;
const lineNumber = req.lineNumber ? `${req.lineNumber}: ` : '';
value = `# ${lineNumber}${req.method} ${req.url} [${statusCode} ${statusText}]\n${value}`;
}
const result = {

View file

@ -203,29 +203,44 @@
max-width: 100%;
}
.monaco__status_badge--primary {
@extend %monaco__status_badge;
background-color: $euiColorVis1;
.monaco__status_code_line--primary {
background-color: transparentize($euiColorVis1, .9);
}
.monaco__status_badge--success {
@extend %monaco__status_badge;
background-color: $euiColorSuccess;
.monaco__status_code_line_number--primary {
background-color: transparentize($euiColorVis1, .5);
}
.monaco__status_badge--default {
@extend %monaco__status_badge;
background-color: $euiColorLightShade;
.monaco__status_code_line--success {
background-color: transparentize($euiColorSuccess, .9);
}
.monaco__status_badge--warning {
@extend %monaco__status_badge;
background-color: $euiColorWarning;
.monaco__status_code_line_number--success {
background-color: transparentize($euiColorSuccess, .5);
}
.monaco__status_badge--danger {
@extend %monaco__status_badge;
background-color: $euiColorDanger;
.monaco__status_code_line--default {
background-color: transparentize($euiColorLightShade, .9);
}
.monaco__status_code_line_number--default {
background-color: transparentize($euiColorLightShade, .5);
}
.monaco__status_code_line--warning {
background-color: transparentize($euiColorWarning, .9);
}
.monaco__status_code_line_number--warning {
background-color: transparentize($euiColorWarning, .5);
}
.monaco__status_code_line--danger {
background-color: transparentize($euiColorDanger, .9);
}
.monaco__status_code_line_number--danger {
background-color: transparentize($euiColorDanger, .5);
}
/*

View file

@ -203,8 +203,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
const response = await PageObjects.console.getOutputText();
log.debug(response);
expect(response).to.contain('# 2: PUT test-index 200');
expect(response).to.contain('# 3: DELETE test-index 200');
expect(response).to.contain('# 2: PUT test-index [200 OK]');
expect(response).to.contain('# 3: DELETE test-index [200 OK]');
});
});

View file

@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await sendMultipleRequests(['\n GET /_search?pretty', '\n GET /_search?pretty']);
const response = await PageObjects.console.getOutputText();
expect(response).to.contain('# 2: GET /_search?pretty');
expect(response).to.contain('# 2: GET /_search?pretty [200 OK]');
});
it('should clear the console output', async () => {