[Shared UX] Migrate code editor from kibana_react plugin to shared_ux package (#148550)

This commit is contained in:
Rachel Shen 2023-01-30 15:13:38 -07:00 committed by GitHub
parent e07a65ef05
commit 58cd6370a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 3238 additions and 1 deletions

5
.github/CODEOWNERS vendored
View file

@ -114,7 +114,7 @@
### Kibana React (to be deprecated)
/src/plugins/kibana_react/ @elastic/appex-sharedux
/src/plugins/kibana_react/public/code_editor @elastic/appex-sharedux @elastic/kibana-presentation
/src/plugins/kibana_react/public/@elastic/appex-sharedux @elastic/kibana-presentation
### Home Plugin and Packages
/src/plugins/home/public @elastic/appex-sharedux
@ -1040,6 +1040,9 @@ packages/shared-ux/button/exit_full_screen/types @elastic/appex-sharedux
packages/shared-ux/card/no_data/impl @elastic/appex-sharedux
packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux
packages/shared-ux/card/no_data/types @elastic/appex-sharedux
packages/shared-ux/code_editor/impl @elastic/shared-ux
packages/shared-ux/code_editor/mocks @elastic/shared-ux
packages/shared-ux/code_editor/types @elastic/shared-ux
packages/shared-ux/file/context @elastic/appex-sharedux
packages/shared-ux/file/file_picker/impl @elastic/appex-sharedux
packages/shared-ux/file/file_upload/impl @elastic/appex-sharedux

View file

@ -144,6 +144,9 @@
"@kbn/cell-actions": "link:packages/kbn-cell-actions",
"@kbn/chart-expressions-common": "link:src/plugins/chart_expressions/common",
"@kbn/chart-icons": "link:packages/kbn-chart-icons",
"@kbn/code-editor": "link:packages/shared-ux/code_editor/impl",
"@kbn/code-editor-mocks": "link:packages/shared-ux/code_editor/mocks",
"@kbn/code-editor-types": "link:packages/shared-ux/code_editor/types",
"@kbn/coloring": "link:packages/kbn-coloring",
"@kbn/config": "link:packages/kbn-config",
"@kbn/config-mocks": "link:packages/kbn-config-mocks",

View file

@ -0,0 +1,24 @@
---
id: sharedUX/Components/CodeEditor
slug: /shared-ux/components/code-editor
title: Code Editor
description: A code editor to display code and edit code in Kibana.
tags: ['shared-ux', 'component']
date: 2022-12-05
---
## Description
This component is an abstraction of the [Monaco Code Editor](https://microsoft.github.io/monaco-editor/) (and the [React Monaco Editor component](https://github.com/react-monaco-editor/react-monaco-editor)). This component still allows access to the other Monaco features.
## Usage
This editor component allows easy access to:
* [Syntax highlighting (including custom language highlighting)](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages)
* [Suggestion/autocompletion widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example)
* Function signature widget
* [Hover widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-hover-provider-example)
The Monaco editor doesn't automatically resize the editor area on window or container resize so this component includes a [resize detector](https://github.com/maslianok/react-resize-detector) to cause the Monaco editor to re-layout and adjust its size when the window or container size changes
## API

View file

@ -0,0 +1,396 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CodeEditor /> hint element should be tabable 1`] = `
<div
aria-label="Code Editor"
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop).,You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
data-test-subj="codeEditorHint"
id="1234"
role="button"
tabindex="0"
/>
`;
exports[`<CodeEditor /> is rendered 1`] = `
<CodeEditor
height={250}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
languageId="loglang"
onChange={[Function]}
value="
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
"
>
<div
css={
Object {
"map": undefined,
"name": "1dubd8m",
"next": undefined,
"styles": "
{
position: relative;
height: 100%;
}
",
"toString": [Function],
}
}
onKeyDown={[Function]}
>
<EuiToolTip
content={
<React.Fragment>
<p>
<FormattedMessage
css={
Object {
"map": undefined,
"name": "1dubd8m",
"next": undefined,
"styles": "
{
position: relative;
height: 100%;
}
",
"toString": [Function],
}
}
defaultMessage="Press {key} to start editing."
id="sharedUXPackages.codeEditor.startEditing"
values={
Object {
"key": <strong>
Enter
</strong>,
}
}
/>
</p>
<p>
<FormattedMessage
css={
Object {
"map": undefined,
"name": "1dubd8m",
"next": undefined,
"styles": "
{
position: relative;
height: 100%;
}
",
"toString": [Function],
}
}
defaultMessage="Press {key} to stop editing."
id="sharedUXPackages.codeEditor.stopEditing"
values={
Object {
"key": <strong>
Esc
</strong>,
}
}
/>
</p>
</React.Fragment>
}
delay="regular"
display="block"
position="top"
>
<EuiToolTipAnchor
display="block"
id="generated-id"
isVisible={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
css="unknown styles"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"uuw4g3-euiToolTipAnchor-block": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-block{display:block;}
</style>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-block *[disabled]{pointer-events:none;}
</style>
<style
data-styled="active"
data-styled-version="5.1.0"
/>
</head>,
"ctr": 2,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-block{display:block;}
</style>,
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-block *[disabled]{pointer-events:none;}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "uuw4g3-euiToolTipAnchor-block",
"next": undefined,
"styles": "*[disabled]{pointer-events:none;};label:euiToolTipAnchor;;;display:block;label:block;;;",
"toString": [Function],
}
}
/>
<span
className="euiToolTipAnchor emotion-euiToolTipAnchor-block"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
aria-label="Code Editor"
css={
Array [
Object {
"map": undefined,
"name": "1dubd8m",
"next": undefined,
"styles": "
{
position: relative;
height: 100%;
}
",
"toString": [Function],
},
Object {
"map": undefined,
"name": "7fzoim",
"next": undefined,
"styles": "
{
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
&:focus {
z-index: 6000;
}
&--isInactive {
display: none;
}
}
",
"toString": [Function],
},
]
}
data-test-subj="codeEditorHint"
id="1234"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/>
</span>
</span>
</EuiToolTipAnchor>
</EuiToolTip>
<Component>
<mockMonacoEditor
editorDidMount={[Function]}
editorWillMount={[Function]}
height="100px"
language="loglang"
onChange={[Function]}
options={
Object {
"fontFamily": "Roboto Mono",
"fontSize": 12,
"lineHeight": 21,
"matchBrackets": "never",
"minimap": Object {
"enabled": false,
},
"padding": Object {},
"renderLineHighlight": "none",
"scrollBeyondLastLine": false,
"scrollbar": Object {
"alwaysConsumeMouseWheel": false,
"useShadows": false,
},
"wordBasedSuggestions": false,
"wordWrap": "on",
"wrappingIndent": "indent",
}
}
theme="euiColors"
value="
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
"
>
<div>
<div />
<textarea
data-test-subj="monacoEditorTextarea"
onKeyDown={[Function]}
/>
</div>
</mockMonacoEditor>
</Component>
</div>
</CodeEditor>
`;

View file

@ -0,0 +1,201 @@
/*
* 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 React from 'react';
import { action } from '@storybook/addon-actions';
import { CodeEditorStorybookMock, CodeEditorStorybookParams } from '@kbn/code-editor-mocks';
import { monaco as monacoEditor } from '@kbn/monaco';
import mdx from './README.mdx';
import { CodeEditor } from './code_editor';
export default {
title: 'Code Editor/Code Editor',
description: 'A code editor',
parameters: {
docs: {
page: mdx,
},
},
};
const mock = new CodeEditorStorybookMock();
const argTypes = mock.getArgumentTypes();
export const Basic = (params: CodeEditorStorybookParams) => {
return (
<CodeEditor {...params} languageId="plainText" onChange={action('on change')} value="Hello!" />
);
};
Basic.argTypes = argTypes;
// A sample language definition with a few example tokens
// Taken from https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
const simpleLogLang: monacoEditor.languages.IMonarchLanguage = {
tokenizer: {
root: [
[/\[error.*/, 'constant'],
[/\[notice.*/, 'variable'],
[/\[info.*/, 'string'],
[/\[[a-zA-Z 0-9:]+\]/, 'tag'],
],
},
};
monacoEditor.languages.register({ id: 'loglang' });
monacoEditor.languages.setMonarchTokensProvider('loglang', simpleLogLang);
const logs = `[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
`;
export const CustomLogLanguage = (params: CodeEditorStorybookParams) => {
return (
<div>
<CodeEditor
{...params}
languageId="loglang"
height={250}
value={logs}
options={{
minimap: {
enabled: false,
},
}}
/>
</div>
);
};
CustomLogLanguage.argTypes = argTypes;
export const JSONSupport = () => {
return (
<div>
<CodeEditor
languageId="json"
editorDidMount={(editor) => {
monacoEditor.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
{
uri: editor.getModel()?.uri.toString() ?? '',
fileMatch: ['*'],
schema: {
type: 'object',
properties: {
version: {
enum: ['v1', 'v2'],
},
},
},
},
],
});
}}
height={250}
value="{}"
onChange={action('onChange')}
/>
</div>
);
};
export const SuggestionProvider = () => {
const provideSuggestions = (
model: monacoEditor.editor.ITextModel,
position: monacoEditor.Position,
context: monacoEditor.languages.CompletionContext
) => {
const wordRange = new monacoEditor.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column
);
return {
suggestions: [
{
label: 'Hello, World',
kind: monacoEditor.languages.CompletionItemKind.Variable,
documentation: {
value: '*Markdown* can be used in autocomplete help',
isTrusted: true,
},
insertText: 'Hello, World',
range: wordRange,
},
{
label: 'You know, for search',
kind: monacoEditor.languages.CompletionItemKind.Variable,
documentation: { value: 'Thanks `Monaco`', isTrusted: true },
insertText: 'You know, for search',
range: wordRange,
},
],
};
};
return (
<div>
<CodeEditor
languageId="loglang"
height={250}
value={logs}
onChange={action('onChange')}
suggestionProvider={{
triggerCharacters: ['.'],
provideCompletionItems: provideSuggestions,
}}
options={{
quickSuggestions: true,
}}
/>
</div>
);
};
export const HoverProvider = () => {
const provideHover = (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
const word = model.getWordAtPosition(position);
if (!word) {
return {
contents: [],
};
}
return {
contents: [
{
value: `You're hovering over **${word.word}**`,
},
],
};
};
return (
<div>
<CodeEditor
languageId="loglang"
height={250}
value={logs}
onChange={action('onChange')}
hoverProvider={{
provideHover,
}}
/>
</div>
);
};

