[CodeEditor] Add grok highlighting (#159334)

## Summary

Close https://github.com/elastic/kibana/issues/155506

<img width="971" alt="Screenshot 2023-06-12 at 17 11 18"
src="7522bcba-e3aa-4229-93b2-27d12a335426">


I didn't introduce any custom colors here but just used built-in token
types for highlighting (`string` for red and `variable` for blue)
This commit is contained in:
Anton Dosov 2023-06-13 12:05:26 +02:00 committed by GitHub
parent 2b42341d04
commit e89313327e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 220 additions and 65 deletions

View file

@ -11,3 +11,4 @@ export { LANG as MarkdownLang } from './markdown/constants';
export { LANG as YamlLang } from './yaml/constants';
export { LANG as HandlebarsLang } from './handlebars/constants';
export { LANG as HJsonLang } from './hjson/constants';
export { LANG as GrokLang } from './grok/constants';

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export const LANG = 'grok';

View file

@ -0,0 +1,12 @@
/*
* 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 { LangModuleType } from '@kbn/monaco';
import { languageConfiguration, lexerRules } from './language';
export const Lang: LangModuleType = { ID: 'grok', languageConfiguration, lexerRules };

View file

@ -0,0 +1,117 @@
/*
* 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, registerLanguage } from '@kbn/monaco';
import { Lang } from '.';
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
window.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
registerLanguage(Lang);
});
test('lang', () => {
expect(monaco.editor.tokenize('\\[(?:-|%{NUMBER:bytes:int})\\]', 'grok')).toMatchInlineSnapshot(`
Array [
Array [
Token {
"language": "grok",
"offset": 0,
"type": "string.escape.grokEscape.grok",
},
Token {
"language": "grok",
"offset": 1,
"type": "source.grokEscaped.grok",
},
Token {
"language": "grok",
"offset": 2,
"type": "regexp.grokRegex.grok",
},
Token {
"language": "grok",
"offset": 5,
"type": "source.grok",
},
Token {
"language": "grok",
"offset": 6,
"type": "regexp.grokRegex.grok",
},
Token {
"language": "grok",
"offset": 7,
"type": "string.openGrok.grok",
},
Token {
"language": "grok",
"offset": 9,
"type": "variable.syntax.grok",
},
Token {
"language": "grok",
"offset": 15,
"type": "string.separator.grok",
},
Token {
"language": "grok",
"offset": 16,
"type": "variable.id.grok",
},
Token {
"language": "grok",
"offset": 21,
"type": "string.separator.grok",
},
Token {
"language": "grok",
"offset": 22,
"type": "variable.type.grok",
},
Token {
"language": "grok",
"offset": 25,
"type": "string.closeGrok.grok",
},
Token {
"language": "grok",
"offset": 26,
"type": "regexp.grokRegex.grok",
},
Token {
"language": "grok",
"offset": 27,
"type": "string.escape.grokEscape.grok",
},
Token {
"language": "grok",
"offset": 28,
"type": "source.grokEscaped.grok",
},
],
]
`);
});

View file

@ -0,0 +1,74 @@
/*
* 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';
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
// special grok symbols []()?:|
grokRegexSymbols: /[\[\]()?:|]/,
tokenizer: {
root: [
// %{SYNTAX}
[/(%\{)([^:}]+)(})/, ['string.openGrok', 'variable.syntax', 'string.closeGrok']],
// %{SYNTAX:ID}
[
/(%\{)([^:}]+)(:)([^:}]+)(})/,
[
'string.openGrok',
'variable.syntax',
'string.separator',
'variable.id',
'string.closeGrok',
],
],
// %{SYNTAX:ID:TYPE}
[
/(%\{)([^:}]+)(:)([^:}]+)(:)([^:}]+)(})/,
[
'string.openGrok',
'variable.syntax',
'string.separator',
'variable.id',
'string.separator',
'variable.type',
'string.closeGrok',
],
],
[/(\\)(@grokRegexSymbols)/, ['string.escape.grokEscape', 'source.grokEscaped']], // highlight escape symbol and don't highlight escaped symbol
[/@grokRegexSymbols/, 'regexp.grokRegex'], // highlight special grok symbols
],
},
};

View file

@ -11,5 +11,6 @@ import { Lang as HandlebarsLang } from './handlebars';
import { Lang as MarkdownLang } from './markdown';
import { Lang as YamlLang } from './yaml';
import { Lang as HJson } from './hjson';
import { Lang as GrokLang } from './grok';
export { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson };
export { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson, GrokLang };

View file

@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
import { registerLanguage } from '@kbn/monaco';
import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson } from './languages';
import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson, GrokLang } from './languages';
registerLanguage(CssLang);
registerLanguage(HandlebarsLang);
registerLanguage(MarkdownLang);
registerLanguage(YamlLang);
registerLanguage(HJson);
registerLanguage(GrokLang);

View file

@ -13,6 +13,7 @@ export {
YamlLang,
HandlebarsLang,
HJsonLang,
GrokLang,
CodeEditor,
CodeEditorField,
} from './code_editor';

View file

@ -9,7 +9,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
import { CodeEditor, GrokLang } from '@kbn/kibana-react-plugin/public';
export function PatternInput({ value, onChange }) {
return (
@ -21,7 +21,7 @@ export function PatternInput({ value, onChange }) {
data-test-subj="patternInput"
>
<CodeEditor
languageId="plaintext"
languageId={GrokLang}
value={value}
height={200}
options={{

View file

@ -67,42 +67,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(response).to.eql(testData);
});
// This test will need to be fixed.
it.skip('applies the correct CSS classes', async () => {
const grokPattern = '\\[(?:-|%{NUMBER:bytes:int})\\]';
await PageObjects.grokDebugger.setPatternInput(grokPattern);
const GROK_START = 'grokStart';
const GROK_PATTERN_NAME = 'grokPatternName';
const GROK_SEPARATOR = 'grokSeparator';
const GROK_FIELD_NAME = 'grokFieldName';
const GROK_FIELD_TYPE = 'grokFieldType';
const GROK_END = 'grokEnd';
const GROK_ESCAPE = 'grokEscape';
const GROK_ESCAPED = 'grokEscaped';
const GROK_REGEX = 'grokRegex';
await PageObjects.grokDebugger.assertPatternInputSyntaxHighlighting([
{ token: GROK_ESCAPE, content: '\\' },
{ token: GROK_ESCAPED, content: '[' },
{ token: GROK_REGEX, content: '(' },
{ token: GROK_REGEX, content: '?' },
{ token: GROK_REGEX, content: ':' },
{ token: GROK_REGEX, content: '|' },
{ token: GROK_START, content: '%{' },
{ token: GROK_PATTERN_NAME, content: 'NUMBER' },
{ token: GROK_SEPARATOR, content: ':' },
{ token: GROK_FIELD_NAME, content: 'bytes' },
{ token: GROK_SEPARATOR, content: ':' },
{ token: GROK_FIELD_TYPE, content: 'int' },
{ token: GROK_END, content: '}' },
{ token: GROK_REGEX, content: ')' },
{ token: GROK_ESCAPE, content: '\\' },
{ token: GROK_ESCAPED, content: ']' },
]);
});
after(async () => {
await security.testUser.restoreDefaults();
});

View file

@ -51,29 +51,4 @@ export class GrokDebuggerPageObject extends FtrService {
});
return value;
}
// This needs to be fixed to work with the new test functionality. This method is skipped currently.
async assertPatternInputSyntaxHighlighting(expectedHighlights: any[]) {
const patternInputElement = await this.testSubjects.find(
'grokDebuggerContainer > acePatternInput > codeEditorContainer'
);
const highlightedElements = await patternInputElement.findAllByXpath(
'.//div[@class="ace_line"]/*'
);
expect(highlightedElements.length).to.be(expectedHighlights.length);
await Promise.all(
highlightedElements.map(async (element: any, index) => {
const highlightClass = await element.getAttribute('class');
const highlightedContent = await element.getVisibleText();
const expectedHighlight = expectedHighlights[index];
const expectedHighlightClass = `ace_${expectedHighlight.token}`;
const expectedHighlightedContent = expectedHighlight.content;
expect(highlightClass).to.be(expectedHighlightClass);
expect(highlightedContent).to.be(expectedHighlightedContent);
})
);
}
}