From 1e31d539261e17be6725523ff064ea485a78cff1 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 11 Nov 2021 18:52:57 +0300 Subject: [PATCH] [Vega] Replace EUICodeEditor with Monaco (#116041) Closes: #106967 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/code_editor/languages/constants.ts | 1 + .../code_editor/languages/hjson/constants.ts | 9 ++ .../code_editor/languages/hjson/index.ts | 13 ++ .../code_editor/languages/hjson/language.ts | 90 ++++++++++++++ .../public/code_editor/languages/index.ts | 3 +- .../public/code_editor/register_languages.ts | 3 +- src/plugins/vis_types/vega/kibana.json | 2 +- .../vega/public/components/vega_editor.scss | 20 ++- .../public/components/vega_vis_editor.tsx | 114 +++++++++++------- .../page_objects/vega_chart_page.ts | 49 ++------ test/functional/services/monaco_editor.ts | 3 +- 11 files changed, 216 insertions(+), 91 deletions(-) create mode 100644 src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts create mode 100644 src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts create mode 100644 src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts diff --git a/src/plugins/kibana_react/public/code_editor/languages/constants.ts b/src/plugins/kibana_react/public/code_editor/languages/constants.ts index af80e4ccc56e226..510cc91cf5e76c4 100644 --- a/src/plugins/kibana_react/public/code_editor/languages/constants.ts +++ b/src/plugins/kibana_react/public/code_editor/languages/constants.ts @@ -10,3 +10,4 @@ export { LANG as CssLang } from './css/constants'; 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'; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts new file mode 100644 index 000000000000000..61e851e0b6f583f --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/constants.ts @@ -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 = 'hjson'; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts new file mode 100644 index 000000000000000..ff3c08267da9bb1 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/index.ts @@ -0,0 +1,13 @@ +/* + * 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'; +import { LANG } from './constants'; + +export const Lang: LangModuleType = { ID: LANG, languageConfiguration, lexerRules }; diff --git a/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts b/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts new file mode 100644 index 000000000000000..d93cdfe4c4a22b8 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/languages/hjson/language.ts @@ -0,0 +1,90 @@ +/* + * 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: '"', notIn: ['string'] }, + ], + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, +}; + +export const lexerRules: monaco.languages.IMonarchLanguage = { + defaultToken: '', + tokenPostfix: '', + escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, + digits: /-?(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:[eE][+-]?\d+)?)?/, + symbols: /[,:]+/, + tokenizer: { + root: [ + [/(@digits)n?/, 'number'], + [/(@symbols)n?/, 'delimiter'], + + { include: '@keyword' }, + { include: '@url' }, + { include: '@whitespace' }, + { include: '@brackets' }, + { include: '@keyName' }, + { include: '@string' }, + ], + + keyword: [[/(?:true|false|null)\b/, 'keyword']], + + url: [ + [ + /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/, + 'string', + ], + ], + + keyName: [[/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*(?=:)/, 'variable']], + + brackets: [[/{/, '@push'], [/}/, '@pop'], [/[[(]/], [/[\])]/]], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], + + string: [ + [/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*/, 'string'], + [/"""/, 'string', '@stringLiteral'], + [/"/, 'string', '@stringDouble'], + ], + + stringDouble: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + + stringLiteral: [ + [/"""/, 'string', '@pop'], + [/\\""""/, 'string', '@pop'], + [/./, 'string'], + ], + }, +} as monaco.languages.IMonarchLanguage; diff --git a/src/plugins/kibana_react/public/code_editor/languages/index.ts b/src/plugins/kibana_react/public/code_editor/languages/index.ts index b797ea44d1f9195..f862997fdc2e3ca 100644 --- a/src/plugins/kibana_react/public/code_editor/languages/index.ts +++ b/src/plugins/kibana_react/public/code_editor/languages/index.ts @@ -10,5 +10,6 @@ import { Lang as CssLang } from './css'; import { Lang as HandlebarsLang } from './handlebars'; import { Lang as MarkdownLang } from './markdown'; import { Lang as YamlLang } from './yaml'; +import { Lang as HJson } from './hjson'; -export { CssLang, HandlebarsLang, MarkdownLang, YamlLang }; +export { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson }; diff --git a/src/plugins/kibana_react/public/code_editor/register_languages.ts b/src/plugins/kibana_react/public/code_editor/register_languages.ts index a32318a7e4b20f7..62eccdabb5d98e5 100644 --- a/src/plugins/kibana_react/public/code_editor/register_languages.ts +++ b/src/plugins/kibana_react/public/code_editor/register_languages.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ import { registerLanguage } from '@kbn/monaco'; -import { CssLang, HandlebarsLang, MarkdownLang, YamlLang } from './languages'; +import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson } from './languages'; registerLanguage(CssLang); registerLanguage(HandlebarsLang); registerLanguage(MarkdownLang); registerLanguage(YamlLang); +registerLanguage(HJson); diff --git a/src/plugins/vis_types/vega/kibana.json b/src/plugins/vis_types/vega/kibana.json index 1a499e284c1a806..cedd73cc6d3989e 100644 --- a/src/plugins/vis_types/vega/kibana.json +++ b/src/plugins/vis_types/vega/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector"], "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_types/vega/public/components/vega_editor.scss b/src/plugins/vis_types/vega/public/components/vega_editor.scss index 709aaa2030f68da..4381c0097f96d53 100644 --- a/src/plugins/vis_types/vega/public/components/vega_editor.scss +++ b/src/plugins/vis_types/vega/public/components/vega_editor.scss @@ -1,18 +1,28 @@ .visEditor--vega { .visEditorSidebar__config { padding: 0; + display: flex; + flex-direction: row; + overflow: hidden; + + min-height: $euiSize * 15; + + @include euiBreakpoint('xs', 's', 'm') { + max-height: $euiSize * 15; + } } } .vgaEditor { - @include euiBreakpoint('xs', 's', 'm') { - @include euiScrollBar; - max-height: $euiSize * 15; - overflow-y: auto; + width: 100%; + flex-grow: 1; + + .kibanaCodeEditor { + width: 100%; } } -.vgaEditor__aceEditorActions { +.vgaEditor__editorActions { position: absolute; z-index: $euiZLevel1; top: $euiSizeS; diff --git a/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx index d2f586eac988547..46e5e331c94554e 100644 --- a/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_types/vega/public/components/vega_vis_editor.tsx @@ -6,14 +6,16 @@ * Side Public License, v 1. */ -import React, { useCallback } from 'react'; -import compactStringify from 'json-stringify-pretty-compact'; +import { XJsonLang } from '@kbn/monaco'; +import useMount from 'react-use/lib/useMount'; import hjson from 'hjson'; -import 'brace/mode/hjson'; + +import React, { useCallback, useState } from 'react'; +import compactStringify from 'json-stringify-pretty-compact'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { EuiCodeEditor } from '../../../../es_ui_shared/public'; +import { CodeEditor, HJsonLang } from '../../../../kibana_react/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; @@ -21,20 +23,6 @@ import { VegaActionsMenu } from './vega_actions_menu'; import './vega_editor.scss'; -const aceOptions = { - maxLines: Infinity, - highlightActiveLine: false, - showPrintMargin: false, - tabSize: 2, - useSoftTabs: true, - wrap: true, -}; - -const hjsonStringifyOptions = { - bracesSameLine: true, - keepWsc: true, -}; - function format( value: string, stringify: typeof hjson.stringify | typeof compactStringify, @@ -42,7 +30,11 @@ function format( ) { try { const spec = hjson.parse(value, { legacyRoot: false, keepWsc: true }); - return stringify(spec, options); + + return { + value: stringify(spec, options), + isValid: true, + }; } catch (err) { // This is a common case - user tries to format an invalid HJSON text getNotifications().toasts.addError(err, { @@ -51,44 +43,82 @@ function format( }), }); - return value; + return { value, isValid: false }; } } function VegaVisEditor({ stateParams, setValue }: VisEditorOptionsProps) { - const onChange = useCallback( - (value: string) => { + const [languageId, setLanguageId] = useState(); + + useMount(() => { + let specLang = XJsonLang.ID; + try { + JSON.parse(stateParams.spec); + } catch { + specLang = HJsonLang; + } + setLanguageId(specLang); + }); + + const setSpec = useCallback( + (value: string, specLang?: string) => { setValue('spec', value); + if (specLang) { + setLanguageId(specLang); + } }, [setValue] ); - const formatJson = useCallback( - () => setValue('spec', format(stateParams.spec, compactStringify)), - [setValue, stateParams.spec] - ); + const onChange = useCallback((value: string) => setSpec(value), [setSpec]); - const formatHJson = useCallback( - () => setValue('spec', format(stateParams.spec, hjson.stringify, hjsonStringifyOptions)), - [setValue, stateParams.spec] - ); + const formatJson = useCallback(() => { + const { value, isValid } = format(stateParams.spec, compactStringify); + + if (isValid) { + setSpec(value, XJsonLang.ID); + } + }, [setSpec, stateParams.spec]); + + const formatHJson = useCallback(() => { + const { value, isValid } = format(stateParams.spec, hjson.stringify, { + bracesSameLine: true, + keepWsc: true, + }); + + if (isValid) { + setSpec(value, HJsonLang); + } + }, [setSpec, stateParams.spec]); + + if (!languageId) { + return null; + } return ( -
- -
+
+
+
); } diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts index f83c5e193034eb1..045e5eedb86f016 100644 --- a/test/functional/page_objects/vega_chart_page.ts +++ b/test/functional/page_objects/vega_chart_page.ts @@ -19,6 +19,7 @@ export class VegaChartPageObject extends FtrService { private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly browser = this.ctx.getService('browser'); private readonly retry = this.ctx.getService('retry'); + private readonly monacoEditor = this.ctx.getService('monacoEditor'); public getEditor() { return this.testSubjects.find('vega-editor'); @@ -36,63 +37,31 @@ export class VegaChartPageObject extends FtrService { return this.find.byCssSelector('[aria-label^="Y-axis"]'); } - public async getAceGutterContainer() { - const editor = await this.getEditor(); - return editor.findByClassName('ace_gutter'); - } - - public async getRawSpec() { - // Adapted from console_page.js:getVisibleTextFromAceEditor(). Is there a common utilities file? - const editor = await this.getEditor(); - const lines = await editor.findAllByClassName('ace_line_group'); - - return await Promise.all( - lines.map(async (line) => { - return await line.getVisibleText(); - }) - ); - } - public async getSpec() { - return (await this.getRawSpec()).join('\n'); - } - - public async focusEditor() { - const editor = await this.getEditor(); - const textarea = await editor.findByClassName('ace_content'); - - await textarea.click(); + return this.monacoEditor.getCodeEditorValue(); } public async fillSpec(newSpec: string) { await this.retry.try(async () => { await this.cleanSpec(); - await this.focusEditor(); - await this.browser.pressKeys(newSpec); + await this.monacoEditor.setCodeEditorValue(newSpec); expect(compareSpecs(await this.getSpec(), newSpec)).to.be(true); }); } public async typeInSpec(text: string) { - const aceGutter = await this.getAceGutterContainer(); + const editor = await this.testSubjects.find('vega-editor'); + const textarea = await editor.findByCssSelector('textarea'); - await aceGutter.doubleClick(); + await textarea.focus(); + await this.browser.pressKeys(this.browser.keys.RIGHT); await this.browser.pressKeys(this.browser.keys.RIGHT); - await this.browser.pressKeys(this.browser.keys.LEFT); - await this.browser.pressKeys(this.browser.keys.LEFT); - await this.browser.pressKeys(text); + await textarea.type(text); } public async cleanSpec() { - const aceGutter = await this.getAceGutterContainer(); - - await this.retry.try(async () => { - await aceGutter.doubleClick(); - await this.browser.pressKeys(this.browser.keys.BACK_SPACE); - - expect(await this.getSpec()).to.be(''); - }); + await this.monacoEditor.setCodeEditorValue(''); } public async getYAxisLabels() { diff --git a/test/functional/services/monaco_editor.ts b/test/functional/services/monaco_editor.ts index 63a5a7105ddb8a0..2cad852ac16b53e 100644 --- a/test/functional/services/monaco_editor.ts +++ b/test/functional/services/monaco_editor.ts @@ -31,7 +31,8 @@ export class MonacoEditorService extends FtrService { public async typeCodeEditorValue(value: string, testSubjId: string) { const editor = await this.testSubjects.find(testSubjId); const textarea = await editor.findByCssSelector('textarea'); - textarea.type(value); + + await textarea.type(value); } public async setCodeEditorValue(value: string, nthIndex = 0) {