View file

@ -0,0 +1,121 @@
/*
* 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 React, { useEffect, KeyboardEventHandler } from 'react';
import { monaco } from '@kbn/monaco';
function createEditorInstance() {
const keyDownListeners: Array<(e?: unknown) => void> = [];
const didShowListeners: Array<(e?: unknown) => void> = [];
const didHideListeners: Array<(e?: unknown) => void> = [];
let placeholderDiv: undefined | HTMLDivElement;
let areSuggestionsVisible = false;
const editorInstance = {
// Mock monaco editor API
getContribution: jest.fn((id: string) => {
if (id === 'editor.contrib.suggestController') {
return {
widget: {
value: {
onDidShow: jest.fn((listener) => {
didShowListeners.push(listener);
}),
onDidHide: jest.fn((listener) => {
didHideListeners.push(listener);
}),
},
},
};
}
}),
focus: jest.fn(),
onDidBlurEditorText: jest.fn(),
onKeyDown: jest.fn((listener) => {
keyDownListeners.push(listener);
}),
addContentWidget: jest.fn((widget: monaco.editor.IContentWidget) => {
placeholderDiv?.appendChild(widget.getDomNode());
}),
applyFontInfo: jest.fn(),
removeContentWidget: jest.fn((widget: monaco.editor.IContentWidget) => {
placeholderDiv?.removeChild(widget.getDomNode());
}),
getDomNode: jest.fn(),
// Helpers for our tests
__helpers__: {
areSuggestionsVisible: () => areSuggestionsVisible,
getPlaceholderRef: (div: HTMLDivElement) => {
placeholderDiv = div;
},
onTextareaKeyDown: ((e) => {
// Let all our listener know that a key has been pressed on the textarea
keyDownListeners.forEach((listener) => listener(e));
// Close the suggestions when hitting the ESC key
if (e.keyCode === monaco.KeyCode.Escape && areSuggestionsVisible) {
editorInstance.__helpers__.hideSuggestions();
}
}) as KeyboardEventHandler,
showSuggestions: () => {
areSuggestionsVisible = true;
didShowListeners.forEach((listener) => listener());
},
hideSuggestions: () => {
areSuggestionsVisible = false;
didHideListeners.forEach((listener) => listener());
},
},
};
return editorInstance;
}
type MockedEditor = ReturnType<typeof createEditorInstance>;
export const mockedEditorInstance: MockedEditor = createEditorInstance();
// <MonacoEditor /> mock
const mockMonacoEditor = ({
editorWillMount,
editorDidMount,
}: Record<string, (...args: unknown[]) => void>) => {
editorWillMount(monaco);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
editorDidMount(mockedEditorInstance, monaco);
}, [editorDidMount]);
return (
<div>
<div ref={mockedEditorInstance?.__helpers__.getPlaceholderRef} />
<textarea
onKeyDown={mockedEditorInstance?.__helpers__.onTextareaKeyDown}
data-test-subj="monacoEditorTextarea"
/>
</div>
);
};
jest.mock('react-monaco-editor', () => {
return function JestMockEditor() {
return mockMonacoEditor;
};
});
// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
htmlIdGenerator: () => {
return () => '1234';
},
};
});

View file

@ -0,0 +1,224 @@
/*
* 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 React from 'react';
import { ReactWrapper } from 'enzyme';
import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers';
import { monaco } from '@kbn/monaco';
import { keys } from '@elastic/eui';
import { mockedEditorInstance } from './code_editor.test.helpers';
import { CodeEditor } from './code_editor';
// A sample language definition with a few example tokens
const simpleLogLang: monaco.languages.IMonarchLanguage = {
tokenizer: {
root: [
[/\[error.*/, 'constant'],
[/\[notice.*/, 'variable'],
[/\[info.*/, 'string'],
[/\[[a-zA-Z 0-9:]+\]/, 'tag'],
],
},
};
const logs = `
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
`;
describe('<CodeEditor />', () => {
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() {}
};
monaco.languages.register({ id: 'loglang' });
monaco.languages.setMonarchTokensProvider('loglang', simpleLogLang);
});
test('is rendered', () => {
const component = mountWithIntl(
<CodeEditor languageId="loglang" height={250} value={logs} onChange={() => {}} />
);
expect(component).toMatchSnapshot();
});
test('editor mount setup', () => {
const suggestionProvider = {
provideCompletionItems: (model: monaco.editor.ITextModel, position: monaco.Position) => ({
suggestions: [],
}),
};
const hoverProvider = {
provideHover: (model: monaco.editor.ITextModel, position: monaco.Position) => ({
contents: [],
}),
};
const editorWillMount = jest.fn();
monaco.languages.onLanguage = jest.fn((languageId, func) => {
expect(languageId).toBe('loglang');
// Call the function immediately so we can see our providers
// get setup without a monaco editor setting up completely
func();
}) as any;
monaco.languages.registerCompletionItemProvider = jest.fn();
monaco.languages.registerSignatureHelpProvider = jest.fn();
monaco.languages.registerHoverProvider = jest.fn();
monaco.editor.defineTheme = jest.fn();
mountWithIntl(
<CodeEditor
languageId="loglang"
value={logs}
onChange={() => {}}
editorWillMount={editorWillMount}
suggestionProvider={suggestionProvider}
hoverProvider={hoverProvider}
/>
);
// Verify our mount callback will be called
expect(editorWillMount.mock.calls.length).toBe(1);
// Verify that both, default and transparent theme will be setup\
// disabling this test because themes had to be refactored in Monaco editor Theme useMemo for light, dark and transparent to work
// expect((monaco.editor.defineTheme as jest.Mock).mock.calls.length).toBe(2)
// Verify our language features have been registered
expect((monaco.languages.onLanguage as jest.Mock).mock.calls.length).toBe(1);
expect((monaco.languages.registerCompletionItemProvider as jest.Mock).mock.calls.length).toBe(
1
);
expect((monaco.languages.registerHoverProvider as jest.Mock).mock.calls.length).toBe(1);
});
describe('hint element', () => {
let component: ReactWrapper;
const getHint = (): ReactWrapper => findTestSubject(component, 'codeEditorHint');
beforeEach(() => {
component = mountWithIntl(
<CodeEditor languageId="loglang" height={250} value={logs} onChange={() => {}} />
);
});
test('should be tabable', () => {
const DOMnode = getHint().getDOMNode();
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeTruthy();
expect(DOMnode.getAttribute('tabindex')).toBe('0');
expect(DOMnode).toMatchSnapshot();
});
test('should be disabled when the ui monaco editor gains focus', async () => {
// Initially it is visible and active
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeTruthy();
getHint().simulate('keydown', { key: keys.ENTER });
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeFalsy();
});
test('should be enabled when hitting the ESC key', () => {
getHint().simulate('keydown', { key: keys.ENTER });
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
keyCode: monaco.KeyCode.Escape,
});
// expect((getHint().props() as any).className).not.toContain('isInactive');
});
test('should detect that the suggestion menu is open and not show the hint on ESC', async () => {
getHint().simulate('keydown', { key: keys.ENTER });
// expect((getHint().props() as any).className).toContain('isInactive');
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(false);
// Show the suggestions in the editor
mockedEditorInstance?.__helpers__.showSuggestions();
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(true);
// Hitting the ESC key with the suggestions visible
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
keyCode: monaco.KeyCode.Escape,
});
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(false);
// The keyboard hint is still **not** active
// expect((getHint().props() as any).className).toContain('isInactive');
// Hitting a second time the ESC key should now show the hint
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
keyCode: monaco.KeyCode.Escape,
});
// expect((getHint().props() as any).className).not.toContain('isInactive');
});
});
/**
* Test whether our custom placeholder widget is being mounted based on our React logic. We cannot do a full
* test with Monaco so the parts handled by Monaco are all mocked out and we just check whether the element is mounted
* in the DOM.
*/
describe('placeholder element', () => {
let component: ReactWrapper;
beforeEach(() => {
component = mountWithIntl(
<CodeEditor
languageId="loglang"
height={250}
value=""
onChange={() => {}}
placeholder="myplaceholder"
/>
);
});
it('displays placeholder element when placeholder text is provided', () => {
expect(component.prop('placeholder')).toBe('myplaceholder');
});
it('does not display placeholder element when placeholder text is not provided', () => {
component.setProps({ ...component.props(), placeholder: undefined, value: '' });
component.update();
expect(component.prop('placeholder')).toBe(undefined);
});
// this does not work on the initial implementation of code editor either from kibana - react in the storybook instance
// in the kibana react storybook placeholder is not set but value is set instead
// it('does not display placeholder element when user input has been provided', () => {
// component.setProps({ value: 'some input', ...component.props() });
// component.update();
// expect(component.prop('placeholder')).toBe(null);
// });
});
});

View file

@ -0,0 +1,615 @@
/*
* 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 React, { useState, useRef, useCallback, useMemo, useEffect, KeyboardEvent } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import ReactMonacoEditor from 'react-monaco-editor';
import {
htmlIdGenerator,
EuiToolTip,
keys,
EuiButtonIcon,
EuiOverlayMask,
EuiI18n,
EuiFocusTrap,
EuiCopy,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
} from '@elastic/eui';
import { monaco } from '@kbn/monaco';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import './register_languages';
import { remeasureFonts } from './remeasure_fonts';
import { PlaceholderWidget } from './placeholder_widget';
import {
codeEditorControlsStyles,
codeEditorControlsWithinFullScreenStyles,
codeEditorFullScreenStyles,
codeEditorKeyboardHintStyles,
codeEditorStyles,
DARK_THEME,
LIGHT_THEME,
DARK_THEME_TRANSPARENT,
LIGHT_THEME_TRANSPARENT,
} from './editor.styles';
export interface Props {
/** Width of editor. Defaults to 100%. */
width?: string | number;
/** Height of editor. Defaults to 100px. */
height?: string | number;
/** ID of the editor language */
languageId: string;
/** Value of the editor */
value: string;
/** Function invoked when text in editor is changed */
onChange?: (value: string, event: monaco.editor.IModelContentChangedEvent) => void;
/**
* Options for the Monaco Code Editor
* Documentation of options can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
*/
options?: monaco.editor.IStandaloneEditorConstructionOptions;
/**
* Suggestion provider for autocompletion
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html
*/
suggestionProvider?: monaco.languages.CompletionItemProvider;
/**
* Signature provider for function parameter info
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.signaturehelpprovider.html
*/
signatureProvider?: monaco.languages.SignatureHelpProvider;
/**
* Hover provider for hover documentation
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.hoverprovider.html
*/
hoverProvider?: monaco.languages.HoverProvider;
/**
* Language config provider for bracket
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
*/
languageConfiguration?: monaco.languages.LanguageConfiguration;
/**
* Function called before the editor is mounted in the view
*/
editorWillMount?: () => void;
/**
* Function called before the editor is mounted in the view
* and completely replaces the setup behavior called by the component
*/
overrideEditorWillMount?: () => void;
/**
* Function called after the editor is mounted in the view
*/
editorDidMount?: (editor: monaco.editor.IStandaloneCodeEditor) => void;
/**
* Should the editor use the dark theme
*/
useDarkTheme?: boolean;
/**
* Should the editor use a transparent background
*/
transparentBackground?: boolean;
/**
* Should the editor be rendered using the fullWidth EUI attribute
*/
fullWidth?: boolean;
placeholder?: string;
/**
* Accessible name for the editor. (Defaults to "Code editor")
*/
'aria-label'?: string;
isCopyable?: boolean;
allowFullScreen?: boolean;
}
export const CodeEditor: React.FC<Props> = ({
languageId,
value,
onChange,
width,
options,
overrideEditorWillMount,
editorDidMount,
editorWillMount,
useDarkTheme,
transparentBackground,
suggestionProvider,
signatureProvider,
hoverProvider,
placeholder,
languageConfiguration,
'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', {
defaultMessage: 'Code Editor',
}),
isCopyable = false,
allowFullScreen = false,
}) => {
const { euiTheme } = useEuiTheme();
// We need to be able to mock the MonacoEditor in our test in order to not test implementation
// detail and not have to call methods on the <CodeEditor /> component instance.
const MonacoEditor: typeof ReactMonacoEditor = useMemo(() => {
const isMockedComponent =
typeof ReactMonacoEditor === 'function' && ReactMonacoEditor.name === 'JestMockEditor';
return isMockedComponent
? (ReactMonacoEditor as unknown as () => typeof ReactMonacoEditor)()
: ReactMonacoEditor;
}, []);
const { FullScreenDisplay, FullScreenButton, isFullScreen, setIsFullScreen, onKeyDown } =
useFullScreen({
allowFullScreen,
});
const isReadOnly = options?.readOnly ?? false;
const _editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const _placeholderWidget = useRef<PlaceholderWidget | null>(null);
const isSuggestionMenuOpen = useRef(false);
const editorHint = useRef<HTMLDivElement>(null);
const textboxMutationObserver = useRef<MutationObserver | null>(null);
const [isHintActive, setIsHintActive] = useState(true);
const defaultStyles = codeEditorStyles();
const hintStyles = codeEditorKeyboardHintStyles(euiTheme.levels);
const promptClasses = useMemo(() => {
return isHintActive ? [defaultStyles, hintStyles] : [defaultStyles];
}, [isHintActive, defaultStyles, hintStyles]);
const _updateDimensions = useCallback(() => {
_editor.current?.layout();
}, []);
useResizeDetector({
handleWidth: true,
handleHeight: true,
onResize: _updateDimensions,
refreshMode: 'debounce',
});
const startEditing = useCallback(() => {
setIsHintActive(false);
_editor.current?.focus();
}, []);
const stopEditing = useCallback(() => {
setIsHintActive(true);
}, []);
const onKeyDownHint = useCallback(
(ev: React.KeyboardEvent) => {
if (ev.key === keys.ENTER) {
ev.preventDefault();
startEditing();
}
},
[startEditing]
);
const onKeydownMonaco = useCallback(
(ev: monaco.IKeyboardEvent) => {
if (ev.keyCode === monaco.KeyCode.Escape) {
// If the autocompletion context menu is open then we want to let ESCAPE close it but
// **not** exit out of editing mode.
if (!isSuggestionMenuOpen.current) {
ev.preventDefault();
ev.stopPropagation();
stopEditing();
editorHint.current?.focus();
}
setIsFullScreen(false);
}
},
[stopEditing, setIsFullScreen]
);
const onBlurMonaco = useCallback(() => {
stopEditing();
}, [stopEditing]);
const renderPrompt = useCallback(() => {
const enterKey = (
<strong>
{i18n.translate('sharedUXPackages.codeEditor.enterKeyLabel', {
defaultMessage: 'Enter',
description:
'The name used for the Enter key on keyword. Will be {key} in sharedUXPackages.codeEditor.startEditing(ReadOnly).',
})}
</strong>
);
const escapeKey = (
<strong>
{i18n.translate('sharedUXPackages.codeEditor.escapeKeyLabel', {
defaultMessage: 'Esc',
description:
'The label of the Escape key as printed on the keyboard. Will be {key} inside sharedUXPackages.codeEditor.stopEditing(ReadOnly).',
})}
</strong>
);
return (
<EuiToolTip
display="block"
content={
<>
<p>
{isReadOnly ? (
<FormattedMessage
css={defaultStyles}
id="sharedUXPackages.codeEditor.startEditingReadOnly"
defaultMessage="Press {key} to start interacting with the code."
values={{ key: enterKey }}
/>
) : (
<FormattedMessage
css={defaultStyles}
id="sharedUXPackages.codeEditor.startEditing"
defaultMessage="Press {key} to start editing."
values={{ key: enterKey }}
/>
)}
</p>
<p>
{isReadOnly ? (
<FormattedMessage
css={defaultStyles}
id="sharedUXPackages.codeEditor.stopEditingReadOnly"
defaultMessage="Press {key} to stop interacting with the code."
values={{ key: escapeKey }}
/>
) : (
<FormattedMessage
css={defaultStyles}
id="sharedUXPackages.codeEditor.stopEditing"
defaultMessage="Press {key} to stop editing."
values={{ key: escapeKey }}
/>
)}
</p>
</>
}
>
<div
css={promptClasses}
id={htmlIdGenerator('codeEditor')()}
ref={editorHint}
tabIndex={0}
role="button"
onClick={startEditing}
onKeyDown={onKeyDownHint}
aria-label={ariaLabel}
data-test-subj={isHintActive ? 'codeEditorHint' : 'codeEditor'}
/>
</EuiToolTip>
);
}, [
onKeyDownHint,
startEditing,
ariaLabel,
isReadOnly,
promptClasses,
defaultStyles,
isHintActive,
]);
const _editorWillMount = useCallback(
(__monaco: unknown) => {
if (__monaco !== monaco) {
throw new Error('react-monaco-editor is using a different version of monaco');
}
if (overrideEditorWillMount) {
overrideEditorWillMount();
return;
}
editorWillMount?.();
monaco.languages.onLanguage(languageId, () => {
if (suggestionProvider) {
monaco.languages.registerCompletionItemProvider(languageId, suggestionProvider);
}
if (signatureProvider) {
monaco.languages.registerSignatureHelpProvider(languageId, signatureProvider);
}
if (hoverProvider) {
monaco.languages.registerHoverProvider(languageId, hoverProvider);
}
if (languageConfiguration) {
monaco.languages.setLanguageConfiguration(languageId, languageConfiguration);
}
});
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
monaco.editor.defineTheme(
'euiColorsTransparent',
useDarkTheme ? DARK_THEME_TRANSPARENT : LIGHT_THEME_TRANSPARENT
);
},
[
overrideEditorWillMount,
editorWillMount,
languageId,
useDarkTheme,
suggestionProvider,
signatureProvider,
hoverProvider,
languageConfiguration,
]
);
const _editorDidMount = useCallback(
(editor: monaco.editor.IStandaloneCodeEditor, __monaco: unknown) => {
if (__monaco !== monaco) {
throw new Error('react-monaco-editor is using a different version of monaco');
}
remeasureFonts();
_editor.current = editor;
const textbox = editor.getDomNode()?.getElementsByTagName('textarea')[0];
if (textbox) {
// Make sure the textarea is not directly accesible with TAB
textbox.tabIndex = -1;
// The Monaco editor seems to override the tabindex and set it back to "0"
// so we make sure that whenever the attributes change the tabindex stays at -1
textboxMutationObserver.current = new MutationObserver(function onTextboxAttributeChange() {
if (textbox.tabIndex >= 0) {
textbox.tabIndex = -1;
}
});
textboxMutationObserver.current.observe(textbox, { attributes: true });
}
editor.onKeyDown(onKeydownMonaco);
editor.onDidBlurEditorText(onBlurMonaco);
// "widget" is not part of the TS interface but does exist
// @ts-expect-errors
const suggestionWidget = editor.getContribution('editor.contrib.suggestController')?.widget
?.value;
// As I haven't found official documentation for "onDidShow" and "onDidHide"
// we guard from possible changes in the underlying lib
if (suggestionWidget && suggestionWidget.onDidShow && suggestionWidget.onDidHide) {
suggestionWidget.onDidShow(() => {
isSuggestionMenuOpen.current = true;
});
suggestionWidget.onDidHide(() => {
isSuggestionMenuOpen.current = false;
});
}
editorDidMount?.(editor);
},
[editorDidMount, onBlurMonaco, onKeydownMonaco]
);
useEffect(() => {
return () => {
textboxMutationObserver.current?.disconnect();
};
}, []);
useEffect(() => {
if (placeholder && !value && _editor.current) {
// Mounts editor inside constructor
_placeholderWidget.current = new PlaceholderWidget(placeholder, _editor.current);
}
return () => {
_placeholderWidget.current?.dispose();
_placeholderWidget.current = null;
};
}, [placeholder, value]);
const { CopyButton } = useCopy({ isCopyable, value });
const controlStyles = useMemo(() => {
const copyableStyles = [defaultStyles, codeEditorControlsStyles(euiTheme.size, euiTheme.base)];
return allowFullScreen || isCopyable ? copyableStyles && defaultStyles : defaultStyles;
}, [allowFullScreen, isCopyable, defaultStyles, euiTheme]);
const theme = useMemo(() => {
// register theme for dark or light
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
return options?.theme ?? (transparentBackground ? 'euiColorsTransparent' : 'euiColors');
}, [useDarkTheme, transparentBackground, options]);
return (
<div css={codeEditorStyles()} onKeyDown={onKeyDown}>
{renderPrompt()}
<FullScreenDisplay>
{allowFullScreen || isCopyable ? (
<div css={controlStyles}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<CopyButton />
</EuiFlexItem>
<EuiFlexItem>
<FullScreenButton />
</EuiFlexItem>
</EuiFlexGroup>
</div>
) : null}
<MonacoEditor
theme={theme}
language={languageId}
value={value}
onChange={onChange}
width={isFullScreen ? '100vw' : width}
// previously defaulted to height which defaulted to 100% but this makes it unviewable
height={isFullScreen ? '100vh' : '100px'}
editorWillMount={_editorWillMount}
editorDidMount={_editorDidMount}
options={{
padding: allowFullScreen || isCopyable ? { top: 24 } : {},
renderLineHighlight: 'none',
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
scrollbar: {
useShadows: false,
// Scroll events are handled only when there is scrollable content. When there is scrollable content, the
// editor should scroll to the bottom then break out of that scroll context and continue scrolling on any
// outer scrollbars.
alwaysConsumeMouseWheel: false,
},
wordBasedSuggestions: false,
wordWrap: 'on',
wrappingIndent: 'indent',
matchBrackets: 'never',
fontFamily: 'Roboto Mono',
fontSize: isFullScreen ? 16 : 12,
lineHeight: isFullScreen ? 24 : 21,
...options,
}}
/>
</FullScreenDisplay>
</div>
);
};
/**
* Fullscreen logic
*/
const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => {
const [isFullScreen, setIsFullScreen] = useState(false);
const toggleFullScreen = () => {
setIsFullScreen(!isFullScreen);
};
const onKeyDown = useCallback((event: KeyboardEvent<HTMLElement>) => {
if (event.key === keys.ESCAPE) {
event.preventDefault();
event.stopPropagation();
setIsFullScreen(false);
}
}, []);
const FullScreenButton: React.FC = () => {
if (!allowFullScreen) return null;
return (
<EuiI18n
tokens={['euiCodeBlock.fullscreenCollapse', 'euiCodeBlock.fullscreenExpand']}
defaults={['Collapse', 'Expand']}
>
{([fullscreenCollapse, fullscreenExpand]: string[]) => (
<EuiButtonIcon
css={[codeEditorStyles(), codeEditorFullScreenStyles]}
onClick={toggleFullScreen}
iconType={isFullScreen ? 'fullScreenExit' : 'fullScreen'}
color="text"
aria-label={isFullScreen ? fullscreenCollapse : fullscreenExpand}
size="xs"
/>
)}
</EuiI18n>
);
};
const { euiTheme } = useEuiTheme();
const FullScreenDisplay = useMemo(
() =>
({ children }: { children: Array<JSX.Element | null> | JSX.Element }) => {
if (!isFullScreen) return <>{children}</>;
return (
<EuiOverlayMask>
<EuiFocusTrap clickOutsideDisables={true}>
<div
css={[
codeEditorStyles(),
codeEditorFullScreenStyles(),
codeEditorControlsWithinFullScreenStyles(euiTheme.size.l),
]}
>
{children}
</div>
</EuiFocusTrap>
</EuiOverlayMask>
);
},
[isFullScreen, euiTheme]
);
return {
FullScreenButton,
FullScreenDisplay,
onKeyDown,
isFullScreen,
setIsFullScreen,
};
};
const useCopy = ({ isCopyable, value }: { isCopyable: boolean; value: string }) => {
const showCopyButton = isCopyable && value;
const CopyButton = () => {
if (!showCopyButton) return null;
return (
<div css={codeEditorStyles()} className="euiCodeBlock__copyButton">
<EuiI18n token="euiCodeBlock.copyButton" default="Copy">
{(copyButton: string) => (
<EuiCopy textToCopy={value}>
{(copy) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
color="text"
aria-label={copyButton}
size="xs"
/>
)}
</EuiCopy>
)}
</EuiI18n>
</div>
);
};
return { showCopyButton, CopyButton };
};

View file

@ -0,0 +1,211 @@
/*
* 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 { ComponentSelector, css, CSSObject, SerializedStyles } from '@emotion/react';
import { ArrayCSSInterpolation } from '@emotion/serialize';
import { Property } from 'csstype';
import { monaco } from '@kbn/monaco';
import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme';
export const codeEditorMonacoStyles = () => css`
{
animation: none !important; // Removes textarea EUI blue underline animation from EUI
}
`;
export const codeEditorStyles = () => css`
{
position: relative;
height: 100%;
}
`;
export const codeEditorPlaceholderContainerStyles = (subduedText: string) => css`
{
color: ${subduedText};
width: max-content;
pointer-events: none;
}
`;
export const codeEditorKeyboardHintStyles = (levels: {
content: Property.ZIndex;
mask: Property.ZIndex;
toast: Property.ZIndex;
modal: Property.ZIndex;
navigation: Property.ZIndex;
menu: Property.ZIndex;
header: Property.ZIndex;
flyout: Property.ZIndex;
maskBelowHeader: Property.ZIndex;
}) =>
css`
{
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
&:focus {
z-index: ${levels.mask};
}
&--isInactive {
display: none;
}
}
`;
export const codeEditorControlsStyles = (
size: {
base: string;
xxs: string;
xs: string;
s: string;
m: string;
l: string;
xl: string;
xxl: string;
xxxl: string;
xxxxl: string;
},
base:
| string
| number
| boolean
| ComponentSelector
| SerializedStyles
| CSSObject
| ArrayCSSInterpolation
| null
| undefined
) => css`
{
top: ${size.xs};
right: ${base};
position: absolute;
z-index: 1000;
}
`;
export const codeEditorFullScreenStyles = () => css`
{
position: absolute;
left: 0;
top: 0;
}
`;
export const codeEditorControlsWithinFullScreenStyles = (size: string) => css`
top: ${size};
right: ${size};
}`;
// NOTE: For talk around where this theme information will ultimately live,
// please see this discuss issue: https://github.com/elastic/kibana/issues/43814
export function createTheme(
euiTheme: typeof darkTheme | typeof lightTheme,
selectionBackgroundColor: string,
backgroundColor?: string
): monaco.editor.IStandaloneThemeData {
return {
base: 'vs',
inherit: true,
rules: [
{
token: '',
foreground: euiTheme.euiColorDarkestShade,
background: euiTheme.euiFormBackgroundColor,
},
{ token: 'invalid', foreground: euiTheme.euiColorAccent },
{ token: 'emphasis', fontStyle: 'italic' },
{ token: 'strong', fontStyle: 'bold' },
{ token: 'variable', foreground: euiTheme.euiColorPrimary },
{ token: 'variable.predefined', foreground: euiTheme.euiColorSuccess },
{ token: 'constant', foreground: euiTheme.euiColorAccent },
{ token: 'comment', foreground: euiTheme.euiColorMediumShade },
{ token: 'number', foreground: euiTheme.euiColorAccent },
{ token: 'number.hex', foreground: euiTheme.euiColorAccent },
{ token: 'regexp', foreground: euiTheme.euiColorDanger },
{ token: 'annotation', foreground: euiTheme.euiColorMediumShade },
{ token: 'type', foreground: euiTheme.euiColorVis0 },
{ token: 'delimiter', foreground: euiTheme.euiTextSubduedColor },
{ token: 'delimiter.html', foreground: euiTheme.euiColorDarkShade },
{ token: 'delimiter.xml', foreground: euiTheme.euiColorPrimary },
{ token: 'tag', foreground: euiTheme.euiColorDanger },
{ token: 'tag.id.jade', foreground: euiTheme.euiColorPrimary },
{ token: 'tag.class.jade', foreground: euiTheme.euiColorPrimary },
{ token: 'meta.scss', foreground: euiTheme.euiColorAccent },
{ token: 'metatag', foreground: euiTheme.euiColorSuccess },
{ token: 'metatag.content.html', foreground: euiTheme.euiColorDanger },
{ token: 'metatag.html', foreground: euiTheme.euiColorMediumShade },
{ token: 'metatag.xml', foreground: euiTheme.euiColorMediumShade },
{ token: 'metatag.php', fontStyle: 'bold' },
{ token: 'key', foreground: euiTheme.euiColorWarning },
{ token: 'string.key.json', foreground: euiTheme.euiColorDanger },
{ token: 'string.value.json', foreground: euiTheme.euiColorPrimary },
{ token: 'attribute.name', foreground: euiTheme.euiColorDanger },
{ token: 'attribute.name.css', foreground: euiTheme.euiColorSuccess },
{ token: 'attribute.value', foreground: euiTheme.euiColorPrimary },
{ token: 'attribute.value.number', foreground: euiTheme.euiColorWarning },
{ token: 'attribute.value.unit', foreground: euiTheme.euiColorWarning },
{ token: 'attribute.value.html', foreground: euiTheme.euiColorPrimary },
{ token: 'attribute.value.xml', foreground: euiTheme.euiColorPrimary },
{ token: 'string', foreground: euiTheme.euiColorDanger },
{ token: 'string.html', foreground: euiTheme.euiColorPrimary },
{ token: 'string.sql', foreground: euiTheme.euiColorDanger },
{ token: 'string.yaml', foreground: euiTheme.euiColorPrimary },
{ token: 'keyword', foreground: euiTheme.euiColorPrimary },
{ token: 'keyword.json', foreground: euiTheme.euiColorPrimary },
{ token: 'keyword.flow', foreground: euiTheme.euiColorWarning },
{ token: 'keyword.flow.scss', foreground: euiTheme.euiColorPrimary },
// Monaco editor supports strikethrough font style only starting from 0.32.0.
{ token: 'keyword.deprecated', foreground: euiTheme.euiColorAccent },
{ token: 'operator.scss', foreground: euiTheme.euiColorDarkShade },
{ token: 'operator.sql', foreground: euiTheme.euiColorMediumShade },
{ token: 'operator.swift', foreground: euiTheme.euiColorMediumShade },
{ token: 'predefined.sql', foreground: euiTheme.euiColorMediumShade },
{ token: 'text', foreground: euiTheme.euiTitleColor },
{ token: 'label', foreground: euiTheme.euiColorVis9 },
],
colors: {
'editor.foreground': euiTheme.euiColorDarkestShade,
'editor.background': backgroundColor ?? euiTheme.euiFormBackgroundColor,
'editorLineNumber.foreground': euiTheme.euiColorDarkShade,
'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade,
'editorIndentGuide.background': euiTheme.euiColorLightShade,
'editor.selectionBackground': selectionBackgroundColor,
'editorWidget.border': euiTheme.euiColorLightShade,
'editorWidget.background': euiTheme.euiColorLightestShade,
'editorCursor.foreground': euiTheme.euiColorDarkestShade,
'editorSuggestWidget.selectedBackground': euiTheme.euiColorLightShade,
'list.hoverBackground': euiTheme.euiColorLightShade,
'list.highlightForeground': euiTheme.euiColorPrimary,
'editor.lineHighlightBorder': euiTheme.euiColorLightestShade,
},
};
}
const DARK_THEME = createTheme(darkTheme, '#343551');
const LIGHT_THEME = createTheme(lightTheme, '#E3E4ED');
const DARK_THEME_TRANSPARENT = createTheme(darkTheme, '#343551', '#00000000');
const LIGHT_THEME_TRANSPARENT = createTheme(lightTheme, '#E3E4ED', '#00000000');
export { DARK_THEME, LIGHT_THEME, DARK_THEME_TRANSPARENT, LIGHT_THEME_TRANSPARENT };

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 { CodeEditor } from './code_editor';

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/code_editor'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/code-editor",
"owner": "@elastic/shared-ux"
}

View file

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

View file

@ -0,0 +1,184 @@
/*
* 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 = {
wordPattern: /(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,
comments: {
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '[', close: ']', notIn: ['string', 'comment'] },
{ open: '(', close: ')', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
folding: {
markers: {
start: new RegExp('^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/'),
end: new RegExp('^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/'),
},
},
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.css',
ws: '[ \t\n\r\f]*',
identifier:
'-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*',
brackets: [
{ open: '{', close: '}', token: 'delimiter.bracket' },
{ open: '[', close: ']', token: 'delimiter.bracket' },
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
{ open: '<', close: '>', token: 'delimiter.angle' },
],
tokenizer: {
root: [{ include: '@selector' }],
selector: [
{ include: '@comments' },
{ include: '@import' },
{ include: '@strings' },
[
'[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)',
{ token: 'keyword', next: '@keyframedeclaration' },
],
['[@](page|content|font-face|-moz-document)', { token: 'keyword' }],
['[@](charset|namespace)', { token: 'keyword', next: '@declarationbody' }],
[
'(url-prefix)(\\()',
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
],
[
'(url)(\\()',
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
],
{ include: '@selectorname' },
['[\\*]', 'tag'],
['[>\\+,]', 'delimiter'],
['\\[', { token: 'delimiter.bracket', next: '@selectorattribute' }],
['{', { token: 'delimiter.bracket', next: '@selectorbody' }],
],
selectorbody: [
{ include: '@comments' },
['[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))', 'attribute.name', '@rulevalue'],
['}', { token: 'delimiter.bracket', next: '@pop' }],
],
selectorname: [['(\\.|#(?=[^{])|%|(@identifier)|:)+', 'tag']],
selectorattribute: [{ include: '@term' }, [']', { token: 'delimiter.bracket', next: '@pop' }]],
term: [
{ include: '@comments' },
[
'(url-prefix)(\\()',
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
],
[
'(url)(\\()',
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
],
{ include: '@functioninvocation' },
{ include: '@numbers' },
{ include: '@name' },
['([<>=\\+\\-\\*\\/\\^\\|\\~,])', 'delimiter'],
[',', 'delimiter'],
],
rulevalue: [
{ include: '@comments' },
{ include: '@strings' },
{ include: '@term' },
['!important', 'keyword'],
[';', 'delimiter', '@pop'],
['(?=})', { token: '', next: '@pop' }], // missing semicolon
],
warndebug: [['[@](warn|debug)', { token: 'keyword', next: '@declarationbody' }]],
import: [['[@](import)', { token: 'keyword', next: '@declarationbody' }]],
urldeclaration: [
{ include: '@strings' },
['[^)\r\n]+', 'string'],
['\\)', { token: 'delimiter.parenthesis', next: '@pop' }],
],
parenthizedterm: [
{ include: '@term' },
['\\)', { token: 'delimiter.parenthesis', next: '@pop' }],
],
declarationbody: [
{ include: '@term' },
[';', 'delimiter', '@pop'],
['(?=})', { token: '', next: '@pop' }], // missing semicolon
],
comments: [
['\\/\\*', 'comment', '@comment'],
['\\/\\/+.*', 'comment'],
],
comment: [
['\\*\\/', 'comment', '@pop'],
[/[^*/]+/, 'comment'],
[/./, 'comment'],
],
name: [['@identifier', 'attribute.value']],
numbers: [
['-?(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?', { token: 'attribute.value.number', next: '@units' }],
['#[0-9a-fA-F_]+(?!\\w)', 'attribute.value.hex'],
],
units: [
[
'(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?',
'attribute.value.unit',
'@pop',
],
],
keyframedeclaration: [
['@identifier', 'attribute.value'],
['{', { token: 'delimiter.bracket', switchTo: '@keyframebody' }],
],
keyframebody: [
{ include: '@term' },
['{', { token: 'delimiter.bracket', next: '@selectorbody' }],
['}', { token: 'delimiter.bracket', next: '@pop' }],
],
functioninvocation: [
['@identifier\\(', { token: 'attribute.value', next: '@functionarguments' }],
],
functionarguments: [
['\\$@identifier@ws:', 'attribute.name'],
['[,]', 'delimiter'],
{ include: '@term' },
['\\)', { token: 'attribute.value', next: '@pop' }],
],
strings: [
['~?"', { token: 'string', next: '@stringenddoublequote' }],
["~?'", { token: 'string', next: '@stringendquote' }],
],
stringenddoublequote: [
['\\\\.', 'string'],
['"', { token: 'string', next: '@pop' }],
[/[^\\"]+/, 'string'],
['.', 'string'],
],
stringendquote: [
['\\\\.', 'string'],
["'", { token: 'string', next: '@pop' }],
[/[^\\']+/, 'string'],
['.', 'string'],
],
},
} as monaco.languages.IMonarchLanguage;

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: 'handlebars', languageConfiguration, lexerRules };

View file

@ -0,0 +1,198 @@
/*
* 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.
*/
/**
* This file is adapted from: https://github.com/microsoft/monaco-languages/blob/master/src/handlebars/handlebars.ts
* License: https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
*/
import { monaco } from '@kbn/monaco';
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
comments: {
blockComment: ['{{!--', '--}}'],
},
brackets: [
['<', '>'],
['{{', '}}'],
['{', '}'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '<', close: '>' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
// Set defaultToken to invalid to see what you do not tokenize yet.
defaultToken: 'invalid',
tokenPostfix: '',
brackets: [
{
token: 'constant.delimiter.double',
open: '{{',
close: '}}',
},
{
token: 'constant.delimiter.triple',
open: '{{{',
close: '}}}',
},
],
tokenizer: {
root: [
{ include: '@maybeHandlebars' },
{ include: '@whitespace' },
{ include: '@urlScheme' },
{ include: '@urlAuthority' },
{ include: '@urlSlash' },
{ include: '@urlParamKey' },
{ include: '@urlParamValue' },
{ include: '@text' },
],
maybeHandlebars: [
[
/\{\{/,
{
token: '@rematch',
switchTo: '@handlebars.root',
},
],
],
whitespace: [[/[ \t\r\n]+/, '']],
text: [
[
/[^<{\?\&\/]+/,
{
token: 'text',
next: '@popall',
},
],
],
rematchAsRoot: [
[
/.+/,
{
token: '@rematch',
switchTo: '@root',
},
],
],
urlScheme: [
[
/([a-zA-Z0-9\+\.\-]{1,10})(:)/,
[
{
token: 'text.keyword.scheme.url',
},
{
token: 'delimiter',
},
],
],
],
urlAuthority: [
[
/(\/\/)([a-zA-Z0-9\.\-_]+)/,
[
{
token: 'delimiter',
},
{
token: 'metatag.keyword.authority.url',
},
],
],
],
urlSlash: [
[
/\/+/,
{
token: 'delimiter',
},
],
],
urlParamKey: [
[
/([\?\&\#])([a-zA-Z0-9_\-]+)/,
[
{
token: 'delimiter.key.query.url',
},
{
token: 'label.label.key.query.url',
},
],
],
],
urlParamValue: [
[
/(\=)([^\?\&\{}]+)/,
[
{
token: 'text.separator.value.query.url',
},
{
token: 'text.value.query.url',
},
],
],
],
handlebars: [
[
/\{\{\{?/,
{
token: '@brackets',
bracket: '@open',
},
],
[
/\}\}\}?/,
{
token: '@brackets',
bracket: '@close',
switchTo: '@$S2.$S3',
},
],
{ include: 'handlebarsExpression' },
],
handlebarsExpression: [
[/"[^"]*"/, 'string.handlebars'],
[/[#/][^\s}]+/, 'keyword.helper.handlebars'],
[/else\b/, 'keyword.helper.handlebars'],
[/[\s]+/],
[/[^}]/, 'variable.parameter.handlebars'],
],
},
} as monaco.languages.IMonarchLanguage;

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: 'hjson', languageConfiguration, lexerRules };

View file

@ -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;

View file

@ -0,0 +1,15 @@
/*
* 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 { 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, HJson };

View file

@ -0,0 +1,11 @@
/*
* 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: 'markdown', languageConfiguration, lexerRules };

View file

@ -0,0 +1,223 @@
/*
* 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.
*/
/**
* This file is adapted from: https://code-room.io/libs/monaco-editor/esm/vs/basic-languages/markdown/markdown.js
* License: https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
*/
import { monaco } from '@kbn/monaco';
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
comments: {
blockComment: ['<!--', '-->'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '<', close: '>', notIn: ['string'] },
],
surroundingPairs: [
{ open: '(', close: ')' },
{ open: '[', close: ']' },
{ open: '`', close: '`' },
],
folding: {
markers: {
start: new RegExp('^\\s*<!--\\s*#?region\\b.*-->'),
end: new RegExp('^\\s*<!--\\s*#?endregion\\b.*-->'),
},
},
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.md',
// escape codes
control: /[\\`*_\[\]{}()#+\-\.!]/,
noncontrol: /[^\\`*_\[\]{}()#+\-\.!]/,
escapes: /\\(?:@control)/,
// escape codes for javascript/CSS strings
jsescapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
// non matched elements
empty: [
'area',
'base',
'basefont',
'br',
'col',
'frame',
'hr',
'img',
'input',
'isindex',
'link',
'meta',
'param',
],
tokenizer: {
root: [
// markdown tables
[/^\s*\|/, '@rematch', '@table_header'],
// headers (with #)
[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/, ['white', 'keyword', 'keyword', 'keyword']],
// headers (with =)
[/^\s*(=+|\-+)\s*$/, 'keyword'],
// headers (with ***)
[/^\s*((\*[ ]?)+)\s*$/, 'meta.separator'],
// quote
[/^\s*>+/, 'comment'],
// list (starting with * or number)
[/^\s*([\*\-+:]|\d+\.)\s/, 'keyword'],
// code block (4 spaces indent)
[/^(\t|[ ]{4})[^ ].*$/, 'string'],
// code block (3 tilde)
[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/, { token: 'string', next: '@codeblock' }],
// github style code blocks (with backticks and language)
[
/^\s*```\s*((?:\w|[\/\-#])+).*$/,
{ token: 'string', next: '@codeblockgh', nextEmbedded: '$1' },
],
// github style code blocks (with backticks but no language)
[/^\s*```\s*$/, { token: 'string', next: '@codeblock' }],
// markup within lines
{ include: '@linecontent' },
],
table_header: [{ include: '@table_common' }, [/[^\|]+/, 'keyword.table.header']],
table_body: [{ include: '@table_common' }, { include: '@linecontent' }],
table_common: [
[/\s*[\-:]+\s*/, { token: 'keyword', switchTo: 'table_body' }],
[/^\s*\|/, 'keyword.table.left'],
[/^\s*[^\|]/, '@rematch', '@pop'],
[/^\s*$/, '@rematch', '@pop'],
[
/\|/,
{
cases: {
'@eos': 'keyword.table.right',
'@default': 'keyword.table.middle',
},
},
],
],
codeblock: [
[/^\s*~~~\s*$/, { token: 'string', next: '@pop' }],
[/^\s*```\s*$/, { token: 'string', next: '@pop' }],
[/.*$/, 'variable.source'],
],
// github style code blocks
codeblockgh: [
[/```\s*$/, { token: 'variable.source', next: '@pop', nextEmbedded: '@pop' }],
[/[^`]+/, 'variable.source'],
],
linecontent: [
// escapes
[/&\w+;/, 'string.escape'],
[/@escapes/, 'escape'],
// various markup
[/\b__([^\\_]|@escapes|_(?!_))+__\b/, 'strong'],
[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/, 'strong'],
[/\b_[^_]+_\b/, 'emphasis'],
[/\*([^\\*]|@escapes)+\*/, 'emphasis'],
[/`([^\\`]|@escapes)+`/, 'variable'],
// links
[/\{+[^}]+\}+/, 'string.target'],
[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/, ['string.link', '', 'string.link']],
[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/, 'string.link'],
// or html
{ include: 'html' },
],
// Note: it is tempting to rather switch to the real HTML mode instead of building our own here
// but currently there is a limitation in Monarch that prevents us from doing it: The opening
// '<' would start the HTML mode, however there is no way to jump 1 character back to let the
// HTML mode also tokenize the opening angle bracket. Thus, even though we could jump to HTML,
// we cannot correctly tokenize it in that mode yet.
html: [
// html tags
[/<(\w+)\/>/, 'tag'],
[
/<(\w+)/,
{
cases: {
'@empty': { token: 'tag', next: '@tag.$1' },
'@default': { token: 'tag', next: '@tag.$1' },
},
},
],
[/<\/(\w+)\s*>/, { token: 'tag' }],
[/<!--/, 'comment', '@comment'],
],
comment: [
[/[^<\-]+/, 'comment.content'],
[/-->/, 'comment', '@pop'],
[/<!--/, 'comment.content.invalid'],
[/[<\-]/, 'comment.content'],
],
// Almost full HTML tag matching, complete with embedded scripts & styles
tag: [
[/[ \t\r\n]+/, 'white'],
[
/(type)(\s*=\s*)(")([^"]+)(")/,
[
'attribute.name.html',
'delimiter.html',
'string.html',
{ token: 'string.html', switchTo: '@tag.$S2.$4' },
'string.html',
],
],
[
/(type)(\s*=\s*)(')([^']+)(')/,
[
'attribute.name.html',
'delimiter.html',
'string.html',
{ token: 'string.html', switchTo: '@tag.$S2.$4' },
'string.html',
],
],
[/(\w+)(\s*=\s*)("[^"]*"|'[^']*')/, ['attribute.name.html', 'delimiter.html', 'string.html']],
[/\w+/, 'attribute.name.html'],
[/\/>/, 'tag', '@pop'],
[
/>/,
{
cases: {
'$S2==style': { token: 'tag', switchTo: 'embeddedStyle', nextEmbedded: 'text/css' },
'$S2==script': {
cases: {
$S3: { token: 'tag', switchTo: 'embeddedScript', nextEmbedded: '$S3' },
'@default': {
token: 'tag',
switchTo: 'embeddedScript',
nextEmbedded: 'text/javascript',
},
},
},
'@default': { token: 'tag', next: '@pop' },
},
},
],
],
embeddedStyle: [
[/[^<]+/, ''],
[/<\/style\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
[/</, ''],
],
embeddedScript: [
[/[^<]+/, ''],
[/<\/script\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
[/</, ''],
],
},
} as monaco.languages.IMonarchLanguage;

View file

@ -0,0 +1,11 @@
/*
* 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: 'yaml', languageConfiguration, lexerRules };

View file

@ -0,0 +1,198 @@
/*
* 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 = {
comments: {
lineComment: '#',
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
folding: {
offSide: true,
},
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
tokenPostfix: '.yaml',
brackets: [
{ token: 'delimiter.bracket', open: '{', close: '}' },
{ token: 'delimiter.square', open: '[', close: ']' },
],
keywords: ['true', 'True', 'TRUE', 'false', 'False', 'FALSE', 'null', 'Null', 'Null', '~'],
numberInteger: /(?:0|[+-]?[0-9]+)/,
numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,
numberOctal: /0o[0-7]+/,
numberHex: /0x[0-9a-fA-F]+/,
numberInfinity: /[+-]?\.(?:inf|Inf|INF)/,
numberNaN: /\.(?:nan|Nan|NAN)/,
numberDate: /\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,
escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
tokenizer: {
root: [
{ include: '@whitespace' },
{ include: '@comment' },
// Directive
[/%[^ ]+.*$/, 'meta.directive'],
// Document Markers
[/---/, 'operators.directivesEnd'],
[/\.{3}/, 'operators.documentEnd'],
// Block Structure Indicators
[/[-?:](?= )/, 'operators'],
{ include: '@anchor' },
{ include: '@tagHandle' },
{ include: '@flowCollections' },
{ include: '@blockStyle' },
// Numbers
[/@numberInteger(?![ \t]*\S+)/, 'number'],
[/@numberFloat(?![ \t]*\S+)/, 'number.float'],
[/@numberOctal(?![ \t]*\S+)/, 'number.octal'],
[/@numberHex(?![ \t]*\S+)/, 'number.hex'],
[/@numberInfinity(?![ \t]*\S+)/, 'number.infinity'],
[/@numberNaN(?![ \t]*\S+)/, 'number.nan'],
[/@numberDate(?![ \t]*\S+)/, 'number.date'],
// Key:Value pair
[/(".*?"|'.*?'|.*?)([ \t]*)(:)( |$)/, ['type', 'white', 'operators', 'white']],
{ include: '@flowScalars' },
// String nodes
[
/.+$/,
{
cases: {
'@keywords': 'keyword',
'@default': 'string',
},
},
],
],
// Flow Collection: Flow Mapping
object: [
{ include: '@whitespace' },
{ include: '@comment' },
// Flow Mapping termination
[/\}/, '@brackets', '@pop'],
// Flow Mapping delimiter
[/,/, 'delimiter.comma'],
// Flow Mapping Key:Value delimiter
[/:(?= )/, 'operators'],
// Flow Mapping Key:Value key
[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/, 'type'],
// Start Flow Style
{ include: '@flowCollections' },
{ include: '@flowScalars' },
// Scalar Data types
{ include: '@tagHandle' },
{ include: '@anchor' },
{ include: '@flowNumber' },
// Other value (keyword or string)
[
/[^\},]+/,
{
cases: {
'@keywords': 'keyword',
'@default': 'string',
},
},
],
],
// Flow Collection: Flow Sequence
array: [
{ include: '@whitespace' },
{ include: '@comment' },
// Flow Sequence termination
[/\]/, '@brackets', '@pop'],
// Flow Sequence delimiter
[/,/, 'delimiter.comma'],
// Start Flow Style
{ include: '@flowCollections' },
{ include: '@flowScalars' },
// Scalar Data types
{ include: '@tagHandle' },
{ include: '@anchor' },
{ include: '@flowNumber' },
// Other value (keyword or string)
[
/[^\],]+/,
{
cases: {
'@keywords': 'keyword',
'@default': 'string',
},
},
],
],
// First line of a Block Style
multiString: [[/^( +).+$/, 'string', '@multiStringContinued.$1']],
// Further lines of a Block Style
// Workaround for indentation detection
multiStringContinued: [
[
/^( *).+$/,
{
cases: {
'$1==$S2': 'string',
'@default': { token: '@rematch', next: '@popall' },
},
},
],
],
whitespace: [[/[ \t\r\n]+/, 'white']],
// Only line comments
comment: [[/#.*$/, 'comment']],
// Start Flow Collections
flowCollections: [
[/\[/, '@brackets', '@array'],
[/\{/, '@brackets', '@object'],
],
// Start Flow Scalars (quoted strings)
flowScalars: [
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/'([^'\\]|\\.)*$/, 'string.invalid'],
[/'[^']*'/, 'string'],
[/"/, 'string', '@doubleQuotedString'],
],
doubleQuotedString: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop'],
],
// Start Block Scalar
blockStyle: [[/[>|][0-9]*[+-]?$/, 'operators', '@multiString']],
// Numbers in Flow Collections (terminate with ,]})
flowNumber: [
[/@numberInteger(?=[ \t]*[,\]\}])/, 'number'],
[/@numberFloat(?=[ \t]*[,\]\}])/, 'number.float'],
[/@numberOctal(?=[ \t]*[,\]\}])/, 'number.octal'],
[/@numberHex(?=[ \t]*[,\]\}])/, 'number.hex'],
[/@numberInfinity(?=[ \t]*[,\]\}])/, 'number.infinity'],
[/@numberNaN(?=[ \t]*[,\]\}])/, 'number.nan'],
[/@numberDate(?=[ \t]*[,\]\}])/, 'number.date'],
],
tagHandle: [[/\![^ ]*/, 'tag']],
anchor: [[/[&*][^ ]+/, 'namespace']],
},
} as monaco.languages.IMonarchLanguage;

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/code-editor",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,49 @@
/*
* 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 class PlaceholderWidget implements monaco.editor.IContentWidget {
constructor(
private readonly placeholderText: string,
private readonly editor: monaco.editor.ICodeEditor
) {
editor.addContentWidget(this);
}
private domNode: undefined | HTMLElement;
public getId(): string {
return 'KBN_CODE_EDITOR_PLACEHOLDER_WIDGET_ID';
}
public getDomNode(): HTMLElement {
if (!this.domNode) {
const domNode = document.createElement('div');
domNode.innerText = this.placeholderText;
domNode.className = 'kibanaCodeEditor__placeholderContainer';
this.editor.applyFontInfo(domNode);
this.domNode = domNode;
}
return this.domNode;
}
public getPosition(): monaco.editor.IContentWidgetPosition | null {
return {
position: {
column: 1,
lineNumber: 1,
},
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
};
}
public dispose(): void {
this.editor.removeContentWidget(this);
}
}

View file

@ -0,0 +1,15 @@
/*
* 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 { registerLanguage } from '@kbn/monaco';
import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson } from './languages';
registerLanguage(CssLang);
registerLanguage(HandlebarsLang);
registerLanguage(MarkdownLang);
registerLanguage(YamlLang);
registerLanguage(HJson);

View file

@ -0,0 +1,28 @@
/*
* 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';
/**
* When using custom fonts with monaco need to call `monaco.editor.remeasureFonts()` when custom fonts finished loading
* Otherwise initial measurements on fallback font are used which causes visual glitches in the editor
*/
export const remeasureFonts = () => {
if ('fonts' in window.document && 'ready' in window.document.fonts) {
window.document.fonts.ready
.then(() => {
monaco.editor.remeasureFonts();
})
.catch((e) => {
// eslint-disable-next-line no-console
console.warn('Failed to remeasureFonts in <CodeEditor/>');
// eslint-disable-next-line no-console
console.warn(e);
});
}
};

View file

@ -0,0 +1,29 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop",
"@kbn/ambient-ui-types",
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/monaco",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/code-editor-mocks",
"@kbn/ui-theme",
"@kbn/test-jest-helpers",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/code-editor-mocks
Empty package generated by @kbn/generate

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 { CodeEditorStorybookMock } from './storybook';
export type { Params as CodeEditorStorybookParams } from './storybook';

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/code_editor/mocks'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/code-editor-mocks",
"owner": "@elastic/shared-ux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/code-editor-mocks",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,99 @@
/*
* 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 { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock';
import type { Props as CodeEditorProps } from '@kbn/code-editor-types';
type PropArguments = Pick<
CodeEditorProps,
| 'languageId'
| 'value'
| 'aria-label'
| 'allowFullScreen'
| 'useDarkTheme'
| 'transparentBackground'
| 'placeholder'
>;
export type Params = Record<keyof PropArguments, any>;
/**
* Storybook mock for the `CodeEditor` component
*/
export class CodeEditorStorybookMock extends AbstractStorybookMock<
CodeEditorProps,
{},
PropArguments,
{}
> {
propArguments = {
languageId: {
control: {
type: 'radio',
},
options: ['json', 'loglang', 'plaintext'],
defaultValue: 'json',
},
value: {
control: {
type: 'text',
},
defaultValue: '',
},
'aria-label': {
control: {
type: 'text',
},
defaultValue: 'code editor',
},
allowFullScreen: {
control: {
type: 'boolean',
},
defaultValue: false,
},
useDarkTheme: {
control: {
type: 'boolean',
},
defaultValue: false,
},
transparentBackground: {
control: {
type: 'boolean',
},
defaultValue: false,
},
placeholder: {
control: {
type: 'text',
},
defaultValue: 'myplaceholder',
},
};
serviceArguments = {};
dependencies = [];
getProps(params?: Params): CodeEditorProps {
return {
languageId: this.getArgumentValue('languageId', params),
value: this.getArgumentValue('value', params),
'aria-label': this.getArgumentValue('aria-label', params),
allowFullScreen: this.getArgumentValue('allowFullScreen', params),
useDarkTheme: this.getArgumentValue('useDarkTheme', params),
transparentBackground: this.getArgumentValue('transparentBackground', params),
placeholder: this.getArgumentValue('placeholder', params),
};
}
getServices() {
return { ...this.getProps() };
}
}

View file

@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/shared-ux-storybook-mock",
"@kbn/code-editor-types",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/code-editor-types
Empty package generated by @kbn/generate

View file

@ -0,0 +1,101 @@
/*
* 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 interface Props {
/** Width of editor. Defaults to 100%. */
width?: string | number;
/** Height of editor. Defaults to 100%. */
height?: string | number;
/** ID of the editor language */
languageId: enum;
/** Value of the editor */
value: string;
/** Function invoked when text in editor is changed */
onChange?: (value: string, event: monaco.editor.IModelContentChangedEvent) => void;
/**
* Options for the Monaco Code Editor
* Documentation of options can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
*/
options?: monaco.editor.IStandaloneEditorConstructionOptions;
/**
* Suggestion provider for autocompletion
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html
*/
suggestionProvider?: monaco.languages.CompletionItemProvider;
/**
* Signature provider for function parameter info
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.signaturehelpprovider.html
*/
signatureProvider?: monaco.languages.SignatureHelpProvider;
/**
* Hover provider for hover documentation
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.hoverprovider.html
*/
hoverProvider?: monaco.languages.HoverProvider;
/**
* Language config provider for bracket
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
*/
languageConfiguration?: monaco.languages.LanguageConfiguration;
/**
* Function called before the editor is mounted in the view
*/
editorWillMount?: () => void;
/**
* Function called before the editor is mounted in the view
* and completely replaces the setup behavior called by the component
*/
overrideEditorWillMount?: () => void;
/**
* Function called after the editor is mounted in the view
*/
editorDidMount?: (editor: monaco.editor.IStandaloneCodeEditor) => void;
/**
* Should the editor use the dark theme
*/
useDarkTheme?: boolean;
/**
* Should the editor use a transparent background
*/
transparentBackground?: boolean;
/**
* Should the editor be rendered using the fullWidth EUI attribute
*/
fullWidth?: boolean;
/**
* Place holder text for the code editor
*/
placeholder?: string;
/**
* Accessible name for the editor. (Defaults to "Code editor")
*/
'aria-label'?: string;
isCopyable?: boolean;
allowFullScreen?: boolean;
}

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/code_editor/types'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/code-editor-types",
"owner": "@elastic/shared-ux"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/code-editor-types",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.d.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/monaco",
]
}

View file

@ -128,6 +128,12 @@
"@kbn/cloud-plugin/*": ["x-pack/plugins/cloud/*"],
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
"@kbn/code-editor": ["packages/shared-ux/code_editor/impl"],
"@kbn/code-editor/*": ["packages/shared-ux/code_editor/impl/*"],
"@kbn/code-editor-mocks": ["packages/shared-ux/code_editor/mocks"],
"@kbn/code-editor-mocks/*": ["packages/shared-ux/code_editor/mocks/*"],
"@kbn/code-editor-types": ["packages/shared-ux/code_editor/types"],
"@kbn/code-editor-types/*": ["packages/shared-ux/code_editor/types/*"],
"@kbn/coloring": ["packages/kbn-coloring"],
"@kbn/coloring/*": ["packages/kbn-coloring/*"],
"@kbn/config": ["packages/kbn-config"],

View file

@ -2857,6 +2857,18 @@
version "0.0.0"
uid ""
"@kbn/code-editor-mocks@link:packages/shared-ux/code_editor/mocks":
version "0.0.0"
uid ""
"@kbn/code-editor-types@link:packages/shared-ux/code_editor/types":
version "0.0.0"
uid ""
"@kbn/code-editor@link:packages/shared-ux/code_editor/impl":
version "0.0.0"
uid ""
"@kbn/coloring@link:packages/kbn-coloring":
version "0.0.0"
uid ""